volatile关键字

2020-09-02  本文已影响0人  布莱安托

volatile是Java虚拟机提供的轻量级的同步机制

具有三大特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

要想讲清楚这三大特性,首先要了解JMM

JMM

JMM(Java内存模型 Java Memory Model)是一种抽象概念,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组的对象)的访问方式

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

JVM会为每个线程开辟独立的工作内存(或称为栈空间)保存线程私有数据,所有变量都保存在主内存中,线程对变量的操作必须在工作内存中完成,首先将变量从主内存拷贝至工作内存,然后对变量进行操作,完成后再将变量写回主内存,线程间无法访问对方的工作内存,线程间通信必须通过主内存来完成

JMM的三大特性:

  1. 可见性
  2. 原子性
  3. 有序性

volatile满足JMM三大特性的两点

可见性

例:

public class VolatileDemo {

    public static void main(String[] args) {

        visibility();

    }

    private static void visibility() {

        Data data = new Data();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " begin");

            // 等待3秒后更新data.num
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.add(10);
            System.out.println(Thread.currentThread().getName() + " updated num: " + data.num);
        }, "thread-1").start();

        // 主线程检测当data.num不为0时,结束循环,否则一直等待
        while (data.num == 0) {

        }

        System.out.println(Thread.currentThread().getName() + " is over, num: " + data.num);
    }

}

class Data {

    int num = 0;

    public void add(int i) {
        this.num += i;
    }

}

结果:

thread-1 begin
thread-1 updated num: 10

(main waiting)

主线程while循环没有结束,而是一直循环,可见某个线程对于共享变量的修改对于其他线程是不可见的,线程读取到的数据副本不会因其他线程修改而改变

现在我们把num变量添加volatile关键字

public class VolatileDemo {

    public static void main(String[] args) {

        visibility();

    }

    private static void visibility() {

        VolatileData data = new VolatileData();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " begin");

            // 等待3秒后更新data.num
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.add(10);
            System.out.println(Thread.currentThread().getName() + " updated num: " + data.num);
        }, "thread-1").start();

        // 主线程检测当data.num不为0时,结束循环,否则一直等待
        while (data.num == 0) {

        }

        System.out.println(Thread.currentThread().getName() + " is over, num: " + data.num);
    }

}

class VolatileData {

    volatile int num = 0;

    public void add(int i) {
        this.num += i;
    }

}

结果:

thread-1 begin
thread-1 updated num: 10
main is over, num: 10

主线程感知到了其他线程对于data.num的修改,跳出循环执行后面的语句,并且主线程中data.num的值与thread-1线程中修改后的值一致,所以说volatile关键字保证了线程间共享变量的可见性

不保证原子性

原子性表示操作的完整性,当某个线程正在对某数据进行操作的过程中,操作过程不可分割,只能操作成功或者操作失败。

例:

public class VolatileDemo {

    public static void main(String[] args) {

        nonAtomic();

    }

    private static void nonAtomic() {

        VolatileData data = new VolatileData();

        // 通过20个线程,每个线程执行1000次自增操作,共20000次自增操作
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data.increase();
                }
            }, "thread-" + i).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " final num: " + data.num);

    }

}

class VolatileData {

    volatile int num = 0;

    public void add(int i) {
        this.num += i;
    }

    public void increase() {
        num++;
    }

}

结果:

main final num: 19450

如果volatile具有原子性,那么共执行20000次的自增的结果应该为20000,所以可见volatile关键字不保证对变量操作的原子性

我们通过javap -c反编译字节码文件的到increase()方法指令如下:

public void increase();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field num:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field num:I
      10: return

我们可以看到num++操作被转化为了三个指令:

  1. getfield:获取原始值
  2. iadd:执行加1操作
  3. putfield:将修改后的值写回

假设有两个线程同时获取到了原始值,线程1被挂起,线程2执行自增并写回,然后线程1执行自增并写回,由于两个线程获取到的原始值相同,所以两个线程写回的值也相同,这就导致了两个线程的写覆盖,也就说明了操作不具有原子性

那么如何解决操作原子性问题呢?

  1. 对于volatile关键字修饰的变量操作添加synchronized关键字
  2. 使用原子变量AtomicInteger

例:

public class VolatileDemo {

    public static void main(String[] args) {

        nonAtomic();

    }

    private static void nonAtomic() {

        VolatileData data = new VolatileData();

        // 通过20个线程,每个线程执行1000次自增操作,共20000次自增操作
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data.increase();
                    data.atomicIncrease();
                }
            }, "thread-" + i).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " final num: " + data.num);
        System.out.println(Thread.currentThread().getName() + " final atomic num: " + data.atomicNum);

    }

}

class VolatileData {

    volatile int num = 0;

    AtomicInteger atomicNum = new AtomicInteger();

    public void add(int i) {
        this.num += i;
    }

    public void increase() {
        this.num++;
    }

    public void atomicIncrease() {
        this.atomicNum.getAndIncrement();
    }

}

结果:

main final num: 19801
main final atomic num: 20000

可见多个线程对于原子变量的操作具有原子性

禁止指令重排

计算机在执行程序时,为了提高性能,编译器和处理器常常对指令进行重新排序,一般会进行如下三步:

image.png

在单线程环境里面确保程序最终执行结果和代码顺序执行结果一致

处理器在进行重排序时必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

volatile实现了禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象

首先了解一个概念,内存屏障(Memory Barrier)也称内存栅栏,是一个CPU指令,它的作用有两个:

  1. 保证特定操作的执行顺序
  2. 保证某些变量的内存可见性(volatile利用该特性实现可见性)

由于编译器和处理器都能进行执行指令重排优化,如果在指令间插入一条内存屏障告诉编译器和CPU,不管什么指令都不能和这条内存屏障指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

volatile变量进行写操作时,会在写操作后加一条store屏障指令,将工作内存中的共享变量刷新回主内存

image.png

volatile变量进行读操作时,会在读操作前加一条load屏障指令,从主内存中读取共享变量

image.png

保证线程安全性

上一篇下一篇

猜你喜欢

热点阅读