JVM学习之路(五)——如何保证可见性

本文探讨了Java内存模型中的可见性问题,分析了线程间变量同步的挑战,并介绍了volatile关键字如何确保线程间变量的及时更新。

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

JVM学习之路(一)——java程序执行流程

JVM学习之路(二)——JVM的内部结构

JVM学习之路(三)——JVM内部结构详细介绍及其相互作用

JVM学习之路(四)——内存模型(java多线程通信)

五、如何保证可见性

一、第四部分说明了java线程之间通信的流程,以及各自的工作内存和共享的主内存在JMM(Java 内存模型)的管理下的工作流程。在这个工作流程中,发现了一个比较严重的问题——变量的不可见性(顾名思义,一个线程看不见另一个线程对变量所做的改变,还傻了吧唧的使用原来旧的值,可不就是“不可见”嘛)。这是由于“工作内存”这个中间层的出现,线程1和线程2必然存在延迟的问题,具体体现在以下2种情况:

1、线程1在工作内存中更新了变量,但还没刷新到主内存,而此时线程2只能获取到未更新的原来旧的变量值。

2、线程1成功将变量更新到主内存,但线程2依然使用自己工作内存中原来旧的变量值,并没有去主内存取新的值。

不管出现哪种情况,都可能导致线程间的通信不能达到预期的目的。那有没有解决办法呢?Of course,这就是volatile关键字的作用所在。

二、volatile如何打破僵局?

Volatile保证两件事:

1、 针对上述第1种情况,使用volatile就要求线程1工作内存中的变量更新强制立即写入到主内存

2、 针对上述第2种情况,使用volatile就要求线程2工作内存中的变量会强制立即失效,这使得线程2必须去主内存中获取最新的变量值。

所以,这就是volatile保证变量可见性的原理。这样的话,线程1对变量的修改就能第一时间让线程2可见。

### 原子性、可见性和有序性的深入解析 #### 一、原子性 原子性指的是一个操作在多线程环境中执行时,其行为是不可分割的,即该操作要么完全被执行,要么完全没有被执行。这可以防止其他线程观察到中间状态或部分更新的状态[^1]。 在Java中,某些基本的操作(如单个读取或写入long/double类型的变量)可能不是原子的,而大多数简单的赋值操作则是原子的。然而,在涉及复合操作的情况下(例如`number++`),这些操作实际上由多个步骤组成:读取当前值、增加数值以及重新存储新值。因此,如果没有额外的同步措施,则可能导致数据竞争问题[^2]。 为了确保原子性,可以通过以下方式实现: - 使用`synchronized`关键字锁定一段代码区域。 - 利用`java.util.concurrent.atomic`包下的类,比如`AtomicInteger`等,它们提供了无锁的高效解决方案[^3]。 ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicExample { private AtomicInteger atomicInt = new AtomicInteger(0); public void increment() { atomicInt.incrementAndGet(); } } ``` --- #### 二、可见性 可见性指当一个线程修改了某个共享变量的值之后,另一个线程能够立即看到这个最新的变化。默认情况下,Java内存模型并不保证这一点,因为编译器可能会对指令重排序,或者CPU缓存机制延迟刷新导致不同线程间的数据视图不一致。 通过声明变量为`volatile`,可以在一定程度上解决这个问题。标记为`volatile`的变量会在每次访问时都从主存加载最新值,并且不会被本地寄存器缓存。但是需要注意的是,尽管`volatile`能提供可见性保障,但它无法单独满足原子性需求。 示例程序展示了即使使用了`volatile`修饰符,但由于缺乏必要的同步控制,最终打印的结果仍可能出现偏差: ```java public class VolatileVisibility { public static volatile int count = 0; public static void increase() { for (int i = 0; i < 1000; ++i) { count++; } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(VolatileVisibility::increase); Thread t2 = new Thread(VolatileVisibility::increase); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); // 输出不一定等于2000 } } ``` 上述情况发生的原因在于`count++`并非单一动作而是包含了三个独立的动作——读取旧值、计算新值和保存回原位置,这三个过程之间可能存在干扰。 --- #### 三、有序性 有序性涉及到程序执行顺序与实际运行时发生的事件之间的关系。由于现代处理器架构支持乱序执行以提高性能,再加上JVM内部也可能进行一些优化调整(如方法内联、循环展开等),所以程序员所写的源码未必严格按照书写顺序去真正运行[^4]。 具体来说,存在两种主要形式的重排现象: 1. 编译期重排:编译器基于特定规则改变语句的实际排列; 2. CPU级重排:硬件层面出于效率考虑所做的变动。 针对这种情况,Happens-Before原则成为理解并维护正确因果链条的重要依据之一。它规定了几种场景下必然存在的先后关联,其中包括但不限于监视器解锁先于同一对象上的加锁等等。 一种常见的实践手段就是借助显式的屏障结构来强制维持预期逻辑流走向稳定方向发展下去。例如前面提到过的锁粗化技术正是利用减少频繁开关代价从而间接达成目的实例表现形式而已。 --- ### 总结 综上所述,在并发编程领域里头,“原子性”关注如何保护关键区段免受外部打扰破坏整体一致性;“可见性”强调跨进程通讯时候消息传递及时准确的重要性;至于最后提及的那个所谓“有序性”,则更多侧重探讨底层物理设备运作规律对我们高层抽象设计带来的潜在影响因素考量方面内容[^3]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值