1.volatile的概念
volatile通常被称为轻量级的synchronized,他与synchronized不同,volatile只修饰变量,无法修饰方法和代码块。
用法:只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。
特点:volatile在线程安全方面,可以保证有序性和可见性,但是不能保证原子性。
synchronized 可以保证原子性,因为synchronized修饰的代码片段,在进入之前加了锁,只要它没执行完,其他线程无法获得锁执行这段代码片段,就可以保证他内部的代码可以被全部被执行。进而保证了原子性。
过程:
2.volatile如何保证可见性的
对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的
指令,将这个缓存中的变量回写到系统主存中。
所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
MESI的核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
3.volatile如何保证有序性
volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排
优化等。
普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。volatile是通过内存屏障来禁止指令重排的,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行,load->add->save 的执行顺序就是:load、add、save。如经典的双重校验锁必须加volatile的问题,就是因为volatile加了内存屏障。
4.为什么volatile不保证原子性
volatile仅保证单个操作的原子性:在Java中,volatile仅保证对单个volatile变量的读操作和写操作是原子的。这意味着,当一个线程读取或写入一个volatile变量时,这个操作是不可中断的。但是,这并不意味着复合操作(如i++)是原子的。
复合操作的非原子性:对于像i++这样的复合操作,它实际上包含三个步骤:读取i的值、将i的值加1、将新值写回i。由于volatile只能保证每个步骤的原子性,而不能保证整个复合操作的原子性,因此当多个线程同时执行这样的操作时,就可能出现数据不一致的问题。
内存可见性与原子性的区别:volatile通过内存屏障等机制保证了变量的内存可见性,即当一个线程修改了volatile变量的值后,这个新值对其他线程是立即可见的。然而,这并不意味着volatile能够保证复合操作的原子性。内存可见性和原子性是两个不同的概念,前者关注的是变量值的传播速度,而后者关注的是操作的不可分割性。
不保证原子性案例:
对于像 i++
这样的复合操作,即使加了 volatile
修饰符,也仍然可能出现数据不一致的情况。这是因为 volatile
仅能保证单个读写操作的原子性,而无法保证复合操作的原子性。
复合操作 i++
实际上包含三个步骤:
- 读取
i
的当前值。 - 将读取到的值加 1。
- 将新值写回
i
。
这三个步骤在单线程环境中是顺序执行的,因此不会出现问题。但在多线程环境中,如果多个线程同时执行 i++
操作,就可能发生竞态条件(race condition),导致数据不一致。
具体来说,如果两个线程几乎同时执行 i++
,它们可能会:
- 线程A读取
i
的当前值(假设为0)。 - 线程B在线程A读取之后但写入之前也读取
i
的值(此时仍然是0,因为线程A的写入尚未发生)。 - 线程A将加1后的值(1)写回
i
。 - 线程B也将其读取到的值(0)加1,并将结果(1)写回
i
,覆盖了线程A的写入。
最终,尽管两个线程都执行了 i++
操作,但 i
的值只增加了1,而不是预期的2。
总结:
volatile修饰的共享变量,其他线程进行读操作时,总能读到最新的数据。
volatile修饰的共享变量,其他线程进行写操作时,直接写在主内存中,并不是缓存。
volatile保证数据的可见性和顺序性。