线程池的工作原理
为什么需要线程池
其实就是池化思想,资源复用,线程池也差不多就是这个道理,它将多个线程预先存储在一个 “池子” 内,当有新的任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要复用 “池子” 内的线程执行对应的任务即可。
线程池的好处:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性:这一点需要着重解释下。我们都知道,系统的资源是有限的,所以线程作为一个消耗系统资源的东西,就不可能无限制的创建。这样,我们通过引入线程池,对线程进行进行统一地分配和监控,降低手动管理每个线程的复杂度
简单来说,线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能由于系统创建大量同类线程而导致消耗完内存或者 “过度切换” 的问题。在阿里巴巴的《Java 开发手册》中也强制规定了:线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
线程池的原理
首先,我们需要知道,线程池里的两个大佬:
- 核心线程池(存储线程)
- 工作队列(存储任务)
饱和策略
- AbortPolicy: 无法处理新任务时直接抛出异常。
- CallerRunsPolicy: 使用用调用者所在的线程来运行新任务(这个很好理解,一般我们都是主线程提交任务,然后扔进线程池执行,对吧。当线程池满了后,如果使用这个策略,就会调用主线程来执行新任务)。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并将新任务加入队列。
DiscardPolicy:不做任何处理,直接将新任务丢弃掉,粗暴!
创建线程池的两种方式
通过 Executors 创建的线程池
-
Executors
Executors 封装了 6 种方法,对应创建 6 种不同的线程池:
FixedThreadPool
- CachedThreadPool
- SingleThreadExecutor
- WorkStealingPool
- ScheduledThreadPool
- SingleThreadScheduledExecutor
- FixedThreadPool
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
// 创建包含 2 个线程的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
// 创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行任务");
}
};
fixedThreadPool.submit(runnable);
fixedThreadPool.submit(runnable);
fixedThreadPool.execute(runnable);
fixedThreadPool.execute(runnable);
execute 和 submit 的不同之处大伙应该也能猜到:execute 用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。submit 用于提交需要返回值的任务,线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get() 方法来获取返回值,get() 方法会阻塞当前线程直到任务完成。
- Executors.newCachedThreadPool
创建一个可缓存的线程池,若线程数超过处理任务所需(供 > 求),多出来的线程缓存一段时间后会被回收掉;而如果线程数不够(供 < 求),则线程池会新建一些线程出来
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 执行任务
for (int i = 0; i < 10; i++) {
cachedThreadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
});
}
pool-1-thread-1 执行任务
pool-1-thread-5 执行任务
pool-1-thread-3 执行任务
pool-1-thread-4 执行任务
pool-1-thread-2 执行任务
pool-1-thread-7 执行任务
pool-1-thread-6 执行任务
pool-1-thread-8 执行任务
pool-1-thread-9 执行任务
pool-1-thread-10 执行任务
- Executors.newSingleThreadExecutor: 创建只包含一个线程的线程池,它可以保证任务先进先出的执行顺序。也就说,先被扔进线程池的任务,就会被先执行
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
// 执行任务
for (int i = 0; i < 10; i++) {
singleThreadPool.execute(() -> {
System.out.println("任务 " + i + " 被执行");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
});
}
- Executors.newWorkStealingPool
和 SingleThreadExecutor 相反,WorkStealingPool 创建的是一个抢占式执行的线程池,也即任务执行顺序不确定。另外,从名字上各位应该也能看出,WorkStealingPool 创建的是包含多个线程的线程池,而 SingleThreadExecutor 创建的是仅包含 1 个线程的线程池。
- Executors.newScheduledThreadPool: 创建一个可执行延迟,定时任务的线程池。
对于scheduledThreadPoolExecutor 来说,不能用execute / submit, 应该使用schedule:
- scheduleAtFixedRate: 按照固定时间间隔,如果上一个线程没有执行完就覆盖上一个线程。
- scheduleWithFixedDelay: 按照固定延迟,等上一个线程执行完再开始计时。
// 创建线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
// 创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("执行任务");
}
};
System.out.println("3 秒后开始执行线程池服务" + new Date());
scheduledThreadPool.schedule(runnable, 3, TimeUnit.SECONDS);
- Executors.newSingleThreadScheduledExecutor: 同样的,从名字可以看出,这个方法创建的是仅包含 1 个线程线程池,并且它可以执行延迟任务
ThreadPoolExecutor
阿里巴巴JAVA开发手册强制要求
【强制要求】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
重点:线程池的7个参数
corePoolSize, MaximumPoolSize, keepAliveTime, TimeUnit, workQueue, threadFactory, RejectedExceptionHandler.
ThreadFactory 用来创建线程。
为什么不建议使用Executors 创建线程池
- FixedThreadPool 和 SingleThreadPool 使用LinkedBlockingQueue, 容易出现OOM。
- CachedThreadPool:允许创建的线程数量最大为Integer.MAX_VALUE, 可能会创建大量的线程,从而导致OOM。
Executor 框架
执行一个任务的标准流程
- 创建Runnable / Callable 任务
- 将任务交给ExecutorService, submit / execute
- 获取执行结果
```java
class Task implements Callable
{ @Override public String call() {
} }// do something
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(xxxxxxxxx);
Future
try { System.out.println(future.get()); } catch(Exception e){ e.printStackTrace(); } finally{ threadPool.shutdown(); }
```
重点:如何配置线程池参数
一般为计算机CPU核心数量+1
- 线上计算密集型,需要快速响应,要调高corePoolSize 和 maxPoolSize 尽可能多的创建线程。
- 线下,IO密集型。 设置阻塞队列区缓冲并发任务。