JAVA并发多线程原理详解
2023-07-04 08:53:26 0 举报
AI智能生成
知识要点和原理解析,详细信息和图片均在黄色图标的注释中,鼠标移动到黄色图标上即会显示,图片加载有时较慢。
作者其他创作
大纲/内容
线程基础信息
概念<br>
线程创建方式<br>
推荐使用Runnable<br>
线程<b>五大</b>状态
创建状态
就绪状态
运行状态
阻塞状态
死亡状态<br>
线程操作<br>
yield<br>
线程礼让<br>
Thread方法,<b>释放CPU</b>
join<br>
合并线程(插队)<br>
Thread方法,<b>释放CPU</b><br>
wait<br>
线程阻塞等待
Object类方法,<b>在同步方法或代码块中使用</b>,无需捕获异常,<b>会释放CPU</b>,需要notify或notifyAll唤醒;<br>
notify/notifyAll<br>
线程唤醒
Object中的方法,<b>在同步方法或代码块中使用</b>,无需捕获异常,唤醒被wait阻塞的线程,和wait配合用于线程间协作;
CPU线程调度<br>
时间片<br>
抢占式<br>
java内存模型<b>JMM</b><br>
目的
主要是为了规定了线程和内存之间的一些关系
并发三大特性<br>
1、原子性<br>
2、可见性<br>
3、有序性<br>
synchronized如何保证以上三个特性<br>
同步锁<br>
对JMM语义的规定
访问规则<br>
交互流程
volatile变量规则<br>
2种特性
保证变量的可见性<br>
屏蔽指令重排序<br>
无法保证原子性,一般需要配合CAS<br>
单例陷阱——双重检查锁中的指令重排问题<br>
示例<br>
1、没有锁机制,不安全
2、对方法加同步锁,方面层面性能开销大<br>
3、双重检查锁,解决了安全和开销,但可能出现指令重排<br>
问题的根源<br>
双重检查锁问题解决方案<br>
代码加入volatile关键字
ThreadLocal(线程变量)<br>
操作方法
<b>set</b>方法
<b>get</b>方法
讲解:ThreadlocalMap为ThreadLocal的成员变量,但这里它是<b><font color="#f44336">当前线程t的一个属性,ThreadLocal本身也就是this作为key使用</font></b>,一个线程可以使用多个ThreadLocal,不同的ThreadLocal会保存在Entry[] table数组中的不同位置。
ThreadLocal弱引用之内存泄漏
ThreadLocal其实是与线程绑定的一个变量
上图中分为栈和堆两部分,程序进入方法时(进栈),会在栈中创建对象的相关引用,<b><font color="#f44336">方法结束(出栈)后引用会被清除</font></b>,对象实际存储在堆中,被栈中的数据所引用。
<b><font color="#f44336">真正产生内存泄露的原因是线程生命周期没结束,线程未销毁导致Entry强引用始终存在。</font></b>
ThreadLocalMap解决Hash冲突<br>
线性探测法<br>
CAS(比较并交换)<br>
操作原理
缺点<br>
ABA问题
自旋CPU开销较大<br>
多变量共享一致性问题<br>
CLH队列锁(也属于自旋锁)<br>
CAS自旋锁缺点
锁饥饿问题<br>
性能问题<br>
CLH锁解决CAS什么问题<br>
1、避免饥饿问题<br>
首先它将线程<b><font color="#f44336">组织成一个队列</font></b>,保证<b><font color="#f44336">先请求的线程先获得锁</font></b>,避免了饥饿问题。
2、锁状态去中心化<br>
其次<b><font color="#f44336">锁状态去中心化</font></b>,<b><font color="#f44336">让每个线程在不同的状态变量中自旋</font></b>,<b>这样当一个线程释放它的锁时,只能使其后续线程的高速缓存失效,<font color="#f44336">缩小了影响范围</font>,从而减少了 CPU 的开销。</b>
CLH锁缺点<br>
每个等待锁的线程一直自旋等待<br>
适应场合
<b><font color="#f44336">自旋锁适用于锁占用时间短的场合</font></b>
CLH锁原理
有一个尾节点指针
通过这个尾结点指针来构建等待线程的逻辑队列,当有新的节点加入队列时,尾节点指针会指向这个新加入的节点,并将原本的尾节点变为当前新加入节点的前驱节点。因此能确保线程线程先到先服务的公平性,尾指针可以说是构建逻辑队列的桥梁;<b><font color="#f44336">此外这个尾节点指针是原子引用类型</font></b>,避免了多线程并发操作的线程安全性问题。
线程在自己的某个变量上自旋等待<br>
通过等待锁的每个线程在自己的某个变量上自旋等待,这个变量指向自己的前驱节点中的变量,通过不断地自旋,感知到前驱节点的变化后成功获取到锁。
代码解析
获得锁、释放锁过程
优缺点<br>
优点<br>
性能优异,获取和释放锁开销小<br>
公平锁<br>
实现简单,易于理解<br>
扩展性强<br>
缺点
1、有自旋操作,当锁持有时间长时会带来较大的 CPU 开销<br>
2、基本的 CLH 锁功能单一,不改造不能支持复杂的功能
AQS 对 CLH 队列锁的改造<br>
1、扩展每个节点waitStatus的状态(CLH的状态只有true和false)<br>
2、显式的维护前驱节点(CLH没有前驱节点)<br>
3、后继节点以及诸如出队节点显式设为 null 等辅助 GC 的优化<br>
AQS维护了一个<b><font color="#f44336">volatile int state</font></b>(代表共享资源)和一个<b><font color="#f44336">FIFO线程等待队列</font></b>(多线程争用资源被阻塞时会进入此队列)<br>
LockSupport<br>
特点
<b><font color="#f44336">许可只有一个,不可累加</font></b>
<b><font color="#f44336">LockSupport是不可重入的,如果一个线程连续2次调用LockSupport.park(),那么该线程一定会一直阻塞下去</font></b>
<b><font color="#000000">线程如果因为调用park而阻塞的话</font><font color="#f44336">,能够响应</font><font color="#b71c1c">中断请求(中断状态被设置成true)</font><font color="#f44336">,但是不会抛出InterruptedException</font></b>
<b><font color="#f44336">休眠期间不会释放所持有的锁(与锁无关)</font></b>
阻塞和唤醒线程原理<br>
每个线程都<b><font color="#f44336">有一个Parker实例</font></b>,LockSupport就是通过控制变量<b><font color="#f44336">_counter</font></b>来对线程阻塞唤醒进行控制的。原理有<b><font color="#f44336">点类似于信号量机制</font></b>。
相关函数<br>
park:线程阻塞<br>
LockSupport.park()<br>
LockSupport.parkNanos(long nanos)<br>
UNSAFE.park(false, nanos);<br>
LockSupport.parkUntil(long deadline)
parkNanos和parkUntil的区别<br>
<b><font color="#f44336">parkNanos</font></b>表示等待多长时间,时间单位是纳秒<br>
<b><font color="#f44336">parkUntil</font></b>表示等待到什么时间,时间单位是毫秒
扩展
LockSupport.park(Object blocker)<br>
LockSupport.parkNanos(Object blocker, long nanos)<br>
LockSupport.parkUntil(Object blocker, long deadline)<br>
为何增加park(Object blocker)扩展<br>
blocker是用来标识当前线程在等待的对象,<b><font color="#f44336">该对象主要用于问题排查和系统监控</font></b>
unpark:线程唤醒
LockSupport.unpark(Thread thread)
连续多次唤醒的效果和一次唤醒是一样
interrupt()与park()<br>
interrupt()被park()阻塞的线程的<b><font color="#f44336">中断状态为true(不会主动消耗中断状态)</font></b>,<font color="#f44336"><b>并调用unpark</b></font>,但<b><font color="#f44336">不会抛出中断异常</font></b>,<b>如果没有将中断状态清除(设置为false),后续对该线程的park阻塞将都失效</b><br>
sleep() / wait() / join()调用后一定<b><font color="#f44336">会消耗掉中断状态</font></b>,无论interrupt()操作先做还是后做
阻塞队列
BlockingQueue<br>
应用场景
BlockingQueue接口<br>
入队逻辑<br>
出队逻辑
常用API<br>
常见阻塞队列<br>
超时等待原理
ArrayBlockingQueue<br>
ArrayBlockingQueue基础<br>
ArrayBlockingQueue特性<br>
ArrayBlockingQueue适用场景<br>
生产速度和消费速度基本匹配<br>
代码分析
ArrayBlockingQueue-核心属性<br>
put-入队方法<br>
put主要做了几件事情<br>
enqueue主要做了几件事情<br>
await-阻塞<br>
构建条件等待队列如下<br>
take-出队方法<br>
图示
LinkedBlockingQueue<br>
LinkedBlockingQueue类图
构造方法<br>
Node结构<br>
关键属性<br>
队列特征<br>
入队方法-put<br>
enqueue<br>
出队方法- take<br>
dequeue
问题
和ArrayBlockingQueue的异同<br>
ArrayBlockingQueue里插入元素后,是调用的notEmpty.signal(),怎么这里还不一样了?<br>
Condition<br>
工作原理<br>
接口<br>
Condition也必须和Lock一起使用<br>
ReadLock读锁不支持Condition,WriteLock写锁和互斥锁都支持Condition<br>
condition
await实现<br>
signal实现<br>
Synchronized相关
概要
重量级锁膨胀过程
五种类型锁的特点
对象内存布局
Monitor对象
锁优化<br>
加锁解锁流程
锁的核心结构(3个队列一个属性)
加锁解锁关键步骤
我们能做什么
设置notify后入队模式,控制唤醒顺序
对象在内存中的布局<br>
对象内存布局解析<br>
Mark word<br>
五种状态、四个取值
lock与biased_lock结合<br>
堆里new出来的对象<br>
synchronized关键字下的锁状态升级<br>
无锁<br>
偏向锁<br>
例子<br>
无锁、偏向锁的 lock 标志位是一样的,都是 01
线程栈<br>
关联Lock Record<br>
偏向锁状态时Lock Record与对象头关系<br>
偏向锁小结<br>
1、偏向锁的"锁"即是Mark Word<br>
2、撤销操作可能需要在安全点执行,效率比较低<br>
3、<b><font color="#f44336">偏向锁的重入计数依靠线程栈里Lock Record个数</font></b><br>
4、偏向锁撤销失败,最终会升级为轻量级锁<br>
5、偏向锁退出时并没有修改Mark Word,也就是没有释放锁<br>
偏向锁相比轻量级锁的优势<br>
为什么要引入偏向锁
偏向锁的撤销过程<br>
<b><font color="#f44336">偏向锁使用了一种等到竞争出现才释放偏向锁的机制</font></b>
轻量级锁<br>
自旋阈值<br>
锁对象和栈帧<br>
栈帧lockRecord
重入锁 - 栈帧lockRecord
轻量级锁小结<br>
1、轻量级锁的"锁"即是Mark Word,修改需要CAS<br>
2、如果初始锁为无锁状态,则每次进入都需要一次CAS尝试修改为轻量级锁,否则判断是否重入
3、如果不满足2的条件,则膨胀为重量级锁<br>
4、轻量级锁退出时即释放锁,变为无锁状态<br>
5、可以看出轻量级锁比较敏感,一旦有线程竞争就会膨胀为重量级锁<br>
部分源码
轻量级锁相比重量级锁的优势
1、每次加锁只需要一次CAS;<br>
2、不需要分配ObjectMonitor对象;<br>
3、线程无需挂起与唤醒;<br>
只有拿到锁的线程才会有释放锁的操作,为什么此处还需要CAS呢?<br>
值得注意的是<br>
重量级锁<br>
Object Monitor区域和工作过程<br>
重量级锁为什么"重"?<br>
偏向、轻量、重量三者不同点<br>
各种锁的优缺点对比<br>
锁优化<br>
锁消除<br>
逃逸和逃逸分析<br>
锁粗化<br>
示例
Monitor对象<br>
ObjectMonitor 的运用<br>
ObjectMonitor类<br>
Monitor结构<br>
膨胀的流程<br>
加锁和解锁<br>
加锁
初次尝试加锁 - ObjectMonitor::enter()<br>
再次尝试加锁<br>
尝试一次
自旋10次
入队后尝试一次
挂起
流程图<br>
双向链表_EntryList、单向链表 _cxq(栈)<br>
解锁
解锁流程 - ObjectMonitor::exit()
可重入锁:<font color="#f44336"><b>_recursions</b></font><br>
Object.notify()<br>
当前占有锁的线程释放锁后会唤醒阻塞等待锁的线程
加锁解锁流程图<br>
重量级锁小结<br>
重量级锁的理解核心<br>
流程总结<br>
尝试获得锁时
当线程释放锁时
线程获得锁后调用<font color="#f44336"><b>Object#wait</b></font>方法<br>
问题<br>
synchronize 底层维护了几个列表存放被阻塞的线程?<br>
为什么ObjectMonitor需要cxq和entryList两个等待队列<br>
cxq队列中等待线程,什么时候会进到EntryList<br>
等待队列中多个线程,唤醒的顺序是什么<br>
偏向锁和轻量级锁下线程是否可以wait和notify<br>
<b>cxq</b>和<b>waitset</b>数据结构有什么区别<br>
<b>cxq</b>是一个<b><font color="#f44336">双向链表</font></b>,采用<font color="#f44336"><b>先进后出</b></font>的策略
<b>waiset</b>是一个<b><font color="#f44336">回环链表</font></b>,采用<font color="#f44336"><b>先进先出</b></font>的策略
为什么调用 Object 的 wait/notify/notifyAll 方法,需要加 synchronized 锁?<br>
<font color="#f44336"><b>notify/notifyAll后的线程和等待队列中线程,谁会优先抢到锁</b></font>
如果是notify<br>
如果是notifyAll<br>
Synchronized有类似AQS的公平锁/非公平锁逻辑吗<br>
AQS
简介
AQS最核心的数据结构是一个<b><font color="#f44336">volatile int state</font></b> 和 一个<font color="#f44336"><b>FIFO线程等待对列</b></font>。state代表共享资源的数量(可以大于1),<b><font color="#f44336">如果是互斥访问,一般设置为1,而如果是共享访问,可以设置为N(N为可共享线程的个数)</font></b>;而线程等待队列是一个双向链表(阻塞队列),无法立即获得锁而进入阻塞状态的线程会加入队列的尾部。当然<b><font color="#f44336">对state以及队列的操作都是采用了volatile + CAS + 自旋的操作方式</font></b>,采用的是乐观锁的概念。
核心行为<br>
管理同步状态<br>
state,等于0时释放锁
维护同步队列<br>
阻塞和唤醒线程<br>
实现原理:<b><font color="#f44336">state+CLH带头节点的双向链表</font></b><br>
AQS存在头节点head,创建时是空节点,<b><font color="#f44336">当同步队列中的线程抢到锁,则将其设置为head节点,head节点代表当前占用线程的节点</font></b>,head节点对应的<b><font color="#f44336">线程执行完毕时</font></b>,会释放资源,<b><font color="#f44336">修改state状态,并唤醒下一节点</font></b>(head.next)。
两种资源共享方式:独占(Exclusive)、共享(Share)<br>
waitStatus枚举值<br>
自定义同步器<br>
自定义同步器实现时主要实现以下几种方法
isHeldExclusively()<br>
tryAcquire(int)<br>
tryRelease(int)<br>
tryAcquireShared(int)<br>
tryReleaseShared(int)<br>
代码实现(以非公平锁为例)<br>
获取锁的入口:ReentrantLock.lock()<br>
NonfairSync.lock<br>
acquire()<br>
NonfairSync.<b>tryAcquire</b><br>
nonfairTryAcquire<br>
加锁成功修改owner和state值
<b>addWaiter</b><br>
enq
<b>acquireQueued</b><br>
shouldParkAfterFailedAcquire
判断当前节点是否在获取独占操作权失败后进入阻塞状态<br>
为什么检测到0后,一定要设置成SIGNAL,然后继续下一次循环。直接返回true不行吗<br>
parkAndCheckInterrupt<br>
用于让当前节点所代表的线程进入阻塞状态,并监控其是否收到了“中断”信号<br>
有两种途径可以唤醒该线程<br>
退出acquireQueued的条件:线程获得锁,否则阻塞在for循环内,线程被唤醒或中断后,重新进入for循环获取锁
释放锁入口:ReentrantLock.unlock()<br>
release()
tryRelease()
unparkSuccessor()<br>
cancelAcquire(xx)<br>
源码
获取锁、释放锁流程图
可/不可中断的独占锁<br>
不可中断的独占锁<br>
可中断的独占锁<br>
问题
为什么需要selfInterrupt()?
Condition<b><font color="#f44336">等待队列</font></b>
<b><font color="#f44336">ReentrantLock</font></b>通过<b><font color="#f44336">Condition</font></b>实现类似wait、notify功能<br>
主要方法<br>
await
void await() throws InterruptedException<br>
long awaitNanos(long nanosTimeout)<br>
boolean await(long time, TimeUnit unit)throws InterruptedException<br>
boolean awaitUntil(Date deadline) throws InterruptedException
signal<br>
signalAll<br>
与synchronized与wait()和nitofy()/notifyAll()功能比较
需要与显式锁Lock配合使用<br>
进入等待队列后会释放锁和cpu<br>
await提供了比wait更加强大的机制<br>
多路通知<br>
有选择性地通知<br>
支持超时<br>
实现原理<br>
<b><font color="#f44336">等待队列</font></b><br>
特点
调用condition.await方法后线程依次<font color="#f44336"><b>尾插入</b></font>到等待队列中<br>
等待队列是一个<font color="#f44336"><b>单向队列</b></font><br>
多个等待队列<br>
await<br>
源码<br>
addConditionWaiter<br>
fullyRelease<br>
调用release(savedState)
isOnSyncQueue
超时机制的支持<br>
不响应中断的支持<br>
signal/signalAll<br>
signal<br>
doSignal<br>
transferForSignal<br>
signalAll<br>
总体关系<br>
同步队列与等待队列的异同点<br>
Condition.await/Condition.signal 与Object.wait/Object.notify区别<br>
相同点<br>
不同点
ReentrantLock<br>
加锁流程<br>
ReentrantLock 非公平锁的构造<br>
ReentrantLock <font color="#f44336"><b>lock()</b></font> 默认实现非公平锁<br>
acquire()<br>
tryAcquire部分由组件自己实现,加入同步队列和阻塞由AQS框架统一
tryAcquire(xx)
加锁流程图
非公平锁的释放<br>
tryRelease<br>
方法
lock()
<font color="#f44336"><b>不可被打断</b></font>
void lockInterruptibly() throws InterruptedException<br>
<font color="#f44336"><b>可以被打断</b></font><br>
tryLock()<br>
设置超时时间<br>
void unlock()<br>
newCondition()<br>
ReentrantLock 与synchronized 异同点<br>
相同点<br>
基本数据结构<br>
实现功能<br>
不同点<br>
实现方式<br>
提供给外界功能<br>
性能区别<br>
synchronized 更耗性能??<br>
对比一下synchronized、 ReentrantLock在高并发的场景下如何处理线程的挂起与唤醒<br>
举证一
synchronized<br>
ReentrantLock<br>
由此可以看出,synchronized 与ReentrantLock 底层挂起线程实现方式是一致的
举证二<br>
ReentrantLock<br>
synchronized<br>
通过比对源码分析ReentrantLock 和 synchronized的CAS、线程挂起方式,发现两者底层实现是一致的。
阻塞队列-BlockingQueue
BlockingQueue接口<br>
入队逻辑<br>
<b>offer(E e)</b>:队列没满,插入成功返回true;<font color="#f44336"><b>队列已满,不阻塞,直接返回false</b></font>
<b>offer(E e,long timeout,TimeUnit unit)</b>:如果队列满了,<font color="#f44336"><b>设置阻塞等待时间</b></font>。超时为入队,返回false<br>
<b>put(E e)</b> :队列没满时正常插入,<font color="#f44336"><b>队列已满则阻塞</b></font>,等待队列空出位置
出队逻辑<br>
<b>poll()</b>:如果队列有数据,出队;<font color="#f44336"><b>如果没有数据,返回null(不阻塞)</b></font><br>
<b>poll(long timeout, TimeUnit unit)</b>:可<b><font color="#f44336">设置阻塞时间</font></b>,如果队列没有数据则阻塞,<b><font color="#f44336">超过阻塞时间,则返回null</font></b>;<br>
<b>take()</b>:队列里有数据会正常取出数据并删除;但是<b><font color="#f44336">如果队列里无数据,则阻塞</font></b>,直到队列里有数据;<br>
<b>peek()</b>:<b><font color="#f44336">获取队首元素,但不移除</font></b>,队列为空则返回null;
常用API总结<br>
抛出异常<br>
add、remove、element<br>
返回结果但不抛出异常<br>
offer、poll、peek<br>
阻塞<br>
put、take<br>
ArrayBlockingQueue<br>
适用场景<br>
<b>生产速度和消费速度基本匹配</b>(当生产方或消费方线程很快,则会导致该部分线程一直阻塞)
结构特点<br>
指定容量[<b><font color="#f44336">有界队列</font></b>],不可扩容<br>
存储结构<b><font color="#f44336">final Object[] items</font></b>;存储队列内容<br>
<b><font color="#f44336">一把锁</font></b>。线程安全由独占锁<b><font color="#f44336">ReentrantLock</font></b>保证[入队、出队都由独占锁锁住]<br>
两条指针分别指向消费索引和生产索引<br>
数据结构[<b><font color="#f44336">环形数组</font></b>]
核心属性<br>
items<br>
存放元素的数组<br>
takeIndex<br>
获取的指针位置<br>
putIndex<br>
插入的指针位置<br>
count<br>
队列中的元素个数<br>
lock<br>
ReentrantLock内部锁<br>
notEmpty<br>
消费者等待队列<br>
notFull<br>
生产者等待队列
put-入队方法<br>
示意图
take-出队方法<br>
示意图<br>
<b><font color="#f44336">阻塞时,不会解除锁占用</font></b>
LinkedBlockingQueue<br>
特点<br>
内部由单链表实现的 <b><font color="#f44336">可选有界阻塞队列</font></b>
队列空间不足时<br>
put方法一直阻塞直到有多余空间<br>
队列元素为空时<br>
take方法一直阻塞直到有元素加入<br>
使用非公平锁ReentrantLock进行并发控制<br>
如果不指定容量,默认为Integer.MAX_VALUE,也就是无界队列<br>
<b><font color="#f44336">阻塞时,不会解除锁占用</font></b>
<b><font color="#f44336">出队</font></b>和<b><font color="#f44336">入队</font></b>使用了<b><font color="#f44336">不同的锁</font></b><br>
出队:<b><font color="#f44336">takeLock</font></b><br>
入队:<b><font color="#f44336">putLock</font></b>
相关方法
和ArrayBlockingQueue的异同
队列长度不同<br>
存储方式不同
锁不同<br>
ArrayBlockingQueue采用数组存储元素,因此在插入和移除过程中不需要生成额外对象,LinkedBlockingQueue会生成新的Node节点,对gc会有影响;
线程池<br>
优势<br>
降低资源消耗<br>
提高响应速度<br>
提高线程的可管理性<br>
调用execute() 方法添加一个任务时<br>
拒绝策略<br>
AbortPolicy<br>
丢弃任务,抛出运行时异常<br>
CallerRunsPolicy<br>
由提交任务的线程来执行任务<br>
DiscardPolicy<br>
丢弃这个任务,但是不抛异常<br>
DiscardOldestPolicy<br>
从队列中剔除最先进入队列的任务,然后再次提交任务
workQueue缓冲队列<br>
有界的任务队列-ArrayBlockingQueue
无界的任务队列-LinkedBlockingQueue<br>
直接提交的队列-SynchronousQueue<br>
优先任务队列-PriorityBlockingQueue<br>
功能线程池(自动创建)<br>
定长线程池(FixedThreadPool)<br>
线程池数量固定(即定长),阻塞队列无界(<b><font color="#f44336">LinkedBlockingQueue</font></b>)<br>
单线程化线程池(SingleThreadExecutor)<br>
只有一个线程,阻塞队列无界<font color="#f44336"><b>(LinkedBlockingQueue)</b></font><br>
可缓存线程池(CachedThreadPool)<br>
线程数无限,无核心线程,每个任务都创建新线程,自动回收多余线程,任务队列为不存储元素的阻塞队列<b><font color="#f44336">(SynchronousQueue)</font></b>
定时线程池(ScheduledThreadPool )<br>
核心线程数量固定,非核心线程数量无限,任务队列为延时阻塞队列<b><font color="#f44336">(DelayedWorkQueue)</font></b><br>
禁止直接使用Executors创建线程池原因<br>
手动创建(推荐)-原生ThreadPoolExecutor创建线程池<br>
核心参数<br>
corePoolSize(必需)<br>
maximumPoolSize(必需)<br>
keepAliveTime(必需)<br>
unit(必需)<br>
workQueue(必需)<br>
threadFactory(可选)<br>
handler(可选)<br>
关闭线程池<br>
shutdownNow()<br>
shutdown()<br>
isTerminated()<br>
线程实现复用的原理<br>
线程封装
线程在线程池内部其实是被封装成一个<b><font color="#f44336">Worker对象</font></b>,Worker<b><font color="#f44336">继承了AQS</font></b>,也就是有一定锁的特性。<br>
线程执行<br>
runWorker()方法<br>
这里有个一个细节就是,因为<b>Worker继承了AQS,每次在执行任务之前都会调用Worker的lock方法</b>,<b>执行完任务之后,会调用unlock方法</b>,这样做的<b><font color="#f44336">目的</font></b>就可以<b><font color="#f44336">通过Woker的加锁状态就能判断出当前线程是否正在运行任务</font></b>。如果想知道线程是否正在运行任务,只需要调用Woker的tryLock方法,根据是否加锁成功就能判断,加锁成功说明当前线程没有加锁,也就没有执行任务了,<b>在调用shutdown方法关闭线程池的时候,就用这种方式来判断线程有没有在执行任务,如果没有的话,来尝试打断没有执行任务的线程</b>。<br>
如何获取任务的以及如何实现超时
getTask()方法<br>
核心代码:Runnable r = timed ? <b><font color="#f44336">workQueue.poll</font></b>(keepAliveTime, TimeUnit.NANOSECONDS) : <b><font color="#f44336">workQueue.take</font></b>();
线程通讯及安全<br>
线程通讯调度
线程状态转换<br>
调度方法<br>
涉及同步<br>
JVM如何避免不正常地调用<br>
wait() / wait(long timeout)<br>
wait 执行流程<br>
1、使当前执行代码的线程进行等待. (把线程放到等待队列中)<br>
2、<b><font color="#f44336">释放当前的锁</font></b>,然后唤醒同步队列里的节点。<br>
4、调用ParkEvent挂起自己<br>
3、当线程被唤醒后(此时线程是在同步队列里),调用ReenterI(xx)竞争锁<br>
结束等待的条件<br>
其他线程调用该对象的 notify 方法<br>
wait 等待时间超时 (wait 方法提供⼀个带有 timeout 参数的版本, 来指定等待时间)<br>
其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常
wait() VS wait(long timeout)<br>
<b>wait(long timeout)</b> <br>
当线程超过设定时间后,会自动恢复执行
使用有参的 wait(long timeout)方法,线程会进入 TIMED_WAITING
wait()
无限期等待<br>
使用无参的 wait() 方法,线程会进入 WAITING
notify()<br>
唤醒等待的线程<br>
方法 notify() 也要在<b><font color="#f44336">同步方法或同步块中调用</font></b>,该方法是用来通知那些可能<b><font color="#f44336">等待该对象的对象锁</font>的其它线程</b>,对其发出通知notify,并使它们重新获取该对象的对象锁<br>
<span style="font-size: inherit;">如果有多个线程等待,则由线程调度器</span><b style="font-size: inherit;"><font color="#b71c1c">随机</font><font color="#f44336">挑选出⼀个呈 wait 状态的线程</font></b><span style="font-size: inherit;">。(并没有 "先来后到")</span><br>
这里的随机也是有规则的
在 notify() 方法后,<b><font color="#f44336">当前线程</font>不会马上释放该对象锁</b>,<b>要等到执行 notify() 方法的线程将程序执行完,也就是<font color="#f44336">退出同步代码块之后才会释放对象锁</font></b>;<br>
notify 操作没有释放锁
源码
1、将节点从等待队列里移出<br>
2、移动到同步队列<br>
notifyAll()<br>
⼀次唤醒所有的等待线程<br>
notifyAll() 并不是唤醒所有 wait 等待的线程,而是唤醒<b><font color="#f44336">当前对象(lock)</font></b>处于 wait 等待的所有线程
wait/notify/notifyAll 流程图<br>
问题<br>
线程A成功获取锁后,线程B再次获取锁会失败,线程B该如何自处?<br>
线程A成功获取锁后,调用wait()方法,此时线程A处在什么状态?<br>
线程A退出临界区释放锁后,又做了什么?<br>
线程B调用notify()方法,发生了什么?<br>
notify 唤醒线程是随机的吗?
什么是虚假唤醒?<br>
流程总结
wait VS sleep<br>
wait(0) 与 sleep(0) 的区别<br>
Thread.sleep(0) 表示重新触发一次 CPU 竞争<br>
wait(0) 表示无期限的等待,直到有线程唤醒它为止
wait 和 sleep 释放锁<br>
wait方法在执行的时候都会释放锁<br>
sleep不会释放锁<br>
相同点和异同点<br>
相同点<br>
都可以让线程休眠<br>
都可以响应 interrupt 的响应<br>
不同点<br>
wait 必须在 synchronized 中使⽤,而 sleep 却不⽤<br>
sleep 是 Thread 的⽅法,而 wait 是 Object 的⽅法<br>
sleep 不释放锁,wait 释放锁<br>
sleep 有明确的终止等待时间,而 wait 有可能无限期的等待下去<br>
sleep 和 wait 产生的线程状态是不同的,<b><font color="#f44336">sleep 是 TIMED_WAITING 状态</font></b>,而 wait 是 WAITING 状态
yield<br>
让出当前线<br>
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态(仍然有可能被执行),然后调度执行其它线程<br>
具体的实现依赖于操作系统的任务调度器<br>
join<br>
用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。<br>
interrupt:线程中断
作用
作用是中断线程。将会设置该线程的中断状态位,即设置为true<br>
如何中断线程
<b><font color="#f44336">运行状态线程</font></b>(running或者是runnable两种状态)<br>
<b><font color="#f44336">不会中断一个正在运行的线程</font></b>,任务中一般都会有循环结构,只要用一个标记控制住循环
<b><font color="#f44336">等待状态线程(</font></b>Object.wait()、Object.wait(long)、Object.wait(long,int)、Thread.join、Thread.join(long,int)、Thread.sleep(long,int)<b><font color="#f44336">)</font></b>
中断状态复位
1、interrupted()<br>
测试当前线程是否已经中断(静态方法),<b><font color="#f44336">具有清除状态的功能</font></b><br>
2、异常使线程复位<br>
代码示例
为什么要复位<br>
isInterrupted()<br>
测试线程是否已经中断,<b><font color="#f44336">但是不能清除状态标识</font></b>
<font color="#f44336"><b>线程的终止原理</b></font>
源码
对线程中断的反应<br>
RUNNABLE<br>
如果<b><font color="#f44336">线程在运行中</font></b>,<b>interrupt()只是会设置线程的中断标志位,没有任何其它作用</b>。线程应该在运行过程中合适的位置检查中断标志位,比如说,如果主体代码是一个循环,可以在循环开始处进行检查
WAITING/TIMED_WAITING<br>
会使得该线程抛出<b><font color="#f44336">InterruptedException</font></b>,需要注意的是,抛出异常后,<font color="#f44336"><b>中断标志位会被清空</b></font>(线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。),而不是被设置。<b><font color="#f44336">InterruptedException是一个受检异常,线程必须进行处理。</font></b><br>
Object.wait()、Object.wait(long)、Object.wait(long,int)、Thread.join、Thread.join(long,int)、Thread.sleep(long,int)<b><font color="#f44336">以上阻塞方法被中断时会消耗中断状态</font></b>
BLOCKED<br>
如果<b><font color="#f44336">线程在等待锁</font></b>,对线程对象调用interrupt()<b><font color="#f44336">只是会设置线程的中断标志位,线程依然会处于BLOCKED状态</font></b>,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。
NEW/TERMINATE
如果线程<b><font color="#f44336">尚未启动(NEW)</font></b>,或者<font color="#f44336"><b>已经结束(TERMINATED)</b></font>,则调用interrupt()对它<b><font color="#f44336">没有任何效果</font></b>,中断标志位也不会被设置。
线程安全
线程安全解决思路<br>
互斥和同步<br>
互斥<br>
如:synchronized、ReentrantLock<br>
同步<br>
如:wait/notify
非阻塞同步<br>
如:CAS指令<br>
存在问题
1.ABA<br>
2.自旋消耗资源<br>
3.多变量共享一致性问题<br>
非阻塞同步对于阻塞同步而言主要解决了<b><font color="#f44336">阻塞同步中线程阻塞和唤醒带来的性能问题</font></b>
无需同步<br>
如:ThreadLocal、final<br>
线程间的通信<br>
思路
关键字 volatile<br>
等待、通知机制<br>
Object类的wait()和notify()
wait()/notify()/notifyAll() 必须配合 synchronized 使用,<b><font color="#f44336">wait 方法释放锁,notify 方法不释放锁</font></b>。<br>
LockSupport<br>
LockSupport对指定线程阻塞或唤醒,与<b><font color="#f44336">锁资源无关</font></b>
Thread.join()<br>
阻塞当前执行线程,当被join 的线程执行完再重新执行
并发包工具类<br>
CountDownLatch<br>
Condition 结合 ReentrantLock<br>
condition.await()<br>
condition.signal()<br>
<b><font color="#f44336">唤醒condition条件队列中等待的线程,当前线程并不会释放锁</font></b>
异步线程
Runnable和Callable<br>
Callable接口<br>
如何使用Callable
Future机制原理<br>
FutureTask继承关系<br>
使用示例<br>
Future源码解析<br>
线程执行状态值<br>
FutureTask源码解析<br>
FutureTask的run方法<br>
set方法
finishCompletion()<br>
FutureTask中的waiters<br>
多个线程等待
get方法<br>
awaitDone<br>
report(int s)
FutureTask的取消<br>
Future的局限性<br>
CompletionService<br>
针对Future的不足<br>
CompletionService接口<br>
ExecutorCompletionService
核心实现
关键属性<br>
重写FutureTask<br>
构造器<br>
执行任务<br>
获取任务返回值<br>
应用场景<br>
1、询价应用:向不同电商平台询价,并保存价格<br>
ThreadPoolExecutor+Future方式
CompletionService方式<br>
2:实现类似 Dubbo 的 Forking Cluster场景<br>
代码模拟实现<br>
应用场景总结<br>
CompletableFuture<br>
CompletableFuture的API<br>
runAsync():异步运行<br>
supplyAsync()<br>
CompletionStage<br>
Funtion方法会产生结果<br>
Comsumer会消耗结果<br>
Runable既不产生结果也不消耗结果<br>
源码分析<br>
CompletableFuture<br>
runAsync()<br>
AsyncRun类<br>
Completion<br>
uniAccept<br>
UniAccept类<br>
uniCompletion<br>
示例代码原理进行分析<br>
示例代码
运作流程<br>
1、主线程调用CompletableFuture的supplyAsync()方法
AsyncSupply实现了Runnable的run()方法<br>
2、main线程会继续执行CompletableFuture的thenAccept<br>
3、回到“源任务”<br>
postComplete<br>
pushStack<br>
使用踩坑
总结<br>
0 条评论
下一页