【问题标题】:The Java volatile Read Visibility GuaranteeJava volatile 读取可见性保证
【发布时间】:2020-11-29 06:46:08
【问题描述】:

我在阅读 JVM 在阅读 volatile 变量时提供的可见性保证时遇到了以下摘录: “当线程 A 写入 volatile 变量并且随后线程 B 读取相同的变量时,在写入 volatile 变量之前对 A 可见的所有变量的值在读取 volatile 变量后对 B 可见。”

我有一个关于 JVM 保证的问题。考虑以下一组类:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        POJO p = new POJO();
        new Th1(p).start();
        new Th2(p).start();
    }
}   
    public class Th1 extends Thread {
        private POJO p1 = null;
        public Th1(POJO obj) {
            p1 = obj;
        }
        @Override
        public void run() {
            p1.a = 10; // t = 1
            p1.b = 10; // t = 2
            p1.c = 10; // t = 5;
            System.out.println("p1.b val: " + p1.b); // t = 8
            System.out.println("Thread Th1 finished"); // t = 9
        }
    }
    
    public class Th2 extends Thread {
        private POJO p2 = null;
        public Th2(POJO obj) {
            p2 = obj;
        }
        @Override
        public void run() {
            p2.a = 30; // t = 3
            p2.b = 30; // t = 4
            int x = p2.c; // t = 6
            System.out.println("p2.b value: " + p2.b); // t = 7
        }
    }
    
    public class POJO {
        int a = 1;
        int b = 1;
        volatile int c = 1;
    }

假设 2 个线程 Th1 和 Th2 在不同的 CPU 中运行,并且它们的指令执行顺序由每行中的注释指示(在它们的运行方法中)。我的问题是: 当代码“int x = p2.c;”在 t = 6 时执行,线程 Th2 可见的变量应根据上述段落从主内存刷新。据我了解,此时主存储器将拥有来自 Th1 的所有写入。变量 p2.b 在 t = 7 打印时会显示什么值?

  • p2.b 是否会显示值 10,因为它的值是从读取 volatile 变量 p2.c 中刷新的?
  • 或者它会以某种方式保留值 30?

【问题讨论】:

    标签: multithreading jvm shared-memory volatile happens-before


    【解决方案1】:

    对于您的代码,p2.b 不保证为 10 或 30。写入是竞争条件。

    “当线程 A 写入 volatile 变量并且随后线程 B 读取同一变量时,在写入 volatile 变量之前对 A 可见的所有变量的值变得可见读取 volatile 变量后到 B。"

    不保证在将 p1.c 写入 Th1 之后完成对 p2.c 的 Th2 读取。

    对于您讨论的特定顺序,在 Th2 中读取 p2.c 将不会将 p2.b 的值恢复为 10。

    【讨论】:

    • 由于对同一变量的访问可能存在冲突,而这些变量不是由发生在边缘之前排序的,我称之为数据竞争。
    • @pveentjer 这是否意味着即使按照描述的方式执行这些行,当在 t=7 打印时变量 b 的值仍然不清楚?
    • 我不会试图推理在数据竞争中可以看到的价值;通常,程序的行为被认为是未定义的。我已经发布了一个完整的示例,显示了您发布的报价的意图。
    【解决方案2】:

    a 的写入和a 的读取之间没有发生边缘之前。由于它们是相互冲突的操作(其中至少一个是写操作)并且位于同一地址上,因此存在数据争用,因此程序行为未定义。

    我认为以下示例更好地解释了您正在寻找的行为:

        public class Test {
            public static void main(String[] args) throws InterruptedException {
                POJO p = new POJO();
                new Th1(p).start();
                new Th2(p).start();
            }
        }   
        
        public class Th1 extends Thread {
            private POJO p1 = null;
            public Th1(POJO obj) {
                p1 = obj;
            }
            @Override
            public void run() {
                a=1;
                b=1;
            }
        }
        
        public class Th2 extends Thread {
            private POJO p2 = null;
            public Th2(POJO obj) {
                p2 = obj;
            }
            @Override
            public void run() {
                if(p.b==1)println("a must be 1, a="+p2.a);
            }
        }
        
        public class POJO {
            int a = 0;
            volatile int b = 0;
        }
    

    a 的写入和b 的写入之间有一个发生在边缘之前(程序顺序规则) 在b 的写入和b 的后续读取之间有一个发生在边缘之前(易失性变量规则) 在b 的读取和a 的读取之间有一个发生在边缘之前(程序顺序规则)

    由于happens before 关系是传递的,在a 的写入和a 的读取之间存在一个发生在边缘。所以第二个线程应该会看到来自第一个线程的a=1

    【讨论】:

    • 请在发布代码之前编写可编译的代码并验证输出。 Th2 中的条件检查依赖于 Th1 中的写入,并且由于两个线程同时运行,因此它可能是 true 或 false。
    • 显然你没有理解它是如何工作的。 A 发生在正在构建写入和读取之间的边缘之前,这回答了 OP 的问题。我确信 OP 能够修复编译错误。
    • 只有在 b=1; 写入在 p.b==1 测试之前执行时,才存在先发生边缘。这受制于未知的执行时间和线程调度行为。这个例子是正确的,因为第二个线程永远不会打印与a must be 1, a=1 不同的东西,但是,在某些执行中,它可能什么也不打印。一个更好的例子是while(p.b != 1) {} println("a must be 1, a="+p2.a);,它真正保证在所有具有抢先式多任务处理的系统上打印 a=1。
    • 当然;这个例子可以改进。但在这两种情况下;如果线程看到 volatile 写入,它必须在 volatile 写入之前看到所有更改。这就是示例的意图。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-10-05
    • 1970-01-01
    • 2020-07-03
    • 1970-01-01
    • 2018-02-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多