一、思考题
- 单例的几种设计模式?
- 单例存在哪些问题
- 单例与静态类的区别
- 有什么替代的解决方案?
二、单例的几种设计模式
单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
一.懒汉模式(即时加载)
package designpattern.singleton;
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {
}
public static Mgr01 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
二.饿汉模式(延迟加载)
package designpattern.singleton;
/**
* Description:比较好的写法,双重检测。
*/
public class Mgr06 {
//volatile避免指令重排
private static volatile Mgr06 INSTANCE;
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//减少同步代码块,试图提升效率,结果是失败的。
synchronized (Mgr06.class) {
//双重判断
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Mgr06.getInstance().hashCode());
}
}).start();
}
}
}
三.静态内部类
package designpattern.singleton;
/**
* Description: 静态内部类,比较完善的写法,看自己喜欢。
*/
public class Mgr07 {
private Mgr07() {
}
private static class Mgr07Holder {
private final static Mgr07 INSTANCE = new Mgr07();
}
private static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Mgr07.getInstance().hashCode());
}
}).start();
}
}
}
四.枚举
package designpattern.singleton;
/**
* Description:大神写的,不仅可以解决线程同步,还可以防止反序列化。
*/
public enum Mgr08 {
INSTANCE;
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Mgr08.INSTANCE.hashCode());
}
}).start();
}
}
}
三、单例存在的问题
大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题。接下来,我们就具体看看到底有哪些问题。
一.单例对 OOP 特性的支持不友好
二.单例会隐藏类之间的依赖关系(不方便阅读源码)
通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。
三.单例对代码的扩展性不友好(慎重使用单例,只有那些真的不需要也不会扩展的类使用单例比较好)
单例类只能创建一个实例对象,需要扩展就会很复杂。
四.单例对代码的可测试性不友好
如果单例类持有成员变量(比如 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。
五.单例不支持有参数的构造函数
如果带参数就会这样,第一次传递进去之后,第二遍传递进去的并不生效,会有误导。
Singleton singleton1 = Singleton.getInstance(10, 50);
Singleton singleton2 = Singleton.getInstance(20, 30);
比较好的解决方案是,参数放到另外一个全局变量中。
public class Config {
public static final int PARAM_A = 123;
public static final int PARAM_B = 245;
}
public class Singleton {
private static Singleton instance = null;
private final int paramA;
private final int paramB;
private Singleton() {
this.paramA = Config.PARAM_A;
this.paramB = Config.PARAM_B;
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
四、替代单例的方案
业务上有表示全局唯一类的需求,如果不用单例,怎么才能保证这个类的对象全局唯一。
为了保证全局唯一,除了使用单例,还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。不过,静态方法这种实现思路,它比单例更加不灵活,比如无法支持延迟加载。
五、单例的唯一性
线程唯一、进程唯一、集群唯一
“进程唯一”指的是进程内唯一、进程间不唯一。“线程唯一”指的是线程内唯一、线程间不唯一。集群相当于多个进程构成的一个集合,“集群唯一”就相当于是进程内唯一、进程间也唯一。也就是说,不同的进程间共享同一个对象,不能创建同一个类的多个对象。
一.单例的唯一性(进程唯一,一个程序有且仅有的一个对象)
单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”
定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是什么呢?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象?答案是后者,也就是说,单例模式创建的对象是进程唯一的。