tip: 作为程序员一定学习编程之道,一定要对代码的编写有追求,不能实现就完事了。我们应该让自己写的代码更加优雅,即使这会费时费力。
推荐:体系化学习Java(Java面试专题)
文章目录
- 线程池的目的
- 线程池的参数
- 几种封装的线程池
线程池的目的
线程池是一种用于管理和复用线程的机制。在多线程应用程序中,线程的创建和销毁需要消耗大量的系统资源,而线程池可以通过预先创建一定数量的线程,然后将任务分配给这些线程来避免频繁地创建和销毁线程,从而提高应用程序的性能和效率。线程池还可以控制并发线程的数量,避免过多的线程竞争资源导致的性能下降和系统崩溃。线程池是多线程编程中常用的一种技术,被广泛应用于各种类型的应用程序中。
线程池的参数
线程池的参数包括以下几个:
-
核心线程数(corePoolSize):线程池中最少保持的线程数。当有新任务提交时,如果当前线程池中的线程数小于核心线程数,则会创建新的线程来处理任务,即使有空闲的线程也会创建新的线程。
-
最大线程数(maximumPoolSize):线程池中最多能创建的线程数。当任务数量超过核心线程数时,线程池会创建新的线程来处理任务,但是最多只能创建最大线程数个线程。
-
空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,空闲线程的存活时间。如果线程在这段时间内没有处理任务,则会被销毁,直到线程池中的线程数量不超过核心线程数。
-
阻塞队列(workQueue):用于存放等待执行的任务的队列。当线程池中的线程数量已经达到核心线程数时,新的任务会被放入阻塞队列中等待执行。
-
线程工厂(threadFactory):用于创建新的线程。
-
时间单位(TimeUnit): 针对救急线程,unit 时间单位
-
拒绝策略(rejectedExecutionHandler):当线程池中的线程数量已经达到最大线程数,且阻塞队列已经满了时,新的任务将无法被处理。拒绝策略用于处理这种情况,可以选择抛出异常、直接丢弃任务、丢弃队列中最早的任务或者在调用线程中执行任务等方式。
这些参数可以根据具体应用程序的需求进行调整,以达到最佳的性能和效率。
package com.pany.camp.thread.pool;
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个线程池
ExecutorService executor = new ThreadPoolExecutor(
1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 提交10个任务给线程池
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.execute(new Runnable() {
public void run() {
System.out.println("Task " + taskId + " is running in thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed");
}
});
}
// 关闭线程池
executor.shutdown();
}
}
几种封装的线程池
以下是几种常见的封装的线程池:
- FixedThreadPool:固定大小线程池,线程数量固定,不会自动调整线程数量。适用于负载比较稳定的情况。
package com.pany.camp.thread.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池,大小为3
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交6个任务给线程池
for (int i = 1; i <= 6; i++) {
final int taskId = i;
executor.execute(new Runnable() {
public void run() {
System.out.println("Task " + taskId + " is running in thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed");
}
});
}
// 关闭线程池
executor.shutdown();
}
}
我们创建了一个固定大小为3的线程池,然后提交了6个任务给线程池。每个任务都会打印出自己的编号和当前运行的线程名,并且在执行完任务后打印出完成信息。由于线程池大小为3,因此只有3个任务会同时运行,其他任务需要等待空闲线程。最后,我们调用了 executor.shutdown() 方法来关闭线程池。
- CachedThreadPool:可缓存线程池,线程数量不固定,会根据任务数量自动调整线程数量。适用于负载比较不稳定的情况。
package com.pany.camp.thread.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
// 创建一个可缓存的线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 提交10个任务给线程池
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.execute(new Runnable() {
public void run() {
System.out.println("Task " + taskId + " is running in thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed");
}
});
}
// 关闭线程池
executor.shutdown();
}
}
我们创建了一个可缓存的线程池,然后提交了10个任务给线程池。每个任务都会打印出自己的编号和当前运行的线程名,并且在执行完任务后打印出完成信息。由于线程池是可缓存的,因此会根据任务数量自动调整线程数量,如果有空闲线程可以使用,则会直接使用空闲线程,否则会创建新的线程。最后,我们调用了 executor.shutdown() 方法来关闭线程池。
- SingleThreadExecutor:单线程线程池,只有一个线程在工作,保证任务按照指定顺序执行。
package com.pany.camp.thread.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
// 创建一个单线程的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交5个任务给线程池
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.execute(new Runnable() {
public void run() {
System.out.println("Task " + taskId + " is running in thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed");
}
});
}
// 关闭线程池
executor.shutdown();
}
}
我们创建了一个单线程的线程池,然后提交了5个任务给线程池。每个任务都会打印出自己的编号和当前运行的线程名,并且在执行完任务后打印出完成信息。由于线程池只有一个线程,因此所有任务都会按照顺序依次执行。最后,我们调用了 executor.shutdown() 方法来关闭线程池。
- ScheduledThreadPool:定时任务线程池,适用于需要定时执行任务的场景。
package com.pany.camp.thread.pool;
import cn.hutool.core.thread.ThreadUtil;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
// 创建一个大小为3的定时线程池
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
// 延迟1秒后执行任务
executor.schedule(() -> System.out.println("Task 1 is running in thread " + Thread.currentThread().getName()), 1, TimeUnit.SECONDS);
// 延迟2秒后执行任务,之后每3秒执行一次
executor.scheduleAtFixedRate(() -> System.out.println("Task 2 is running in thread " + Thread.currentThread().getName()), 2, 3, TimeUnit.SECONDS);
// 延迟3秒后执行任务,之后每5秒执行一次
executor.scheduleWithFixedDelay(() -> System.out.println("Task 3 is running in thread " + Thread.currentThread().getName()), 3, 5, TimeUnit.SECONDS);
// 关闭线程池
ThreadUtil.sleep(10000);
executor.shutdown();
}
}
我们创建了一个大小为3的定时线程池,然后提交了3个任务给线程池。第一个任务会延迟1秒后执行,第二个任务会延迟2秒后执行,并且之后每3秒执行一次,第三个任务会延迟3秒后执行,并且之后每5秒执行一次。每个任务都会打印出自己的编号和当前运行的线程名。最后,我们调用了 executor.shutdown() 方法来关闭线程池。
- WorkStealingPool:工作窃取线程池,线程数量固定,但是线程之间可以窃取任务执行,提高了任务执行的效率。
package com.pany.camp.thread.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class WorkStealingPoolExample {
public static void main(String[] args) throws InterruptedException {
// 获取当前系统可用的处理器数量
int processors = Runtime.getRuntime().availableProcessors();
// 创建一个工作窃取线程池
ExecutorService executor = Executors.newWorkStealingPool(processors);
// 提交10个任务给线程池
IntStream.range(1, 11).forEach(i -> executor.submit(() -> {
System.out.println("Task " + i + " is running in thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + i + " is completed");
}));
// 等待所有任务完成
executor.awaitTermination(5, TimeUnit.SECONDS);
// 关闭线程池
executor.shutdown();
}
}
,我们使用 Runtime.getRuntime().availableProcessors() 获取当前系统可用的处理器数量,然后创建了一个工作窃取线程池。我们提交了10个任务给线程池,每个任务都会打印出自己的编号和当前运行的线程名,并且在执行完任务后打印出完成信息。由于工作窃取线程池的特性,线程会自动从其他线程窃取任务来执行,因此我们无法预测每个任务会在哪个线程中执行。最后,我们调用了 executor.awaitTermination(5, TimeUnit.SECONDS) 等待所有任务完成,然后调用了 executor.shutdown() 方法来关闭线程池。
这些线程池都是Java中自带的线程池,可以直接使用。不同的线程池适用于不同的场景,根据实际需要选择合适的线程池可以提高应用程序的性能和效率。
封装的线程池的缺点主要有以下几点:
-
无法满足特定需求:封装的线程池通常是为了方便使用而设计的,因此可能无法满足某些特定需求。例如,如果需要使用某种特定的线程调度算法,就需要自己实现线程池。
-
难以调试:封装的线程池通常会隐藏一些底层细节,这使得在出现问题时难以进行调试。例如,如果线程池中的某个任务出现了异常,我们可能无法知道是哪个任务出了问题。
-
可能存在性能问题:封装的线程池通常会提供一些默认配置,这可能无法满足某些高性能的需求。例如,如果需要使用更高效的队列实现,就需要自己实现线程池。
-
无界队列可能会引起内存溢出。
因此,在某些情况下,我们可能需要自己实现线程池来满足特定需求。