volatile 和 synchronized 特点
首先需要理解线程安全的两个方面:执行控制 和 内存可见。
执行控制 的目的是控制代码执行(顺序)及是否可以并发执行
内存可见 控制的是线程执行结果在内存中对其它线程的可见性。根据 Java 内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU 缓存),操作完成后再把结果从线程本地刷到主存
synchronized
关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized
关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized
还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都 happens-before 于随后获得这个锁的线程的操作
volatile
关键字解决的是内存可见性的问题,会使得所有对volatile
变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求
使用volatile
关键字仅能实现对原始变量,如 boolen、short、int、long 等操作的原子性,但需要特别注意,volatile
不能保证复合操作的原子性,即使只是i++
,实际上也是由多个原子操作组成:read i; inc; write i
,假如多个线程同时执行i++
,volatile
只能保证他们操作的i
是同一块内存,但依然可能出现写入脏数据的情况
volatile 实现可见性
volatile 通过加入内存屏障和禁止重排序优化来实现的:
- 对 volatile 变量执行写操作时,会在写操作后加入一条 store 屏障指令
- 对 volatile 变量执行读操作时,会在读操作前加入一条 load 屏障指令
重排序:
在虚拟机层面,为了尽可能减少内存操作速度远慢于 CPU 运行速度所带来的 CPU 空置的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用 CPU
通俗的讲:volatile 变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值
线程写 volatile 变量的过程:
- 改变线程工作内存中 volatile 变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
线程读 volatile 变量的过程:
- 从主内存中读取 volatile 变量的最新值到线程的工作内存中
- 从工作内存中读取 volatile 变量的副本
要在多线程中安全的使用 volatile 变量,必须同时满足:
- 对变量的写入操作不依赖其当前值,或者能确保只有单个线程更新变量的值
- 该变量没有包含在具有其他变量的不变式中
第一个原则的理解就是,当你需要改变这个变量时,要保证你要改变的值跟这个变量原先的值没有任何关系,比如:
count = 10;
这里无论 count 的值是什么都直接赋值了 10,这个变化跟它原先的值没有任何关系,如果是如下的方式:
count ++;
这里 count 的结果是依赖于它原来的值加 1 得到的,所以这种场景不适合使用 volatile 关键字
从 Java 内存模型的角度理解:被 volatile 关键字修饰的变量只能保证assgin -> store -> write
操作和read -> load -> use
操作的原子性,但count ++
操作包括的原子操作有:read -> load -> use -> assgin -> store -> write
操作,所以自加操作并非一个原子操作,线程 A 在读取到count
的最新值之后,在assgin
操作之前可能切换到线程 B,线程 B 此时执行的操作可能为read -> load -> use -> assgin -> store -> write
操作,完成了count
的自加操作,此时线程 A 由于已经读取到count
的值,所以不再从主存中刷新count
的值,但此时线程 A 的工作内存中保存的count
的值已经过期,线程 A 对过期的count
值进行自加操作后写会了主内存,从而造成数据的错误
第二个原则的理解,我们举如下例子:
public class NumberRange {
private volatile int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
这里我们看到,setLower
和setUpper
两个方法中,使用了大小的边界检查,保证了lower
总是小于upper
,在单线程环境下没有问题,但是如果有另外两个线程并发的调用setLower
和setUpper
,比如,初始状态是(0, 5)
,某个时刻线程 A 调用setLower(4)
的同时线程 B 调用setUpper(3)
,这个时候两个调用都可以通过边界检查,最后得到(4, 3)
,这个边界结果显然是没有意义的,但是 volatile 在这里并不能起作用,这种情况应该使用锁来保证边界结果的有效性
volatile 和 synchronized 的区别
-
volatile
本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住 -
volatile
修饰变量;synchronized
修饰方法 -
volatile
不会造成线程的阻塞;synchronized
可能会造成线程的阻塞 -
volatile
仅能实现变量的修改可见性,不能保证原子性,因为一个线程 A 修改了变量还没结束时,另外的线程 B 可以看到已修改的值,而且可以修改这个变量,而不用等待 A 释放锁,因为volatile
变量没上锁。而synchronized
则可以保证变量的修改可见性和原子性
网友评论