多线程--volatile关键字

volatile是java中的一个关键字,用于修饰变量,主要解决多线程环境下的可见性和有序性问题。

一、volatile的作用

  • 可见性:保证一个线程对volatile变量的修改对其他线程立即可见
  • 有序性:禁止指令重排序,确保代码的执行顺序与编写顺序一致

(关于可见性和有序性请参考 多线程JMM)

二、内存屏障的类型

1、LoadLoad Barrier

  • 示例:Load1;LoadLoad;Load2,确保Load1先于Load2完成

  • 确保屏障前的读操作先于屏障后的读操作完成

    int x = a; // 普通读操作
    LoadLoadBarrier(); // 禁止重排序
    int y = volatileVar; // volatile 读操作
    

2、StoreStore Barrier

  • 示例:Store1;StoreStore;Store2,确保Store1先于Store2完成

  • 确保屏障前的写操作先于屏障后的写操作完成

    a = 1; // 普通写操作
    b = 2; // 普通写操作
    StoreStoreBarrier(); // 禁止重排序
    volatileVar = 3; // volatile 写操作
    

3、LoadStore Barrier

  • 示例:Load1;LoadStore;Store2,确保Load1先于Store2完成

  • 确保屏障前的读操作先于屏障后的写操作完成

    int y = volatileVar; // volatile 读操作
    LoadStoreBarrier(); // 禁止重排序
    b = 2; // 普通写操作
    

4、StoreLoad Barrier

  • 示例:Store1;StoreLoad;Load2,确保Store1先于Load2完成

  • 确保屏障前的写操作先于屏障后的读操作完成

  • 这是最严格的内存屏障,通常开销最大

    volatileVar = 3; // volatile 写操作
    StoreLoadBarrier(); // 强制刷新缓存
    

(这4个屏障其实从字面上就说明了顺序,先写的单词是在后写的单词前)

三、volatile的可见性原理

1、可见性原理

在多线程环境下,每个线程都有自己的工作内存(缓存),线程对变量的操作(读取、写入)都是基于工作内存的副本。如果线程A修改了共享变量的值,线程B可能无法立即看到修改后的值,这就是可见性问题。

2、volatile如何保证可见性

  • 当一个线程修改volatile变量的值时,会立即将值刷新到主内存
  • 当一个线程读取volatile变量的值时,会从主内存中重新加载到最新值

3、源码实现

volatile的可见性是通过内存屏障(Memory Barrier)实现的。内存屏障是一种CPU指令,用于控制指令的执行顺序和内存的可见性。

在JVM层面,volatile变量的读写操作会被插入内存屏障

  • 写操作:在写volatile变量前后插入StoreStore和StoreLoad屏障,确保写操作的结果对其他线程可见
  • 读操作:在读volatile变量前后插入LoadLoad和LoadStore屏障,确保读取的是最新值

4、案例

public class VolatileVisibilityExample {
    private volatile boolean flag = false;

    public void setFlag() {
        flag = true; // volatile 写操作
    }

    public void doSomething() {
        while (!flag) {
            // 等待 flag 变为 true
        }
        System.out.println("Flag is true");
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileVisibilityExample example = new VolatileVisibilityExample();

        Thread thread1 = new Thread(() -> {
            example.doSomething();
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000); // 模拟延迟
                example.setFlag();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

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

        thread1.join();
        thread2.join();
    }
}

(上面这个案例 模拟延迟1秒后修改的flag会同步到主内存,因为volatile修饰 否则工作内存无法同步)

四、volatile的有序性原理

1、指令重排序问题

为了提高性能,编译器和处理器可能会对指令进行重排序

int a = 1;
int b = 2;
boolean c=false;

如上面代码块,三个变量的赋值没有任何关联关系,那么先后顺序就没有任何问题

但是在多线程环境下,如果发生上述的指令重排问题,就很有可能导致并发问题

2、 volatile 如何禁止指令重排序

  • volatile 通过内存屏障禁止指令重排序。
  • volatile 写操作之前插入 StoreStore 屏障,确保之前的写操作先完成。
  • volatile 写操作之后插入 StoreLoad 屏障,确保写操作的结果对其他线程可见。
  • volatile 读操作之前插入 LoadLoad 屏障,确保读取的是最新值。
  • volatile 读操作之后插入 LoadStore 屏障,确保后续的读/写操作不会重排序到 volatile 读操作之前。

3、案例

public class VolatileOrderingExample {
    private int a = 0;
    private int b = 0;
    private volatile boolean flag = false;

    public void write() {
        a = 1; // 普通写操作
        b = 2; // 普通写操作
        flag = true; // volatile 写操作
    }

    public void read() {
        if (flag) {
            System.out.println("a: " + a + ", b: " + b);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileOrderingExample example = new VolatileOrderingExample();

        Thread thread1 = new Thread(() -> {
            example.write();
        });

        Thread thread2 = new Thread(() -> {
            example.read();
        });

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

        thread1.join();
        thread2.join();
    }
}

五、总结

特性 描述
可见性 通过内存屏障强制刷新 CPU 缓存,确保变量的修改对其他线程立即可见。
有序性 通过内存屏障禁止指令重排序,确保代码的执行顺序与编写顺序一致。
适用场景 状态标志、简单的可见性需求。
性能开销 synchronized 轻量,但比普通变量稍高。

你可能感兴趣的:(多线程,java,多线程)