变量是不是应该在 2 个正在运行的线程之间是 volatile 的?

Posted

技术标签:

【中文标题】变量是不是应该在 2 个正在运行的线程之间是 volatile 的?【英文标题】:Should a variable be volatile between 2 running threads?变量是否应该在 2 个正在运行的线程之间是 volatile 的? 【发布时间】:2021-05-15 16:16:37 【问题描述】:

在这种情况下,int a 是否应该是 volatile 以保证线程之间的可见性?

private volatile static int a = 0;

public static void main(String[] args) 
    
    Thread t1 = new Thread(new Runnable() 
        
        @Override
        public void run() 
            a = 10;
        
    );

    
    Thread t2 = new Thread(new Runnable() 
        
        @Override
        public void run() 
            System.out.println(a);
        
    );
    
    t1.start();
    t2.start();
    

输出

10

【问题讨论】:

1) 你知道哪个线程先启动吗? (提示:你不知道)2)volatile 大约是两个实体 - 当一些写入被其他线程看到时,这意味着happens-before 已建立...... 您能否进一步解释一下“之前发生”?如果在这种情况下需要 volatile 关键字,您并没有真正回答。 问题是这个级别很难知道基础知识并正确应用它。但在这种情况下,制作一个 volatile 就可以解决问题(正如 Eugene 已经指出的那样,它建立了发生之前的关系)。 【参考方案1】:

happens-before is clearly defined in the language specification,从阅读开始;开始吧。

然后要完全了解发生了什么,您需要知道Program order 和synchronization order 是什么。

说的很简单,看下面:

private volatile static int a = 0;
private static int b = 0;

public static void main(String[] args) 

    Thread t1 = new Thread(new Runnable() 

        @Override
        public void run() 
            b = 100;
            a = 10;
        
    );


    Thread t2 = new Thread(new Runnable() 

        @Override
        public void run() 
            if(a == 10)
                System.out.println(b);
            

        
    );

    t1.start();
    t2.start();


您拥有的唯一保证是,当且仅当t2 打印某些内容时,它将始终为100。这是因为t2看到volatile writea。发生这种情况是因为已经建立了“之前发生”,从写入线程到读取线程,并且在a = 10 之前完成的每个操作都保证对看到a10 的线程可见。

【讨论】:

因为 hb(b=100, a=10) 因为它们是按程序顺序(规则 1)在同一个线程中的操作,而 a == 10 意味着 hb(a=10, t2) ,这意味着来自规则 4 的 hb(b=100, t2) - 对吧? @daniu 是的,但当然你需要保持在JLS 的适当范围内。 here 和 here 只是我废弃它的表面。但真的很开心。【参考方案2】:

您能否进一步解释一下“之前发生”?

关于“发生在之前”的最重要的一点是,它是transitive relation。这意味着,如果 Java 语言规范 (JLS) 承诺 A“发生在”B 之前,并且承诺 B “发生在”C 之前,那么您可以推断出 A “发生在”C 之前的承诺。

JLS 表示对某个 volatile 变量的写入“发生在”随后读取同一变量之前。

嗯嗯!听起来很明显,不是吗?

但它很明显,因为 JLS 确实 为非易失性变量提供相同的保证。如果处理器 A 将值 7 写入非易失性 int,然后一段时间后处理器 B 写入 5,则 JLS 不能保证在很长一段时间后,变量的最终值将为 5。处理器 B 将看到 5 (这是一个不同的“发生在”承诺,见下文)。处理器 A 可以看到 5 或 7,任何其他处理器都可以看到 5 或 7 或变量最初具有的任何值(例如,0)。

volatile 承诺如何提供帮助

假设我们有

 volatile boolean flag = false;
 /*non-volatile*/ int i = 0;

假设线程 A 这样做:

i = 7;
flag = true;

假设线程 B 这样做:

if (flag) 
    System.out.println(i);

else 
    System.out.println("Bleah!");

线程 B 可以打印“7”,也可以打印“Bleah!”但是由于“发生在之前”的保证,我们绝对知道线程 B 永远不会打印“0”。为什么不呢?

线程 A 在设置 i = 7 之前设置 flag = true。 JLS 保证如果单个线程在执行第二条语句之前执行一条语句,则第一条语句“发生在”第二条语句之前。 (这听起来非常显而易见,但同样不应该。很多与线程有关的事情显而易见。)

线程 B 在打印 i 之前测试 flag。所以 *IF* 线程 A 之前设置了flag=true,那么我们知道i 必须等于7:传递性:i=7“发生在”flag=true 之前,并且写入到@987654336 @,如果它发生了,“发生在”阅读相同的flag

如果它发生了

数据竞争和竞争条件

要记住的最重要的事情是,当 JLS 承诺 A“发生在”B 之前时,他们并不是说 A 总是确实发生在 B 之前:他们是说您可以依赖这种传递关系。他们是说如果A确实发生在B之前,那么“发生在”A之前的所有事情也一定确实发生在B之前。

程序可以打印“Bleah!”因为没有什么能阻止线程 B 在线程 A 设置它之前测试 flag。有人称之为“数据竞赛”。两个线程正在“竞赛”,看哪个线程先到达flag,程序的结果取决于哪个线程赢得了这场竞赛。

当程序的正确性取决于数据竞争的结果时,我们中的一些人称其为“竞争条件”,这确实令人头疼。无法保证具有竞争条件的程序在您的测试期间不会做一千次正确的事情,然后在对您的客户最重要的时候做错事情。

【讨论】:

以上是关于变量是不是应该在 2 个正在运行的线程之间是 volatile 的?的主要内容,如果未能解决你的问题,请参考以下文章