目录
一、线程池是什么?
在Java中,池(Pool)的概念被广泛应用于各种场景,如:常量池、数据库连接池、对象池等,这些池的设计都是为了提高资源的重用性和系统的性能,降低资源创建和销毁的开销。线程池达成的也是类似的效果。
假设你是一个餐厅的经理,而餐厅的厨房就是一个需要管理的多线程系统。
-
厨房中的厨师就是线程: 想象一下,你的厨房里有一群厨师,每个厨师都是一个线程,负责烹饪不同的菜肴。
-
顾客点菜就是任务: 每当顾客来点菜,相当于有一个新的任务进入系统,需要厨师去做。
-
任务的处理速度: 假设每个菜肴需要一定的时间来烹饪。如果每次有顾客点菜都要招募一个新的厨师(创建新线程),那么会增加很多开销,比如为新厨师分配工作空间、准备厨具等,这就是线程创建和销毁的开销。
-
线程池的作用: 相反,如果你事先准备好一群厨师(线程池),当顾客点菜时,只需从这群厨师中选一个空闲的厨师来做即可,不需要再额外招募新的厨师(创建新线程)。
-
控制厨师数量: 另外,你可能会考虑到厨房的大小和工作效率,不会让太多的厨师同时在厨房里工作(控制线程数量),以免拥挤和混乱。
通过这种方式,你能更有效地利用厨师(线程)的资源,减少了频繁招募和解散厨师(创建和销毁线程)的开销,提高了整个厨房(系统)的效率和稳定性。
二、Java标准库中的线程池
1. ThreadPoolExecutor类
ThreadPoolExecutor是线程池的主要实现类之一。它提供了一个灵活的线程池实现,可以根据需要调整核心线程数、最大线程数、线程存活时间等参数。通过 ThreadPoolExecutor
,可以创建自定义的线程池,以满足特定的需求。
这个线程池用起来比较复杂,构造方法中有很多的参数.(面试考点)
对于上图中的四种构造方法,只要掌握最后一种,即带7个参数的构造方法,前面的就都能理解了,面试中会问到的也是这个带7个参数的构造.
这7个参数主要可以分为几组来理解:
- 参数一(corePoolSize)和参数二(maximunPoolSize):分别表示线程池的核心线程数和最大线程数。可以理解为,如果任务比较轻量,仅有核心线程就可以解决了;如果任务比较繁重,则会创建几个临时线程(核心线程+临时线程<=最大线程数)来解决任务,等到任务执行的差不多了,临时线程就会被释放(回收).
- 参数三(keepAliveTime)和参数四(unit):分别表示临时线程(非核心线程)的最长空闲时间和设置该空闲时间的单位。临时线程在这个设定的时间内一直没有任务执行,就会被回收。unit的类型是TimeUnit,这是一个枚举类,用于表示时间单位,它提供了一组枚举常量用于表示不同的时间单位,包括秒、毫秒、微秒、纳秒等。
- 参数五(workQueue):用于指定等待执行的任务队列,即案例(2)的阻塞队列。线程池会提供一个 submit 方法,让其他线程将任务提交给线程池,submit 中就是将任务加入到这个任务队列。
- 参数六(threadFactory):创建线程的工厂,提供了多个静态方法,是线程的不同初始化方式,默认的线程工厂是通过 Executors 类中的静态方法 defaultThreadFactory() 返回的。
- 参数七(handler):任务量超出负荷的拒绝策略。
使用自定义线程池的代码示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo2 {
public static void main(String[] args) {
//自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3, //核心线程数
6, //最大线程数
60, //临时线程的最长空闲时间
TimeUnit.SECONDS, //空闲时间的单位
new ArrayBlockingQueue<>(100), //传递任务的阻塞队列
Executors.defaultThreadFactory(), //用于创建新线程的线程工厂
new ThreadPoolExecutor.AbortPolicy() //超出负荷时的拒绝策略
);
for (int i = 0; i < 100; i++) {
int id = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello " + id + " " + Thread.currentThread().getName());
}
});
}
//关闭线程池
threadPool.shutdown();
}
}
可以看见,上述自定义线程池这种方式使用起来是比较麻烦的,Java大佬也是又提供了更方便的标准库,对 ThreadPoolExecutor 又进一步封装了。
2. Executors类
Executors 本质上是对 ThreadPoolExecutor 类的封装,它提供了各种静态工厂方法,用于创建不同类型的线程池,以满足不同的需求和场景。
Executors 创建线程池的几种方式:
-
newFixedThreadPool(int nThreads)
:创建一个固定线程数的线程池, -
newCachedThreadPool()
:创建线程数目动态增长的线程池 -
newSingleThreadExecutor()
:创建一个单线程的线程池,该线程池中只包含一个线程。 -
newScheduledThreadPool(int corePoolSize)
:创建一个定时执行任务的线程池,该线程池中包含指定数量的核心线程。是进阶版的Timer(定时器)。
这个类的使用比较简单,同样使用 submit 方法提交任务到线程池即可.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
class MyRunnable implements Runnable {
@Override
public synchronized void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ": hello thread");
}
}
}
public class Demo1 {
public static void main(String[] args) {
//Executors 创建线程的几种方式
//创建固定线程数的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//创建线程数目动态增长的线程池
ExecutorService threadPool1 = Executors.newCachedThreadPool();
//创建只包含单个线程的线程池
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
//设定延迟时间后执行命令,或定期执行命令,是进阶版的Timer 参数1:核心线程数 参数2:线程工厂(默认值)
ScheduledExecutorService threadPool3 = Executors.newScheduledThreadPool(10);
MyRunnable myRun = new MyRunnable();
for (int i = 0; i < 5; i++) {
threadPool.submit(myRun);
}
threadPool.shutdown();
}
}
3. 创建线程池的两种方式中应该怎么选择?
使用 Executors
工厂方法的情况:
-
简单的线程池需求: 如果你只需要一个简单的线程池来执行任务,而且不需要对线程池的参数进行精细调整,那么可以使用
Executors
提供的工厂方法。例如,执行一些简单的并发任务,不需要对线程池的大小、任务队列等进行特定配置。 -
便捷性:
Executors
提供了许多预定义的线程池类型,使用这些工厂方法可以更加方便地创建标准类型的线程池,而无需手动配置线程池的参数。
自定义线程池的情况:
-
特定的线程池需求: 如果你的应用程序对线程池的行为有特定的需求,比如需要配置线程池的大小、任务队列类型、拒绝策略等,就需要自定义线程池。自定义线程池可以满足更复杂的场景需求。
-
性能优化: 通过自定义线程池,可以根据具体的应用场景和硬件环境进行性能优化。例如,调整核心线程数、最大线程数、任务队列类型等,以达到更好的并发性能和资源利用率。
-
更精细的控制: 自定义线程池可以提供更精细的控制和配置选项,如设置线程池的命名、拒绝策略、线程工厂等,以满足特定的业务需求和管理要求。
总结:
-
如果只是简单的并发任务执行,或者需要快速创建标准类型的线程池,可以使用
Executors
提供的工厂方法。 -
如果需要对线程池的行为进行精细控制,或者应用程序对并发性能和资源利用有较高要求,建议自定义线程池以满足特定的需求。
三、自己实现线程池
为了更好地理解线程池的执行流程,这里实现一个简单的线程池:
- 核心操作为 submit,将任务加入到线程池中
- 要有n个工作线程,使用 Runnable 描述一个任务
- 使用一个 BlockingQueue 组织所有的任务
- 每个工作线程要做的事情:不停的从 BlockingQueue 中取任务执行
- 指定一下线程池中的最大线程数 maxWorkerCount;当 当前线程数达到这个最大值时,就不在新增线程了
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
//带最大线程数和核心线程的线程池
class MyThreadPool2 {
//阻塞队列
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
private int maxPoolSize;
//保存当前的所有线程
private List<Thread> threadList = new ArrayList<>();
/**
* @param corePoolSize 核心线程数
* @param maxPoolSize 最大线程数
*/
public MyThreadPool2(int corePoolSize, int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
//创建n个线程,并设定每个线程要执行的任务
for (int i = 0; i < corePoolSize; i++) {
Thread t = new Thread(() -> {
try {
while (true) {
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
throw new RuntimeException();
}
});
t.start();
threadList.add(t);
}
}
public void submit(Runnable runnable) throws InterruptedException {
//此处进行判定,判定当前任务数量(任务队列的元素个数)是否较多
//如果多了,就创建新的线程;如果不多,就不需要
queue.put(runnable);
//设置一个任务数量的阈值(自己按需求指定),达到这个阈值,且当前线程数量不为最大线程数,就创建新线程
if (queue.size() >= threadList.size() * 50 && threadList.size() < maxPoolSize) {
for (int i = 0; i < maxPoolSize - threadList.size(); i++) {
Thread t = new Thread(() -> {
try {
while (true) {
Runnable task = queue.take();
task.run();
}
} catch (InterruptedException e) {
throw new RuntimeException();
}
});
t.start();
threadList.add(t);
}
}
}
}
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool2 myThreadPool = new MyThreadPool2(10, 20);
for (int i = 1; i <= 10000; i++) {
int id = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello " + id + " " + Thread.currentThread().getName());
}
});
}
}
}
还有一些地方可以改进和优化:
-
线程池的关闭机制:目前线程池没有提供关闭的方法,最好添加一个
shutdown()
方法,用于正确关闭线程池。关闭线程池时,应该停止向队列中添加新任务,并等待已提交的任务执行完成。 -
异常处理:线程池中的线程应该能够处理异常情况,例如当任务执行过程中发生异常时,应该能够捕获并处理异常,而不是简单地抛出
RuntimeException
。 -
任务拒绝策略:在任务队列满时,当前实现是通过阻塞
put()
方法来等待队列空闲。但是,当队列满时,可能希望采取一些其他的任务拒绝策略,比如直接抛出异常、丢弃任务等。
对于工作中的日常开发而言,掌握标准库中的线程池的使用即可。