可见性、有序性、原子性问题
可见性、有序性、原子性问题
并发编程有什么用? 毋庸置疑是为了挖掘服务器的潜力,提升程序的性能。但是这又给我们带来了新的问题。
多核CPU 增加了缓存,以均衡与内存的速度差异;导致
可见性
问题编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。导致
有序性
问题进程,线程,分时使用CPU, 以均衡CPU与I/O之间的差异。导致了数据的
原子性
学习使用并发编程就是既要提升性能,同时又要避免上面的问题。
可见性问题
每颗CPU都有自己的缓存,多个线程在不同的CPU上执行时,这些线程操作的是不同的CPU缓存。比如下图中,线程A操作的是CPU-1上的缓存,而线程B操作的是CPU-2上的缓存,很明显,这个时候线程A对变量V的操作对于线程B而言就不具备可见性了
下面用代码的方式将可见性问题复现
我们使用for循环的方式创建多个线程去操作同一个变量cnt
。
package com.bdf.juc.visibility;
import cn.hutool.core.thread.ThreadUtil;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadUnsafeExample {
private int cnt = 0;
public void add() {
cnt++;
}
public int get() {
return cnt;
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = ThreadUtil.newExecutor();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
}
输出结果
986
期望的输出结果是1000。可是最终的输出结果是变化的, 总是达不到1000。
线程是在CPU里面运行的,会将线程需要运行的数据拉到CPU的缓存中运算。多个CPU就会有多个缓存。 这里就是将CNT拉取到不同的缓存中, 每一个线程都在自己的缓存中操作,操作完成之后在同步到内存中。
这样当线程A和线程B都去内存中读取CNT,读取到的值都是1, 并将CNT改写成2,写到内存中。此时正确实的值应该是3,可是最终内存中的值是2。 这就是可见性问题。
原子性问题
首先要明确的一点是多线程的原子性和数据库的原子性类似,但不是一回事。
多线程开发的原子性是指的 一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性。 一个操作是CPU的一条指令。
数据库的原子性是事务不可分割,事务中的逻辑要么全部成功要么全部失败。 事务中的逻辑可以交由多个线程去实现。
在java代码中一个简单的语句也需要CPU中多条指令来完成。 例如: count+=1这个操作。
通常来说一个java语句去操作一个变量,同时这个语句使用多个线程运行, 是会有原子性问题的。 因为这是有很多个CPU指令去完成的。
怎么来保证线程的切换不影响我们的业务,这就需要一些高级语言给我们提供的一些工具。 至于什么工具,怎么用这些工具? 我们会在后面讲。
有序性问题
我们在写代码的时候是按照一定的顺序来写的,也希望代码有顺序的执行。 但是在编译器执行代码的时候并不是按照顺序来执行。
long i = 0;
boolean flag = false;
i = 1L; //语句1
flag = true; //语句2
语句1和语句2一定是先执行 语句1和语句2吗? 不一定。 在程序执行是为了提高性能,编译器和处理器会对指令进行重新排序。
一个经典的单例问题, 其中只分析当有序性可能会引发的问题。
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) { //节点B
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
首先需要知道 instance = new Singleton();
这个语句是由多个命令组成:
- 分配一块内存M
- 在内存M上初始化Singleton对象。
- 然后M的地址赋值给instance变量。
假设有两个线程A、B调用getInstance()方法,线程A获取到了锁,开始执行创建对象语句。 而因为指令重排序。 instance = new Singleton();
的命令顺序有所改变(假设! 不确定! 有可能会变成下面说的那样)
- 分配一块内存M;
- 将M的地址赋值给instance变量;
- 最后在内存M上初始化Singleton对象。
而当创建对象指令执行到第二步时并且 线程B执行到第一个if (instance == null) {
代码中节点B时。 线程B执行这个判断语句会得到false,最终会得到一个instance对象,而这个instance对象还没有创建。 如果这个时候使用就会空指针。