1.线程池
1.1 线程池是什么
线程池就是 事先将多个线程对象放到一个容器中,当使用的时候就不用 new
线程而是直接去池中拿线程即可,节省了开辟线程的时间,提高了代码执行效率。
那么线程池的出现是要解决什么样的问题呢?
- 降低线程创建和销毁的开销: 线程的创建和销毁是一项开销较大的操作。如果在每次需要执行任务时都创建新的线程,会导致系统性能下降。
- 控制并发线程数量: 在高并发的情况下,如果不限制线程数量,可能会导致资源过度消耗,甚至出现系统崩溃的情况。线程池可以控制并发线程的数量,防止系统资源被耗尽。
- 提供线程管理和监控: 线程池提供了线程的生命周期管理和监控,可以更好地控制线程的状态、健康情况和执行情况。
1.2 Java 标准库中的线程池
在Java标准库中,提供了一个用于管理线程池的Executor
框架,它位于java.util.concurrent
包下。
Executors
类:这是一个包含一些静态工厂方法的辅助类,用于创建不同类型的线程池实例,比如:
newFixedThreadPool(int nThreads)
:创建一个固定大小的线程池,其中包含指定数量的线程。newCachedThreadPool()
:创建一个缓存线程池,线程数可以根据任务数量的变化自动调整。newSingleThreadExecutor()
:创建一个单线程的线程池,只有一个线程在工作,用于顺序执行任务。newScheduledThreadPool(int corePoolSize)
:创建一个可以执行定时任务的线程池。这个方法返回一个ScheduledExecutorService
对象,它是ExecutorService
接口的子接口,专门用于支持定时任务的执行。参数corePoolSize
是指定线程池的核心线程数,它表示在没有定时任务执行时,线程池会保持的线程数量。
ExecutorService
接口:这是线程池的主要接口,定义了一些常用的线程池方法,比如submit()
用于提交任务、shutdown()
用于关闭线程池等。
public class ThreadPoolDemo {
public static void main(String[] args) {
//创建一个拥有 10 个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
//提交 1000 个任务
for (int i = 0; i < 1000; i++) {
int n = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("n:" + n);
}
});
}
}
}
1.2.1 ThreadPoolExecutor 构造方法的参数
Executors
本质上是 ThreadPoolExecutor
类的封装,比如在Executors
中:
ThreadPoolExecutor
最长的构造方法如下:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
它们都是些什么含义呢?
corePoolSize
: 核心线程池大小,即线程池中保持的线程数。即使线程是空闲的,也不会被回收。这些线程会一直存在,除非线程池关闭。maximumPoolSize
: 线程池中允许的最大线程数,包括核心线程和非核心线程。超过核心线程数的线程,如果空闲时间超过keepAliveTime
,则会被回收,直到线程数不超过核心线程数。keepAliveTime
: 非核心线程的空闲时间,超过这个时间,非核心线程会被回收。只有在线程池中线程数量超过核心线程数时,这个参数才会起作用。unit
: 空闲时间的时间单位,例如TimeUnit.SECONDS
表示秒,TimeUnit
是枚举类型。workQueue
: 存放任务的阻塞队列。待执行的任务会被放入这个队列,等待线程池中的线程执行。threadFactory
: 线程工厂,用于创建线程。可以自定义线程的创建方式,例如设置线程名、线程优先级等。handler
: 拒绝策略,用于处理任务添加到线程池被拒绝的情况。当线程池中的线程数量已达到最大线程数,且阻塞队列已满时,新提交的任务将根据指定的拒绝策略进行处理。ThreadPoolExecutor.AbortPolicy(默认)
:该策略会直接抛出RejectedExecutionException
异常,表示拒绝执行新的任务。这是默认的拒绝策略,当线程池的任务队列和线程池都已满时,新的任务将被拒绝。ThreadPoolExecutor.CallerRunsPolicy
:该策略不会抛出异常,而是将被拒绝的任务交给调用线程来执行。也就是说,如果线程池的任务队列和线程池都已满,执行任务的线程将会尝试执行被拒绝的任务。ThreadPoolExecutor.DiscardPolicy
:该策略会默默地丢弃被拒绝的任务,不会抛出任何异常,也不会执行任务。如果对任务丢失无关紧要,可以选择此策略。ThreadPoolExecutor.DiscardOldestPolicy
:该策略会丢弃任务队列中最老的一个任务(即队列头部的任务),然后尝试提交当前被拒绝的任务。这样做的目的是让线程池尽可能保留更近提交的任务。
public static void main(String[] args) {
// 核心线程池大小为 2,最大线程池大小为 5,非核心线程的空闲时间为 1 秒
int corePoolSize = 2;
int maximumPoolSize = 5;
long keepAliveTime = 1;
TimeUnit unit = TimeUnit.SECONDS;
// 使用 LinkedBlockingQueue 作为任务队列,最大容量为 10
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10);
// 自定义线程工厂,用于设置线程名称
ThreadFactory threadFactory = new ThreadFactory() {
private int counter = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "MyThread-" + counter++);
}
};
// 自定义拒绝策略,当任务添加到线程池被拒绝时,在调用者线程中直接执行任务
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
// 创建 ThreadPoolExecutor 对象
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
//……
}
1.3 简单模拟实现线程池
我们这里实现一下线程池的核心代码。这里模拟一个固定大小的线程池。
public class MyThreadPool {
//用阻塞队列来存储任务
private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//n 表示线程池中的线程数量
public MyThreadPool(int n){
for (int i = 0; i < n; i++) {
Thread t = new Thread(()->{
while(true){
try {
//如果队列中没有元素则阻塞
Runnable runnable = queue.take();
//执行任务
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
//添加任务
public void submit(Runnable runnable){
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里的模拟得很简单,但是这个思想很重要。