AQS和线程池
AQS
AQS 即抽象队列同步器,它是一个通过继承,来实现锁和同步组件的基础框架,它维护了一个被 volatile 修饰的 int 变量 state,用来表示当前资源被线程获取的次数;还维护 CLH 队列,它是一个双向链表,表示线程获取资源时被阻塞的等待队列。
操作 state 可以分成独占和共享,在独占方式下,获取资源逻辑是 acquire 方法,acquire 会调用 tryAcquire 方法去尝试获取资源,如果获取失败,会通过 CAS 来放到队列尾部,入队后的线程会通过自旋来获取同步状态,获取失败会根据前驱节点的等待状态来决定是否阻塞自己。当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。而共享模式下,会 tryAcquireShared 去获取,获取失败,也是类似的,会 CAS 来放到队列尾部,然后自旋来获取同步状态,会根据前驱节点的等待状态来决定是否阻塞自己。
(前驱是 SIGNAL,那么当前线程应该被阻塞;前驱是 CANCELLED,向前遍历,移除 CANCELLED 的节点;前驱结点是其他状态,那么就 CAS 设置前驱为 SIGNAL 状态)
AQS 还有一个静态内部类 ConditionObject,这个类实现了 Condition 接口,声明了一组等待/通知的方法,这些方法的功能与 Object 中的 wait/notify/notifyAll 等方法很像的,Condition 中的方法要配合锁对象使用,并且 Condition 是支持不响应中断和超时功能的等待接口。我了解了一点它的原理,ConditionObject 是基于单链表的条件队列来管理等待线程的。线程调用 await 方法进行等待时,会释放同步状态。并被封装到一个等待节点中,放到置入条件队列尾部进行等待。调用 signal 或 singalAll 方法时,队列中的等待线程将会被唤醒,重新竞争锁。一个锁对象可创建多个 ConditionObject 对象,这样一个线程可在不同的条件队列中进行等待。
AQS 有一些方法是留给子类实现的,比如 tryAcquire、tryAcquireShared、tryRelease 之类的方法。比如 ReentrantLock 就重写了 tryAcquire、tryRelease 方法。
ReentrantLock
ReentrantLock 实现了 Lock 接口,有一些 lock、tryLock 这样的方法,它主要依赖 AQS 实现,锁资源以 state 状态描述,利用 CAS 对锁资源的抢占,CLH 同步队列阻塞获取失败线程,它有三个内部类:
- Sync 继承了 AbstractQueuedSynchronizer
- NonfairSync,非公平锁
- FairSync,公平锁
非公平锁和公平锁区别是 lock 和 tryAcquire 方法实现有些不同,
- lock:非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了,如果被占用了,那么两种锁都会调用 acquire 方法。acquire 会继续调用 tryAcquire 方法,两种锁的实现逻辑并不一样的。
- tryAcquire:在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,继续等待。
默认是使用非公平锁,刚才我也说了区别主要是 lock 方法和 tryAcquire 方法,非公平锁都会直接用 CAS 去尝试获取锁,如果获取成功了就不用阻塞,因为阻塞唤醒的开销也是比较大的。
当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态
Synchronized 和 Lock 区别
synchronized 的实现是虚拟机层面的,而 Lock 是依赖于 API;并且相比于 Synchronized,Lock 提供了更多功能, 比如:
- 等待可中断:等待的线程可以选择放弃等待;
- 公平锁:多个线程按照申请锁的时间顺序来获得锁;
- 锁可以绑定多个条件:锁可以绑定多个 Condition 对象,并且可以实现选择性通知。
在 JDK 1.6 进行优化后,两者性能基本持平,并且虚拟机在后续版本可能会对 synchronized 进行性能优化,所以只考虑性能情况下,优先考虑使用 synchronized。
ReentrantReadWriteLock
ReentrantReadWriteLock 实现了 ReadWriteLock,而不是 Lock 接口,它维护了一个 ReadLock,和一个 WriteLock,实现了读写的分离,并且它也支持公平锁和非公平锁以及重入,读锁不支持 Condition,而写锁是支持的。ReentrantLock 是将 AQS 的 state 作为同步状态,0 表示未占用,其他值是重入次数。而 ReentrantReadWriteLock 是将 state 分割为高 16 位表示读和低 16 位表示写。读锁是共享锁,它能够被多个线程同时获取,没有写锁时,读锁总是会被成功获取,已经有写锁时,会导致线程进入阻塞;写锁是排他锁,获取时如果读锁已经被获取或者线程不是已经获取写锁的线程,则线程进入阻塞。读写锁支持锁降级,也就是当前线程获取了写锁,再获取读锁,释放写锁。这是为了保证数据的可见性,如果 A 线程获取写锁后修改了数据,直接释放写锁, B 线程又修改了数据,A 线程无法感知数据修改。所以按照锁降级的步骤,可以保证数据的可见性。
CountDownLatch
CountDownLatch 允许一个或多个线程等待其他线程完成操作 。它相当于一个计数器,在创建时传入参数初始化,每次调用 countDown 方法,使计数器减一,直到为 0;它是通过共享锁实现的,当某个线程调用 await 方法时,会等待共享锁可用,也就是计数器为 0 时,才可以继续执行。
调用 countDown 的线程和调用 await 的线程角色是不一样的。比如运动会赛跑,只有所有参赛者跑到了,裁判才决定名次,这里参赛者和裁判的角色是不一样的,裁判是等待参赛者跑完。
CyclicBarrier
CyclicBarrier 它允许一组线程相互等待, 直到全部到达某个公共屏障点, 然后这组线程再同步往后执行。CyclicBarrier 的内部是使用重入锁 ReentrantLock 和 Condition 实现的,每个线程调用 await 方法,表示自己到达屏障,阻塞等待。当所有的线程都到达屏障,由最后到达的线程执行传入的回调方法,执行完后唤醒所有阻塞线程,继续执行。
应用:
比如运动会赛跑,只有所有参赛者跑到了,才算结束。参赛者之间的角色是相等的,他们跑到终点后互相等待。
区别
- CountDownLatch 的作用是允许一个或多个线程等待其他线程完成执行;而 CyclicBarrier 则是允许一组线程相互等待。
- CountDownLatch 的计数器无法重置,CyclicBarrier 的计数器是可重置使用的。
- CyclicBarrier 可以让最后到达的线程执行回调逻辑。
Semaphore
Semaphore 信号量是一个控制访问资源的计数器,通过 acquire 获取一个许可,没有便阻塞等待,release 释放一个许可。
线程池
一个线程池管理了一组工作线程, 同时它还包括了一个用于放置等待执行任务的任务队列。
好处:
- 重复利用线程,减少创建和销毁线程上的开销
- 提高响应速度,任务到达时,不需要等到线程创建就可以立即执行
- 便于管理线程,可以对线程进行统一的分配和监控
参数
ThreadPoolExecutor 是线程池实现类,它的参数:
- corePoolSize:核心线程数量
- maximumPoolSize:线程池允许的最大线程数
- keepAliveTime:线程空闲时存活时间,非核心线程空闲超过这个值后,会被回收,而核心线程在 allowCoreThreadTimeOut 为 true 时也可以被回收。
- threadFactory:线程工厂,用来创建线程(默认 Executors.defaultThreadFactory())
- workQueue:任务阻塞队列,用于存储等待执行的任务
- rejectHandler:任务拒绝策略,
- AbortPolicy:默认,丢掉任务,并抛 RejectedExecutionException 异常
- DiscardPolicy:直接丢掉任务,不抛异常
- DiscardOldestPolicy:丢掉最老的任务,然后调用 execute 执行该任务(新进来的任务)
- CallerRunsPolicy:在调用者的当前线程去执行这个任务
线程池状态
并且它维护了一个 AtomicInteger,来存放线程池的状态和当前池中的线程数,其中高 3 位用于存放线程池状态,低 29 位表示线程数。
线程池中的各个状态和状态变化的转换过程:
- RUNNING(-1):接受新的任务,处理等待队列中的任务
- SHUTDOWN(0):不接受新的任务提交,但是会继续处理等待队列中的任务
- STOP(1):不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程
- TIDYING(2):所有的任务都销毁了,workCount 为 0。线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()
- TERMINATED(3):表示 terminated 方法执行结束
逻辑
- 线程
线程池将每个线程都封装为 Worker 类,worker 中的线程启动后,其 run 方法会调用 runWorker 方法,它会通过 while 循环来不断地从任务队列中获取任务并执行,如果任务队列中没有任务,会根据线程数量和 allowCoreThreadTimeOut 属性来决定是否限期等待,如果不限期等待,那么线程会一直阻塞等待任务队列,否则会在 keepAliveTime 的时间后返回 null,来退出刚才说的 while 循环,使得线程执行完毕被回收。
- execute
线程池的主要逻辑是 execute 方法,
- 当调用 execute 方法传入 Runnable 类型任务后,会先判断当前线程数,如果小于核心线程数,会创建线程并封装为 Worker 去执行新来的任务。
- 如果大于等于核心线程数,会继续判断线程池所处状态,如果是 RUNNING 状态,会把这个任务添加到任务队列 workQueue 中。
- 这里可能添加任务队列成功或失败,如果成功就会再次检查线程池状态,如果不是 RUNNING 状态,就移除任务,并且执行拒绝策略;如果在 Running 状态,判断线程数是否为 0,为 0 便开启新的线程去执行任务队列中的任务(因为已经放入队列了)。
- 如果任务队列满了,会尝试创建新的线程并执行新来的任务,如果创建失败则会执行拒绝策略。
Executors
Executors 是工具类,它提供四种线程池,分别为:
-
CachedThreadPool:创建一个缓存线程池。核心线程数为 0,也就是说只要有线程空闲(60 s),就会被回收,它使用的是 SynchronousQueue。此线程池不会对线程数量做限制,所有当任务多时,可能会造成 OOM。
-
FixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
-
SingleThreadExecutor:创建一个单线程的线程池。 这个线程池只有一个线程在工作, 也就是相当于单线程串行执行所有任务。 如果这个唯一的线程因为异常结束, 那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。FixedThreadPool 和 SingleThreadExecutor 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
-
ScheduledThreadPool,主要用来指定延时执行任务和周期性重复执行任务。分为两类:
- ScheduledThreadPoolExecutor,包含若干个线程,适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。
- SingleThreadScheduledExecutor,只包含一个线程的ScheduledThreadPoolExecutor。适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。
它们的任务队列是 DelayedWorkQueue,内部使用数组作为堆,只有在延迟期满时才能从中提取元素。堆顶是延迟期满后保存时间最长的 Delayed 元素。
方法
- execute 和 submit 区别:
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
- submit() 方法用于提交需要返回值的任务。线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get() 方法来获取返回值,get() 方法会阻塞当前线程直到任务完成,也可以使用 get 方法传入时间,会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
- shutdown 和 shutdownNow 的区别:
- shutdown 方法: 此方法执行后不得向线程池再提交任务, 如果有空闲线程则销毁空闲线程, 等待所有正在执行的任务及位于阻塞队列中的任务执行结束, 然后销毁所有线程。
- shutdownNow 方法:此方法执行后不得向线程池再提交任务,如果有空闲线程则销毁空闲线程, 取消所有位于阻塞队列中的任务,并将其放入 List 容器, 作为返回值。取消正在执行的线程(实际上仅仅是设置正在执行线程的中断标志位,调用线程的 interrupt 方法来中断线程)
BlockingQueue
ArrayBlockingQueue 和 LinkedBlockingQueue
- ArrayBlockingQueue 是一个由数组支持的有界阻塞队列。它维护了一个 Object 数组和两个整型变量,标识着队头和队尾的位置。读操作和写操作都会获取同一个锁,所以 ArrayBlockingQueue 同时只能有一个线程操作。
- LinkedBlockingQueue 基于链表的阻塞队列,它维护一个单向链表,它使用的锁是分离的,读操作和写操作分别使用了不同的锁来控制数据,所以生产者和消费者可以并行地操 作队列。当队列达到最大值容量时,会阻塞生产者线程,默认最大值容量是 Integer.MaxValue,也就是 2147483647,所以可能会 OOM 异常。
put 和 take方法,可以实现阻塞:
- put 方法:把元素加入到阻塞队列中,如果阻塞队列没有空间,则调用此方法的线程被阻塞,直到有空间的时候再继续。
- take方法:取出排在阻塞队列首位的对象,若阻塞队列为空,则调用此方法的线程被阻塞,直到有新的对象被加入的时候再继续。
offer 和 poll 方法,不具有阻塞:
- offer 方法:把元素加入到阻塞队列中,如果可以容纳, 则返回 true;如果不可以容纳,则返回 false
- poll方法:取出排在阻塞队列首位的对象,若阻塞队列为空, 则返回 null;如果不为空,则返回取出来的那个元素
PriorityBlockingQueue
PriorityBlockingQueue 是基于数组的优先级无界阻塞队列。 它会按照元素的优先级对元素进行排序,按照优
先级顺序出队,每次出队的元素都是优先级最高的元素。它所存储的对象必须是实现 Comparable 接口, 队列通过这个接口的 compare 方法确定对象的 priority;队列的元素并不是全部按优先级排序的, 但是队头的优先级肯定是最高的。 每取一个头元素时候, 都会对剩余的元素做一次调整, 这样就能保证每次队头的元素都是优先级最高的元素。
DelayQueue
DelayQueue 是一个无界阻塞队列,实现关键在于:
- ReentrantLock
- 阻塞和通知的 Condition 对象
- 放置 Delayed 接口对象的 PriorityQueue,堆顶是 delay 时间最小的对象(getDelay 获得剩余时间)
- 线程引用 leader,它指向在等待堆顶元素过期的线程,因为如果有多个线程在阻塞等堆顶元素,等元素过期,都被唤醒,但只能有一个能获取元素,其实是没必要的唤醒
主要是 offer 方法和 take 方法,
take 方法会加锁,并且获得堆顶元素的过期时间,如果还没过期,需要阻塞等待,会看看 leader 指向 null,如果不是,说明有线程已经在等堆顶元素了,那么当前线程只能阻塞,如果没别人等,那就自己等,leader 就指向自己,并且超时等待。
offer 方法会加锁,并往堆放元素,如果堆顶是当前元素,那么 leader 设为 null,因为堆顶被更换成更快过期的了。并且唤醒其他线程。
SynchronousQueue
同步队列是一个不存储元素的队列, 它的 size() 方法总是返回 0。 每个线程的插入操作必须等待另一个线程的移除操作, 同样任何一个线程的移除操作都必须等待另一个线程的插入操作。