线程
- 定义:线程是程序执行的最小单元,一个进程可以包含多个线程。
- 创建方式:
- 继承
Thread
类。 - 实现
Runnable
接口。 - 实现
Callable
接口(带返回值,这个由用户自己定返回的数据)。
- 继承
- 特点:每个线程独立运行,共享进程资源。
多线程任务的常见创建方式
在没有线程池的情况下,创建线程主要有以下几种方式:
(1) 继承 Thread
类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
- 这种方式直接继承
Thread
类并重写run()
方法。 - 缺点:Java 不支持多继承,如果类已经继承了其他类,则无法再继承
Thread
。
(2) 实现 Runnable
接口
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Task running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyTask());
thread.start(); // 启动线程
}
}
- 这种方式更灵活,因为一个类可以同时实现多个接口。
- 需要手动创建
Thread
对象并将Runnable
实例传递给它。
(3) 实现 Callable
接口
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("Task running: " + Thread.currentThread().getName());
return 42; // 返回结果
}
}
public class Main {
public static void main(String[] args) throws Exception {
FutureTask<Integer> futureTask = new FutureTask<>(new MyTask());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("Result: " + futureTask.get()); // 获取返回值
}
}
Callable
接口允许任务返回结果,并可以抛出异常。- 需要与
FutureTask
配合使用。
多线程
- 定义:通过多个线程并发或并行执行任务,实现高效处理。
方式 1:直接创建多个线程
这是最基础的多线程实现方式,通过手动创建多个线程对象,并调用 start()
方法启动线程。每个线程独立运行,任务逻辑由线程执行。
实现步骤
- 定义任务逻辑:
- 使用
Thread
类或Runnable
接口定义线程的任务逻辑。
- 使用
- 创建线程对象:
- 每个任务需要对应一个线程对象。
- 启动线程:
- 调用线程对象的
start()
方法,启动线程并执行任务。
- 调用线程对象的
代码示例
方法 1:继承 Thread
类
class MyThread extends Thread {
@Override
public void run() {
// 线程的任务逻辑
System.out.println("线程 " + Thread.currentThread().getName() + " 正在运行");
}
}
public class Main {
public static void main(String[] args) {
// 创建多个线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 启动线程
t1.start(); // 启动线程 1
t2.start(); // 启动线程 2
}
}
输出示例(顺序可能不同,因为线程是并发执行的):
线程 Thread-0 正在运行
线程 Thread-1 正在运行
方法 2:实现 Runnable
接口
class MyTask implements Runnable {
@Override
public void run() {
// 线程的任务逻辑
System.out.println("线程 " + Thread.currentThread().getName() + " 正在运行");
}
}
public class Main {
public static void main(String[] args) {
// 创建多个线程对象
Thread t1 = new Thread(new MyTask());
Thread t2 = new Thread(new MyTask());
// 启动线程
t1.start(); // 启动线程 1
t2.start(); // 启动线程 2
}
}
输出示例(顺序可能不同):
线程 Thread-0 正在运行
线程 Thread-1 正在运行
方法 3:使用匿名内部类
如果任务逻辑简单,可以直接使用匿名内部类来简化代码:
public class Main {
public static void main(String[] args) {
// 使用匿名内部类创建线程
new Thread(() -> {
System.out.println("线程 1 正在运行");
}).start();
new Thread(() -> {
System.out.println("线程 2 正在运行");
}).start();
}
}
输出示例(顺序可能不同):
线程 1 正在运行
线程 2 正在运行
关键点解析
- 线程启动:
- 调用
start()
方法会启动线程,并自动调用run()
方法。 - 不要直接调用
run()
方法,否则不会启动新线程,而是在当前线程中执行任务。
- 调用
- 线程独立性:
- 每个线程独立运行,任务之间没有共享状态(除非显式共享变量)。
- 缺点:
- 频繁创建和销毁线程会导致性能开销。
- 线程数量过多时,可能导致资源耗尽。
方式 2:使用线程池
线程池是一种更高效的多线程实现方式,通过预先创建一组线程并复用来执行任务,避免频繁创建和销毁线程。
实现步骤
- 创建线程池:
- 使用
Executors
工具类快速创建线程池,或者使用ThreadPoolExecutor
自定义线程池。
- 使用
- 提交任务:
- 将任务(
Runnable
或Callable
)提交给线程池。
- 将任务(
- 关闭线程池:
- 使用
shutdown()
方法优雅地关闭线程池。
- 使用
代码示例
方法 1:使用 Executors
工具类
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 创建固定大小为 2 的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
// 提交任务
pool.submit(() -> {
System.out.println("任务 1 正在运行,线程:" + Thread.currentThread().getName());
});
pool.submit(() -> {
System.out.println("任务 2 正在运行,线程:" + Thread.currentThread().getName());
});
// 关闭线程池
pool.shutdown();
}
}
输出示例(顺序可能不同):
任务 1 正在运行,线程:pool-1-thread-1
任务 2 正在运行,线程:pool-1-thread-2
方法 2:使用 ThreadPoolExecutor
自定义线程池
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 核心线程数
int corePoolSize = 2;
// 最大线程数
int maximumPoolSize = 4;
// 空闲线程存活时间
long keepAliveTime = 60L;
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10);
// 线程工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + " 正在运行,线程:" + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
输出示例(顺序可能不同):
任务 1 正在运行,线程:pool-1-thread-1
任务 2 正在运行,线程:pool-1-thread-2
任务 3 正在运行,线程:pool-1-thread-1
任务 4 正在运行,线程:pool-1-thread-2
任务 5 正在运行,线程:pool-1-thread-1
关键点解析
- 线程复用:
- 线程池中的线程会被复用,减少线程创建和销毁的开销。
- 任务队列:
- 当线程池中的线程都在忙碌时,新任务会被放入任务队列等待。
- 拒绝策略:
- 如果任务队列已满且线程池达到最大线程数,新任务会被拒绝,触发拒绝策略。
- 优雅关闭:
- 使用
shutdown()
方法可以等待所有任务完成后关闭线程池。 - 使用
shutdownNow()
方法会尝试立即停止所有任务。
- 使用
直接创建多个线程适合小规模任务,使用线程池适合大规模任务或需要高效管理线程的场景。
线程池
- 定义:一种管理线程的工具,预先创建一组线程,复用它们来执行多个任务,旨在提高程序性能和资源利用率。
- 优点:
- 避免频繁创建和销毁线程,减少性能开销。
- 提供任务队列、拒绝策略等高级功能。
- 常见线程池:
- 固定大小线程池:
Executors.newFixedThreadPool(n)
。 - 单线程池:
Executors.newSingleThreadExecutor()
。 - 可缓存线程池:
Executors.newCachedThreadPool()
。 - 定时任务线程池:
Executors.newScheduledThreadPool(n)
。
- 固定大小线程池:
在 Java 中,java.util.concurrent
包提供了多种方式来创建和管理线程池,以下是几种常见的线程池创建方法:
1. 使用 Executors
工具类
Executors
是一个线程池工厂类,提供了多种静态方法来创建不同类型的线程池。这些方法简单易用,但在某些场景下可能导致资源浪费或性能问题(如固定大小的线程池可能导致任务堆积)。
(1) 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
- 特点:线程池中始终保持固定数量的线程(这里是 5 个),超出任务会被放入队列等待。
- 适用场景:适用于任务量相对稳定、需要控制并发数的场景。
(2) 创建单线程的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
- 特点:线程池中只有一个线程,所有任务按顺序执行。
- 适用场景:适用于需要保证任务按顺序执行且无需并发的场景。
(3) 创建可缓存的线程池
ExecutorService executor = Executors.newCachedThreadPool();
- 特点:线程池会根据需要动态创建线程,空闲线程会被回收(默认 60 秒无任务时回收)。
- 适用场景:适用于大量短期异步任务的场景,但不适合长期运行的任务,因为可能会导致线程过多。
(4) 创建定时任务线程池
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
- 特点:支持定时任务和周期性任务。
- 适用场景:适用于需要调度任务的场景。
2. 使用 ThreadPoolExecutor
手动创建线程池
虽然 Executors
提供了便捷的线程池创建方法,但在实际开发中,推荐使用 ThreadPoolExecutor
来手动创建线程池,因为它提供了更细粒度的配置选项。
示例代码:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 核心线程数
int corePoolSize = 5;
// 最大线程数
int maximumPoolSize = 10;
// 空闲线程存活时间
long keepAliveTime = 60L;
// 时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
// 线程工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
参数解析:
- corePoolSize:核心线程数,线程池中保持的最小线程数。
- maximumPoolSize:最大线程数,当任务队列满时,线程池最多可以扩展到的线程数。
- keepAliveTime:空闲线程的存活时间,超过这个时间后,多余的非核心线程会被回收。
- unit:
keepAliveTime
的时间单位。 - workQueue:任务队列,用于存放待执行的任务。
- threadFactory:线程工厂,用于创建线程。
- handler:拒绝策略,当线程池无法处理新任务时的处理方式。
3. 常见的拒绝策略
当线程池和任务队列都满时,新的任务将被拒绝,此时会触发拒绝策略。以下是几种常见的拒绝策略:
- AbortPolicy(默认):抛出
RejectedExecutionException
异常。 - CallerRunsPolicy:由调用线程执行该任务。
- DiscardPolicy:直接丢弃任务,不抛异常。
- DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试重新提交新任务。
什么是拒绝策略?
当线程池和任务队列都已满时(即:1. 线程池中的线程数已经达到最大线程数。2. 任务队列已满),新的任务无法被处理。此时,线程池会根据配置的 拒绝策略 决定如何处理新提交的任务。
拒绝策略具体应用
1. AbortPolicy
(默认策略)
- 行为:直接抛出
RejectedExecutionException
异常,表示任务被拒绝。 - 代码示例:
ExecutorService executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() // 默认策略 ); executor.submit(() -> { try { Thread.sleep(1000); // 模拟任务耗时 } catch (InterruptedException e) {} System.out.println("Task 1"); }); executor.submit(() -> System.out.println("Task 2")); executor.submit(() -> System.out.println("Task 3")); // 超过容量,触发拒绝策略
- 输出结果:
Task 1 Task 2 Exception in thread "main" java.util.concurrent.RejectedExecutionException: ...
- 适用场景:
- 希望在任务无法执行时立即发现问题并处理异常。
- 适用于对任务丢失零容忍的场景。
2. CallerRunsPolicy
- 行为:由调用线程(提交任务的线程)执行该任务。
- 代码示例:
ExecutorService executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy() ); executor.submit(() -> { try { Thread.sleep(1000); // 模拟任务耗时 } catch (InterruptedException e) {} System.out.println("Task 1"); }); executor.submit(() -> System.out.println("Task 2")); executor.submit(() -> System.out.println("Task 3")); // 超过容量,由主线程执行
- 输出结果:
Task 1 Task 2 Task 3
- 特点:
- 提交任务的线程(如主线程)会被阻塞,直到它完成任务的执行。
- 这是一种“降级”机制,可以防止任务丢失。
- 适用场景:
- 适用于任务量波动较大的场景,允许任务稍微延迟执行。
- 不适合对性能要求极高的场景,因为调用线程可能会被阻塞。
3. DiscardPolicy
- 行为:直接丢弃任务,且不抛出任何异常。
- 代码示例:
ExecutorService executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy() ); executor.submit(() -> { try { Thread.sleep(1000); // 模拟任务耗时 } catch (InterruptedException e) {} System.out.println("Task 1"); }); executor.submit(() -> System.out.println("Task 2")); executor.submit(() -> System.out.println("Task 3")); // 被丢弃,无任何提示
- 输出结果:
Task 1 Task 2
- 特点:
- 任务被静默丢弃,不会有任何反馈。
- 适用于对任务丢失不敏感的场景。
- 适用场景:
- 任务优先级较低,或者任务丢失对系统影响较小的情况。
- 不适合需要高可靠性的场景。
4. DiscardOldestPolicy
- 行为:丢弃任务队列中最老的任务(最早进入队列的任务),然后尝试重新提交当前任务。
- 代码示例:
ExecutorService executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy() ); executor.submit(() -> { try { Thread.sleep(1000); // 模拟任务耗时 } catch (InterruptedException e) {} System.out.println("Task 1"); }); executor.submit(() -> System.out.println("Task 2")); executor.submit(() -> System.out.println("Task 3")); // 触发 DiscardOldestPolicy
- 输出结果:
Task 1 Task 3
- 特点:
- 最老的任务被丢弃,新任务有机会被执行。
- 可能导致某些任务永远无法执行。
- 适用场景:
- 任务有时间敏感性,较新的任务比旧任务更重要。
- 不适合对任务完整性有严格要求的场景。
触发条件
拒绝策略会在以下情况下触发:
- 线程池已达到最大线程数:线程池中的线程都在忙碌。
- 任务队列已满:新任务无法加入队列等待。
总结对比
策略名称 | 行为 | 适用场景 |
---|---|---|
AbortPolicy | 抛出异常,拒绝任务 | 需要快速发现问题并对异常进行处理的场景 |
CallerRunsPolicy | 由调用线程执行任务 | 允许任务稍有延迟,但不能丢失的场景 |
DiscardPolicy | 静默丢弃任务 | 对任务丢失不敏感的场景 |
DiscardOldestPolicy | 丢弃最老的任务,重试新任务 | 新任务比旧任务更重要的场景 |
4. 注意事项
-
避免使用
Executors
默认线程池
Executors
提供的线程池在某些场景下可能导致内存泄漏或性能问题,例如newFixedThreadPool
和newSingleThreadExecutor
使用的是无界队列,可能会导致任务堆积,最终耗尽内存。 -
合理设置线程池参数
根据任务类型(CPU 密集型或 I/O 密集型)和硬件资源(CPU 核心数)合理设置线程池大小。例如:- CPU 密集型任务:线程数 ≈ CPU 核心数 + 1。
- I/O 密集型任务:线程数可以适当增加,以充分利用等待时间。
-
及时关闭线程池
使用完线程池后,务必调用shutdown()
或shutdownNow()
方法释放资源。
5. 线程池中的任务提交方式
在线程池中,任务的提交方式主要依赖于 Runnable
和 Callable
接口,而不是直接创建线程。以下是两种常见的任务提交方式:
(1) 提交 Runnable
任务
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
System.out.println("Task running: " + Thread.currentThread().getName());
});
executor.shutdown();
Runnable
任务不返回结果,适用于不需要获取任务执行结果的场景。
(2) 提交 Callable
任务
ExecutorService executor = Executors.newFixedThreadPool(5);
Future<Integer> future = executor.submit(() -> {
System.out.println("Task running: " + Thread.currentThread().getName());
return 42; // 返回结果
});
System.out.println("Result: " + future.get()); // 获取返回值
executor.shutdown();
Callable
任务返回结果,适用于需要获取任务执行结果的场景。Future
对象用于获取任务的返回值或检查任务状态。
当使用线程池时,不需要手动创建线程。线程池已经为你管理了线程的生命周期,你只需要关注任务逻辑即可。具体来说:
- 如果任务实现了
Runnable
接口,可以通过submit()
或execute()
方法提交任务。 - 如果任务实现了
Callable
接口,可以通过submit()
方法提交任务,并通过Future
获取结果。
换句话说,在使用线程池的情况下,传统的创建线程的方式(如继承 Thread
类或直接创建 Thread
对象)已经被线程池的机制所取代。
关系总结
- 线程 是基础,是多线程的基本组成单位。
- 多线程 是目标,通过多个线程并发执行任务。
- 线程池 是多线程的优化工具,用于高效管理线程。
为什么推荐使用线程池?
-
性能优化
线程的创建和销毁开销较大,线程池通过复用线程显著减少了这种开销。 -
资源控制
线程池可以限制并发线程数,避免系统资源耗尽。 -
任务管理
线程池提供了任务队列、拒绝策略等机制,方便管理和调度任务。 -
代码简洁
使用线程池后,代码更加清晰,无需手动管理线程的生命周期。
通俗比喻
- 线程:像工人,负责干活。
- 多线程:雇佣多个工人同时干活。
- 线程池:提前组建一个工人团队,按需分配任务,避免频繁招聘和辞退。
线程是基础,多线程是目标,线程池是实现多线程的高效工具。