AnthonyZero's Bolg

Java并发-理解Volatile

定义

Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,可见性的意思是当一个线程修改了一个共享变量时,另一个线程能读到这个修改的值。

共享变量(多个线程访问的变量),volatile一般修饰类的成员变量、 类的静态成员变量

可见性

通过 volatile 实现了缓存一致性: 当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
volatile 的可见性只针对当CPU从主内存中加载共享变量的时候。但是当线程A、B同时加载了共享变量i,如果线程A先加载了i,在A将i写入主内存之前,B加载了i,B加载的i仍然是主内存中i的初始值。

有序性

在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,但是会影响到多线程并发执行的正确性。
volatile 关键字可以禁止指令重新排序,可以保证一定的有序性。

int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3

理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3。
volatile 修饰的变量的有序性有两层含义:

  1. 所有在 volatile 修饰的变量写操作之前的写操作,将会对随后该 volatile 修饰的变量读操作之后的语句可见。

  2. 禁止 JVM 重排序:volatile 修饰的变量的读写指令不能和其前后的任何指令重排序.

一个经典的使用场景就是双重懒加载的单例模式了

非原子性

volatile不能保证原子性,数据一致性,首先,我们要知道保证一致性要满足三个条件:原子性,有序性,可见性

  • 原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行。

原子操作: 对于int a = b + 1; 处理器在处理代码的时候,需要处理以下三个操作:
1.从内存中读取b的值
2.进行a = b + 1这个运算
3.把a的值写回到内存中。
而这三个操作处理器是不一定就会连续执行的,有可能执行了第一个操作之后,处理器就跑去执行别的操作的。(当前线程就是阻塞的)


举例:线程A对变量 i(初始值为0)进行自增操作,线程A先读取了变量 i 的原始值,然后线程A被阻塞了(获取值、自增、赋值的操作并不能保证能同时完成);
然后线程B对变量进行自增操作,线程B也去读取变量 i 的原始值,由于线程A只是对变量 i 进行读取操作,而没有对变量进行修改(Write)操作,所以不会导致线程B的工作内存中缓存变量 i 的缓存行无效,也不会导致主存中的值刷新,所以线程2会直接去主存读取 i 的值,发现 i 的值是0,然后进行加1操作,并把1写入工作内存,最后写入主存。
然后线程A接着进行加1操作,由于已经读取了 i 的值,注意此时在线程A的工作内存中 i 的值仍然为0,所以线程A对 i 进行加1操作后 i 的值为1,然后将1写入工作内存,最后写入主存。
最后两个线程分别进行了一次自增操作后, i 只增加了1。不是预想的结果2

public class Counter {

    private AtomicInteger atomicInteger = new AtomicInteger(0);
    private volatile int i = 0;

    public static void main(String[] args) {
        final Counter counter = new Counter();
        List<Thread> list = new ArrayList<>();
        for (int i =0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        counter.safeCount();
                        counter.unsafeCount();
                    }
                }
            }));
        }
        for (Thread thread : list) {
            thread.start();
        }
        for (Thread r : list) {
            try {
                r.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(counter.i);
        System.out.println(counter.atomicInteger.get());
    }

    private void safeCount() {
        for (;;){
            int i = atomicInteger.get();
            boolean flag = atomicInteger.compareAndSet(i, ++i);
            if (flag) {
                break;
            }
        }
    }

    private void unsafeCount() {
        i++;
    }
}

上面例子可以使用 synchronized (unsafeCount方法) 或者是锁的方式来保证原子性,还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性

原理浅析

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令。Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏)
Lock前缀指令在多核处理器中会引发两件事:
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

小结

  • volatile 保证可见性,有序性,不保证原子性,不能解决并发计算问题
  • 满足以下两个条件的情况下,volatile就能保证变量的线程安全问题:1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值 2.变量不需要与其他状态变量共同参与不变约束。