目录
1. wait()/notify()/notifyAll()
3、拒绝策略(RejectedExecutionHandler)
一、核心概念
1. 进程 vs 线程
- 进程:程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
- 线程:进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程。
2. 并行 vs 并发
- 并行:多个任务在同一时刻同时执行(多核 CPU)。
- 并发:多个任务在同一时间段内交替执行(单核 CPU 通过时间片切换实现)。
3. 线程状态
Java 线程有 6 种状态(Thread.State
):
状态名称 | 描述 |
---|---|
NEW | 线程对象已创建,但尚未调用start() 方法。 |
RUNNABLE | 线程正在 Java 虚拟机中执行,或者准备就绪等待 CPU 时间片。 |
BLOCKED | 线程正在等待获取监视器锁(如synchronized 块),处于阻塞状态。 |
WAITING | 线程无限期等待另一个线程执行特定操作(如调用wait() 、join() )。 |
TIMED_WAITING | 线程在指定时间内等待另一个线程执行操作(如Thread.sleep() )。 |
TERMINATED | 线程已执行完毕,终止运行。 |
4. 状态转换图
二、线程创建与启动
在 Java 中,创建线程主要有三种方式,分别是继承Thread
类、实现Runnable
接口以及使用Callable
和Future
。下面将详细介绍这三种方式及其区别。
1、继承 Thread 类
步骤:
- 定义一个类继承自
Thread
类。 - 重写
run()
方法,在该方法中定义线程要执行的任务。 - 创建该类的实例,并调用
start()
方法启动线程。
示例代码:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread类创建的线程正在执行,线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行完毕");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
System.out.println("主线程继续执行");
}
}
特点:
- 优点:实现简单,直接通过继承
Thread
类并重写run()
方法即可。 - 缺点:由于 Java 是单继承的,继承了
Thread
类后就无法再继承其他类,会导致类的扩展性受限。
2、实现 Runnable 接口
步骤:
- 定义一个类实现
Runnable
接口。 - 实现
run()
方法,在该方法中定义线程要执行的任务。 - 创建该类的实例,并将其作为参数传递给
Thread
类的构造函数。 - 调用
Thread
实例的start()
方法启动线程。
示例代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口创建的线程正在执行,线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行完毕");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
System.out.println("主线程继续执行");
}
}
特点:
优点:避免了单继承的限制,一个类可以在实现Runnable
接口的同时继承其他类,提高了类的扩展性。适合多个线程共享同一个资源的场景,例如多个线程同时操作同一个银行账户。
- 缺点:代码相对复杂一些,需要创建
Runnable
实现类的实例并将其传递给Thread
类。
3、使用 Callable 和 Future
步骤:
- 定义一个类实现
Callable
接口,指定返回值的类型。 - 实现
call()
方法,在该方法中定义线程要执行的任务,并返回结果。 - 创建
ExecutorService
线程池。 - 调用线程池的
submit()
方法提交Callable
任务,并获取Future
对象。 - 调用
Future
对象的get()
方法获取线程执行的结果。
示例代码:
import java.util.concurrent.*;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("实现Callable接口创建的线程正在执行,线程名:" + Thread.currentThread().getName());
Thread.sleep(1000);
return 100; // 返回计算结果
}
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(1);
// 提交Callable任务
Future<Integer> future = executor.submit(new MyCallable());
try {
// 获取线程执行结果(可能会阻塞,直到任务完成)
Integer result = future.get();
System.out.println("线程执行结果:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// 关闭线程池
executor.shutdown();
}
System.out.println("主线程继续执行");
}
}
特点:
- 优点:
Callable
接口的call()
方法可以返回结果,并且可以抛出异常,这是Runnable
接口所不具备的功能。通过Future
对象可以方便地获取线程的执行结果,还可以检查线程是否完成、取消线程等操作。 - 缺点:代码复杂度较高,需要使用线程池来执行任务,适合需要返回结果的异步任务场景。
4、三种方式的对比
方式 | 继承 Thread 类 | 实现 Runnable 接口 | 实现 Callable 接口 |
---|---|---|---|
启动方式 | 直接调用start() 方法 | 需要创建Thread 对象 | 需要通过线程池执行 |
是否支持返回结果 | 否 | 否 | 是(通过Future 获取) |
是否支持异常抛出 | 否 | 否 | 是 |
是否受单继承限制 | 是 | 否 | 否 |
适用场景 | 简单的线程任务 | 共享资源的多线程任务 | 需要返回结果的异步任务 |
- 如果需要线程执行的任务简单,且不需要返回结果,可以选择继承
Thread
类的方式。 - 如果需要多个线程共享同一个资源,或者为了避免单继承的限制,建议选择实现
Runnable
接口的方式。 - 如果线程执行的任务需要返回结果,或者需要抛出异常,那么应该选择实现
Callable
接口的方式,并结合线程池使用。
三、线程安全与同步机制
1. 线程安全问题
当多个线程同时访问共享资源时,可能出现以下问题:
- 竞态条件(Race Condition):多个线程对共享数据的读写操作导致结果不确定。
- 内存可见性问题:一个线程修改了共享变量,其他线程可能看不到最新值。
2. synchronized 关键字
- 同步方法:
public synchronized void increment() { count++; }
- 同步代码块:
public void increment() { synchronized (this) { count++; } }
3. ReentrantLock(JUC 包)
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
4. 原子类(AtomicXXX)
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}
}
四、线程间协作
1. wait()/notify()/notifyAll()
public class ProducerConsumer {
private final Object lock = new Object();
private int data = 0;
private boolean available = false;
public void produce(int value) throws InterruptedException {
synchronized (lock) {
while (available) {
lock.wait(); // 等待消费者取走数据
}
data = value;
available = true;
lock.notifyAll(); // 通知消费者数据已就绪
}
}
public int consume() throws InterruptedException {
synchronized (lock) {
while (!available) {
lock.wait(); // 等待生产者生产数据
}
available = false;
lock.notifyAll(); // 通知生产者数据已取走
return data;
}
}
}
2. Condition 接口
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final java.util.LinkedList<Integer> buffer = new java.util.LinkedList<>();
private static final int MAX_SIZE = 10;
public void put(int value) throws InterruptedException {
lock.lock();
try {
while (buffer.size() == MAX_SIZE) {
notFull.await(); // 缓冲区满,等待
}
buffer.add(value);
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (buffer.isEmpty()) {
notEmpty.await(); // 缓冲区空,等待
}
int value = buffer.poll();
notFull.signal(); // 通知生产者
return value;
} finally {
lock.unlock();
}
}
}
五、线程池(ExecutorService)
线程池可以对线程进行统一管理和复用,避免频繁创建和销毁线程带来的性能开销。Java 通过ExecutorService
接口和Executors
工厂类来创建线程池。
1. 线程池的优势
- 降低资源消耗
避免频繁创建和销毁线程,重复利用已创建的线程。 - 控制并发数量
通过设置核心线程数、最大线程数等参数,防止因线程过多导致的资源竞争和阻塞。 - 简化线程管理
提供统一的任务提交接口(如submit()
),并支持任务优先级、定时任务等高级功能。 - 提高响应速度
线程池中的线程可直接复用,无需等待新线程创建。
2.线程池的五种状态
1. RUNNING(运行中)
- 状态值:
111
(二进制,高 3 位)。 - 含义:
- 线程池正常运行,接受新任务,并处理阻塞队列中的任务。
- 初始状态即为
RUNNING
。
- 转换来源:
- 新建线程池时自动进入此状态。
2. SHUTDOWN(关闭中)
- 状态值:
000
(二进制,高 3 位)。 - 含义:
- 不再接受新任务,但会继续处理阻塞队列中已有的任务。
- 调用
shutdown()
方法会触发此状态转换。
- 转换来源:
- 从
RUNNING
状态调用shutdown()
。
- 从
3. STOP(停止中)
- 状态值:
001
(二进制,高 3 位)。 - 含义:
- 立即停止线程池:
- 中断所有正在执行的任务。
- 丢弃阻塞队列中未处理的任务。
- 调用
shutdownNow()
方法会触发此状态转换。
- 立即停止线程池:
- 转换来源:
- 从
RUNNING
或SHUTDOWN
状态调用shutdownNow()
。
- 从
4. TIDYING(整理中,过渡状态)
- 状态值:
010
(二进制,高 3 位)。 - 含义:
- 所有任务(包括正在执行的和队列中的)已终止,工作线程数为 0。
- 即将进入
TERMINATED
状态前的过渡状态。
- 触发条件:
- 当
SHUTDOWN
或STOP
状态下,任务队列和工作线程均为空时,自动进入此状态。
- 当
5. TERMINATED(终止)
- 状态值:
011
(二进制,高 3 位)。 - 含义:
- 线程池已完全终止,所有资源已释放。
- 触发条件:
- 当
TIDYING
状态下,执行完终止钩子函数(terminated()
方法)后,进入此状态。
- 当
3. 线程池状态转换
4、核心类:ThreadPoolExecutor
Java 线程池的核心实现类是ThreadPoolExecutor
,通过构造方法可自定义参数,灵活控制线程池行为。
构造方法参数详解:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数:线程池长期维持的最小线程数,即使空闲也不会销毁
int maximumPoolSize, // 最大线程数:线程池允许创建的最大线程数
long keepAliveTime, // 存活时间:非核心线程空闲时的存活时长
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列:存放待执行的任务
ThreadFactory threadFactory, // 线程工厂:创建线程的工厂(可自定义线程名、优先级等)
RejectedExecutionHandler handler // 拒绝策略:任务无法执行时的处理逻辑
)
参数配置逻辑:
-
任务提交流程:
- 当线程数 <
corePoolSize
:创建新线程执行任务。 - 当线程数 ≥
corePoolSize
:任务加入workQueue
队列。 - 当队列已满且线程数 <
maximumPoolSize
:创建非核心线程执行任务。 - 当队列已满且线程数 ≥
maximumPoolSize
:触发拒绝策略。
- 当线程数 <
-
队列类型选择:
- 有界队列(如 ArrayBlockingQueue):避免内存溢出,需配合拒绝策略使用。
- 无界队列(如 LinkedBlockingQueue):
maximumPoolSize
参数失效(线程数不超过corePoolSize
),可能导致 OOM。 - 优先队列(如 PriorityBlockingQueue):按任务优先级执行。
5、线程池的创建方式
1. 使用 Executors 工厂类(简化但有局限性)
-
FixedThreadPool(固定大小线程池):
ExecutorService fixedPool = Executors.newFixedThreadPool(5); // corePoolSize = maxPoolSize = 5,使用无界队列
适用场景:任务量已知、负载均衡的场景(如数据库连接池)。
风险:无界队列可能导致内存溢出。 -
CachedThreadPool(可缓存线程池):
ExecutorService cachedPool = Executors.newCachedThreadPool(); // corePoolSize=0,maxPoolSize=Integer.MAX_VALUE
特点:线程空闲 60 秒后销毁,适合短时间任务(如 HTTP 请求处理)。
风险:线程数可能无限增长,导致 OOM。 -
ScheduledThreadPool(定时任务线程池):
适用场景:定时任务、周期性任务(如日志上报)。ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3); // 延迟1秒执行任务 scheduledPool.schedule(() -> System.out.println("延迟执行"), 1, TimeUnit.SECONDS);
-
SingleThreadExecutor(单线程池):
适用场景:需要保证任务顺序执行或串行执行的场景。ExecutorService singlePool = Executors.newSingleThreadExecutor(); // 保证任务按顺序执行
2. 自定义 ThreadPoolExecutor(推荐)
// 自定义有界队列和拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
30, // 非核心线程存活时间(秒)
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10), // 有界队列(容量10)
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用者线程执行任务
);
3、拒绝策略(RejectedExecutionHandler)
当任务无法被线程池处理时(队列已满且线程数达上限),触发拒绝策略,共有 4 种内置策略:
策略名称 | 行为描述 |
---|---|
AbortPolicy(默认) | 抛出RejectedExecutionException ,终止任务执行(需手动捕获异常)。 |
CallerRunsPolicy | 由提交任务的线程(如主线程)直接执行任务,降低提交任务的速度。 |
DiscardPolicy | 静默丢弃无法处理的任务,不抛出异常(可能丢失数据)。 |
DiscardOldestPolicy | 丢弃队列中最旧的任务,尝试提交当前任务(可能导致重要任务被丢弃)。 |
4、自定义策略
class CustomRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义处理逻辑(如记录日志、写入数据库)
System.out.println("任务被拒绝:" + r.toString());
}
}
5、线程池的生命周期管理
-
关闭线程池:
shutdown()
:平滑关闭,不再接受新任务,但会等待已提交的任务执行完毕。shutdownNow()
:立即关闭,尝试中断正在执行的任务,并返回等待执行的任务列表。
executor.shutdown(); // 推荐使用此方式,避免任务丢失 try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 等待60秒 executor.shutdownNow(); // 超时后强制关闭 } } catch (InterruptedException e) { executor.shutdownNow(); }
6.最佳实践与注意事项
1. 线程数配置策略
- 计算密集型任务:
线程数 = CPU 核心数(如Runtime.getRuntime().availableProcessors()
),避免上下文切换开销。 - IO 密集型任务:
线程数 = CPU 核心数 × 2(或更高),因 IO 等待时线程可释放 CPU 资源。
公式参考:线程数 = CPU 核心数 × (1 + 平均等待时间 / 平均工作时间) - 通用原则:
健壮的线程池设计 = 有界队列 + 明确拒绝策略 + 合理线程数
2. 避免使用无界队列
Executors.newFixedThreadPool()
和newSingleThreadExecutor()
默认使用LinkedBlockingQueue
(无界队列),可能导致内存耗尽,建议自定义有界队列:
// 错误示例(无界队列)
ExecutorService badPool = Executors.newFixedThreadPool(5);
// 正确示例(有界队列+拒绝策略)
ExecutorService goodPool = new ThreadPoolExecutor(
5, 5, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100), // 限制队列容量
new ThreadPoolExecutor.AbortPolicy()
);
3. 优先使用线程池而非手动创建线程
// 推荐:使用线程池复用线程
executor.submit(() -> processTask());
// 不推荐:手动创建线程(缺乏管理)
new Thread(() -> processTask()).start();
4. 异常处理
- 任务中抛出的未捕获异常会导致线程终止,需在
run()
或call()
中捕获异常,或通过Future
处理:Future<?> future = executor.submit(() -> { if (hasError()) { throw new RuntimeException("任务失败"); // 需通过future.get()捕获ExecutionException } }); try { future.get(); // 捕获异常 } catch (ExecutionException e) { handleError(e.getCause()); }
7、典型应用场景
- Web 服务器请求处理:
每个 HTTP 请求作为任务提交到线程池,避免为每个请求创建新线程。 - 异步数据处理:
批量处理数据(如日志解析、文件上传),利用线程池并行加速。 - 定时任务调度:
使用ScheduledThreadPoolExecutor
实现定时统计、缓存更新等功能。 - 分布式系统中的任务队列:
结合消息中间件(如 Kafka),使用线程池消费消息,控制并发量。
六、高级工具类(JUC 包)
1. CountDownLatch
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int taskId = i;
new Thread(() -> {
System.out.println("任务" + taskId + "开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务" + taskId + "执行完毕");
latch.countDown(); // 计数器减1
}).start();
}
latch.await(); // 主线程等待所有任务完成
System.out.println("所有任务已完成");
}
}
2. CyclicBarrier
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
System.out.println("所有线程已到达屏障");
});
for (int i = 0; i < parties; i++) {
final int threadId = i;
new Thread(() -> {
try {
System.out.println("线程" + threadId + "正在准备");
Thread.sleep(1000);
System.out.println("线程" + threadId + "已准备好,等待其他线程");
barrier.await(); // 等待其他线程
System.out.println("线程" + threadId + "继续执行");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
3. Semaphore
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int permits = 2; // 允许2个线程同时访问
Semaphore semaphore = new Semaphore(permits);
for (int i = 0; i < 5; i++) {
final int threadId = i;
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println("线程" + threadId + "获取到许可,开始执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
System.out.println("线程" + threadId + "释放许可");
}
}).start();
}
}
}
七、线程安全集合类
1. 传统集合的线程安全问题
// 非线程安全的集合
List<String> list = new ArrayList<>();
// 线程安全的替代方案
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
2. JUC 包中的线程安全集合
// 高效的线程安全队列
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
// 并发Map(分段锁实现)
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
八、多线程设计模式
1. 生产者 - 消费者模式
使用BlockingQueue
实现:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerPattern {
private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
public static void main(String[] args) {
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put(i);
System.out.println("生产:" + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
while (true) {
Integer item = queue.take();
System.out.println("消费:" + item);
if (item == 9) break; // 结束条件
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
2. 单例模式(双重检查锁)
public class Singleton {
private static volatile Singleton instance; // 使用volatile保证可见性
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
九、线程调试与性能优化
1. 线程调试工具
- jstack:打印线程堆栈信息,用于分析死锁和阻塞问题。
- VisualVM:可视化监控工具,查看线程状态、CPU 使用率等。
- ThreadMXBean:编程方式获取线程信息。
2. 性能优化建议
- 减少锁的粒度,避免
synchronized
块过大。 - 使用无锁数据结构(如
ConcurrentHashMap
)替代传统同步集合。 - 合理配置线程池大小:
计算密集型任务:线程数 = CPU核心数 + 1 IO密集型任务:线程数 = CPU核心数 * 2
十、注意事项
-
避免死锁:
1. 按相同顺序获取锁。
2. 设置锁获取超时时间。 -
线程池使用规范:
1.避免使用无界队列(如Executors.newFixedThreadPool
)。
2. 明确拒绝策略(如CallerRunsPolicy
)。 -
内存泄漏:
1. 静态集合持有线程引用。
2. 未正确关闭资源(如线程池未调用shutdown()
)。
多线程编程是 Java 开发中的核心技能,掌握线程安全、同步机制和高级工具类的使用,能够显著提升系统的性能和稳定性。在实际开发中,应根据具体场景选择合适的并发模型和工具,避免过度设计。