CSDN热搜
在讲双重校验锁之前先来看一下其他模式
饿汉模式和懒汉模式的区别在于: 饿汉模式是在类加载时将其实例化的,在饿汉模式下,在Class Loader完成后该类的实例便已经存在于JVM中了,即,在getInstance方法第一次被调用前该实例已经存在了,new对象的操作不在getInstance方法内。 而懒汉模式在类中只是定义了变量但是并未实例化,实例化的过程是在获取单例对象的方法中实现的,即,在getInstance方法第一次被调用后该实例才会被创建,new对象的操作在getInstance方法内。 此外注意: 饿汉模式的实例在类加载的时候已经存在于JVM中了,因此是线程安全的; 懒汉模式通过第一次调用getInstance才实例化,该方法不是线程安全的(后面讲怎么优化)
饿汉模式和静态内部类实现单例模式的优点是写法简单,缺点是不适合复杂对象的创建。 对于涉及复杂对象创建的单例模式,比较优雅的实现方式是懒汉模式, 但是懒汉模式是非线程安全的, 下面就讲一下懒汉模式的升级版——DCL双重构校验锁模式(双重构校验锁是线程安全的)。
仔细看,这里是粗暴地对整个 getInstance() 方法加锁,这样做代价很大,因为,只有当第一次调用 getInstance() 时才需要同步创建对象,创建之后再次调用 getInstance() 时就只是简单的返回成员变量,而这里是无需同步的,所以没必要对整个方法加锁。 由于同步一个方法会降低上百倍甚至更高的性能, 每次调用获取和释放锁的开销似乎是可以避免的:一旦初始化完成,获取和释放锁就显得很不必要。所以可以只对方法的部分代码加锁!!public class Lock2Singleton { private static Lock2Singleton INSTANCE; private Lock2Singleton() {} public static Lock2Singleton getSingleton() { // 因为INSTANCE是静态变量,所以给Lock2Singleton的Claa对象上锁 synchronized(Lock2Singleton.class) { // 加 synchronized if (INSTANCE == null) { INSTANCE = new Lock2Singleton(); } } return INSTANCE; }}复制代码 优化后的代码选择了对 if (INSTANCE == null) 和 INSTANCE = new Lock2Singleton()加锁 这样,每个线程进到这个方法中之后先加锁,这样就保证了 if (INSTANCE == null) 和 INSTANCE = new Lock2Singleton() 这两行代码被同一个线程执行时不会有另外一个线程进来,由此保证了创建的对象是唯一的。 对象的唯一性保证了,也就是解决了问题①,同时也解决了问题②。 为什么说也解决了问题②呢?synchronized不是不能禁止指令重排序吗? 其实当我们对INSTANCE == null和INSTANCE = new Lock2Singleton();加锁时,也就表示只有一个线程能进来,尽管发生了指令重排序,也只是在持有锁的期间发生了指令重排序,当该线程创建完对象释放锁时,new出来的已经是一个完整的对象。 如此,我们仿佛完美地解决了问题 ① 和 ② ,然而你以为这就结束了吗?NO!这段代码从功能层面来讲确实是已经结束了,但是性能方面呢?是不是还有可以优化的地方? 答案是:有!! 值得优化的地方就在于 synchronized 代码块这里。每个线程进来,不管三七二十一,都要先进入同步代码块再说,如果说现在 INSTANCE 已经不为null了,那么,此时当一个线程进来,先获得锁,然后才会执行 if 判断。我们知道加锁是非常影响效率的,所以,如果 INSTANCE 已经不为null,是不是就可以先判断,再进入 synchronized 代码块。如下public class Lock2Singleton { private static Lock2Singleton INSTANCE; private Lock2Singleton() {} public static Lock2Singleton getSingleton() { if (INSTANCE == null) { // 双重校验:第一次校验 synchronized(Lock2Singleton.class) { // 加 synchronized if (INSTANCE == null) { // 双重校验:第二次校验 INSTANCE = new Lock2Singleton(); } } } return INSTANCE; }}复制代码 在 synchronized 代码块之外再加一个 if 判断,这样,当 INSTANCE 已经存在时,线程先判断不为null,然后直接返回,避免了进入 synchronized 同步代码块。 那么可能又有人问,好了,我明白了在 synchronized 代码块外加一个 if 判断,是不是就意味着里面的那个 if 判断可以去掉? 当然不可以!! 如果把里面的 if 判断去掉,就相当于只对 INSTANCE = new Lock2Singleton() 这一行代码加了个锁,只对一行代码加锁,那你岂不是加了个寂寞(加锁的目的就是防止在第二个if判断和new操作之间有别的线程进来!!),结果还是会引起问题①。 所以,两次校验,一次都不能少!! 但是,问题又来了,由于我们在外层又加了一层if (INSTANCE == null)的判断,导致原本被我们解决的问题② (即指令重排序问题)又出现了! 比如:线程A拿到锁后刚走到INSTANCE = new Lock2Singleton(),但是还没执行完,因为new Lock2Singleton()不是原子操作,且发生了指令重排序,那么此时INSTANCE就是一个不完整的对象,恰巧此时,线程B来到第一个if (INSTANCE == null)判断,由于INSTANCE不为null,结果获取到一个不完整的对象。 那么怎么解决呢? 答案是加 volatile 关键字,volatile可以禁止指令重排序public class Lock2Singleton { private volatile static Lock2Singleton INSTANCE; // 加 volatile private Lock2Singleton() {} public static Lock2Singleton getSingleton() { if (INSTANCE == null) { // 双重校验:第一次校验 synchronized(Lock2Singleton.class) { // 加 synchronized if (INSTANCE == null) { // 双重校验:第二次校验 INSTANCE = new Lock2Singleton(); } } } return INSTANCE; }}复制代码
使用道具 举报
本版积分规则 回帖并转播 回帖后跳转到最后一页
程序园优秀签约作者
0
粉丝关注
23
主题发布