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

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

正常的执行顺序是

执行构造函数

构造函数执行完毕

将新构造的对象赋值到引用

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

执行构造函数

将对象赋值到引用

构造函数执行完毕

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

335b83df261c422459d4afc29ba290e5.png

d1b641f023dd079c9e4a800b96607d9d.gif

1 classFoo {2 private Helper helper = null;3 publicHelper getHelper() {4 if (helper == null)5 synchronized(this) {6 if (helper == null)7 helper = newHelper();8 }9 returnhelper;10 }11 //other functions and members...

12 }

View Code

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

在静态初始化函数中初始化一个对象的引用。

将对象引用保存到volatile类型的域中或者AtomicReference对象中。

将对象引用保存到某个正确构造对象的final类型域中。

将对象的引用保存到一个由锁保护的域中。

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

335b83df261c422459d4afc29ba290e5.png

d1b641f023dd079c9e4a800b96607d9d.gif

1 classHelperSingleton {2 static Helper singleton = newHelper();3 }

View Code

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

335b83df261c422459d4afc29ba290e5.png

d1b641f023dd079c9e4a800b96607d9d.gif

classFoo {private Helper helper = null;public synchronizedHelper getHelper() {if (helper == null)

helper= newHelper();returnhelper;

}//other functions and members...

}

View Code

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

335b83df261c422459d4afc29ba290e5.png

d1b641f023dd079c9e4a800b96607d9d.gif

1 classFoo {2 private volatile Helper helper = null;3 publicHelper getHelper() {4 if (helper == null) {5 synchronized(this) {6 if (helper == null)7 helper = newHelper();8 }9 }10 returnhelper;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.4) happens-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读所感知到。

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

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

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

在这里,我们需要引用该文章中的两张表格和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字段赋值时的字段引用的数组或者对象。也就是能够看到构造函数中赋值的对象。 当然这个的前提是对象是安全发布的,也就是在构造函数调用的过程中没有暴露给其他线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值