线程池[重点]
一、概述
- 线程池就是一个可以复用线程的技术。
1、问题?
不使用线程池的问题
- 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的(占用内存空间、CPU资源),这样会严重影响系统的性能。
2、工作原理
- 线程池中有三个人,这三个人就相当于三个线程、三个员工;
- 三个人下面的多个红点,就是每个用户发起的请求;
- 这三个人(线程),一人处理一个请求,处理完一个再接着处理下一个;
- 这样就避免了不断要创建新线程来处理每个用户发起的请求,将内存、CPU资源耗尽的风险!!
二、线程池实现的API、7个参数说明
1、谁代表线程池?
- JDK 5.0起提供了代表线程池的接口:
ExecutorService
2、如何得到线程池对象?
方式一[重点]:
-
使用
ExecutorService
的实现类ThreadPoolExecutor
自创建一个线程池对象
(1)ThreadPoolExecutor构造器的参数说明
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数 | 说明 | 备注 |
---|---|---|
corePoolSize | 指定线程池的线程数量(核心线程) | 不能小于0 |
maximumPoolSize | 指定线程池可支持的最大线程数 | 最大数量 >= 核心线程数量 |
keepAliveTime | 指定临时线程的最大存活时间 | 不能小于0 |
unit | 指定存活时间的单位(秒、分、时、天) | 时间单位 |
workQueue | 指定任务队列 | 不能为null |
threadFactory | 指定用哪个线程工厂创建线程 | 不能为null |
handler | 指定线程忙、任务满的时候,新任务来了如何做 | 不能为null |
(2)结合实际生活说明7个参数
- KTV:
- 如果你不用线程池的话:
- 每来一个客人,你就得立马招一个新服务员来招待客人;
- 如果一次性来几百个客人,这样就得招几百个新服务员,这样成本就高了,资源就浪费了,亏本!!
- 如果你使用线程池的话:
- 那先招3个正式员工(corePoolSize),正式员工不可开除,最多支持10个员工(maximumPoolSize);
- 这样就可以招7个临时员工,最多可以在KTV工作2天(keepAliveTime),2天后还没有新任务,那就开除掉这7个临时员工;
- 在KTV门口放置5个座位(workQueue),给客人排队用的;
- KTV人力资源(threadFactory)负责招人,最多可招7人,因为有3个是正式员工;
- 3个正式员工(corePoolSize)、7个临时员工(maximumPoolSize)都在忙的时候,KTV门口的5个座位(workQueue)也满了,此时新来了一个客人,该怎么应对(handler)?
- 如果你不用线程池的话:
(3)线程池常见面试题
临时线程什么时候创建?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
方式二:Executors工具类实现线程池
- 使用
Executors
(线程池的工具类)调用方法返回不同特点的线程池对象
(1)Executors的常用API
- Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加, 如果线程任务执行完毕 且空闲了一段时间则会被回收掉 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池, 如果某个线程因为执行异常而结束, 那么线程池会补充一个新线程替代它 |
public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象, 如果该线程出现异常而结束, 那么线程池会补充一个新线程 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定 的延迟后运行任务,或者定期执行任务 |
注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。
package com.app.d8_thread_pool;
/**
定义线程任务类
*/
public class MyRunnable extends Thread{
/**
重写run方法(里面是线程任务)
*/
@Override
public void run() {
// 获取线程名称
String name = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println(name + "线程输出了第" + i + "个HelloWorld!!");
}
// 模拟线程很忙的时候
try {
System.out.println(name + "线程与任务绑定了,线程进入休眠了~~");
Thread.sleep(100000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.app.d8_thread_pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
目标:使用Executors工具类实现线程池。
它的底层也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的
*/
public class ThreadPoolDemo3 {
public static void main(String[] args) {
// 1、创建固定线程数量的线程池
ExecutorService pools = Executors.newFixedThreadPool(3);
// 2、创建线程任务交给线程池处理
pools.execute(new MyRunnable());
pools.execute(new MyRunnable());
pools.execute(new MyRunnable());
// 已超出固定线程池的数量范围,因此该任务不会执行!!
pools.execute(new MyRunnable());
}
}
(2)使用Executors的注意事项
-
大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
- newFixedThreadPool(int nThreads)、newSingleThreadExecutor():
- 底层没有对任务队列的数量做限制,因此可以无限添加线程任务,可能会导致内存溢出!
- newCachedThreadPool()、newScheduledThreadPool(int corePoolSize):
- 底层没有对线程、任务队列做数量限制,因此可能会导致内存、CPU资源耗尽!
方法名称 | 说明 |
---|---|
public static ExecutorService newFixedThreadPool(int nThreads) public static ExecutorService newSingleThreadExecutor() | 允许请求的任务队列长度是 Integer.MAX_VALUE, 可能出现OOM错误(java.lang.OutOfMemoryError) |
public static ExecutorService newCachedThreadPool() public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建的线程数量最大上限是 Integer.MAX_VALUE, 线程数可能会随着任务1:1增长, 也可能出现OOM错误(java.lang.OutOfMemoryError) |
总结
1、Executors工具类底层是基于什么方式实现线程池对象的?
- 线程池ExecutorService的实现类:ThreadPoolExecutor
2、Executors是否适合做大型互联网场景的线程池方案?
- 不合适
- 建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险
三、线程池处理Runnable任务
1、ThreadPoolExecutor创建线程池对象
ExecutorService pools = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(6),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
2、ExecutorService的常用API
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行任务/命令,无返回值,一般用来执行 Runnable 任务 |
Future< T > submit(Callable< T > task) | 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() | 等待全部任务执行完毕后关闭线程池 |
List< Runnable > shutdownNow() | 立刻关闭,停止正在执行的所有任务,并返回队列中未执行的任务 |
3、新任务拒绝策略
策略 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出 RejectedExecutionException 异常(是默认的策略) |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常(不推荐) |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的 run() 方法从而绕过线程池直接执行(相当于是老板亲自服务) |
4、范例
package com.app.d8_thread_pool;
/**
定义线程任务类
*/
public class MyRunnable extends Thread{
/**
重写run方法(里面是线程任务)
*/
@Override
public void run() {
// 获取线程名称
String name = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println(name + "线程输出了第" + i + "个HelloWorld!!");
}
// 模拟线程很忙的时候
try {
System.out.println(name + "线程与任务绑定了,线程进入休眠了~~");
Thread.sleep(100000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.app.d8_thread_pool;
import java.util.concurrent.*;
/**
目标:学习使用线程池处理 Runnable 任务
作用:线程池大大提高了线程的复用技术,不需要为每个新任务都创建一个新线程!!
*/
public class ThreadPoolDemo1 {
public static void main(String[] args) {
// 1、创建线程池
/*
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
*/
ExecutorService pools = new ThreadPoolExecutor(3, 5, 5, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(6), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// 2、创建 Runnable 任务
Runnable target = new MyRunnable();
/**
3、将Runnable任务交给线程池处理
*/
// a.3个核心线程被占用
pools.execute(target);
pools.execute(target);
pools.execute(target);
// b.任务队列已满6个
pools.execute(target);
pools.execute(target);
pools.execute(target);
pools.execute(target);
pools.execute(target);
pools.execute(target);
// c.核心线程在忙,任务队列已满,开始创建临时线程
pools.execute(target);
pools.execute(target);
// d.3个核心线程 + 2个临时线程,已达到线程池最大线程数量,此时开始触发新任务拒绝策略
try {
pools.execute(target); // 抛出异常: RejectedExecutionException
} catch (Exception e){ // 捕获异常!
System.out.println("临时线程数量已超出线程池最大线程数量~~");
e.printStackTrace();
}
// 4、关闭线程池(开发中一般不会使用:因为无论是系统、网站、购物网等等,都是要一直处理每个用户发起的请求的。)
// pools.shutdownNow(); // 立即关闭线程池,即使任务没有完成,也会关闭,会丢失任务!!
// pools.shutdown(); // 等待所有任务执行完毕后再关闭线程池!!
}
}
临时线程数量已超出线程池最大线程数量~~
pool-1-thread-2线程输出了第1个HelloWorld!!
pool-1-thread-5线程输出了第1个HelloWorld!!
pool-1-thread-3线程输出了第1个HelloWorld!!
pool-1-thread-1线程输出了第1个HelloWorld!!
pool-1-thread-3线程输出了第2个HelloWorld!!
pool-1-thread-5线程输出了第2个HelloWorld!!
pool-1-thread-4线程输出了第1个HelloWorld!!
pool-1-thread-2线程输出了第2个HelloWorld!!
pool-1-thread-4线程输出了第2个HelloWorld!!
pool-1-thread-5线程输出了第3个HelloWorld!!
pool-1-thread-3线程输出了第3个HelloWorld!!
pool-1-thread-1线程输出了第2个HelloWorld!!
pool-1-thread-4线程输出了第3个HelloWorld!!
pool-1-thread-2线程输出了第3个HelloWorld!!
pool-1-thread-1线程输出了第3个HelloWorld!!
pool-1-thread-4线程与任务绑定了,线程进入休眠了~~
pool-1-thread-5线程与任务绑定了,线程进入休眠了~~
pool-1-thread-1线程与任务绑定了,线程进入休眠了~~
pool-1-thread-2线程与任务绑定了,线程进入休眠了~~
pool-1-thread-3线程与任务绑定了,线程进入休眠了~~
java.util.concurrent.RejectedExecutionException: Task Thread[Thread-0,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@378bf509[Running, pool size = 5, active threads = 5, queued tasks = 6, completed tasks = 0]
at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2065)
at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:833)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1365)
at com.app.d8_thread_pool.ThreadPoolDemo1.main(ThreadPoolDemo1.java:51)
总结
1、线程池如何处理Runnable任务?
- 使用ExecutorService的方法:
- void execute(Runnable target)
四、线程池处理Callable任务
1、范例
package com.app.d8_thread_pool;
import java.util.concurrent.Callable;
/**
定义线程任务类
*/
public class MyCallable implements Callable<String> {
private int n; // 定义变量,用于存储一个数
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0; // 定义变量,用于求和
// 计算1-n的和
for (int i = 1; i <= n; i++) {
sum += i;
}
// 返回线程执行任务的结果
return Thread.currentThread().getName() + "线程执行计算 1-" + n + " 的和,结果是:" + sum;
}
}
package com.app.d8_thread_pool;
import java.util.concurrent.*;
/**
目标:使用线程池处理 Callable 任务
作用:线程池大大提高了线程的复用技术,不需要为每个新任务都创建一个新线程!!
*/
public class ThreadPoolDemo2 {
public static void main(String[] args) throws Exception {
// 1、创建线程池
ExecutorService pools = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5), new ThreadPoolExecutor.AbortPolicy());
// 2、创建Callable线程任务并交给线程池处理
Future<String> s1 = pools.submit(new MyCallable(100));
Future<String> s2 = pools.submit(new MyCallable(200));
Future<String> s3 = pools.submit(new MyCallable(300));
Future<String> s4 = pools.submit(new MyCallable(400));
Future<String> s5 = pools.submit(new MyCallable(500));
Future<String> s6 = pools.submit(new MyCallable(600));
// 3、获取线程执行任务完毕后的结果并输出
// String rs = s1.get();
// System.out.println(rs);
System.out.println(s1.get());
System.out.println(s2.get());
System.out.println(s3.get());
System.out.println(s4.get());
System.out.println(s5.get());
System.out.println(s6.get());
}
}
总结
1、线程池如何处理Callable任务,并得到任务执行完成后返回的结果?
- 使用ExecutorService的方法:
- Future< T > submit(Callable< T > command)