一、 先搞懂几个基础概念
1. 等待 vs 阻塞:买钢笔的故事
假设小卖铺只剩一支钢笔了。
-
顾客A 先到,买走了钢笔。
-
顾客B 后到,也想要钢笔。
这时顾客B有两种选择:
-
阻塞 (Blocking): 顾客B就杵在柜台前,啥也不干,一直死等,直到老板进货上架新钢笔他立马买走。(线程占着位置不干活,干等资源)
-
等待 (Waiting): 顾客B跟老板说:“钢笔到了叫我一声!”,然后他就回家干别的事去了。等老板进货了,通知他,他再来买。(线程让出位置去做别的事或休眠,等通知唤醒)
关键点: 阻塞通常是系统自动做的(比如抢不到锁),等待通常是程序员主动让线程做的(比如调用 wait())。
2. 用户态 vs 内核态:办事的权限级别
想象一下公司有两个办公区域:
-
用户态 (User Mode): 普通员工的办公区。在这里,员工(应用程序)能做的事情有限,权限比较低。如果员工想做一些需要更高权限的事情(比如申请特殊设备、发重要通知),就得打报告。
-
内核态 (Kernel Mode): 公司核心管理层的办公区。这里拥有最高权限,可以访问所有硬件资源(CPU、内存、硬盘、网卡),执行所有指令。
线程切换为啥慢? 当线程需要做一件只有内核才能做的事(比如读写文件、网络通信、创建新线程),它就得从“普通员工区”(用户态)跑到“核心管理层区”(内核态)去申请。这个“跑腿”的过程(上下文切换)是比较耗时的。频繁切换就会降低效率。
图解示意 (想象一下):
[ 用户程序 (线程A, 线程B) ] [ 操作系统内核 (最高权限) ]
^ ^
| |
|--- 普通操作 (用户态) -------------------|
| |
|--- 需要硬件/核心资源 (切换到内核态) ------|
二、 Java线程:创建、状态与管理
1. Java线程 = 操作系统线程?
是的!Java线程 (java.lang.Thread) 在底层是通过调用操作系统的线程API(比如Linux的 pthread_create)来实现的。所以,Java程序创建的线程,本质上就是操作系统的线程,是 1:1 的线程模型。一个Java线程对应一个操作系统线程。
2. 多线程要注意啥?安全第一!
多个顾客(线程)同时操作同一个商品(共享数据),很容易乱套!比如:
-
数据竞争: 两个收银员同时修改同一个商品的库存,可能导致库存数算错。
-
可见性问题: 一个收银员改了价格标签,但另一个收银员没看到新标签,还在按旧价格卖。
为了保证线程安全(数据不乱),Java主要从三个方面入手:
-
原子性 (Atomicity): 一个操作要么全做完,要么一点不做,中间不能被其他线程打断。就像收银员扫码收款,要么扫完收完钱完成交易,要么没扫成,不能扫一半被另一个收银员打断。
-
工具:
synchronized关键字,java.util.concurrent.atomic包下的类 (如AtomicInteger)。
-
-
可见性 (Visibility): 一个线程修改了共享变量的值,其他线程能马上看到最新值。就像价格标签改了,所有收银员要立刻知道。
-
工具:
volatile关键字,synchronized。
-
-
有序性 (Ordering): 程序执行的顺序要符合我们的预期。编译器或处理器有时会优化指令顺序来提高效率(指令重排序),但在多线程下可能导致奇怪的结果。需要保证某些关键操作的顺序。
-
工具:
volatile(部分有序性),synchronized(完全有序性),Happens-Before 原则。
-
保证数据一致性方案:
-
加锁 (Locking): 最常用。像给收银台加把锁(
synchronized或ReentrantLock),同一时间只允许一个收银员(线程)操作。简单粗暴有效。 -
原子变量 (Atomic Variables): 对单个变量(如计数器)的操作保证原子性 (
AtomicInteger等),性能通常比锁好。 -
不可变对象 (Immutable Objects): 数据一旦创建就不能改。大家都只能读,自然安全!(比如
String)。 -
线程局部变量 (ThreadLocal): 给每个线程发一个专属小本本(
ThreadLocal),数据只写在自己的本本上,互相不干扰。 -
版本控制 (乐观锁): 常用于数据库。修改数据前先记下版本号,提交时检查版本号没变才更新(如
CAS或数据库version字段)。 -
事务 (Transactions): 数据库层面的保障,一组操作要么全成功,要么全失败回滚(ACID特性)。
3. 创建线程的四种姿势
姿势一:继承 Thread 类 (不推荐常用)
class MyThread extends Thread { // YA33提醒:继承Thread类
@Override
public void run() {
// 线程要执行的代码在这里
System.out.println("YA33的线程-继承方式运行了!");
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // 启动线程,注意是start()不是run()!
}
-
优点: 写法简单,在
run()方法里直接用this就能拿到当前线程对象。 -
缺点: Java是单继承,继承了
Thread就不能继承别的类了,限制了扩展性。
姿势二:实现 Runnable 接口 (最常用)
class MyRunnable implements Runnable { // YA33提醒:实现Runnable接口
@Override
public void run() {
// 线程要执行的代码在这里
System.out.println("YA33的线程-实现Runnable运行了!");
}
}
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable()); // 把Runnable任务传给Thread
t.start();
}
-
优点:
-
灵活!实现接口不影响继承其他类。
-
多个线程可以共享同一个
Runnable对象(比如操作同一份资源),代码和数据分离更清晰。
-
-
缺点: 在
run()方法里获取当前线程需要用Thread.currentThread()。
姿势三:实现 Callable 接口 + FutureTask (能返回结果)
import java.util.concurrent.*;
class MyCallable implements Callable { // YA33提醒:实现Callable
@Override
public Integer call() throws Exception {
// 线程要执行的代码,可以返回结果
Thread.sleep(1000);
return 42; // 返回一个整数结果
}
}
public static void main(String[] args) {
MyCallable task = new MyCallable();
FutureTask futureTask = new FutureTask(task); // 包装Callable
Thread t = new Thread(futureTask); // 传给Thread
t.start();
try {
Integer result = futureTask.get(); // YA33提醒:这里会等待线程执行完拿到结果
System.out.println("YA33的Callable线程结果: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
-
优点:
-
能返回执行结果 (
call()方法的返回值)。 -
能抛出异常。
-
同样具有实现
Runnable的灵活性优点。
-
-
缺点: 获取当前线程也需要用
Thread.currentThread(),使用稍复杂点。
姿势四:使用线程池 (ExecutorService) (生产环境首选)
import java.util.concurrent.*;
class Task implements Runnable { // YA33提醒:还是实现Runnable
@Override
public void run() {
// 任务代码
System.out.println("YA33的线程池任务运行中...");
}
}
public static void main(String[] args) {
// 创建一个固定大小为10的线程池 (YA33常用方式)
ExecutorService executor = Executors.newFixedThreadPool(10);
// 提交100个任务给线程池
for (int i = 0; i
-
优点 (YA33强烈推荐):
-
性能高: 重用已存在的线程,避免频繁创建销毁线程的巨大开销。
-
管理方便: 有效控制并发线程数量,防止过多线程耗尽系统资源(如内存溢出OOM)。
-
功能强大: 提供任务提交、结果获取(
Future)、定时执行、关闭管理等功能。
-
-
缺点:
-
配置参数需要一定理解(核心线程数、最大线程数、队列类型、拒绝策略等)。
-
使用不当可能造成死锁、资源耗尽等问题,排查相对复杂。
-
总结选哪个? 日常开发优先选 姿势二 (Runnable) 和 姿势四 (线程池)。需要返回值或异常处理用 姿势三 (Callable)。姿势一了解即可。
4. 启动与停止线程
-
启动线程: 必须调用线程对象的
start()方法!千万别直接调用run()方法(那就变成普通方法调用,没有新线程了!)。Thread thread1 = new Thread(...); Thread thread2 = new Thread(...); thread1.start(); // YA33: 正确启动! thread2.start(); -
停止线程 (安全协作式): Java 不推荐用已废弃的
stop()(太暴力,容易导致数据不一致)。正确姿势是:-
使用标志位: 在任务循环中检查一个
volatile变量。public class SafeStop implements Runnable { private volatile boolean running = true; // YA33: volatile保证可见性 @Override public void run() { while (running) { // 检查标志 // ... 工作代码 ... } System.out.println("YA33: 线程安全停止"); } public void stop() { running = false; // 外部调用停止 } } -
使用中断 (
interrupt()): 调用线程的interrupt()方法设置中断标志。任务中需要检查这个标志 (Thread.currentThread().isInterrupted()) 或处理InterruptedException。public class InterruptExample implements Runnable { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { // 检查中断标志 try { // ... 可能阻塞的代码 (如sleep, wait, join) ... Thread.sleep(1000); } catch (InterruptedException e) { // 阻塞时被中断会抛异常,并且清除中断状态!通常在这里退出 System.out.println("YA33: 睡眠中被中断"); Thread.currentThread().interrupt(); // 重新设置中断标志 (重要!) } } System.out.println("YA33: 线程被中断停止"); } } // 主线程中 thread.interrupt(); // 请求中断关键点:
interrupt()只是设置一个标志位,不会强制停止线程。线程需要自己检查这个标志并决定如何退出。
-
5. 线程的六种状态 (生命周期)
Java 线程有 6 种状态 (java.lang.Thread.State):
-
NEW (新建): 刚创建出来 (
new Thread()),还没调用start()方法。 -
RUNNABLE (可运行): 调用
start()后进入此状态。包含了操作系统层面的【就绪】(等待CPU分配时间片) 和 【运行中】(正在执行) 两种状态。 -
BLOCKED (阻塞): 线程等待获取一个监视器锁 (Monitor Lock) 而进入的状态。比如,试图进入一个
synchronized块/方法,但锁被其他线程占用时。被动触发。 -
WAITING (无限期等待): 线程等待另一个线程执行特定操作(唤醒)。需要其他线程显式唤醒。调用以下方法会进入WAITING:
-
Object.wait()(不配超时) -
Thread.join()(不配超时) -
LockSupport.park()(不配超时)
-
-
TIMED_WAITING (限期等待): 和 WAITING 类似,但设置了等待时间。时间到了或收到唤醒通知就会退出。调用以下方法会进入TIMED_WAITING:
-
Thread.sleep(long millis) -
Object.wait(long timeout) -
Thread.join(long millis) -
LockSupport.parkNanos(...)/LockSupport.parkUntil(...)
-
-
TERMINATED (终止): 线程执行完毕 (
run()方法结束) 或者因异常退出后的状态。生命周期结束。
图解线程状态流转 (简化版):

6. sleep() 和 wait() 的恩怨情仇
| 特性 | sleep() |
wait() |
|---|---|---|
| 所属类 |
Thread 类的静态方法
|
Object 类的实例方法
|
| 锁释放 | 不会释放持有的任何锁 | 会释放调用它的对象上的锁 |
| 使用前提 | 可在任何地方调用 |
必须在 synchronized 块或方法内 (持有锁) |
| 唤醒机制 | 超时后自动恢复到可运行(RUNNABLE) | 需要其他线程调用 notify() / notifyAll() 或超时 |
| 设计用途 | 单纯让当前线程暂停执行指定时间 | 用于线程间协调,释放锁让其他线程工作 |
YA33白话解释:
-
sleep(1000):哥们我困了,睡1秒。我占着的收银台(锁)不让,你们(其他线程)别想用,等我睡醒接着干活。 -
wait():老板货没到,我先出去抽根烟(释放收银台锁),你们谁要用收银台先用。老板货到了记得notify()我一下!我收到通知就回来(重新进入锁竞争)。
sleep() 会释放 CPU 吗? 是的!调用 Thread.sleep() 时,线程会主动让出 CPU,进入 TIMED_WAITING 状态。CPU 会去执行其他就绪状态的线程。但它不会释放它已经持有的任何锁!
7. BLOCKED vs WAITING:都是等,区别在哪?
-
触发原因:
-
BLOCKED:线程被动触发。因为想抢一个锁 (synchronized保护的资源),但锁被别人占着,抢失败了,系统自动把它放进了阻塞队列。 -
WAITING:线程主动触发。程序员在代码里明确调用了wait(),join()等方法,让它主动放弃锁并进入等待队列。
-
-
唤醒机制:
-
BLOCKED:自动唤醒。当它想要的锁被释放了,系统会自动通知所有在等这把锁的线程:“锁有空了!快来抢!”。然后这些线程从BLOCKED状态变成RUNNABLE状态去竞争锁。 -
WAITING:需要显式唤醒。必须由其他线程调用特定的方法 (notify(),notifyAll(), 或者LockSupport.unpark(对应线程)) 来唤醒它。或者等到设置的超时时间(TIMED_WAITING)。
-
核心区别两点:
-
BLOCKED是被动等锁,WAITING是主动放弃锁等通知。 -
BLOCKED的唤醒是自动的(锁释放时),WAITING的唤醒必须靠别人 (notify) 或超时。
8. 线程间如何通信?(说说话)
多个线程要协作完成任务,总得交流吧?有几种常见方式:
-
共享变量 + 同步 (最基本): 多个线程读写同一个变量。必须用
volatile或锁 (synchronized) 保证可见性和原子性!class SharedVariableExample { private static volatile boolean flag = false; // YA33: volatile保证可见性 public static void main(String[] args) { // 生产者线程 (设置flag) Thread producer = new Thread(() -> { try { Thread.sleep(2000); // 模拟工作 flag = true; // 修改共享变量 System.out.println("YA33-Producer: Flag set true!"); } catch (InterruptedException e) { ... } }); // 消费者线程 (等待flag) Thread consumer = new Thread(() -> { while (!flag) { // 等待flag变true (忙等待 - 浪费CPU!) // 可以加Thread.sleep(少量)减少CPU消耗 } System.out.println("YA33-Consumer: Flag is true now!"); }); producer.start(); consumer.start(); } }-
缺点:消费者线程的
while循环 (忙等待) 会浪费 CPU。volatile只保证可见性,不保证复合操作原子性。
-
-
wait()/notify()/notifyAll()(经典等待通知): 基于对象的监视器锁 (synchronized)。这是Object类的方法。class WaitNotifyExample { private static final Object lock = new Object(); // YA33: 共享锁对象 public static void main(String[] args) { // 消费者线程 (先启动,等待) Thread consumer = new Thread(() -> { synchronized (lock) { // 1. 先获取锁 System.out.println("YA33-Consumer: 等待老板进货..."); try { lock.wait(); // 2. 等待(释放锁)!等老板notify } catch (InterruptedException e) { ... } System.out.println("YA33-Consumer: 老板喊我,买到笔了!"); } }); // 生产者线程 (后启动,通知) Thread producer = new Thread(() -> { synchronized (lock) { // 3. 获取锁 (此时消费者在wait释放了锁) System.out.println("YA33-Producer: 正在进货..."); try { Thread.sleep(2000); // 模拟进货时间 } catch (InterruptedException e) { ... } System.out.println("YA33-Producer: 货到了!通知顾客"); lock.notify(); // 4. 通知一个等待的顾客 (消费者) // lock.notifyAll(); // 通知所有等待的顾客 } }); consumer.start(); // 稍微等一下让consumer先拿到锁 (实际开发需要更健壮机制) try { Thread.sleep(10); } catch (Exception e) {} producer.start(); } }-
关键点:
wait(),notify(),notifyAll()必须在synchronized块内调用! 调用它们的对象必须是你获取锁的那个对象 (lock)。 -
notify()随机唤醒一个在lock上wait()的线程。 -
notifyAll()唤醒所有在lock上wait()的线程。它们会一起竞争锁。
-
-
Lock+Condition(更灵活的等待通知): Java 5+ 引入的java.util.concurrent.locks包提供了更强大的锁和条件变量。import java.util.concurrent.locks.*; class LockConditionExample { private static final Lock lock = new ReentrantLock(); // YA33: 可重入锁 private static final Condition condition = lock.newCondition(); // 条件变量 public static void main(String[] args) { // 消费者线程 (等待) Thread consumer = new Thread(() -> { lock.lock(); // 获取锁 try { System.out.println("YA33-Consumer: 等待条件满足..."); condition.await(); // 等待 (类似wait,释放锁) System.out.println("YA33-Consumer: 条件满足,继续干活!"); } catch (InterruptedException e) { ... } finally { lock.unlock(); // 释放锁 (必须在finally!) } }); // 生产者线程 (通知) Thread producer = new Thread(() -> { lock.lock(); // 获取锁 try { System.out.println("YA33-Producer: 工作中..."); Thread.sleep(2000); System.out.println("YA33-Producer: 工作完成,通知消费者"); condition.signal(); // 通知一个等待者 (类似notify) // condition.signalAll(); // 通知所有等待者 } catch (InterruptedException e) { ... } finally { lock.unlock(); // 释放锁 } }); consumer.start(); // 稍微等一下 try { Thread.sleep(10); } catch (Exception e) {} producer.start(); } }-
优点: 一个锁可以关联多个
Condition(条件队列),实现更精细的线程分组通知(比如生产者通知消费者,消费者通知打包员)。Lock本身也比synchronized功能多(可中断、超时、公平锁等)。
-
-
BlockingQueue(阻塞队列): 线程安全的队列,是生产者-消费者模型的绝佳实现。队列满时生产者阻塞,队列空时消费者阻塞。import java.util.concurrent.*; class BlockingQueueExample { private static final BlockingQueue queue = new LinkedBlockingQueue(1); // YA33: 容量1的队列 public static void main(String[] args) { // 生产者线程 Thread producer = new Thread(() -> { try { System.out.println("YA33-Producer: 生产商品1"); queue.put(1); // 放入队列,如果队列满则阻塞等待 System.out.println("YA33-Producer: 商品1已上架"); } catch (InterruptedException e) { ... } }); // 消费者线程 Thread consumer = new Thread(() -> { try { System.out.println("YA33-Consumer: 等待商品..."); int item = queue.take(); // 从队列取,如果队列空则阻塞等待 System.out.println("YA33-Consumer: 买到商品 " + item); } catch (InterruptedException e) { ... } }); consumer.start(); // 消费者先启动等着 producer.start(); } }-
优点: 使用简单,解耦生产者和消费者,内部实现了线程同步。
-
-
同步工具类 (
CountDownLatch,CyclicBarrier,Semaphore): 更高级的协调工具。-
CountDownLatch(倒计时门闩): 让一个或多个线程等待其他一组线程完成任务。初始化一个计数(N),每个线程完成任务调用countDown()(计数-1)。调用await()的线程阻塞,直到计数减到0。一次性使用。 -
CyclicBarrier(循环栅栏): 让一组线程相互等待,到达一个公共屏障点(barrier)再一起继续执行。初始化指定参与线程数。每个线程调用await()表示到达屏障点并阻塞,直到最后一个线程到达屏障点,所有线程被唤醒继续执行。可循环使用。 -
Semaphore(信号量): 控制同时访问特定资源的线程数量。初始化许可数(permits)。线程通过acquire()获取许可(许可-1,没许可则阻塞),通过release()释放许可(许可+1)。常用于资源池(如数据库连接池)。
-
总结通信方式:
-
简单状态传递:
volatile+ 循环 (慎用忙等待) -
等待/通知:
wait/notify(基于synchronized),Condition(基于Lock) -
生产者-消费者:
BlockingQueue(首选) -
线程集合点/计数器:
CountDownLatch,CyclicBarrier -
资源数量控制:
Semaphore
三、 线程安全与锁的江湖
1. 保证多线程安全的核心武器
-
synchronized关键字: 内置锁,简单易用。修饰代码块或方法。// 同步方法 (锁this实例) public synchronized void addMoney() { ... } // 同步代码块 (锁指定对象) public void transfer() { synchronized (accountLock) { // YA33: 使用专门锁对象更清晰 ... // 操作账户 } } -
volatile关键字: 保证变量的可见性和部分有序性(防止指令重排序)。不能保证原子性! 适合做状态标志位。private volatile boolean shutdownRequested = false; // YA33: 关机标志 -
Lock接口 (如ReentrantLock): 比synchronized更灵活强大。可中断、可超时、公平锁、多个条件变量。private final Lock lock = new ReentrantLock(); // YA33: 可重入锁 public void criticalSection() { lock.lock(); // 获取锁 try { ... // 临界区代码 } finally { lock.unlock(); // 必须在finally释放锁! } } -
原子类 (
AtomicInteger,AtomicReference等): 利用 CAS 操作保证对单个变量的操作是原子的。性能通常优于锁。private AtomicInteger counter = new AtomicInteger(0); // YA33: 原子计数器 public void increment() { counter.incrementAndGet(); // 原子自增 } -
不可变对象: 对象一旦创建状态就不能改变。线程安全是天然的。
String,Integer等都是不可变对象。 -
线程局部变量 (
ThreadLocal): 每个线程有自己的变量副本,互不干扰。常用于存储用户会话信息、数据库连接(不推荐连接池场景)等。private static ThreadLocal currentUser = new ThreadLocal(); // YA33: 当前线程用户
2. synchronized vs ReentrantLock
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 实现方式 | JVM 内置实现 (监视器锁 monitor) |
基于 AQS (AbstractQueuedSynchronizer) 在 JDK 实现 |
| 锁获取释放 | 自动获取和释放锁 (进入/退出同步块) | 必须手动 lock() 和 unlock() (通常在 try-finally) |
| 可中断 | 不支持 |
支持 (lockInterruptibly()) |
| 超时获取 | 不支持 |
支持 (tryLock(long time, TimeUnit unit)) |
| 公平锁 | 非公平锁 (无法指定) |
可选公平锁或非公平锁 (构造参数 true/false) |
| 条件变量 | 一个锁关联一个隐式条件 (wait/notify) |
一个锁可关联多个 Condition
|
| 锁绑定 | 绑定在对象头或 Class 对象上 | 不绑定特定对象 |
| 代码可读性 | 简洁 | 相对复杂,需显式管理锁 |
| 锁升级 | 支持 (无锁->偏向锁->轻量级锁->重量级锁) | 不支持 |
| 性能 (低竞争) | 通常更好 | 稍差 |
| 性能 (高竞争) | 通常较差 | 通常更好 |
YA33 选择建议:
-
简单场景、代码量少:用
synchronized。 -
需要高级功能(中断、超时、公平锁、多条件)、高竞争性能要求:用
ReentrantLock。 -
生产环境线程池任务:优先考虑
ReentrantLock或原子类。
3. 什么是可重入锁?
可重入锁是指:同一个线程可以多次获取自己已经持有的锁,而不会被自己阻塞。
想象场景: 你进了自己家大门(获取了锁),可以随意进出家里的各个房间(再次获取同一把锁),不需要出了大门再重新进。synchronized 和 ReentrantLock 都是可重入锁。
实现原理: 内部维护一个计数器(hold count)。第一次获取锁,计数器=1。同一线程再次获取,计数器+1。释放锁时计数器-1。只有当计数器减到0时,锁才真正释放,其他线程才能获取。
4. 锁升级 (synchronized 优化)
为了提高 synchronized 的性能,JVM 会根据锁竞争情况动态升级锁的状态:
-
无锁 (Lockless): 初始状态,没有线程竞争。
-
偏向锁 (Biased Locking): 假设锁总是由同一个线程获得。这个线程获取锁时,会在对象头和栈帧锁记录里存储线程ID。以后该线程进入只需简单检查ID,无需CAS。适合几乎没有竞争的场景。
-
轻量级锁 (Lightweight Locking): 当有轻微竞争时(两个线程交替执行)。线程通过 CAS 操作尝试将对象头替换为指向线程栈中锁记录的指针。成功则获取锁。失败则说明有竞争,升级为重量级锁。自旋消耗CPU。
-
重量级锁 (Heavyweight Locking): 当竞争激烈时(多线程同时竞争)。未抢到锁的线程会被阻塞,进入操作系统内核的等待队列,等待操作系统调度唤醒。涉及用户态到内核态切换,开销最大。
升级路径: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 (不可逆)
升级目的: 在无竞争或低竞争时,避免昂贵的系统调用(用户态/内核态切换),提高性能。

5. 死锁:四个必要条件与破解之道
死锁就像交通堵塞,多个线程互相等待对方释放资源,导致大家都卡住。发生死锁需要同时满足四个条件:
-
互斥 (Mutual Exclusion): 资源一次只能被一个线程使用。(比如锁)
-
持有并等待 (Hold and Wait): 线程A拿着资源1,同时还想申请资源2(被线程B拿着),它在等资源2时不会释放资源1。
-
不可剥夺 (No Preemption): 资源只能由持有它的线程主动释放,不能被其他线程强行抢走。
-
循环等待 (Circular Wait): 线程A等线程B的资源,线程B等线程A的资源,形成一个等待环。
破解死锁: 破坏其中任意一个条件即可。最常用、最实用的方法是破坏循环等待条件:
-
资源顺序申请法: 给所有需要加锁的资源定义一个全局顺序(如按ID大小)。所有线程都严格按照这个顺序去申请资源。
// 定义资源顺序 (假设 resource1.id这样就不可能形成
线程A锁了资源1等资源2,线程B锁了资源2等资源1的循环等待了。
其他方法:
-
超时放弃 (
tryLock超时):申请锁超时则放弃并释放已有锁。 -
检测与恢复:系统检测死锁发生,强制剥夺某个资源或回滚线程。
四、 线程池:管理线程的高手
1. 为什么用线程池?
想象一下,每次有顾客(任务)来,小卖铺就新雇一个收银员(线程),顾客走了就解雇。频繁招人解雇(创建销毁线程)成本(系统开销)太高了!线程池就是开一家有固定/弹性编制收银员的超市:
-
降低资源消耗: 重用已存在的线程,避免频繁创建销毁线程。
-
提高响应速度: 任务来了,通常有现成的线程可用,无需等待线程创建。
-
提高线程可管理性: 统一管理线程的生命周期、数量、行为(如异常处理)。
-
防止资源耗尽: 控制最大线程数,防止创建过多线程耗尽内存(OOM)或CPU。
2. 线程池核心参数 (ThreadPoolExecutor)
创建线程池最核心的是 ThreadPoolExecutor 的7个参数:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // YA33: 核心线程数 (常驻员工数)
maximumPoolSize, // YA33: 最大线程数 (含核心,最多能招多少人)
keepAliveTime, // YA33: 非核心线程空闲多久被回收 (临时工空闲时间)
unit, // YA33: keepAliveTime的时间单位 (秒/毫秒等)
workQueue, // YA33: 任务队列 (排队区容量和类型)
threadFactory, // YA33: 线程工厂 (如何创建线程,可设名字等)
handler // YA33: 拒绝策略 (人满+队满时怎么办)
);
工作流程 (YA33小卖铺比喻):
-
顾客(任务)来了。
-
如果核心收银员(核心线程)有空闲,直接服务(执行任务)。
-
如果核心收银员都忙,顾客去排队区(工作队列)等着。
-
如果排队区也满了,老板就招临时收银员(非核心线程)来服务新到的顾客(注意:是执行新提交的任务,不是队列里的任务!)。
-
如果老板招人也招满了(达到最大线程数),并且还有新顾客来,老板就启动 拒绝策略(handler) 处理这个顾客(比如赶走、自己接待、记下来晚点联系等)。
-
当高峰期过去,临时收银员(非核心线程)空闲时间超过
keepAliveTime,就会被解雇(回收)。核心收银员(核心线程)只要线程池不关闭,会一直保留(除非设置allowCoreThreadTimeOut)。
3. 任务队列 (workQueue) 类型
-
SynchronousQueue(直接传递队列): 容量为0。来一个任务,必须立刻有线程执行它,否则就会尝试创建新线程(如果没到最大线程数)或执行拒绝策略。吞吐量高,避免任务排队。Executors.newCachedThreadPool()使用它。 -
LinkedBlockingQueue(无界队列): 基于链表,理论容量无限(Integer.MAX_VALUE)。任务来多了都堆积在队列里。最大线程数maximumPoolSize参数基本失效。可能导致 OOM。Executors.newFixedThreadPool(),Executors.newSingleThreadExecutor()使用它。 -
ArrayBlockingQueue(有界队列): 基于数组,固定容量。队列满了才会触发创建非核心线程。更安全可控。生产推荐使用有界队列!
4. 拒绝策略 (handler)
当线程池 关闭 或者 工作队列满且线程数已达 maximumPoolSize 时,新提交的任务会被拒绝。有四种内置策略:
-
ThreadPoolExecutor.AbortPolicy(默认): 直接抛出RejectedExecutionException异常。最严格。 -
ThreadPoolExecutor.CallerRunsPolicy: “调用者运行”策略。被拒绝的任务会退回给提交它的线程(execute或submit的调用者线程)去执行。让提交任务的线程自己干,可以减缓提交速度。 -
ThreadPoolExecutor.DiscardPolicy: 默默丢弃被拒绝的任务,不抛异常也不做任何处理。佛系。 -
ThreadPoolExecutor.DiscardOldestPolicy: 丢弃工作队列中等待时间最久(队列头)的任务,然后尝试重新提交当前任务。喜新厌旧。
YA33建议: 根据业务场景选择。需要保证不丢任务可以考虑 CallerRunsPolicy 或自定义策略(如记录日志、持久化、转存到其他队列等)。
5. 关闭线程池
-
shutdown(): 温和关闭。不再接受新任务,但会执行完已提交的任务(包括队列里的任务)。 -
shutdownNow(): 粗暴关闭。尝试停止所有正在执行的任务(通过Thread.interrupt()),不再处理队列中等待的任务,并返回未执行的任务列表。注意:interrupt()不一定能立刻停止任务(比如任务没处理中断信号)。
最佳实践: 通常先用 shutdown(),如果希望更快速关闭,可以结合 awaitTermination 等待一段时间,如果超时再用 shutdownNow()。
executor.shutdown(); // YA33: 启动有序关闭
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 等待60秒
executor.shutdownNow(); // YA33: 超时后强制关闭
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
五、 高级工具点睛 (JUC 利器)
1. CountDownLatch (倒计时门闩)
作用: 让一个或多个线程等待其他一组线程完成各自工作后再继续执行。就像运动会赛跑,所有运动员(工作线程)都到达终点(countDown())后,裁判(await() 的线程)才能宣布比赛结束。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3;
CountDownLatch latch = new CountDownLatch(workerCount); // YA33: 倒计时3次
for (int i = 0; i {
System.out.println("YA33-工人" + Thread.currentThread().getId() + "干活...");
try {
Thread.sleep((long) (Math.random() * 2000)); // 模拟工作时间
} catch (InterruptedException e) {}
System.out.println("YA33-工人" + Thread.currentThread().getId() + "干完了!");
latch.countDown(); // YA33: 完成工作,倒计时-1
}).start();
}
System.out.println("YA33-老板等所有工人完工...");
latch.await(); // YA33: 老板(主线程)在此等待,直到倒计时为0
System.out.println("YA33-老板:所有工人完工,收工!");
}
}
2. Semaphore (信号量)
作用: 控制同时访问特定资源的线程数量。就像一个只有N个座位的候诊室(permits=N)。线程(acquire())拿到一个座位(许可)才能进去看病(访问资源),看完病出来(release())释放座位给下一个人。
public class SemaphoreDemo {
public static void main(String[] args) {
int permits = 2; // 只有2个许可证 (资源)
Semaphore semaphore = new Semaphore(permits);
for (int i = 1; i {
try {
String threadName = Thread.currentThread().getName();
semaphore.acquire(); // YA33: 获取许可 (没许可就阻塞)
System.out.println("YA33-" + threadName + "抢到资源,开始使用...");
Thread.sleep(2000); // 模拟使用资源时间
System.out.println("YA33-" + threadName + "使用完毕,释放资源");
} catch (InterruptedException e) {
} finally {
semaphore.release(); // YA33: 释放许可 (必须在finally!)
}
}, "线程" + i).start();
}
}
}
3. Future & Callable (异步结果)
-
Callable: 类似Runnable,但它能返回结果,也能抛出异常。 -
Future: 表示异步计算的结果。提供了检查计算是否完成(isDone())、获取结果(get(), 会阻塞)、取消任务(cancel())的方法。
ExecutorService executor = Executors.newFixedThreadPool(2);
// YA33: 提交Callable任务,返回Future
Future future = executor.submit(() -> {
System.out.println("YA33-Callable任务计算中...");
Thread.sleep(1000);
return 42; // 返回结果
});
// ... 主线程可以继续做其他事情 ...
try {
Integer result = future.get(); // YA33: 阻塞等待结果
System.out.println("YA33-计算结果: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executor.shutdown();
4. ConcurrentHashMap (并发哈希表)
作用: 线程安全的 HashMap。相比 Collections.synchronizedMap() 包装的 HashMap 性能更好,特别是在读多写少的场景。它采用分段锁(JDK7)或 CAS+synchronized(JDK8+)等技术实现高并发。
ConcurrentHashMap map = new ConcurrentHashMap();
map.put("YA33-Score", 100);
// 并发安全的操作
map.computeIfPresent("YA33-Score", (k, v) -> v + 10);
结语
你已经完成了Java多线程的入门之旅。从基础概念到线程创建、状态管理、通信协作,再到线程安全、锁机制、线程池和JUC工具,我们一路打怪升级过来。多线程编程是Java中一个既重要又有挑战性的领域,关键是理解原理和多实践。记住几个核心:
-
安全第一: 时刻警惕共享数据的线程安全问题,善用锁、原子类等机制。
-
性能优先: 优先考虑线程池、非阻塞算法(
CAS)、减少锁竞争。 -
选对工具: 理解
synchronized,Lock,volatile, 原子类, 阻塞队列,CountDownLatch,Semaphore等工具的特性和适用场景。 -
避免雷区: 小心死锁、资源耗尽(OOM)、过度同步、
volatile误用等问题。
文章来源于互联网:java基础(五)多线程篇
相关推荐: 教你用Lovart+Midjourney轻松批量输出分镜图!
嗨大家好!我是阿真! 响应群友催更,今天我们来看点轻轻松松的,Lovart第三弹,分享关于Midjourney在Lovart工作流与其他模型批量输出分镜图的一些方法给大家。 上链接:https://www.lovart.ai/zh Lovart最近把Midjo…
5bei.cn大模型教程网










