文章目录
线程池的工作原理
-
为什么要有线程池?
Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好地利用CPU资源。 -
什么是线程池?
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。 -
Java线程池的工作原理为:
JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。 -
线程池的主要的作用:
- 线程复用
- 线程资源管理
- 控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行
线程复用
线程复用原理:
- Java中,每个Thread类都有一个start方法。
- 执行start方法启动线程时,Java虚拟机会调用该类的run方法。
- 查看Thread源代码,在Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread类,在start方法中不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。
- 以将在循环方法中不断获取的Runnable对象存放在Queue中,当前线程在获取下一个Runnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。
- 这样就简单实现了一个线程池,达到了线程复用的效果。
线程池的核心组件和核心类
核心组件:
- 线程池管理器:用于创建并管理线程池。
- 工作线程:线程池中执行具体任务的线程。
- 任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度。
- 任务队列:存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除。
核心类:
线程池是通过Executor框架实现的。
Executor框架包括3大部分:
-
任务。也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口;
-
任务的执行。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。
-
异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。
核心类为:ThreadPoolExecutor类
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
相关参数含义:
线程池工作流程
线程流程图:
在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
简易流程图:
详解execute()方法(在线程池核心类ThreadPoolExecutor中):
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
查看源代码:
- 传入的Runnable对象为空时,抛出空指针异常
- 如果运行的线程少于corePoolSize,启动一个新线程任务。对addWorker的调用会自动检查runState和workerCount,这样可以防止虚假警报的增加线程时,它不应该执行,通过返回false。
- 如果任务可以成功排队,那么我们仍然需要来再次检查我们是否应该添加一个线程(因为在上次检查后已有线程已经死亡)或那样自从进入此方法后池就关闭了。所以我们重新检查状态,如果没有线程,如有必要回滚队列停止,或启动一个新线程。
- 如果我们不能将任务放入队列,那么我们尝试添加一个新的线程。如果它失败了,我们知道我们被关闭或饱和了因此拒绝这项任务。
线程池拒绝策略
- 若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。
- 为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。
- JDK内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy这4种,默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。
- 在默认的拒绝策略不能满足应用的需求时,可以自定义拒绝策略。
1.AbortPolicy
ThreadPoolExecutor中AbortPolicy源代码:
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
通过源代码可以看到AbortPolicy是直接抛出RejectedExecutionException异常。
2.CallerRunsPolicy
ThreadPoolExecutor中CallerRunsPolicy源代码:
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }
/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();//执行被丢弃的任务r
}
}
}
如果被丢弃的线程任务未关闭,则执行该线程任务。注意,CallerRunsPolicy拒绝策略不会真的丢弃任务。
3.DiscardOldestPolicy
ThreadPoolExecutor中DiscardOldestPolicy源代码:
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() { }
/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();//丢弃线程队列中最后一个线程任务
e.execute(r);//尝试提交当前任务
}
}
}
移除线程队列中最早的一个线程任务,并尝试提交当前任务。
4.DiscardPolicy
ThreadPoolExecutor中DiscardPolicy源代码:
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }
/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下丢弃部分任务,则这将是保障系统安全、稳定的一种很好的方案。
5.自定义拒绝策略
我们可以看到前面4种拒绝策略都是实现了RejectedExecutionHandler接口,我们自定义拒绝策略也只需实现RejectedExecutionHandler接口即可。
例如:实现一个自定义拒绝策略DiscardOldestNPolicy,该策略根据传入的参数丢弃最老的N个线程,以便在出现异常时释放更多的资源,保障后续线程任务整体、稳定地运行。
public class DiscardOldestNPolicy implements RejectedExecutionHandler {
private int discardNumber = 5;
private List<Runnable> discardList = new ArrayList<Runnable>();
public DiscardOldestNPolicy(int discardNumber) {
this.discardNumber = discardNumber;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (e.getQueue().size() > discardNumber) {
//批量移除线程队列中的discardNumber个线程任务
e.getQueue().drainTo(discardList, discardNumber);
//清空discardList列表
discardList.clear();
if (!e.isShutdown()) {
//尝试提交当前任务
e.execute(r);
}
}
}
}
5种常用的线程池
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。然后通过ExecutorService实现Executor接口并执行具体的线程操作,ExecutorService接口有多个实现类可用于创建不同的线程池。
newCachedThreadPool:可缓存的线程池
- newCachedThreadPool用于创建一个缓存的线程池。
- 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
- 调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。
- 终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
- 创建方式:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
newFixedThreadPool:固定大小的线程池
- newFixedThreadPool用于创建一个固定线程数量的线程池,并将线程资源存放在队列中循环使用。
- 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
- 在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
- 创建方式:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
newScheduledThreadPool:可做任务调度的线程池
- 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- 代码示例:
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
//创建一个延迟3s执行的线程
scheduledThreadPool.schedule(new Runnable(){
@Override
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
//创建一个延迟1s执行的线程且每3s执行一次线程
scheduledThreadPool.scheduleAtFixedRate(new Runnable(){
@Override
public void run() {
System.out.println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
}
}
newSingleThreadExecutor:单个线程的线程池
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
newWorkStealingPool:足够大小的线程池
- JDK1.8新增
- newWorkStealingPool创建持有足够线程的线程池来达到快速运算的目的,在内部通过使用多个队列来减少各个线程调度产生的竞争。
- 有足够的线程指JDK根据当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行,并很大程度地使用系统资源,提高并发计算的效率,省去用户根据CPU资源估算并行度的过程。
想自己定义线程的并发数,则也可以将其作为参数传入