文章目录
目录
一、多线程专题
1、线程与进程的区别
类型 | 根本区别 | 切换速度 |
---|---|---|
线程 | 线程是处理器任务调度与执行的基本单位 | 同一类型线程共享数据空间与所在进程的资源,切换不涉及虚拟地址空间,且每个线程有独立的程序计数器与虚拟机栈,线程切换速度快; |
进程 | 进程是操作系统资源分配的基本单位 | 进程之间的地址空间与资源都是相互独立的,进程切换涉及到:切换页表以使用新的虚拟地址空间(进程切换后,页表缓存失效,导致了虚拟地址转换为物理地址很慢)、内核态与用户态的切换、硬件上下文的切换, |
2、实现多线程的方式
方式 | 描述 |
---|---|
实现Runnable接口 / Callable接口 | ① 重写run()方法或者call()方法 ② Runnable接口无返回值,Callable接口有返回值 ③ call()可以抛出异常,run()不行 ④ Callable任务提供了Future对象(阻塞式等待),可以通过它获取异步执行结果 |
继承Thread类 | 缺点:不能继承其它类 |
线程池 | 4种线程池类型、5种阻塞队列、7大核心参数、4种拒绝策略 |
3、线程的五种状态
状态 | 时机 | 描述 |
---|---|---|
新建 | new Thread() | 线程创建后的初始状态 |
就绪 | 调用start()方法时 | 进入就绪状态,等待被CPU调用后自动执行run(),现在不执行 |
运行 | CPU调用就绪状态的线程时,执行run() | 只有就绪状态才能变成运行状态,run()方法前必须执行start(),否则直接执行run()相当于一个普通方法,而且是在主线程中执行的 |
阻塞 | 调用阻塞方法时 | ① 等待阻塞: 调用wait()方法时 ② 同步阻塞: 获取synchronized锁失败时 ③ 其它阻塞: 调用sleep() / join()方法时,或者发出I/O请求时;阻塞完会线程为重新变为就绪状态 |
死亡 | 线程执行结束或异常 | ① run() / call() 执行结束,线程正常结束; ② 线程抛出未捕获的异常(Runnable接口不能捕获异常,Callable可以捕获) ③ 调用stop()方法,容易造成死锁,不推荐用 |
4、线程死锁及解决办法
1)死锁条件
死锁条件 | 描述 | 破坏死锁条件 |
---|---|---|
互斥 | 任一时刻资源只被一个线程获取 | 不能破坏,我们本来就想让临界资源互斥 |
请求与保持 | 一个进程因请求资源阻塞时,对已持有的资源保持不放 | 一次性申请全部的资源 |
不剥夺 | 线程在没有使用完资源前,不能被其它线程所剥夺,只能主动释放 | 当申请不到其它资源时,主动释放自己持有的资源 |
循环等待 | 互相等待对方释放资源,造成头尾相连 | 锁排序,指定获取锁的顺序;资源排序,按顺序获取资源 |
2)死锁解决
方法 | 关键 | 描述 |
---|---|---|
死锁预防 | 破坏死锁条件 | ① 允许资源抢占: 当一个线程长时间申请不到其它资源时,主动释放自己所持有的资源; ② 一次性申请全部的资源 ③ 资源排序或者锁排序,按顺序申请资源 |
死锁避免 | 系统在进行资源分配之前,计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待; | 银行家算法: 保证循环等待条件不成立 |
死锁检测 | 通过dfs深度优先遍历,遍历资源分配图检测是否存在环 | ① 无环:没有死锁 ② 有环,但是涉及到的资源类有多个资源:不一定会死锁; ③ 有环,但是涉及到的资源类仅有一个资源:死锁; |
死锁解除 | 终止死锁进程 / 抢占死锁进程的资源 | 打破循环等待条件 |
鸵鸟策略 | 忽略死锁 | 例如Linux、Windows等处理死锁都不采取任何行动,保持高性能 |
5、sleep()、wait(),join()、yield()
区别 | sleep() | wait() | yield | join |
---|---|---|---|---|
归属类 | JVM提供的Thread类静态方法 | JDK提供的Object类实例方法 | JVM提供的Thread类静态方法 | JDK提供的Object类实例方法(底层是wait()方法) |
作用 | 暂停线程 | 暂停线程 | 重回就绪状态 | 线程调度 |
是否释放锁 | 睡眠不释放锁 | 进入阻塞队列,释放锁 | 不释放锁 | 不释放锁 |
使用时机 | 任何时候 | 搭配synchronized关键字使用 | 想使得同级别的线程重回就绪状态时 | 需要等待异步线程执行完毕返回结果时 |
解除状态 | 超时或调用Interrupt()方法 | 其他线程调用notify()或notifyAll()方法 | 根本不会暂停线程,因此不存在唤醒条件 | 当异步线程执行结束时 |
解除后的状态 | 运行 | 就绪 | 就绪 | 运行 |
6、notify()与notifyAll()的区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象关联的Monitor监视器的阻塞队列WaitSet中,阻塞队列中的线程不会去竞争该对象的锁。
-
notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。
-
notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
7、为什么wait()、notify()、notifyAll()方法不在JVM提供的Thread类静态方法里
- ① 因为Java提供的锁是对象级的而不是线程级别的,每个对象都关联了一个Monitor监视器对象,因此对应的wait()、notify()、notifyAll()方法也是对象级别的(即JDK提供的Object方法);
- ② 如果这些方法定义在JVM中的Thread类中,那么线程正在等待的是哪个对象锁就不明确了;
8、为什么wait()与notify()方法要放在同步代码块中执行?
- ① 这是Java的API强制要求的,如果不这样做,会抛出【Monitor状态异常】的异常,
- ② 同时也是为了避免wait()与notify()之间产生竞争条件;
9、线程的阻塞和等待状态有什么区别?
区别就在于,线程阻塞没有获取到锁,而线程等待是已经获取到锁了;
类型 | 区别 |
---|---|
阻塞 | 当前线程试图获取锁,而锁被其他线程持有着,则当前线程进入阻塞状态。也就是线程和其他线程抢锁没抢到,就处于阻塞状态了;(此时线程还没进同步代码块) |
等待 | 线程抢到了锁进了同步代码块,(由于某种业务需求)某些条件下Object.wait()或join了,就处于了等待状态。(此时线程已经进入了同步代码块);是一种主动行为,你不知道它什么时候被阻塞,也不清楚它什么时候会恢复阻塞。 |
10、如何在两个线程间共享数据?
- 同一个 Runnable ,使用全局变量。
- 第一种:将共享数据封装到一个对象中,把这个共享数据所在的对象传递给不同的 Runnable
- 第二种:将这些 Runnable 对象作为某一个类的内部类,共享的数据作为外部类的成员变量,对共享数据的操作分配给外部类的方法来完成,以此实现对操作共享数据的互斥和通信,作为内部类的 Runnable 来操作外部类的方法,实现对数据的操作
11、什么是FutureTask
- 在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get() 方法将会阻塞。
- 一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable 接口所以它可以提交给 Executor 来执行。
12、如何检测一个线程是否持有锁?
- 在Thread类中有一个holdsLock()方法,返回ture则代表该线程目前持有锁;
13、同步代码块内的线程抛出异常会怎么办?
- 同步代码块内的线程抛出异常,会释放锁;
14、守护线程
- 守护线程是运行在后台的一种特殊线程;独立于控制中断并且周期性的执行某种任务或者等待处理某些发生的事件,JVM垃圾回收线程就是守护线程;
二、线程池专题
1、Executors创建线程池的方式
- 通过Executors创建的线程池,
底层都是ThreadPoolExecutors
,一般是这样的: - 最大线程数是无限的线程池,因为有任务就可以创建线程,因此不需要阻塞队列保存线程任务,所以通常为了防止线程数过多而导致栈溢出或堆溢出,会搭配一个有界队列;
- 最大线程数有限的线程池,因为线程任务可能会多于最大线程池数,因此等待执行的线程任务会搭配一个无界阻塞队列;
- 与时间相关的线程池,会搭配延迟队列;
类型 | 线程数 | 阻塞队列 |
---|---|---|
CachedThreadPool (可缓存线程池) | 核心线程数 = 0,非核心线程 = 无限 | 搭配有界阻塞队列:SynchronousQuene |
FixedThreadPool (可缓存线程池) | 核心线程数 = 最大核心线程数,线程不会被回收 | 搭配无界阻塞队列:LinkedBlockingQuene |
SingleThreadPool (单线程线程池) | 核心线程数 = 最大核心线程数 = 1 | 搭配无界阻塞队列:LinkedBlockingQuene |
ScheduleThreadPool (定时线程池) | 核心线程数 = 最大核心线程数,线程不会被回收; keepAliveTime = 0 | 搭配延迟队列:DelayQueue |
为什么不用Excutors创建,而推荐用ThreadPoolExcutor创建线程池?
2、ThreadPool线程池的7大核心参数、拒绝策略、阻塞队列
public ThreadPoolExecutor(int corePoolSize, // 线程池的核心线程数
int maximumPoolSize, // 线程池的最大线程数
long keepAliveTime, // 当线程数大于核心时,多余的空闲线程等待新任务的存活时间。
TimeUnit unit, // keepAliveTime的时间单位
ThreadFactory threadFactory, // 线程工厂
BlockingQueue<Runnable> workQueue,// 用来储存等待执行任务的队列
RejectedExecutionHandler handler // 拒绝策略
)
参数 | 描述 |
---|---|
corePoolSize (核心线程数) | 当提交一个任务时,线程池会创建一个新线程执行任务,此时线程不会复用。如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行。此时如果核心线程有空闲,回去阻塞队列中领取任务,此时核心线程复用。 |
maximumPoolSize (最大线程数) | 线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,此时若当前线程数小于maximumPoolSize,则可以创建新的线程(此时就是属于非核心线程)执行任务, |
keepAliveTime (非核心线程存活时间) | 当线程池中的线程数量大于corePoolSize时,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被销毁,最终会收缩到corePoolSize的大小。 |
TimeUnit | keepAliveTime的时间单位 |
workQueue (阻塞队列) | 用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列: ① ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务 ② LinkedBlockingQuene:基于链表结构的无界阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene; ③ SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。 ④ priorityBlockingQuene:具有优先级的无界阻塞队列; ⑤ DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素; |
handler (拒绝策略) | 线程池的拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种拒绝策略: ① AbortPolicy:直接抛出异常,默认策略; ② CallerRunsPolicy:用调用者所在的线程来执行任务; ③ DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务(最老的任务),并执行当前任务; ④ DiscardPolicy:直接丢弃任务,不报错; |
ThreadFactory (线程工厂) | 创建线程的工厂,可以设定线程名、线程编号等;默认使用 Executors.defaultThreadFactory() 来创建线程 通过自定义线程工厂,使用Thread.setDefaultUncaughtExceptionHandler()方法可以捕获线程池中的异常 |
3、线程池执行任务的流程
引用博主----【知识分子_ 】的博客内容
runWorker(Worker w)的方法
线程池执行流程——总结性描述(必背)
步骤 | 方法 | 描述 |
---|---|---|
Step1 | 执行execute(任务) / submit(任务) | ① 调用ctl.get() 方法,获取当前线程池状态(Running-Shutdown-Stop-Tidying-Terminated ),若当前线程池状态不是Running状态 或者为Shutdown状态但是工作队列已经为空 ,那么就直接返回,执行失败;② 调用 workerCount() 方法,获取当前【工作线程数】;基于此判断是执行拒绝策略还是执行addWorker()方法 ;③ 当 【工作线程数 > 核心线程数】 并且 【阻塞队列未满】 ,那就向阻塞队列中添加任务;添加成功后,再次调用ctl.get() 方法判断线程池当前状态;若此时线程池状态不是Running ,则删除任务并执行拒绝策略 ; |
Step2 | addWorker(任务,boolean)方法执行时机; 传入true用核心线程执行任务,false用非核心线程执行任务 | ① 当【工作线程数 < 核心线程数】 那就调用addWorker(任务,true) ,创建一个核心线程 去执行任务;② 若任务已经添加到了阻塞队列中,但是此时 工作线程数为0 ,那么调用addWorker(null,false) ,创建一个没有绑定任务的非核心线程 ,当runWorker()中的getTask()方法获取到任务后,会将阻塞队列中的任务置换到Worker对象中的null任务;③ 若任务 【工作线程数 > 核心线程数】 并且 【阻塞队列已满】 ,那么也调用addWorker(任务,true) 方法; |
Step3 | 执行addWorker(任务,boolean) 方法,返回值是boolean; 任务执行成功为true,执行失败则为false; | ① 首先调用wokerCount() ,获取当前线程池工作线程数量,若【工作线程数 > 线程池最大容量】 ,则返回false,并执行【拒绝策略】 ;② 如果没有执行拒绝策略,那么正常执行;创建一个 Worker对象 ,并与当前要执行的任务绑定 【即new Worker(任务)】,将这个Worker对象添加到Workers容器(HashSet) 中;若添加成功则在addWorker()的最后调用了Worker中任务的start() 方法,使得任务进入就绪状态 ,等待run()方法执行;③ Worker对象继承了AQS并实现了Runnable接口,因此可以当作一个并发安全的线程使用,线程池中真正工作的线程就是绑定了任务的Worker线程; |
Step4 | 分配一个线程执行run() 方法,底层调用的是runWorker(Worker w) 方法其中还有一个 getTask() 方法 | ① runWorker(Worker w) 方法取出Worker对象中封装的Runnable任务 ;② 若 任务为null ,那么就while死循环的调用getTask()方法,从阻塞队列中获取任务 ;③ getTask() 方法通过调用workcount() 方法获取当前线程池的工作线程数量,判断当前线程是核心线程还是非核心线程 ;1) 若是非核心线程:那么就调用workQueue.poll(keepAliveTime) 方法,在存活时间内从阻塞队列中获取任务,若超过了keepAliveTime还没有获得任务 ,则该非核心线程就被回收掉 ,这就是【非核心线程被回收的原理】 ;2) 若是核心线程:那么就调用workQueue.take() 一直从阻塞队列中获取任务,直到获取成功,没有期限 ;【这也是为什么核心线程能够不被回收的原理】 ;④ 获取到任务后,Worker对象内部的空Runnable任务会置换为阻塞队列中获取的Runnable对象,来达到线程复用的效果;任务不为null之后,那么就直接执行接下来的逻辑;首先执行 beforeExecute() 方法,做一些线程执行任务之前的工作;然后执行任务.run() ,开始真正的执行任务;最后在执行afterExecute() 方法,做一些线程执行任务完的一些工作;⑤ 当任务执行结束后,就会从 Workers容器中移除 ; |
-
对应Step1~Step2:执行execute()时的逻辑(submit()的逻辑一样,只是封装了一个线程执行返回结果Future对象)
- 执行逻辑:
-
addWorker()方法:创建Worker对象绑定任务线程放入Works容器中,最后调用start(),完成线程池工作线程的创建与启动
Worker对象
Worker
是ThreadPoolExecute
的一个内部类,继承了AQS
,可以通过加锁保证线程安全
Worker
实现了Runnable
接口,可以当做一个线程去使用
Worker中有两个线程变量:
firstTask
:代表任务线程。通过start方法执行任务thread
:代表Worker本身,由ThreadFactory创建,参数为this,所以创建的线程其实指向的是Worker本身,装饰者模式;
线程池中是怎么复用线程的?——runWorker()
非核心线程超时剔除是如何实现的?——getTask()
步骤 | 描述 |
---|---|
Step1 | 在源码中ThreadPoolExecutor中提供了一个内置对象Worker,Worker继承了AQS 而且实现了Runnable 接口,可以当作一个并发安全的线程去使用; |
Step2 | 当执行了run()方法时,线程池中有个方法叫做 runWorker() ,它内部有一个while()死循环,每个Worker对象 不断的调用 getTask() 方法,尝试从阻塞队列中获取任务; |
Step3 | getTask() 方法内部通过调用ctl.get()方法 ,获取工作线程数量,并判断工作线程数是否大于核心线程数;如果当前是非核心线程,那么就结合参数keepAliveTime ,在有效存活时间while()死循环调用同步队列中的workQueue.poll(keepAliveTime),如果超时则回收掉非核心线程;若是核心线程,则一直调用workQueue.take()方法获取任务,无限期限,直到线程池关闭; |
Step4 | 当getTask() 方法从阻塞队列中获取到了任务时,Worker对象内部的Runnable属性会替换为阻塞队列中获取的Runnable对象,然后调用run()方法,来达到线程复用的效果; |
- runWorker()源码
- getTask()源码
4、线程池的必会API方法
API | 作用 |
---|---|
execute() | 执行Ruannable类型的任务 |
submit() | 可用来提交Callable或Runnable任务,并返回任务执行后的结果Future对象 |
shutdown() | 温柔的关闭线程池,停止接受新任务,并执行完未完成的任务 |
shutdownNow() | 强制关闭线程池,未完成的任务会以【列表形式】返回 |
isTerminated() | 返回所有任务是否执行完毕。当调用shutdown()方法后,并且所有提交的任务完成后返回为true;当调用shutdownNow()方法后,成功停止后返回为true; |
isShutdown() | 返回线程池是否关闭,当调用shutdown()或shutdownNow()方法后返回为true。 |
5、线程池的五种状态
ctl:AtomicInteger类型,记录了线程的状态与线程数量;
状态 | 状态说明 | 切换条件 |
---|---|---|
Running | 运行状态,可以接收任务、处理任务 | 线程池被一旦被创建,就处于RUNNING状态 |
Shutdown | Shutdown状态:停止接受新任务,并执行完剩余的任务 | 执行shutdown()时,线程池由Running-> Shutdown |
Stop | stop状态:不接收新任务,不处理剩余的任务(阻塞队列中的不处理了),且会中断正在处理的任务 | 执行shutdownNow()时,线程池由Running-> Stop |
Tidying | 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING 状态,此时会执行钩子函数terminated(),用户可自定义其实现 | ① 当线程池在Shutdown状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 Shutdown -> Tidying; ②当线程池在Stop状态下,线程池中执行的任务为空时,就会由Stop-> Tidying; |
Terminated | 线程池彻底终止,就变成Terminated状态 | 线程池处在Tidying状态时,执行完terminated()之后,就会由 Tidying- > Terminated |
6、线程池出现异常如何处理?
方式 |
---|
手动在run()方法中加try-catch |
自定义线程工厂(7大参数中的),使用Thread.setDefaultUncaughtExceptionHandler方法捕获异常 |
自定义线程池,重写afterExecute进行异常处理,即可处理execute()也可以处理submit()的异常 |
-
方案一:在线程任务的run()方法中添加 try-catch,缺点就是每个线程任务都需要加try-catch,太麻烦了;
-
方案二:自定义线程工厂(7大参数中的),使用Thread.setDefaultUncaughtExceptionHandler方法捕获异常
-
方案三:自定义线程池,重写afterExecute进行异常处理,即可处理execute()也可以处理submit()的异常
7、自定义线程池的参数依据
类型 | 线程数设置 | 描述 |
---|---|---|
CPU密集型 | 线程数 = CPU核数+1 | 也叫计算密集型,系统中大部份时间用来做计算、逻辑判断等,一般而言CPU占用率相当高。多线程跑的时候,可以充分利用起所有的cpu核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间; |
IO密集型 | 线程数 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 | 指任务需要执行大量的IO操作,涉及到网络、磁盘IO操作。当线程因为IO阻塞而进入阻塞状态后,该线程的调度被操作系统内核立即停止,不再占用CPU时间片段,而其他IO线程能立即被操作系统内核调度,等IO阻塞操作完成后,原来阻塞状态的线程重新变成就绪状态,而可以被操作系统调度。所以,像数据库服务器中的IO密集型线程来说,线程的数量就应该适量的多点; |
三、AQS专题
1、什么是AQS
-
Java并发编程核心在于java.concurrent.util包,而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如
同步队列、条件队列、独占获取、共享获取
等; -
而
AQS就是定义了一套抽象行为,是一个【多线程访问共享资源的同步器框架】,同时也是一个依赖【状态(state)】的同步器
; -
AQS:抽象队列式同步器
也是除了java自带的synchronized关键字之外的锁机制。
2、AQS的底层原理
- 总结性答案: AQS锁框架基于模板方法的设计模式,提供了两种资源共享方式:独占式与共享式。使用的时候只需要继承AbstractQueuedSynchronizer类,然后实现tryAcquire()-tryRelease()或者tryAcquireShared()-tryReleaseShared()其中的一种即可;AQS的底层原理维护了一个用volatile修饰的int类型的state状态字段,用于表示当前共享资源的状态;
- 维护了一个双向链表结构的同步队列CLH用于管理因获取不到共享资源的线程的排队与释放,同步队列的链表节点存储着当前线程的信息;
- 同时提供了一个单项链表结构的条件队列,用于管理因调用了await()方法而释放锁阻塞的线程;
- 总的来说,AQS就是基于同步等待CLH队列,获取volatile修饰的共享变量state,线程通过CAS自旋的改变状态符,若改变成功则获取锁成功,失败则进入同步等待队列;
关键词 | 原理及作用 |
---|---|
state状态 | volatile int state:用于描述当前共享资源的状态,有3种访问方式:① int getState() :用于获取当前state;② void setState(int newState) :用于设置state;② boolean compareAndSetState(int expect, int update) :AQS底层通过While(!cas())的方式,不断的自旋获取锁,直到被某个线程获取成功; |
同步队列(CLH) | AQS依赖双向链表结构的CLH同步队列,用于管理因获取不到锁的线程的排队与释放,步骤如下: ① 当前线程如果获取锁失败时, AQS则会将当前线程的等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列 ,同时再一次尝试获取锁,如果获取失败则会阻塞当前线程;② 当获得锁的线程释放锁时,会把CLH种的头节点唤醒(公平锁),也可以指定不按队列顺序抢锁(非公平锁),使其再次尝试获取锁; ③ 此外,若调用signal() / signalAll()等方法进行线程唤醒时,条件队列中被唤醒的线程节点就会移动到同步队列中,等待再次获取锁; |
条件队列 | AQS依赖一个单向链表结构的条件队列,用nextWaiter来连接,用于管理调用了await()方法而阻塞的队列; 当获得锁的线程(即同步队列的首节点),调用 await() 的时候会释放锁,然后线程会加入到条件队列,当调用signal()/signalAll() 唤醒的时候会把条件队列中由于await()而阻塞的线程节点移动到同步队列中的尾节点处,等待再次获得锁 |
资源共享方式 | ① Exclusive(独占式) :只有一个线程能够执行,如ReentrantLock;此外,AQS可以根据同步队列是否按照FIFO的方式分配,分为公平锁与非公平锁 ;② Share(共享式) :多个线程可以同时执行,如Semaphore(信号量)、CountDownLatch(倒计时器)、CyclicBarrier(正计时器:循环栅栏) 、ReadWriteLock(读写锁) |
3、AQS底层设计模式
-
AQS底层基于模板方法模式:
可以通过自定义同步器来设计锁的模式【继承AbstractQueuedSynchronizer,并重写指定的方法(即重写对于共享资源state的获取和释放逻辑)】,至于同步队列与条件队列的维护,AQS已经在底层实现好了,不需要我们处理; -
自定义同步器的步骤:
步骤 | 描述 |
---|---|
Step1 | 继承AbstractQueuedSynchronizer,并实现指定的方法(即重写对于共享资源state的获取和释放逻辑),主要有以下几种: ① isHeldExclusively() :该线程是否正在独占资源。只有用到条件队列时才需要去实现它;② tryAcquire(int) :【独占方式】,尝试获取资源,成功则返回true,失败则返回false;③ tryRelease(int) :【独占方式】,尝试释放资源,成功则返回true,失败则返回false;④ tryAcquireShared(int) :【共享方式】,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源;⑤ tryReleaseShared(int) :【共享方式】,尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false; |
Step2 | 将AQS组合在自定义同步组件的实现中,并【调用其模板方法】,而这些模板方法会调用使用者重写的方法 |
- 举例说明:
独占锁ReentrantLock,与共享锁:Semaphore(信号量)、CountDownLatch(倒计时器)、CyclicBarrier(正计时器:循环栅栏) 、ReadWriteLock(读写锁)都是基于AQS延伸的产物;
类型 | |
---|---|
ReentrantLock(可重入独占式锁) | ① 继承AbstractQueuedSynchronizer类,重写tryAcquire()-tryRelease()方法; ② state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1,之后其他线程再想tryAcquire()时就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁;A释放锁之前,自己可以重复获取此锁(state累加),这就是可重入的概念; |
CountDownLatch(倒计时器) | ① 继承AbstractQueuedSynchronizer类,重写tryAcquireShared()-tryReleaseShared()方法; ② 任务分N个子线程去执行,state就初始化为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS的-1。当N子线程全部执行完毕,state=0,会unpark()主线程,主线程就会从await()函数返回,继续之后的动作; |
不可重入互斥锁 | ① 继承AbstractQueuedSynchronizer类,重写tryAcquire()-tryRelease()方法; ② 锁资源(state)只有两种状态:0:未被锁定;1:锁定。 |
4、如何实现等到线程池中的线程执行完毕后,再执行主线程?
1)使用AQS框架中的共享式资源CountDownLatch(减计数器,不可重复使用)
或者 CyclicBarrier(循环栅栏)
CountDownLatch(多线程减计数器)
:调用countDown()
方法每执行一个线程计数器-1
;然后调用await()
方法阻塞,当计数器为0时,即表示线程池中的线程执行完毕,可以接着执行主线程;不用重复使用;
CountDownLatch countDownLatch = new CountDownLatch(7); //7个线程
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
//业务逻辑
HashMap jiaoQiang = shxReportService.getJiaoQiang("805012016440398024554");
//每执行一个线程,计数器减一
countDownLatch.countDown();
}).start();
}
//7个线程未执行完毕,都卡在这里等待!
countDownLatch.await();
....//主线程操作省略
CyclicBarrier(循环栅栏,多线程加计数器)
,初始值代表首先要执行完成的线程个数;线程之间互相等待,执行完一个就+1,直到最后一个线程达到设定值,才能继续执行;可以重复使用;
2)使用线程池的 submit提交方式
,将每个线程的返回结果Future<?>
保存在集合list中,遍历集合list,逐一调用get()方法,因为get()方法会等待线程执行完毕获取结果
,在get()后边写主线程逻辑;