volatile 关键字

内容分享7天前发布
0 0 0

在多线程的环境下,需要考虑代码的原子性、可见性、有序性。

原子性



public class Demo {
 
    public static int count;
 
    static void add() {
        count += 2;
    }
 
    static void subtract() {
        count -= 2;
    }
 
    public static void main(String[] args) throws InterruptedException {
 
        Thread thread1 = new Thread(() -> {
            add();
        });
 
        Thread thread2 = new Thread(() -> {
            subtract();
        });
 
        thread1.start();
        thread2.start();
 
        thread1.join();
        thread2.join();
 
        System.out.println("count: " + count);
    }
}

在上面代码的运行结果中,大部分的情况下 count 的值为 0,但是少部分会出现 2 或 -2,这就是由于 count += 2 和 count -= 2 这两段代码不是原子的。

count += 2 对应的反编译结果如下:



         0: getstatic
         3: iconst_2
         4: iadd
         5: putstatic 

count -= 2 对应的反编译结果如下:



         0: getstatic
         3: iconst_2
         4: isub
         5: putstatic

这段指令的执行顺序可能如下所示:

volatile 关键字

我们可以看到,subtract 的运行在 add 代码中间,这就导致 subtract 运行结束后 count 的值为 -2,但是 add 读取一开始读取的 count 的值为 0,并在后续运行时一直按照 0 进行运行,就造成 add 运行结束后 count 的值为 2,将 subtract 的结果覆盖了。

若我们对 count 添加了 volatile 关键字后,也不能解决上面的问题,因为 volatile 并不能保证代码的原子性,这里可以使用加锁保证原子性,代码如下:



public class Demo {
 
    public static int count;
    
    public static final Object object = new Object();
 
    static void add() {
        count += 2;
    }
 
    static void subtract() {
        count -= 2;
    }
 
    public static void main(String[] args) throws InterruptedException {
 
        Thread thread1 = new Thread(() -> {
            synchronized (object) {
                add();
            }
        });
 
        Thread thread2 = new Thread(() -> {
            synchronized (object) {
                subtract();
            }
        });
 
        thread1.start();
        thread2.start();
 
        thread1.join();
        thread2.join();
 
        System.out.println("count: " + count);
    }
}

可见性



public class Demo {
 
    public static boolean flag;
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
 
            flag = true;
        });
 
        thread.start();
 
        method();
    }
 
    public static void method() {
        int count = 0;
 
        while (!flag) {
            count++;
        }
 
        System.out.println("while stop");
    }
}

在上面的代码中,while 循环先运行,thread 线程休眠 1 秒后运行,当 flag 被改为 true 时,while 循环就会停止运行,代码运行结果如下:

volatile 关键字

可以看到,虽然此时 flag 已经变为 true,但是 while 循环依旧没有停止。

这是因为,while 循环一秒就可以运行接近百万次,但是这百万次需要从内存中读取 flag 的值,虽然一两次很快,但是累积起来就会有一定的时间。这时 JIT 就会对代码进行优化,JIT 发现在这百万次运行中,flag 的值都是 false,那么就会将 flag 直接替换为 false,导致即使 thread 线程将 flag 替换为 true,while 循环也无法感知到。

这时当我们将 thread 的休眠时间缩短为 1ms,代码的运行结果如下:

volatile 关键字

这里可以看见,while 循环停止了。这是因为在这 1ms 中,while 循环的执行次数少,JIT 不会对这段代码进行优化,当 flag 被 thread 改为 true 时,循环就会停止。

但是,缩短时间,总归不是一个好的解决办法,在这里有两个比较好的解决办法:

禁止 JIT 优化:在执行代码前添加 -Xint 参数,以禁止 JIT 优化,操作如下:
volatile 关键字
但是禁止 JIT 优化后,就会降低一定的代码运行效率使用 volatile 关键字:将 flag 使用 volatile 修饰,这样就能保证变量的可见性

有序性



public class Demo {
 
    public static int x;
    public static int y;
 
    public static int a;
    public static int b;
 
    public static void method1() {
        x = 1;  //1
        y = 2;  //2
    }
 
    public static void method2() {
        a = y;  //3
        b = x;  //4
    }
 
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            method1();
        });
 
        Thread thread2 = new Thread(() -> {
            method2();
        });
 
        thread1.start();
        thread2.start();
 
        System.out.println("a: " + a);
        System.out.println("b: " + b);
    }
}

在上面的代码中,a 和 b 的值可能出现下面几种情况:

a = 0, b = 0,代码运行顺序为:3 -> 4 -> 1 -> 2a = 0, b = 1,代码运行顺序为:1 -> 3 -> 4 -> 2a = 2, b = 1,代码运行顺序为:1 -> 2 -> 3 -> 4

但是会不会出现 a = 2, b = 0 的情况呢?

答案是可能的。这是由于由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致,让 2 跑到了 1 之前运行,运行顺序为:2 -> 3 -> 4 -> 1。

对于这种情况,可以使用 volatile 关键字修饰 y 变量,这样就能预防上面的情况。

但是 volatile 修饰的变量也是有讲究的,若让 volatile 修饰 x,就不会起到作用。

所以,更好的方式是加锁,让 thread1 先运行,再让 thread2 运行。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...