当不使用volatile时,它仍然可以看到其他线程发出的更改

  • 本文关键字:线程 其他 volatile java jvm
  • 更新时间 :
  • 英文 :

public class VisibleDemo {
private boolean flag;
public VisibleDemo setFlag(boolean flag) {
this.flag = flag;
return this;
}
public static void main(String[] args) throws InterruptedException {
VisibleDemo t = new VisibleDemo();
new Thread(()->{
long l = System.currentTimeMillis();
while (true) {
if (System.currentTimeMillis() - l > 600) {
break;
}
}
t.setFlag(true);
}).start();
new Thread(()->{
long l = System.currentTimeMillis();
while (true) {
if (System.currentTimeMillis() - l > 500) {
break;
}
}
while (!t.flag) {
//                if (System.currentTimeMillis() - l > 598) {
//
//                }
}
System.out.println("end");
}).start();
}
}

如果它不具有以下代码;结束";。

if (System.currentTimeMillis() - l > 598) {
}

如果它具有这些代码;结束";。有时它不会显示出来。

  1. 当小于598或没有这些代码时,就像使用550一样,它不会显示";结束">
  2. 当是598时;结束">
  3. 当大于598时;结束";每次

注意事项:

  • 598在我的电脑上,可能是你的电脑是另一个号码
  • 标志不是用volatile,为什么可以知道最新的值

第一:我想知道为什么?

第二:我需要帮助,

我想知道场景:jvm线程的工作缓存何时刷新到主存中。

操作系统:windows 10

java:jdk8u231

您的代码正在遭受数据竞争,这就是它行为不可靠的原因。

JMM是根据先发生后发生的关系来定义的。因此,如果你有两个动作A和B,而A发生在B之前,那么B应该看到A和A之前的一切。非常重要的是要明白,之前发生的事情并不意味着之前发生(所以根据物理时间排序),反之亦然。

同时访问"flag"字段;一个线程正在读取它,而另一个线程在写入它。在JMM术语中,这被称为冲突访问。

只要使用某种形式的同步,冲突访问就可以,因为同步会在边缘之前发生。但是,由于"flag"访问是纯加载/存储,因此没有同步,因此,在边缘发生之前不会对加载和存储进行排序。不按先发生后边缘排序的冲突访问被称为数据竞赛,这就是您所面临的问题。

当出现数据竞赛时;有趣的事情可能会发生,但它不会导致未定义的行为,就像在C++下可能发生的那样(未定义行为可以有效地导致任何可能的结果,包括崩溃和超级奇怪的行为)。所以load仍然需要看到一个写入的值,而不能看到凭空产生的值。

如果我们查看您的代码:

while (!t.flag) {
...
}

因为标志字段在循环中没有更新,只是一个普通加载,所以编译器可以将此代码优化为:

if(!t.flag){
while(true){...}
}

这种特殊的优化称为循环提升(或循环不变代码运动)。

这就解释了为什么循环不需要完成。

为什么在访问System.currentTimeMillis时它会完成?因为你很幸运;显然,这阻止了JIT应用上述优化。但请记住,System.currentTimeMillis没有任何形式的同步语义,因此不会在边缘之前引发事件。

如何修复代码?

修复代码的最简单方法是使"flag"不稳定,或者访问同步块的读/写。如果你想成为真正的硬核:使用VarHandle获取/设置不透明。官方称,这仍然是一场数据竞赛,因为不透明不会在边缘之前进行标记,但它会阻止编译器优化加载/存储。这是一场良性的数据竞赛。主要优点是性能稍好,因为它不会阻止周围负载/存储的重新排序。

我想知道场景:jvm线程的工作缓存何时刷新到主内存。

这是一个谬论。现代CPU上的缓存总是一致的;这是由像MESI这样的高速缓存一致性协议来处理的。对于每个易失性读/写,写入主内存的速度将非常慢。有关更多信息,请参阅以下优秀的帖子。如果你想了解更多关于缓存一致性和内存排序的信息,请查看这本可以免费下载的优秀书籍。

我想知道场景:jvm线程的工作缓存何时刷新到主内存。

当泰勒·斯威夫特在你的音乐播放器上播放时,它将是598,除非是星期二,否则它将是599。

不,真的。这太武断了。JVM规范赋予JVM在代码没有得到适当保护的情况下出于任何原因得出任何旧数字的权利。

问题在于JVM的多样性。有一个疯狂的组合爆炸:

  • 大约有8个OS给予或接受
  • 大约有20条不同的"芯片线",具有不同的流水线行为
  • 这些芯片可以处于各种缓解模式,以缓解像Spectre这样的攻击。让我们称之为3
  • 大约有8个不同的主要JVM供应商
  • 它们有大约10个不同的版本(java 8、java 9、java 10、java 11等)

这给了我们大约384000种不同的组合。

JMM(Java内存模型)的目的是从JVM实现中移除手铐。JVM实现正在寻找这种最佳情况:

  • 它希望能够自由地使用CPU用来尽可能快地运行代码的各种技巧。例如,它希望自由度能够"重新排序"(给定a(); b(),先运行b(),然后运行a()。这是可以的,如果a和b完全独立,并且不以任何方式看待彼此(修改)。它之所以要这样做,是因为CPU是管道:即使处理一条指令实际上也是一个由许多独立步骤组成的链,而"解析指令"步骤在完成另一条指令的那一刻就可以破解它,即使该指令仍在由管道的其余部分处理。事实上,CPU可以有4个独立的"指令解析器单元",它们可以并行解析4条指令。这是NOT多个核心所做的并行:这是一个将并行解析4条连续指令的单个核心,因为解析指令比运行它们稍慢。例如但这只是Z-系列的英特尔芯片。这就是重点。如果java规范的内存模型表明JVM根本不能使用这些东西,那么这就意味着在特定的英特尔芯片上的JVM运行得很慢。我们不想那样。

  • 尽管如此,内存模型规则不能优先于赋予JVM重新排序和做各种疯狂事情的权利,以至于无法为JVM编写可靠的代码。想象一下,java lang规范说JVM可以在任何时候对一个方法中的任何两条指令重新排序,即使这两条指令涉及同一个字段。这对JVM工程师来说太棒了,他们可以随时优化代码,以最佳方式重新排序。但是编写java代码是不可能的。

因此,达成了平衡。该余额采用以下形式:

  • JMM为您提供了特定的规则-这些规则的形式为:;如果你做X,那么JVM保证Y">
  • 但仅此而已。特别是,关于如果你做而不是做X会发生什么,没有任何内容。你所知道的是,那么Y是不保证的。但"不保证"并不意味着:绝对不会发生

下面是一个例子:

class Data {
static int a = 0;
static int b = 0;
}
class Thread1 extends Thread {
public void run() {
Data.a = 5;
Data.b = 10;
}
}
class Thread2 extends Thread {
public void run() {
int a = Data.a;
int b = Data.b;
System.out.println(a);
System.out.println(b);
}
}
class Main {
public static void main(String[] args) {
new Thread1().start();
new Thread2().start();
}
}

此代码:

  • 生成两个字段,从0和0开始
  • 运行一个线程,先将a设置为5,然后将b设置为10
  • 启动第二个线程,将这两个字段读取到本地变量中,然后打印这些字段

JVM规范规定JVM对有效

  • 打印0/0
  • 打印5/0
  • 打印0/10
  • 打印5/10

但是JVM打印"20/20"或"10/5"是不合法的。

让我们放大0/10的情况,因为这非常奇怪——JVM怎么可能做到这一点?好吧,重新排序!

JVM是否会打印0/10?在JVM供应商和版本+架构+操作系统+月球阶段的一些组合中,是的。大多数情况下,不会。曾经尽管如此,想象一下你写了这个代码,你依赖0/10从未发生过,你测试了你的代码,你验证了事实上,即使运行了一百万次测试,它也从未发生过。你把它送到生产服务器,它运行了一周,然后就在你给真正重要的潜在客户演示的时候,一切都失控了:你的应用程序坏了,因为0/10的情况不时发生。

您向JVM供应商提交了一个错误。他们将其关闭为"预期行为-wontfix">这真的会发生,因为这确实是预期行为_如果你写的代码依赖于JMM不能保证的真实性,那么你就写了一个错误,即使在这个特定的日子里在你的特定硬件上,你现在也不可能让这个错误发生

这意味着一个简单而令人讨厌的结论是唯一正确的结论:你不能测试这些东西

所以,如果你坚持这样一条规则,如果没有测试,你就不知道你的代码是否有效,你猜怎么着您永远无法知道您的代码是否正常。曾经

然后得出这样的结论:你不想写任何这样的代码。

这听起来很疯狂(你怎么能不写任何多核的东西呢?)但它并不像你想象的那么疯狂。只有当两个线程依赖于某些进程内操作的相对顺序时,才会出现这种情况。例如,如果两个线程都在访问同一实例的同一字段。仅仅不要那样做。

这比你想象的要容易:如果线程之间的所有"通信"都通过数据库进行,并且你知道如何在数据库中使用事务,瞧。或者使用像RabbitMQ这样的消息总线服务。

如果对于某项工作,你真的必须编写线程相互交互的多线程代码,不要射杀信使:不可能测试你做得对。所以写得非常小心。

第二个结论是,JMM没有解释事情是如何工作或发生了什么。它只是说:如果你遵守这些规则,我向你保证这会发生。如果你不遵守这些规则,任何事情都可能发生。JVM可以自由地进行各种疯狂的恶作剧,而本文档和任何其他文档都不会列举可能发生的所有疯狂的事情。毕竟,至少有38400种不同的组合,试图记录所有38400种是疯狂的!

那么,核心规则是什么?

核心规则是所谓的先发生后关系。基本规则很简单:

  • 建立H-B关系有多种方法。这样的关系总是在两行代码之间。从H-B的角度来看,两行代码可能是不相关的。或者,规则规定A行"发生在"B行之前
  • 如果并且仅当规则声明了这一点,那么就不可能在B行观察到宇宙的状态(整个JVM中所有实例的所有字段的值),就像在a行运行之前一样

就是这样。例如,如果第A行"发生在"第B行之前,但第B行没有试图见证A所做的任何字段更改,那么JVM仍然可以重新排序,并让B在A之前运行。关键是,这应该无关紧要-你没有观察到,那么为什么重要呢?

我们可以通过设置H-B来"修复"我们奇怪的0/0/5/10问题:如果"获取静态字段值并将其保存到本地a/bvars"代码发生在thread1设置它之后,那么我们可以确保代码将始终打印5/10,JMM保证意味着JVM不会打印损坏的内容。

H-B也是可传递的(如果HB(A,B)为真,并且HB(B,C)为真的,则HB(A、C)也是真的)。

你是如何设置HB的?

  • 如果按照通常对事物运行方式的理解,行B将在行A之后运行,并且两者都由同一线程HB(A,B)运行。这是显而易见的:如果您只写x(); y();,那么y无法观察到状态,因为它在x运行之前是
  • HB(thread.start(),X),其中X是起始线程中的第一行
  • HB(EndS,StartS),其中EndS是对象ref Z上同步块的退出,StartS是稍后进入同步块(也在ref Z上)的另一个线程
  • HB(V,V),其中V是"访问挥发性变量Z",但很难知道HB与挥发性物质的关系

还有一些更奇特的方式。构造函数和它们初始化的最终变量也有一个单独的HB关系,但通常这一关系很容易理解(一旦构造函数返回,它初始化的任何最终字段都是明确设置的,不能被观察到没有设置,即使在其他情况下还没有建立实际的HB关系。这只适用于final字段)。

这就解释了为什么你会观察到奇怪的价值观。这也解释了为什么"我想知道JVM线程何时会刷新到主内存/从主内存刷新"的问题是不可回答的:因为java内存模型规范和java虚拟机规范有意且具体地没有承诺如何工作。一个JVM可以以一种方式工作,另一个JVM则可以完全不同。

我开始对扮演泰勒·斯威夫特开玩笑的原因是:CPU有核心,而核心是有限的。现代计算机,尤其是台式机,正在同时做成千上万的事情,因此将一直在核心中旋转应用程序。字段更新是否"刷新"到主内存(注意:这是危险的想法-DOCS实际上并没有强制JVMS可以在这些术语中被理解!)可能取决于它是否从核心中旋转出来。这反过来可能取决于你的音乐播放器处理一个特定的压缩音乐文件,该文件需要更多的内核来解压缩下一个块,以便它可以在音频缓冲区中排队。

因此,这不是开玩笑的,你在音乐播放器上播放的歌曲实际上可以改变你得到的数字。因此,为什么你必须放弃:你不能枚举"如果我的计算机处于这种状态,那么这个代码将始终产生Y数"。你必须列举数十亿个州。不可能的

最新更新