什么是单例模式?
单例模式:类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
说简单点了就是不用new Class()的方式创建对象,直接用Class.getInstance()的方式去获取这个类唯一的对象。(此处Class为声明类名)
public class Singleton { private volatile static Singleton instance; /*类的属性必须为volatile+static volatile共享锁的性质可以保证类属性对所有线程可见 static使Singleton类可以不通过对象去访问类属性*/ private Singleton (){} //构造方法必须为private public static Singleton getInstance() { //必须有一个获取单例的方法比如getInstance() if (instance== null) { //第一次校验 synchronized (Singleton.class) {//当线程进入此步骤即对整个类信息加锁 if (instance== null) { //第二次校验 instance = new Singleton(); //两次交验都为空的话即创建实例 } } } return instance; } }
单例模式的七个要求
1.成员变量volatile
2.成员变量static
3.构造函数私有
4.getInstance方法静态
5.getInstance方法第一层有校验
6.getInstance方法第二层有校验
7.在第一层校验后给类信息加锁synchronized
为什么要双重校验?
首先我们假设有两个线程A和B并且只有第一层校验,代码变成下面的这样:
public class Singleton { private volatile static Singleton instance; //类的属性必须为volatile static private Singleton (){} //构造方法必须为private public static Singleton getInstance() { //必须有一个获取单例的方法比如getInstance() if (instance== null) { //第一次校验 synchronized (Singleton.class) {//当线程进入此步骤即对整个类信息加锁 //第二层if删去了 instance = new Singleton(); //两次交验都为空的话即创建实例 } } return instance; } }
这段代码有可能运行以下情况:
第一步:线程A获取CPU执行完第一次校验,但是此时到没有对整个类信息加锁这一步,线程A失去CPU资源,将A线程的当前运行状态保存。
第二步:线程B获取CPU执行完第一次校验,并且成功对类信息加锁,此时线程B持有锁,然后成功执行instance=new Singleton();创建Singleton实例,此时我们称这个引用为b1。线程B释放锁。
第三步:线程A再次获取CPU,经过上下文切换,默认执行完第一次校验,然后对类信息加锁,线程A执行instance=new Singleton();,创建Singleton实例,此时我们称这个引用为a1。
从这一步开始就已经打破了单例模式的要求
====================华丽的分割
接下来我们把第二重校验恢复:
第一步:线程A获取CPU执行完第一次校验,但是此时到没有对整个类信息加锁这一步,线程A失去CPU资源,将A线程的当前运行状态保存。
第二步:线程B获取CPU执行完第一次校验,并且成功对类信息加锁,此时线程B持有锁,进行第一重校验成功,然后成功执行instance=new Singleton();创建Singleton实例,此时我们称这个引用为b1。线程B释放锁。
第三步:线程A再次获取CPU,经过上下文切换,默认执行完第一次校验,然后对类信息加锁,第二重校验失败,线程A释放锁。
此处我们假设有第三个线程C参与竞争:
第三步:线程C获取CPU,执行第一次校验失败,线程C释放锁。
结论可得,在多线程环境下,单例模式进行双重校验锁就是为了提高线程安全性,防止多个线程创建额外的类实例。