为什么要用线程池?
池化技术:线程池、数据库连接池、Http连接池 池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
使用线程池的好处:
降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
实现Runnable接口和Callable接口的区别
Callable接口是为了处理Runnable不支持的用例。Runnable接口不会返回结果或抛出检查异常,但是Callable接口可以。如果任务不需要返回结果或抛出异常使用Runnable接口,这样代码看起来会更加简洁
工具类Executors可以实现Runnable对象和Callable对象之间的相互转换(Executors.callable(Runnable task))或Executors.callable(Runnable task, Object resule)
执行execute()方法和submit()方法的区别是什么
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
如何创建线程池
ThreadPoolExecutor
方式一:通过构造方法实现 ThreadPoolExecutor
方式二:通过Executor框架的工具类Executors来实现
Executors返回线程池对象的弊端:
FixedThreadPool和SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM
CachedThreadPool和ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。
FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务
SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务
CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
ThreadPoolExecutor类分析-->提供了四个构造方法
1 //用给定的初始参数创建一个新的ThreadPoolExecutor 2 public ThreadPoolExecutor(int corePoolSize, 3 int maximumPoolSize, 4 long keepAliveTime, 5 TimeUnit unit, 6 BlockingQueue<Runnable> workQueue, 7 ThreadFactory threadFactory, 8 RejectedExecutionHandler handler){ 9 if (corePoolSize < 0|| 10 maximumPoolSize <= 0|| 11 maximumPoolSize < corePoolSize|| 12 keepAliveTime < 0) 13 throw new IllegalArgumentException(); 14 if (workQueue == null || threadFactory == null || handler == null) 15 throw new NullPointerException(); 16 this.corePoolSize = corePoolSize; 17 this.maximumPoolSize = maximumPoolSize; 18 this.workQueue = workQueue; 19 this.keepAliveTime = unit.toNanos(keepAliveTime); 20 this.threadFactory = threadFactory; 21 this.handler = handler; 22 }
ThreadPoolExecutor 3个最重要的参数:
corePoolSize:核心线程数 定义了最小可以同时运行的线程数量
corePoolSize the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set
maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中
ThreadPoolExecutor其他常见参数:
keepAliveTime:当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁
unit:keepAliveTime参数的时间单位
threadFactory:executor创建新线程的时候会用到
handler:饱和策略
ThreadPoolExecutor饱和策略:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,ThreadPoolTaskExecutor定义一些策略:
ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理,直接抛出异常阻止线程正常运行
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果你的应用程序可以承受此延迟并且不能让任务丢弃任何一个任务请求的话,可以选择这个策略
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉
ThreadPoolExecutor.DiacardOldestPolicy:此策略将丢弃最早的未处理的任务请求
自定义拒绝策略:内置的拒绝策略都实现了RejectedExecutionHandler接口,若无法满足实际需要,用户可以自己扩展RejectedExecutionHandler接口来实现拒绝策略,并捕获异常来实现自定义拒绝策略
线程池原理分析
1 //execute 2 3 //存放线程池的运行状态(runState)和线程池内有效线程的数量(workerCount) 4 5 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); 6 7 private static int workerCountOf(int c){ 8 return c & CAPACITY; 9 } 10 11 private final BlockingQueue<Runnable> workQueue; 12 13 public void execute(Runnable command){ 14 // 如果任务为null,则抛出异常 15 if (command == null) 16 throw new NullPointerException(); 17 // ctl中保存的线程池当前的一些状态信息 18 int c = ctl.get(); 19 20 // 1.首先判断当前线程池中执行的任务数量是否小于corePoolSize 21 // 如果小于的话,通过addWorker(command, true)新建一个线程, 22 //并将任务(command)添加到该线程中;然后,启动该线程从而 23 //执行任务 24 if (workerCountOf(c) < corePoolSize){ 25 if (addWorker(command, true)) 26 return; 27 c = ctl.get(); 28 } 29 30 // 2.如果当前执行的任务数量大于等于corePoolSize的时候 31 //通过isRunning方法判断线程池状态,线程池处于RUNNING状态并且队列可以加入任务,该任务才会被加入进去 32 if (isRunning(c) && workQueue.offer(command)) { 33 int recheck = ctl.get(); 34 // 再次获取线程池状态,如果线程池状态不是RUNNING状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略 35 if (!isRunning(recheck) && remove(command)) 36 reject(command); 37 // 如果当前线程池为空就创建一个线程并执行 38 else if (workerCountOf(recheck) == 0) 39 addWorker(null, false); 40 } 41 // 3.通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务 42 //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容 43 else if (!addWorker(command, false)) 44 reject(command); 45 }
Java线程池的工作流程:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务
如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务
如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中
在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该任务
在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常
在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取出下一个线程任务继续执行
在线程处于空闲状态的时间超过keepAliveTime时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此该线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小