并发编程笔记-核心问题与BUG源头

并发编程核心问题#

其实并发编程可以总结为三个核心问题:分工、同步、互斥。

所谓分工指的是如何高效地拆解任务并分配给线程,而同步指的是线程之间如何协作,互斥则是保证同一时刻只允许一个线程访问共享资源。Java SDK 并发包很大部分内容都是按照这三个维度组织的,例如 Fork/Join 框架就是一种分工模式,CountDownLatch 就是一种典型的同步方式,而可重入锁则是一种互斥手段。

并发编程全景图之思维导图

并发编程BUG源头#

1、缓存导致的可见性问题

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性

问题在于每个cpu更改的通常是自身cpu缓存,会导致最终的共享变量的结果不可控。

2、线程切换带来的原子性问题

我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性

比如:count+=1,非原子性操作,多线程时,最终count的值不会是想要的值,会小一点,因为会同时get, +1 ,然后写到共享内存,这就是原子性问题。

3、编译优化带来的有序性问题

有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序

比如: new 操作上,我们以为的 new 操作应该是:

1.分配内存 2.在内存M上初始化对象 3将M的地址赋给变量引用

但实际上,1分配内存 2将M的地址赋给变量引用 3 在内存M上初始化对象

这时候就会发生引用变量出现空指针,这是一种指令重排序导致的有序性问题,在双检锁的时候会出现。

针对双检锁的个人分析,

happens-before应该是为了并发安全, 包括可见性和指令重排,可见性比如一个线程锁了,另一个线程得等到第一个线程释放锁,才能往下走,这时候,再获取到的对象,如果是从内存中获取,就已经是最新的了,因为解锁的时候,jvm强制刷新缓存,相当于第一个线程的修改,对第二个线程可见,

单例模式双检锁的 synchronized 就是用synchronized 保证第二个线程获取到锁之前,第一个线程已经把所处cpu缓存刷新到内存中了 ,在第二个线程进到锁块儿里 也就是第二个if的时候, 可以判断obj != null , 这是synchronized的作用,保证synchronized块儿里的第二个线程对第一个线程的可见性, 但synchronized块儿外是没有锁的,

如果现在第一个线程走到new 的时候(, 此时new 里是指令重排序的, 先分配内存,obj不为null,最后初始化), 第二个线程走到 双检锁的第一个 if的时候, 会判断不为null, 会返回一个没有初始化的对象, 第二个线程里会报空指针,

这是因为指令重排序造成的并发问题, 用 happens-before的 第三条 volatile的规则,写操作先行于后面对这个变量的读操作,如果这个obj 是volatile, 第二个线程在走到第一个if的时候,虽然没在synchronized但会等待,因为volatile的obj 还在被第一个线程写入,等写入完毕 , 第二个线程就可以读了,此时就不会返回一个没有初始化的对象了。

第二个线程读之前,第一个线程写入volatile变量完毕,会刷新cpu缓存到内存,第二个线程可以获取到最新的对象信息,也算是可见性的体现

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×