一、 并发的主题和三个问题
-
主题:线程安全
我们到底要干什么
从程序员的角度来讲,线程安全,即并发时必须保证多线程任务执行顺序的正确性。为了保证这个正确性:
· JMM(Java Memory Model)提供了一些保障:happens-before、as-if-serial机制;
· JMM提供了一些API:volatile、synchronized;
· JDK也提供了一些并发包:JUC(Java Util Concurrent)。程序员在面对并发问题时,主要任务就是根据JVM和JDK提供的工具,对多个想要同时访问某资源的线程进行约束,从而保障并发的安全性。可以说,程序员编写并发程序,最重要的就是要保证并发安全。
线程安全是怎么做到的
到底怎么实现线程安全?应该用什么机制?换句话说,前面提到的我们应使用的工具,是怎么实现线程安全的?
· 阻塞同步(如synchronized、AQS同步器等)
· 非阻塞同步(一般采用CAS实现)
· 无同步方案(ThreadLocal等)线程安全,说白了,就是希望各个线程按照顺序访问资源。那么最直接的思路,就是保证资源在同一时刻,只被一个线程占有,这就是同步;
阻塞同步和非阻塞同步,逻辑上来说是一样的,只是实现的方式有所区别:阻塞同步通过让在资源抢占过程中失败的线程进行挂起,来保证同步,本质上是一种悲观锁的思想;而非阻塞同步则是通过循环CAS的方式,尝试是否有线程在占用资源,有就进行下一次尝试,没有就占用资源,本质上是一种乐观锁的思想。另外一个思路就是,回避并发。程序是由数据(meta data)和代码(code)组成的,线程安全保护是其中的数据。那么如果某方法不存在共享数据的访问,或者访问的数据可以在本线程内直接拿到,也算是解决了这个问题。
-
三个问题:原子性、可见性和有序性
什么是原子性、可见性和有序性,为什么要保证他们的实现
这三个问题是对线程安全的细化要求。
· 原子性:类似于数据库里的“事务”的概念,强调一系列操作的不可分割性。
若某操作不具备原子性,那么在并发的环境中,它在执行的过程中有可能被其他操作打断,出现“操作到一半”的数据暴露在其他线程面前的情况,所以,线程安全的操作需要实现原子性。· 可见性:某线程对共享变量的修改须被其他线程得知。
可见性是相对于JMM中“所有对变量的读写都必须在工作内存中完成”来说的,即正常情况下,线程间互相不知道各自对变量的更改。所以若想同步,某线程对共享变量的修改须被其他线程得知,即实现可见性。· 有序性:代码逻辑上的有序性。
有序性是相对于指令重排序和多线程并发访问的随机性来说的,正是这两个可能造成语句顺序混乱的机制,导致我们需要保证代码逻辑上的有序性,即实现有序性。
-
重要的话
在后面的讨论中,会对几乎所有并发工具进行讨论。讨论的主要思路就是
- 它采用了哪种机制(阻塞/非阻塞同步/无同步)?
- 它是否保证了上述三个性质?是怎么实现的?
二、JMM(Java Memory Model)对三个问题作出的保障
-
有序性保障——happens-before与重排序、并发访问
重排序
为了提高程序处理性能 ,编译器和处理器可能会对代码执行顺序进行乱序执行:int i = 5;//1 int k = 1;//2 int j = 5;//3 i = 10; //4 k= i; //5
在如上代码片中,1、2和3可以进行重排序。因为1和2都被赋值为了5,在机器码层面上,可以取出5这个值,依次赋给i和j,再执行给k赋为1的操作,这样少了一次5这个数字的取出操作。
但是,并不是所有语句都可以乱序排序的,要依照JMM提供的happens-before机制。JMM同时也规定并保证了常用的几种happens-before的实现。
happens-before
happens-before很简单,就是在语句顺序可能被打乱的前提下,保证程序逻辑的正确性,即保证程序的有序性。
换个角度,就是保证语句对其后的语句是可见的。换句话说,语句所造成的“影响”,应该被其后(可能受到它影响的)语句所得知。从这个角度来看,也保证了可见性。比如上述代码片中,4与5就不可以进行重排序,因为语句5用到了语句4运行的结果,4的执行与否对5有“影响”。
JMM 对 happens-before的一些支持
- 程序顺序规则:一个线程中的操作 happens-before 其后任意操作
- 监视器规则:monitor锁的解锁 happens-before 之后对此锁的加锁
- volatile规则:volatile写 happens-before 其后对这个变量的volatile读
- start()规则:线程A执行ThreadB.start() 则 start() happens-before 线程B中的任意操作
- join()规则:线程A执行ThreadB.join() 则 B中任意操作 happens-before A从join语句中返回
- 传递性:A happens-before B 且 B happens-before C 则 A happens-before C
可以看出来,JMM对基本的程序逻辑有序进行了保证,也对sychronized和volatile关键字都进行了支持。
-
可见性的逻辑支持
JMM内存模型
主内存:各线程公用,存储了所有变量。
工作内存:各线程私有,保存着该线程可能用到的变量,拷贝自主内存。线程对变量的所有操作必须在工作线程内进行。这种内存模型,可以通过工作内存与主内存的数据交互,来完成数据可见性的实现,事实上,volatile就是这么做的(后面再说)。
-
原子性的逻辑支持
锁或循环CAS
原子性的实现只能依靠锁(悲观/乐观)来实现,因为原子性要依赖于对资源的独占。
-
JVM提供的 两种并发方法及其三个问题的解决
volatile
【深入学习并发之二】volatile关键字详解
synchronized
【深入学习并发之三】synchronized关键字详解
三、JUC提供的并发工具及他们完成的工作
- AQS(Abstract Queued Synchronizer)
//链接坑位 - CAS(Compare And Swap)
//链接坑位 - ReentrantLock
//链接坑位 - CountDownLatch
//链接坑位 - CyclicBarrier
//链接坑位 - Semaphore
//链接坑位
四、集合类并发工具
//链接坑位