单例模式的使用动机
某些系统中的某些类的对象只需要一个, 或者只能是一个, 如果多于一个甚至会出现错误,这时候我们就要用到单例模式。日志对象, 打印池对象, 序列ID生成器等应用。单例模式可以让系统在减少内存空间的情况下仍然能正常工作。
生成单例的方式:1.懒汉式(线程不安全);2.懒汉式(线程安全);3.饿汉式;4.静态内部类;
5.双重检验锁;6.枚举
这里只介绍下:双重检验锁。貌似现在都这个比较多。
其他单例模式可以参考:
https://www.zybuluo.com/pastqing/note/164787
http://cantellow.iteye.com/blog/838473
双重校验锁
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么要判断两次singleton
对象是否为空, 原因还是为了避免多个对象实例的生成。假如线程A拿到锁进入同步块生成了一个对象并返回。此时线程B拿到锁进入同步块,如果此时不判断singleton
的话,就会多次创建对象, 造成单例失败。 volatile关键字,确保进程之间获取到的singleton是最新的。
如果不在声明 Singleton singleton 加volatile,那么对于singleton = new Singleton不是一个原子操作。此时的双重锁的方式是有问题的:
在执行new时JVM会发生以下几个动作:
1.给singleton分配内存
2.调用构造函数, 初始化成员变量
3.将singleton对象指向所分配的内存空间 当第三步完成后,对象就不再是null了。但是JVM会进行指令的优化重排序, 将原本的1-2-3的顺序可能变为1-3-2。这样就有可能在线程A实例化一个对象之后,线程B在3执行完后就抢占了,此时实例已经非Null此时线程二就会返回该实例, 这样就会出错了。
在JDK1.5之后,可以使用volatile解决以上问题。
关键字volatile可以说是java虚拟机提供的最轻量级的同步机制,它包含了两方面的语义:
一、当一个变量定义为volatile时,它保证了此变量对所有线程的可见性。这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即知道的。而普通变量无法做到这一点。普通变量的值在线程间传递需要通过主内存来完成。
例如,线程A修改了一个普通变量的值,然后向主内存中回写,线程B等线程A回写完成后再次主内存进行读取时,新值才会可见。
二、禁止指令重排优化。普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能保证取到正确的结果,而不能保证变量赋值的操作顺序与代码一致。
上面使用volatile来解决双重锁实现带来的问题, 正是运用了volatile的第二个语义。