线程状态
来源于https://www.uml-diagrams.org/java-thread-uml-state-machine-diagram-example.html
Thread vs Runnable
操作系统调用的是Thread的run方法,子类必须重写这个方法,否则会什么都不做,或者传入Runnable,赋值给target,调用target的run方法。
getStackTrace可以用来异常情况下跟踪调用栈
sleep vs wait
sleep是Thread的方法
wait是Object的方法
sleep不会释放对象锁
wait会释放对象锁
因为释放对象锁的原因
wait需要在同步代码块中,否则会抛出IllegalMonitorException
Runnable vs Callable
通过Callable创建FutureTask()对象,进而创建线程
Callable可以有返回值,可以抛出异常
yield
只会让CPU给相同优先级的线程
interrupt vs Thread.interrupted vs thread.isInterrupted
interrupt 不会中断IO阻塞和synchronized阻塞
Thread.interrupted会清空线程的interrupt标志位.
thread.isInterrupted不会
线程池
Executor
任务提交
- 核心线程数
- 队列是否满来
- 最大线程数
- 拒绝策略
shutdown vs shutdownNow
shutdown将线程池状态至为SHUTDOWN
shutdownNow将线程池状态至为STOP
shutdown任务会继续执行,线程池只会将闲置work线程杀死,会通过tryLock方法判断worker是否空闲
shutdownNow对work线程调用interrupt,队列内任务直接返回
线程池源码分析
线程池状态定义
runWorker
如果task不为空或者getTask不为空则不会线程不会执行结束
getTask方法,实现了keepAliveTime
如何开启线程
小于核心线程数,new Worker() woker继承自AQS,用来shutdown时检测是否线程在工作
如何判断线程超时
大于核心线程数 & 允许回收超过核心线程数的线程
用poll(time, timeUnit)返回空的方式来回收线程,返回空会把timedOut至为true,在下一次自旋的时候调用compareAndDecrementWorkerCount减少核心线程数
ctl
高三位为线程状态
Running 111
SHUTDOWN 000
STOP 001
TIDYING 010
TERMINATE 011
线程池如何结束
- 调用shutdown至标志位为SHUTDOWN,然后awaitTermination
- 线程执行完任务会调用processWorkerExit,该方法会调用tryTerminate。检查线程池是不是空,是空则termination.signAll()
awaitTermination收到sign,继续运行
BlockingQueue
分组
存:offer,put,add,
取:peek,poll,take,remove,element
三种方式:
抛异常
add,remove,element
返回成功失败
offer, poll, peek
阻塞
take,put
实现类
LinkedBlockingQueue
ArrayBlockingQueue
PriorityBlockingQueue
Synchronized实现
synchronized方法常量池中多了ACC_SYNCHRONIZED标示符
代码块时通过指令monitorenter和monitorexit来完成
ContentionList
ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行,通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock-Free的队列。
因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。
EntryList
EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在Hotspot中把OnDeck的选择行为称之为“竞争切换”。
OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。
AQS
独占非公平锁
lock
此处体现了非公平,来了先试试能不能加锁。acquire方法为AQS实现的方法
acquire
tryAcquire由子类实现
getState返回AQS的state,为0表示没有线程获得锁,此处再一次尝试获取锁,也体现了非公平。如果失败,看看是不是自己,此处体现了可重入
nonfairTryAcquire,方法体现了非公平和可重入
addWaiter(Node.EXCLUSIVE)
以当前线程新建一个节点,模式为独占(排他)
先尝试一下把新节点加到队尾,compareAndSetTail方法比较有趣,此处各种offset均为变量相对于对象头的偏移量,unsafe采用提换此变量的这个偏移量的方式实现cas(所谓if else for码农确实没见过这个新鲜玩意)
如果设置失败,采用enq进行自旋设置(volatile + cas实现原子性)
此方法进行了初始化头节点设置为空的节点,addWaiter方法中如果tail==null则不处理,在end中处理。如果不为空则cas进行自旋添加到尾部
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
该方法首先判断自己是不是第二个节点,如果是则尝试拿锁,此处是为了提升效率如果拿锁失败则,shouldParkAfterFailedAcquire && parkAndCheckInterrupt
shouldParkAfterFailedAcquire
此方法用来检测,之前的节点是不是设置成了SIGNAL,还有一个作用是从后向前删除取消状态的前置节点(改了node的前置指针,改了非取消态节点的后置指针)如图,中间节点相当于无论是从前向后,还是从后向前都无法访问。通过上层的自旋,实现最后一个else设置到第一个if的成功。
+-----------+ next +-----------+ +-----------+
head | | --------> | | --------> | |
| | <-------- | <-------- | | tail
+-----------+ prev +-----------+ +-----------+
+-----------+ next +-----------+ +-----------+
head | | --------------------------------> | |
| | <-------------------------------- | | tail
+-----------+ prev +-----------+ +-----------+
parkAndCheckInterrupt
该方法先park当前线程,如果被唤醒则,返回线程是否被interrupt & 重置interrupt位。顺便说下LockSupport.park方法会使线程进入等待状态,非Block状态。由LockSupport.park方法的注视可知,如果该方法会因为三种情况返回,有人调用了unpark,有人调用了interrupt,没有原因的错误。所以acquireQueued方法进行了检验是不是第二个元素如果不是继续休眠。之所以调用Thread.interrupted而不是this.isInterrupted方法是因为,防止shouldParkAfterFailedAcquire方法空转,因为有人调用interrupt会导致LockSupport.park方法立即返回。
最后在finally里面调用了cancelAcquire方法,截图太长,粘代码了
/**
* Cancels an ongoing attempt to acquire.
*
* @param node the node
*/
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
该方法调用不保证成功,
主要作用有两个,一个是唤起下一个等待的(应该不是主要作用),另一个是删除队列中CANCLE的节点。分三种情况:
1是自己是队尾,就把自己设置成队尾。
2. pred已经是对头了或者自己不是头节点状态为CANCEL或者自己不是头节点
此方法后续再看
至此加锁过程已看完
解锁
解锁过程公平锁和非公平锁一样
unlock
release
tryRelease
释放锁时先判断是不是当前持有锁的线程在释放锁,不是的话跑异常,是的话就不需要同步,进行state的值的减少,如果减少后为0则至为free。释放锁成功后唤醒head的后继节点
尝试把当前节点(head)的ws设置为0,从后向前找到离自己最近的需要唤醒的节点 & 唤醒他。此时为多线程并发操作队列。意外的情况可能有,此时该节点已被唤醒(没影响)、已被取消(会tryAcquire失败),进入到shouldParkAfterFailedAcquire方法,删除掉自己。