- 浏览: 253289 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
runjia1987:
cpu 100%,是因为读=-1时,没有解注册OP_READ, ...
java nio网络编程的一点心得 -
two_plus:
按照你的例子 服务端变成读循环了...
java nio网络编程的一点心得 -
ilovemyyang:
我也遇到同样的空循环问题,也是百度才找到了的,楼主给力哦
java nio网络编程的一点心得 -
高攀sky:
Python发邮件 -
fobdddf:
fobdddf 写道朋友你好,我试了一下数组的那个例子,反汇编 ...
读《深入理解计算机系统》
我之前写过一篇谈DCL的文章,最近又收到一个问题,本想直接回复,但我又不想再看原来写的文章,那些顺序分析其实很绕。这次我不会直接分析顺序,而是从基础概念讲起,希望大家能看得轻松一些。
如果你搜索网上分析dcl为什么在java中失效的原因,都会谈到编译器会做优化云云,我相信大家看到这个一定会觉得很沮丧、很无助,对自己写的程序很没信心。我很理解这种感受,因为我也经历过,这或许是为什么网上一直有人喜欢谈dcl的原因。如果放在java5之前,从编译器的角度去解释dcl也无可厚非,在java5的JMM(内存模型)已经得到很大的修正,如果到现在还只能从编译器的角度去解释dcl,那简直就在污辱java,要知道java的最大优势就是只需要考虑一个平台。你可以完全无视网上绝大多数关于dcl的讨论,很多时候他们自己都说不清楚,除Doug Lea等几个大牛,我不相信谁比谁更权威。
很多人不理解dcl,不是dcl有多么复杂,恰恰相反,而是对基础掌握得不够。所以,我会先从基础讲起,然后再分析DCL。
我们都知道,当两个线程同时读写(或同时写)一个共享变量时会发生数据竞争。那我怎么才能知道发生了数据竞争呢?我需要去读取那个变量,发生数据竞争通常有两个表现:一是读取到陈旧数据,即读取到虽是曾经写入的数据,但不是最新的。二是读取到之前根本没有写入的值,也就是说读到垃圾。
数据陈旧性
为了读取到另一个线程写入的最新数据,JMM定义了一系列的规则,最基本的规则就是要利用同步。在Java中,同步的手段有synchronized和volatile两种,这里我只会涉及到syncrhonized。请大家先记住以下规则,接下来我会细讲。
规则一:必须对变量的所有写和所有读同步,才能读取到该最新的数据。
先看下面的代码:
让我们来分析一个线程写,另一个线程读的情形,一共四种情形。初始情况都是a = new A(),暂不考虑其它线程。
情形一:读写都不同步。
这种情况下,即使thread1先写入some为13,thread2再读取some,它能读到13吗?在没有同步协调下,结果是不确定的。从图上看出,两个线程独立运行,JMM并不保证一个线程能够看到另一个线程写入的值。在这个例子中,就是thread2可能读到0(即some的初始值)而不是13。注意,在理论上,即使thread2在thread1写入some之后再等上一万年也还是可能读到some的初始值0,尽管这在实际几乎不可能发生。
情形二:写同步,读不同步
情形三:读同步,写不同步
在这两种情况下,thread1和thread2只对读或只对写some加了锁,这不起任何作用,和[情形一]一样,thread2仍有可能读到some的初始值0。从图上也可看出,thread1和thread2互相之间并没有任何影响,一方加锁并不影响另一方的继续运行。图中也显示,同步操作相当于在同步开始执行lock操作,在同步结束时执行unlock操作。
情形四:读写都同步
在情形四中,thread1写入some时,thread2等待thread1写入完成,并且它能看到thread1对some做的修改,这时thread2保证能读到13。实际上,thread2不仅能看到thread1对some的修改,而且还能看到thread1在修改some之前所做的任何修改。说得更精确一些,就是一个线程的lock操作能看见另一线程对同一个对象unlock操作之前的所有修改,请注意图中的红色箭头。 沿着图中箭头指示方向,箭头结尾处总能看到箭头开始处操作做的修改。这样,a.some[thread2]能看见lock[thread2],lock[thread2]能看见unlock[thread1],unlock[thread1]又能看见a.some=13[thread1],即能看到some的值为13。
再来看一个稍微复杂一点的例子:
例子五
thread2最后会读到another的什么值呢?会不会读到another的初始值0呢,毕竟所有对another的访问都没有同步?不会。从图中很清晰地可以看出,thread2的another至少到看到thread1在lock之前写入的5,却并不能保证它能看到thread1在unlock写入的7。因此,thread2可以什么读到another的值可能5或7,但不会是0。你或许已经发现,如果去掉图中thread2读取a.some的操作,这时相当于一个空的同步块,对结论并没有任何影响。这说明空的同步块是起作用的,编译器不能擅自将空的同步块优化掉,但你在使用空的同步块应该特别小心,通常它都不是你想要的结果。另外需要注意,unlock操作和lock操作必须针对同一个对象,才能保证unlock操作能看到lock操作之前所做的修改。
例子六:不同的锁
在这种情况下,虽然getSome和setSome都加了锁,但由于它们是不同的锁,一个线程运行时并不能阻塞另一个线程运行。因此这里的情形和情形一、二、三一样,thread2不保证读到thread1写入的some最新值。
现在来看DCL:
例子七: DCL
假设thread1先调用getInstance(),由于此时还没有任何线程创建LazySingleton实例,它会创建一个实例s并返回。这是thread2再调用getInstance(),当它运行到(2)处,由于这时读instance没有同步,它有可能读到s或者null(参考情形二)。先考虑它读到s的情形,画出流程图就是下面这样的:
由于thread2已经读到s,所以getInstance()会立即返回s,这是没有任何问题,但当它读取s.someFiled时问题就发生了。 从图中可以看thread2没有任何同步,所以它可能看不到thread1写入someField的值20,对thread2来说,它可能读到s.someField为0,这就是DCL的根本问题。从上面的分析也可以看出,为什么试图修正DCL但又希望完全避免同步的方法几乎总是行不通的。
接下来考虑thread2在(2)处读到instance为null的情形,画出流程图:
接下来thread2会在有锁的情况下读取instance的值,这时它保证能读到s,理由参考情形四或者通过图中箭头指示方向来判定。
关于DCL就说这么多,留下两个问题:
原子性
回到情形一,为什么我们说thread2读到some的值只可能为为0或13,而不可能为其它?这是由java对int、引用读写都是原子性所决定的。所谓“原子性”,就是不可分割的最小单元,有数据库事务概念的同学们应该对此容易理解。当调用some=13时,要么就写入成功要么就写入失败,不可能写入一半。但是,java对double, long的读写却不是原子操作,这意味着可能发生某些极端意外的情况。看例子:
thread2读取x的值可能为0,1122334400112233外,还可能为别的完全意想不到的值。一种情况假设jvm对long的写入是先写低4字节,再写高4字节,那么读取到x的值还可能为112233。但是我们不对jvm做如此假设,为了保证对long或double的读写是原子操作,有两种方式,一是使用volatile,二是使用synchronized。对上面的例子,如果取消(1)处的volatile注释,将能保证thread2读取到x的值要么为0,要么为1122334400112233。如果使用同步,则必须像下面这样对getX,setX都同步:
因此对原子性也有规则(volatile其实也是一种同步)。
规则二:对double, long变量,只有对所有读写都同步,才能保证它的原子性
有时候我们需要保证一个复合操作的原子性,这时就只能使用synchronized。
当没有同步的情况下,thread2的getPos可能会得到[1, 2], 尽管该点可能从来没有出现过。之所以会出现这样的结果,是因为thread2在调用getPos()时,curX有0,1或2三种可能,同样curY也有0,1或2三种可能,所以getPos()可能返回[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]共九种可能。要避免这种情况,只有将getPos()和moveTo都设为同步方法。
总结
以上分析了数据竞争的两种症状,陈旧数据和非原子操作,都是由于没有恰当同步引起的。这些其实都是相当基础的知识,同步可有两种效果:一是保证读取最新数据,二是保证操作原子性,但是大多数书籍都对后者过份强调,对前者认识不足,以致对多线程的认识上存在很多误区。如果想要掌握java线程高级知识,我只推荐《Java并发编程设计原则与模式》。其实我已经好久没有写Java了,这些东西都是我两年前的知识,如果存在问题,欢迎大家指出,千万不要客气。
--------------------分割线--------------------
2011/1/21更新:加了许多图,大幅度修改数据陈旧性部分文字描述。
+1.
BRS很帅啊。呵呵
就DLC来讲,本例中的instance的值是存在于heap上的,但是有可能thread为了操作的效率会在自己的stack上copy一份instance的副本,所以在多线程操作的时候,同步操作会导致thread将自己stack中的instance反写回heap中,然后其他的读操作,如果有同步操作才能从heap中得到最新的值,如果不同步读操作,那么有可能该thread的stack中的值还是old的
instance在thread2的运行过程中一直都没变,只是在没加锁访问时它有可能读不到最新值。在第(4)处加锁访问才能保证读到最新的值。
我一直避免谈得太底层,但多一种理解也是好处的。除了共享内存(shared memory)之外,每个线程都可能有自己缓存,读取线程会先从缓存中读取。对于thread2来说,在(2)处它读到缓存值的null,虽然此时主内存里已经被thread2修改过了,但thread2并不能观察到这点,但到(4)处,由于加了锁,它强制缓存失效,必须从共享内存读取最新值,即thread1修改过的值。
综合上面的引用,我个人的理解是这样子的
我只所以对DCL这么纠结是曾经被某位牛人在这个问题上给鄙视了,所以我也花了点时间来了解这个问题
marlonyao,是程序员“耐心、有条理”的良好示范。:)
你再想想,回答得都不对。
也许是我表达不准,也许真的是我搞错了,我再解释一下
(1)对于多线程程序来讲,是否能得到最新值,应该以happen-before规则来判断线程能否得到最新值,如果要保证这种最新值,就必须做同步操作(使得线程的内存和主存同步以此获得最新值),而之所以DLC会有问题,是因为对于某一线程,如果在第一次判断(即操作(2))为false的情况下,那么该线程就不会有任何作同步操作的机会,此外getSomeField本身也非同步方法,因此可能导致getSomeField得到的值不确定
(2)(4)处的check应该有两层含义,第一:它位于同步锁内,所以他保证此时instance是内存中最新值,也能保证instance的field是最新的;第二:如果有别的线程已经初始化了instance,那么他需要判断是否为null,以此决定自己是否需要再new一次。
以上是我的理解,有误之处请指正(最好详细点:)),谢谢。
原来一直说有可能thread1创建了对象但是初始化还没完成,thread2就去访问。请问1.5怎么避免这种情况。
thread2读取x的值可能为0,1122334400112233外,还可能为别的完全意想不到的值。一种情况假设jvm对long的写入是先写低4位,再写高4位,那么读取到x的值还可能为112233。
这里是不是弄错了,我翻了下《java并发编程实践》。
The Java Memory Model requires fetch and store operations to be atomic, but for nonvolatile long and double variables, the JVM is permitted to treat a 64-bit(位) read or write as two separate 32-bit operations. If the reads and writes occur in different threads, it is therefore possible to read a nonvolatile long and get back the high 32 bits of one value and the low 32 bits of another
不好意思,我写错了,应该是4字节。
还是那句话,进锁之前后instance的值没有变化,只是进锁前看到的陈旧值,进锁后看到的是最新值,希望你已经明白了。
明白了
thread2读取x的值可能为0,1122334400112233外,还可能为别的完全意想不到的值。一种情况假设jvm对long的写入是先写低4位,再写高4位,那么读取到x的值还可能为112233。
这里是不是弄错了,我翻了下《java并发编程实践》。
The Java Memory Model requires fetch and store operations to be atomic, but for nonvolatile long and double variables, the JVM is permitted to treat a 64-bit(位) read or write as two separate 32-bit operations. If the reads and writes occur in different threads, it is therefore possible to read a nonvolatile long and get back the high 32 bits of one value and the low 32 bits of another
还是那句话,进锁之前后instance的值没有变化,只是进锁前看到的陈旧值,进锁后看到的是最新值,希望你已经明白了。
instance在thread2的运行过程中一直都没变,只是在没加锁访问时它有可能读不到最新值。在第(4)处加锁访问才能保证读到最新的值。
我一直避免谈得太底层,但多一种理解也是好处的。除了共享内存(shared memory)之外,每个线程都可能有自己缓存,读取线程会先从缓存中读取。对于thread2来说,在(2)处它读到缓存值的null,虽然此时主内存里已经被thread2修改过了,但thread2并不能观察到这点,但到(4)处,由于加了锁,它强制缓存失效,必须从共享内存读取最新值,即thread1修改过的值。
还要看第二条,其实不矛盾。如果你看第一章的例子,他读取粒子位置时也是加了锁。
另外,第三条是为了防止发生死锁。
你再想想,回答得都不对。
是因为thread2在调用getPos()时,curX有0,1或2三种可能,同样curY也有0,1或2三种可能,所以getPos()可能返回[0,1], [0,2], [0,3], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]共九种可能
[0,3]应为[0,0]吧
谢谢指出,已更正。
就DLC来讲,本例中的instance的值是存在于heap上的,但是有可能thread为了操作的效率会在自己的stack上copy一份instance的副本,所以在多线程操作的时候,同步操作会导致thread将自己stack中的instance反写回heap中,然后其他的读操作,如果有同步操作才能从heap中得到最新的值,如果不同步读操作,那么有可能该thread的stack中的值还是old的
如果你搜索网上分析dcl为什么在java中失效的原因,都会谈到编译器会做优化云云,我相信大家看到这个一定会觉得很沮丧、很无助,对自己写的程序很没信心。我很理解这种感受,因为我也经历过,这或许是为什么网上一直有人喜欢谈dcl的原因。如果放在java5之前,从编译器的角度去解释dcl也无可厚非,在java5的JMM(内存模型)已经得到很大的修正,如果到现在还只能从编译器的角度去解释dcl,那简直就在污辱java,要知道java的最大优势就是只需要考虑一个平台。你可以完全无视网上绝大多数关于dcl的讨论,很多时候他们自己都说不清楚,除Doug Lea等几个大牛,我不相信谁比谁更权威。
很多人不理解dcl,不是dcl有多么复杂,恰恰相反,而是对基础掌握得不够。所以,我会先从基础讲起,然后再分析DCL。
我们都知道,当两个线程同时读写(或同时写)一个共享变量时会发生数据竞争。那我怎么才能知道发生了数据竞争呢?我需要去读取那个变量,发生数据竞争通常有两个表现:一是读取到陈旧数据,即读取到虽是曾经写入的数据,但不是最新的。二是读取到之前根本没有写入的值,也就是说读到垃圾。
数据陈旧性
为了读取到另一个线程写入的最新数据,JMM定义了一系列的规则,最基本的规则就是要利用同步。在Java中,同步的手段有synchronized和volatile两种,这里我只会涉及到syncrhonized。请大家先记住以下规则,接下来我会细讲。
规则一:必须对变量的所有写和所有读同步,才能读取到该最新的数据。
先看下面的代码:
public class A { private int some; public int another; public int getSome() { return some; } public synchronized int getSomeWithSync() { return some; } public void setSome(int v) { some = v; } public synchronized void setSomeWithSync(int v) { some = v; } }
让我们来分析一个线程写,另一个线程读的情形,一共四种情形。初始情况都是a = new A(),暂不考虑其它线程。
情形一:读写都不同步。
Thread1 | Thread2 |
(1) a.setSome(13) | |
(2) a.getSome() |
这种情况下,即使thread1先写入some为13,thread2再读取some,它能读到13吗?在没有同步协调下,结果是不确定的。从图上看出,两个线程独立运行,JMM并不保证一个线程能够看到另一个线程写入的值。在这个例子中,就是thread2可能读到0(即some的初始值)而不是13。注意,在理论上,即使thread2在thread1写入some之后再等上一万年也还是可能读到some的初始值0,尽管这在实际几乎不可能发生。
情形二:写同步,读不同步
Thread1 | Thread2 |
(1) a.setSomeWithSync(13) | |
(2) a.getSome() |
情形三:读同步,写不同步
Thread1 | Thread2 |
(1) a.setSome(13) | |
(2) a.getSomeWithSync() |
在这两种情况下,thread1和thread2只对读或只对写some加了锁,这不起任何作用,和[情形一]一样,thread2仍有可能读到some的初始值0。从图上也可看出,thread1和thread2互相之间并没有任何影响,一方加锁并不影响另一方的继续运行。图中也显示,同步操作相当于在同步开始执行lock操作,在同步结束时执行unlock操作。
情形四:读写都同步
Thread1 | Thread2 |
(1) a.setSomeWithSync(13) | |
(2) a.getSomeWithSync() |
在情形四中,thread1写入some时,thread2等待thread1写入完成,并且它能看到thread1对some做的修改,这时thread2保证能读到13。实际上,thread2不仅能看到thread1对some的修改,而且还能看到thread1在修改some之前所做的任何修改。说得更精确一些,就是一个线程的lock操作能看见另一线程对同一个对象unlock操作之前的所有修改,请注意图中的红色箭头。 沿着图中箭头指示方向,箭头结尾处总能看到箭头开始处操作做的修改。这样,a.some[thread2]能看见lock[thread2],lock[thread2]能看见unlock[thread1],unlock[thread1]又能看见a.some=13[thread1],即能看到some的值为13。
再来看一个稍微复杂一点的例子:
例子五
Thread1 | Thread2 |
(1) a.another = 5 | |
(2) a.setSomeWithSync(13) | |
(3) a.getSomeWithSync() | |
(4) a.another = 7 | |
(5) a.another |
thread2最后会读到another的什么值呢?会不会读到another的初始值0呢,毕竟所有对another的访问都没有同步?不会。从图中很清晰地可以看出,thread2的another至少到看到thread1在lock之前写入的5,却并不能保证它能看到thread1在unlock写入的7。因此,thread2可以什么读到another的值可能5或7,但不会是0。你或许已经发现,如果去掉图中thread2读取a.some的操作,这时相当于一个空的同步块,对结论并没有任何影响。这说明空的同步块是起作用的,编译器不能擅自将空的同步块优化掉,但你在使用空的同步块应该特别小心,通常它都不是你想要的结果。另外需要注意,unlock操作和lock操作必须针对同一个对象,才能保证unlock操作能看到lock操作之前所做的修改。
例子六:不同的锁
class B { private Object lock1 = new Object(); private Object lock2 = new Object(); private int some; public int getSome() { synchronized(lock1) { return some; } } public void setSome(int v) { synchronized(lock2) { some = v; } } }
Thread1 | Thread2 |
(1) b.setSome(13) | |
(2) b.getSome() |
在这种情况下,虽然getSome和setSome都加了锁,但由于它们是不同的锁,一个线程运行时并不能阻塞另一个线程运行。因此这里的情形和情形一、二、三一样,thread2不保证读到thread1写入的some最新值。
现在来看DCL:
例子七: DCL
public class LazySingleton { private int someField; private static LazySingleton instance; private LazySingleton() { this.someField = 201; // (1) } public static LazySingleton getInstance() { if (instance == null) { // (2) synchronized(LazySingleton.class) { // (3) if (instance == null) { // (4) instance = new LazySingleton(); // (5) } } } return instance; // (6) } public int getSomeField() { return this.someField; // (7) } }
假设thread1先调用getInstance(),由于此时还没有任何线程创建LazySingleton实例,它会创建一个实例s并返回。这是thread2再调用getInstance(),当它运行到(2)处,由于这时读instance没有同步,它有可能读到s或者null(参考情形二)。先考虑它读到s的情形,画出流程图就是下面这样的:
由于thread2已经读到s,所以getInstance()会立即返回s,这是没有任何问题,但当它读取s.someFiled时问题就发生了。 从图中可以看thread2没有任何同步,所以它可能看不到thread1写入someField的值20,对thread2来说,它可能读到s.someField为0,这就是DCL的根本问题。从上面的分析也可以看出,为什么试图修正DCL但又希望完全避免同步的方法几乎总是行不通的。
接下来考虑thread2在(2)处读到instance为null的情形,画出流程图:
接下来thread2会在有锁的情况下读取instance的值,这时它保证能读到s,理由参考情形四或者通过图中箭头指示方向来判定。
关于DCL就说这么多,留下两个问题:
- 接着考虑thread2在(2)读到instance为null的情形,它接着调用s.someFiled会得到什么?会得到0吗?
- DCL为什么要double check,能不能去掉(4)处的check?若不能,为什么?
原子性
回到情形一,为什么我们说thread2读到some的值只可能为为0或13,而不可能为其它?这是由java对int、引用读写都是原子性所决定的。所谓“原子性”,就是不可分割的最小单元,有数据库事务概念的同学们应该对此容易理解。当调用some=13时,要么就写入成功要么就写入失败,不可能写入一半。但是,java对double, long的读写却不是原子操作,这意味着可能发生某些极端意外的情况。看例子:
public class C { private /* volatile */ long x; // (1) public void setX(long v) { x = v; } public long getX() { return x; } }
Thread1 | Thread2 |
(1) c.setX(0x1122334400112233L) | |
(2) c.getX() |
thread2读取x的值可能为0,1122334400112233外,还可能为别的完全意想不到的值。一种情况假设jvm对long的写入是先写低4字节,再写高4字节,那么读取到x的值还可能为112233。但是我们不对jvm做如此假设,为了保证对long或double的读写是原子操作,有两种方式,一是使用volatile,二是使用synchronized。对上面的例子,如果取消(1)处的volatile注释,将能保证thread2读取到x的值要么为0,要么为1122334400112233。如果使用同步,则必须像下面这样对getX,setX都同步:
public class C { private /* volatile */ long x; // (1) public synchronized void setX(long v) { x = v; } public synchronized long getX() { return x; } }
因此对原子性也有规则(volatile其实也是一种同步)。
规则二:对double, long变量,只有对所有读写都同步,才能保证它的原子性
有时候我们需要保证一个复合操作的原子性,这时就只能使用synchronized。
public class Canvas { private int curX, curY; public /* synchronized */ getPos() { return new int[] { curX, curY }; } public /* synchronized */ void moveTo(int x, int y) { curX = x; curY = y; } }
Thread1 | Thread2 |
(1) c.moveTo(1, 1) | |
(2) c.moveTo(2, 2) | |
(3) c.getPos() |
当没有同步的情况下,thread2的getPos可能会得到[1, 2], 尽管该点可能从来没有出现过。之所以会出现这样的结果,是因为thread2在调用getPos()时,curX有0,1或2三种可能,同样curY也有0,1或2三种可能,所以getPos()可能返回[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]共九种可能。要避免这种情况,只有将getPos()和moveTo都设为同步方法。
总结
以上分析了数据竞争的两种症状,陈旧数据和非原子操作,都是由于没有恰当同步引起的。这些其实都是相当基础的知识,同步可有两种效果:一是保证读取最新数据,二是保证操作原子性,但是大多数书籍都对后者过份强调,对前者认识不足,以致对多线程的认识上存在很多误区。如果想要掌握java线程高级知识,我只推荐《Java并发编程设计原则与模式》。其实我已经好久没有写Java了,这些东西都是我两年前的知识,如果存在问题,欢迎大家指出,千万不要客气。
--------------------分割线--------------------
2011/1/21更新:加了许多图,大幅度修改数据陈旧性部分文字描述。
评论
29 楼
xingxiujie100
2015-04-13
我认为DCL最大的问题就是不符合happen-before原则,导致同步快中的对象构造存在指令重排。指令重排后,从另外一条线程中去观察,可能会发现引用已经不为null了,但是这个时候的对象还不是一个完整的对象(因为还没有运行<init>,所以对象中的各个域还是初始值),此时使用这个对象就会产生非常诡异的问题。
28 楼
miniJJ
2012-03-14
楼主,恕我驽钝,看你2篇帖子看了几个小时,有个地方始终是有疑问
你在此文中写道:
“假设thread1先调用getInstance(),由于此时还没有任何线程创建LazySingleton实例,它会创建一个实例s并返回。这是thread2再调用getInstance(),当它运行到(2)处,由于这时读instance没有同步,它有可能读到s或者null(参考情形二)。先考虑它读到s的情形,画出流程图就是下面这样的:”
在这里,你假设了thread2在(2)处读到了s。thread2读到s,我是不是可以理解为thread1已经将s与主内存变量同步,所以thread2才从主内存中读到了s。按照你头篇帖子里happen-Before原则的定义:操作A happen-before操作B时,我们其实是在说在发生操作B之前,操作A对内存施加的影响能够被观测到。
你假设thread1对instance的赋值操作已被 thread2观测到
那我是不是可以理解你已经假设了thread1中的
instance = new LazySingleton(); // (5) happens-before
thread2中的 if (instance == null) { // (4)
又按照happens-before的传递规则,
thread1中的 this.someField = 201; // (1) happens-before
thread2的if (instance == null) { // (4)
那又怎么会发生你说的“对thread2来说,它可能读到s.someField为0”
还望楼主不吝赐教
你在此文中写道:
“假设thread1先调用getInstance(),由于此时还没有任何线程创建LazySingleton实例,它会创建一个实例s并返回。这是thread2再调用getInstance(),当它运行到(2)处,由于这时读instance没有同步,它有可能读到s或者null(参考情形二)。先考虑它读到s的情形,画出流程图就是下面这样的:”
在这里,你假设了thread2在(2)处读到了s。thread2读到s,我是不是可以理解为thread1已经将s与主内存变量同步,所以thread2才从主内存中读到了s。按照你头篇帖子里happen-Before原则的定义:操作A happen-before操作B时,我们其实是在说在发生操作B之前,操作A对内存施加的影响能够被观测到。
你假设thread1对instance的赋值操作已被 thread2观测到
那我是不是可以理解你已经假设了thread1中的
instance = new LazySingleton(); // (5) happens-before
thread2中的 if (instance == null) { // (4)
又按照happens-before的传递规则,
thread1中的 this.someField = 201; // (1) happens-before
thread2的if (instance == null) { // (4)
那又怎么会发生你说的“对thread2来说,它可能读到s.someField为0”
还望楼主不吝赐教
27 楼
hastune
2011-04-21
aabcc 写道
要说清楚这个问题,得先说明一下 JMM 啊,内存模型不摆出来,这个问题永远只能是争个你死我活...
+1.
BRS很帅啊。呵呵
26 楼
uin57
2011-04-21
neo_q 写道
aabcc 写道
要说清楚这个问题,得先说明一下 JMM 啊,内存模型不摆出来,这个问题永远只能是争个你死我活...
就DLC来讲,本例中的instance的值是存在于heap上的,但是有可能thread为了操作的效率会在自己的stack上copy一份instance的副本,所以在多线程操作的时候,同步操作会导致thread将自己stack中的instance反写回heap中,然后其他的读操作,如果有同步操作才能从heap中得到最新的值,如果不同步读操作,那么有可能该thread的stack中的值还是old的
marlonyao 写道
ak121077313 写道
有一点不明白
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
}
thread2如果通过2判断 instance == null 进入4 lz说会读到最新的值
我不明白在没有赋值的情况下为什么instance的值会变动??????
谁在这个过程中赋值了?难道是锁?
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
}
thread2如果通过2判断 instance == null 进入4 lz说会读到最新的值
我不明白在没有赋值的情况下为什么instance的值会变动??????
谁在这个过程中赋值了?难道是锁?
instance在thread2的运行过程中一直都没变,只是在没加锁访问时它有可能读不到最新值。在第(4)处加锁访问才能保证读到最新的值。
我一直避免谈得太底层,但多一种理解也是好处的。除了共享内存(shared memory)之外,每个线程都可能有自己缓存,读取线程会先从缓存中读取。对于thread2来说,在(2)处它读到缓存值的null,虽然此时主内存里已经被thread2修改过了,但thread2并不能观察到这点,但到(4)处,由于加了锁,它强制缓存失效,必须从共享内存读取最新值,即thread1修改过的值。
综合上面的引用,我个人的理解是这样子的
public class LazySingleton { private int someField; private static LazySingleton instance; private LazySingleton() { this.someField = 201; // (1) } public static LazySingleton getInstance() { if (instance == null) { // (2) synchronized(LazySingleton.class) { // (3) if (instance == null) { // (4) instance = new LazySingleton(); // (5) } instance.getSomeField(); // thread2这里的值应该是正确的,因为在同步块内,读到的是heap里的someField } instance.getSomeField(); // 这里读到的有可能是thread2自己stack里的someField,此时在解除锁的时候只把instance引用从heap更新到stack中,并没有把somefield更新到stack中,造成了有可能读取到的是0 } return instance; // (6) } public int getSomeField() { return this.someField; // (7) } }
25 楼
neo_q
2011-01-24
marlonyao 写道
@neo_q 对第一个问题,可能你理解错我的意思了,我的问题假设thread第一次检查时发现instance为null。这时它在有锁情况下进行第二次检测,这是它保证能读到someField的最新值,不会读到0。
我的理解是对多个线程同时并发的状况下,两个线程之间的状态共享,我现在理解你的意思了
我的理解是对多个线程同时并发的状况下,两个线程之间的状态共享,我现在理解你的意思了
我只所以对DCL这么纠结是曾经被某位牛人在这个问题上给鄙视了,所以我也花了点时间来了解这个问题
24 楼
neo_q
2011-01-24
marlonyao 写道
添加了大量示意图,数据陈旧性部分作了很大修改。
marlonyao,是程序员“耐心、有条理”的良好示范。:)
23 楼
marlonyao
2011-01-22
添加了大量示意图,数据陈旧性部分作了很大修改。
22 楼
marlonyao
2011-01-21
@neo_q 对第一个问题,可能你理解错我的意思了,我的问题假设thread第一次检查时发现instance为null。这时它在有锁情况下进行第二次检测,这是它保证能读到someField的最新值,不会读到0。
对第二个问题,你解释是对的,如果没有(4)处的check,有可能创建多个实例,就完全违背了Singleton模式的原则。
对第二个问题,你解释是对的,如果没有(4)处的check,有可能创建多个实例,就完全违背了Singleton模式的原则。
21 楼
neo_q
2011-01-21
marlonyao 写道
neo_q 写道
[1]如果thread2执行getInstance()在(2)处发现instance为null,对getInstance()的返回结果继续调用getSomeField()将得到什么?会得到0吗?
因为在调用构造方法时,thread2做了赋值,而且是做了同步操作的,如果调用的线程本身有做同步操作或者是在同一线程那么可以得到确定的值,如果调用的线程是别的线程且未做同步操作那么值是不确定的,因为从头到尾该线程都未取得该对象的锁,因此无法保证它能获得最新的值
[2]DCL为什么要double check,能不能去掉(4)处的check?若不能,为什么?
不能去掉,因为在(2)执行的时候,如果已经进行了判断,之后另外一个线程进行了操作有可能使instance不为null
你再想想,回答得都不对。
也许是我表达不准,也许真的是我搞错了,我再解释一下
(1)对于多线程程序来讲,是否能得到最新值,应该以happen-before规则来判断线程能否得到最新值,如果要保证这种最新值,就必须做同步操作(使得线程的内存和主存同步以此获得最新值),而之所以DLC会有问题,是因为对于某一线程,如果在第一次判断(即操作(2))为false的情况下,那么该线程就不会有任何作同步操作的机会,此外getSomeField本身也非同步方法,因此可能导致getSomeField得到的值不确定
(2)(4)处的check应该有两层含义,第一:它位于同步锁内,所以他保证此时instance是内存中最新值,也能保证instance的field是最新的;第二:如果有别的线程已经初始化了instance,那么他需要判断是否为null,以此决定自己是否需要再new一次。
以上是我的理解,有误之处请指正(最好详细点:)),谢谢。
20 楼
yanical
2011-01-20
marlonyao 写道
@yanical java1.5后的dcl也不安全,将someField加上volatile才是安全的。
原来一直说有可能thread1创建了对象但是初始化还没完成,thread2就去访问。请问1.5怎么避免这种情况。
19 楼
marlonyao
2011-01-20
@yanical java1.5后的dcl也不安全,将someField加上volatile才是安全的。
18 楼
yanical
2011-01-20
挣扎,花了一个多小时看两篇文章,还是不明白为什么1.5后DCL是安全的。太笨了~~
如果thread2执行getInstance()在(2)处发现instance为null,对getInstance()的返回结果继续调用getSomeField()将得到什么?会得到0吗?
如果这个时候是null,应该就能保证不是0了。但是如果不是null,是不是还有可能返回的对象是没有构造完的,所以有可能返回0
特别对前面一篇文章提到的
在java 5中多增加了一条happen-before规则:
* 对volatile字段的写操作happen-before后续的对同一个字段的读操作。
利用这条规则我们可以将instance声明为volatile,即:
private volatile static LazySingleton instance;
根据这条规则,我们可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。
线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5). 这个是什么原理,不是应该(5)先执行吗?
1. public static LazySingleton getInstance() {
2. if (instance == null) { // (4)
3. synchronized(LazySingleton.class) { // (5)
4. if (instance == null) { // (6)
5. LazySingleton localRef = new LazySingleton();
6. instance = localRef; // (7)
7. }
8. }
9. }
10. return instance; // (8)
11. }
这个不行的原因是不是因为没法保证其他线程立刻看到这个线程修改的内容? 那如果把instance 和someField 加上volatile呢?
marlonyao 写道
如果这个时候是null,应该就能保证不是0了。但是如果不是null,是不是还有可能返回的对象是没有构造完的,所以有可能返回0
特别对前面一篇文章提到的
marlonyao 写道
在java 5中多增加了一条happen-before规则:
* 对volatile字段的写操作happen-before后续的对同一个字段的读操作。
利用这条规则我们可以将instance声明为volatile,即:
private volatile static LazySingleton instance;
根据这条规则,我们可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。
线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5). 这个是什么原理,不是应该(5)先执行吗?
marlonyao 写道
1. public static LazySingleton getInstance() {
2. if (instance == null) { // (4)
3. synchronized(LazySingleton.class) { // (5)
4. if (instance == null) { // (6)
5. LazySingleton localRef = new LazySingleton();
6. instance = localRef; // (7)
7. }
8. }
9. }
10. return instance; // (8)
11. }
这个不行的原因是不是因为没法保证其他线程立刻看到这个线程修改的内容? 那如果把instance 和someField 加上volatile呢?
17 楼
marlonyao
2011-01-20
明天的昨天 写道
marlonyao 写道
thread2读取x的值可能为0,1122334400112233外,还可能为别的完全意想不到的值。一种情况假设jvm对long的写入是先写低4位,再写高4位,那么读取到x的值还可能为112233。
这里是不是弄错了,我翻了下《java并发编程实践》。
引用
The Java Memory Model requires fetch and store operations to be atomic, but for nonvolatile long and double variables, the JVM is permitted to treat a 64-bit(位) read or write as two separate 32-bit operations. If the reads and writes occur in different threads, it is therefore possible to read a nonvolatile long and get back the high 32 bits of one value and the low 32 bits of another
不好意思,我写错了,应该是4字节。
16 楼
ak121077313
2011-01-20
marlonyao 写道
ak121077313 写道
还是没有说明谁给instance 赋值了。。。
进锁之前是null 进锁之后又不是null
进锁之前是null 进锁之后又不是null
还是那句话,进锁之前后instance的值没有变化,只是进锁前看到的陈旧值,进锁后看到的是最新值,希望你已经明白了。
明白了
15 楼
明天的昨天
2011-01-20
marlonyao 写道
thread2读取x的值可能为0,1122334400112233外,还可能为别的完全意想不到的值。一种情况假设jvm对long的写入是先写低4位,再写高4位,那么读取到x的值还可能为112233。
这里是不是弄错了,我翻了下《java并发编程实践》。
引用
The Java Memory Model requires fetch and store operations to be atomic, but for nonvolatile long and double variables, the JVM is permitted to treat a 64-bit(位) read or write as two separate 32-bit operations. If the reads and writes occur in different threads, it is therefore possible to read a nonvolatile long and get back the high 32 bits of one value and the low 32 bits of another
14 楼
marlonyao
2011-01-19
ak121077313 写道
还是没有说明谁给instance 赋值了。。。
进锁之前是null 进锁之后又不是null
进锁之前是null 进锁之后又不是null
还是那句话,进锁之前后instance的值没有变化,只是进锁前看到的陈旧值,进锁后看到的是最新值,希望你已经明白了。
13 楼
marlonyao
2011-01-19
ak121077313 写道
有一点不明白
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
}
thread2如果通过2判断 instance == null 进入4 lz说会读到最新的值
我不明白在没有赋值的情况下为什么instance的值会变动??????
谁在这个过程中赋值了?难道是锁?
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
}
thread2如果通过2判断 instance == null 进入4 lz说会读到最新的值
我不明白在没有赋值的情况下为什么instance的值会变动??????
谁在这个过程中赋值了?难道是锁?
instance在thread2的运行过程中一直都没变,只是在没加锁访问时它有可能读不到最新值。在第(4)处加锁访问才能保证读到最新的值。
我一直避免谈得太底层,但多一种理解也是好处的。除了共享内存(shared memory)之外,每个线程都可能有自己缓存,读取线程会先从缓存中读取。对于thread2来说,在(2)处它读到缓存值的null,虽然此时主内存里已经被thread2修改过了,但thread2并不能观察到这点,但到(4)处,由于加了锁,它强制缓存失效,必须从共享内存读取最新值,即thread1修改过的值。
12 楼
marlonyao
2011-01-19
sswh 写道
怎样解释《Java并发编程设计原则与模式》中的这几句话呢?
永远只是在更新对象的成员变量时加锁?
LZ的结论是:
规则一:必须对变量的所有写和所有读同步,才能读取到该最新的数据。
引用
永远只是在更新对象的成员变量时加锁.
永远只是在访问有可能被更新的对象的成员变量时才加锁.
永远不要在调用其他对象的方法时加锁.
永远只是在访问有可能被更新的对象的成员变量时才加锁.
永远不要在调用其他对象的方法时加锁.
永远只是在更新对象的成员变量时加锁?
LZ的结论是:
marlonyao 写道
规则一:必须对变量的所有写和所有读同步,才能读取到该最新的数据。
还要看第二条,其实不矛盾。如果你看第一章的例子,他读取粒子位置时也是加了锁。
public void draw(Graphics g) { int lx, ly; synchronized(this) { lx = x; ly = y; } g.drawRect(lx, ly, 10, 10); }
另外,第三条是为了防止发生死锁。
11 楼
marlonyao
2011-01-19
neo_q 写道
[1]如果thread2执行getInstance()在(2)处发现instance为null,对getInstance()的返回结果继续调用getSomeField()将得到什么?会得到0吗?
因为在调用构造方法时,thread2做了赋值,而且是做了同步操作的,如果调用的线程本身有做同步操作或者是在同一线程那么可以得到确定的值,如果调用的线程是别的线程且未做同步操作那么值是不确定的,因为从头到尾该线程都未取得该对象的锁,因此无法保证它能获得最新的值
[2]DCL为什么要double check,能不能去掉(4)处的check?若不能,为什么?
不能去掉,因为在(2)执行的时候,如果已经进行了判断,之后另外一个线程进行了操作有可能使instance不为null
你再想想,回答得都不对。
neo_q 写道
是因为thread2在调用getPos()时,curX有0,1或2三种可能,同样curY也有0,1或2三种可能,所以getPos()可能返回[0,1], [0,2], [0,3], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]共九种可能
[0,3]应为[0,0]吧
谢谢指出,已更正。
10 楼
neo_q
2011-01-19
aabcc 写道
要说清楚这个问题,得先说明一下 JMM 啊,内存模型不摆出来,这个问题永远只能是争个你死我活...
就DLC来讲,本例中的instance的值是存在于heap上的,但是有可能thread为了操作的效率会在自己的stack上copy一份instance的副本,所以在多线程操作的时候,同步操作会导致thread将自己stack中的instance反写回heap中,然后其他的读操作,如果有同步操作才能从heap中得到最新的值,如果不同步读操作,那么有可能该thread的stack中的值还是old的
发表评论
-
startup java fast
2011-05-07 18:21 2612据我所知,有不少人鄙视java,认为它笨重而缓慢,笨重倒是事实 ... -
java nio网络编程的一点心得
2011-04-17 17:52 15104前几日用java nio写了一个tcp端口转发小工具,还颇费周 ... -
模式对话框为什么不会让界面失去响应?
2011-03-07 23:14 1965我很早就有这个疑问了,但一直懒得去弄清楚,直到最近又要开始写桌 ... -
观察到volatile效果的例子
2010-04-07 19:11 5090Java中要停止一个线 ... -
ConcurrentHashMap之实现细节
2009-03-06 19:34 14693ConcurrentHashMap是Java 5中支持高并发、 ... -
Java SE 6同步性能优化
2009-03-04 01:07 1802Java SE的每个版本都花费 ... -
令人抓狂的java程序,String可变!
2009-03-02 11:18 1164下面的程序会输出什么? public class Hell ... -
Java乱码问题
2009-02-02 20:02 14951 乱码的根源 在计 ... -
说说字符集、编码概念
2009-01-17 15:53 1549要谈编码就要先谈两个 ... -
《企业应用架构模式》介绍部分笔记
2009-01-15 20:39 1777架构 架构一般来说意味着: 从最高层将系统分解成多个部 ... -
抓取火车票程序
2009-01-15 18:13 1678在同事的建议下写了一个自动从网上抓取火车票信息的程序,抓取完之 ...
相关推荐
Java线程安全是多线程编程中的一个核心概念,尤其在服务器端开发中,理解并掌握线程安全至关重要。线程安全是指当多个线程访问一个对象时,如果这个对象的状态始终保持一致,那么我们就说这个对象是线程安全的。在...
4. 双重检查锁定(DCL,线程安全): 在懒汉式的基础上添加了同步锁,避免了线程安全问题。 ```java public class Singleton { private volatile static Singleton INSTANCE; private Singleton() {} ...
Java中双检锁/双重检查锁定(Double-Check Locking,DCL)和静态内部类是实现线程安全单例的常用方法。 5. 状态对象模式:用于在多线程中同步访问对象的状态,例如CountDownLatch、CyclicBarrier和Semaphore等并发...
然而,早期的DCL在Java早期版本中并不完全线程安全,因为Java内存模型的某些细节可能导致`instance`字段在初始化之前被非原子性地读取。 4. 安全发布 安全发布是指确保对象在完全初始化后被其他线程可见。在DCL模式...
在"Java多线程设计模式_清晰完整PDF版"文档中,你可能还会学习到如何结合实际场景选择合适的线程模型,如何优化多线程程序,以及如何调试和处理线程安全问题。这些内容对于提升Java并发编程能力非常有帮助,对于开发...
### 线程安全的单例模式详解 #### 一、单例模式简介 单例模式(Singleton Pattern)是软件开发中最常用的创建型设计模式之一,它的主要目标是确保一个类只有一个实例,并提供一个全局访问点。单例模式在很多场景下...
### Java线程内存模型的缺陷 #### Java内存模型(JMM)概述 Java作为一种高度抽象化的编程语言,致力于提供统一的内存管理模型,以便开发者能够跨平台地编写多线程程序。为此,Java引入了一个核心概念——Java内存...
3. 线程同步:Java提供了synchronized关键字、Lock接口(如ReentrantLock)以及wait()、notify()、notifyAll()等工具来保证线程安全,防止数据竞争。 二、设计模式在多线程中的应用 1. 生产者消费者模式:通过阻塞...
创建Java线程有两种主要方法:通过实现`Runnable`接口或继承`Thread`类。`Runnable`通常更灵活,因为它允许线程与其他对象共享数据,而不会遇到单继承的限制。 高并发则涉及系统能够同时处理大量请求的能力。在...
Java多线程设计模式是Java开发中不可或缺的一部分,它涉及到如何在并发环境下高效、安全地组织程序执行。本文将深入探讨Java多线程设计模式及其应用,帮助开发者理解和掌握这一重要技术。 首先,理解Java多线程的...
在Java线程中,同步是非常重要的概念,用于解决多线程环境下的数据安全问题。Java提供了多种同步机制,包括synchronized关键字、Lock接口(如ReentrantLock)以及java.util.concurrent包中的并发工具类(如Semaphore...
4. 双重检查锁定(DCL):结合了懒汉式的延迟加载和饿汉式的线程安全,既保证了线程安全,又降低了性能影响。 ```java public class Singleton { private volatile static Singleton INSTANCE; private Singleton...
单例模式是设计模式中的一种,它在...DCL单例模式在性能和线程安全之间找到了平衡;而枚举单例模式则是最安全且推荐的实现方式,适用于大多数情况。在实际开发中,开发者应根据项目特点和性能需求选择合适的单例模式。
7. **线程状态**:Java线程有五种状态,分别是新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated)。理解这些状态有助于诊断和解决多线程问题。 8. **死锁、活锁与饥饿**:多...
Java中常见的单例实现有懒汉式(线程不安全)、饿汉式(线程安全)、双重检查锁定(DCL,线程安全)等。DCL模式通过`volatile`关键字和`synchronized`关键字确保了线程安全,是推荐的多线程环境下的单例实现方式。 ...
6. **设计模式**:在Java多线程编程中,一些经典的设计模式如生产者消费者模型、线程池模式、双检锁/双重校验锁定(DCL)等,能够帮助我们构建高效、安全的并发程序。 这个"Java多线程设计模式上传文件"可能包含这些...
然而,原始的DCL在Java早期版本中存在线程安全问题,因为编译器的指令重排序可能导致非预期的结果。Java 5之后引入了volatile关键字,解决了这个问题。 2. volatile关键字: volatile确保共享变量在多线程环境中的...