程序的运行,本质就是消耗系统资源,当我们频繁的创建和销毁线程时,会十分浪费系统资源,因此为了优化资源的使用,我们就有必要使用线程池的技术。
池化技术:实现分配好一定的资源,当有需要用时,就从分配好的资源中取出,用完后再归还;
1、线程池的优势
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
总体来说,线程池的使用提高了线程的复用性,并且能对最大的并发数进行控制,并对线程进行管理;
2、线程池必会(重点)
3大方法,7大参数,4种拒绝策略
3大方法:
在阿里开发手册中,规定线程池不得使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式能让我们更加明确线程池的运行规则,规避资源耗尽的风险。
Executor创建线程池的风险如下:
- 运行创建线程的最大数量为Integer的最大值,这个值约为21亿,这样会造成请求堆积,导致OOM。
由于线程的3大方法跟Executors有关,我们去学习这3大方法还是需要研究一下Executors;
Executors类似集合中的Collections一样,是一个工具类,在Executors中有3大方法:
- Executors.newSingleThreadExecutor(); //创建单个线程
- Executors.newFixedThreadPool(n); //创建指定线程
- Executors.newCachedThreadPool(); //根据运行环境自动创建线程(遇强则强,遇弱则弱)
代码演示
public class ThreadPoolDemo_Executors {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor(); //创建单个线程
// ExecutorService executor = Executors.newFixedThreadPool(5);//创建指定线程
// ExecutorService executor = Executors.newCachedThreadPool(); //根据运行环境自动创建线程(遇强则强,遇弱则弱)
try {
for (int i = 0; i < 10; i++) {
//使用了线程池之后,我们不再用new Thread的方式创建线程,直接使用线程池来获取即可
executor.execute(()->{
System.out.println(Thread.currentThread().getName()+" is running");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//程序结束,一定要关闭线程池
executor.shutdown();
}
}
}
运行结果
可以看到,这里打印了10次,都是同一条线程在运行。
现在切换为第二种创建线程池的方式,指定线程池的线程数:
Executors.newFixedThreadPool(n);
运行结果:
程序运行10次,最大使用到了5条线程同时运行
现在切换为第三种方式,弹性使用线程数:
Executors.newCachedThreadPool();
运行结果:
可以看到,最大支持了10个并发
7大参数
我们点进这3大方法,查看一下它们的源码:
//单一线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(
1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
);
}
//指定线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
);
}
//弹性线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()
);
}
通过源码可以看出,这三个方法本质都是使用ThreadPoolExecutor
来创建线程池的,我们再来找到ThreadPoolExecutor
的源码:
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;
}
可以看到在这个方法中,会传递进7个参数,这就是前面所说的7大参数
。
corePoolSize | 核心线程池大小 |
---|---|
maximumPoolSize | 最大线程池大小 |
keepAliveTime | 超时不调用就会释放 |
unit | 超时时间单位 |
workQueue | 阻塞队列 |
threadFactory | 线程工厂,创建线程的,一般不动该参数 |
handler | 拒绝策略 |
假设一个场景:
- 我们在银行办理业务,假设现在是有5个窗口,但是只开放了2个窗口,现在办理业务的人不多,2个窗口足以满足这些需要办理业务的人。(这2个窗口就可以看作核心线程池
corePoolSize
,是始终开放的)- 过了一段时间,办理业务的人逐渐增多,需要开始排队,而银行后候客区,办理业务的人就可以在候客区等候。(候客区就可以看作阻塞队列
workQueue
)- 又过了一段时间,办理业务的人越来越多,2个窗口已经忙不过来了,并且候客区也满了,因此,银行就需要开放剩下没开放的3个窗口来帮助处理大量等待的客户办理业务。(这时,一共开放了5个窗口,而这5个窗口就是线程池的最大线程数
maximumPoolSize
)- 到后面,越来越多的客户来办理业务,现在候客区也满了,所有窗口都在办理业务,银行处理不过来这么多客户要办理的业务,不得不要求后面进来的客户去别的地方办理,或者改天再来办理。(而这就是拒绝策略
handler
)。- 等到大部分客户办理完业务,3、4、5号窗口长时间没人办理了,就可以关闭3、4、5号窗口。(超时释放
keepAliveTime
。超时就一定会有一个时间单位unit
)。
根据这个场景,我们完成这个案例的实现:
public class ThreadPoolDemo_ThreadPoolExecutor {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2, //银行已开放的两个窗口
5, //银行总窗口数
1, //超时时间长度
TimeUnit.MICROSECONDS, //时间单位
new LinkedBlockingQueue<>(3), //候客区大小
Executors.defaultThreadFactory(), //线程池
new ThreadPoolExecutor.AbortPolicy()); //拒绝策略(默认策略)
try {
for (int i = 1; i <= 8 ; i++) {
final int temp = i;
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"号窗口为第"+temp+"客户办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//一定要关闭线程池
threadPool.shutdown();
}
}
}
运行结果:
可以看到,当有8个客户要办理业务时,所有窗口都会开辟,而最大的处理数量 = 线程池最大容量+阻塞队列长度
。这时我们如果有9个人办理业务,程序就会抛出异常,实行拒绝策略。
当有9个人办理业务时,运行结果:
而抛出的异常的这个拒绝策略,就是我们在传递参数时的最后一个参数ThreadPoolExecutor.AbortPolicy()
。
4种拒绝策略
AbortPolicy | 不处理该线程,抛出异常 |
---|---|
CallerRunPolicy | 哪里来的回哪里,返回给调用过来的线程处理,不会抛出异常 |
DiscardPolicy | 丢掉任务,不会抛出异常 |
DiscardOldestPolicy | 尝试与最早获取到资源的线程竞争,如果没有竞争到,会丢掉任务,不会抛出异常 |
扩展
在实际开发中,一般线程池的最大线程数会根据两种方式来确定:
- CPU密集型:根据当前主机的核心线程数来确定最大线程;
(在Java中可通过Runtime.getRuntime().availableProcessors()
获取,在创建线程池时传递给maximumPoolSize
即可)- IO密集型:判断程序中十分占用IO的线程,一般设置为该资源的2倍;