Java双重检查加锁单例模式的详解

 更新时间:2019年03月25日 11:07:28   作者:Scub  
今天小编就为大家分享一篇关于Java双重检查加锁单例模式的详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

什么是DCL

DCL(Double-checked locking)被设计成支持延迟加载,当一个对象直到真正需要时才实例化:

class SomeClass {
 private Resource resource = null;
 public Resource getResource() {
 if (resource == null)
  resource = new Resource();
 return resource;
 }
}

为什么需要推迟初始化?可能创建对象是一个昂贵的操作,有时在已知的运行中可能根本就不会去调用它,这种情况下能避免创建一个不需要的对象。延迟初始化能让程序启动更快。但是在多线程环境下,可能会被初始化两次,所以需要把getResource()方法声明为synchronized。不幸的是,synchronized方法比非synchronized方法慢100倍左右,延迟初始化的初衷是为了提高效率,但是加上synchronized后,提高了启动速度,却大幅下降了执行时速度,这看起来并不是一桩好买卖。DCL看起来是最好的:

class SomeClass {
 private Resource resource = null;
 public Resource getResource() {
 if (resource == null) {
  synchronized(this) {
  if (resource == null) 
   resource = new Resource();
  }
 }
 return resource;
 }
}

延迟了初始化,又避免了竞态条件。看起来是一个聪明的优化--但它却不能保证正常工作。为提高计算机系统性能,编译器、处理器、缓存会对程序指令和数据进行重排序,而对象初始化操作并不是一个原子操作(可能会被重排序);因此可能存在这种情况:一个线程正在构造对象过程中,另一个线程检查时看见了resource的引用为非null。对象被非安全发布(逸出)。

根据Java内存模型,synchronized的语义不仅仅是在同一个信号上的互斥(mutex),也包含线程和主存之间数据交互的同步,它确保在多处理器、多线程下对内存能有可预见的一致性视图。获取或释放锁会触发一次内存屏障(memory barrier)--强迫线程本地内存和主存同步。当一个线程退出一个synchronized block时,触发一次写屏障(write barrier )--在释放锁前必须把所有在这个同步块里修改过的变量值刷新到主存;同样,进入一个synchronized block时,触发一次读屏障(read barrier)--让本地内存失效,必须从主存中重新获取在这个同步块中将要引用的所有变量的值。正确使用同步能保证一个线程能以可预见的方式看到另一个线程的结果,线程对同步块的操作就像是原子的。“正确使用”的含义是:必须是在同一个锁上同步。

DCL是怎么失效的

了解了JMM后,再来看看DCL是怎么失效的。DCL依赖于一个非同步的resource字段,看起来无害,实则不然。假如线程A进入了synchronized block,正在执行resource = new Resource();此时线程B进入 getResource()。考虑到对象初始化在内存上的影响:为new对象分配内存;调用构造方法,初始化对象的成员变量;把新创建好对象的引用赋值给SomeClass的resource字段。然而线程B没有进入synchronized block,却可能以不同于线程A执行的顺序看到上述内存操作。B看到的可能是如下顺序(指令重排序):分配内存,把对象引用赋值给SomeClass的resource字段,调用构造器。当内存已经分配好,A线程把SomeClass的resource字段设值完成后,线程B进入检查发现resource不是null,跳过synchronized block返回一个未构造完成的对象!显而易见,结果不是预期的也不是想要的。

下面代码是一个试图修复DCL的加强版,遗憾的是它仍然不能保证正常工作。

// (Still) Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
 private Helper helper = null;
 public Helper getHelper() {
 if (helper == null) {
  Helper h;
  synchronized (this) {
  h = helper;
  if (h == null)
   synchronized (this) {
   h = new Helper();
   } // release inner synchronization lock
  helper = h;
  }
 }
 return helper;
 }
 // other functions and members...
}

这段代码把Helper对象的构造放在一个内部的同步块,又用了一个局部变量h来先接收初始化完成后的引用,直觉就是当这个内部的同步块退出时,应该会触发一次内存屏障,能阻止对初始化Helper对象和给Foo的helper字段赋值的两个操作重排序。不幸的是,直觉是完全错误的,对同步规则理解得不对。对于monitorexit规则(即,释放同步),监视器被释放之前必须执行monitorexit之前的动作。然而,没有规定说monitorexit后的操作,不能在监视器释放前执行。编译器把赋值语句helper = h;移动到内部同步块之前是完全合理合法的,在这种情况下,我们又重新回到了以前。许多处理器提供执行这种单向内存屏障指令。改变语义要求释放锁是一个完整的内存屏障会有性能损失。然而即使初始化时有一个完整的内存屏障,也不能保证,在一些系统上,保证线程能看到helper的属性字段的值为非null也需要同样的内存屏障。因为处理器有自己的本地缓存拷贝,某些处理器在执行缓存一致性指令前,即使其他的处理器使用内存屏障强制把最新值写入主存,该处理器读到的还是本地缓存拷贝的旧值。

关于重排序(reorder)有3种来源:编译器、处理器、内存系统。承诺“write-once, run-anywhere concurrent applications in Java” 的Java是接受处理器和内存系统为优化而重排序的,所以DCL单例模式没有完美的解决方案,在多线程下编程要异常小心。下面讨论多线程环境下单例模式的实现。

多线程环境下单例的实现

第一种,同步方法(synchronized)

优点:所有情况下都能正常工作,延迟初始化;

缺点:同步严重损耗了性能,因为只有第一次实例化时才需要同步。

不推荐,绝大部分情况是没必要延迟初始化的,不如采用急切实例化(eager initialization)

// Correct multithreaded version
class Foo {
 private Helper helper = null;
 public synchronized Helper getHelper() {
 if (helper == null)
  helper = new Helper();
 return helper;
 }
 // other functions and members...
}

第二种,使用IODH(Initialization On Demand Holder)

利用static块做初始化,如下定义一个私有的静态类去做初始化,或者直接在静态块代码中去做初始化,能保证对象被正确构造前对所有线程不可见。

class Foo {
 private static class HelperSingleton {
 public static Helper singleton = new Helper();
 }
 public Helper getHelper() {
 return HelperSingleton.singleton;
 }
 // other functions and members...
}

第三种,急切实例化(eager initialization)

class Foo {
 public static final Helper singleton = new Helper();
 // other functions and members...
}
class Foo {
 private static final Helper singleton = new Helper();
 public Helper getHelper() {
 return singleton;
 }
 // other functions and members...
}

第四种,枚举单例

public enum SingletonClass {
 INSTANCE;
 // other functions...
}

上面4种方式在所有情况下都能保证正常工作

第五种,只对32位基本类型的值有效

缺陷:对64位的long和double及引用对象无效,因为64位的基本类型的赋值操作不是原子的。利用场景有限。

// Lazy initialization 32-bit primitives
// Thread-safe if computeHashCode is idempotent
class Foo {
 private int cachedHashCode = 0;
 public int hashCode() {
 int h = cachedHashCode;
 if (h == 0) {
  h = computeHashCode();
  cachedHashCode = h;
 }
 return h;
 }
 // other functions and members...
}

第六种,DCL加上volatile语义

旧内存模型(在JDK1.5发行之前)下失效,只能在JDK1.5后使用。

另外不推荐次方法,多核处理器下线程每次写volatile字段都会把工作内存及时刷新到主存,每次读都会从主存获取数据,因为要和主存交换数据,volatile的频繁读写会占用数据总线资源。

// Works with acquire/release semantics for volatile
// Broken under current semantics for volatile
class Foo {
 private volatile Helper helper = null;
 public Helper getHelper() {
 Helper h = helper;
 if (helper == null) {// First check (no locking)
  synchronized (this) {
  h = helper;
  if (helper == null)
   helper = h = new Helper();
  }
 }
 return helper;
 }
}

第七种,不可变对象的单例

对于不可变对象(immutable object)本身是线程安全的,不需要同步,单例实现起来最简单。比如Helper是一个不可变类型,只用用final修饰singleton字段就行:

class Foo {
 private final Helper singleton = new Helper();
 public Helper getHelper() {
 return singleton;
 }
 // other functions and members...
}

缺陷:旧内存模型(在JDK1.5发行之前)下失效,只能在JDK1.5后使用,因为新内存模型对final和volatile语义进行了加强。还有一个问题就是明确什么是不可变对象,如果对不可变对象含义不确定,请不要使用,另外当前是不可变对象不能保证将来此类一直是不可变对象(代码总是在不断修改),慎用!

需要使用单例时,慎用延迟初始化,优先考虑急切实例化(简单优雅,不易出错)

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。如果你想了解更多相关内容请查看下面相关链接

相关文章

  • Java多线程并发的指令重排序问题及volatile写屏障原理详解

    Java多线程并发的指令重排序问题及volatile写屏障原理详解

    这篇文章主要介绍了Java多线程并发的指令重排序问题及volatile写屏障原理详解,指令重排序是编译器或处理器为了提高性能而对指令执行顺序进行重新排列的优化技术,需要的朋友可以参考下
    2024-01-01
  • 深入理解happens-before和as-if-serial语义

    深入理解happens-before和as-if-serial语义

    本文大部分整理自《Java并发编程的艺术》,温故而知新,加深对基础的理解程度。下面可以和小编来一起学习下
    2019-05-05
  • Java.toCharArray()和charAt()的效率对比分析

    Java.toCharArray()和charAt()的效率对比分析

    这篇文章主要介绍了Java.toCharArray()和charAt()的效率对比分析,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • SpringBoot中@Autowired生效方式详解

    SpringBoot中@Autowired生效方式详解

    @Autowired注解可以用在类属性,构造函数,setter方法和函数参数上,该注解可以准确地控制bean在何处如何自动装配的过程。在默认情况下,该注解是类型驱动的注入
    2022-06-06
  • Spring注解与P/C命名空间超详细解析

    Spring注解与P/C命名空间超详细解析

    Spring注解方式减少了配置文件内容,更加便于管理,并且使用注解可以大大提高了开发效率!注解本身是没有功能的,和xml一样,注解和xml都是一种元数据,元数据即解释数据的数据,也就是所谓的配置
    2022-11-11
  • 基于Java实现Actor模型

    基于Java实现Actor模型

    Actor模型是一种常见的并发模型,与最常见的并发模型—共享内存(同步锁)不同,它将程序分为许多独立的计算单元—Actor,文中有详细的代码示例,感兴趣的同学可以参考阅读
    2023-05-05
  • SpringBoot使用Spring-Data-Jpa实现CRUD操作

    SpringBoot使用Spring-Data-Jpa实现CRUD操作

    这篇文章主要为大家详细介绍了SpringBoot使用Spring-Data-Jpa实现CRUD操作,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08
  • javaMybatis映射属性,高级映射详解

    javaMybatis映射属性,高级映射详解

    下面小编就为大家带来一篇javaMybatis映射属性,高级映射详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • Java实现自动压缩文件并加密的方法示例

    Java实现自动压缩文件并加密的方法示例

    这篇文章主要介绍了Java实现自动压缩文件并加密的方法,涉及java针对文件进行zip压缩并加密的相关操作技巧,需要的朋友可以参考下
    2018-01-01
  • 教你开发脚手架集成Spring Boot Actuator监控的详细过程

    教你开发脚手架集成Spring Boot Actuator监控的详细过程

    这篇文章主要介绍了开发脚手架集成Spring Boot Actuator监控的详细过程,集成包括引入依赖配置文件及访问验证的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05

最新评论