这是一种比较传统的创建线程的方式。你可以创建一个类,继承自 Thread 类,并重写 run 方法来定义线程的执行逻辑。
class MyThread extends Thread {
@Override
public void run() {
// 线程的执行逻辑
}
}
// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
这种方式更常用,它避免了 Java 的单继承限制,你可以实现 Runnable 接口,然后将其实例作为参数传递给 Thread 构造函数。
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程的执行逻辑
}
}
// 创建并启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();
你可以在创建线程时使用匿名内部类,实现 Runnable 接口的 run 方法。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 线程的执行逻辑
}
});
thread.start();
如果 Runnable 接口只有一个抽象方法,你可以使用 Lambda 表达式简化代码。
Thread thread = new Thread(() -> {
// 线程的执行逻辑
});
thread.start();
Callable 接口允许线程返回结果或抛出异常。需要通过 ExecutorService 来执行。
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程的执行逻辑
return "Hello from Callable";
}
}
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(new MyCallable());
String result = future.get(); // 获取线程执行结果
Java 中的 Runnable 接口和 Callable 接口都是用来创建多线程的接口,它们的区别如下:
方法名不同。
run()
方法,call()
方法。返回值不同。
Runnable 的 run()
方法没有返回值,
而 Callable 的 call()
方法可以返回执行结果。
异常处理不同。
run()
方法不能抛出异常,call()
方法可以抛出异常,并且需要在调用 Future.get()
方法时进行异常处理。调用方式不同。
用途不同。
线程池就是管理一系列线程的资源池。
当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
简单来说,是因为使用线程池可以提高资源的利用率。
线程池可以帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。
我们写代码的过程中,学会池化思想,最直接相关的就是使用线程池而不是去
new
一个线程。
使用线程池有三大好处:
execute()
方法添加一个任务时,线程池会做如下判断:
图解:
这里使用 isTerminated 判断线程池是否完全停止了。只有状态为 terminated 才能说明线程池关闭了,结束循环,退出方法。
@Test
void contextLoads() {
//创建含有3个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
//提交3个任务到线程池中
for (int i = 0; i < 3; i++) {
final int taskNo = i;
threadPool.execute(() -> {
logger.info("执行任务{}", taskNo);
});
}
//关闭线程池
threadPool.shutdown();
//如果线程池还没达到Terminated状态,说明线程池中还有任务没有执行完,则继续循环等待线程池执行完任务
while (!threadPool.isTerminated()) {
}
}
输出结果
2023-03-21 23:01:16.198 INFO 40176 --- [pool-4-thread-1] .j.JavaCommonMistakes100ApplicationTests : 执行任务0
2023-03-21 23:01:16.198 INFO 40176 --- [pool-4-thread-2] .j.JavaCommonMistakes100ApplicationTests : 执行任务1
2023-03-21 23:01:16.225 INFO 40176 --- [pool-4-thread-3] .j.JavaCommonMistakes100ApplicationTests : 执行任务2
在 Java 5 之后,通过 Executor
来启动线程比使用 Thread
的 start
方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。
这是因为线程池的执行过程会等待构造完成后再进行任务的执行,从而避免了 this 逃逸问题的发生。
什么是 this 逃逸问题?
在 Java 中,对象的构造过程可能会涉及到多个线程,而当一个对象尚未完全构造完成但已经被其他线程引用时,就可能产生 this 逃逸问题。
具体来说,当一个对象正在构造过程中,它的引用就被发布到了其他线程,这时其他线程可能会使用这个尚未完全构造的对象,从而导致意料之外的行为和错误。这可能会因为对象的状态不稳定而引发线程安全问题。
Executor
框架结构主要由三大部分组成:
Runnable
或者 Callable
接口的任务对象。Runnable
/Callable
接口的【对象】直接交给 ExecutorService
执行: ExecutorService.execute(Runnable command)
或者也可以把 Runnable
对象或Callable
对象提交给 ExecutorService
执行(ExecutorService.submit(Runnable task)
或 ExecutorService.submit(Callable task)
)。ExecutorService.submit(…)
,ExecutorService
将返回一个实现Future
接口的对象(我们刚刚也提到过了执行 execute()
方法和 submit()
方法的区别,submit()
会返回一个 FutureTask 对象)。由于 FutureTask
实现了 Runnable
,我们也可以创建 FutureTask
,然后直接交给 ExecutorService
执行。FutureTask.get()
方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)
来取消此任务的执行。代码示例:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 创建实现Runnable接口的任务
Runnable task1 = () -> {
System.out.println("Task 1 is running on thread: " + Thread.currentThread().getName());
};
// 创建实现Callable接口的任务
Callable<String> task2 = () -> {
System.out.println("Task 2 is running on thread: " + Thread.currentThread().getName());
return "Task 2 Result";
};
try {
// 执行Runnable任务
executorService.execute(task1);
// 提交Callable任务,并获取Future对象
Future<String> future = executorService.submit(task2);
// 主线程等待Callable任务执行完成,并获取结果
String result = future.get();
System.out.println("Task 2 Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// 关闭线程池
executorService.shutdown();
}
}
}
通过 Executors
框架创建的线程池,从源码可以看到,它底层是通过 ThreadPoolExecutor
完成线程池的创建,具体参数如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
corePoolSize:线程池的核心线程数,即线程池中始终保持的线程数。
maximumPoolSize:线程池中最大的线程数,包括核心线程数和非核心线程数。
keepAliveTime:非核心线程的闲置时间,超过该时间后将被回收。
unit:keepAliveTime 非核心线程的闲置时间的单位。
workQueue:任务队列,用于存储还未被执行的任务。
threadFactory:线程工厂,用于创建线程。
handler:饱和策略,即当线程池中的线程都在执行任务时,新的任务会如何处理。(也称为拒绝策略)
核心线程数和最大线程数的区别在于:
在任务数超过核心线程数时,线程池会优先创建核心线程来执行任务,只有当任务队列已满且核心线程都在执行任务时,才会创建非核心线程来执行任务,直到达到最大线程数为止。
有四种常见的拒绝策略:
Java 中常用的阻塞队列有以下 4 种:
ArrayBlockingQueue
:一个由数组结构组成的有界阻塞队列,按照先进先出的原则对元素进行排序。
LinkedBlockingQueue
:一个由链表结构组成的可选有界阻塞队列,按照先进先出的原则对元素进行排序。
如果队列容量没有限制,则为无界阻塞队列。
PriorityBlockingQueue
:一个支持优先级排序的无界阻塞队列。
SynchronousQueue
:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,否则插入操作一直处于阻塞状态。
新线程的添加有以下 4 个流程:
如果当前线程池中的线程数小于核心线程数,那么就创建一个新的核心线程来执行这个任务;
如果当前线程池中的线程数已经达到了核心线程数,那么就将任务添加到任务队列中等待执行;
如果任务队列已满,但当前线程池中的线程数还没有达到最大线程数,那么就创建一个新的非核心线程来执行这个任务;
非核心线程在执行完任务之后会被回收,直到线程池中的线程数又重新降至核心线程数。
如果当前线程池中的线程数已经达到了最大线程数,那么就根据饱和策略来处理这个任务。
方式一:通过 ThreadPoolExecutor
构造函数来创建(推荐)。
我们可以创建多种类型的 ThreadPoolExecutor
:
FixedThreadPool
:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。SingleThreadExecutor
:该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。CachedThreadPool
: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。ScheduledThreadPool
:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。方式二:通过 Executor
框架的工具类 Executors
来创建。
Executors
返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor:使用的是无界的 LinkedBlockingQueue
,任务队列最大长度为 Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool:使用的是同步队列 SynchronousQueue
, 允许创建的线程数量为 Integer.MAX_VALUE
,可能会创建大量线程,从而导致 OOM。
ScheduledThreadPool 和 SingleThreadScheduledExecutor : 使用的无界的延迟阻塞队列 DelayedWorkQueue
,任务队列最大长度为 Integer.MAX_VALUE
, 可能堆积大量的请求,从而导致 OOM。
OOM(Out of Memory)是指内存溢出,即程序在运行过程中申请的内存超过了JVM所能提供的最大内存限制,导致无法继续分配内存,从而抛出内存溢出异常。
execute
用于提交不需要返回值的任务threadsPool.execute(new Runnable() {
@Override public void run() {
// TODO Auto-generated method stub }
});
submit()
方法用于提交需要返回值的任务
线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get()
方法来获取返回值。
Future<Object> future = executor.submit(harReturnValuetask);
try { Object s = future.get(); } catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池 executor.shutdown();
}
线程池的停止方式有两种: