volatile的可见性保证

本文详细解释了volatile关键字在Java中的作用,包括其可见性依赖内存屏障确保数据更新及时,但不提供原子性,多线程操作volatile仍需额外同步。

1.volatile关键字简介

        volatile 是 JVM 提供的轻量级的同步机制,volatile具有两个特性:可见性,有序性。不保证并发情况下的原子性。

2.volatile关键字的可见性,有序性

        可见性是指一个线程修改了volatile变量的值,其他线程能够立即知道发生的变更。

内存屏障

        volatile可见性是依靠内存屏障(Memory Bariers)来实现的,内存屏障是一类同步屏障指令,是编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,以避免代码重排序,从而达成了其他线程可见的效果。

内存屏障的作用可分为两种:

  • 内存屏障之前的所有写操作都要回写到主内存
  • 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果

        当某个线程收到通知,去读取volatile修饰的变量的值的时候,线程私有工作内存的数据失效,需要重新回到主内存中去读取最新的数据,也叫做写后读从而保证了volatile的可见性

细究内存屏障的工作流程

对应的内存屏障共四种,主要可分为两种,读屏障写屏障

  • 读屏障让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新的数据。

  • 处理器在写屏障之前将所有存储在缓存(storebufferes)中的数据同步到主内存。

(事实上内存屏障有四种,上面两种分别是load load bariers 和 store store bariers,还有load store baries 和 store loadbariers,但下面两个屏障的主要效果是保障上两个屏障正常运行,类似于innodb的插入意向锁的功能,故在此默认只有读屏障和写屏障,方便理解)

        由于内存屏障前的内容是不可以重排到内存屏障后的,故对于volatile的写,发生于任何一个对于volatile的读之前,也叫做写后读,这样就保证了不同线程对于volatile变量的可见性。

3.volatile不具备原子性

原子性是指一个操作是不可打断的,即多线程环境下,操作不能被其他线程干扰。

volatile变量和普通变量一样都是不具备原子性的,下面是一个普通变量进行读写的过程。

JMM(Java Memory Model)中定义了8中线程工作内存与主内存的原子操作:

这8个原子操作的功能如下:

  1. read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
  2. load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
  3. use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
  4. assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
  5. store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
  6. write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
  7. lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
  8. unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

        对于volatile变量(在JMM中和普通变量的操作相同),在工作内存write/load主内存时,主内存加锁,进入线程独占状态,又由于此时因为内存屏障的原因代码是有序的,所以读写volatile变量的过程是安全的。又由于解锁后,本线程会重新将主内存的值更新到工作内存,所以对于单个线程的volatile操作是安全的。

        但是当多个线程同时操作一个volatile变量,此时一个线程独占主内存进行操作,读屏障只能保证其他线程读volatile变量时(load数据时)是最新的数据,当这些阻塞线程因加锁而等待后,独占线程修改数据而释放锁,这时这些阻塞线程并没有更新数据,故不能保证原子性(会受到其他线程的干扰)。

        故volatile变量和普通变量一样,在并发环境下也需要手动加锁进行同步操作。

### Java 中 `volatile` 和 `synchronized` 的可见性原理及区别 #### 1. 可见性原理 在 Java 的多线程环境中,每个线程都有自己的工作内存,而变量存储在主内存中。当线程读取变量时,会优先从自己的工作内存中读取;如果工作内存中不存在该变量,则通过 Load 操作从主内存加载。因此,如果没有适当的同步机制,一个线程对变量的修改可能无法及时被其他线程看到。 - **`volatile` 的可见性原理** 当一个变量被声明为 `volatile` 时,它会强制要求每次读取该变量时都必须从主内存中获取最新值,而不是使用线程本地的工作内存中的副本[^3]。这意味着,一旦某个线程修改了 `volatile` 变量的值,其他线程能够立即看到这个变化。此外,`volatile` 还禁止指令重排,确保程序执行顺序与代码书写顺序一致[^1]。 - **`synchronized` 的可见性原理** `synchronized` 关键字通过加锁机制来保证可见性。当一个线程进入 `synchronized` 代码块或方法时,它会锁定该对象,并将该对象的工作内存刷新到主内存中。当线程退出 `synchronized` 块时,它会将本地工作内存中的变量值写回到主内存中[^5]。这种机制确保了线程之间的可见性,即一个线程对共享变量的修改能够被其他线程感知。 #### 2. 区别 尽管 `volatile` 和 `synchronized` 都能保证可见性,但它们在实现细节和适用场景上存在显著差异: - **轻量级 vs 重量级** `volatile` 是一种轻量级的同步机制,不需要加锁,也不会阻塞线程。相比之下,`synchronized` 是一种重量级的同步机制,它通过加锁来确保互斥访问,可能会导致性能开销[^4]。 - **功能范围** `volatile` 仅能保证变量的可见性和有序性,无法保证操作的原子性。例如,对于复合操作(如 `i++`),即使变量是 `volatile` 的,仍然可能出现竞态条件[^4]。而 `synchronized` 不仅能保证可见性,还能确保操作的原子性[^2]。 - **适用场景** - 如果只需要保证单个变量的可见性,且不涉及复杂的复合操作,可以使用 `volatile`。 - 如果需要对多个变量或代码块进行同步访问,或者需要保证原子性,则应使用 `synchronized`。 #### 示例代码 以下是一个简单的示例,展示了如何使用 `volatile` 和 `synchronized` 来保证可见性: ```java // 使用 volatile 保证可见性 public class VolatileExample { private volatile boolean isRunning = true; public void stop() { isRunning = false; } public void doWork() { while (isRunning) { // 执行任务 } } } // 使用 synchronized 保证可见性 public class SynchronizedExample { private boolean isRunning = true; public synchronized void stop() { isRunning = false; } public void doWork() { while (isRunning) { // 执行任务 } } } ``` #### 总结 `volatile` 和 `synchronized` 在 Java 并发编程中扮演着重要角色。`volatile` 提供了轻量级的可见性和有序性保证,适用于简单的场景;而 `synchronized` 提供了更全面的同步机制,适用于复杂的并发控制需求。开发者应根据具体场景选择合适的关键字,有时还需要结合两者以达到最佳效果[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值