线程安全-可见性

概述

指一个线程对主内存的修改,可以及时地被其他线程观察到。

导致共享变量在线程间不可见的原因有:

  1. 多线程交叉进行。
  2. 重排序结合多线程运行。
  3. 共享变量更新后的值未及时进行工作内存与主内存的更新。

可见性之synchronized关键字

Java内存模型对synchronized关键字有两条规定。

  1. 线程解锁前必须把共享变量的最新值刷新到主内存。
  2. 线程加锁时,将清空工作内存中共享变量的值。那么工作内存需要时只能从工作内存中取值。

工作原理:此时的加锁和解锁用的是一个锁,即多线程之间共享synchronized一个锁。

可见性之volatile关键字

工作原理:通过加入内存屏障禁止重排序优化来实现,即通过内存屏障保证了禁止重排序优化
内存屏障从以下两点进行说明:

  1. 对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将本地工作内存中的值刷新到主内存。
  2. 对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存读取共享变量的值。

    即强迫从主内存中的共享变量进行交互。

volatile写原理:

volatile写

volatile读原理:

volatile读

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Slf4j
@NotThreadSafe
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
}

多次运行结果:仍然无法保证线程安全
两次测试结果如下:

图示

图示

分析:

volatile修饰的count进行++时,分为三步进行:

  1. 取出count
  2. count+1
  3. 写入count

已知在第一步的取数时,取出的都是主内存的最新值。但是写入的时候就可能发生问题。 当多个线程进行操作时,同时取出主内存中的count,并执行+1,然后同时写入主内存,那么导致请求总数永远比clientcount值小,即volatile关键字只能保证可见性,但并具有原子性,不能保证线程安全

volatile使用的场景

场景需具备的条件:

  1. 对变量的写操作不依赖与当前值
  2. 该变量没有包含在具有其他变量的不变的式子中。

因此,

  1. volatile适合作为状态标记量(boolean型)
  2. 适用于doublecheck场景。(例子会在之后手记中添上……)
SupriseMF wechat
欢迎关注微信订阅号【星球码】,分享学习编程奇淫巧技~
喜欢就支持我呀(*^∇^*)~

热评文章