线程池是一种多线程处理形式,它处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池中的线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。线程池避免了在处理短时间任务时创建与销毁线程的代价,从而提高了程序的效率和性能。
我们都知道,在 Java 中使用多进程效率是比较低的,因为进程的创建和销毁的开销是比较大的,这样就会导致进程的创建和销毁的速度比较慢。所以在多进程的基础上就出现了线程。线程的创建和销毁都比较轻量,多个线程共用一套资源,这就避免了多次向计算机申请资源,极大提高了代码的执行速度。但是如果一个线程多次创建和销毁的话,也会导致系统资源的频繁调用,并且创建和销毁线程的而操作是内核态的,计算机通过调用相关的 API,然后进行线程的创建和销毁,但是既然是内核态操作,那么在计算机创建和销毁线程的过程中可能不是只干了这一件事,可能还会顺便帮其他线程提供资源等,这样就降低了代码的执行速度,所以为了解决线程多次创建和销毁,并且保证线程的创建和销毁属于用户态的操作的问题,就出现了线程池这一概念。在线程池中会提前创建 n 个线程,这些线程在执行完后不会销毁,而是继续存储在线程池当中等待下一次调用,正是因为线程池的这一概念,就使得线程创建和销毁的频率降低了。
总结来说,线程池的优点有以下这些:
在Java的 java.util.concurrent
包中提供了线程池相关的方法。那么如何创建出能执行线程池相关操作的对象呢?
ExecutorService service = Executors.newScheduledThreadPool(3);
ExecutorService
是Java中的一个接口,它继承了Executor
接口。
ExecutorService
接口在 java.util.concurrent
包中,它用于管理线程池。它提供了一种方式来管理和控制线程的生命周期。具体来说,它用于创建和管理线程池,可以执行线程,也可以关闭线程池。
而 Executors
则是一个工厂类,用来创建不同类型的 ThreadPoolExecutor
实例。
看到工厂类就需要提到一个常用的模式——工厂模式了,那么什么又是工厂模式呢?
工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式,通过将对象的实例化过程封装在工厂类中,使得创建对象的方式更加灵活和可扩展。
在工厂模式中,客户端代码只需关注接口,而无需关注对象的具体创建过程。工厂模式通过提供一个统一的接口来创建不同类型的对象,这个接口定义了创建对象的标准方式。
工厂模式的作用是用来创建一个类的不同类型对象,既然这样的话,我们在一个类中使用多种重载的构造方法不就好了吗,为什么要多此一举再创建一个工厂类来创建一个类的不同类型的对象呢?
如果我们不使用工厂类来创建不同类型的对象,那么在创建对象的时候就需要在客户端中显式地选择合适的构造方法并提供对应的参数,这样的话类的具体创建逻辑就暴露了。而使用工厂模式的话,客户端代码只需调用工厂类的接口即可,而无需了解具体的创建逻辑。这样可以将对象的创建与使用代码分离,使得系统更加灵活,可扩展性更强。同时,使用工厂模式还可以避免在客户端代码中暴露对象的创建逻辑,提高了系统的安全性。
当创建线程池对象的时候,我们只需要调用 Executors
工厂类的对应静态方法,并且传递对应的参数就可以得到不同类型的 ThreadPoolExexutor
实例了,通过这个工厂模式既实现了创建一个类的不同实例的功能,又保证了系统的安全性。
在知道什么是工厂模式之后,我们就利用这个工厂类来创建出需要的线程池实例,那么 Executors
工厂类又提供了哪些创建线程池对象的方法呢?它们又分别具有什么特性呢?
Executors 工厂类还有很多不同的创建线程池对象的方法,这里我就不给大家一一展示出来了,大家如果感兴趣的话,可以去Java帮助文章上去查看。
通过查看源码,我们可以知道,Executors 工厂类创建的线程池对象都是通过传递不同的参数来实例化 ThreadPool 类的,也就是说 ThreadPool 类具有多种构成重载的构造方法,那么来看看这些不同的构造函数的参数分别代表什么吧。
这里解决策略是面试中容易考的高频考点,那么这里我们就来详细的说说关于线程池的拒绝策略。
当线程池中容纳的任务数量到达了最大限制之后,如果继续往里面添加任务的话,会出现什么情况呢?Java 中提供了4种拒绝策略。
当创建了适当的线程池对象并且了解了其中创建的细节了之后,我们就需要调用该线程对象的相关方法来执行代码。
使用 submit 方法来添任务。
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");
}
});
}
}
同样的虽然 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();
}
}
}
测试
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标准库提供的四种拒绝策略。