目录
AbstractQueuedSynchronizer(AQS)
version:jdk1.8
什么是JUC
java并发包名称缩写:JUC (java.util.concurrent)
juc中主要内容
AbstractQueuedSynchronizer(AQS)
AQS,AbstractQueuedSynchronizer,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。
AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过CAS修改同步状态的值。当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将 state=1。如果 state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
AQS实现锁的大体过程
- 通过一个int的state变量来记录锁状态+锁重入次数
- 对int state使用cas实现抢锁,加锁,解锁,调用navicate方法底层使用 lock cmpxchg
- 获取不到锁的线程使用自旋,阻塞,自旋+阻塞,最后放入阻塞队列中
- 释放锁使用自旋,阻塞唤醒
AQS核心方法
acquire() 获取锁
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire() 尝试获取锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//ReentantLock的tryAcquire的实现
// 尝试直接获取锁,返回值是boolean,代表是否获取到锁
// 返回true:1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// state == 0 此时此刻没有线程持有锁
if (c == 0) {
// 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
// 看看有没有别人在队列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
// 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了
// 因为刚刚还没人的,我判断过了
compareAndSetState(0, acquires)) {
// 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
setExclusiveOwnerThread(current);
return true;
}
}
// 会进入这个else if分支,说明是重入了,需要操作:state=state+1
// 这里不存在并发问题
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁
// 回到上面一个外层调用方法继续看:
// if (!tryAcquire(arg)
// && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// selfInterrupt();
return false;
}
acquireQueued() 添加暂未获取到锁线程到阻塞队列中
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取p的前置节点
final Node p = node.predecessor();
//p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
//注意,同步队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为同步队列
// 所以当前节点可以去试抢一下锁
// 节省CPU上下文切换的时间,[非公平锁比公平锁效率高的原因之一]
// 首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,
// enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程
// 也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 到这里,说明上面的if分支没有成功,要么当前node本来就不是对头
// 要么就是tryAcquire(arg)没有抢赢别人
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
addWaiter() 线程添加到node节点中
//作用:构造节点以及加入到队列里面
//参数mode此时是Node.EXCLUSIVE,表示独占模式
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//快速加入
Node pred = tail;
if (pred != null) {
node.prev = pred;
//从CAS来设置加入到队列里面
//这里node和tail进行了绑定
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 说明 pred==null(队列是空的) 或者 CAS失败(有线程在竞争入队)
//通过死循环的方法来保证节点的正确添加
enq(node);
return node;
}
enq() 线程节点放入阻塞队列中
// 采用自旋的方式入队
// 到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,
// 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,就多次竞争,总会排到的
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 队列为空也会进来这里
if (t == null) { // Must initialize
// 初始化head节点
// 原来 head 和 tail 初始化的时候都是 null 的
// 还是一步CAS,你懂的,现在可能是很多线程同时进来呢
if (compareAndSetHead(new Node()))
// 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
// 这个时候有了head,但是tail还是null,设置一下,
// 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
// 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return
// 所以,设置完了以后,继续for循环,下次就到下面的else分支了
tail = head;
} else {
// 下面几行,和上一个方法 addWaiter 是一样的,
// 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
release() 解锁
public final boolean release(int arg) {
// 判断有没有被释放
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
//表明该锁被重入了多次,当前并没有解锁
return false;
}
tryRelease()
// 回到ReentrantLock看tryRelease方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否完全释放锁
boolean free = false;
// 其实就是重入的问题,如果c==0,也就是说没有嵌套锁了,可以释放了,否则还不能释放掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor()唤醒后续的节点
// 唤醒后继节点
// 参数node是head头结点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果head节点当前waitStatus<0, 将其修改为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1)
// 从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找,中间有节点取消(waitStatus==1)的情况
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
并发工具包
CountDownLatch用于某个线程等待其他线程执行完任务再执行,与thread.join()功能类似。
CyclicBarrier(同步屏障),用于一组线程互相等待到某个状态,然后这组线程再同时执行。允许重置计数器
Semaphore类似于锁,它用于控制同时访问特定资源的线程数量,控制并发线程数
线程池
线程池执行原理
常见线程池
常见的线程池有 FixedThreadPool、SingleThreadExecutor、CachedThreadPool 和 ScheduledThreadPool。这几个都是 ExecutorService (线程池)实例。
- newFixedThreadPool
固定线程数的线程池。任何时间点,最多只有 nThreads 个线程处于活动状态执行任务。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
使用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会拒绝任务,即不会调用RejectedExecutionHandler.rejectedExecution()方法。maxThreadPoolSize 是无效参数,故将它的值设置为与 coreThreadPoolSize 一致。keepAliveTime 也是无效参数,设置为0L,因为此线程池里所有线程都是核心线程,核心线程不会被回收(除非设置了executor.allowCoreThreadTimeOut(true))。
适用场景:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。需要注意的是,FixedThreadPool 不会拒绝任务,在任务比较多的时候会导致 OOM。
- newSingleThreadExecutor
只有一个线程的线程池。
public static ExecutionService newSingleThreadExecutor() {
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
使用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的任务放入工作队列,线程处理完任务就循环从队列里获取任务执行。保证顺序的执行各个任务。适用场景:适用于串行执行任务的场景,一个任务一个任务地执行。在任务比较多的时候也是会导致 OOM。
- newCachedThreadPool
根据需要创建新线程的线程池。
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
如果主线程提交任务的速度高于线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。使用没有容量的SynchronousQueue作为线程池工作队列,当线程池有空闲线程时,SynchronousQueue.offer(Runnable task)提交的任务会被空闲线程处理,否则会创建新的线程处理任务。
适用场景:用于并发执行大量短期的小任务。CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
- ScheduledThreadPool
在给定的延迟后运行任务,或者定期执行任务。在实际项目中基本不会被用到,因为有其他方案选择比如quartz。使用的任务队列 DelayQueue 封装了一个 PriorityQueue,PriorityQueue 会对队列中的任务进行排序,时间早的任务先被执行(即ScheduledFutureTask 的 time 变量小的先执行),如果time相同则先提交的任务会被先执行(ScheduledFutureTask 的 squenceNumber 变量小的先执行)。
执行周期任务步骤:
- 线程从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任务是指 ScheduledFutureTask的 time 大于等于当前系统的时间;
- 执行这个 ScheduledFutureTask;
- 修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时间;
- 把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。
线程池7大入参
分别是 corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。
那么接下来分别说说这七个参数。
- corePoolSize 线程池核心线程大小,线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
- maximumPoolSize 线程池最大线程数量,一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
- keepAliveTime 空闲线程存活时间,一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
- unit keepAliveTime的计量单位
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
- workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
ArrayBlockingQueue 有界队列
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
LinkedBlockingQuene 无界队列
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
SynchronousQuene 直接提交
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
PriorityBlockingQueue 优先级队列
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和SynchronousQueue。线程池的排队策略与BlockingQueue有关 (可查看线程池 newScheduledThreadPool 的创建方式)。
- threadFactory 线程的工厂
用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程做些更有意义的事情,比如 设定线程名、设置优先级等等
- handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
CallerRunsPolicy 只用调用者所在线程来运行任务该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
AbortPolicy 直接抛出异常 默认该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
DiscardPolicy 不处理,丢弃掉该策略下,直接丢弃任务,什么都不做。
DiscardOldestPolicy 丢弃队列里最近的一个任务,并执行当前任务该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务
个人基于源码分析,学习总结分享,如有问题,请联系博主,蟹蟹!