线程安全的延迟初始化方式
2017-02-11 19:39
246 查看
参考书籍:
java并发编程的艺术
在java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。因此需要引入延迟初始化技术。
在单线程中延迟初始化对象的代码,在多线程中是非线程安全的,因为此时会出现竞态条件(Race Condition),典型的先判断后执行。
有可能出现的问题:
两个线程同时进入代码2,可能两个线程返回了两个不同的instance实例引用,如果两个线程后续还会对instance实例对象中的域进行修改操作,可能会出现数据不一致等错误。
假设线程A执行代码1的同时,B线程执行代码2,此时,线程A可能会看到instance引用的对象还没有完成初始化,也就是返回了一个不完整的实例对象引用。
如果把instance引用声明为volatile类型的呢?这样确实可以保证instance的可见性了,但是整个方法仍不是原子的,除非能确保只有一个线程对instance进行修改操作。
为了保证整体方法的原子性,可以在方法上进行同步
由于对整个方法做了同步处理,将会产生额外的性能开销。如果getInstance()方法被多个线程频繁的调用,将会导致程序性能的下降。反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。
在早期的jvm中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销。因此人们通过双重检查锁定(Double-Checked Locking DCL )来降低同步的开销。
因为只有在第一次初始化instance时需要同步,因此第一次代码1的检查不加锁;之所以需要在同步代码块中第二次检查,是因为两个线程有可能同时通过代码1往下执行,当线程A获得锁执行完成同步代码块的代码后(此时instance已经初始化完成了),释放锁,然后线程B获得锁进入同步代码块,如果不再次对instance进行null值检查,那么将错误地初始化两次instance!因此第二次检查是必要的(由于synchronized能保证代码块中变量的可见性,因此线程B能够检测到线程A对instance的修改)。
看起来很完美了,但是还是存在缺陷:代码3在jvm中执行使,可以分解为如下的3行代码:
其中代码b和c可能会被重排序。因此,可能会出现以下情境:
线程A进入代码2第二个检查,开始初始化对象,由于指令重排序,abc执行顺序变为了acb,当执行完代码a和c时,instance已经被赋值,不为空,此时线程B调用getInstance()方法,进入代码1第一个检查,发现不为空,直接返回了instance的引用。因此线程B将会访问到一个还未初始化的对象(或者未初始化完成)。也就是说对象未被正确地发布。
因此为了禁止代码b和c指令重排序,需要将instance引用声明为volatile类型。注意,声明volatile是为了禁止指令重排序,不是为了保证可见性。
一个典型的双重锁检查方案的实际应用:sfj4j初始化LoggerFactory
另外还有一种类初始化的解决方案:
volatile的双重检查锁定方案:比较繁琐,但是可以对实例字段实现延迟初始化。
基于类初始化的方案:简单,但是只能对静态字段进行延迟初始化。
java并发编程的艺术
在java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。因此需要引入延迟初始化技术。
在单线程中延迟初始化对象的代码,在多线程中是非线程安全的,因为此时会出现竞态条件(Race Condition),典型的先判断后执行。
有可能出现的问题:
两个线程同时进入代码2,可能两个线程返回了两个不同的instance实例引用,如果两个线程后续还会对instance实例对象中的域进行修改操作,可能会出现数据不一致等错误。
假设线程A执行代码1的同时,B线程执行代码2,此时,线程A可能会看到instance引用的对象还没有完成初始化,也就是返回了一个不完整的实例对象引用。
public class UnsafeLazyInitialization{ private static Instance instance; public static Instance getInstance(){ if(instance == null)//1 instance = new Instance();//2 return instance; } }
如果把instance引用声明为volatile类型的呢?这样确实可以保证instance的可见性了,但是整个方法仍不是原子的,除非能确保只有一个线程对instance进行修改操作。
为了保证整体方法的原子性,可以在方法上进行同步
public class SafeLazyInitialization{ private static Instance instance; public synchronized static Instance getInstance(){ if(instance == null)//1 instance = new Instance();//2 return instance; } }
由于对整个方法做了同步处理,将会产生额外的性能开销。如果getInstance()方法被多个线程频繁的调用,将会导致程序性能的下降。反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。
在早期的jvm中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销。因此人们通过双重检查锁定(Double-Checked Locking DCL )来降低同步的开销。
因为只有在第一次初始化instance时需要同步,因此第一次代码1的检查不加锁;之所以需要在同步代码块中第二次检查,是因为两个线程有可能同时通过代码1往下执行,当线程A获得锁执行完成同步代码块的代码后(此时instance已经初始化完成了),释放锁,然后线程B获得锁进入同步代码块,如果不再次对instance进行null值检查,那么将错误地初始化两次instance!因此第二次检查是必要的(由于synchronized能保证代码块中变量的可见性,因此线程B能够检测到线程A对instance的修改)。
public class DoubleCheckedInitialization{ private static Instance instance; public static Instance getInstance(){ if(instance == null){//1 synchronized(DoubleCheckedInitialization.class{ if(instance == null){//2 instance = new Instance();//3 } } } return instance; } }
看起来很完美了,但是还是存在缺陷:代码3在jvm中执行使,可以分解为如下的3行代码:
memory = allocate();//a 分配对象的内存空间 ctorInstance(memory);//b 初始化对象 instance = memory;//c 设置instance指向刚分配的内存地址
其中代码b和c可能会被重排序。因此,可能会出现以下情境:
线程A进入代码2第二个检查,开始初始化对象,由于指令重排序,abc执行顺序变为了acb,当执行完代码a和c时,instance已经被赋值,不为空,此时线程B调用getInstance()方法,进入代码1第一个检查,发现不为空,直接返回了instance的引用。因此线程B将会访问到一个还未初始化的对象(或者未初始化完成)。也就是说对象未被正确地发布。
因此为了禁止代码b和c指令重排序,需要将instance引用声明为volatile类型。注意,声明volatile是为了禁止指令重排序,不是为了保证可见性。
public class DoubleCheckedInitialization{ private volatile static Instance instance; public synchronized static Instance getInstance(){ if(instance == null){//1 synchronized(DoubleCheckedInitialization.class{ if(instance == null){//2 instance = new Instance();//3 } } } return instance; } }
一个典型的双重锁检查方案的实际应用:sfj4j初始化LoggerFactory
/** * Return the {@link ILoggerFactory} instance in use. * <p/> * <p/> * ILoggerFactory instance is bound with this class at compile time. * * @return the ILoggerFactory instance in use */ public static ILoggerFactory getILoggerFactory() { if (INITIALIZATION_STATE == UNINITIALIZED) { synchronized (LoggerFactory.class) { if (INITIALIZATION_STATE == UNINITIALIZED) { INITIALIZATION_STATE = ONGOING_INITIALIZATION; performInitialization(); } } } ...
另外还有一种类初始化的解决方案:
public class InstanceFactory{ private static class InstanceHolder{ public static Instance instance = new Instance(); } public static Instance getInstance(){ return InstanceHolder.instance; } }
volatile的双重检查锁定方案:比较繁琐,但是可以对实例字段实现延迟初始化。
基于类初始化的方案:简单,但是只能对静态字段进行延迟初始化。
相关文章推荐
- 线程安全的延迟初始化
- Java 中的多线程-两种创建方式,定时器的应用,线程的安全问题可以用银行转账来说明
- 单例在多线程下的问题: "懒汉"初始化的线程安全
- 细说匿名内部类方式创建线程、初始化HashMap
- Java线程安全-同步方式
- 创建线程的两种方式区别,安全问题
- [转]Java线程安全四种方式五个等级
- 线程、线程匿名内部类、解决线程不安全的方式
- 线程通信,线程安全及解决方式
- 线程静态字段(ThreadStatic) 延迟初始化
- 懒汉式的安全优化方式,两种方式。线程同时运行的时候,不会创建两个对象
- 用多步的密匙交换方式来进行非常安全的会话初始化和用户名密码登陆
- C++ 单例模式,考虑线程安全和性能的几种方式
- SimpleDateFormat 的线程安全问题与解决方式
- 并发控制(3) 使用double check方式的单例,来确保并发下的线程安全的单例模式
- servlet的实例变量是线程不安全的,而其JSP也默认是以多线程方式执行(原创)
- java中singleton模式与延迟初始化实现方式总结
- 为您提供Swift初始化的一些安全方式
- 高级并发编程之 线程范围内安全共享数据(使用Map方式)
- 线程安全的单例模式 -- 使用pthread_once一次初始化