文章目录
1. volatile 和 synchronize
关键字 volatile 的主要作用是使变量在多个线程间可见,同时还会防止指令重排序。
使用 volatile 关键字增加了实例变量在多个线程之间的可见性。但是 volatile 关键字最致命的缺点就是不支持原子性。
1.1. synchronize 和 volatile 的区别
- 关键字 volatile 是线程同步的轻量级实现,所以性能肯定比 synchronize 要好。volatile 只能修饰变量,而 synchronize 修饰方法和代码块;
- 多线程访问 volatile 不会发生阻塞,而 synchronize 会出现阻塞
- volatile 能保证数据的可见性,但不能保证原子性;而 synchronize 可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步
- 关键字 volatile 解决的是变量在多个线程之间的可见性;而 synchronize 关键字解决的多个线程之间访问资源的同步性
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
1.2. 线程间变量可见性验证
public class ThreadVisibility {
// 当不使用volatile关键字修饰变量时,程序不会停止,且不会打印end。
private static /*volatile*/ boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
//do sth
}
System.out.println("end");
}, "server").start();
Thread.sleep(1000);
flag = false;
}
}
1.3. volatile 非原子性验证
public class NoACIDVerify extends Thread {
private volatile static int count;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count=" + count);
}
@Override
public void run() {
addCount();
}
public static void main(String[] args) {
NoACIDVerify[] noACIDVerifies = new NoACIDVerify[100];
for (int i = 0; i < 100; i++) {
noACIDVerifies[i] = new NoACIDVerify();
}
for (int i = 0; i < 100; i++) {
noACIDVerifies[i].start();
//多次执行后,最终结果不一定是10000,
}
}
}
关键字 volatile 提示线程每次从共享内存中读取变量,而不是从私有内存中读取变量,这样就保证了同步数据的可见性。
但是需要注意的是:如果有修改实例变量的数据,比如 i++,由于 i++ 并不是一个原子操作,即非线程安全的。
表达式 i++ 的操作步骤分解如下:
1.从内存中取出 i 的值
2.计算 i 的值
3.将 i 的值写到内存中
假如在第2步计算的时候,另外一个线程也在修改i的值,那么这个时候就会出现脏读数据。
volatile 本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存。
1.4. volatile 非线程安全的原因
变量在内存中工作的过程如下图所示;
read 和 load 阶段:从主内存中复制变量到当前线程工作内存
use 和 assign 阶段:执行代码,改变共享变量值。
store 和 write 阶段:用工作内存中数据刷新主内存对应的变量的值。
在多线程环境中 use 和 assign 是多次出现的,但这一操作并不是原子性,也就是在 read 和 load 之后,如果主内存中 count 变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,即私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期的不一样,也就出现了非线程安全问题。
对于 volatile 修饰的变量,JVM 虚拟机只是保证从主内存中加载到线程工作的内存的值是最新的。也就是说, volatile关键字解决的是变量读取时的可见性问题,但无法办证原子性,对于多个线程访问同一个实例变量还需要加锁同步。
1.5. synchronize 具有线程间变量可见性功能
关键字 synchronize 可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。
对上述验证线程线程可见性的代码稍作修改,在while循环中增加一个synchronize锁,就会发现在没有使用volatile修饰的情况下,对变量flag的修改不同线程间可见。
public class ThreadVisibility_Synchronized {
private static /*volatile*/ boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
String s = "1";
while (flag) {
//do sth
//保证进入到同步方法或同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果
synchronized (s) {
}
}
System.out.println("end");
}, "server").start();
Thread.sleep(1000);
flag = false;
}
}
关键字 synchronize 可以保证在同一个时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特性:互斥性和可见性。
同步 synchronize 不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。