线程池介绍
线程池(thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,对线程统一管理。
线程池就是存放线程的池子,池子里存放了很多可以复用的线程。
创建线程和销毁线程的花销是比较大的(手动new Thread 类),创建和消耗线程的时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程是比较消耗资源的。(我们可以把创建和销毁的线程的过程去掉)。
使用线程池的优势
- 提高效率,创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;
四种创建线程池的方式
Executors类(并发包)提供了4种创建线程池方法,这些方法最终都是通过配置ThreadPoolExecutor的不同参数,来达到不同的线程管理效果。
newCacheTreadPool
创建一个可以缓存的线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,没回收的话就新建线程
newFixedThread
创建一个定长的线程池,可控制最大并发数,超出的线程进行队列等待。
newScheduleThreadPool
可以创建定长的、支持定时任务,周期任务执行。
newSingleExecutor
创建一个单线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newCacheTreadPool
创建一个可以缓存的线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,没回收的话就新建线程。
总结: 线程池的最大核心线程为无限大,当执行第二个任务时第一个任务已经完成,则会复用执行第一个任务的线程;如果第一个线程任务还没有完成则会新建一个线程。
public static void main(String[] args) {
// 创建可缓存线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
//创建任务
Runnable runnable = new Runnable(){
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
};
newCachedThreadPool.execute(runnable);
}
}
newFixedThreadPool
创建一个定长的线程池,可控制最大并发数,超出的线程进行队列等待。
总结:创建指定长度的线程池,任务超出当前线程池执行线程数量则会一直等待,直到运行。
public static void main(String[] args) {
// 创建定长线程池
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
//创建任务
Runnable runnable = new Runnable(){
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
};
// 将任务交给线程池管理
newFixedThreadPool.execute(runnable);
}
}
newScheduledThreadPool
创建定长的、支持定时任务,周期任务执行。
总结:以下案例中延迟2秒后开始执行线程池中的所有任务。
public static void main(String[] args) {
// 创建支持定时线程池
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 5; i++) {
//创建任务
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
// 将任务交给线程池管理,延迟2秒后才开始执行线程池中的所有任务
newScheduledThreadPool.schedule(runnable, 2, TimeUnit.SECONDS);
}
}
newSingleExecutor
创建一个单线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public static void main(String[] args) {
// 创建单线程-线程池,任务依次执行
ExecutorService newScheduledThreadPool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
//创建任务
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
// 将任务交给线程池管理
newScheduledThreadPool.execute(runnable);
}
}
推荐多线程用法
推荐通过 new ThreadPoolExecutor() 的写法创建线程池,这样写线程数量更灵活,开发中多数用这个类创建线程。
ThreadPoolExecutor核心参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数 |
含义 |
解释 |
corePoolSize |
线程池中的核心线程数 |
核心线程生命周期无限,即使空闲也不会死亡。 |
maximumPoolSize |
线程池中最大线程数 |
任务队列满了以后当有新任务进来则会增加一个线程来处理新任务, (线程总数 < maximumPoolSize) |
keepAliveTime |
闲置超时时间 |
当线程数大于核心线程数时,经过keepAliveTime时间将会回收非核心线程 |
unit |
超时时间的单位 (时/分/秒等) |
* |
workQueue |
线程池中的任务队列 |
存放任务(Runnable)的容器 |
threadFactory |
为线程池提供创建新线程的线程工厂 |
* |
rejectedExecutionHandler |
拒绝策略 |
新增一个任务到线程池,如果线程池任务队列超过最大值之后,并且已经开启到最大线程数时,默认为抛出ERROR异常 |
线程池的工作原理
多线程四种拒绝策略
这四种拒绝策略,在ThreadPoolExecutor是四个内部类。
AbortPolicy abortPolicy = new ThreadPoolExecutor.AbortPolicy();
DiscardPolicy discardPolicy = new ThreadPoolExecutor.DiscardPolicy();
DiscardOldestPolicy discardOldestPolicy =
new ThreadPoolExecutor.DiscardOldestPolicy();
CallerRunsPolicy callerRunsPolicy = new ThreadPoolExecutor.CallerRunsPolicy();
1. AbortPolicy
当任务添加到线程池中被拒绝时,直接丢弃任务,并抛出
RejectedExecutionException异常。
2. DiscardPolicy
当任务添加到线程池中被拒绝时,丢弃被拒绝的任务,不抛异常。
3. DiscardOldestPolicy
当任务添加到线程池中被拒绝时,丢弃任务队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
4. CallerRunsPolicy
被拒绝任务的处理程序,直接在execute方法的调用线程中运行被拒绝的任务。
总结:就是被拒绝的任务,直接在主线程中运行,不再进入线程池。
CallerRunsPolicy这种方式好多同学可能不太理解,这里给大家看个Demo:
public static void main(String[] args) {
// 创建单线程-线程池,任务依次执行
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2,
60, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(2),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
};
// 将任务交给线程池管理
threadPoolExecutor.execute(runnable);
}
}
控制台打印:
如何合理配置线程池
使用线程池时通常我们可以将执行的任务分为两类:
- cpu 密集型任务
- io 密集型任务
cpu 密集型任务,需要线程长时间进行的复杂的运算,这种类型的任务需要少创建线程(以CPU核数+1为准),过多的线程将会频繁引起上文切换,降低任务处理速度。
而 io 密集型任务,由于线程并不是一直在运行,可能大部分时间在等待 IO 读取/写入数据,增加线程数量可以提高并发度,尽可能多处理任务。
最后给大家补充一个知识点,配置线程池最好的方式是可以动态修改线程池配置,
例如调用线程池的threadPoolExecutor.setCorePoolSize();方法,
搭配分布式配置中心可以随着运行场景动态的修改核心线程数等功能。