线程池用法详解
线程池用法详解
前言
为什么需要用到线程池?
众所周知线程是操作系统一种宝贵的资源,创建和销毁都很耗费资源,对此,java中针对这种情况实现出线程池思想,维护一定数量的线程,用到时候就不需要去创建,不使用时候,就销毁一定量的,保留一部分线程数量。
Java线程池的工作流程
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。
-
如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。
-
如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。
-
在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务。
-
在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常。
-
在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
-
在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行 的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因 此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
5种常用的线程池
JAVA中提供了5种常用的线程池
缓存线程池 newCachedThreadPool
用于创建一个缓存线程池。之所以叫缓存线程 池,是因为它在创建新线程时如果有可重用的线程,则重用它们,否则重 新创建一个新的线程并将其添加到线程池中。对于执行时间很短的任务而言,newCachedThreadPool线程池能很大程度地重用线程进而提高系统的性能。
适用场景:
执行时间很短的任务
特点:
执行时间很短的任务,无容量队列,提交一个任务创建一个线程。
使用 SynchronousQueue
队列,特点是没有容量,没有线程是放不进去的。
没有线程任务运行时,newCachedThreadPool将不会占用系统的线程资源。
固定线程池newFixedThreadPool
特点:
核心线程数 == 最大线程数,无需超时销毁。
阻塞队列是无界的,可以存放任意数量的任务。
适用场景:
适用于任务量已知,相对耗时的任务。
可定时调度 newScheduledThreadPool
newScheduledThreadPool创建了一个可定时调度的线程池,可设置在给定的延迟时间后执行或者定期执行某个线程任务
构造
延迟执行任务
按固定频率执行,与任务本身执行时间无关。但 有个前提条件,任务执行时间必须小于间隔时间,例如间隔时间是5 s,每5s执行一次任务,任务的执行时间必须小于5s。
周期执行任务
WithFixedDelay:按固定间隔执行,与任务本身执行时间有 关。例如,任务本身执行时间是10s,间隔2s,则下一次开始执行的
时间就是12s。
newSingleThreadScheduledExecutor 单个线程任务调度
newSingleThreadExecutor线程池会保证永远有且只有一个可用的线程,在该线程停止或发生异常时,newSingleThreadExecutor线程池会启动一个新的线程来代替该线程继续执行任务
相比于TimerTask 定时任务类,发生异常不会退出,不影响执行任务。
newWorkStealingPool
JDK提供的一个新的线程池newWorkStealingPool
该线程池以上线程池构造不同,采用ForkJoin
线程池,并发是使用ThreadPoolExecutor
构造
newWorkStealingPool创建持有足够线程的线程池来达到快速运算的目
的,在内部通过使用多个队列来减少各个线程调度产生的竞争。这里所说 的有足够的线程指JDK根据当前线程的运行需求向操作系统申请足够的线 程,以保障线程的快速执行,并很大程度地使用系统资源,提高并发计算
的效率,省去用户根据CPU资源估算并行度的过程。当然,如果开发者想自
己定义线程的并发数,则也可以将其作为参数传入。
//获取cpu核数 // System.out.println(Runtime.getRuntime().availableProcessors()); // 不指定线程大小 则是cpu默认核心数 ExecutorService executorService = Executors.newWorkStealingPool(); //可以提交 有返回 或者 无返回 Future<String> submit = executorService.submit(() -> { TimeUnit.SECONDS.sleep(1); return "run able"; }); Future<String> submit1 = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { TimeUnit.SECONDS.sleep(1); return "call able "; } }); System.out.println(submit.get()); System.out.println(submit1.get()); System.out.println(" ------------------- "); //可以 提交一个集合 会等全部线程运行结束 // List<Callable> list = new ArrayList(); // executorService.invokeAny(list);
ThreadPoolExecutor构造方法
一般情况下,构造线程使用ThreadPoolExecutor
来显示的指定线程池参数,包括核心线程数,最大线程数,时间,队列选择以及大小,线程工厂,拒绝策略。防止因为默认的参数任务数量大或者频繁创建线程带来系统问题。
线程池中提交任务方法
关闭线程池
shutdown()
非阻塞方法
//打断 // 20 个线程 // 10 个空闲 会打断线程 并且标记 // 10个工作 等待工作执行完成
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess();//设置SHUTDOWN 标志 打断空闲线程,设置执行线程中断标志,等待任务运行结束 advanceRunState(SHUTDOWN); interruptIdleWorkers(); //打断空闲线程 onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } tryTerminate(); //尝试在打断 }
shutdownNow()
非阻塞方法
// 20个线程 // 10个在队列里 // 1 、会立即将队列里任务打断返回(这些不会处理完) // 2 、打断工作中10个线程,最后执行任务中的10个线程 等待执行完成(处理完) // 3 、 20个线程退出
public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(STOP); //设置stop 标志 打断 执行和没执行的线程都打断 interruptWorkers(); //打断线程 tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); return tasks; //返回队列中任务 }
如
List<Runnable> runnables = executorService.shutdownNow(); //返回任务 System.out.println(runnables); System.out.println(runnables.size());
后台线程方式停止线程池
需要自定义线程池 线程工厂
ExecutorService executorService = new ThreadPoolExecutor(10,20,30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), //线程工厂 参数 r ->{ Thread t = new Thread(r); t.setDaemon(true); return t; },new ThreadPoolExecutor.AbortPolicy());
自定义线程工厂,设置主线程是后台线程, 线程池等待时间 5s 同时线程组也是后台线程,整个线程池就会一起退出!
最多等待时间打断
设置等待时间,最多等待多少时间,没有执行完,则销毁线程池。
executorService.awaitTermination(5,TimeUnit.SECONDS);
对于非返回参数任务而言,想知道每个线程是否执行完成,这个时候,配合CountDownLatch
使用。
拒绝策略
当最大线程池和队列都满的时候,此时再来任务,这时候就需要做出抉择,是抛弃任务还是怎么办。线程池中提供了4中策略。
AbortPolicy
线程池默认就是AbortPolicy直接抛出异常,阻止线程正常运行
CallerRunsPolicy
CallerRunsPolicy的拒绝策略为:如果被丢弃的线程任务未关闭,则 执行该线程任务。
DiscardOldestPolicy
移除线程队列中最早的一个线程任务,并尝试提交当前任务。
DiscardPolicy
DiscardPolicy的拒绝策略为:丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下丢弃部分任务,则这将是保障系统安
全、稳定的一种很好的方案。
当然也可以自定义策略
上面四种都实现了RejectedExecutionHandler接口,若无法 满足实际需要,则用户可以自己扩展RejectedExecutionHandler接口来实 现拒绝策略,并捕获异常来实现自定义拒绝策略。
线程池异常处理
线程池异常处理
线程大小如何选择
线程池最核心的东西是线程数量选择,一般而言,线程数量选择取决去该任务是计算型
还是IO型
任务。
理论而言
计算型
线程数量 = CPU核心数
+ 1
IO型
线程数量 = 2 * CPU核心数
+ 1
IO型还有一种计算公式
线程数量 = 核心数 * 期望CPU利用率 * 总时间(CPU计算时间 + 等待时间) / CPU 计算时间
当然具体参数是需要根据实际任务和CPU处理能力压测动态调节线程大小的。