单例模式是设计模式中相对是来说最容易理解的设计模式,但其实大多数人只是知道单例模式的一些简单场景的使用和实现。比如饿汉单例、懒汉单例等。实际上,在很多场景下,这些实现方式有很多问题。那么在不同场景下我们应该如何选择单例的实现呢。这次我们会由浅入深的学习,彻底的来认识一下单例模式。
一、单例模式的定义
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
二、UML类图
它的类图很简单,就是client使用这个单例类就行了。
需要注意的是单例类的构造方法是私有的。
三、使用场景
它的使用场景也很明确。
确保某个类只要一个对象的场景,从而避免产生多个对象消耗过多的资源。
从定义、UML类图和使用场景都透露出两个字--简单。
可它的背后真的只是那么简单吗?哈哈,表明简单的东西背后往往是不简单的。
嗯哼,不信,那就先放一张图来震下场子
单例模式.png这就是我这篇文章要说的核心内容了。
四、不同场景下的实现
1.说实现之前,我们先要看看实现单例模式的关键点
(1)构造方法不对外开放。这个很容易理解,如果开放了那么和话,外面随便new一下就实现了一个对象,还单例个毛线呢。
(2)确保单例类的对象只有一个,尤其是在多线程环境下。这个后面会详细介绍。
(3)确保单例类的对象在反序列化时不会重新构建对象。
那么下面我一一介绍各种实现方法和它们的优缺点
2.最简单的实现方法--饿汉单例
饿汉,顾名思义,非常饥饿的汉子,所以一定义这个对象就给实例化了。
public class HungerSingleton {
private static HungerSingleton singleton=new HungerSingleton();
private HungerSingleton() {}
public static HungerSingleton getSingleton() {
return singleton;
}
}
实现非常简单,通过一个静态方法返回一个静态对象。
但是缺点也很明显,如果我不需要这个对象的话,它也会创建对象,浪费空间资源和影响时间性能。
3.给饥饿的汉子一张床--懒汉单例
给了饥饿的汉子一张床后会忘记饥饿,但它会变懒。也就不急着实例化对象了,就是所谓的懒加载。
public class LazySingleton {
private LazySingleton(){};
private static LazySingleton singleton=null;
public static synchronized LazySingleton getSingleton() {
if(singleton==null){
singleton=new LazySingleton();
}
return singleton;
}
}
仔细一看我们发现这个getSingleton方法加了个synchronize关键字。这个就是我们之前说的”保证在多线程下对象唯一“的方法。
咋一看很完美,但还是有一个很大的问题就是:
我们知道synchronize同步是比较消耗性能的,而且每次调用getSingleton方法都会进行同步,这回造成不必要的性能开销。
4.懒汉单例的优化--DCL单例
针对上面的懒汉单例的缺点,出现了DCL(Double Check Lock)模式。其实解决的方式很简单了,只需要在外层加一个判空就OK了。
这样就整个程序就有了两次判空,第一次的目的是为了避免多余的同步操作。第二次是为了对象实例化的唯一性。
public class DCLSingleton {
private DCLSingleton(){};
private static DCLSingleton singleton=null;
private static DCLSingleton getSingleton(){
if (singleton==null) {
synchronized (DCLSingleton.class) {
if(singleton==null)
singleton=new DCLSingleton();
}
}
return singleton;
}
}
这里我们考虑到了多线程的情景又考虑到性能的问题。看起来就完美了。其实不深入研究下去,这的确是完美的单例写法。那么还有什么不完美的地方呢?
就是这句话
singleton=new DCLSingleton();
这个语句他不是一个原子操作。
也就是说
它表明上只有一步其实有三步操作
(1)给DCLSingleton的实例分配内存
(2)调用Singleton()的构造方法,初始化成员字段
(3)将singleton对象指向分配的内存空间
由于java编译器允许处理器乱序执行,以及jdk1.5之前的java内存模型(JMM)中寄存器、cache到主内存回写顺序的规定。所以不能保证上面三步执行的顺序。很有可能会按照(1)-(3)-(2)的顺序
这就会有一个问题,高并发的情况下,当A线程执行到(3),被切换到了B线程,那么B线程就会直接取走了singleton对象,但是该对象因为没有执行第二步,就没有真正的实例化。所以就会被B拿去使用导致出错,发生DCL失效。
那肿么办呀?这个我们没法控制啊。
幸运的是,在jdk1.5的时候,出现了一个volatile关键字。当我们的jdk版本大于等于1.5的时候,只要在定义的时候加上这个volatile就能够解决了问题了。
private static volatile DCLSingleton singleton=null;
虽然有点小瑕疵但是DCL的优点还是多的,大多数情景下可以保证单例对象的唯一性。但如果你的并发场景复杂或者版本低于jdk1.5的时候,可能需要一个更好的实现方式了。
5.《java并发编程实践》推荐--静态内部类单例
java并发编程实践书中并不赞同DCL实现方式,因为这种方式还是会出现DCL失效。它建议使用静态内部类实现
public class InternalClassSingleton {
private InternalClassSingleton(){};
public static InternalClassSingleton getSingleton(){
return SingletonHolder.singleton;
}
private static class SingletonHolder{
private static final InternalClassSingleton singleton=new InternalClassSingleton();
}
}
这种方式也是很简洁的,它只会在第一次调用getSingleton()的时候才会让singleton被初始化。因此第一次加载getSingleton的时候会导致虚拟机加载SingletonHolder类,这种方式不仅能够保证单例对象的唯一性,同时也延迟了单例的实例化。推荐使用的单例模式实现方式。
6.最简单的单例实现--枚举单例
我们都知道枚举和普通的java类是一样的,不仅有字段还有自己的方法。重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它只有一个单例。
public class EnumSingleton {
private EnumSingleton(){};
enum Enum{
INSTANCE;
public static Enum getInstance(){
return INSTANCE;
}
}
}
实在是简单,看着还舒服,重点不需要考虑多线程的场景问题。美滋滋~~
除了上面说到,其实还有其他的方式实现,比如利用容器等等。但上面的方式足以应对各种的场景了,我们只需要根据不同的场景和自己的喜好来选择合适的就好了。
网友评论