Java多线程之volatile

一、是什么?

volatile关键字是Java虚拟机提供的最轻量级的线程间同步机制。

特性:

  • 可见性:当一个变量被volatile修饰时,将保证此变量对所有线程的可见性。

  • 有序性:使用volatile修饰的变量能禁止指令重排优化。

一、保证可见性

在Java多线程内存模型中,线程对某个主存中的变量进行修改时,对另一个也用到这个变量的线程来说是不可见的。这样会导致多线程的情况下,会影响程序的运行结果。

例如下图代码,即使线程1修改了a,线程2中的a还是1。

public class Demo1 {

    private static int a = 1;

    public static void main(String[] args) {

        new Thread(() -> {

            while (a == 1) {}

            System.out.println("a不等于1了....");
        }, "线程2").start();


       new Thread(() -> a = 2, "线程1").start();
    }
}

private static volatile int a = 1;

使用volatile关键字,可以保证线程间对同一个资源的可见性。

二、保证有序性

2.1 重排序?

在不影响程序最终结果的情况下,JVM可修改代码的执行顺序,以改善性能。

思考一下,下面代码的打印的结果,有没有可能a和b都是1?

     static int a = 0, b = 0, c = 0, d = 0;

    private static Set<String> result = new HashSet<>();

    public static void main(String[] args) throws InterruptedException {

        while (true) {
            a = 0;
            b = 0;
            c = 0;
            d = 0;

            Thread thread1 = new Thread(() -> {
                a = d;
                c = 1;
            }, "线程1");

            Thread thread2 = new Thread(() -> {
                b = c;
                d = 1;
            }, "线程2");

            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            result.add("a: " + a + "   b: " + b);
            System.out.println(result);
        }
    }

执行结果

在这里插入图片描述

是有可能的,就以线程1来说,a=d;c=1交换执行顺序,是不会影响线程1的执行结果的,线程2也一样。

所以当程序执行顺序如下所示时a和b都为1。
c = 1;
d = 1;
a = d;
b = c;

编译器重排序只关注单线程的指令重排,所以volatile关键字也只能保证单线程中代码前后的有序性,因为多线程之间的代码执行顺序是没有规律的,太复杂了。

源代码到最终执行的指令序列示意图
#### 2.1

2.2 使用

下面代码中如果不加volatile,有可能num=2先于b = b * 2;执行,加了volatile,就可以保证num=2b = b * 2;之后执行。

但需要注意的是,volatile只能保证用到volatile变量代码前后的有序性,不保证再往后的代码(比如println之后的)。

public static volatile int num = 0;
 
public static void main(String[] args) {

          int a = 1, b = 2;
          b = b * 2;
          num = 2;
          System.out.println(b);
          b  = b * 3;
          a = 2;
          
}
2.3 happens-before规则

编译器重排序也不是随意排序的,如果代码不符合以下规则,就可以进行重排序。

  1. 程序次序规则: 在一个线程中,前面的操作 Happens-Before 于后续的任意操作。
a = 1;
b = a;

a必须先于b执行,不然逻辑就变了。

  1. volatile变量规则: 对一个 volatile 变量的写操作 Happens-Before 于对这个 volatile 变量的读操作。

  2. 传递性规则: A Happens-Before B,B Happens-Before C,那么 A Happens-Before C。

a = 1;
b = a;
c = b;

a先于b,b先于c,a就先于c。

  1. 锁规则: 对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。

  2. 线程启动规则: Thread 对象的 start()方法先行发生于此线程的每一个动作。

  Thread thread1 = new Thread(() -> {
                a = d;
                c = 1;
            }, "线程1");
  thread1.start();

thread1.start();先于a = d;和c = 1;

  1. 线程终止规则: 和启动相似,线程中的所有操作都先行发生于对此线程的终止操作。
  2. 线程中断规则: 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  3. 对象终结规则: 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。

三、不保证原子性

下面的代码,我们预想的结果应该是4000,但执行结果是无法预测的。因为num++看似是一个操作,但它包含了三个操作。

解决办法:1. 加锁 ; 2.使用AtomicInteger(其他类型使用同包下的其他类)


private static volatile int num = 0;

public static void main(String[] args) throws InterruptedException {

    Runnable runnable = () -> {
        for (int i = 0; i < 2000; i++) {

            num++;
        }
    };

    Thread thread1 = new Thread(runnable);

    Thread thread2 = new Thread(runnable);

    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
    System.out.println(num);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吖土豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值