前言:在看JAVA程序员面试总结,高手整理加强版的时候看到设计模式,突然想起来设计模式都忘了差不多了,其中单例模式还是挺有内涵的,正好总结一下。安利一下《HEAD First设计模式》,我觉得写的特别好~
参考博客:
1.单例模式的几种用法比较
demo:点击打开链接
单例模式顾名思义就是"一个实例"的设计模式,需要使用设计模式的思路达成调用类的时候都是同一个实例。那么自然的构造函数肯定不能随意调用了,那肯定是私有的。但是肯定要有接口让外部调用啊,所以得提供一个接口,这个接口呢,返回的每一个实例都应该是一样的。
单例模式刚接触的时候以为只有懒汉式和饥汉式,随着了解的逐步深入,了解了更多优雅更全面的写法,需要兼顾线程安全。
package com.example.demo_11_singleton.demo;
/**
* Created by jiatai on 18-3-15.
*/
public class JHanSingleton {
//static final单例对象,类加载的时候就初始化
private static final JHanSingleton instance = new JHanSingleton();
//私有构造方法,使得外界不能直接new
private JHanSingleton() {
}
//公有静态方法,对外提供获取单例接口
public static JHanSingleton getInstance() {
return instance;
}
}
饥汉式解决了多线程并发的问题,因为在加载这个类的时候,就实例化了instance。当getInstatnce方法被调用时,得到的永远是类加载时初始化的对象(反序列化的情况除外)。但这也带来了另一个问题,如果有大量的类都采用了饥汉式,那么在类加载的阶段,会初始化很多暂时还没有用到的对象,这样肯定会浪费内存,影响性能。
package com.example.demo_11_singleton.demo;
/**
* Created by jiatai on 18-3-15.
*/
public class LanHanSingleton {
private static LanHanSingleton instance;
private LanHanSingleton() {
}
/**
* 增加synchronized关键字,该方法为同步方法,保证多线程单例对象唯一
*/
public static synchronized LanHanSingleton getInstance() {
if (instance == null) {
instance = new LanHanSingleton();
}
return instance;
}
}
可以注意到getInstance方法前加了synchronized 关键字,让getInstance方法成为同步方法,这样就保证了当getInstance第一次被调用,即instance被实例化时,别的调用不会进入到该方法,保证了多线程中单例对象的唯一性。
优点:单例对象在第一次调用才被实例化,有效节省内存,并保证了线程安全。
缺点:同步是针对方法的,以后每次调用getInstance时(就算intance已经被实例化了),也会进行同步,造成了不必要的同步开销。
package com.example.demo_11_singleton.demo;
/**
* Created by jiatai on 18-3-15.
*/
public class DCLSingleton {
//增加volatile关键字,确保实例化instance时,编译成汇编指令的执行顺序
private volatile static DCLSingleton instance;
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
//当第一次调用getInstance方法时,即instance为空时,同步操作,保证多线程实例唯一
//当以后调用getInstance方法时,即instance不为空时,不进入同步代码块,减少了不必要的同步开销
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
如果instance不加volatile的话DCL失效:
在JDK1.5之前,可能会有DCL实现的问题,上述代码中的如下代码,在Java里虽然是一句代码,但它并不是一个真正的原子操作。
instance = new DCLSingleton();
它编译成最终的汇编指令,会有下面3个阶段:
在jdk1.5之前,上述的2、3步骤不能保证顺序,也就是说有可能是1-2-3,也有可能是1-3-2。如果是1-3-2,当线程A执行完步骤3(instance已经不为null),但是还没执行完2,线程B又调用了getInstance方法,这时候线程B所得到的就是线程A没有执行步骤2(没有执行完构造函数)的instance,线程B在使用这样的instance时,就有可能会出错。这就是DCL失效。
其他博客这里也叫做指令重排。
知识点:什么是指令重排?
简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。
在jdk1.5之后,可以使用volatile关键字,保证汇编指令的执行顺序,虽然会影响性能,但是和程序的正确性比起来,可以忽悠不计。这里用一句话概况就是volatile会禁止指令重排。(volatile另一个作用是保证可见性)
优点:第一次执行getInstance时instance才被实例化,节省内存;多线程情况下,基本安全;并且在instance实例化以后,再次调用getInstance时,不会有同步消耗。
缺点:jdk1.5以前,有可能DCL失效;Java内存模型影响导致失效;jdk1.5以后,使用volatile关键字,虽然能解决DCL失效问题,但是会影响部分性能。
我看博客中有的地方提到了sychronized会禁止指令重排,然后搜了下这个不是很靠谱,不值得信服。
1.下面这个博客举得例子不对。如下面评论所说:静态方法加上synchronized关键字后 相当于锁类 同一个时间只有一个线程能够访问这个类 one和two不能同时被访问,这个例子看不出来是否禁止了指令重排
点击打开链接
2.下面的链接是对这个问题的一个讨论,其中一位如下所说,看起来挺有道理的。
点击打开链接
3.另外在这篇博客中点击打开链接有提到加volatile的这种方式EventBus也这么用的。
大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()就是用这种方法来实现的。
package com.example.demo_11_singleton.demo;
/**
* Created by jiatai on 18-3-15.
*/
public class StaticClassSingleton {
//私有的构造方法,防止new
private StaticClassSingleton() {
}
public static StaticClassSingleton getInstance() {
return StaticClassSingletonHolder.instance;
}
/**
* 静态内部类
*/
private static class StaticClassSingletonHolder {
//第一次加载内部类的时候,实例化单例对象
private static final StaticClassSingleton instance = new StaticClassSingleton();
}
}
第一次加载StaticClassSingleton类时,并不会实例化instance,只有第一次调用getInstance方法时,Java虚拟机才会去加载StaticClassSingletonHolder类,继而实例化instance,这样延时实例化instance,节省了内存,并且也是线程安全的。这是推荐使用的一种单例模式。
这个我不是很明白,为什么静态内部类里面的实例变量不会被加载,我写个demo验证一下正确性,至于为什么估计涉及到JVM 类的加载=-=
demo:
package com.example.demo_11_singleton.demo;
/**
* Created by jiatai on 18-3-15.
*/
public class StaticClassSingleton {
//私有的构造方法,防止new
private StaticClassSingleton() {
}
public static StaticClassSingleton getInstance() {
System.out.println("StaticClassSingleton getInstance");
return StaticClassSingletonHolder.instance;
}
public static void test(){
System.out.println("StaticClassSingleton test");
}
/**
* 静态内部类
*/
private static class StaticClassSingletonHolder {
//第一次加载内部类的时候,实例化单例对象
private static final StaticClassSingleton instance = new StaticClassSingleton();
static {
System.out.println("StaticClassSingletonHolder load static field");
}
}
}
package com.example.demo_11_singleton;
import com.example.demo_11_singleton.demo.StaticClassSingleton;
public class SingletonTest {
public static void main(String[] args){
StaticClassSingleton.test();
StaticClassSingleton.getInstance();
}
}
执行结果:
StaticClassSingleton test
StaticClassSingleton getInstance
StaticClassSingletonHolder load static field
Process finished with exit code 0
恩,从结果来看是对的。我这个demo的依据是静态代码块和静态成员变量都只会加载一次,所以如果是紧接着getInstance后面加载那上面说的没错。相应的如果我多调用一个getInstance,那static代码块也不会再次加载了。
StaticClassSingleton test
StaticClassSingleton getInstance
StaticClassSingletonHolder load static field
StaticClassSingleton getInstance
package com.example.demo_11_singleton.demo;
/**
* Created by jiatai on 18-3-15.
*/
public enum EnumSingleton {
//枚举实例的创建是线程安全的,任何情况下都是单例(包括反序列化)
INSTANCE;
public void doSomething(){
}
}
枚举不仅有字段还能有自己的方法,并且枚举实例创建是线程安全的,就算反序列化时,也不会创建新的实例。
----------
除了枚举模式以外,其他实现方式,在反序列化时都会创建新的对象。
为了防止对象在反序列化时创建新的对象,需要加上如下方法:
private Object readResole() throws ObjectStreamException {
return instance;
}
这是一个钩子函数,在反序列化创建对象时会调用它,我们直接返回instance就是说,不要按照默认那样去创建新的对象,而是直接将instance返回。
---------不是很了解,枚举内部实现保证了它内部成员的唯一性
看了这么多写法,其实懒汉式和饥汉式已经被后面更优雅的实现方式比下去了,看起来用double check、静态内部类或者枚举会更好一点。