并发与多线程,你未必知道的10大考点!(内含线程池、CAS与ABA等知识详解)

随着技术人才大幅增加以及公司招聘更加严苛,程序员的职场正面临着史无前例的激烈竞争。以Java为例,不只要了解操做系统、掌握JVM等知识点,还要深耕数据结构与算法,掌握Spring全家桶等框架。java

而在这其中,对于并发与多线程的处理,也是一个优秀的技术工程师成长过程当中必须攻下的难关。它贯穿着平常工做,也是入职面试重点考察的重点。程序员

咱们用5分钟复习一下并发与多线程。面试

01知识点汇总

在这里插入图片描述

多线程协做时,由于对资源的锁定与等待会产生死锁,须要了解产生死锁的四个基本条件,要明白竞争条件与临界区的概念,知道经过破坏形成死锁的4个条件来防止死锁。算法

除了了解进程间的通讯方式,还要知道线程的通讯方式,通讯主要指线程之间的协做机制,例如wait、notify编程

另外须要知道java为多线程提供的一些机制,例如threadlocal用来保存线程独享的数据,fork/join机制用于大任务的分割与汇总,volatile对多线程数据可见性的保证以及线程的中断机制。安全

其余还有: threadlocal的实现机制。fork/join的工做窃取算法等内容。数据结构

02面试考察点

一、要理解线程的同步与互斥的原理,包括临界资源、临界区的概念,知道重量级锁、轻量级锁、自旋锁、偏向锁、重入锁、读写锁的概念。多线程

二、要掌握线程安全相关机制,例如 cas、synchronized、lock三种同步方式的实现原理、要明白threadlocal是每一个线程独享的局部变量,了解threadlocal使用弱引用的ThreadLocalMap保存不一样的threadlocal变量。并发

三、要了解JUC中的工具类的使用场景与主要的几种工具类的实现原理,例如reentrantlock,concurrenthashmap、longadder等实现方式框架

四、要熟悉线程池的原理、使用场景、经常使用配置,例如大量短时间任务的场景适合使用cached线程池;系统资源比较紧张时,能够选择固定线程池。

另外注意慎用无界队列,可能会有oom的风险。

五、要深入理解线程的同步与异步、阻塞与非阻塞,同步和异步的区别是任务是不是同一个线程执行,阻塞与非阻塞的区别是异步执行任务时,线程是否是会阻塞等待结果,仍是会继续执行后续逻辑。

03知识点详解

一、详解-线程的状态转换

先介绍线程状态转换。

线程是jvm执行任务的最小单元,理解线程的状态转换是理解后续多线程问题的基础。

在这里插入图片描述

在jvm运行中,线程一共有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六种状态,这些状态对应Thread.State枚举类中的状态。

当建立一个线程的时候,线程处在new状态,运行thread的start方法后,线程进入runnable可运行状态。

这个时候,全部可运行状态的线程并不能立刻运行,而是须要先进入就绪状态等待线程调度,如图中间的ready状态。在获取到cpu后才能进入运行状态,如图中的running。运行状态能够随着不一样条件转换成除new之外的其余状态。

在运行态中的线程进入synchronized同步块或者同步方法时,若是获取锁失败,则会进入到blocked状态。当获取到锁后,会从blocked状态恢复到就绪状态。

运行中的线程还会进入等待状态,这两个等待一个是有超时时间的等待,例如调用object.wait、thread.join等。另一个时无超时的等待,例如调用thread.join或者locksupport.park。

这两种等待均可以经过notify或unpark结束等待状态恢复到就绪状态。

最后是线程运行完成结束时,线程状态变成TERMINATED

二、详解-CAS与ABA问题

解决线程同步与互斥的主要方式是cas、synchronized、和lock。

cas是属于乐观锁的一种实现,是一种轻量级锁,juc中不少工具类的实现就是基于cas。

cas操做是线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其余线程修改则写回,若已被修改,则从新执行读取流程。这是一种乐观策略,认为并发操做并不总会发生。

比较并写回的操做是经过操做系统原语实现的,保证执行过程当中不会被中断。

CAS容易出现ABA问题,若是线程T1读取值A以后,发生过两次写入,先由线程T2写回了b,又由t3写回了a,此时t1在写回比较时,值仍是a,就没法判断是否发生过修改。

aba问题不必定会影响结果,但仍是须要防范,解决的办法能够增长额外的标志位或者时间戳。juc工具包中提供了这样的类。

三、详解-synchronized

synchronized是最经常使用的线程同步手段之一,它是如何保证同一时刻只有一个线程能够进入临界区呢?

咱们知道synchronized是对对象进行加锁,在JVM中,对象在内存中分为三块区域:对象头、实例数据和对齐填充。在对象头中保存了锁标志位和指向monitor对象的起始地址。当monitor被某个线程持有后,就会处于锁定状态,owner部分会指向持有monitor对象的线程。另外monitor中还有两个队列,用来存放进入及等待获取锁的线程。

synchronized应用在方法上时,在字节码中是经过方法的ACC_SYNCHRONIZED标志来实现的,synchronized应用在同步块上时,在字节码中是经过monitorenter和monitorexit实现的。

针对synchronized获取锁的方式,jvm使用了锁升级的优化方式,就是先使用偏向锁优先同一线程再次获取锁,若是失败,就升级为CAS轻量级锁,若是再失败会短暂自旋,防止线程被系统挂起。最后若是以上都失败就是升级为重量级锁。

四、详解-aqs与lock

在介绍lock前,先介绍aqs,也就是队列同步器,这是实现lock的基础。

aqs有一个state标记位,值为1时表示有线程占用,其余线程须要进入到同步队列等待。同步队列是一个双向链表。

当得到锁的线程须要等待某个条件时,会进入condition的等待队列,等待队列能够有多个。

当condition条件知足时,线程会从等待队列从新进入到同步队列进行获取锁的竞争。

reentrantlock就是基于aqs实现的,reentrantlock内部有公平锁和非公平锁两种实现,差异就在于新来的线程会不会比已经在同步队列中的等待线程更早得到锁。

和reentrantlock实现方式相似,semaphore也是基于aqs,差异在于reentrantlock是独占锁,semaphore是共享锁。

五、详解-线程池

线程池经过复用线程,避免线程频繁建立和销毁。

java的Executors工具类中,提供了5种类型线程池的建立方法,它们的特色和适用场景以下:

第1种是:固定大小线程池,特色是线程数固定,使用无界队列,适用于任务数量不均匀的场景、对内存压力不敏感,但系统负载比较敏感的场景;
第2种是:cached线程池,特色是不限制线程数,适用于要求低延迟的短时间任务场景;
第3种是:单线程线程池,也就是一个线程的固定线程池,适用于须要异步执行但须要保证任务顺序的场景;
第4种是:scheduled线程池,适用于按期执行任务场景,支持按固定频率按期执行和按固定延时按期执行两种方式;
第5种是:工做窃取线程池,使用的ForkJoinPool,是固定并行度的多任务队列,适合任务执行时长不均匀的场景。

六、详解-线程池参数介绍

前面提到的线程池,除了工做窃取线程池外,都是经过ThreadPoolExecutor的不一样初始化参数来建立的。

在这里插入图片描述

第一个参数设置核心线程数。默认状况下核心线程会一直存活。

第二个参数设置最大线程数。决定线程池最多能够建立的多少线程。

第三个参数和第四个参数用来设置线程空闲时间,和空闲时间的单位,当线程闲置超过空闲时间就会被销毁。能够经过allowCoreThreadTimeOut方法来容许核心线程被回收。

第五个参数设置缓冲队列,图中左下方的三个队列是设置线程池时常使用的缓冲队列。其中ArrayBlockingQueue是一个有界队列,就是指队列有最大容量限制。LinkedBlockingQueue是无界队列,就是队列不限制容量。最后一个是SynchronousQueue,是一个同步队列,内部没有缓冲区。

第六个设置线程池工厂方法,线程工厂用来建立新线程,能够用来对线程的一些属性进行定制,例如线程的group、线程名、优先级等。通常使用默认工厂类便可。

第七个设置线程池满时的拒绝策略。如右下角所示有四种策略,abort策略在线程池满后,提交新任务时会抛出RejectedExecutionException,这个也是默认的拒绝策略。

Discard策略会在提交失败时对任务直接进行丢弃。CallerRuns策略会在提交失败时,由提交任务的线程直接执行提交的任务。DiscardOldest策略会丢弃最先提交的任务。

前面的5种线程池都是使用怎样的参数来建立的呢?

固定大小线程池建立时核心和最大线程数都设置成指定的线程数,这样线程池中就只会使用固定大小的线程数。队列使用无界队列linkedblockingqueue。

single线程池就是线程数设置为1的固定线程池。cached线程池的核心线程数设置为0,最大线程数是Integer.MAX_VALUE,主要是经过把缓冲队列设置成SynchronousQueue,这样只要没有空闲线程就会新建。scheduled线程池与前几种不一样的是使用了DelayedWorkQueue,这是一种按延迟时间获取任务的优先级队列。

七、详解-线程池执行流程

咱们向线程提交任务时可使用execute和submit,区别就是submit能够返回一个future对象,经过Future对象能够了解任务执行状况,能够取消任务的执行,还可获取执行结果或执行异常。submit最终也是经过execute执行的。

线程池提交任务时的执行顺序以下:

向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数,若是不大于,就建立一个核心线程来执行任务。

若是大于核心线程数,就会判断缓冲队列是否满了,若是没有满,则放入队列,等待线程空闲时执行任务。
若是队列已经满了,则判断是否达到了线程池设置的最大线程数,若是没有达到,就建立新线程来执行任务。
若是已经达到了最大线程数,则执行指定的拒绝策略。这里须要注意队列的判断与最大线程数判断的顺序,不要搞反。

八、详解-juc工具类

前面基础知识部分已经提到过,juc是java提供的用于多线程处理的工具类库,其中的经常使用工具类的做用以下:

在这里插入图片描述

第一行的类都是基本数据类型的原子类:包括atomicboolean、atomiclong、atomicinteger类。

AtomicLong经过unsafe类实现,基于CAS。unsafe类是底层工具类,juc中不少类的底层都使用到了unsafe包中的功能。unsafe类提供了相似c的指针操做,提供CAS等功能。Unsafe类中的全部方法都是native修饰的;

另外longadder等四个类是jdk1.8中提供的更高效的操做类。LongAdder基于Cell实现,使用分段锁思想,是一种空间换时间的策略,更适合高并发场景;

LongAccumulator提供了比LongAdder更强大的功能,可以指定对数据的操做规则,例如能够把对数据的相加操做改为相乘操做。

第二行中的类提供了对对象的原子读写功能,后两个类AtomicStampedReference和AtomicMarkableReference是用来解决咱们前面提到的ABA问题,分别基于时间戳和标记位来解决。

九、详解-juc2

在这里插入图片描述

这一页表格中,第一行的类主要是锁相关的类,例如咱们前面介绍过的reentrant重入锁。

与ReentrantLock的独占锁不一样,Semaphore是共享锁,容许多个线程共享资源,适用于限制使用共享资源线程数量的场景,例如100个车辆要使用20个停车位,那么最多容许20个车占用停车位。

StampedLock是1.8改进的读写锁,是使用一种CLH的乐观锁,可以有效防止写饥饿。所谓写饥饿就是在多线程读写时,读线程访问很是频繁,致使老是有读线程占用资源,写线程很难加上写锁。

第二行中主要是异步执行相关的类,这里能够重点了解jdk1.8中提供的CompletableFuture,能够支持流式调用,能够方便的进行多future的组合使用,例如能够同时执行两个异步任务,而后对执行结果进行合并处理。还能够很方便的设置完成时间。

另一个是1.7中提供的ForkJoinPool,采用分治思想,将大任务分解成多个小任务处理,而后在合并处理结果。ForkJoinPool的特色是使用工做窃取算法,能够有效平衡多任务时间长短不一的场景。

十、详情-juc3

在这里插入图片描述

表格中第一行是经常使用的阻塞队列,刚才讲解线程池时已经简单介绍过了,这里在补充一点,LinkedBlockingDeque是双端队列,也就是能够分别从队头和队尾操做入队、出队。

而ArrayBlockingQueue单端队列,只能从队尾入队,队头出队。

第二行是控制多线程协做时使用的类。其中CountDownLatch实现计数器功能,能够用来控制等待多个线程执行任务后进行汇总。

CyclicBarrier可让一组线程等待至某个状态以后,再所有同时执行,通常在测试时使用,可让多线程更好的并发执行。

Semaphore前面已经介绍过,用来控制对共享资源的访问并发度。

最后一行是比较经常使用的两个集合类,能够了解一下CopyOnWriteArrayList,COW经过在写入数据时进行copy修改,而后在更新引用的方式,来消除并行读写中的锁使用,比较适合读多写少,数据量比较小,可是并发很是高的场景。

04面试加分项

掌握了上面这些内容,若是能作到这几点加分项,必定会给面试官留下更好的印象。

一、能够结合实际项目经验或者实际案例介绍原理,例如介绍线程池设置时,能够提到本身的项目中有一个须要高吞吐量的场景,使用了cached的线程池;

二、若是有过解决多线程问题的经验或者排查思路的话会得到面试加分;

三、可以熟悉经常使用的线程分析工具与方法,例如会用jstack分析线程的运行状态,查找锁对象持有情况等;

四、了解Java8对JUC工具类作了哪些加强,例如提供了longadder来替换atomiclong,更适合并发度比较高的场景;

五、能够了解Reactive异步编程思想,了解back pressure背压的概念与应用场景。

以上内容摘取自《32个Java面试必考点》第04讲:并发与多线程,点此学习更多

相关文章
相关标签/搜索