java volatile 重排序的疑惑

微凉
  • 464

有如下代码

public class VolatileSortTest {
    private static  int a = 0;
 private static  int b = 0;
 private static  int c = 0;
 private static  volatile int d = 0;
 private static  int e = 0;
 private static  int f = 0;
 private static  int g = 0;
 private static  int h = 0;
 public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 500000; i++) {
            //join可以保证线程a b都执行完成之后,再继续下一次循环
 ThreadA threadA = new ThreadA();
 ThreadB threadB = new ThreadB();
 threadA.start();
 threadB.start();
 threadA.join();
 threadB.join();
 //清空数据,便于测试
 a = 0;
 b = 0;
 c = 0;
 d = 0;
 e = 0;
 f = 0;
 g = 0;
 h = 0;
 }
    }
    static class ThreadA extends Thread {
        @Override
 public void run() {
            a = 1;
 b = 1;
 c = 1;
 d = 1;
 e = 1;
 f = 1;
 g = 1;
 h = 1;
 }
    }
    static class ThreadB extends Thread {
        @Override
 public void run() {
            if (b == 1 && a == 0) {
                System.out.println("b=1");
 }
            if (c == 1 && (a == 0 || b == 0)) {
                System.out.println("c=1");
 }
            if (d == 1 && (a == 0 || b == 0 || c == 0)) {
                System.out.println("d=1");
 }
            if (e == 1 && (a == 0 || b == 0 || c == 0 )) {
                System.out.println("e=1");
 }
            if (f == 1 && (a == 0 || b == 0 || c == 0 )) {
                System.out.println("f=1");
 }
            if (g == 1 && (a == 0 || b == 0 || c == 0 )) {
                System.out.println("g=1");
 }
            if (h == 1 && (a == 0 || b == 0 || c == 0 )) {
                System.out.println("h=1");
 }
        }
    }
}

我想要测试 volatile 关键词的禁止指令重排序,但是结果很出人意外,依然会打印出一些"g=1","f=1","e=1" 的字样,已经把d 添加了关键字 volatile,在操作d的时候会添加内存屏障,为什么 g = 1 的时候,a,b,c还有存在为0的现象?

回复
阅读 697
4 个回答

volatile 应该只能保证你看不到 "d=1" ...

//对代码做了小小修改  你会发现 输出 e f g 等 但是a b c  的输出全是1  why? 看下面的字节码
static class ThreadB extends Thread {
    @Override
 public void run() {
        if (b == 1 && a == 0) {
            System.out.println("b=1"+b);
 }
        if (c == 1 && (a == 0 || b == 0)) {
            System.out.println(a+""+b+c);//验证a b c的值到底是 1 还是 0 
  System.out.println("c=1"+c);
 }
        if (d == 1 && (a == 0 || b == 0 || c == 0)) {
            System.out.println("d=1"+d);
 System.out.println(a+""+b+c);
 }
        if (e == 1 && (a == 0 || b == 0 || c == 0 )) {
            System.out.println("e=1"+e);
 System.out.println(a+""+b+c);
 }
        if (f == 1 && (a == 0 || b == 0 || c == 0 )) {
            System.out.println("f=1"+f);
 System.out.println(a+""+b+c);
 }
        if (g == 1 && (a == 0 || b == 0 || c == 0 )) {
            System.out.println("g=1"+g);
 System.out.println(a+""+b+c);
 }
        if (h == 1 && (a == 0 || b == 0 || c == 0 )) {
            System.out.println("h=1"+h);
 System.out.println(a+""+b+c);//验证a b c的值到底是 1 还是 0 
 }
    }
}

结果:

g=11
111
h=11
111
111
c=11

线程B对应的run字节码:

invokestatic #2 <org/threadpool/testtask/TestJion.access$100>~~~~
  3 iconst_1// invokestatic ?这是调用静态方法? 不应该是直接取b值吗?
  4 if_icmpne 40 (+36)
  7 invokestatic #3 <org/threadpool/testtask/TestJion.access$000>
 10 ifne 40 (+30)
 13 getstatic #4 <java/lang/System.out>
 16 new #5 <java/lang/StringBuilder>
 19 dup
 20 invokespecial #6 <java/lang/StringBuilder.<init>>
 23 ldc #7 <b=1>
 25 invokevirtual #8 <java/lang/StringBuilder.append>
 28 invokestatic #2 <org/threadpool/testtask/TestJion.access$100>
 31 invokevirtual #9 <java/lang/StringBuilder.append>
 34 invokevirtual #10 <java/lang/StringBuilder.toString>
 37 invokevirtual #11 <java/io/PrintStream.println>
 40 invokestatic #12 <org/threadpool/testtask/TestJion.access$200>
 43 iconst_1
 44 if_icmpne 125 (+81)
 47 invokestatic #3 <org/threadpool/testtask/TestJion.access$000>
 50 ifeq 59 (+9)
 53 invokestatic #2 <org/threadpool/testtask/TestJion.access$100>
 56 ifne 125 (+69)

调用截图:
image.png
猜想:

//出现abc 111 说明 volatile起作用了
//至于条件判断里为啥a==0 b==0 c==0成立 说明 volatile的作用域是在本方法体内  
//取abc的值变成执行对应的静态方法 这个方法读取的缓存行没更新过?~~~~   
//我猜测是jvm优化的原因  O(∩_∩)O
//验证正确的事情是正确的比验证错误的事情是错误的难。。。。
//还有我总感觉你这个验证有问题

后续条件debug:
image.png
中午我想了下 操作系统对于失效数据是怎么处理的 大概思路 就是交给用户自己解决 就是加锁(串行)保证操作系统得到有效数据 或者自旋cas操作

1--------------------------------------------------------
难受,越想越纠结 又从其他方面解析进行反推

public void run() {
 int aa=2;//这三兄弟是用来检验判断条件里的值
 int bb=2;//为啥不直接输出 这里面有锁 
 int cc=2;
 int dd=2;//用来检验对D的读写看AB哪个线程先完成
 if (b == 1 && a == 0) {
        System.out.println("b=1"+b);
 }
    if (c == 1 && ((aa=a) == 0 || (bb=b) == 0)) {
        System.out.println("aa:"+aa+"---  bb:"+bb+"------cc:"+cc+"dd--"+dd);
 System.out.println("a:"+a+"---  b:"+b+"------c:"+c+"d---"+d);
 System.out.println("c=1");
 }
    if ((dd=d) == 1 && ((aa=a) == 0 || (bb=b) == 0 || (cc=c) == 0)) {
        System.out.println("aa:"+aa+"---  bb:"+bb+"------cc:"+cc +"dd--"+dd);
 System.out.println("a:"+a+"---  b:"+b+"------c:"+c+"d---"+d);
 System.out.println("d=1"+d);
 }
    if (e == 1 && ((aa=a) == 0 || (bb=b) == 0 || (cc=c) == 0 )) {
        System.out.println("aa:"+aa+"---  bb:"+bb+"------cc:"+cc+"dd--"+dd);
 System.out.println("a:"+a+"---  b:"+b+"------c:"+c+"d---"+d);
 System.out.println("e=1"+e);
 }
    if (f == 1 && ((aa=a) == 0 || (bb=b) == 0 || (cc=c) == 0)) {
        System.out.println("aa:"+aa+"---  bb:"+bb+"------cc:"+cc+"dd--"+dd);
 System.out.println("a:"+a+"---  b:"+b+"------c:"+c+"d---"+d);
 System.out.println("f=1"+f);
 }
    if (g == 1 && ((aa=a) == 0 || (bb=b) == 0 || (cc=c) == 0)) {
        System.out.println("aa:"+aa+"---  bb:"+bb+"------cc:"+cc+"dd--"+dd);
 System.out.println("a:"+a+"---  b:"+b+"------c:"+c+"d---"+d);
 System.out.println("g=1"+g);
 }
    if (h == 1 && ((aa=a) == 0 || (bb=b) == 0 || (cc=c) == 0)) {
        System.out.println("aa:"+aa+"---  bb:"+bb+"------cc:"+cc+"dd--"+dd);
 System.out.println("a:"+a+"---  b:"+b+"------c:"+c+"d---"+d);
 System.out.println("h=1"+h);
 }

结果:
aa:1--- bb:0------cc:2dd--2
a:1--- b:1------c:1d---1
c=1
aa:0--- bb:2------cc:2dd--0
a:1--- b:1------c:1d---1
h=11
aa:0--- bb:2------cc:2dd--0
a:1--- b:1------c:1d---1
e=11
aa:0--- bb:2------cc:2dd--0
a:1--- b:1------c:1d---1
e=11
aa:0--- bb:2------cc:2dd--2
a:1--- b:1------c:1d---1
c=1
aa:0--- bb:2------cc:2dd--0
a:1--- b:1------c:1d---1
e=11
aa:1--- bb:0------cc:2dd--2
a:1--- b:1------c:1d---1
c=1
推论:
dd:2 先写 后渎 就是打印 C 了
dd:0 说明 先读 后写 缓存失效重读 e=1 至于a=0 我不知道操作系统是怎么处理线程工作缓存的
猜猜 ----------------~~~~ 为啥没有 dd=1 这个
还有个现象 就是 E 能成立 说明后面的也能成立 为啥只打印E
System.out.println() 是因为这个里加锁了

public void println(String x) {
    synchronized (this) {
        print(x);
 newLine();
 }
}

使用了 synchronized 上锁会做以下操作:

    1.获得同步锁;
    2.清空工作内存;
    3.从主内存拷贝对象副本到工作内存;
    4.执行代码(计算或者输出等);
    5.刷新主内存数据;
    6.释放同步锁。

这也解释了判断里为 0 输出为1 用局部变量保存是线程私有空间
还有就是 字节码调静态方法 是因为

private static  int e = 0; //私有字段

今天就到这里了
image.png
又对代码进行改进 这次应该是最终版01
h=1 d=0 指令重排
也能解释dabc缓存行问题了
可见性 如果都在同一缓存行 vt变量会附带更新这缓存行的数据

volatile关键字是可以解决禁止重排序,和可见性的问题
如果不想出现"g=1","f=1","e=1" 的字样 那么需要abcdefgh变量都加上volatile,因为其他不加volatile关键字,你改变的值还是存在本地内存中,加上volatile关键字后,变量是会立即刷入到主内存,实现可见性。

参考https://www.jianshu.com/p/b91...
volatile变量规则:对一个volatile变量的写操作happen—before后面(时间上)对该变量的读操作。
如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)

个人理解
如果线程B执行到了d = 1,但是线程A还未执行到d = 1。不满足happen—before所有规律,什么魔幻事情都能发生,此时进行了打印输出。
如果线程B执行到了d = 1,且线程A已执行了d = 1。满足happen—before。那么abcd = 1对线程B来说应该是立即可见的,此时不会打印输出。

这个问题我先关注下,等到了公司问问公司里的大佬看看。

你知道吗?

宣传栏