Volatile关键字

概念

volatile是Java虚拟机提供的轻量级的同步机制。
volatile能保证并发编程的两个问题:可见性和有序性,但是不能保证原子性.

可见性

先来看下列代码:
若共享变量flag未增加volatile修饰,则当线程2执行完后,线程1不能立刻感知到flag共享变量已经被修改.(参考JMM模型)

public class VolatileTest {
	// 共享变量
    private static  boolean flag = false;

    public static void main(String[] args) {
    	
        Thread t1 = new Thread(() -> {
            while (!flag) {
			// 空循环
            }
            System.out.println("done");
        });
		
        Thread t2 = new Thread(() -> {
        	// 修改共享变量flag的值
            flag = true;
            System.out.println("changed flag");
        });
		// 线程1执行
        t1.start();
        // 主线程睡眠200ms
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 线程2执行
        t2.start();
    }
}

然而只要flag变量使用了volatile修饰,线程1则可以立刻感知到并且停止空循环.

private static volatile  boolean flag = false;
...

缓存一致性原则(MESI)

volatile之所以能保证共享变量的可见性,底层便是通过MESI实现的.

// MESI
MESI实际是值变量缓存行的状态.
M  已修改状态 (加锁成功)
E  独占状态 (一个cpu读到)
S  共享状态(多个cpu读到)
I  失效状态 (收到其他cpu的消息)

首先让我们先来看上述代码的执行步骤,如下图:
在这里插入图片描述

// 大致流程
X 代表flag所在缓存行的状态.
1.线程1从主内存中将flag=false加载到自己的工作内存  ->X=E
2.线程1从自己的工作内存中拿到flag然后使用(空循环) ->X=E
3.线程2从主内存中将flag=false加载到自己的工作内存 ->X=S
4.线程2从自己的工作内存拿到flag=false,并且修改了flag=true -> X=S
5.然后线程2通过总线,将flag的最新值写回到主内存. -> X=M
6.这时线程1通过总线嗅探机制,感知到了flag已经被修改,所以就将自己工作内存中的flag变量失效,重新去主内存中读取最新值. -> X=I

// 总结
当共享变量被volatile修饰的时候,底层汇编会给这个变量加上lock前缀,当多线程场景下,变量被多个线程加载修改时,会通过MESI机制,总线嗅探立刻被其他线程感知到。

有序性

volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象.

指令重排序

java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

// as-if-serial
不管怎么重排序单线程程序的执行结果不能被改变。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
// happens-before
1.程序次序规则:一个线程内的操作按照程序代码的书写顺序执行.
2.监视器锁规则:对一个锁的解锁操作 happens-before 于后续对同一个锁的加锁操作。
3.volatile变量规则:对一个 volatile 变量的写操作 happens-before 于后续对这个变量的读操作。
4.传递性:如果 A happens-before B,且 B happens-before C,则 A happens-before C
// 下列代码为单例模式double check,LazySingleton的实例化需要经过
public class DCLTest {
    private static Object instance = null;

    private static Object getSingleInstanceObj() {
        if (instance == null) {
            // 实例化对象  加锁
            synchronized (DCLTest.class) {
                if (instance == null) {
                    instance = new Object();
                }
            }
        }
        // 返回对象
        return instance;
    }

}

上述代码在并发情况下存在问题.

// 创建一个对象的过程涉及到多个底层步骤,按如下顺序:
instance = new Object()
// 1.给Object分配内存
在堆内存中分配一块空间,用于存储 Object 对象的实例
// 2.初始化对象Object对象进行初始化,包括成员变量的默认值或指定的初始值
// 3.将引用赋值给instance
将刚刚创建的Object对象的引用赋值给instance变量。这一步在内存中完成,确保instance引用指向了刚刚分配的内存空间。

// 由于指令重排序的存在,这三个步骤的执行顺序可能会发生变化
// 在多线程环境下,如果没有适当的同步机制,可能导致其他线程在 instance 还没有完全初始化之前就访问到了它,从而产生不正确的结果

内存屏障

内存屏障的主要作用是防止编译器和处理器在不改变程序的语义的前提下对指令进行重排序。

// Load Barrier(加载屏障):
保证在加载指令之前的所有读操作都完成,防止加载指令把后面的读指令提前执行。
// Store Barrier(存储屏障):
保证在存储指令之前的所有写操作都完成,防止存储指令把后面的写指令提前执行。
// 上述问题解决
上述例子中对象初始化和引用赋值这两个步骤可能出去指令重排序.
解决:单例对象Object加上volatile修饰,就会在底层加上lock前缀,instance变量就会插入相应的内存屏障(Store Barrier),就可以避免指令重排序.

总结

总体而言,volatile 是一种简单而有效的多线程编程机制,适用于某些特定场景下,但在一些复合操作的情况下,仍需要考虑其他同步机制。使用 volatile 需要理解它的特性和限制,以确保正确、高效、线程安全的多线程编程。

// 1.保证可见性
当一个线程修改了一个 volatile 变量的值,这个变化对其他线程是可见的。即,一个线程修改了 volatile 变量后,其他线程能够立即看到这个变化。
// 2.禁止指令重排序
volatile 变量的读写操作会在指令层面插入内存屏障。
禁止了编译器和处理器在volatile 变量的操作前后进行指令重排序,确保操作的有序性。
// 3.不保证原子性
volatile关键字保证了可见性和禁止指令重排序,但并不保证复合操作的原子性。
例如:i++ 操作不是原子的,volatile 不能解决这种复合操作的线程安全性问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值