目录
目的
个人将concurrent包中的众多类分为加锁和不加锁两个分类,本文针对加锁分类进行介绍。
JDK版本
JDK8
Executor
interface Executor
价值
- 提供[将 任务提交 与 任务执行机制(包括线程的使用与调度) 解耦]的方式
- 避免显示的创建Thread。
JMM规定
- ThreadA将任务提交到执行器 happens before ThreadB执行任务
定义的接口
-
void execute(Runnable command)
interface ExecutorService
更具有扩展性的执行器接口,concurrent包中执行器都实现该接口。提供“管理终止” 和 以返回future的方式跟踪任务执行情况的方法。
JMM规定
- ThreadA提交任务到ExecutorService之前的执行 happens before 任务被执行 happens before Future.get获取到结果
ThreadPoolExecutor
具有扩展性的线程池实现
解决两个问题
- 提升表现,如性能/吞吐量;方式:减少每个任务的调用周期,即减少从提交到执行的时间。
- 对执行任务时使用的资源进行边界划定与管理,如线程池中的线程
自定义线程池
- corePoolSize and maxPoolSize
通过构造函数进行设置,后续也可通过方法设置。setCorePoolSize/setMaximumPoolSize线程池会根据两者设定的范围,来自动调节poolsize. - 按需创建
即默认情况下,只有当任务到达时,线程才会被构建与启动。当然可以通过重写进行自定义,以提前构建。
prestartCoreThread/prestartAllCoreThreads - 创建线程
使用ThreadFactory,需要保证线程安全。 - 存活时间 Keep-alive times
setKeepAliveTime
allowCoreThreadTimeOut - 排队
- 使用阻塞队列传输和持有提交的任务
- 队列的用途和poolsize之间存在交互关系
- 当poolsize小于core时,执行器直接使用任务新建线程,而不是排队。
- 当poolsize大于等于core时,执行器将直接让任务去排队。
- 当任务无法进入队列且poolsize小于max时,新建线程,否则抛弃任务。
- 3中排队策略
- 直接传送
即任务直接传送给线程,不需要其他中间存储。
问题:当任务到达速度>任务被消费的速度,线程无限增加 - 无边界队列存储任务
即所有任务都是先放到队列中存储,之后待消费。
问题:
poolsize始终维持在core,可能导致资源未合理使用,如cpu;
增加了任务等待时间;
当任务到达速度>任务消费速度时,任务队列无限赠长; - 有边界的任务队列
队列和线程池边界的控制需要根据实际情况制定
IO密集型 CPU密集型 任务量
- 直接传送
- 拒绝任务RejectedExecutionHandler
- when
- 执行器已经shutdown
- 线程池和任务队列都已经饱和了
- 默认4中策略
- ThreadPoolExecutor.AbortPolicy 抛出异常RejectedExecutionException,以异常的形式提现现象
- ThreadPoolExecutor.CallerRunsPolicy 提交任务的线程自行跑任务
- ThreadPoolExecutor.DiscardPolicy 直接将新任务扔掉
- ThreadPoolExecutor.DiscardOldestPolicy 直接扔掉最老的任务
- when
- 钩子方法
- 用于操作执行环境
beforeExecute/afterExecute/terminated
- 用于操作执行环境
- 任务队列查看
使用getQueue获取任务队列,可用于仅仅限于监控/debug/获取任务数量等只读操作
remove/purge当大量队列任务被取消时用于存储空间的回收 - Finalization
终止化
为了避免线程池已不再使用后无法回收的问题,应该合理设置:
keep-alive time/allowCoreThreadTimeOut
如何确定队列长度和线程数量
明白线程池的内部原理后,我认为如下公式具有参考意义:
单位时间内产生的任务量 = 任务队列长度 + 单位时间内单个线程处理的任务量 * 最大线程量
可以保证不发生拒绝任务的情况 。
线程池poolsize与任务队列大小的关系图示
如何存储线程池状态和工人数量
线程池控制单元使用原子Integer类型,
高3 bit表示线程池状态;
低29bit表示线程池中有效工人的数量;(workerCount表示处于start和stop之间的线程数,包含正在退出但还没有被terminate的线程,所以workerCount >= lived thread的真实数量)
任务如何存储
private final BlockingQueue<Runnable> workQueue;
工人如何存储
private final HashSet<Worker> workers = new HashSet<Worker>();
工人模型如何实现
- Worker是AQS和runnable的实现体,不允许重入,使用AQS的state表示工人正在工作。
-
private final class Worker extends AbstractQueuedSynchronizer implements Runnable
-
- 属性
-
/** Thread this worker is running in. Null if factory fails. */ final Thread thread;工人在这个线程中执行任务 /** Initial task to run. Possibly null. */ Runnable firstTask;需要执行的任务 /** Per-thread task counter */ volatile long completedTasks;工人已经完成的任务计数器
-
- 工人时如何工作的
- 将自己作为参数,回调线程池的runWorker方法。
-
public void run() { runWorker(this); }
-
- 主要流程
- 循环从任务队列中获取任务,并使用idle作为超时时间;
- 如果任务存在,则依次执行beforeExecute task.run afterExecute,如果执行期间出现异常,则退出循环且进入线程退出线程池的操作;
- 如果任务不存在,则退出循环且进入线程退出线程池的操作;
- 强调2点:worker执行过程出现异常;线程池空闲,长时间无新任务。
- 退出线程池主要流程:
- 从wokerSet中移除当前worker;
- 如果时异常退出 或者 当前工人数少与下限workerCount<corePoolSize,则新增普通worker;
- 将自己作为参数,回调线程池的runWorker方法。
- 可能引起误区的方法
- getTaskCount获取已经被执行过task的数量+任务队列大小
- getLargestPoolSize获取历史线程池数量最高点
Executors
提供了很多方便的执行器工厂方法
缺点
- 构造线程池时无法进行自定义,例如idle,maxPoolSize,workerQueue,拒绝策略;
- 使用的workerQueue类型时无限的new LinkedBlockingQueue<Runnable>(),这可能会引起内存紧张。
- idle基本设置为0,这样导致idle毫无意义,可能频繁新建和销毁线程。
所以尽可能自行创建线程池,让一切都在控制范围内,不使用这些工厂方法。
ScheduledThreadPoolExecutor
extends ThreadPoolExecutor implements ScheduledExecutorService
如何实现按照时间进行调度的
- 任务队列模型
-
DelayedWorkQueue extends AbstractQueue<Runnable> implements BlockingQueue<Runnable>
在进行take/poll的逻辑中会判断task是否该执行
-
- 任务模型
-
ScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V>
-
- 主要field
- private long time;下次执行的绝对时间nanoTime,从任务队列取时,会通过该数据判断是否该执行。
- private final long period;整数表示fix-rate固定频率执行,负数表示fix-delay固定延期执行,0表示只延迟执行一次。
- run任务执行逻辑
- 如果线程池当前已不允许执行任务,则cancel
- 如果是非周期性任务,则执行
- 如果是周期任务,则执行,计算下次执行时间,再次放入任务队列
阻塞队列
使用lock和condition,await signal实现
copyOnWriteArrayList
- 采用volatile+lock
- 线程安全的ArrayList;虽然消耗高,但在[遍历(读)操作场景远超于写操作]且不可能也不应该使用synchronized时,其更高效。
- 与ArrayList一致,使用[数组]作为数据存储, volatile Object[] array;
- 写加锁,读不加锁
- 提供了线程安全的修改操作,锁+副本操作
- 使用重入锁lock
- 复制当前数组得到其副本,Arrays.copyOf
- 对副本进行操作
- 使用副本覆盖原数组
- 最后释放重入锁unlock
- 每次写操作后,array引用新数组,原数组被抛弃。
- 提供的Iterator,数据源为遍历器创建时的array所指向的实际数组,写操作不影响遍历,因为写后array引用已发生变化。该遍历器不支持修改操作。
copyOnWriteArraySet
- 基于copyonwritearraylist实现的set,使用场景读远大于写
- final CopyOnWriteArrayList<E> al,set的唯一性使用al.addIfAbsent实现
多个线程间流程同步协调器-CyclicBarrier
- 一组线程中,每个线程一直等待,直到组内其他n-1个线程也到达了障碍物点。count可重置。
- 涉及
ReentrantLock
Condition 在这里充当障碍物,signalAll代表障碍物倾倒。
condition.await/signalAll - await([timeout])大致流程
- 加锁
- count减1
- 若count为0则执行commond,condition.signalAll,count恢复等,return;
- 否则loop condition.await 直到障碍物倾倒/超时/线程中断等(当与lock关联的condition await时,其lock会自动释放)
ConcurrentHashMap
- 元素存储单元
-
Node impl Map.Entry hash//key.hash进行XORs((h ^ (h >>> 16)) & HASH_BITS)后的数据,目的:高16位移到低16位然后高16位设0 key volatile value//可见性 volatile next//可见性
-
- 元素存储
-
volatile Node[] table volatile Node[] nextTable//transfer时使用,即数组扩大或减小
-
- 操作Note[]时均使用Unsafe,依赖jvm实现
- tabAt获取Node[index],使用到getObjectVolatile
- tabAt获取Node[index],使用到getObjectVolatile
- setTabAt设置Node[index],使用到putObjectVolatile
- 需对具体Node[index]进行写操作时,使用synchronized锁,粒度为Node[index]
-
Node f = tabAt(tab, i = (n - 1) & hash); synchronized(f){ //xxx }
-
即使用Note[index]链表的第一个Note的监视器锁
-
所以扩容后,锁数量会增加。
-
- 读操作没有锁!
- 计数原理
- 不保证准确,因为无锁,例如遍历到index=10时,cell[4]数据发生变化。
- volatile cell[] 计数cell数据存储每个线程put的数据数量,遍历叠加cell[]获取当前总数据量。
- put()时,会获取当前线程对应的cell:cell[unsafe获取当前线程的探针&cell.length],然后unsafe cas设置新数(+1)。
- 每次put之后,均会判断是否需要transfer交换。交换过程采用CAS+同步tab[i]结合的方式