Java基础知识之多线程
熊猫
在实际开发中,很多时候都需要使用到多线程来提升系统的服务性能,那么如何正确的使用多线程就是一个值得思考的问题。
一、多线程基础
-
进程与线程基本概念 进程: 进程是程序的一次动态执行过程(进程类比人的生命,生命就是人的一生动态生活的过程),是操作系统进行==资源分配和调度的基本单位== 资源分配:分配独立的内存空间、CPU 时间片、文件描述符等资源。 特点:独立性、资源独占、开销较大、隔离性 独立性:每个进程拥有独立的内存空间(代码段、数据段、堆等),进程间默认不能直接访问对方的内存。 资源独占:进程是资源分配的最小单位,拥有操作系统分配的独立资源(如内存、文件句柄)。 开销较大:创建、销毁进程时,操作系统需要分配 / 释放资源,切换进程时需要==保存和恢复整个进程的状态==,因此开销较高。 隔离性:一个进程崩溃通常不会影响其他进程(如浏览器的一个标签页崩溃,其他标签页可正常运行)。 线程: 线程是进程内的一个执行单元,是CPU最小调度单位。 一个进程可以包含多个线程,这些线程是共享进程的内存空间和资源(如堆内存、文件句柄),但每个线程有自己独立的执行栈和程序计数器。 特点:共享性、轻量级、依赖性、协作性 共享性:同一进程内的线程共享进程的资源(内存、文件等),因此线程间通信更高效(直接读写共享内存即可)。 轻量级:线程本身不拥有资源,仅需少量栈空间,创建、销毁和切换的开销远小于进程。 依赖性:线程不能独立存在,必须依赖于进程。一个进程的所有线程都结束后,进程才会终止。 协作性:同一进程内的线程协作紧密,但也因共享资源可能导致线程安全问题(如多个线程同时修改共享变量)。
-
线程与进程的关系
- ==一个进程至少包含一个线程==(称为 “主线程”,如 Java 程序的
main方法线程)。 - 进程是资源分配的单位,线程是 CPU 调度的单位。
- 进程间通过 IPC(进程间通信,如管道、Socket)交互,线程间通过共享内存交互。
- 进程崩溃不影响其他进程,但线程崩溃可能导致整个进程崩溃(因为共享资源)。
- ==一个进程至少包含一个线程==(称为 “主线程”,如 Java 程序的
-
创建线程的方式(不包含线程池)
// 通过继承Thread类型,并且重写run()方法,后续直接new该类,调用start()方法
public class T1 extends Thread{
@Override
public void run() {
// 具体任务
}
}
// 通过实现Runnable接口,并且实现run()方法,后续作为Thread类的初始化参数,调用start()方法
public class T2 implements Runnable{
@Override
public void run() {
// 具体任务
}
}
// 通过实现Callable接口,并且实现run()方法,后续作为Thread类的初始化参数,调用start()方法
public class T3 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 具体任务
return 0;
}
}
二、线程同步与并发安全
-
线程安全问题的根源
- 共享资源竞争: 当多个线程同时操作共享资源(全局变量、静态变量、堆内存中的对象)时,若操作没有进行控制,可能会导致数据不一致,核心原因就是==违背了并发编程的三大特性==。 可见性问题: 当一个线程修改了共享资源后,其它线程不能及时感知到共享资源最新状态,例如:线程A修改了变量count,但未刷新到主内存中,线程B就会读取到旧值。 原子性问题: 线程修改共享资源的操作可能有多个步骤构成,例如:i++,在CPU层面可能拆分为:“读取-修改-写入” 三个步骤,若被其它线程打断,会导致数据修改错误。 有序性问题: CPU为优化性能可能对指令进行重排,例如:obj = new Object(); flag = true; 可能被重排为:flag = true; obj = new Object();导致其它线程读取到flag=true,但是obj还未初始化。
-
同步机制(
synchronized关键字)- 作用范围
通过加锁保证同一时间只有一个线程执行特定代码块,解决共享资源竞争问题。
// 实例同步方法:锁对象为当前实例(this) public synchronized void method() { ... } // 静态同步方法:锁对象为当前类的 Class 对象(.class) public static synchronized void staticMethod() { ... } // 锁对象可以是任意对象(推荐使用 private final 对象作为锁,避免锁对象被修改) private final Object lock = new Object(); public void method() { synchronized (lock) { } } - 锁对象
- 实例锁(this):同步实例方法或
synchronized (this)时,锁为当前对象实例。==不同实例间的锁互不干扰。== - 类锁(.class):同步静态方法或
synchronized (Xxx.class)时,锁为类的 Class 对象。==所有实例共享同一把锁。==
- 实例锁(this):同步实例方法或
- 底层原理:
- 依赖 Monitor(监视器锁) 实现,每个 Java 对象都关联一个 Monitor(可理解为 “锁标记”)。
- 线程进入同步代码前需获取 Monitor 的所有权:
- 若 Monitor 未被占用,当前线程直接获取(锁计数器 = 1)。
- 若已被占用,线程进入阻塞队列等待。
- 线程退出同步代码时释放 Monitor(锁计数器 = 0),唤醒阻塞队列中的线程竞争锁。
- JDK 对
synchronized进行了多次优化(如偏向锁、轻量级锁、重量级锁的升级机制),性能已接近Lock。
- 特性: 可重入性:同一线程可多次获取同一把锁(计数器累加),避免自己锁死自己。 自动释放锁:无论正常退出还是抛出异常,JVM 都会自动释放锁,无需手动操作。
- 作用范围
通过加锁保证同一时间只有一个线程执行特定代码块,解决共享资源竞争问题。
-
显示锁(AQS) 1. ReentrantLock 2. ReadWriteLock
| 特性 | synchronized | Lock |
| -------- | ------------------------------- | ------------------------------------- |
| 锁获取 / 释放 | 自动(JVM 管理) | 手动(需 lock()/unlock(),建议放 finally) |
| 公平性 | 仅支持非公平锁 | 可选择公平 / 非公平锁 |
| 可中断性 | 不支持(等待锁的线程无法被中断) | 支持(lockInterruptibly()) |
| 超时获取 | 不支持 | 支持(tryLock(timeout)) |
| 条件变量 | 仅通过 wait()/notify() 实现,功能有限 | 支持 Condition,可创建多个条件队列 |
| 性能 | JDK 1.6+ 优化后与 Lock 接近 | 高并发场景下更灵活,性能略优 |
简单场景可以使用synchronized,需要公平锁、超时控制、中断响应或多条件队列是,用Lock
- 线程同步辅助工具 volatile关键字:==可以保证可见性、有序性,但是无法保证原子性== CAS(Compare And Swap,比较并交换) : 操作实现==无锁原子性==,避免锁竞争的性能开销 优点:无锁机制,高并发下性能优于锁。 缺点:仅适用于简单原子操作,复杂逻辑需组合多个原子操作(可能仍需锁);==存在 “自旋” 开销==(多次 CAS 失败时循环重试)。
三、线程通信与协作
- 线程之间的通信方式
- wait()/notify()/notifyAll()机制(基于对象锁)
核心作用:让线程在特定条件下等待,当条件满足时被唤醒,实现线程间的 “等待 - 唤醒” 协作。
使用前提:必须在==
synchronized同步代码块 / 方法中使用==(需先获取对象锁,否则抛出IllegalMonitorStateException)。 核心方法: wait()方法:当前线程释放对象锁,进入该对象的等待队列并阻塞,直到被notify()/notifyAll()唤醒或被中断。 notify()方法:==随机唤醒==对象等待队列中的一个线程,使其进入就绪状态(需重新竞争锁才能继续执行)。 notifyAll()方法:唤醒对象等待队列中的所有线程,使其进入就绪状态(重新竞争锁)。 - Condition接口(结合 Lock 实现灵活通信)
核心作用:
Lock对应的 “条件变量”,功能类似wait()/notify(),但支持多个独立的等待队列,更灵活。 获取方式:通过Lock.newCondition()创建,一个Lock可创建多个Condition(对应不同条件) 核心方法: await()方法:类似wait(),释放锁并进入当前Condition的等待队列。 signal()方法:唤醒当前Condition等待队列中的一个线程。 signalAll()方法:唤醒当前Condition等待队列中的所有线程。
- wait()/notify()/notifyAll()机制(基于对象锁)
核心作用:让线程在特定条件下等待,当条件满足时被唤醒,实现线程间的 “等待 - 唤醒” 协作。
使用前提:必须在==
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // 消费者等待的条件(队列非空)
Condition notFull = lock.newCondition(); // 生产者等待的条件(队列不满)
// 消费者线程
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空,等待“非空”信号
}
// 消费数据
notFull.signal(); // 通知生产者队列不满
} finally {
lock.unlock();
}
- 线程协作工具类(AQS)
-
CountDownLatch(倒计时器) 核心作用:让一个或多个线程等待其他多个线程完成特定操作后再执行。 工作原理:
- 初始化一个计数器(如
new CountDownLatch(3)表示等待 3 个线程)。 - 被等待的线程完成后调用
countDown(),计数器减 1。 - 等待线程调用
await()阻塞,直到计数器变为 0 后唤醒。
// 案例:主线程等待3个子线程完成 CountDownLatch latch = new CountDownLatch(3); // 子线程执行任务 for (int i = 0; i < 3; i++) { new Thread(() -> { try { // 执行任务(如加载资源) Thread.sleep(1000); } finally { latch.countDown(); // 任务完成,计数器减 1 } }).start(); } // 主线程等待所有子线程完成 latch.await(); // 阻塞直到计数器为 0 System.out.println("所有任务完成,继续执行主线程");特点:
- 计数器只能递减,一旦变为 0 就不可重置(一次性使用)。
- 支持超时等待(
await(long timeout, TimeUnit unit))。
- 初始化一个计数器(如
-
CyclicBarrier(循环屏障) 核心作用:让一个或多个线程等待其他多个线程完成特定操作后再执行。 工作原理:
- 初始化屏障 parties(参与线程数)和屏障动作(
Runnable,所有线程到达后执行)。 - 每个线程到达屏障时调用
await(),计数器减 1,直到所有线程到达后,执行屏障动作,然后所有线程被唤醒。 适用场景:多线程分阶段协作(如多线程计算数据分片,所有分片完成后汇总结果)。
// 4 个线程到达屏障后,先执行屏障动作(打印提示) CyclicBarrier barrier = new CyclicBarrier(4, () -> System.out.println("所有线程已到达,开始下一步") ); for (int i = 0; i < 4; i++) { new Thread(() -> { try { // 执行前置任务 Thread.sleep((long) (Math.random() * 1000)); System.out.println(Thread.currentThread().getName() + " 到达屏障"); barrier.await(); // 等待其他线程 // 所有线程到达后,执行后续任务 System.out.println(Thread.currentThread().getName() + " 继续执行"); } catch (Exception e) { ... } }).start(); } - 初始化屏障 parties(参与线程数)和屏障动作(
-
Semaphore(信号量) 核心作用:控制同时访问某个资源的线程数量(限流),通过 “许可证” 机制实现。 工作原理:
- 初始化许可证数量(如
new Semaphore(5)允许 5 个线程同时访问)。 - 线程获取资源前调用
acquire()申请许可证(无可用则阻塞)。 - 线程释放资源后调用
release()归还许可证(唤醒等待的线程)。 适用场景:限流(如控制数据库连接池的并发连接数)、保护有限资源(如打印机共享)。
// 限制 3 个线程同时访问资源 Semaphore semaphore = new Semaphore(3); // 3 个许可证 for (int i = 0; i < 10; i++) { new Thread(() -> { try { semaphore.acquire(); // 申请许可证(最多 3 个线程同时进入) System.out.println(Thread.currentThread().getName() + " 访问资源"); Thread.sleep(1000); // 模拟资源访问 } finally { semaphore.release(); // 归还许可证 } }).start(); } - 初始化许可证数量(如
-
四、线程池与并发框架
线程池是管理线程生命周期的容器,通过复用线程减少创建 / 销毁线程的开销,控制并发线程数量,避免资源耗尽
1. 为什么需要使用线程池?
- 降低资源消耗:线程创建 / 销毁需要分配栈内存、切换 CPU 状态等开销,线程池通过复用线程减少这些操作。
- 提高响应速度:任务到达时可直接使用空闲线程,无需等待线程创建。
- 控制并发数量:避免无限制创建线程导致的 CPU 过载、内存耗尽(如千级线程同时运行可能引发 OOM)。
- 便于管理监控:统一管理线程生命周期,支持任务统计、超时控制等功能。
2.线程池核心参数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
1. 核心参数详解:
- corePoolSize(核心线程数):线程池长期保留的线程数量(即使空闲也不销毁,除非设置 allowCoreThreadTimeOut)。
- maximumPoolSize(最大线程数):线程池允许创建的最大线程数(核心线程 + 非核心线程)。
- keepAliveTime + unit:非核心线程空闲超过该时间后会被销毁,节省资源。
- workQueue(任务队列):用于存放待执行任务的阻塞队列(如 ArrayBlockingQueue、LinkedBlockingQueue)。
- threadFactory(线程工厂):自定义线程创建(如设置线程名、优先级、是否为守护线程)。
- handler(拒绝策略):任务队列满且线程数达最大值时,处理新任务的策略。
2. 任务提交流程
当提交一个任务时:
1. 若当前线程数 < corePoolSize:立即创建核心线程执行任务。
2. 若当前线程数 ≥ corePoolSize:将任务加入 workQueue 等待。
3. 若 workQueue 已满,且当前线程数 < maximumPoolSize:创建非核心线程执行任务。
4. 若 workQueue 已满,且当前线程数 ≥ maximumPoolSize:触发拒绝策略。
3. 构造方法与参数配置
- 核心参数配置原则:
- CPU 密集型任务(如计算):线程数 ≈ CPU 核心数 + 1(减少线程切换开销)。
- IO 密集型任务(如网络请求、文件读写):线程数 ≈ CPU 核心数 × 2(利用 IO 等待时间并行处理)。
- 队列选择:
- 有界队列(如
ArrayBlockingQueue):避免任务无限堆积导致 OOM,适合资源有限的场景。 - 无界队列(如
LinkedBlockingQueue):任务可无限排队,但可能因内存溢出崩溃(需谨慎使用)。
- 有界队列(如
4. 任务提交:execute() vs submit()
-
execute(Runnable):- 用于提交无返回值的任务。
- 无法捕获任务异常(需在任务内部处理)。
-
submit(Runnable)/submit(Callable<T>):- 用于提交有返回值的任务(
Callable有返回值,Runnable可通过Future获取 null)。 - 返回
Future对象,可通过future.get()获取结果(会阻塞直到任务完成),或通过future.get(timeout)设置超时。 - 任务异常会被包装在
ExecutionException中,可通过future.get()捕获。
- 用于提交有返回值的任务(
5. 拒绝策略 当任务队列满且线程数达最大值时,触发拒绝策略,JDK 提供 4 种默认实现:
AbortPolicy(默认):直接抛出RejectedExecutionException,中断任务提交。CallerRunsPolicy:让提交任务的线程(调用者)自己执行任务,减缓提交速度(起到限流作用)。DiscardPolicy:默默丢弃新任务,不抛异常。DiscardOldestPolicy:丢弃队列中最旧的任务(队头),然后尝试提交新任务。 自定义拒绝策略(如记录日志并降级处理)
6. Executors 工具类创建的线程池及隐患
Executors 提供了快捷创建线程池的方法,但部分方法存在资源耗尽风险,不推荐在生产环境使用:
五、高级特性与并发问题
1.线程中断机制 线程中断是一种协作式机制,用于通知线程 “应该停止当前工作”,但线程可自主决定是否响应中断(避免暴力终止导致资源泄漏)。 核心方法与区别
| 方法 | 作用 | 特点 |
| -------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- |
| interrupt() | 给线程设置中断标记(interrupted status 为 true) | 不直接终止线程,仅发送中断信号;若线程处于阻塞状态(wait/sleep/join),会抛出 InterruptedException 并清除中断标记 |
| isInterrupted() | 判断当前线程的中断标记是否为 true | 不清除中断标记(多次调用结果一致,直到标记被修改) |
| Thread.interrupted() | 判断当前线程的中断标记是否为 true(静态方法) | 会清除中断标记(第一次调用后标记重置为 false,第二次调用返回 false) |
Thread t = new Thread(() -> {
while (true) {
// 检查中断标记(isInterrupted() 不清除标记)
if (Thread.currentThread().isInterrupted()) {
System.out.println("检测到中断,准备退出");
break;
}
}
});
t.start();
t.interrupt(); // 设置中断标记为 true
System.out.println(t.isInterrupted()); // true(标记未清除)
System.out.println(Thread.interrupted()); // false(当前线程是主线程,未被中断)
// 使用 volatile 变量控制 (适用于不响应中断的场景),线程在退出前必须释放资源(放在 finally 中),确保资源不泄漏。
volatile boolean isRunning = true;
Thread t = new Thread(() -> {
while (isRunning) {
// 执行任务
}
});
// 终止时设置 isRunning = false
2.死锁、活锁与饥饿
1. 死锁:
定义:两个或多个线程互相持有对方需要的锁,且都不释放,导致永久阻塞。
产生条件(缺一不可):
- 互斥条件:资源(锁)只能被一个线程持有。
- 持有并等待:线程持有一个资源,同时等待另一个资源。
- 不可剥夺:资源(锁)不能被强制剥夺,只能由持有者主动释放。
- 循环等待:线程间形成环状等待链(如 A 等 B 的锁,B 等 A 的锁)。
排查工具与方法
- jconsole/jvisualvm:图形化工具,在 “线程” 标签页可直接检测死锁。
- jstack 命令:打印线程栈,搜索 BLOCKED 状态的线程,查看等待的锁和持有者。
jps # 查看进程ID jstack 进程ID # 分析线程栈,死锁会被明确标记(如 "Found 1 deadlock.")
**2. 活锁(Livelock)**
- **定义**:线程未阻塞,但因过度谦让(如不断重试)导致任务无法推进(类似 “两人迎面让路,同时向同一方向避让”)。
- **示例**:两个线程发现锁冲突后立即释放锁并重试,导致无限循环。
- **解决方案**:
- 引入随机延迟:重试前随机休眠一段时间,避免同步谦让。
- 限制重试次数:超过次数后放弃或降级处理。
**3. 饥饿(Starvation)**
- **定义**:某些线程长期无法获取资源(如锁、CPU 时间片),导致任务无法执行。
- **常见原因**:
- 线程优先级差异:高优先级线程抢占所有 CPU 时间,低优先级线程饿死。
- 锁持有时间过长:一个线程长期持有锁,其他线程无法获取。
- 资源分配不均:如线程池核心线程数过少,非核心线程频繁被销毁。
- **解决方案**:
- 避免设置线程优先级(依赖操作系统调度,可能导致饥饿)。
- 减少锁持有时间,采用公平锁(`ReentrantLock(true)`)按顺序分配资源。
- 线程池使用合理的核心线程数,避免非核心线程频繁销毁。