线程池学习
涉及到线程池之前,我们需要知道创建线程的几种方式,在线程的基础之上,我们进一步学习线程池。
线程池
线程池是什么?
线程池是一种池化技术,使用池化技术管理和使用线程的一种机制。
池化技术:在利用资源之前,我们需要提前准备一些资源,在需要时候重复使用提前准备的资源。
池化技术(Pooling)是一种常用的优化技术,它的目的是通过重复使用已经创建的对象或者资源,来避免频繁的创建和销毁对象或者资源所带来的开销。通过池化技术,可以减少创建和销毁对象的开销,从而提高程序的性能和稳定性。在计算机领域中,池化技术通常用于以下几个方面:
数据库连接池:通过重复使用已经创建的数据库连接,避免频繁地创建和销毁数据库连接所带来的开销。
线程池:通过重复使用已经创建的线程,避免频繁地创建和销毁线程所带来的开销。
对象池:通过重复使用已经创建的对象,避免频繁地创建和销毁对象所带来的开销。
内存池:通过重复使用已经分配的内存,避免频繁地分配和释放内存所带来的开销。
在实际应用中,池化技术可以帮助我们提高程序的性能和稳定性,因此在设计和开发程序时,可以考虑采用池化技术来优化程序的性能。
线程池的使用方式
- 通过ThreadPoolExecutor创建的线程池;
- 通过Executors创建的线程池。
线程池的创建方式总共包含以下7种(其中6种是通过Executors 创建的,1种是通过ThreadPoolExecutor创建的):
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可以控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadExecutor:创建单个线程的线程池,它可以保证先进先出的执行顺序
- Executors.newSingleThreadScheduledExecutor:创建一个可以执行延迟任务的单个线程的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池;
- ThreadPoolExecutor:手动创建线程池 的方式,它最多包含了7个参数可供设置,最少可设置5个参数。
Executors
创建固定数量的线程池
1 | public class Demo1 { |
1 | public class Demo2 { |
通过上述两种方式,我发现向线程池中添加任务的方式有两种:
- execute : 只执行不带返回值的任务
- submit:可以执行又返回值的任务或者没有返回值的任务。
线程池工厂
在上面的执行过程中,可以看到没有设置线程名称时候,他就会是一个默认名,线程工厂可以让我们自定义线程名称或者优先级。
1 | public class Demo3 { |
线程池开始执行了:我的线程-1826771953 10
线程池开始执行了:我的线程-455659002 10
线程池开始执行了:我的线程-245257410 10
线程池开始执行了:我的线程-1705736037 10
线程池开始执行了:我的线程-1406718218 10
带缓存的线程池
1 | public class Demo4 { |
在此我们发现,我们给线程一千个任务的时候,他并不会创建1000个线程,我们通过执行发现最多会创建100多个线程,然后会进行复用。
故此我们在这里总结一下:
线程池中的线程也不能无限制地被创建。线程池中的线程数量应该根据实际情况进行适当的设置,以便充分利用系统资源,提高系统的性能和稳定性。如果线程池中的线程数量过多,会导致系统资源的耗尽,从而导致系统的性能下降或崩溃。另外,线程的创建和销毁也会带来一定的开销,如果线程池中的线程数量过多,会增加线程的创建和销毁开销,从而降低系统的性能和稳定性。
因此,在实际应用中,需要根据实际情况来设置线程池的大小,以满足应用的需要,并保证系统的性能和稳定性。一般来说,线程池中的线程数量应该根据系统的 CPU 核心数、内存大小、任务类型和系统负载等因素来进行设置,以达到最优的效果。
执行定时任务的线程池
延迟执行一次:
1 | public class Demo5 { |
本延迟执行任务,只能执行一次。
创建这个线程池有3个参数
- 执行任务
- 延迟n秒执行
- 配合执行的时间单位
固定频率执行
我们可以让线程以以固定频率间隔n秒执行,创建这个线程池有四个参数:
- 执行任务
- 延迟n秒执行
- 执行定时任务的频率
- 配合3执行的时间单位
1 | public class Demo6 { |
1 | public class Demo7 { |
在 Java 中,Timer 类提供了两个方法用于定时执行任务,分别是 scheduleAtFixedRate() 和 scheduleWithFixedDelay()。这两个方法都可以实现定时执行任务的功能,但它们的执行方式略有不同,具体区别如下:
scheduleAtFixedRate() 方法:该方法是按照固定的时间间隔执行任务,它会按照指定的时间间隔不断地执行任务,无论上一次任务是否执行完成,也不考虑任务的执行时间,即使任务的执行时间超过了时间间隔,也会按照指定的时间间隔再次执行任务。
scheduleWithFixedDelay() 方法:该方法是在任务执行完成后,等待指定的时间间隔再次执行任务,它会在任务执行完成后,等待指定的时间间隔,然后再次执行任务,考虑了任务的执行时间。也就是说,该方法会在上一次任务执行完成后,等待指定的时间间隔,然后再次执行任务。
因此,如果需要按照固定的时间间隔执行任务,无论任务是否执行完成,都需要按照指定的时间间隔再次执行任务,可以使用 scheduleAtFixedRate() 方法;如果需要在任务执行完成后,等待指定的时间间隔再次执行任务,可以使用 scheduleWithFixedDelay() 方法。
单线程的线程池
1 | public class Demo8 { |
单线程的线程池和单线程有什么区别?以及其各自的作用?
单线程的线程池和单线程之间有以下几点区别:
线程池是一种可以管理和调度多个线程的机制,可以重复利用线程,从而避免频繁创建和销毁线程的开销,提高程序的性能和稳定性。而单线程则只能执行一个任务,无法进行任务的并行处理。
线程池中的线程数量可以根据实际需求进行动态调整,可以增加或减少线程的数量,以适应不同的负载和任务类型。而单线程则只能处理一个任务,无法进行线程数量的动态调整。
- 线程池可以管理和调度多个任务。
定时任务的单线程线程池
1 | public class Demo9 { |
根据当前CPU生成线程池
1 | public class Demo10 { |
ThreadPoolExecutor
ThreadPoolExecutor和Executors比较
都是Java中用于生成线程池的类,它们有以下几个区别:
ThreadPoolExecutor是一个更底层的类,它提供了更多的配置选项,如线程池的核心线程数、最大线程数、任务队列等,可以更加灵活地控制线程池的行为。而Executors则是在ThreadPoolExecutor基础上进行了封装,提供了一些预定义的线程池配置选项,如newFixedThreadPool、newCachedThreadPool等,方便用户快速创建线程池。
ThreadPoolExecutor可以通过自定义RejectedExecutionHandler来处理任务被拒绝执行的情况,而Executors只提供了一些简单的拒绝策略,如抛出异常、丢弃任务等。
Executors生成的线程池通常是无界队列的,如果任务数量过多,可能会导致内存溢出等问题。而ThreadPoolExecutor则可以通过配置有界队列或者拒绝策略来避免这种情况。
在Java 8之前,Executors生成的线程池中的工作线程都是非守护线程,即使主线程结束,工作线程也会继续执行。而ThreadPoolExecutor则可以通过设置工作线程为守护线程来避免这种情况。
ThreadPoolExecutor和Executors各自可以解决的问题也有所差异。ThreadPoolExecutor适合于需要更精细的线程池配置、需要自定义拒绝策略、需要有界队列等情况。而Executors则适合于快速创建线程池,且任务数量不会过多的情况。需要根据具体场景选择合适的类来生成线程池。
Executors存在的问题
虽然Executors可以快速创建线程池,但在一些情况下可能会存在问题,如下:
FixedThreadPool和SingleThreadExecutor的线程数是固定的,如果任务数量过多,会导致队列中的任务堆积,从而占用大量内存,甚至导致OOM异常。
CachedThreadPool的最大线程数是Integer.MAX_VALUE,如果任务数量过多,会创建大量线程,占用大量系统资源,导致系统崩溃。
Executors生成的线程池中的工作线程都是非守护线程,即使主线程结束,工作线程也会继续执行。如果应用程序没有显式地终止线程池,会导致应用程序无法正常退出,从而导致内存泄漏等问题。
Executors提供的拒绝策略有一些局限性,例如在任务被拒绝后,无法重新提交任务等。
因此,对于不同的应用场景,应该选择合适的线程池类型或者直接使用ThreadPoolExecutor来手动创建线程池,以避免出现上述问题。
ThreadPoolExecutor的介绍
ThreadPoolExecutor是Java中用于创建线程池的类,它有以下几个参数:
corePoolSize:线程池的核心线程数,即线程池中始终存在的线程数。如果线程池中的线程数小于corePoolSize,则会创建新的线程来执行任务,直到线程数等于corePoolSize。
maximumPoolSize:线程池的最大线程数,即线程池中最多能存在的线程数。如果任务数量超过了线程池的容量,则会根据拒绝策略处理这些任务。
keepAliveTime:线程池中非核心线程的空闲存活时间。如果线程池中的线程数大于corePoolSize,并且某个线程空闲的时间超过了keepAliveTime,则该线程会被销毁直到线程池中的线程数等于corePoolSize。
TimeUnit:keepAliveTime的时间单位,例如TimeUnit.SECONDS。
workQueue:线程池中的任务队列,用于存储等待执行的任务。可以选择不同类型的队列,如有界队列和无界队列。
threadFactory:线程工厂,用于创建新的线程。可以自定义线程的名称、优先级等属性。
handler:拒绝策略,用于处理无法执行的任务。可以选择不同的策略,如抛出异常、丢弃任务、等待一段时间再重试等。
这些参数的具体作用如下:
- 通过corePoolSize和maximumPoolSize来控制线程池中的工作线程数量,以达到最优的性能和资源利用效率。
- 通过keepAliveTime和workQueue来控制线程池中线程的存活时间和任务的排队策略,以避免任务堆积和资源浪费。
- 通过threadFactory可以自定义线程的属性,如线程名称等。
当我们使用ThreaPoolExecutor创建线程池的时候,默认最少使用5个参数
1 | new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue) |
1 | public class Demo1 { |
线程池的执行流程
线程池的执行流程如下:
首先,线程池会创建一些初始的核心线程,并将它们放入工作队列中。
当有任务进来时,线程池会从工作队列中选取一个线程来执行任务。
如果此时线程池中的线程数小于核心线程数(corePoolSize)且有空闲线程,则会立即创建新的线程来执行任务。
如果此时线程池中的线程数已经达到了核心线程数,且工作队列已满,则会将任务提交到线程池的任务队列中。
如果此时任务队列已满,且线程池中的线程数小于最大线程数(maximumPoolSize),则会创建新的线程来执行任务。
如果此时线程池中的线程数已经达到了最大线程数,且任务队列已满,则会按照拒绝策略(RejectedExecutionHandler)来处理任务。常见的拒绝策略有:抛出异常、丢弃任务、等待一段时间再重试等。
当某个线程执行完任务后,会从任务队列中取出下一个任务继续执行,直到线程池关闭或者出现异常。
需要注意的是,线程池的执行流程是异步的,任务的执行顺序是不确定的。线程池在执行任务时,会根据任务的优先级、执行时间等因素来选择执行顺序,具体的执行流程会根据不同的线程池实现而有所差异。
拒绝策略
目前线程池的拒绝策略共计有5种,其中有四种JDK提供的和一种自定义拒绝策略。
线程池的拒绝策略指的是当线程池中的线程已经全部被占用,队列也已满,无法继续接受新的任务时,应该如何处理这些被拒绝的任务。常见的线程池拒绝策略包括:
- AbortPolicy(默认策略):直接抛出RejectedExecutionException异常,表示拒绝执行该任务。
- CallerRunsPolicy:由调用线程处理该任务,即在提交任务的线程中直接执行该任务。这种策略可能会降低整个系统的吞吐量,因为提交任务的线程可能不是专门用来处理任务的线程,而是业务线程。
- DiscardPolicy:直接丢弃该任务,不做任何处理。
- DiscardOldestPolicy:抛弃最早加入队列的任务,并尝试再次提交当前任务。
- 自定义拒绝策略:用户可以根据业务场景,自定义拒绝策略,例如将任务记录到日志中、将任务放到消息队列中等等。
选择合适的线程池拒绝策略,可以更好地保障系统的稳定性和可靠性。
第一种AbortPolicy
1 | public class Demo2 { |
第二种CallerRunsPolicy()
1 | import java.util.concurrent.*; |
第三种:DiscardPolicy ()
1 | public class Demo4 { |
第四种:DiscardOldestPolicy()
1 | public class Demo5 { |
第五种:自定义拒绝策略
1 | public class Demo6 { |
线程池的状态
线程池的状态通常包括以下几种:
RUNNING:线程池处于运行状态,可以接受新任务并处理已有的任务。
SHUTDOWN:线程池处于关闭状态,不再接受新任务,但会处理已有的任务。
STOP:线程池处于停止状态,不再接受新任务,也不会处理已有的任务,会中断正在执行的任务。
TIDYING:线程池正在整理线程池中的线程,例如删除已经停止的线程。
TERMINATED:线程池已经终止,不再接受任何任务。
线程池的状态可以通过ThreadPoolExecutor类的getState()方法获取,返回一个枚举类型的值,表示当前线程池的状态。在使用线程池的过程中,需要根据当前线程池的状态,避免不必要的操作,保证线程池的稳定性和可靠性
RUNNING状态的例子:
1
2
3
4
5
6
7
8
9ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
executor.execute(new Runnable() {
public void run() {
System.out.println("Task executed.");
}
});
System.out.println("Current thread pool state: " + executor.getState());
// 输出:Current thread pool state: RUNNING这个例子中,线程池处于RUNNING状态,因为线程池已经创建成功,可以接受新任务并处理已有的任务。
SHUTDOWN状态的例子:
1 | ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); |
这个例子中,线程池处于SHUTDOWN状态,因为调用了executor.shutdown()方法,线程池不再接受新任务,但会处理已有的任务。
STOP状态的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
executor.execute(new Runnable() {
public void run() {
while (true) {
System.out.println("Task executed.");
}
}
});
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Current thread pool state: " + executor.getState());
// 输出:Current thread pool state: STOP这个例子中,线程池处于STOP状态,因为调用了executor.shutdown()方法后,线程池不再接受新任务,并且中断正在执行的任务。
TIDYING状态的例子:
1
2
3
4
5
6
7
8ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
executor.shutdown();
while (!executor.isTerminated()) {
System.out.println("Current thread pool state: " + executor.getState());
}
// 输出:
// Current thread pool state: TIDYING
// Current thread pool state: TERMINATED这个例子中,线程池处于TIDYING状态,因为调用了executor.shutdown()方法后,线程池正在整理线程池中的线程,例如删除已经停止的线程。
TERMINATED状态的例子:
1 | ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); |
这个例子中,线程池处于TERMINATED状态,因为线程池已经终止,不再接受任何任务。在调用executor.shutdown()方法后,等待所有任务执行完毕并且所有线程都被回收后,线程池进入TERMINATED状态。
关闭线程池的方法
ThreadPoolExecutor类提供了两种关闭线程池的方法,分别是shutdown()和shutdownNow()。这两种方法的区别如下:
shutdown()方法:调用该方法后,线程池会拒绝新的任务提交,但会继续处理已经提交的任务,直到所有任务都被完成。在所有任务完成后,线程池才会真正关闭,并释放所有资源。如果在调用shutdown()方法之后,继续提交新的任务,这些任务会被拒绝并抛出RejectedExecutionException异常。
shutdownNow()方法:调用该方法后,线程池会拒绝新的任务提交,并尝试中断正在执行的任务。在中断任务的过程中,如果任务响应中断,则任务会被成功中断并从任务队列中移除。如果任务无法响应中断,则任务会继续执行。在所有任务都被中断或已经完成后,线程池会真正关闭,并释放所有资源。如果在调用shutdownNow()方法之后,继续提交新的任务,这些任务会被拒绝并抛出RejectedExecutionException异常。
因此,shutdown()方法是优雅地关闭线程池,等待所有任务都被完成后再关闭线程池,而shutdownNow()方法是强制关闭线程池,立即停止所有任务的执行,并尝试中断正在执行的任务。选择哪个方法取决于业务需求,如果需要优雅地关闭线程池,可以使用shutdown()方法,如果需要立即停止所有任务的执行,可以使用shutdownNow()方法。