Java 并发编程 可见性、原子性、有序性
并发编程的三大核心:可见性、原子性、有序性
关于硬件
计算机的CPU、内存、IO设备在不停的更新迭代,但是,在这些硬件之间,存在着一个核心矛盾,就是三者之间的存在巨大的速度差异。
为了合理利用CPU的高性能,平衡三者之间的差异,
- CPU 增加了缓存机制,来均衡与内存之间的速度差异。
- 操作系统增加了进程、线程,通过线程切换来分时复用CPU资源,均衡CPU与I/O设备的速度差异
- 编译程序优化指令的执行顺序,使得缓存能够更加合理的利用
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
在单核时代,所有的线程都是在一颗CPU上执行,CPU缓存与内存的数据一致性容易解决。因为所有的线程都是操作同一个CPU的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。
在多核时代,每颗CPU都有自己的缓存,这时CPU缓存与内存的数据的一致性就没那么容易解决了。当多个线程在不同的CPU上执行时,此时操作的时不同CPU上的缓存。
看一个程序实例:
public class Test {
private long count = 0;
private void test() {
int id = 0;
while (id++ < 100000) {
count += 1;
}
}
public long getCount() {
return count;
}
public static long testThread() throws InterruptedException {
final Test test = new Test();
Thread thread1 = new Thread(() -> test.test());
Thread thread2 = new Thread(() -> test.test());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
return test.getCount();
}
}
这段代码会出现一个奇怪的现在,当两个线程执行完之后,发现代码的结果并不是200000
原子性
我们把一个或者多个操作在CPU执行的过程中不被中断的特性成为原子性
首先我们先了解下线程切换
多线程并不一定是在多核处理器下才能完成的,在单核处理器上也可以支持多线程。CPU通过给每个线程分配一定的时间片段,时间片非常短,通常是几十毫秒,CPU不停的切换线程执行任务从而达到多线程的效果。
在一个时间段内,如果一个进程进行一个IO操作,例如读个文件,这个时候该进程可以把自己标记为“休眠状态”并出让CPU的使用权,待文件读进内存,操作系统会把这个休眠的进程唤醒,唤醒后的进程就重新获得CPU的使用权了。
count += 1
count +=1 的CPU 指令:
- 指令1:首先,需要把变量count从内存加载到CPU的寄存器
- 指令2:在寄存器中执行+1的操作
- 指令3:将结果写入CPU缓存
我们把一个或者多个操作在CPU执行的过程中不被终端的特性成为原子性
。CPU能够保证的院子操作是CPU指令级别的,而不是高级语言的操作符,因此,我们需要在编编写代码时保证操作的原子性。
有序性
有序性指的是程序按照代码的先后顺序执行
编译器为了优化性能,有时候会改变程序中语句的先后顺序
int a = 1; // 语句A
int b = 2; // 语句B
int c = a + b; /// 语句C
在上段代码中,正常的执行顺序应该是 语句A >> 语句B >> 语句C
。但是有时 JVM
为了提高整体的效率会进行CPU指令重排导致执行的顺序可能是语句B >> 语句A >> 语句C
。
重排在单线程中不会出现问题,但是在多线程中就会出现数据不一致的问题。
实例:双重检查创建单例对象
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
上面的代码中,在获取实例getInstance()
的方法中,我们首先判断singleton
是否为空,如果为空,则锁定Singleton.class
,并再次检查singleton
是否为空,如果为空则创建一个Singleton
实例。
问题
singleton = new Singleton();
此段代码的执行流程:
- 分配一块内存空间M
- 在内存M上初始化Singleton对象
- 将
singleton
对象指向分配的内存空间
实际上 CPU会对这三句指令进行指令重排,优化后的路径为
- 分配一块内存空间M。
- 将M的地址赋值给
singleton
变量。 - 最后在内存M上初始化Singleton对象。
优化后的指令执行顺序有可能导致某个线程拿到的单例对象还没有初始化,以至于使用报错。
如何解决
课后思考 单例模式的实现
有人提出用volatile
解决可以吗?
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}