文章目录
- [1. 什么是线程池](#1. 什么是线程池)
- [2. 为什么要使用线程池(线程池有什么优点)](#2. 为什么要使用线程池(线程池有什么优点))
- [3. 如何使用Java标准库提供的线程池](#3. 如何使用Java标准库提供的线程池)
-
- [3.1 创建一个线程池对象](#3.1 创建一个线程池对象)
- [3.2 什么是工厂模式](#3.2 什么是工厂模式)
- [3.3 为什么要使用工厂模式](#3.3 为什么要使用工厂模式)
- [3.4 Executors 创建不同具有不同特性的线程池](#3.4 Executors 创建不同具有不同特性的线程池)
- [3.5 ThreadPool 类的构造方法](#3.5 ThreadPool 类的构造方法)
- [3.6 线程池的拒绝策略](#3.6 线程池的拒绝策略)
- [3.7 调用 submit 方法添加任务](#3.7 调用 submit 方法添加任务)
- [4. 自己实现一个线程池](#4. 自己实现一个线程池)
1. 什么是线程池
线程池是一种多线程处理形式,它处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池中的线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。线程池避免了在处理短时间任务时创建与销毁线程的代价,从而提高了程序的效率和性能。
2. 为什么要使用线程池(线程池有什么优点)
我们都知道,在 Java 中使用多进程效率是比较低的,因为进程的创建和销毁的开销是比较大的,这样就会导致进程的创建和销毁的速度比较慢。所以在多进程的基础上就出现了线程。线程的创建和销毁都比较轻量,多个线程共用一套资源,这就避免了多次向计算机申请资源,极大提高了代码的执行速度。但是如果一个线程多次创建和销毁的话,也会导致系统资源的频繁调用,并且创建和销毁线程的而操作是内核态的,计算机通过调用相关的 API,然后进行线程的创建和销毁,但是既然是内核态操作,那么在计算机创建和销毁线程的过程中可能不是只干了这一件事,可能还会顺便帮其他线程提供资源等,这样就降低了代码的执行速度,所以为了解决线程多次创建和销毁,并且保证线程的创建和销毁属于用户态的操作的问题,就出现了线程池这一概念。在线程池中会提前创建 n 个线程,这些线程在执行完后不会销毁,而是继续存储在线程池当中等待下一次调用,正是因为线程池的这一概念,就使得线程创建和销毁的频率降低了。
总结来说,线程池的优点有以下这些:
- 降低资源消耗:通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务无需等待线程创建,可以立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配,调优和监控。
- 避免系统过度切换:如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者产生"过度切换"的问题。
3. 如何使用Java标准库提供的线程池
3.1 创建一个线程池对象
在Java的 java.util.concurrent
包中提供了线程池相关的方法。那么如何创建出能执行线程池相关操作的对象呢?
java
ExecutorService service = Executors.newScheduledThreadPool(3);
ExecutorService
是Java中的一个接口,它继承了Executor
接口。
ExecutorService
接口在 java.util.concurrent
包中,它用于管理线程池。它提供了一种方式来管理和控制线程的生命周期。具体来说,它用于创建和管理线程池,可以执行线程,也可以关闭线程池。
而 Executors
则是一个工厂类,用来创建不同类型的 ThreadPoolExecutor
实例。
看到工厂类就需要提到一个常用的模式------工厂模式了,那么什么又是工厂模式呢?
3.2 什么是工厂模式
工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式,通过将对象的实例化过程封装在工厂类中,使得创建对象的方式更加灵活和可扩展。
在工厂模式中,客户端代码只需关注接口,而无需关注对象的具体创建过程。工厂模式通过提供一个统一的接口来创建不同类型的对象,这个接口定义了创建对象的标准方式。
3.3 为什么要使用工厂模式
工厂模式的作用是用来创建一个类的不同类型对象,既然这样的话,我们在一个类中使用多种重载的构造方法不就好了吗,为什么要多此一举再创建一个工厂类来创建一个类的不同类型的对象呢?
如果我们不使用工厂类来创建不同类型的对象,那么在创建对象的时候就需要在客户端中显式地选择合适的构造方法并提供对应的参数,这样的话类的具体创建逻辑就暴露了。而使用工厂模式的话,客户端代码只需调用工厂类的接口即可,而无需了解具体的创建逻辑。这样可以将对象的创建与使用代码分离,使得系统更加灵活,可扩展性更强。同时,使用工厂模式还可以避免在客户端代码中暴露对象的创建逻辑,提高了系统的安全性。
当创建线程池对象的时候,我们只需要调用 Executors
工厂类的对应静态方法,并且传递对应的参数就可以得到不同类型的 ThreadPoolExexutor
实例了,通过这个工厂模式既实现了创建一个类的不同实例的功能,又保证了系统的安全性。
3.4 Executors 创建不同具有不同特性的线程池
在知道什么是工厂模式之后,我们就利用这个工厂类来创建出需要的线程池实例,那么 Executors
工厂类又提供了哪些创建线程池对象的方法呢?它们又分别具有什么特性呢?
newFixedThreadPool() 方法
newCachedThreadPool() 方法
newScheduledThreadPool() 方法
Executors 工厂类还有很多不同的创建线程池对象的方法,这里我就不给大家一一展示出来了,大家如果感兴趣的话,可以去Java帮助文章上去查看。
3.5 ThreadPool 类的构造方法
通过查看源码,我们可以知道,Executors 工厂类创建的线程池对象都是通过传递不同的参数来实例化 ThreadPool 类的,也就是说 ThreadPool 类具有多种构成重载的构造方法,那么来看看这些不同的构造函数的参数分别代表什么吧。
- corePoolSize 表示线程池中的核心线程数
- maximumPoolSize 表示线程池中可含有的最大线程数
- keepAliveTime 表示当线程池中的线程数量超过核心线程数(corePoolSize)时,多余的空闲线程在终止之前等待新任务的最长时间。
- TimeUnit unit 用于指定keepAliveTime参数的时间单位。
- workQueue 表示阻塞队列,可以根据需要设置阻塞队列的类型,如果需要优先级,则可以使用PriorityBlockingQueue;如果不需要优先级且任务的数目是恒定的,则可以使用ArrayBlockingQueue;如果任务的数目不是恒定的,则可以使用LinkedBlockingQueue
- ThreadFactory 表示工厂类
- RejectedExecutionHandle handle 表示拒绝策略
这里解决策略是面试中容易考的高频考点,那么这里我们就来详细的说说关于线程池的拒绝策略。
3.6 线程池的拒绝策略
当线程池中容纳的任务数量到达了最大限制之后,如果继续往里面添加任务的话,会出现什么情况呢?Java 中提供了4种拒绝策略。
- ThreadPoolExecutor.AbortPolicy 抛出异常
- ThreadPoolExecutor.CallerRunsPolicy 新添加的任务由添加任务的线程执行该任务
- ThreadPoolExecutor.DiscardOldestPolicy 丢弃掉最旧的未被处理的请求
- ThreadPoolExecutor.DiscardPolicy 丢弃掉当前新加的任务
3.7 调用 submit 方法添加任务
当创建了适当的线程池对象并且了解了其中创建的细节了之后,我们就需要调用该线程对象的相关方法来执行代码。
使用 submit 方法来添任务。
java
public class Demo1 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程1");
}
});
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程2");
}
});
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程3");
}
});
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程3");
}
});
}
}
4. 自己实现一个线程池
同样的虽然 Java 标准库提供了线程池,但是我们作为初学者如果能够自己实现一个线程池,那么对于我们理解其中的逻辑和细节很有帮助。
java
class MyThreadPool {
//创建一个阻塞队列
BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
//实现submit方法
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
//实现构造方法,类创建的时候就会执行任务
public MyThreadPool(int n) {
for(int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
Runnable runnable = null;
try {
runnable = queue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
runnable.run();
});
t.start();
}
}
}
测试
java
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(4);
for(int i = 0; i < 4; i++) {
int id = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行线程 " + id);
}
});
}
}
}
由于使用的是阻塞队列,所以当线程池中的任务达到数量限制的时候,如果再添加任务,会进入阻塞等待状态,这是不同于Java标准库提供的四种拒绝策略。