线程池
手动创建线程的缺点
线程需要消耗大量资源;线程的数量也不是越多越好!
- 系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建标准不一样(比如线程没有名字)。当系统运行起来,所有线程都在疯狂抢占资源,会引发混乱——
不受控风险! - 过多的线程也会引起
上下文切换的开销! - Thread是一个重量级的资源,创建、启动以及销毁都是比较耗费系统资源!
创建线程过程
Java创建线程语法new Thread()在操作系统层面并没有创建新的线程,这是编程语言特有的。真正转换为操作系统层面创建一个线程,还要调用操作系统内核的API(start()方法),然后操作系统要为该线程分配一系列的资源!
Object obj = new Object()过程:
- 分配一块内存M;
- 在内存M上初始化该对象;
- 将内存M的地址赋值给引用变量obj。
创建一个线程的过程:
- JVM为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧。每一栈帧由一个局部变量数组、返回值、操作数堆栈组成;
- 一些支持本机方法的JVM也会分配一个
本机堆栈; - 每个线程获得一个
程序计数器,告诉它当前处理器执行的指令是什么; - 系统创建一个与Java线程对应的
本机线程; - 将与线程相关的描述符添加到JVM内部数据结构中;
- 线程共享
堆和方法区。
创建一个线程(即便不干什么)需要大约1M左右!因此频繁手动创建、销毁线程的代价是非常巨大的!
线程池作用
Java线程池就是为了最大化高并发带来的性能提升,并最小化手动创建线程的风险,将多个线程统一在一起管理。
- 利用线程池管理并复用线程,控制最大并发数(手动创建线程很难得到保证);
- 实现任务线程队列缓存策略和拒绝机制;
- 实现某些与时间相关的功能,如
定时执行,周期执行等(比如列车指定时间运行); 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大。因此,通过配置独立的线程池,将较慢的交易服务与搜索服务个离开,避免个服务线程互相影响。
ThreadPoolExecutor
三大方法
三大方法本质都是调用ThreadPoolExecutor:
// 1.单个线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 源码Executors#newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 2.创建一个固定的线程池的大小
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 源码Executors#newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 3.可伸缩的,遇强则强,遇弱则弱
ExecutorService threadPool = Executors.newCachedThreadPool();
// 源码Executors#newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
实例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Pool1
{
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newSingleThreadExecutor();// 单个线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个固定的线程池的大小
ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的,遇强则强,遇弱则弱
try {
for (int i = 0; i < 100; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
/*
ExecutorService threadPool = Executors.newSingleThreadExecutor()
...
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
...
ExecutorService threadPool = Executors.newFixedThreadPool(5);
...
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-5 ok
pool-1-thread-5 ok
pool-1-thread-4 ok
...
ExecutorService threadPool = Executors.newCachedThreadPool();
...
pool-1-thread-6 ok
pool-1-thread-23 ok
pool-1-thread-8 ok
pool-1-thread-9 ok
pool-1-thread-37 ok
pool-1-thread-36 ok
...
*/
七大参数
Java线程池就是为了最大化高并发带来的性能提升,并最小化手动创建线程的风险,将多个线程统一在一起管理!
了解了线程池的几个核心参数概念后,需要经过调优的过程来设置最佳线程参数值!
public class ThreadPoolExecutor extends AbstractExecutorService {
// 七大参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
}
为了形象的理解ThreadPoolExecutor的七大参数:
| 序号 | 参数名称 | 参数解释 | 春运形象说明 |
|---|---|---|---|
| 1 | corePoolSize | 表示常驻核心线程数,如果大于0,即使本地任务执行完也不会被销毁 | 日常固定的列车数辆(不管是不是春运,都要有固定车次运行) |
| 2 | maximumPoolSize | 表示线程池能够容纳可同时执行的最大线程数(结合workQueue) |
春运客流量大,临时加车,加车后,总列车次数不能超过这个最大值,否则就会出现调度不开等问题 (结合workQueue) |
| 3 | keepAliveTime | 表示线程池中线程空闲的时间,当空闲时间达到该值时,线程会被销毁,只剩下corePoolSize个线程位置 |
春运压力过后,临时的加车(如果空闲时间超过keepAliveTime)就会被撤掉,只保留日常固定的列车车次数量用于日常运营 |
| 4 | unit | keepAliveTime的时间单位,最终都会转换成纳秒,因为CPU的执行速度杠杠滴 |
keepAliveTime的单位,春运以天为计算单位 |
| 5 | workQueue | 当请求的线程数大于corePoolSize时,线程进入该阻塞队列 |
春运压力异常大,达到corePoolSize也不能满足要求,所有乘坐请求都会进入该阻塞队列中排队,队列满,还有额外请求,就需要加车了 |
| 6 | threadFactory | 顾名思义,线程工厂,用来生产一组相同任务的线程,同时也可以通过它增加前缀名,虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。 | 比如(北京——上海)就属于该段列车所有前缀,表明列车运输职责 |
| 7 | handler | 执行拒绝策略,当workQueue达到上限,同时也达到maximumPoolSize就要通过这个来处理,比如拒绝、丢弃等,这是一种限流的保护措施 |
当workQueue排队也达到队列最大上线,maximumPoolSize就要提示无票等拒绝策略了,因为我们不能加车了,当前所有车次已经满负载 |
ThreadPoolExecutor工作情景
《阿里巴巴Java手册》建议:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,可以更明确线程池的运行规则,规避资源耗尽的风险。
- 情形一:当正在运行的线程数(包括空闲线程数)\(<\)
corePoolSize时,新建线程执行任务; - 情形二:当正在运行的线程数\(\geq\)
corePoolSize时,新插入的任务进入workQueue排队(如果workQueue长度允许),等待空闲线程来执行。 - 情形三:当
队列里的任务达到上限,并且正在进行的线程\(<\)maxinumPoolSize,对于新加入的任务,新建线程。 - 情形四:当
队列里的任务达到上限,并且正在运行的线程\(=\)maximumPoolSize,对于新加入的任务,执行拒绝策略(线程池默认的策略是抛异常)。
情形一
当池中正在运行的线程数(包括空闲线程数)小于corePoolSize时,新建线程执行任务:
If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
package pool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestThreadPoolExecutor {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, 3, 60L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)
);
// 任务1
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务1!"));
// 任务2
threadPool.execute(
() -> {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " 执行任务2!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
);
threadPool.shutdown();
}
}
输出结果:
pool-1-thread-1 执行任务1!
pool-1-thread-2 执行任务2!
当执行任务1的线程(thread-1)执行完成之后,任务2并没有去复用thread-1而是新建线程(thread-2)去执行任务!
情形二
当池中正在运行的线程数大于等于corePoolSize时,新插入的任务进入workQueue排队(如果workQueue长度允许),等待空闲线程来执行。
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
package pool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestThreadPoolExecutor {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, 3, 60L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)
);
// 任务1
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务1!"));
// 任务2
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务2!"));
// 任务3
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务3!"));
threadPool.shutdown();
}
}
输出结果:
pool-1-thread-1 执行任务1!
pool-1-thread-2 执行任务2!
pool-1-thread-1 执行任务3!
线程池不会为任务3新建线程,因为有一个空的队列,先将其放入队列中,等thread-1执行完任务1,再去执行任务3。此时,maximumPoolSize=3这个参数不起作用。
情形三
当队列里的任务达到上限,并且正在进行的线程小于maxinumPoolSize,对于新加入的任务,新建线程。
package pool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestThreadPoolExecutor {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, 3, 60L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)
);
// 任务1
threadPool.execute(
() -> {
try {
System.out.println(Thread.currentThread().getName() + " 执行任务1!");
TimeUnit.SECONDS.sleep(1); // 注意这里需要加延时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
);
// 任务2
threadPool.execute(
() -> {
try {
System.out.println(Thread.currentThread().getName() + " 执行任务2!");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
);
// 任务3
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务3!"));
// 任务4
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务4!"));
threadPool.shutdown();
}
}
输出结果:
pool-1-thread-1 执行任务1!
pool-1-thread-2 执行任务2!
pool-1-thread-3 执行任务4!
pool-1-thread-3 执行任务3!
任务1、2启动后尚未结束,此时任务3在队列,队列里的任务达到上限,由于正在进行的线程数是2<maximumPoolSize,只能新建一个线程了。然后任务4就进了新线程thread-3,任务4结束,队列里的任务3在线程thread-3进行处理。
情形四
队列里的任务达到上限,并且池中正在运行的线程等于maximumPoolSize,对于新加入的任务,执行拒绝策略(线程池默认的策略是抛异常)。
package pool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestThreadPoolExecutor {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, 3, 60L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)
);
// 任务1
threadPool.execute(
() -> {
try {
System.out.println(Thread.currentThread().getName() + " 执行任务1!");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
);
// 任务2
threadPool.execute(
() -> {
try {
System.out.println(Thread.currentThread().getName() + " 执行任务2!");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
);
// 任务3
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务3!"));
// 任务4
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务4!"));
// 任务5
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务5!"));
threadPool.shutdown();
}
}
输出结果:
pool-1-thread-1 执行任务1!
pool-1-thread-2 执行任务2!
pool-1-thread-3 执行任务4!
pool-1-thread-3 执行任务3!
Exception in thread "main" java.util.concurrent.RejectedExecutionException
队列达到上限,线程池达到最大值,故执行拒绝策略,抛出异常。
四大拒绝策略
当workQueue达到上限,同时也达到maximumPoolSize就要通过这个来处理:
-
new ThreadPoolExecutor.AbortPolicy()- 默认拒绝策略
- 丢弃任务并抛出
RejectedExecutionException异常; - 如果是比较关键的业务,推荐使用此拒绝策略,这样在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
-
new ThreadPoolExecutor.CallerRunsPolicy()// 哪来的去哪里!- 由调用线程(提交任务的线程)处理该任务。
-
new ThreadPoolExecutor.DiscardPolicy()- 直接丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
- 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,博客网站统计阅读量就是采用的这种拒绝策略。
-
new ThreadPoolExecutor.DiscardOldestPolicy()// 队列满了,尝试去和最早的竞争,也不会抛出异常!- 把最早进入工作队列的任务(等待最久)丢弃,然后把新任务加入到工作队列。
为什么不推荐使用Executors
// 1.单个线程
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 2.创建一个固定的线程池的大小
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 3.可伸缩的,遇强则强,遇弱则弱
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
线程池静态工厂Executors的三大方法本质都是调用ThreadPoolExecutor,并且传入的workQueue是一个边界为Integer.MAX_VALUE的无界队列,因为边界太大了,这么大的等待队列非常消耗内存,存在资源耗尽的风险:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
另外调用的ThreadPoolExecutor方法使用的是默认拒绝策略(直接拒绝),但并不是所有业务场景都适合使用这个策略,当很重要的请求过来,直接选择拒绝,显然是不合适的:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
总之,使用Executors创建的线程池太过于理想化,并不能满足很多现实中的业务场景:Executors中默认的线程工厂和拒绝策路过于简单,通常对用户不够友好。
- 线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生产批号一样,为线程本身指定有意义的名称和相应的序列号。
- 拒绝策略应该考虑到业务场景,返回相应的提示或者友好地跳转
所以要求我们通过ThreadPoolExecutor来创建,并传入合适的参数!

浙公网安备 33010602011771号