前两课我们夯实了基础语法和面向对象思想。这一课进入 Java 进阶的重中之重------多线程与并发。这就像让程序从"单兵作战"升级为"团队协作",能极大提升效率,但也带来了资源争抢、数据安全等新挑战。
- 进程与线程概述
· 进程(Process):操作系统中正在运行的程序实例(如打开的 IDEA、浏览器)。拥有独立的内存空间。 · 线程(Thread):进程中的最小执行单元,同一进程内的多个线程共享该进程的内存资源(如堆、方法区),但每个线程拥有自己的栈和程序计数器。
Java 天生支持多线程,主方法(main)本身就是一个主线程。
- 创建线程的三种方式
2.1 继承 Thread 类
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行中");
}
}
// 启动
MyThread t1 = new MyThread();
t1.start(); // 调用 start() 才会开启新线程,直接调用 run() 只是普通方法
2.2 实现 Runnable 接口(推荐,因 Java 单继承限制)
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行中");
}
}
// 启动
Thread t2 = new Thread(new MyRunnable());
t2.start();
2.3 实现 Callable 接口(可带返回值,配合 Future)
java
import java.util.concurrent.*;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行完毕,返回结果";
}
}
// 启动(需借助线程池或 FutureTask)
FutureTask<String> task = new FutureTask<>(new MyCallable());
new Thread(task).start();
System.out.println(task.get()); // 阻塞获取返回值
- 线程调度与生命周期
3.1 线程状态(Java 中 6 种)
状态 说明 NEW(新建) 线程对象创建,尚未 start() RUNNABLE(可运行) 调用了 start(),等待 CPU 时间片(包含就绪和运行中) BLOCKED(阻塞) 等待获取锁(synchronized) WAITING(无限等待) 调用了 wait() / join(),需被显式唤醒 TIMED_WAITING(计时等待) 调用了 sleep(long) / wait(long) TERMINATED(终止) run() 执行完毕或异常退出
3.2 调度常用方法
· sleep(long millis):当前线程休眠指定毫秒,不释放锁。 · yield():主动让出 CPU 时间片,回到就绪状态,供同优先级线程竞争(不保证成功)。 · join():当前线程等待调用线程执行完毕再继续(常用于保证顺序)。
java
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) System.out.println("子线程:" + i);
});
t.start();
t.join(); // 主线程阻塞,等 t 跑完
System.out.println("主线程后执行");
}
}
线程优先级:setPriority(1-10),默认 5。优先级不保证绝对顺序,仅增加权重,底层依赖操作系统。
- 线程同步(Synchronization)
多线程共享资源时,容易发生数据竞争(如卖票出现负数)。解决方法:加锁,保证同一时刻只有一个线程操作共享数据。
4.1 synchronized 关键字(内置锁 / 对象锁)
用法一:同步方法
java
public class TicketSeller {
private int tickets = 10;
// 锁住当前实例对象(this)
public synchronized void sell() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets-- + " 张票");
}
}
}
用法二:同步代码块(更灵活,锁定指定对象)
java
public void sell() {
// 锁住当前实例(也可以锁任意 Object)
synchronized (this) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets-- + " 张票");
}
}
}
注意:静态同步方法锁的是 Class 对象(类名.class)。
4.2 同步原理
每个 Java 对象都有一个内置的监视器锁(Monitor)。线程进入 synchronized 代码块前,必须先获取该锁;执行完毕或异常时自动释放锁。
- 死锁(Deadlock)
定义:两个或以上线程互相持有对方需要的锁,且都等待对方释放,导致所有线程无限阻塞。
5.1 死锁产生的四个必要条件
- 互斥:资源只能被一个线程持有。
- 占有并等待:线程持有一个资源,并等待另一个被占有的资源。
- 不可抢占:已分配的资源不能被强制剥夺。
- 循环等待:线程间形成首尾相接的等待环路。
5.2 经典死锁示例
java
public class DeadlockDemo {
private static final Object A = new Object();
private static final Object B = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (A) {
System.out.println("线程1 持有 A,等待 B...");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (B) {
System.out.println("线程1 获取 B");
}
}
}).start();
new Thread(() -> {
synchronized (B) {
System.out.println("线程2 持有 B,等待 A...");
synchronized (A) {
System.out.println("线程2 获取 A");
}
}
}).start();
}
}
// 结果:互相等待,程序卡死(可按 IDEA 的 Thread Dump 查看)
避免策略:保持锁顺序一致(如都先锁 A 后锁 B),或使用 tryLock(见下文)。
- 重入锁(ReentrantLock)
ReentrantLock 是 java.util.concurrent.locks 包下的显式锁,相比 synchronized 更加灵活。"重入" 意味着同一个线程可以多次获取同一把锁(内部有计数器)。
6.1 基本用法(必须手动解锁)
java
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 加锁
try {
count++;
System.out.println(Thread.currentThread().getName() + " -> " + count);
} finally {
lock.unlock(); // 务必在 finally 中释放,防止死锁
}
}
}
6.2 核心优势(对比 synchronized)
特性 synchronized ReentrantLock 语法 自动(代码块/方法结束释放) 手动 lock()/unlock() 灵活性 低 高(支持公平锁、中断、超时) 尝试获取 不支持 支持 tryLock()(尝试获取,失败立即返回) 中断响应 不支持 支持 lockInterruptibly() 公平锁 非公平 构造参数可指定 new ReentrantLock(true) 性能(Java 早期) 较差 较好;现代 JDK 两者差距不大
6.3 高效用法:tryLock 避免死锁
java
public void transfer(Lock lockA, Lock lockB) {
while (true) {
if (lockA.tryLock()) {
try {
if (lockB.tryLock()) {
try {
System.out.println("成功获取两把锁,执行转账...");
return;
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
// 休眠一会再重试,避免活锁
try { Thread.sleep(10); } catch (Exception e) {}
}
}
6.4 Condition 精准唤醒(代替 wait/notify)
ReentrantLock 可以创建多个 Condition 条件,实现分组唤醒,比 synchronized 的 wait/notifyAll 更精准。
java
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 类似生产者-消费者模式中,分别唤醒"不满"和"不空"的等待线程
- 锁的宏观分类:乐观锁 vs 悲观锁(重要补充)
上文的 synchronized 和 ReentrantLock 都属于悲观锁,但并发包中还有基于乐观锁思想的一套工具,两者是面试和选型的高频考点。
7.1 核心思想对比
· 悲观锁(Pessimistic Lock):"我很保守,肯定有人改"。每次拿数据都假设别人会修改,直接加锁(阻塞其他线程)。适合写多读少、冲突频繁的场景。 · 乐观锁(Optimistic Lock):"我很乐观,没人来改"。拿数据时不加锁,更新时再检查数据是否被改过。适合读多写少、冲突极少的场景(如缓存)。
7.2 Java 中的实现方式
悲观锁代表:synchronized、ReentrantLock、数据库 select ... for update。
乐观锁代表(核心是 CAS):
· CAS(Compare-And-Swap,比较并交换):CPU 级别的原子指令。更新时比较当前值与预期值,若相等则更新,否则重试(自旋)。 · Java 实现:java.util.concurrent.atomic 包下的工具类。
java
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticDemo {
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger(0);
// CAS 更新:期望值是 0,如果是 0 就改为 1
boolean success = count.compareAndSet(0, 1);
System.out.println("更新成功?" + success + ",当前值:" + count.get());
// 失败示例(期望值是 2,但实际是 1,所以失败)
boolean fail = count.compareAndSet(2, 3);
System.out.println("更新成功?" + fail + ",当前值:" + count.get());
}
}
乐观锁的另一种实现:版本号机制(常见于数据库)
sql
-- 查询时带版本号
SELECT id, name, version FROM user WHERE id = 1; -- version=1
-- 更新时检查版本号
UPDATE user SET name='新名', version=version+1
WHERE id=1 AND version=1; -- 影响行数为 0 则表示被修改过
7.3 深度对比表
对比维度 悲观锁 乐观锁 加锁时机 访问数据前就加锁 更新数据时才检查 阻塞情况 会阻塞其他线程(挂起) 不阻塞,失败则重试(自旋) 适用场景 写操作多、竞争激烈(如银行扣款) 读操作多、竞争少(如生成全局自增 ID) 潜在问题 线程挂起/恢复开销大,易死锁 ABA 问题、自旋消耗 CPU Java 代表 synchronized、ReentrantLock AtomicInteger、LongAdder、StampedLock(乐观读)
7.4 乐观锁的"硬伤":ABA 问题
现象:变量从 A 改为 B,又改回 A。乐观锁通过值比较(A == A)会认为没变,但实际业务可能已经变了(比如余额被挪用又归还)。
解决方案:版本号/时间戳(AtomicStampedReference)。
java
// 带版本号的原子引用,可解决 ABA
AtomicStampedReference<Integer> ref =
new AtomicStampedReference<>(100, 0); // (初始值, 初始版本号)
int stamp = ref.getStamp(); // 获取当前版本
ref.compareAndSet(100, 200, stamp, stamp + 1); // 版本号+1
7.5 实战选型建议(面试加分点)
- 竞争非常激烈(如秒杀扣库存)→ 选悲观锁(或 Redis 分布式锁),因为乐观锁自旋失败率太高,浪费 CPU。
- 轻度竞争(如计数器累加)→ 选乐观锁(AtomicLong),性能远高于 synchronized。
- 读多写少(如配置信息)→ 乐观锁(StampedLock 的乐观读模式),允许读时不加锁,性能极佳。
- 数据库层面:冲突高用 for update(悲观),冲突低用 version 字段(乐观)。
💡 一句话总结:悲观锁是"先抢房间再睡觉",乐观锁是"先睡觉,醒了发现有人抢就重睡"。Java 中前者靠 synchronized/Lock,后者靠 CAS(Atomic 类)。
- 本课总结
核心知识点 要点提炼 线程创建 Thread / Runnable / Callable,推荐 Runnable 线程调度 sleep(不释放锁)、join(等别人)、yield(让一下) 同步锁(同步) synchronized 方法/代码块,解决数据竞争,锁的是对象(实例/Class) 死锁 相互持有 + 相互等待,破坏四个必要条件之一(如顺序上锁) 重入锁(重入锁) 手动加锁/解锁,支持 tryLock 超时、中断、公平模式,避免死锁更灵活 乐观锁 vs 悲观锁 悲观锁阻塞(synchronized),乐观锁重试(CAS);根据竞争激烈程度选型,注意 ABA 问题
✨ 下一课预告:集合框架(ArrayList/LinkedList、HashSet/TreeSet、HashMap/ConcurrentHashMap)及泛型。
练习题(巩固)
- 使用 Runnable 创建两个线程,分别打印 1
5 和 AE,观察输出顺序(无规律才正确)。 - 模拟 3 个窗口卖 100 张票(使用 synchronized 保证不超卖、不错卖)。
- 改写第 2 题,使用 ReentrantLock 实现相同功能。
- 编写一个银行转账方法(Account a 转给 Account b,金额 amount),使用 ReentrantLock 和 tryLock 设计避免死锁(提示:按账户 ID 顺序上锁)。
- 思考题:在秒杀场景中,你会选择乐观锁还是悲观锁?为什么?如果使用乐观锁,如何避免 ABA 问题带来的业务风险?
💡 核心思想:锁保护的是共享资源,而非代码。合理设计锁粒度,既能保障数据安全,又能提升并发性能!加油 🔥