➡️博客首页 https://blog.csdn.net/Java_Yangxiaoyuan
欢迎优秀的你👍点赞、🗂️收藏、加❤️关注哦。
本文章CSDN首发,欢迎转载,要注明出处哦!
先感谢优秀的你能认真的看完本文,有问题欢迎评论区交流,都会认真回复!
如何让Java的线程池顺序执行任务
- 一、✅典型解析
- 1.1 ✅使用单线程线程池
- 1.2 ✅使用有依赖关系的任务调度方式
- 二、✅拓展知识仓
- 2.1 ✅什么是SingleThreadExecutor
- 2.2 ✅SingleThreadExecutor时的注意事项
- 2.3 ✅如何设置任务的优先级
- 三、✅有哪些其他线程池可以用来处理多个任务
- 3.1 ✅CachedThreadPool
- 3.2 ✅FixedThreadPool
- 3.3 ✅ScheduledThreadPool
- 3.4 ✅SingleThreadExecutor
- 3.5 ✅SingleThreadScheduledExecutor
- 3.6 ✅ForkJoinPool
- 四、✅线程池的优缺点
- 4.1 ✅线程池可以提高多核CPU的利用率吗
- 4.2 ✅如何配置和管理线程池
- 五、✅如何避免线程池中阻塞
- 5.1 ✅如何解决线程池中的线程泄漏
- 5.2 ✅如何防止任务丢失
- 5.3 ✅持久化存储怎么实现
一、✅典型解析
Java中的线程池本身并不提供内置的方式来保证任务的顺序执行的,因为线程池的设计目的是为了提高并发性能和效率,如果顺序执行的话,那就和单线程没区别了。
但是如果被问到想要实现这个功能该怎么做,有以下两种方式。
1.1 ✅使用单线程线程池
我们可以使用 SingleThreadExecutor
这种线程池来执行任务,因为这个线程池中只有一个线程,所以他可以保证任务可以按照提交任务被顺序执行。
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
// 任务按照提交的顺序逐个执行
executor.shutdown() ;
1.2 ✅使用有依赖关系的任务调度方式
可以使用 ScheduledThreadPoolExecutor
结合 ScheduledFuture
来实现任务的顺序执行。将任务按照顺序提交给线程池,每个任务的执行时间通过 ScheduledFuture
的 get()
方法等待前一个任务完成。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
ScheduledFuture<?> future1 = executor.schedule(task1, 0,TimeUnit.MILLISECONDS);
ScheduledFuture<?> future2 = executor.schedule(task2, future1.get(), TimeUnit.MILLISECONDS);
ScheduledFuture<?> future3 = executor.schedule(task3, future2.get(), TimeUnit,MILLISECONDS);
// 任务会按照依赖关系和前一个任务的执行时间逐个执行
executor .shutdown();
二、✅拓展知识仓
2.1 ✅什么是SingleThreadExecutor
SingleThreadExecutor的定义与特点:
- 定义:SingleThreadExecutor是Java线程池的一种,用于管理单一线程。这个线程池的工作机制是:所有提交的任务都会在这个单一线程上按顺序执行。
-
特点:
- 顺序执行:由于只有一个工作线程,任务的执行顺序与提交顺序一致。
- 阻塞队列:当工作线程正在执行任务,新的任务会被放入一个阻塞队列中等待。
- 中断处理:如果任务可以被中断,当任务被中断时,工作线程会尝试在下次执行任务时响应中断。但如果工作线程在执行任务时阻塞(如等待IO),它可能不会及时响应中断。
-
适用场景:适用于需要保证任务顺序执行的场景,如任务的串行处理等。
2.2 ✅SingleThreadExecutor时的注意事项
使用SingleThreadExecutor时的注意事项:
-
任务数量限制:由于只有单一线程,当提交的任务数量超过线程池的最大容量时,新提交的任务会被拒绝。
-
中断处理:考虑任务的响应中断能力,特别是当工作线程可能阻塞时(如等待IO)。
-
关闭与资源释放:使用完毕后,应调用shutdown或shutdownNow方法来正确关闭线程池并释放资源。
-
异常处理:确保任务实现了
Runnable
接口的run
方法,并适当处理可能抛出的异常。
-
线程池大小:由于是单线程,当该线程由于长时间运行或异常而无法处理新任务时,会导致其他任务等待。因此,需要确保任务是短时间可完成的,或考虑其他线程池类型。
2.3 ✅如何设置任务的优先级
任务的优先级可以通过线程的优先级来设置。在Java中,线程优先级是一个重要的概念,它决定了线程的执行顺序。Java提供了三种优先级:MIN_PRIORITY(1)、NORM_PRIORITY(5)和MAX_PRIORITY(10)。通过调用线程对象的setPriority()方法,可以设置线程的优先级。
注意:线程优先级是一个有争议的话题,因为不同的操作系统和JVM实现可能会有不同的行为。尽管Java提供了一种方式来设置线程优先级,但并不建议在多线程编程中使用它来控制线程的执行顺序,因为这可能导致难以预测和调试的行为。
因此,建议在设置任务的优先级时,应该优先考虑使用其他方法,例如使用任务队列和调度器来管理任务的执行顺序。这些方法可以更好地控制任务的执行顺序和资源分配,并且更加可靠和可预测。
在Java中,我们无法直接为线程设置优先级,因为这并不是线程安全的操作。Java中的线程优先级是通过Thread类的静态变量实现的,但实际上并没有实现优先级调度。线程调度是由操作系统控制的,而Java并不提供访问和修改操作系统的线程调度机制的API。
但是,我们可以使用Java的并发工具类库中的PriorityBlockingQueue来实现任务优先级的功能。PriorityBlockingQueue是一个支持优先级的阻塞队列,它可以按照元素的优先级对元素进行排序。
import java.util.concurrent.*;
/**
* @author xinbaobaba
* 使用了Java的并发工具类库中的ExecutorService、Future
* 和PriorityBlockingQueue来管理任务的执行和优先级
*/
public class TaskManager {
private ExecutorService executor;
private PriorityBlockingQueue<Task> taskQueue;
public TaskManager(int numThreads) {
executor = Executors.newFixedThreadPool(numThreads);
taskQueue = new PriorityBlockingQueue<>();
}
public void addTask(Runnable task, int priority) {
taskQueue.add(new Task(task, priority)); // 将任务添加到优先级队列中
}
public void executeTasks() {
while (!taskQueue.isEmpty()) {
try {
Task task = taskQueue.take(); // 阻塞直到队列非空
Future<?> future = executor.submit(task); // 提交任务到线程池执行
future.get(); // 等待任务完成并获取结果(可选)
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
private static class Task implements Comparable<Task> {
private Runnable task;
private int priority;
private Future<?> future; // 用于跟踪任务的执行状态(可选)
public Task(Runnable task, int priority) {
this.task = task;
this.priority = priority;
}
public void run() {
task.run(); // 执行任务逻辑
}
@Override
public int compareTo(Task other) {
return Integer.compare(this.priority, other.priority); // 按照优先级排序
}
}
}
在上面的Demo中,我们创建了一个
TaskManager
类,它使用ExecutorService
和PriorityBlockingQueue
来管理任务的执行和优先级。我们通过addTask()
方法向TaskManager中添加任务,每个任务都有一个优先级。在TaskManager
内部,我们创建了一个Task类,它实现了Comparable接口
,按照任务的优先级进行排序。然后,我们使用ExecutorService
的submit()
方法将Task提交给线程池执行。线程池会按照Task的优先级顺序依次执行任务。在executeTasks()
方法中,我们使用while循环从队列中取出任务并执行,直到队列为空。任务的取出顺序按照它们的优先级进行排序,因此优先级高的任务会被优先执行。在任务的执行过程中,我们可以通过Future
对象来获取任务的执行状态和结果(可选)。
三、✅有哪些其他线程池可以用来处理多个任务
除了Java的Executor框架提供的线程池外,还有其他一些线程池可以用来处理多个任务。以下是一些常见的线程池:
-
FixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。适用于需要限制线程数的场景,如服务器端并发请求。
-
CachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。适用于执行很多短期异步任务的程序。
-
ScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。适用于需要延迟执行或定期执行任务的场景。
-
SingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。适用于需要顺序执行任务的场景,如保证线程安全、避免竞态条件等。
-
SingleThreadScheduledExecutor:创建一个单一定时线程池,只有一个线程的定时线程池。适用于需要延迟执行或定期执行任务,且只需一个线程的场景。
-
ForkJoinPool:适用于执行大量并行的、任务可以拆分为子任务的场景,通常用于计算密集型任务。可以自动拆分任务并提交到线程池中,当每个子任务完成后,自动将结果合并。
这些线程池各有特点,可以根据具体需求选择合适的线程池来处理多个任务。
3.1 ✅CachedThreadPool
CachedThreadPool是Java中的一个线程池类,它是Executors类工厂中的一种实现。CachedThreadPool会根据需要创建新线程,并且线程池的大小无限制。当线程池中的线程数量超过当前任务所需时,CachedThreadPool会回收空闲的线程。这种线程池适用于执行大量的短期异步任务,例如Web请求的处理。
使用CachedThreadPool时,需要注意以下几点:
- 线程池大小无限制,因此可能会占用大量系统资源。
- 空闲线程会被回收,但长时间不使用的线程可能会一直存在。
- 可以设置线程的工厂和守护线程等属性,以更好地控制线程的行为。
CachedThreadPool适用于执行大量短期异步任务的场景,但需要注意资源占用和线程管理的问题。
看一个demo:
import java.util.concurrent.*;
/**
* CachedThreadPool
*/
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
// 提交任务到线程池执行
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("Task 1 is running.");
}
});
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("Task 2 is running.");
}
});
// 关闭线程池
executor.shutdown();
}
}
首先创建了一个CachedThreadPool对象,然后通过executor.execute()方法提交了两个任务到线程池中执行。由于使用了CachedThreadPool,线程池的大小是无限的,因此会根据需要创建新线程来执行任务。在执行完任务后,我们通过executor.shutdown()方法关闭了线程池。
3.2 ✅FixedThreadPool
FixedThreadPool 是 Java 并发库 java.util.concurrent
中的一个类,它是线程池的一种实现。它维护了一个固定大小的线程池,用于执行提交给它的任务。
FixedThreadPool 的主要特点如下:
- 线程池大小固定:创建
FixedThreadPool
时,你需要指定线程池的大小,这个大小是固定的,不会因为任务的增加而改变。
- 队列缓冲:当提交的任务超过线程池的大小时,这些任务会被放在一个队列中等待执行。
- 线程复用:线程池中的线程是复用的,一个线程可以执行多个任务。
下面是一个使用 FixedThreadPool
的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池,大小为5
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务到线程池执行
for (int i = 0; i < 10; i++) {
executor.execute(new Task()); // Task是一个实现了Runnable接口的类
}
// 关闭线程池
executor.shutdown();
}
}
class Task implements Runnable {
@Override
public void run() {
System.out.println("Task " + Thread.currentThread().getId() + " is running.");
}
}
在这个示例中:
- 我们首先使用
Executors.newFixedThreadPool(5)
创建了一个大小为5的FixedThreadPool
。这意味着线程池中最多有5个线程同时执行任务。- 我们提交了10个任务到线程池中,但因为线程池的大小只有5,所以只有5个任务会立即开始执行,其余的任务会在队列中等待。
- 当我们调用
executor.shutdown()
时,会等待所有已提交的任务执行完毕后关闭线程池。
3.3 ✅ScheduledThreadPool
ScheduledThreadPool 是 Java 并发库中的一个类,它是线程池的一种实现,主要用于定时或周期性地执行任务。
ScheduledThreadPool 的主要特点如下:
- 定时或周期性任务执行:通过
ScheduledThreadPool
,你可以安排一个任务在给定的延迟后开始执行,或者定期执行。- 线程池大小可调:与
FixedThreadPool
和CachedThreadPool
不同,ScheduledThreadPool
的线程池大小不是固定的,但可以通过参数进行配置。- 优先级调度:与
ScheduledThreadPool
配合使用的任务可以有不同的优先级。
下面是一个使用 ScheduledThreadPool
的示例:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
// 创建一个定时线程池,大小为5
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
// 安排一个任务在5秒后开始执行,然后每隔2秒执行一次
executor.scheduleAtFixedRate(new Task(), 5, 2, TimeUnit.SECONDS);
}
}
class Task implements Runnable {
@Override
public void run() {
System.out.println("Task is running at " + System.currentTimeMillis());
}
}
在这个示例中:
- 我们使用
Executors.newScheduledThreadPool(5)
创建了一个大小为5的ScheduledThreadPool
。这意味着线程池中最多有5个线程同时执行任务。- 我们使用
executor.scheduleAtFixedRate()
方法安排了一个任务。这个任务会在5秒后开始执行,然后每隔2秒执行一次。Task
类实现了Runnable
接口,并重写了run()
方法来定义任务的行为。在这个例子中,任务只是简单地打印当前时间。
3.4 ✅SingleThreadExecutor
SingleThreadExecutor 是 Java 并发库中的一个类,它是 ExecutorService
接口的一个实现。它使用一个单线程来执行提交的任务,按照任务提交的顺序一个接一个地执行。
SingleThreadExecutor 的主要特点如下:
- 单线程执行:使用
SingleThreadExecutor
,所有提交的任务都会由一个单一的工作线程来执行。 - 顺序执行:由于只有一个工作线程,任务会按照提交的顺序一个接一个地执行。
- 无并发问题:由于是单线程执行,因此不会出现并发问题,如线程安全问题或竞态条件。
下面是一个使用 SingleThreadExecutor
的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
// 创建一个单线程化的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交任务到线程池执行
executor.execute(new Task());
executor.execute(new Task());
// 关闭线程池
executor.shutdown();
}
}
class Task implements Runnable {
@Override
public void run() {
System.out.println("Task " + Thread.currentThread().getId() + " is running.");
}
}
在这个示例中:
- 我们使用
Executors.newSingleThreadExecutor()
创建了一个SingleThreadExecutor
。这意味着所有提交的任务都会由一个单一的工作线程来执行。- 我们提交了两个任务到线程池中,由于是单线程执行,这两个任务会按照提交的顺序一个接一个地执行。
- 在所有任务执行完毕后,我们调用
executor.shutdown()
来关闭线程池。
3.5 ✅SingleThreadScheduledExecutor
SingleThreadScheduledExecutor 是 Java 并发库中的一个类,它是 ScheduledExecutorService
接口的一个实现。它使用一个单线程来执行定时或周期性任务。
SingleThreadScheduledExecutor 的主要特点如下:
- 单线程执行:与
SingleThreadExecutor
类似,SingleThreadScheduledExecutor
也使用一个单一的工作线程来执行任务。
- 定时和周期性任务执行:与
ScheduledThreadPool
不同,SingleThreadScheduledExecutor
主要用于执行定时或周期性任务,而不是处理大量一次性任务。
- 优先级调度:与
ScheduledThreadPool
配合使用的任务可以有不同的优先级。
下面是一个使用 SingleThreadScheduledExecutor
的示例:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class SingleThreadScheduledExecutorExample {
public static void main(String[] args) {
// 创建一个单线程化的定时线程池
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
// 安排一个任务在5秒后开始执行,然后每隔2秒执行一次
executor.scheduleAtFixedRate(new Task(), 5, 2, TimeUnit.SECONDS);
}
}
class Task implements Runnable {
@Override
public void run() {
System.out.println("Task is running at " + System.currentTimeMillis());
}
}
在这个示例中:
- 我们使用
Executors.newSingleThreadScheduledExecutor()
创建了一个SingleThreadScheduledExecutor
。这意味着所有安排的任务都会由一个单一的工作线程来执行。- 我们使用
executor.scheduleAtFixedRate()
方法安排了一个任务。这个任务会在5秒后开始执行,然后每隔2秒执行一次。
3.6 ✅ForkJoinPool
ForkJoinPool 是 Java 并发库中的一个类,它是 ExecutorService
接口的一个实现,主要用于支持 Fork/Join 框架。Fork/Join 框架是一种基于工作窃取(work-stealing)的并行计算模式,特别适用于处理那些可以分解为多个子任务的问题。
ForkJoinPool 的主要特点如下:
- 工作窃取:当一个任务被分解为多个子任务时,ForkJoinPool 会将这些子任务放入内部队列中。如果一个工作线程完成了它的任务并且内部队列中还有任务,那么这个工作线程可以从队列中“窃取”并执行其他任务。
- 任务分解与合并:ForkJoinTask 是一个特殊的任务类,它允许任务被分解为多个子任务。当一个子任务完成时,它会将结果合并到父任务中。
- 工作窃取与任务合并的平衡:ForkJoinPool 内部有一个工作队列和一个任务队列。当一个工作线程从工作队列中窃取任务时,它会将完成的任务放回任务队列中供其他线程使用。
- 自动调优:ForkJoinPool 会根据系统的负载自动调整线程的数量。当系统负载较轻时,线程数会减少;当系统负载较重时,线程数会增加。
下面是一个使用 ForkJoinPool
的示例:
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class ForkJoinPoolExample {
public static void main(String[] args) {
// 创建一个 ForkJoinPool,使用默认的线程数
ForkJoinPool pool = new ForkJoinPool();
// 提交一个 ForkJoinTask 到 ForkJoinPool 中执行
pool.invoke(new Task());
}
}
class Task extends RecursiveAction {
private static final int THRESHOLD = 1000;
private int[] array;
private int start, end;
public Task() {}
@Override
protected void compute() {
if (end - start < THRESHOLD) {
for (int i = start; i < end; i++) {
array[i] *= 2;
}
} else {
int mid = (start + end) / 2;
Task left = new Task();
left.array = array;
left.start = start;
left.end = mid;
Task right = new Task();
right.array = array;
right.start = mid;
right.end = end;
invokeAll(left, right); // 并行执行两个子任务
}
}
}
在这个示例中:
- 我们使用
new ForkJoinPool()
创建了一个ForkJoinPool
实例。这个实例将使用默认的线程数来执行任务。- 我们定义了一个
Task
类,它继承自RecursiveAction
。RecursiveAction
是 ForkJoinTask 的一个实现,用于表示可以并行执行的任务。在这个例子中,我们使用递归的方式来分解任务。如果任务的规模小于一个阈值(这里是1000),则直接在单个线程上执行;否则,将任务分解为两个子任务,并并行执行它们。最后,通过调用invokeAll()
方法来执行两个子任务。当子任务完成后,结果会合并到父任务中。
四、✅线程池的优缺点
线程池是一种用于管理线程的机制,它可以在应用程序启动时预先创建一组线程,并将这些线程放入线程池中。当应用程序需要执行某些任务时,它可以从线程池中获取一个线程,用于执行任务。使用线程池的好处主要包括:
- 资源复用:线程池中的线程可以重复使用,避免了频繁地创建和销毁线程,降低了系统开销。
- 性能提升:通过合理的线程管理和调度,线程池可以避免线程的无谓浪费,提高系统的性能和响应速度。
- 系统稳定性:线程池可以控制并发线程的数量,避免过多的线程导致系统资源耗尽或竞争过度,从而提高系统的稳定性和可靠性。
线程池缺点:
- 无法充分利用多核资源:如果线程数量过多,可能会占用过多系统资源,反而降低性能。在多核CPU环境下,过多的线程无法充分利用CPU资源。
- 无法适应动态负载变化:线程池中的线程数量是固定的,因此在面对任务负载的动态变化时,线程池可能无法做到自适应调整。
- 维护成本较高:线程池的参数配置、线程管理、任务调度等都需要进行细致的规划和设计,相对于普通的多线程编程,线程池的维护成本更高。
使用线程池可以提高应用程序的性能和稳定性,但同时也需要注意合理配置和管理线程池,避免出现资源浪费或性能下降的问题。
4.1 ✅线程池可以提高多核CPU的利用率吗
线程池可以提高多核CPU的利用率:
在多核CPU环境下,线程可以并行执行,通过合理的线程管理和调度,线程池可以充分利用多核资源,提高系统的并发处理能力和效率。当一个线程完成任务后,线程池会自动将其分配给下一个任务,避免了线程的创建和销毁开销,提高了系统的性能和响应速度。因此,使用线程池可以更好地利用多核资源,提高多核CPU的利用率。但是需要注意的是,如果线程数量过多,可能会占用过多系统资源,反而降低性能,因此在使用线程池时需要进行合理的配置和管理。
下面是一个模拟实际业务场景的Demo:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class RealWorldThreadPoolExample {
public static void main(String[] args) {
// 创建一个可重用固定线程数的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
Runnable worker = new RealWorldWorkerThread("" + i);
executor.execute(worker); // 使用execute方法将任务提交给线程池
}
// 关闭线程池,不再接受新的任务
executor.shutdown();
// 等待所有任务完成
while (!executor.isTerminated()) {
// 休眠一段时间后再次检查任务是否完成
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("所有任务已完成");
}
}
class RealWorldWorkerThread implements Runnable {
private String command;
public RealWorldWorkerThread(String command) {
this.command = command;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始处理:" + command);
processCommand();
System.out.println(Thread.currentThread().getName() + "结束处理:" + command);
}
private void processCommand() {
try {
// 模拟耗时操作,例如网络请求、文件读写等
TimeUnit.SECONDS.sleep(3); // 休眠3秒钟来模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
代码解释:
- 导入必要的类:导入了
java.util.concurrent
包中的ExecutorService
和Executors
类,以及java.util.concurrent.TimeUnit
类。这些类用于创建线程池、提交任务和管理任务执行。还导入了java.lang.InterruptedException
类,用于处理线程中断异常。 - 创建线程池:使用
Executors.newFixedThreadPool(5)
方法创建一个固定大小为5的线程池。这意味着线程池中最多可以同时执行5个任务。使用fixedThreadPool
而不是cachedThreadPool
是为了限制线程池的大小,避免过多线程被创建和销毁。 - 提交任务:通过循环创建了10个
RealWorldWorkerThread
对象,每个对象代表一个任务。然后使用executor.execute(worker)
方法将任务提交给线程池执行。这里使用execute
方法直接提交任务到线程池,而不是等待任务完成后再返回结果。这样可以提高程序的并发性能。 - 关闭线程池:一旦所有任务都提交给了线程池,调用
executor.shutdown()
方法关闭线程池。这意味着不再接受新的任务,但已经提交的任务会继续执行。使用shutdown
方法而不是shutdownNow
方法是为了确保已经提交的任务能够完成执行。 - 等待任务完成:通过循环和检查
executor.isTerminated()
方法的返回值,等待所有任务完成。在循环中,我们使用TimeUnit.SECONDS.sleep(1)
方法让当前线程休眠1秒钟,然后再次检查任务是否完成。这样可以避免阻塞主线程,并在等待任务完成的同时进行其他操作。当所有任务都完成后,循环会退出。 - 任务执行:在
RealWorldWorkerThread
类中,实现了Runnable
接口并覆盖了run
方法。在run
方法中,首先打印出当前线程的名称和处理中的命令,然后调用processCommand()
方法模拟耗时操作(如网络请求、文件读写等),最后再打印出任务完成的消息。在示例中,我们简单地让线程休眠3秒钟来模拟耗时操作。需要注意的是,实际业务场景中的耗时操作可能会抛出异常,因此需要进行适当的异常处理。
4.2 ✅如何配置和管理线程池
线程池的配置和管理是一个关键的步骤,特别是在处理大量并发任务时。下面是一些关于如何配置和管理线程池的基本步骤和最佳实践:
- 选择合适的线程池类型:Java提供了几种线程池类型,例如
FixedThreadPool
,CachedThreadPool
,SingleThreadExecutor
等。根据你的应用需求选择合适的线程池类型。例如,如果你的任务数量是固定的,那么FixedThreadPool
可能是一个好选择。如果你不确定任务的数量,并且希望线程池根据需要自动调整大小,那么CachedThreadPool
或ScheduledThreadPool
可能更适合你。 - 设置合适的线程池大小:线程池的大小应根据你的硬件、系统资源和任务特性来设置。太大的线程池可能会导致系统资源过度消耗,而太小的线程池可能会导致任务处理速度慢或者系统吞吐量低。通常,线程池的大小应设置为CPU核心数的1-2倍。
- 使用适当的任务队列:线程池通常与任务队列一起使用,任务队列用于存储待处理的任务。你需要选择适当的任务队列大小,以确保系统可以处理的任务数量。
- 合理处理异常:当任务抛出未捕获的异常时,线程池会终止该线程。为了避免这种情况,你应该在任务中适当地处理异常,或者使用try-catch块来捕获并处理异常。
- 关闭线程池:当你不再需要使用线程池时,应关闭它以释放系统资源。你可以调用
shutdown()
或shutdownNow()
方法来关闭线程池。shutdown()
方法会等待所有任务完成后再关闭线程池,而shutdownNow()
方法会尝试立即关闭线程池,停止所有正在执行的任务。 - 监控和调优:使用适当的工具监控线程池的性能,并根据监控结果进行调优。例如,你可以监控任务的平均处理时间、队列大小、线程池大小等参数,并根据这些参数进行调整。
看Demo:
import java.util.concurrent.*;
/**
* @author xinbaobaba
* 如何配置和管理线程池
*/
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 提交任务到线程池
for (int i = 0; i < 50; i++) {
Runnable worker = new Worker("" + i);
executor.execute(worker);
}
// 关闭线程池
executor.shutdown();
// 等待所有任务完成
while (!executor.isTerminated()) {
}
System.out.println("所有任务已完成");
}
}
class Worker implements Runnable {
private String command;
public Worker(String command) {
this.command = command;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始处理:" + command);
processCommand();
System.out.println(Thread.currentThread().getName() + "结束处理:" + command);
}
private void processCommand() {
try {
Thread.sleep(2000); // 模拟耗时操作,例如网络请求、文件读写等
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
五、✅如何避免线程池中阻塞
线程池中的阻塞是一个常见问题,可以通过以下几种方式来避免或减轻:
- 合理设置线程池大小:线程池的大小应根据你的任务数量和系统资源来设置。如果线程池太小,可能无法处理所有的任务,导致任务等待时间过长;如果线程池太大,则可能消耗过多的系统资源,降低系统性能。
- 合理设置任务队列大小:线程池通常与任务队列一起使用,任务队列用于存储待处理的任务。你需要选择适当的大小,以确保系统可以处理的任务数量。如果队列满了,新提交的任务就会被阻塞。
- 合理设置任务的优先级:你可以为任务设置优先级,优先级高的任务会优先被执行。这可以帮助你控制任务的执行顺序,避免某些任务长时间等待。
- 避免长时间运行的任务:长时间运行的任务会占用线程池中的线程,导致其他任务等待。你可以考虑将长时间运行的任务拆分成多个短时间运行的任务,或者使用异步处理的方式,避免阻塞线程池中的线程。
- 异常处理:当任务抛出未捕获的异常时,线程池会终止该线程。为了避免这种情况,你应该在任务中适当地处理异常,或者使用try-catch块来捕获并处理异常。
- 监控和调优:使用适当的工具监控线程池的性能,并根据监控结果进行调优。例如,你可以监控任务的平均处理时间、队列大小、线程池大小等参数,并根据这些参数进行调整。
- 使用其他并发工具:除了线程池外,Java还提供了其他一些并发工具,如
CompletableFuture
、Reactor
等。这些工具可以帮助你更好地处理并发任务,避免阻塞。
具体使用哪种方法取决于你的应用需求和系统环境。
5.1 ✅如何解决线程池中的线程泄漏
线程池中的线程泄漏可能是由于线程池中不再使用的线程没有正确关闭所导致的。当一个线程不再使用时,我们应该调用其interrupt()
方法来中断该线程,以便它可以释放系统资源。然而,如果线程没有正确关闭,那么它将继续运行,消耗系统资源,导致线程泄漏。
解决线程池中的线程泄漏问题,可以采取以下措施:
- 正确关闭线程:当一个线程不再需要时,应该调用其
interrupt()
方法来中断该线程。在线程中,你应该检查是否已经接收到中断请求(通过检查Thread.interrupted()
或Thread.isInterrupted()
),并在任务完成时手动设置中断状态。 - 使用
try-with-resources
语句:在Java 7及更高版本中,可以使用try-with-resources
语句来自动关闭实现了AutoCloseable
接口的资源。这可以帮助你确保线程池中的线程在不再需要时被正确关闭。 - 定期检查和清理:可以定期检查线程池的使用情况,并清理不再需要的线程。例如,你可以定期调用
ThreadPoolExecutor.getCompletedTaskCount()
和ThreadPoolExecutor.getCorePoolSize()
,比较它们的大小,如果完成的线程数远远超过核心线程数,说明存在泄漏的可能。 - 使用监控工具:使用监控工具来监控线程池的使用情况,以便及时发现线程泄漏问题。例如,你可以使用Java Management Extensions (JMX) 或第三方监控工具来监控线程池的状态和性能。
- 避免长时间运行的任务:长时间运行的任务会占用线程池中的线程,导致其他任务等待。你可以考虑将长时间运行的任务拆分成多个短时间运行的任务,或者使用异步处理的方式,避免阻塞线程池中的线程。
当线程池中的线程泄漏时,这意味着某些线程在完成任务后没有被正确关闭,仍然在运行并消耗系统资源。这通常是由于代码中的错误或疏忽导致的。以下是一个简单的Java代码示例,演示如何避免线程池中的线程泄漏:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 提交任务到线程池
for (int i = 0; i < 50; i++) {
Runnable worker = new Worker("" + i);
executor.execute(worker);
}
// 关闭线程池
executor.shutdown();
// 等待所有任务完成
while (!executor.isTerminated()) {
}
System.out.println("所有任务已完成");
}
}
class Worker implements Runnable {
private String command;
public Worker(String command) {
this.command = command;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始处理:" + command);
processCommand();
System.out.println(Thread.currentThread().getName() + "结束处理:" + command);
}
private void processCommand() {
try {
Thread.sleep(2000); // 模拟耗时操作,例如网络请求、文件读写等
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面的代码中,我们创建了一个固定大小的线程池executor
,并提交了50个任务到线程池中。每个任务都是一个实现了Runnable
接口的Worker
对象。在Worker
的run()
方法中,我们模拟了一个耗时操作(使用Thread.sleep()
方法模拟等待2秒钟),然后打印任务完成的消息。最后,我们调用executor.shutdown()
方法来关闭线程池,并使用循环等待所有任务完成。
要避免线程池中的线程泄漏,我们需要确保每个线程在完成任务后被正确关闭。在上面的代码中,每个Worker
对象都实现了Runnable
接口,并在其run()
方法中执行任务。当任务完成后,run()
方法会自动结束,线程池会自动回收该线程。因此,我们不需要显式地关闭每个线程。当调用executor.shutdown()
方法时,线程池会等待所有任务完成后自动关闭所有线程。
5.2 ✅如何防止任务丢失
要防止任务丢失,通常可以采取以下几种策略:
- 持久化存储:对于可能会因为程序崩溃或重启而丢失的任务,你可以选择将其持久化存储。这意味着当任务开始时,你可以将其写入一个可靠的存储系统,如数据库、消息队列等。这样,即使程序出现故障,你也能从存储中恢复任务。
- 使用事务:如果你正在使用支持事务的数据库或其他存储系统,你可以利用事务来确保数据的完整性和一致性。通过将任务与事务关联,你可以确保要么任务完全执行,要么完全不执行,从而避免部分执行导致的数据不一致问题。
- 重试机制:对于可能会因为临时性错误而失败的任务,你可以设置重试机制。当任务失败时,系统自动尝试重新执行任务。你可以根据任务的类型和重要性设置不同的重试策略,例如立即重试、延迟重试、有限次重试等。
- 异步处理:对于不需立即完成但也不能丢失的任务,你可以选择异步处理。将任务放入一个队列中,由后台的消费者线程或进程异步处理。这样,即使主线程或进程崩溃,也不会影响任务的执行。
- 监控和告警:建立全面的监控系统,实时跟踪任务的执行状态。当发现异常或长时间无响应的任务时,及时发出告警,以便尽快处理和解决潜在的问题。
- 备份和恢复:定期备份重要的任务数据和状态信息,以便在发生故障时能够快速恢复。确保备份的完整性和可用性,以便在需要时能够迅速恢复系统到正常状态。
通过结合以上策略,你可以大大降低任务丢失的风险,确保任务的可靠性和完整性。根据具体的业务需求和系统环境,选择适合的策略并进行合理的设计和实施。
当涉及到防止任务丢失时,Java提供了多种工具和框架来帮助实现这一目标。Demo:
import java.util.concurrent.*;
/**
* 如何防止任务丢失:
*/
public class TaskManager {
private ExecutorService executor;
private BlockingQueue<Runnable> taskQueue;
private int maxQueueSize;
public TaskManager(int maxQueueSize) {
this.maxQueueSize = maxQueueSize;
taskQueue = new LinkedBlockingQueue<>(maxQueueSize);
executor = Executors.newFixedThreadPool(10);
}
public void submitTask(Runnable task) throws InterruptedException {
if (taskQueue.remainingCapacity() == 0) {
throw new IllegalStateException("队列已满,无法接受新任务");
}
taskQueue.put(task); // 将任务添加到队列中
executor.execute(task); // 执行任务
}
public void shutdown() {
executor.shutdown(); // 关闭线程池
try {
if (!executor.awaitTermination(24, TimeUnit.HOURS)) {
executor.shutdownNow(); // 如果线程池未在24小时内关闭,则强制关闭
}
} catch (InterruptedException e) {
executor.shutdownNow(); // 线程被中断,强制关闭线程池
}
}
}
在上述代码中,我们定义了一个TaskManager
类,它使用一个线程池和一个阻塞队列来管理任务的提交和执行。通过submitTask()
方法,你可以将任务提交到队列中,并由线程池中的线程执行。maxQueueSize
参数指定了队列的最大容量,以防止队列无限增长而导致资源耗尽。如果队列已满,submitTask()
方法将抛出IllegalStateException
异常。
为了防止任务丢失,我们使用了阻塞队列来实现任务的排队和缓冲。当任务被提交到队列中时,taskQueue.put(task)
将阻塞直到队列有空余空间。然后,线程池中的线程会从队列中取出任务并执行。这样,即使在程序崩溃或重启的情况下,已经提交但尚未执行的任务也不会丢失,因为它们被保存在队列中等待执行。
最后,通过调用shutdown()
方法,你可以关闭线程池并等待所有任务完成。在关闭之前,该方法会尝试等待24小时,以便给任务足够的时间来完成。如果线程池未在规定时间内关闭,则会强制关闭线程池。这样可以确保所有任务都有机会完成,而不会因为线程池的关闭而导致任务丢失。
5.3 ✅持久化存储怎么实现
要实现持久化存储,你可以使用以下几种方法:
- 关系型数据库(RDBMS):使用关系型数据库如MySQL、PostgreSQL等来存储任务数据。这些数据库提供了ACID事务、数据一致性和数据完整性等特性,适合存储结构化数据。你可以将任务数据以表格的形式存储在数据库中,每个任务对应一行记录。
- NoSQL数据库:NoSQL数据库如MongoDB、Cassandra等提供了灵活的数据模型和可扩展性,适用于存储非结构化或半结构化数据。你可以将任务数据以文档的形式存储在NoSQL数据库中。
- 消息队列:使用消息队列如RabbitMQ、Kafka等来异步处理任务。消息队列可以存储任务的请求和响应消息,通过发布-订阅模式或队列模式进行通信。你可以将任务请求发送到消息队列中,由消费者进程或线程从队列中获取任务并处理。
- 文件系统:将任务数据写入文件系统进行持久化存储。你可以将任务数据序列化为字节流,写入到文件中。这种方法适用于简单的任务管理,但可能不适合大规模和高并发的场景。
- 分布式存储系统:使用分布式存储系统如HDFS、S3等来存储任务数据。这些系统提供了可扩展的存储容量和数据可靠性,适用于大规模数据的存储和访问。你可以将任务数据上传到分布式存储系统中,以便进行备份和恢复。
Demo:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* 将任务持久化存储到数据库中
*/
public class TaskPersistence {
private static final String DB_URL = "jdbc:mysql://localhost:3306/tasks";
private static final String USERNAME = "your_username";
private static final String PASSWORD = "your_password";
public static void main(String[] args) {
String taskId = "12345";
String taskName = "Sample Task";
String taskDescription = "This is a sample task description.";
try (Connection conn = DriverManager.getConnection(DB_URL, USERNAME, PASSWORD)) {
String sql = "INSERT INTO tasks (task_id, task_name, task_description) VALUES (?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, taskId);
pstmt.setString(2, taskName);
pstmt.setString(3, taskDescription);
pstmt.executeUpdate();
System.out.println("Task saved successfully!");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
使用Java实现一个完整的任务管理系统,包括任务的持久化存储、查询和更新:
首先,我们定义一个 Task
类来表示任务:
public class Task {
private String id;
private String name;
private String description;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 构造函数、Getter和Setter方法
public Task(String id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// 其他属性和方法...
}
接下来,创建一个TaskManager类来管理任务:
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
public class TaskManager {
private static final ConcurrentHashMap<String, Task> tasks = new ConcurrentHashMap<>();
private static final AtomicReference<Task> currentTask = new AtomicReference<>();
public static void saveTask(Task task) {
tasks.put(task.getId(), task);
currentTask.set(task);
}
public static Task getTask(String id) {
return tasks.get(id);
}
public static void updateTask(Task task) {
Task existingTask = tasks.get(task.getId());
if (existingTask != null) {
existingTask.setDescription(task.getDescription());
existingTask.setUpdatedAt(LocalDateTime.now());
}
}
public static void deleteTask(String id) {
tasks.remove(id);
}
public static Task getCurrentTask() {
return currentTask.get();
}
// 其他方法...
}
一个线程安全的 ConcurrentHashMap
来存储任务数据。saveTask()
方法用于保存任务,getTask()
方法用于根据任务ID获取任务,updateTask()方法用于更新任务描述和更新时间,deleteTask()方法用于删除任务,getCurrentTask()
方法用于获取当前任务。我们还使用了一个 AtomicReference
来跟踪当前任务。你可以根据需要添加更多的方法和功能,比如查询未完成的任务、按时间排序。