码迷,mamicode.com
首页 > 编程语言 > 详细

单例设计模式和Java内存模型

时间:2019-08-08 21:42:55      阅读:119      评论:0      收藏:0      [点我收藏+]

标签:完成   reload   bsp   memory   res   display   获取   线程   过程   

使用双检索的单例设计模式延迟加载存在的问题

这篇文章介绍了使用双检索延迟加载的单例模式存在的问题,以下的代码由于指令重排序可能会无法正常工作。

正常的执行顺序是

  1. 执行构造函数
  2. 构造函数执行完毕
  3. 将新构造的对象赋值到引用

但由于指令的乱序执行,代码的执行顺序可能变为

  1. 执行构造函数
  2. 将对象赋值到引用
  3. 构造函数执行完毕

由此,线程可能获取到一个没有初始化完毕的对象。

技术图片
 1 class Foo { 
 2   private Helper helper = null;
 3   public Helper getHelper() {
 4     if (helper == null) 
 5       synchronized(this) {
 6         if (helper == null) 
 7           helper = new Helper();
 8       }    
 9     return helper;
10     }
11   // other functions and members...
12   }
View Code

然后给出了几种修复方案,这其实是一个安全发布的问题,能够解决问题的方案不外乎以下情况(链接的文章中没有出现第3种):

  1. 在静态初始化函数中初始化一个对象的引用。
  2. 将对象引用保存到volatile类型的域中或者AtomicReference对象中。
  3. 将对象引用保存到某个正确构造对象的final类型域中。
  4. 将对象的引用保存到一个由锁保护的域中。

对于第1种修复方案,因为静态初始化函数在类加载的初始化阶段执行,这部分的代码由JVM保证同步,因此是行之有效的。

技术图片
1 class HelperSingleton {
2   static Helper singleton = new Helper();
3   }
View Code

我们先跳过第2种和第3种修复方案。 对于第4种修复方案,因为synchronized的代码段或者函数是同步的,具有原子性和可见性,因此也是能够工作的。

技术图片
class Foo { 
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }
View Code

 

我先给出第2种修复方案的代码,但不急着去分析,我们需要先了解一些其他的知识。因为仅仅根据之前的知识是无法解决问题的。

技术图片
 1   class Foo {
 2         private volatile Helper helper = null;
 3         public Helper getHelper() {
 4             if (helper == null) {
 5                 synchronized(this) {
 6                     if (helper == null)
 7                         helper = new Helper();
 8                 }
 9             }
10             return helper;
11         }
12     }
View Code

之前的知识:

happens-before:

  • An unlock on a monitor happens-before every subsequent lock on that monitor.

  • A write to a volatile field (§8.3.1.4happens-before every subsequent read of that field.

  • A call to start() on a thread happens-before any actions in the started thread.

  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

  • The default initialization of any object happens-before any other actions (other than default-writes) of a program.

对于一个volatile字段的写happens-before对一个volatile字段的读,也就是线程A的volatile写能够被线程B的volatile读所感知到。

但仅凭这点,对于乱序执行导致线程获取到一个没有初始化完毕的对象没有一点帮助。

我们还需要内存屏障相关的知识。

Java Memory Model Cookbook

内存屏障相关的知识可以该链接的文章中获取,也有一些人已经翻译过其中的内容发布到自己的博客上。

在这里,我们需要引用该文章中的两张表格和StoreStore内存屏障的知识。

Required barriers 2nd operation
1st operation Normal Load Normal Store Volatile Load
MonitorEnter
Volatile Store
MonitorExit
Normal Load


LoadStore
Normal Store


StoreStore
Volatile Load
MonitorEnter
LoadLoad LoadStore LoadLoad LoadStore
Volatile Store
MonitorExit


StoreLoad StoreStore
Java
Instructions
class X {
  int a, b;
  volatile int v, u;
  void f() {
    int i, j;
   
    i = a;
    j = b;
    i = v;
   
    j = u;
   
    a = i;
    b = j;
   
    v = i;
   
    u = j;
   
    i = u;
   
   
    j = b;
    a = i;
  }
}

     






load a
load b
load v
   LoadLoad
load u
   LoadStore
store a
store b
   StoreStore
store v
   StoreStore
store u
   StoreLoad
load u
   LoadLoad
   LoadStore
load b
store a

 

 StoreStore Barriers

The sequence: Store1; StoreStore; Store2
ensures that Store1‘s data are visible to other processors (i.e., flushed to memory) before the data associated with Store2 and all subsequent store instructions. In general, StoreStore barriers are needed on processors that do not otherwise guarantee strict ordering of flushes from write buffers and/or caches to other processors or main memory.

观察第一张表,我们可以发现,对于volatile存储操作,不管上一条指令时什么操作,编译器会在volatile存储指令和上一条指令中间插入内存屏障指令。

对于第2种修复方案,当我们向volatile引用存储对象的时候,编译器会插入一条StoreStore屏障。对于 Store1; StoreStore; Store2 指令序列,(在这里Store2指令就是volatile存储引用操作),Store1存储的数据会先于Store2存储的数据和其后续的存储数据对其他处理器可见(也就是刷新到内存)。在其他线程第一次获取到对象引用的时候,必定能够获取到对象引用中的字段。

也就是volatile修饰的对象赋值时,能够保证之前对volatile对象字段的编辑都被写入到主内存中。

 

最后,对于第3种情况。将对象保存到正确构造的对象的final域中。为什么这样能够保证对象的安全发布?final字段的基本语义是不可变更的字段,它除此之外还有着一些其他的语义。在 Java Language Specification 第17章 第5小节有这样几句话,

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object‘s final fields.

The usage model for final fields is a simple one: Set the final fields for an object in that object‘s constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object‘s constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object‘s final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

对于final修饰的字段(也就是我们构造的对象),只有在持有该final字段的对象构造函数完成之后,持有该final字段的对象才可以被其他线程可见,这是为了保证其他线程能够访问final字段(也就是我们构造的对象)。更重要的是,其他线程也能够看到final字段赋值时的字段引用的数组或者对象。也就是能够看到构造函数中赋值的对象。 当然这个的前提是对象是安全发布的,也就是在构造函数调用的过程中没有暴露给其他线程。

 

单例设计模式和Java内存模型

标签:完成   reload   bsp   memory   res   display   获取   线程   过程   

原文地址:https://www.cnblogs.com/notUlnix/p/notUlnix.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!