共计 1448 个字符,预计需要花费 4 分钟才能阅读完成。
共享变量在线程间不可见的原因
- 线程的交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主内存间及时更新
- 使用 synchronized 的来保证可见性
使用 synchronized 的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意加锁与解锁是同一把锁)
- volatile 来实现可见性
通过加入内存屏障和禁止重拍讯优化来实现可见性。
- 对 volatile 变量写操作时,会在写操作后加入一条 store 屏障指令,将本地内存中的共享变量值刷新到主内存
- 对 volatile 变量进行读操作时,会在读操作前加入一条 load 屏障指令,从主内存中读取共享变量。
也就是说使用 volatile 关键字在读和写操作时都会强迫从主内存中获取变量值。
下图是使用 volatile 写操作的示意图
使用 volatile 写操作前会插入一条 StoreStore 指令来禁止在 volatile 写之前的普通写对 volatile 写的指令重排序优化,在写之后会插入一条 StoreLoad 屏障指令来防止上面的 volatile 写操作和下面可能有的读或者写进行指令重排序。
下图是 volatile 读操作示意图
- volatile 操作都是 cpu 指令级别的
下面看一段演示代码
@Slf4j
public class CountExample4 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static volatile int count = 0;
public static void main(String[] args) throws Exception {ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {executorService.execute(() -> {
try {semaphore.acquire();
add();
semaphore.release();} catch (Exception e) {log.error("exception", e);
}
countDownLatch.countDown();});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
count++;
// 1、count
// 2、+1
// 3、count
}
}
我们多次运行个这段代码,发现结果并不是我们预期 5000,volatile 只能保证可见性并不能保证原子性。
通常来说使用 volatile 需要具备两个条件
- 对变量写操作不依赖当前值
- 该变量没有包含在其他变量的所在的式中
所以 volatile 非常适合用作状态标记量,比如做为线程是否被初始化。还有就是用 double check 我之前的博客就提到的单例模式中就使用了 volatile 来做 double check 双重检查实现单例。
正文完