volatile 无法保证原子性一个简单示例的疑问

toss2000 2016-08-05 06:02:22
在学习volatile从网上找了资料,源码如下

public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<2;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}}

文章中写道“假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,inc只增加了1。”

我的问题是:线程1在读取inc为10后被阻塞了,没有进行修改所以不会去通知其他线程,此时线程2拿到的还是10,这点可以理解。但是后来线程2修改了inc变成11后写回主内存,这下是修改了,线程1再次运行时,难道不会去主存中获取最新的值吗?按照volatile的定义,如果volatile修饰的变量发生了变化,其他线程应该去主存中拿变化后的值才对啊?
...全文
592 9 打赏 收藏 转发到动态 举报
写回复
用AI写文章
9 条回复
切换为时间正序
请发表友善的回复…
发表回复
我不是攻城狮 2020-03-27
  • 打赏
  • 举报
回复
我觉得可以从volatile的禁止指令重排去考虑。 1、线程A从主存中读取到了10这个值,线程切换为B,B执行操作,修改了主存,同时复制一份新的缓存。 2、线程B处理完,切换回线程A,A由于volatile禁止指令重排,可以保证了A已读,后续操作将有序执行,所以此时线程A处理的过程完全和线程B一致。
BaldBear 2020-03-18
  • 打赏
  • 举报
回复
当对使用了volatile关键字修饰的变量进行写操作(赋值)时,JVM会自动在赋值指令后生成一个LOCK指令,从而形成内存屏障,强制地将该变量从该线程的工作内存中写入到主内存中,并且这一过程是原子性的。而其他线程若持有该变量,则会通过总线嗅探机制和缓存一致性协议来更新这个变量在各自工作内存中的值。
mlmnbjljsh 2019-09-02
  • 打赏
  • 举报
回复
引用 6 楼 wangbiao007 的回复:
[quote=引用 5 楼 NewMoons 的回复:] 楼主,原贴说的很清楚了,volatile 只保证【读的可见性】。楼上解释的也不错。 也就是对于一段非原子操作的代码(inc++不是原子操作分为读inc,加inc,回写inc)volatile 只保证线程在获取共享变量的时候会根据通知去从工作内存(即缓存)还是从主存获取。阻塞之后虽然主存的inc已经被改写了,inc++这个线程操作已经做了读取inc的子操作,它是不会知道主存的inc已经被改写了(再说一遍,volatile 只保证【读的可见性】),于是悲剧发生了。 你纠结于既然volatile 保证了读的可见性,为嘛不好人做到底,谁改了就通知大家重新操作嘛。 从现实中想想,如果一个大型协作的工程,一个小分支工程改了点东西,其它已经做了很多工序的子工程都要重新做,是不是更悲剧?所以说,真正想保证数据的同步,还是得用同步锁。要想保证数据一致性,只能一个个来,别想偷懒加塞! 那volatile 到底用在什么场合(一般来说用于监控线程,比如用一个线程监视另一个循环线程,通过标识变量的真假来决定循环是否一直执行下去。),原贴有具体例子已经说得很清楚了,我这就不重复了。
关于可见性,先看一段代码,假如线程1先执行,线程2后执行: //线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true; 这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。 下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。 那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。 但是用volatile修饰之后就变得不一样了: 第一:使用volatile关键字会强制将修改的值立即写入主存; 第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效); 第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。 那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。 那么线程1读取到的就是最新的正确的值。 那这个例子中线程1先将stop=flase读取到了工作内存中,然后去执行循环操作,线程2将stop=true写入到主存后,为什么线程1的工作内存中stop=false会变成无效的? 请大神解答啊 [/quote]我也很疑惑,这个回答也是我自己的看法,不懂对不对:假定一个共享变量a被volatile修饰,线程1在主存中将这个变量读取到缓存中,然后阻塞;线程2修改了这个变量,此时,线程1中的缓存失效,但是: 1、变量a自增时,虽然缓存失效,但是此时cpu执行的是自增操作,不会再去缓存中读取,只管自增; 2、变量a如果是stop(就是你有疑问的这段代码),while(!stop) 这段代码会让cpu再去缓存中读一次stop的值,因为缓存失效,所以会读出最新的值;这是volatile的作用就显现出来; 不懂这个解释对不对,有没有大神指正一下的,谢谢!
wangbiao007 2017-02-22
  • 打赏
  • 举报
回复
引用 5 楼 NewMoons 的回复:
楼主,原贴说的很清楚了,volatile 只保证【读的可见性】。楼上解释的也不错。 也就是对于一段非原子操作的代码(inc++不是原子操作分为读inc,加inc,回写inc)volatile 只保证线程在获取共享变量的时候会根据通知去从工作内存(即缓存)还是从主存获取。阻塞之后虽然主存的inc已经被改写了,inc++这个线程操作已经做了读取inc的子操作,它是不会知道主存的inc已经被改写了(再说一遍,volatile 只保证【读的可见性】),于是悲剧发生了。 你纠结于既然volatile 保证了读的可见性,为嘛不好人做到底,谁改了就通知大家重新操作嘛。 从现实中想想,如果一个大型协作的工程,一个小分支工程改了点东西,其它已经做了很多工序的子工程都要重新做,是不是更悲剧?所以说,真正想保证数据的同步,还是得用同步锁。要想保证数据一致性,只能一个个来,别想偷懒加塞! 那volatile 到底用在什么场合(一般来说用于监控线程,比如用一个线程监视另一个循环线程,通过标识变量的真假来决定循环是否一直执行下去。),原贴有具体例子已经说得很清楚了,我这就不重复了。
关于可见性,先看一段代码,假如线程1先执行,线程2后执行: //线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true; 这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。 下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。 那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。 但是用volatile修饰之后就变得不一样了: 第一:使用volatile关键字会强制将修改的值立即写入主存; 第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效); 第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。 那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。 那么线程1读取到的就是最新的正确的值。 那这个例子中线程1先将stop=flase读取到了工作内存中,然后去执行循环操作,线程2将stop=true写入到主存后,为什么线程1的工作内存中stop=false会变成无效的? 请大神解答啊
NewMoons 2016-08-08
  • 打赏
  • 举报
回复
楼主,原贴说的很清楚了,volatile 只保证【读的可见性】。楼上解释的也不错。 也就是对于一段非原子操作的代码(inc++不是原子操作分为读inc,加inc,回写inc)volatile 只保证线程在获取共享变量的时候会根据通知去从工作内存(即缓存)还是从主存获取。阻塞之后虽然主存的inc已经被改写了,inc++这个线程操作已经做了读取inc的子操作,它是不会知道主存的inc已经被改写了(再说一遍,volatile 只保证【读的可见性】),于是悲剧发生了。 你纠结于既然volatile 保证了读的可见性,为嘛不好人做到底,谁改了就通知大家重新操作嘛。 从现实中想想,如果一个大型协作的工程,一个小分支工程改了点东西,其它已经做了很多工序的子工程都要重新做,是不是更悲剧?所以说,真正想保证数据的同步,还是得用同步锁。要想保证数据一致性,只能一个个来,别想偷懒加塞! 那volatile 到底用在什么场合(一般来说用于监控线程,比如用一个线程监视另一个循环线程,通过标识变量的真假来决定循环是否一直执行下去。),原贴有具体例子已经说得很清楚了,我这就不重复了。
zw0283 2016-08-08
  • 打赏
  • 举报
回复
小弟我发表一下自己的看法,可能不对,有错误的话希望能指正。 首先,以题主的问题做背景,我的想法是这样的: 1、线程A从主存中读取到了10这个值,然后没来得及操作,线程B就开始操作了。 2、线程B完成一系列的操作之后,将11写回主存。 3、线程A继续,但是有一个问题是,自增操作是分为3步的。读取,增加1,写入。现在读取操作完成了,所以不会再次读取,而是从第二部增加1开始。 4、我们假设有如下情况: 线程A:其他操作1,其他操作2,读取值,加1,写入值,其他操作3.。。。。 线程B:读取值,加1,写入值 如果线程A在“ 其他操作2,读取值 “ 这个操作中被线程B打断了。线程B在操作完之后,线程A就应该可以读取到线程B更改的值了。 不知道我表述清楚没。。。反正我觉得根源问题就在于自增操作不是原子性的。。
toss2000 2016-08-08
  • 打赏
  • 举报
回复
自己顶一下。
toss2000 2016-08-07
  • 打赏
  • 举报
回复
谢谢,newmoons,我也是通过这篇文章学习的,然后产生了这样的问题,也有好多读者问了同样的问题,只是作者没有解答,所以不得以才来这里问的
NewMoons 2016-08-06
  • 打赏
  • 举报
回复
这个一两句话真难说清楚,不过我觉得这篇文章不错,推荐给你,可能有些帮助。 http://www.cnblogs.com/dolphin0520/p/3920373.html

62,614

社区成员

发帖
与我相关
我的任务
社区描述
Java 2 Standard Edition
社区管理员
  • Java SE
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧