Java多线程并发中的双重检查锁定与延迟初始化
来源:互联网 发布:sql语句添加列 编辑:程序博客网 时间:2024/05/19 05:38
双重检查锁定与延迟初始化
在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。双重检查锁定是常见的延迟初始化技术。
下面我们看一个非线程安全的延迟初始化对象的例子:
public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) // 1:A线程执行 instance = new Singleton(); // 2:B线程执行 return instance; }}
假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化
下面我们对上面的代码改造一下,让他变成线程安全
public class Singleton { private static Singleton instance; public static synchronized Singleton getInstance() { if (instance == null) // 1:A线程执行 instance = new Singleton(); // 2:B线程执行 return instance; }}
由于对getInstance()方法做了同步处理,synchronized将导致性能开销。如果getInstance()方法被多个线程频繁的调用,将会导致程序执行性能的下降(一般情况在我们项目中提供的单例总是被频繁的调用)。反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。
下面我们进一步通过双重检查锁定来降低同步的开销,代码如下:
public class Singleton { private static Singleton instance; public static Singleton getInstance() { // 第一次检查 if(instance == null){ // 加锁 synchronized(Singleton.class){ if (instance == null) //分配内存空间、初始化对象、instance指向分配的内存地址 instance = new Singleton(); } } return instance; }}
上面的代码真的能保证单例吗?让我们来分析下
如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美。多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。双重检查锁定看起来似乎很完美,但这是一个错误的优化!在代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
让我们来继续分析
前面的双重检查锁定示例代码(instance=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。
memory = allocate(); // 1:分配对象的内存空间ctorInstance(memory); // 2:初始化对象instance = memory; // 3:设置instance指向刚分配的内存地址
面3行伪代码中的2和3之间,可能会被重排序2和3之间重排序之后的执行时序如下。
memory = allocate(); // 1:分配对象的内存空间instance = memory; // 3:设置instance指向刚分配的内存地址;注意,此时对象还没有被初始化!ctorInstance(memory); // 2:初始化对象
下面让我们看一下多线程并发的执行情况
由于单线程内要遵守intra-thread semantics,从而能保证A线程的执行结果不会被改变。但是,当线程A和B按图3-38的时序执行时,B线程将看到一个还没有被初始化的对象。
基于上面的问题现象我们有2种解决方案
1、不允许2和3重排序
2、允许2和3重排序,但是不允许对其他线程“看到”这个重排序
方案一:基于volatile解决方案
只需要前面基于双重检查锁定来实现的延迟方案,把instance改成volatile(JDK1.5以上支持)
public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { // 第一次检查 if(instance == null){ // 加锁 synchronized(Singleton.class){ if (instance == null) //分配内存空间、初始化对象、instance指向分配的内存地址 instance = new Singleton(); } } return instance; }}
当声明为volatile以后2和3重排将被禁止,代码将按照如下顺序执行执行
方案二:基于类初始化的解决方案
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案
public class Singleton { private static class InstanceHolder { public static Singleton instance = new Singleton(); } public static Singleton getInstance() { // 这里将导致InstanceHolder类被初始化 return InstanceHolder.instance; }}
假设两个线程并发执行getInstance()方法,下面是执行的示意图
- Java多线程并发中的双重检查锁定与延迟初始化
- 并发编程的艺术-双重检查锁定与延迟初始化
- java多线程学习(十一) 双重检查锁定和延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 双重检查锁定与延迟初始化
- 设计模式之备忘录模式
- Codeforces 598D Igor In the Museum【预处理Bfs】
- ReactJS读书笔记五:DOM操作
- Qualcomm平台的SPI驱动框架分析
- syslog的搭建
- Java多线程并发中的双重检查锁定与延迟初始化
- firstChild和firstElementChild的区别
- 动态内存分配
- 再谈“我是怎么招聘程序员的”
- 快速开方
- ReactJS学习笔记六:感想
- SparkStreaming的checkpoint(可靠性、一致性、高可用性)
- redis的长短来链接
- outlook配置阿里企业邮箱