线程池_初步认识_01
- 一、定义
- 管理一组工作线程。
- 二、好处
- 1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗,比如内存;
- 2. 提高响应速度。任务到达时,可以不需要等到线程创建就能执行;
- 3. 提高线程的可管理性。通过线程池,实现对线程的统一分配,调优和监控;比如可以避免无线创建线程引起OutOfMemoryError。
- 三、实现原理
- 当向线程池提交一个任务之后,线程池是如何处理这个任务的呢?
- 上图就是线程池的主要处理流程;
- ThreadPoolExecutor 执行execute()方法的四种情况(流程):
- 1、如果当前运行的线程少于核心线程,则创建新线程来执行任务;(需要注意的是,此操作需要获取全局锁)
- 2、如果运行的线程等于或对于核心线程,则将任务加入工作队列中;
- 3、如果工作队列已满,则创建新的线程(非核心线程)来处理任务;(需要获取全局锁)
- 4、如果创建线程超出最大线程数,则任务被拒绝,调用饱和策略;(RejectedExecutionHandler.rejectedExecution())
- 总结: 如此设计的原因?
- 为了在执行execute()方法时,尽可能地避免获取全局锁;因为获取全局锁是一个严重可伸缩瓶颈。
- 期望: 每次任务提交的时候,都是当前运行的线程数大于等于核心线程数,此时不用获取全局锁,即加入工作队列。
- 当向线程池提交一个任务之后,线程池是如何处理这个任务的呢?
- 四、通过ThreadPoolExecutor创建线程池
- public ThreadPoolExecutor(
- int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler defaultHandler)
- 1. corePoolSize
- 线程池的基本大小;如果要执行的任务数小于线程池基本大小,提交一个任务到线程池后,就会创建一个线程即便有空闲线程;否则,不创建,等待。
- 2. maximumPoolSize
- 线程池最大线程数。如果线程池中的线程数大于核心线程数并且队列满了,且线程数小于最大线程数,则会创建新的线程。
- 如果maximumPoolSize与corePoolSize相等,即是固定大小线程池。
- 如果使用无界队列,该参数无效;
- 3. keepAliveTime
- 空闲线程存活时间。
- 默认情况下,当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用;当线程池中的线程空闲时,如果空闲时间等于keepAliveTime,线程会被销毁,直到线程数等于核心线程数,避免浪费内存和句柄资源。
- 当ThreadPoolExecutor的allowCoreThreadTimeOut变量设置为true时,核心线程超时后也会被回收。
- 如果任务多,并且每个任务的执行时间较短,增大时间,提高线程的利用率。
- 4. unit
- 时间单位
- 5. BlockingQueue<Runnable> workQueue
- 任务队列,用于保存等待执行的任务的阻塞队列。包括以下几种:
- 1.ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,FIFO(先进先出)原则对元素进行排序;
- 2.LinkedBlockingQueue:一个基于链表结构的阻塞队列,也是先进先出,一般情况下吞吐量高于ArrayBlockingQueue 。 FixedThreadPool()和SingleThreadExecutor()使用此队列。
- 3.SynchronousQueue:一个不存储元素的阻塞队列。上一个线程执行移除操作后,之后的插入操作才能执行,否则会一直处于阻塞状态。吞吐量高于LinkedBlockingQueue。CachedThreadPool()使用此队列。
- 4.PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
- 任务队列,用于保存等待执行的任务的阻塞队列。包括以下几种:
- 6. ThreadFactory threadFactory
- 线程池创建线程使用的工厂;
- 使用线程池创建线程的时候可以给予更有意义的名称,便于定位问题。
- 7. RejectedExecutionHandler defaultHandler
- 线程池对拒绝任务的处理策略;发生在队列和线程池都满了的时候。默认是AbortPolicy,表示无法处理新任务时抛出异常。
- 1.AbortPolicy : 直接抛出异常;
- 2.CallerRunsPolicy: 只用调用者所在线程来运行任务;
- 3.DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务;
- 4.DiscardPolicy:不处理,不丢弃;
- 线程池对拒绝任务的处理策略;发生在队列和线程池都满了的时候。默认是AbortPolicy,表示无法处理新任务时抛出异常。
- 自定义拒绝策略实现RejectedExecutionHandler接口,实现需要的场景。比如持久化拒绝的任务、记录日志等等。
- public ThreadPoolExecutor(
- 五、提交任务到线程池的方法
- (一)、 通过execute
- executorService.execute(newRunnable() {
- @Override
- public void run() {
- System.out.println("我要开始运行了");
- }
- });
- 提交不需要返回值的任务,无法判断任务是否被线程池执行成功。
- executorService.execute(newRunnable() {
- (二)、 通过submit
- 1.通过提交Runnable
- Future future = executorService.submit(()->{});
- 2.通过提交Callable
- Future future = executorService.submit(() -> {
- return null;
- });
- Future future = executorService.submit(() -> {
- 3. Runnable 和 Callable 的区别
- 1. 非常相似,这两个接口都表示可以由线程或ExecutorService同时执行的任务。
- 2. 不同之处是两个接口内部执行的方法不同。
- Runnable
- public interface Runnable {
- public void run();
- }
- public interface Runnable {
- Callable
- public interface Callable{
- public Object call() throws Exception;
- }
- public interface Callable{
- Runnable
- - 可以看出,call()方法是有返回值的,而且可以引发异常。run没有返回值,也不能引发异常(除非未经检查的异常-RuntimeException的子类)。
- 4.如果执行的任务需要返回结果,使用Callable。
- 1.通过提交Runnable
- (三)、两种执行方法的区别
- 1. execute 只能提交Runnable类型的任务;submit 除了可以提交Runnable类型的任务外,还可以提交Callable类型。
- 2. execute 直接抛出任务执行的异常;submit 会捕获,可以返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,通过get方法获取返回值,get()方法会阻塞当前线程直到任务完成,使用get(long timeout,TimeUnit unit)方法会阻塞当前线程一段时间后立即返回,这时候任务可能没有执行完。
- 3. submit 的顶级接口是ExecutorService;execute 的顶级接口是 Executor;
- (一)、 通过execute
- 六、获取返回结果
- (一)、 两种 invokeAny() 和 invokeAll()
- (二)、invokeAny
- 1. 当任意一个任务得到结果后,会调用interrupt方法将其他的任务中断;
- 2. 部分任务失败,会使用第一个成功的任务返回的结果;
- 3. 任务全部失败了,抛出Execption,invokeAny 方法将抛出ExecutionException。
- (三)、invokeAll 返回所有任务的执行结果,该方法的执行效果也是阻塞执行的,要把所有的结果都取回时再继续向下执行。
- 七、关闭线程池
- 1、shutdown()
- 1. 将线程池的状态设置成ShutDown;
- 2. 中断所有没有正在执行任务的线程。在终止前允许执行以前提交的任务;
- 2、shutdownNow()
- 1. 将线程池的状态设置成Stop;
- 2. 阻止等待任务的启动并试图停止当前正在执行的任务,并返回等待执行任务的列表;不允许执行以前提交的任务。
- 1、2 两个方法的原理:
- 遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
- 调用1、2方法后,isShutdown 方法返回true。当所有任务都已关闭,才表示线程池关闭成功,调用isTerminaed方法返回true。
- 3、awaitTermination()
- 1. 接收timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经
- 关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。
- 在实际使用过程中, 使用shutdown()关闭,回收资源。如果有必要,可以在其后执行shutdownNow(),取消所有遗留的任务。
- 1、shutdown()
- 八、配置线程池
- 角度:
- 1. 任务的性质: CPU密集型任务、IO密集型任务和混合型任务。
- 2. 任务的优先级: 高、中与低;
- 3. 任务的执行时间: 长、短;
- 4. 任务的依赖性: 是否依赖其他系统资源,如数据库连接。
- 选择:
- 性质不同的任务可以用不同规模的线程池分开处理;
- CPU 密集型任务应配置尽可能小的线程,如cpu数+1 的线程池;
- IO密集型任务线程并不是一直在执行,则应配置极可能多的线程;
- 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行;
- 如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
- 依赖数据库连接的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置的越大,这样才能更好低利用CPU。
- 建议:
- 有界队列;有界队列可以增加系统的稳定性和预警能力。
- 体现:
- 任务线程池的队列和线程池满了,不断抛出抛弃任务的异常,通过排查是数据库出现问题,导致执行sql的速度变慢,由于后台任务线程池里的任务都是需要向数据库插入数据的和查询,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。如果是无界队列,线程池中的队列越来越多,可能撑爆内存,导致系统不可用。
- 角度:
- 八、例子
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- Set<Callable<String>> callables = newHashSet<Callable<String>>();
- callables.add(() -> {
- return "Task1";
- }});
- callables.add(() -> {
-
- callables.add(() -> {
- return "Task2";
- }});
- callables.add(() -> {
- List<Future<String>> futures =executorService.invokeAll(callables);
- for(Future<String> future : futures){
- System.out.println("future.get = " + future.get());
- }
- for(Future<String> future : futures){
- executorService.shutdown();
- while (!service.awaitTermination(1, TimeUnit.SECONDS)) {
- System.out.println("线程池没有关闭");
- }
- Set<Callable<String>> callables = newHashSet<Callable<String>>();
- ExecutorService executorService = Executors.newSingleThreadExecutor();