java 并发之 volatile

本文介绍了Java并发编程中volatile关键字的作用,包括解决缓存一致性问题、保证可见性和一定的有序性。volatile无法保证原子性,但可以通过内存屏障防止指令重排序。文章讨论了Happen-before原则,并列举了volatile的应用场景,如状态标记和double check。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实例:

两个线程同时执行的情况下,执行i++ 操作,i初始化为1,得到的结果并非为3,而是2.过程为:

线程1和2 同时从主存中获取i 的值并写入缓存,在分别执行+1 操作,之后再分别写入主存,这时出现了最终主存的值为2的情况。这个是因为线程内操作对其他线程不可见,解决缓存一致性的方案有:

  1. 通过在总线加LOCK#锁的方式;

  2. 通过缓存一致性协议。

但是方案1的缺点是总线加锁会出现阻塞的情况,效率低。

方案2的核心思想是:缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。

多线程并发的特点来源于:可见性,原子性和有序性

特点:

1、volatile 无法保证原子性操作(可理解为指令级别,java 对基本的数据类型的赋值操作进行了原子性操作的保证。但long和double 不是),

2、volatile 可保证可见性:当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。当然,synchronize和锁都可以保证可见性。

3、在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。如单例模式中的双重检查

public class Singleton {
    private static Singleton instance = null;
    //private volatile static Singleton instance = null;
    public  static Singleton getInstance() {
        if(null == instance) {    // 线程二检测到instance不为空
            synchronized (Singleton.class) {
                if(null == instance) {                    
                    instance = new Singleton();    // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)    
                }
            }
        }
        
        return instance;    // 后面线程二执行时将引发:对象尚未初始化错误
        
    }
}

:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

  1)给instance实例分配内存;

  2)初始化instance的构造器;

  3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)

如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:

  a)给instance实例分配内存;

  b)将instance对象指向分配的内存空间;

  c)初始化instance的构造器;

  这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化

具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)

  根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量

volatile 原理:

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。

上面那段话,有两层语义:

  1. 保证可见性、不保证原子性;

  2. 禁止指令重排序。

Happen-before 原则:

其定义如下:

  • 同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。

  • 监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)

  • 对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)

  • 线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)

  • 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。

  • 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

volatile经常用于两个场景:状态标记、double check

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值