volatile 关键字深入理解

x33g5p2x  于2022-04-10 转载在 其他  
字(3.2k)|赞(0)|评价(0)|浏览(202)

一 volatile 关键字的语义

被 volatile 修饰的实例变量或者类变量有如下两层含义:

  • 保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改 volatile 修饰的变量,另外一个线程会立即看到最新的值。
  • 禁止对指令进行重排序操作。

二 volatile 保证可见性

package concurrent.volatileDemo;

import java.util.concurrent.TimeUnit;

public class VolatileFoo {
    // init_value 的最大值
    final static int MAX = 5;
    // init_value 的初始值
    // 使用 volatile
    static volatile int init_value = 0;

    public static void main(String[] args) {
        // 启动一个 Reader 线程,当发现 local_value 和 init_value 不同时,则输出 init_value 被修改的信息
        new Thread(() -> {
            int localValue = init_value;
            while (localValue < MAX) {
                if (init_value != localValue) {
                    System.out.printf("The init_value is update to [%d]\n", init_value);
                    // 对 localValue 进行重新赋值
                    localValue = init_value;
                }
            }
        }, "Reader").start();

        // 启动 uddater 线程,主要用于对 init_value 的修改,当 local_value >= 5 时,退出
        new Thread(() -> {
            int localValue = init_value;
            while (localValue < MAX) {
                // 修改 init_value
                System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
                init_value = localValue;
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }, "Updater").start();
    }
}

Updater 线程对 init_value 变量的每一次更改都会使得 Reader 线程能够看到,其具体步骤如下。

1 Reader 线程从主内存中获取 init_value 的值为0,并且将其缓存到本地工作内存中。

2 Updater 线程将 init_value 的值在本地修改为1,然后立即刷新到主内存。

3 Reader 线程在本地工作内存中 init_value 失效。

4 由于 Reader 线程工作内存中的 init_value 失效,因此需要到主内存中重新读取 init_value 的值。

三 volatile 保证顺序性

volatile 关键值直接禁止 JVM 和处理器对 volatile 关键字修饰的指令重排序,但是对于 volatile 前后无依赖关系的指令则可以随意怎么排序。

int x = 0;
int y = 1;
volatile int z = 20;
x++;
y--;

在语句 volatile int z = 20; 之前,先执行 x 的定义还是先执行 y 的定义,并不关心,只要能够百分百地保证在执行到 z=20 的时候 x=0,y=1,同理关于 x 的自增以及 y 的自减操作都必须在 z = 20 以后才发生。

再看下面这个例子,当 initialized 增加了 volatile 的修饰,那就意味着 initialized = true; 一定在 context = loadContext(); 之后执行。

private volatile boolean initialized = false;
private Context context;
public Context load(){
    if(!initialized){
        context = loadContext();
        initialized = true; // 阻止重排序
    }
    return context;
}

四 volatile 不保证原子性

1 代码

package concurrent.volatileDemo;

import java.util.concurrent.CountDownLatch;

public class VolatileTest {
    // 使用 volatile 修饰共享资源 i
    private static volatile int i = 0;
    private static final CountDownLatch latch = new CountDownLatch(10);

    public static void inc() {
        i++;
    }

    public static void main(String[] args) {
        // 计数器为10
        CountDownLatch countDownLatch = new CountDownLatch(10);

        // 将 CountDownLatch 对象传递到线程的 run() 方法中,当每个线程执行完毕 run() 后就将计数器减1
        MyThread myThread = new MyThread(countDownLatch);
        long start = System.currentTimeMillis();
        // 创建 10 个线程,并执行
        for (int i = 0; i < 10; i++) {
            new Thread(myThread).start();
        }
        try {
            // 主线程(main)等待:等待的计数器为0;即当 CountDownLatch 中的计数器为0时,Main 线程才会继续执行。
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println(i);
    }
}
class MyThread implements Runnable {
    private CountDownLatch latch;

    public MyThread(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 1000; i++) {
                VolatileTest.inc();
            }
        } finally {
            latch.countDown(); // 每个子线程执行完毕后,触发一次 countDown(),即计数器减1
        }
    }
}

2 分析

上面这段代码创建了10个线程,每个线程执行 1000 次对共享变量 i 的自增操作,但是最终结果肯定不是 10000,而且每次运行的结果也各不相同。原因分析如下:

i++ 的操作其实是由三步组成的,具体如下:

1)从主内存中获取 i 的值,然后缓存到线程工作内存中。

2)在线程工作内存中为 i 进行加 1 的操作。

3)将 i 的最新值写入主内存中。

上面三个操作单独的每一个操作都是原子性操作,但是合起来就不是,因为在执行的过程中可能被其他线程打断,例如如下操作情况。

1)假设某个时刻 i 的值为 100,线程 A 要对变量 i 执行自增操作,首先它需要到主内存中读取 i 的值,可是此时由于 CPU 时间片调度的关系,执行权切换到线程 B,A 线程进入了 RUNNABLE 状态而不是 RUNNING 状态。

2)线程 B 同样需要从主内存中读取 i 的值,由于线程 A 没有对 i 做过任何修改操作,因此此时 B 获取到的 i 仍然是 100。

3)线程 B 工作内存中为 i 执行了加 1 操作,工作内存中变成 101,但是未刷新到主内存。

4)CPU 时间片调度又将执行权交给了线程 A,A 线程对工作线程中的 100 进行了加 1 运算,工作内存中变成 101。

5)线程 A 将 i=101 写入主内存中。

6)线程 B 将 i=101 写入主内存中。

这样两次运算实际上只对 i 进行了一次修改变化。

相关文章