单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
在 2.6.2 中,我们读取了配置文件中的内容。假设我们把读入的配置文件封装成一个类:
AppConfig.java:
package singleton;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 读取配置文件
*/
public class AppConfig {
/**
* 用来存放配置文件中属性A的值
*/
private String parameterA;
/**
* 用来存放配置文件中属性B的值
*/
private String parameterB;
/**
* 取出属性A的值
* @return 属性A的值
*/
public String getParameterA(){
return parameterA;
}
/**
* 取出属性B的值
* @return 属性B的值
*/
public String getParameterB(){
return parameterB;
}
/**
* 构造方法
*/
public AppConfig() {
//调用读取配置文件的方法
readConfig();
}
/**
* 读取配置文件
*/
public void readConfig() {
Properties p = new Properties();
InputStream in = null;
try {
in = ApiFactory.class.getResourceAsStream("AppConfig.properties");
p.load(in);
this.parameterA = p.getProperty("parameterA");
this.parameterB = p.getProperty("parameterB");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
AppConfig.properties:
parameterA=A
parameterB=B
客户端(main 方法):
package singleton;
public class Client {
public static void main(String[] args) {
AppConfig config = new AppConfig();
String parameterA = config.getParameterA();
String parameterB = config.getParameterB();
System.out.println("parameterA="+parameterA+" parameterB="+parameterB);
}
}
输出结果:
parameterA=A parameterB=B
以上就是我们之前实现读取配置文件这个功能时的做法。那么,这样的做法有什么问题呢?
可以看出,客户端使用这个类时,是通过 new AppConfig()
获得一个 AppConfig 的实例来得到一个操作配置文件内容的对象。如果在系统运行中,有很多地方都需要使用配置文件的内容,那么就会在很多地方都创建 AppConfig 对象的实例。换句话说,在系统运行期间,系统中会存在很多个 AppConfig 的实例对象,这有什么问题吗?
答案当然是有问题的。试想一下,每一个 AppConfig 实例对象里面都封装着配置文件的内容。系统中有多个AppConfig 实例对象,也就是说系统中会同时存在多份配置文件的内容,这样会严重浪费内存资源。如果配置文件内容较少,问题可能不会严重;但如果配置文件内容本来就多的话,对于系统资源的浪费问题就会明显暴露出来。事实上,对于 AppConfig 这种类,在运行期间,只需要一个实例对象就足够了。
因此,把上面的描述进一步抽象一下,问题就描述出来了:在一个系统运行期间,某个类只需要一个类实例就可以了,这该怎样实现呢?
仔细分析 3.2 中产生的问题,不难发现,这是由于构造方法公开化导致的。因为这样,外部类可以调用构造方法来创建任意个实例。换句话说,只要构造方法还是公开的,就没有办法控制外部类创建这个类的实例的个数。
要想控制一个类只被创建一个实例,那么首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类实例的创建工作。然后由这个类来提供外部可以访问这个类实例的方法,这就是单例模式的实现方式。
在 Java 中,单例模式的实现又分为两种,一种称为懒汉式,一种称为饿汉式。其实就是在具体创建对象实例的处理上,有不同的实现方式。下面分别来看看这两种实现方式的代码示例:
LazySingleton.java:
package singleton;
/**
* 懒汉式单例模式
*/
public class LazySingleton {
/**
* 定义一个变量来存储创建好的类实例
*/
private static LazySingleton uniqueInstance = null;
/**
* 私有化构造方法
*/
private LazySingleton(){}
/**
* 为外部类提供创建实例的方法
* @return 存储的实例
*/
public static synchronized LazySingleton getInstance(){
//判断存储实例的变量是否有值
if (uniqueInstance == null) {
//如果没有,就创建一个,否则就不创建
uniqueInstance = new LazySingleton();
}
return uniqueInstance;
}
/**
* 用来测试输出的属性
*/
private String text;
/**
* 赋值方法
* @param text 留待输出的内容
*/
public void setText(String text) {
this.text = text;
}
/**
* 用来测试输出的方法
*/
public void test(){
System.out.println("懒汉式:" + text);
}
}
客户端(main 方法):
package singleton;
public class Client {
public static void main(String[] args) {
LazySingleton lazy1 = LazySingleton.getInstance();
LazySingleton lazy2 = LazySingleton.getInstance();
LazySingleton lazy3 = LazySingleton.getInstance();
lazy1.setText("这是懒汉式方式的单例模式");
lazy1.test();
lazy2.setText("单利模式下无论调用多少次getInstance()方法");
lazy2.test();
lazy3.setText("都只会创建一个实例");
lazy3.test();
System.out.println("三个变量是否为同一个实例:" + (lazy1 == lazy2 && lazy2 ==lazy3 && lazy1 == lazy3));
lazy1.test();
}
}
输出结果:
懒汉式:这是懒汉式方式的单例模式
懒汉式:单利模式下无论调用多少次getInstance()方法
懒汉式:都只会创建一个实例
三个变量是否为同一个实例:true
懒汉式:都只会创建一个实例
在本例中,我们调用了三次 LazySingleton 的 getInstance
方法,保存到了三个变量中。显然,这三个变量指向的是同一个实例。因此,当我们对它们三个两两进行 == 的判断时,返回的结果为 true;也因此,当我们调用 lazy3 的方法 setText
改变其属性 text
之后,lazy1 和 lazy2 的属性也就跟着变了。
由此可以得出,使用懒汉式的具体方法步骤如下:
package singleton;
/**
* 饿汉式单例模式
*/
public class HungrySingleton {
/**
* 定义一个变量来存储创建好的类实例
*/
private static HungrySingleton uniqueInstance = new HungrySingleton();
/**
* 私有化构造方法
*/
private HungrySingleton(){}
/**
* 为外部类提供创建实例的方法
* @return 存储的实例
*/
public static HungrySingleton getInstance(){
//直接返回存储实例的变量即可
return uniqueInstance;
}
/**
* 用来测试输出的属性
*/
private String text;
/**
* 赋值方法
* @param text 留待输出的内容
*/
public void setText(String text) {
this.text = text;
}
/**
* 用来测试输出的方法
*/
public void test(){
System.out.println("懒汉式:" + text);
}
}
客户端(main 方法):
package singleton;
public class Client {
public static void main(String[] args) {
HungrySingleton hungry1 = HungrySingleton.getInstance();
HungrySingleton hungry2 = HungrySingleton.getInstance();
HungrySingleton hungry3 = HungrySingleton.getInstance();
hungry1.setText("这是饿汉式的单例模式");
hungry1.test();
hungry2.setText("与懒汉式单例模式的差别不大");
hungry2.test();
hungry3.setText("唯一的区别在于存储实例的变量何时初始化");
hungry3.test();
System.out.println("三个变量是否为同一个实例:" + (hungry1 == hungry2 && hungry2 ==hungry3 && hungry1 == hungry3));
hungry1.test();
}
}
输出结果:
饿汉式:这是饿汉式的单例模式
饿汉式:与懒汉式单例模式的差别不大
饿汉式:唯一的区别在于存储实例的变量何时初始化
三个变量是否为同一个实例:true
饿汉式:唯一的区别在于存储实例的变量何时初始化
饿汉式与懒汉式的特性没有任何不同,它们之间唯一的区别在于创建实例的时机。懒汉式是在调用 getInstance
方法(推荐总是这么命名)的时候才去创建,而饿汉式则是在类加载的时候就一并创建了。
由此可以得出,使用饿汉式的具体方法步骤如下:
既然单例模式的饿汉式与懒汉式并没有特性上的区别,因此我们就只用一种方式去改写就好了,这里选择懒汉式:
AppConfig.java:
package singleton;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 读取配置文件
*/
public class AppConfig {
/**
* 用来存放创建的实例
*/
private static AppConfig appConfig = null;
/**
* 用来存放配置文件中属性A的值
*/
private String parameterA;
/**
* 用来存放配置文件中属性B的值
*/
private String parameterB;
/**
* 取出属性A的值
* @return 属性A的值
*/
public String getParameterA(){
return parameterA;
}
/**
* 取出属性B的值
* @return 属性B的值
*/
public String getParameterB(){
return parameterB;
}
/**
* 私有化构造方法
*/
private AppConfig() {
//调用读取配置文件的方法
readConfig();
}
/**
* 懒汉式获取实例的方法
* @return 存储的实例
*/
public static synchronized AppConfig getInstance() {
if (appConfig==null) {
appConfig = new AppConfig();
}
return appConfig;
}
/**
* 读取配置文件
*/
public void readConfig() {
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfig.class.getResourceAsStream("AppConfig.properties");
p.load(in);
this.parameterA = p.getProperty("parameterA");
this.parameterB = p.getProperty("parameterB");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端(main 方法):
package singleton;
public class Client {
public static void main(String[] args) {
AppConfig config = AppConfig.getInstance();
String parameterA = config.getParameterA();
String parameterB = config.getParameterB();
System.out.println("parameterA="+parameterA+" parameterB="+parameterB);
}
}
输出结果:
parameterA=A parameterB=B
懒汉式的优势在于实现了延迟加载,而饿汉式的优势在于线程安全。懒汉式虽然通过添加 synchronized
的方式也能实现线程安全,但是这样会大幅度地降低访问速度。那么,有没有一种方法,既能实现延迟加载,又能在不降低访问速度的情况下实现线程安全呢?
事实上,饿汉式已经做到了在不降低访问速度的情况下实现线程安全。只是,它没有实现延迟加载,因而会在类装载的时候就初始化对象,不论是否需要,这回造成空间的浪费。
那么,如果有一种方法能够让类装载的时候不去初始化对象,问题不就解决了吗?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,就不会创建对象实例,从而同时实现延迟加载和线程安全。
InnerSingleton.java:
package singleton;
/**
* 类级内部类实现单例模式
*/
public class InnerSingleton {
/**
* 类级内部类
*/
private static class SingletonHolder {
/**
* 静态初始化器
*/
private static InnerSingleton instance = new InnerSingleton();
}
/**
* 私有化构造方法
*/
private InnerSingleton(){}
/**
* 获取实例
*/
public static InnerSingleton getInstance() {
return SingletonHolder.instance;
}
/**
* 测试用的属性
*/
private String text;
/**
* 设置属性
* @param text 带输出的文本
*/
public void setText(String text) {
this.text = text;
}
/**
* 测试用的方法
*/
public void test(){
System.out.println("类级内部类:"+text);
}
}
客户端(main 方法):
package singleton;
public class Client {
public static void main(String[] args) {
InnerSingleton inner1 = InnerSingleton.getInstance();
InnerSingleton inner2 = InnerSingleton.getInstance();
inner1.setText("可以同时实现延迟加载和线程安全");
inner1.test();
inner2.setText("比懒汉式和饿汉式更好的单例实现方式");
inner2.test();
inner1.test();
}
}
输出结果:
类级内部类:可以同时实现延迟加载和线程安全
类级内部类:比懒汉式和饿汉式更好的单例实现方式
类级内部类:比懒汉式和饿汉式更好的单例实现方式
当 getInstance
方法第一次被调用的时候,它第一次读取 SingletonHolder.instance,导致 SingletonHolder 类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建 Singleton 的实例。由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
这个模式的优势在于,getInstance
方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化的机制,并由 JVM 从根本上提供保障,绝对防止包括反射方式在内的多次实例化,是更简洁、高效、安全的实现单例的方式。示例代码如下:
package singleton;
/**
* 枚举
*/
public enum SingletonEnum {
/**
* 定义一个枚举的元素,它就代表 SingletonEnum 的一个实例
*/
uniqueInstance;
/**
* 测试属性
*/
private String text;
/**
* 设置属性
*/
public void setText(String text) {
this.text = text;
}
/**
* 测试方法
*/
public void test(){
System.out.println("通过枚举实现单例:"+text);
}
}
客户端(main 方法):
package singleton;
public class Client {
public static void main(String[] args) {
SingletonEnum singletonEnum1 = SingletonEnum.uniqueInstance;
SingletonEnum singletonEnum2 = SingletonEnum.uniqueInstance;
singletonEnum1.setText("更简洁、高效、安全");
singletonEnum1.test();
singletonEnum2.setText("是最佳实践");
singletonEnum2.test();
singletonEnum1.test();
}
}
输出结果:
通过枚举实现单例:更简洁、高效、安全
通过枚举实现单例:是最佳实践
通过枚举实现单例:是最佳实践
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。