从“线程小白”到“池主”:Java线程与线程池的修炼秘籍

线程:并发世界的基础

在 Java 的编程宇宙中,线程是一个不可或缺的重要概念。它就像是并发编程的 “超级英雄”,赋予程序同时执行多个任务的超能力,极大地提升了程序的效率和响应性。

想象一下,你去一家餐厅吃饭,如果餐厅只有一个服务员,他只能一个一个地为顾客点菜、上菜,效率会非常低。但如果餐厅有多个服务员,他们就可以同时为不同的顾客服务,大大提高了效率。在 Java 编程中,线程就类似于餐厅里的服务员,允许程序在同一时间执行多个任务。

线程是程序执行的最小单位,每个 Java 应用程序至少有一个主线程,这就好比餐厅的 “总负责人”,负责整个程序的启动和运行。除此之外,我们还可以创建其他线程来执行特定的任务,就像招聘更多的服务员来分担工作一样。

线程基础知多少

线程和进程的爱恨情仇

在深入了解线程之前,我们先来搞清楚线程和进程之间的关系。进程就像是一个独立的 “小王国”,它是程序的一次执行过程,拥有自己独立的内存空间、系统资源,是操作系统进行资源分配和调度的基本单位。而线程则是这个 “小王国” 中的 “小助手”,是进程中的一个执行单元,也是操作系统进行调度的最小单位 。一个进程可以包含多个线程,这些线程共享进程的资源,比如内存空间、打开的文件等。

举个例子,当你打开一个浏览器,这个浏览器就是一个进程,而你在浏览器中同时打开多个网页标签,每个网页标签的加载和渲染就可以看作是一个线程。这些线程共享浏览器进程的资源,如网络连接、内存等,它们协同工作,让你能够同时浏览多个网页。线程和进程的关系密切,线程是进程的一部分,进程为线程提供了执行环境和资源。但线程又具有独立性,它们可以并发执行,提高程序的执行效率。

线程的诞生之道

在 Java 中,创建线程有三种常见的方式:继承 Thread 类、实现 Runnable 接口和实现 Callable 接口。下面我们分别来看一下这三种方式的具体实现。

继承 Thread 类

继承 Thread 类是创建线程最直接的方式。我们只需要创建一个类继承 Thread 类,并重写它的 run () 方法,run () 方法中包含了线程要执行的代码。然后创建这个类的实例,并调用 start () 方法来启动线程。

public class MyThread extends Thread {

@Override

public void run() {

// 线程执行的代码

for (int i = 0; i < 5; i++) {

System.out.println(Thread.currentThread().getName() + " 正在执行: " + i);

try {

Thread.sleep(100); // 线程休眠100毫秒

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

public static void main(String[] args) {

MyThread myThread = new MyThread();

myThread.start(); // 启动线程

// 主线程继续执行其他任务

for (int i = 0; i < 5; i++) {

System.out.println("主线程正在执行: " + i);

try {

Thread.sleep(100); // 主线程也休眠100毫秒,以便观察线程交替执行的效果

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

在这个例子中,MyThread 类继承了 Thread 类,并重写了 run () 方法。在 main () 方法中,我们创建了 MyThread 类的实例 myThread,并调用 start () 方法启动线程。start () 方法会创建一个新的线程,并调用 run () 方法来执行线程的任务。

实现 Runnable 接口

实现 Runnable 接口也是创建线程的常用方式。这种方式的好处是可以避免 Java 单继承的限制,因为一个类可以同时实现多个接口。我们需要创建一个类实现 Runnable 接口,并重写它的 run () 方法。然后创建一个 Thread 类的实例,并将实现 Runnable 接口的类的实例作为参数传递给 Thread 类的构造函数,最后调用 start () 方法启动线程。

public class MyRunnable implements Runnable {

@Override

public void run() {

// 线程执行的代码

for (int i = 0; i < 5; i++) {

System.out.println(Thread.currentThread().getName() + " 正在执行: " + i);

try {

Thread.sleep(100); // 线程休眠100毫秒

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

public static void main(String[] args) {

MyRunnable myRunnable = new MyRunnable();

Thread thread = new Thread(myRunnable, "MyRunnableThread");

thread.start(); // 启动线程

// 主线程继续执行其他任务

for (int i = 0; i < 5; i++) {

System.out.println("主线程正在执行: " + i);

try {

Thread.sleep(100); // 主线程也休眠100毫秒,以便观察线程交替执行的效果

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

在这个例子中,MyRunnable 类实现了 Runnable 接口,并重写了 run () 方法。在 main () 方法中,我们创建了 MyRunnable 类的实例 myRunnable,并将其作为参数传递给 Thread 类的构造函数,创建了一个 Thread 类的实例 thread。最后调用 start () 方法启动线程。

实现 Callable 接口

实现 Callable 接口创建线程与实现 Runnable 接口类似,不同之处在于 Callable 的 call () 方法可以返回一个结果,并且可以抛出异常。我们需要创建一个类实现 Callable 接口,并重写它的 call () 方法。然后使用 FutureTask 类来包装 Callable 接口的实现类的实例,并将 FutureTask 类的实例作为参数传递给 Thread 类的构造函数,最后调用 start () 方法启动线程。通过 FutureTask 类的 get () 方法可以获取线程执行的结果。

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.FutureTask;

public class MyCallable implements Callable {

@Override

public String call() throws Exception {

// 线程执行的代码

for (int i = 0; i < 5; i++) {

System.out.println(Thread.currentThread().getName() + " 正在执行: " + i);

Thread.sleep(100); // 线程休眠100毫秒

}

return "任务执行完成";

}

public static void main(String[] args) {

MyCallable myCallable = new MyCallable();

FutureTask futureTask = new FutureTask<>(myCallable);

Thread thread = new Thread(futureTask, "MyCallableThread");

thread.start(); // 启动线程

try {

String result = futureTask.get(); // 获取线程执行的结果

System.out.println("线程执行结果: " + result);

} catch (InterruptedException | ExecutionException e) {

e.printStackTrace();

}

}

}

在这个例子中,MyCallable 类实现了 Callable 接口,并重写了 call () 方法。在 main () 方法中,我们创建了 MyCallable 类的实例 myCallable,并使用 FutureTask 类来包装它。然后将 FutureTask 类的实例作为参数传递给 Thread 类的构造函数,创建了一个 Thread 类的实例 thread。最后调用 start () 方法启动线程,并通过 futureTask.get () 方法获取线程执行的结果。

线程的生命周期

线程的生命周期就像是一个人的人生旅程,从出生到死亡,经历了不同的阶段。在 Java 中,线程的生命周期包括五个状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。理解线程的生命周期对于我们编写高效、稳定的多线程程序非常重要。

新建(New):当我们使用 new 关键字创建一个线程对象时,线程就处于新建状态。此时,线程对象已经被分配了内存空间,但是还没有开始执行。就像一个刚出生的婴儿,已经具备了生命的基本条件,但还没有开始真正的生活。

就绪(Runnable):当线程对象调用了 start () 方法之后,线程就进入了就绪状态。在这个状态下,线程已经具备了运行的条件,但是还没有获得 CPU 的执行权。它就像一个站在起跑线上的运动员,等待着裁判的一声令下,随时准备起跑。

运行(Running):如果处于就绪状态的线程获得了 CPU 的执行权,开始执行 run () 方法的线程执行体,那么线程就进入了运行状态。在这个状态下,线程正在执行任务,就像运动员在赛道上奋力奔跑。

阻塞(Blocked):当处于运行状态的线程失去所占用的资源之后,便进入阻塞状态。阻塞状态的线程暂时停止运行,等待某个条件的满足。比如,线程执行了 sleep () 方法,或者等待获取某个对象的锁,或者执行了 I/O 操作等。这就好比运动员在比赛中遇到了障碍物,不得不停下来等待。

死亡(Dead):当线程的 run () 方法执行结束,或者因为异常退出了 run () 方法,线程就进入了死亡状态。一旦线程进入死亡状态,就不能再重新启动。这就像人的生命结束一样,无法复生。

线程在生命周期中会在不同的状态之间转换,下面是线程状态转换的示意图:

新建(New) --> 就绪(Runnable) --> 运行(Running)

^ |

| |

| |

|<-- 阻塞(Blocked) <-- 等待(Waiting) |

| |

| |

| |

`--> 死亡(Dead) <-- 超时等待(Timed Waiting)

了解线程的生命周期和状态转换,有助于我们更好地控制线程的执行,提高程序的性能和稳定性。在实际编程中,我们需要根据具体的需求,合理地管理线程的生命周期,避免出现线程死锁、资源竞争等问题。

线程池:线程管理的神器

为什么要用线程池

在多线程编程中,虽然线程能让程序并发执行,提高效率,但如果频繁地创建和销毁线程,就好比一家餐厅,每来一位顾客就招聘一位新服务员,顾客离开就辞退服务员,这不仅会消耗大量的时间和资源,还会导致系统性能下降。线程池的出现,就像是为餐厅建立了一个服务员储备库,当有顾客来的时候,直接从储备库中调配服务员,顾客离开后,服务员也不会被辞退,而是留在储备库中等待下一次服务,大大提高了效率。

线程池可以复用线程,减少线程创建和销毁的开销,提高系统性能;还能控制线程的数量,避免线程过多导致资源竞争和系统崩溃;并且可以对线程进行统一管理,方便进行任务调度和监控。所以,线程池是多线程编程中不可或缺的工具。

线程池的核心参数

Java 中的线程池由ThreadPoolExecutor类来实现,它有七个核心参数,这些参数就像是线程池的 “设置按钮”,通过合理设置它们,可以让线程池更好地工作。

  1. 核心线程数(corePoolSize):这是线程池中始终存活的线程数,即使它们处于空闲状态,也不会被销毁(除非设置了允许核心线程超时)。就像餐厅里的正式员工,无论生意好坏,他们都在岗位上随时准备工作。
  1. 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。当任务队列已满且核心线程都在忙碌时,线程池会创建新的线程,直到达到最大线程数。这就好比餐厅在高峰期时,除了正式员工,还会临时招聘一些兼职员工来帮忙。
  1. 线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,多余的空闲线程在被销毁之前等待新任务的最长时间。如果在这段时间内没有新任务,这些线程就会被回收。例如,餐厅的兼职员工,如果在一段时间内没有顾客需要服务,就会被辞退。
  1. 时间单位(unit):用于指定线程存活时间的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。它和线程存活时间一起,决定了空闲线程的存活时长。
  1. 任务队列(workQueue):用于存储等待执行的任务。当线程池中的线程都在忙碌时,新的任务会被放入任务队列中等待执行。常见的任务队列有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)、SynchronousQueue(不存储元素的队列)等。不同的任务队列有不同的特性,需要根据实际情况选择。比如,ArrayBlockingQueue有固定的大小,当队列满时,新的任务无法加入;而LinkedBlockingQueue可以是无界的,理论上可以存储无限个任务,但可能会导致内存溢出。
  1. 线程工厂(threadFactory):用于创建新线程的工厂。通过线程工厂,我们可以自定义线程的属性,如线程名称、优先级、是否为守护线程等。例如,我们可以为线程设置有意义的名称,方便在调试和监控时识别线程。
  1. 拒绝策略(handler):当线程池无法处理新任务时(即任务队列已满且线程数达到最大线程数),采取的策略。常见的拒绝策略有以下几种:
    • AbortPolicy(中止策略):默认策略,直接抛出RejectedExecutionException异常,阻止系统正常工作。就像餐厅已经忙得不可开交,拒绝接待新的顾客,并告诉顾客餐厅已经满员。
    • CallerRunsPolicy(调用者运行策略):将任务交给提交任务的线程来执行。例如,如果是主线程提交的任务,就由主线程来执行这个任务。这相当于餐厅让顾客自己帮忙服务,虽然有点奇怪,但也能解决问题。
    • DiscardPolicy(丢弃策略):直接丢弃无法处理的任务,不做任何处理。就像餐厅直接无视新的顾客,不给予任何回应。
    • DiscardOldestPolicy(丢弃最旧任务策略):丢弃任务队列中最早的任务,然后尝试提交新任务。这就好比餐厅把等待时间最长的顾客的订单取消,然后接受新的顾客订单。

线程池的执行流程

当一个任务提交到线程池时,线程池会按照以下流程来处理这个任务:

  1. 首先,线程池会判断当前线程数是否小于核心线程数。如果是,就会创建一个新的线程来执行这个任务。例如,餐厅刚开门营业,只有几个顾客,正式员工还没有全部忙碌起来,所以会直接安排一个正式员工去服务新顾客。
  1. 如果当前线程数已经达到或超过核心线程数,那么线程池会判断任务队列是否已满。如果任务队列未满,就将任务添加到任务队列中等待执行。这就好比餐厅的正式员工都在忙碌,但还有空的座位,新的顾客就会先在座位上等待,由后续空闲的员工来服务。
  1. 如果任务队列已满,线程池会继续判断当前线程数是否小于最大线程数。如果是,就会创建一个新的线程(非核心线程)来执行这个任务。比如餐厅的座位已经坐满,正式员工也都在忙碌,但还有顾客不断到来,这时餐厅就会临时招聘兼职员工来服务新顾客。
  1. 如果当前线程数已经达到最大线程数,任务队列也已满,那么线程池就会执行拒绝策略,处理这个无法处理的任务。这就相当于餐厅已经人满为患,正式员工和兼职员工都在忙碌,没有多余的座位和员工来服务新顾客,只能采取拒绝策略。

下面是一个简单的代码示例,展示如何创建一个线程池并提交任务:

import java.util.concurrent.*;

public class ThreadPoolExample {

public static void main(String[] args) {

// 创建线程池

ThreadPoolExecutor executor = new ThreadPoolExecutor(

2, // 核心线程数

4, // 最大线程数

60, TimeUnit.SECONDS, // 线程存活时间和时间单位

new ArrayBlockingQueue<>(10), // 任务队列,容量为10


Executors.defaultThreadFactory(), // 默认线程工厂

new
ThreadPoolExecutor.AbortPolicy() // 拒绝策略,默认是抛出异常

);

// 提交任务

for (int i = 0; i < 15; i++) {

final int taskNumber = i;

executor.submit(() -> {

System.out.println("Task " + taskNumber + " is running in thread " + Thread.currentThread().getName());

try {

Thread.sleep(1000); // 模拟任务执行时间

} catch (InterruptedException e) {

e.printStackTrace();

}

});

}

// 关闭线程池

executor.shutdown();

}

}

在这个示例中,我们创建了一个线程池,核心线程数为 2,最大线程数为 4,线程存活时间为 60 秒,任务队列是一个容量为 10 的ArrayBlockingQueue,使用默认的线程工厂和拒绝策略。然后我们向线程池提交了 15 个任务,每个任务会在执行时输出任务编号和执行线程的名称,并睡眠 1 秒钟模拟任务执行时间。最后,我们调用shutdown()方法关闭线程池。

线程池的使用实操

创建线程池的正确姿势

在 Java 中,创建线程池有两种常见的方式:使用Executors工具类和直接使用ThreadPoolExecutor类。虽然Executors提供了一些便捷的方法来创建线程池,如newFixedThreadPool(创建固定大小的线程池)、newCachedThreadPool(创建可缓存的线程池)、newSingleThreadExecutor(创建单线程的线程池)等,但阿里巴巴的 Java 开发手册中明确建议不使用Executors来创建线程池,而是通过ThreadPoolExecutor的方式来创建。

这是因为Executors创建的线程池存在一些潜在的风险。例如,newFixedThreadPool和newSingleThreadExecutor使用的是无界的LinkedBlockingQueue,当任务提交速度持续大于任务处理速度时,可能会导致队列大量阻塞,从而堆积大量的请求,最终耗尽内存,引发 OOM(OutOfMemoryError)异常。而newCachedThreadPool和newScheduledThreadPool允许创建的线程数量为Integer.MAX_VALUE,如果任务数量过多且执行速度较慢,可能会创建大量的线程,同样会导致 OOM 异常。

因此,为了更好地控制线程池的行为,避免资源耗尽的风险,我们应该使用ThreadPoolExecutor手动创建线程池,这样可以更明确地设置线程池的各项参数,如核心线程数、最大线程数、线程存活时间、任务队列、线程工厂和拒绝策略等。例如:

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;

public class ThreadPoolExample {

public static void main(String[] args) {

// 核心线程数为5

int corePoolSize = 5;

// 最大线程数为10

int maximumPoolSize = 10;

// 线程存活时间为60秒

long keepAliveTime = 60;

// 时间单位为秒

TimeUnit unit = TimeUnit.SECONDS;

// 使用容量为100的有界任务队列

ArrayBlockingQueue workQueue = new ArrayBlockingQueue<>(100);

// 创建线程池

ThreadPoolExecutor executor = new ThreadPoolExecutor(

corePoolSize,

maximumPoolSize,

keepAliveTime,

unit,

workQueue

);

// 提交任务

for (int i = 0; i < 150; i++) {

final int taskNumber = i;

executor.submit(() -> {

System.out.println("Task " + taskNumber + " is running in thread " + Thread.currentThread().getName());

try {

Thread.sleep(1000); // 模拟任务执行时间

} catch (InterruptedException e) {

e.printStackTrace();

}

});

}

// 关闭线程池

executor.shutdown();

}

}

在这个例子中,我们创建了一个线程池,核心线程数为 5,最大线程数为 10,线程存活时间为 60 秒,任务队列是一个容量为 100 的ArrayBlockingQueue。这样的配置可以根据实际需求来调整,以确保线程池在不同的场景下都能稳定、高效地运行。

向线程池提交任务

当我们创建好线程池后,就可以向线程池中提交任务了。在 Java 的线程池中,有两种常用的提交任务的方法:execute和submit。

execute方法是Executor接口中定义的方法,它只能提交Runnable类型的任务,并且没有返回值。例如:

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class ExecuteExample {

public static void main(String[] args) {

ExecutorService executor = Executors.newFixedThreadPool(2);

executor.execute(() -> {

System.out.println("Task executed using execute()");

});

executor.shutdown();

}

}

在这个例子中,我们创建了一个固定大小为 2 的线程池,并使用execute方法提交了一个Runnable任务。这个任务会在某个线程中执行,但是我们无法获取任务的执行结果。

submit方法是ExecutorService接口中定义的方法,它不仅可以提交Runnable类型的任务,还可以提交Callable类型的任务,并且会返回一个Future对象,通过这个对象可以获取任务的执行结果、判断任务是否完成、取消任务的执行等。例如:

import java.util.concurrent.*;

public class SubmitExample {

public static void main(String[] args) throws ExecutionException, InterruptedException {

ExecutorService executor = Executors.newFixedThreadPool(2);

// 提交Callable任务

Future future = executor.submit(() -> {

System.out.println("Task executed using submit()");

return "Task result";

});

try {

// 获取任务执行结果,会阻塞直到任务完成

String result = future.get();

System.out.println("Task result: " + result);

} catch (InterruptedException | ExecutionException e) {

e.printStackTrace();

}

executor.shutdown();

}

}

在这个例子中,我们使用submit方法提交了一个Callable任务,这个任务会返回一个字符串结果。通过future.get()方法可以获取任务的执行结果,但是这个方法会阻塞当前线程,直到任务完成。如果任务还没有完成,调用future.get()方法会导致线程等待。

除了获取任务结果,Future对象还提供了其他有用的方法,比如isDone()方法可以判断任务是否已经完成,cancel(boolean mayInterruptIfRunning)方法可以尝试取消任务的执行。例如:

import java.util.concurrent.*;

public class FutureExample {

public static void main(String[] args) throws ExecutionException, InterruptedException {

ExecutorService executor = Executors.newFixedThreadPool(2);

Future future = executor.submit(() -> {

try {

Thread.sleep(3000); // 模拟任务执行时间

return "Task result";

} catch (InterruptedException e) {

e.printStackTrace();

return "Task interrupted";

}

});

while (!future.isDone()) {

System.out.println("Task is still running...");

try {

Thread.sleep(500); // 每隔500毫秒检查一次任务是否完成

} catch (InterruptedException e) {

e.printStackTrace();

}

}

try {

String result = future.get();

System.out.println("Task result: " + result);

} catch (InterruptedException | ExecutionException e) {

e.printStackTrace();

}

// 尝试取消任务(这里任务已经完成,取消操作会失败)

boolean cancelled = future.cancel(true);

System.out.println("Task cancelled: " + cancelled);

executor.shutdown();

}

}

在这个例子中,我们使用isDone()方法不断检查任务是否完成,在任务完成后获取任务结果,并尝试取消任务(由于任务已经完成,取消操作会失败)。

总的来说,execute方法适用于提交不需要返回结果的任务,比如后台日志记录、事件处理等场景;而submit方法适用于提交需要返回结果或需要捕获异常的任务,比如需要等待任务执行结果的数据处理任务、需要对异常进行处理的场景。

关闭线程池

当我们不再需要使用线程池时,应该及时关闭它,以释放资源。在 Java 中,线程池提供了两个方法来关闭线程池:shutdown和shutdownNow。

shutdown方法是一种比较温和的关闭方式。当调用shutdown方法后,线程池不再接受新的任务,但会继续执行已经提交到任务队列中的任务,直到所有任务都执行完毕后,线程池才会真正关闭。例如:

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class ShutdownExample {

public static void main(String[] args) {

ExecutorService executor = Executors.newFixedThreadPool(2);

for (int i = 0; i < 5; i++) {

final int taskNumber = i;

executor.submit(() -> {

System.out.println("Task " + taskNumber + " is running in thread " + Thread.currentThread().getName());

try {

Thread.sleep(1000); // 模拟任务执行时间

} catch (InterruptedException e) {

e.printStackTrace();

}

});

}

// 调用shutdown方法关闭线程池

executor.shutdown();

// 检查线程池是否已经关闭

while (!executor.isTerminated()) {

System.out.println("Waiting for tasks to complete...");

try {

Thread.sleep(500); // 每隔500毫秒检查一次线程池是否关闭

} catch (InterruptedException e) {

e.printStackTrace();

}

}

System.out.println("All tasks completed, thread pool is shutdown.");

}

}

在这个例子中,我们创建了一个固定大小为 2 的线程池,并提交了 5 个任务。然后调用shutdown方法关闭线程池,通过isTerminated方法检查线程池是否已经关闭。在所有任务执行完毕后,isTerminated方法返回true,表示线程池已经关闭。

shutdownNow方法则是一种比较激进的关闭方式。当调用shutdownNow方法后,线程池会立即停止接受新的任务,并尝试中断正在执行的任务,同时会将任务队列中尚未开始执行的任务全部移除,并返回一个包含这些任务的列表。需要注意的是,shutdownNow方法只是尝试中断任务,并不保证所有正在执行的任务都能被成功中断。例如:

import java.util.List;

import java.util.concurrent.*;

public class ShutdownNowExample {

public static void main(String[] args) {

ExecutorService executor = Executors.newFixedThreadPool(2);

for (int i = 0; i < 5; i++) {

final int taskNumber = i;

executor.submit(() -> {

System.out.println("Task " + taskNumber + " is running in thread " + Thread.currentThread().getName());

try {

Thread.sleep(1000); // 模拟任务执行时间

} catch (InterruptedException e) {

e.printStackTrace();

}

});

}

// 调用shutdownNow方法关闭线程池

List remainingTasks = executor.shutdownNow();

System.out.println("Number of remaining tasks: " + remainingTasks.size());

for (Runnable task : remainingTasks) {

System.out.println("Remaining task: " + task);

}

}

}

在这个例子中,我们调用shutdownNow方法关闭线程池,并获取了任务队列中尚未开始执行的任务列表。通过打印任务列表的大小和每个任务的信息,可以了解到哪些任务没有被执行。

一般情况下,如果我们希望线程池在完成所有任务后再关闭,应该使用shutdown方法;如果我们需要立即停止线程池,并且不关心正在执行的任务是否完成,可以使用shutdownNow方法。在实际应用中,还可以结合awaitTermination方法来等待线程池完全关闭,以确保资源的正确释放。例如:

import java.util.concurrent.*;

public class GracefulShutdownExample {

public static void main(String[] args) {

ExecutorService executor = Executors.newFixedThreadPool(2);

for (int i = 0; i < 5; i++) {

final int taskNumber = i;

executor.submit(() -> {

System.out.println("Task " + taskNumber + " is running in thread " + Thread.currentThread().getName());

try {

Thread.sleep(1000); // 模拟任务执行时间

} catch (InterruptedException e) {

e.printStackTrace();

}

});

}

// 调用shutdown方法关闭线程池

executor.shutdown();

try {

// 等待线程池在60秒内完成所有任务

if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {

// 如果60秒内没有完成所有任务,调用shutdownNow方法强制关闭

executor.shutdownNow();

// 再次等待10秒,确保所有任务都被处理

if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {

System.out.println("Pool did not terminate");

}

}

} catch (InterruptedException ie) {

// 如果等待过程中线程被中断,调用shutdownNow方法强制关闭

executor.shutdownNow();

// 恢复中断状态

Thread.currentThread().interrupt();

}

}

}

在这个例子中,我们首先调用shutdown方法关闭线程池,然后使用awaitTermination方法等待线程池在 60 秒内完成所有任务。如果 60 秒内没有完成所有任务,我们调用shutdownNow方法强制关闭线程池,并再次等待 10 秒。如果 10 秒后线程池仍然没有完全关闭,我们打印提示信息。这样可以确保线程池在关闭时尽可能地完成所有任务,同时也能在必要时及时释放资源。

总结与展望

Java 线程和线程池是 Java 并发编程中非常重要的知识点,它们为我们编写高效、稳定的多线程程序提供了强大的支持。通过本文的学习,我们了解了线程的基本概念、创建方式、生命周期,以及线程池的原理、核心参数、执行流程和使用方法。

线程的创建方式有继承 Thread 类、实现 Runnable 接口和实现 Callable 接口,每种方式都有其特点和适用场景。线程的生命周期包括新建、就绪、运行、阻塞和死亡五个状态,了解这些状态的转换有助于我们更好地控制线程的执行。

线程池作为线程管理的神器,通过复用线程、控制线程数量和统一管理线程,大大提高了系统的性能和稳定性。我们学习了线程池的核心参数,如核心线程数、最大线程数、线程存活时间、任务队列、线程工厂和拒绝策略,这些参数的合理设置是线程池高效运行的关键。同时,我们还掌握了线程池的执行流程,以及如何创建线程池、向线程池提交任务和关闭线程池。

在实际项目中,我们应根据具体的业务需求和系统资源情况,合理地使用线程和线程池。避免创建过多的线程导致资源耗尽,也要注意线程安全问题,防止出现数据竞争和死锁等情况。希望大家能够将本文所学的知识运用到实际项目中,不断提升自己的编程能力。同时,Java 并发编程还有很多深入的知识等待我们去探索,如线程同步、并发集合等,期待大家在这个领域中不断深入学习,创造出更优秀的程序。

相关文章

我做java面试官时,常问的问题

大家好,我是贠学文,点击右上方“关注”,每天为您分享java程序员需要掌握的知识点干货。前不久,我写了一篇《如何成为一个优秀面试官》的文章,具体可点击如下链接阅读:如何成为一个优秀的面试官那么今天,在...

java程序员面试时经常被问到的10个问题

java程序员,尤其是做web开发的,面试时,面试官最喜欢问到以下10个问题,掌握面试的动态和技巧,有利于提高我们面试的成功率,了解以下10个问题,有利于java程序员的面试。1、简单描述一下Log4...

记一次CPU使用率低负载高的排查过程

一、背景历史原因,当前有一个服务专门用于处理mq消息,mq使用的阿里云rocketmq,sdk版本1.2.6(2016年)。随着业务的发展,该应用上的consumer越来越多,接近200+,导致该应用...

面试官:核心线程数为0时,线程池如何执行?

线程池是 Java 中用于提升程序执行效率的主要手段,也是并发编程中的核心实现技术,并且它也被广泛的应用在日常项目的开发之中。那问题来了,如果把线程池中的核心线程数设置为 0 时,线程池是如何执行的?...

从IO到NIO:Java数据传输的进阶之路

引言:Java IO 的进化在 Java 编程的世界里,I/O(Input/Output)操作就像是程序与外部世界沟通的桥梁。无论是读取文件、网络通信,还是写入数据,I/O 操作无处不在。早期的 Ja...