并发工具类
CountDownLatch
允许一个或多个线程等待其他线程完成操作,比传统的join
机制更好。
其构造函数接收一个int
类型的参数作为计数器(初始化后无法改变该值),即要等待的线程数量(也可以是一个线程的多个执行步骤)。每次调用countDown
都会使计数器减一,await()
方法会一直阻塞当前线程知道计数器归零,同时该方法也提供超时机制。
CyclicBarrier同步屏障
所有线程都会止步于一个屏障(同步点),直到最后一个线程也抵达屏障才会放行。默认构造器参数为拦截线程数,线程调用await()
方法表明已经抵达屏障。同时提供重载构造器接收Runnable
任务,当线程抵达屏障后会先去执行该任务。
计数器可以使用reset()
重置,如果计算错误可以重新算一遍,还提供了诸如getNumberWaiting()
获得阻塞线程数量,isBroken()
是否被中断。
Semaphore信号量
用于控制同时访问特定资源的线程数量,用于流量控制,如数据库连接。
构造方法参数为并发数量,线程中调用semaphore.acquire()
获得并行许可,使用release()
释放许可
Exchanger
用于线程间的数据交换。它提供一个同步点,两个线程可以交换彼此的数据,通过exchange()
方法进行数据的交换。当一个线程首先执行该方法,会等待第二个线程也执行该方法,当两线程都达到同步点时,可以将本线程生产的数据传递给对方。提供超时机制。
应用场景为:遗传算法、校对工作
并发框架
Fork/Join框架
并行执行任务框架,用于将大任务分割成若干小任务,再汇总小任务结果得到大任务结果。
设计思路
- 分割任务。一个fork类将大任务分割成足够小的小任务
- 合并结果。子任务放在双端队列中,每次添加到队首,启动几个线程进行执行,若某些线程执行速度快会进行工作窃取,从其他工作线程队列尾部获取任务执行,所有结果放在一个队列中由专门的线程合并结果。
ForkJoinTask
的子类RecursiveAction
用于无返回结果的任务,RecursiveTask
用于有返回值的任务
ForkJoinPool
用来执行ForkJoinTask
forkJoinPool.submit()
执行总任务,返回一个Future
结果;
countTask.fork()
开始执行子任务,进入子任务的compute
方法,使用countTask.join
拿到子任务返回的结果
该框架使用isCompletedAbnormally()
检测任务是否已经抛出异常或被取消,使用gerException()
方法获取抛出的异常,若没有异常或未完成返回null
实现原理
fork()
会调用ForkJoinWorkerThread.pushTask()
异步执行该任务并立即返回结果,pushTask()
把当前任务存放在ForkJoinTask
数组队列里再调用ForkJoinPool
的SignalWork()
方法唤醒或新建一个工作线程执行任务。
join
主要是阻塞当前线程并等待获取结果。它调用doJoin()
方法得到当前任务状态来判断返回什么结果
- 已完成:直接返回结果
- 被取消:抛出取消异常
- 信号
- 出现异常:抛出对应异常
doJoin()
先查看任务状态是否已经完成,完成返回状态;没执行完从任务数组里取出任务并执行。
Executor
Executor
执行器提供Runnable
对象的管理,使用线程池有以下三种好处:
- 重复利用已创建的线程,降低资源消耗
- 无需等待线程创建后执行任务,提高响应速度
- 统一分配、调优、监控,提高可管理性
提交任务给线程池时,其工作处理流程如下:
- 判断核心线程池是否已满,若未满,则创建线程处理任务;
- 若核心池已满,将任务加入到阻塞队列中;
- 若阻塞队列已满,无法加入,则创建新线程处理任务;
- 若创建新线程使线程数大于线程池最大容量,拒绝任务并抛出异常。
线程池的创建
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Runnable());
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
不建议使用Executors
创建线程池,可能会产生以下两个问题
newFixedThreadPool
和newSingleThreadExecutor
队列中可能会有过多的等待处理的事物,占用很多空间,可能产生OOMnewCachedThreadPool
和newScheduledThreadPool
线程最大数量为Integer.MAX_VALUE
,可能会产生很多的线程,造成OOM
建议手动创建线程池,使用构造器
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
线程池的使用
通过execute()
和submit()
方法提交任务,其中前者提交无返回值任务,后者提交有返回值任务,见下方Callable
。
线程池的关闭
shtudown()
和shutdownNow()
用来关闭线程池,实现方式是遍历线程调用interrupt
,区别是前者不在接收新任务,继续执行正在执行的任务;后者不再接收新任务,终止正在执行的任务,返回等待任务列表。
线程池的监控
taskCount
:线程池需要执行的任务数量completedTaskCount
:线程池运行过程中已完成任务数量,小于等于taskCount
largestPoolSize
:线程池曾经创建过的最大线程数量getPoolSize
:线程池线程数量getActiveCount
:活动的线程数- 继承线程池自定义线程池,重写空方法
beforeExecute()
、afterExecute()
、terminated
方法。
Callable
Callable
接口与Runnable
的区别在于前者拥有返回值,实现该接口实现call()
方法。该接口对象必须与线程池的executorService.submit()
方法绑定:executorService.submit(new Callable())
,返回一个Future
对象,可以提前使用它,但只有计划任务完成后改对象才有具体的结果,若还未完成就调用get()
取值使用,将进入阻塞直到计划任务完成,或者使用isDone()
检查是否完成。