在单核CPU时代,多线程曾是“伪并行”的代名词;如今,面对多核处理器与分布式系统的浪潮,真正的并行计算已成为Java高并发编程的基石。
然而,线程在共享资源时的不确定性,如同一场没有红绿灯的十字路口交通——竞态条件(Race Condition)、死锁(Deadlock)、内存可见性(Memory Visibility)问题频发。如何让多个线程安全有序地协同工作?这正是线程同步(Thread Synchronization)的核心使命。本文将带你穿透synchronized、Lock、CAS等技术的迷雾,构建线程安全的铜墙铁壁。
线程同步(Thread Synchronization)是一种协调多个线程对共享资源的访问的机制,确保在同一时刻只有一个线程能够操作特定的共享数据。其核心目标是防止因并发操作导致的数据不一致问题。
典型场景:
在多线程程序中,常见的问题主要来自于线程对共享资源的访问。当多个线程并发操作共享资源时,可能会出现以下问题:
举例:
多线程可以充分利用多核 CPU 的计算能力,那多线程难道就没有一点缺点吗?有。
多线程很难掌握,稍不注意,就容易使程序崩溃。我们以在路上开车为例:
在一个单向行驶的道路上,每辆汽车都遵守交通规则,这时候整体通行是正常的。『单向车道』意味着『一个线程』,『多辆车』意味着『多个 job 任务』。
如果需要提升车辆的同行效率,一般的做法就是扩展车道,对应程序来说就是『加线程池』,增加线程数。这样在同一时间内,通行的车辆数远远大于单车道。
然而车道一旦多起来,『加塞』的场景就会越来越多,出现碰撞后也会影响整条马路的通行效率。这么一对比下来『多车道』就比『单车道』慢多了。
防止汽车频繁变道加塞可以在车道间增加『护栏』,那在程序的世界里该怎么做呢?
竞态条件是指多个线程对共享资源的访问顺序影响程序的最终结果。例如,以下代码模拟了两个线程同时修改一个银行账户的余额:
package org.example;
public class UnsafeAccount {
private int balance = 1000;
public void withdraw(int amount) {
// 人为添加延迟,放大竞态条件窗口
if (balance >= amount) {
try {
Thread.sleep(100); // 模拟耗时操作,增加其他线程介入的机会
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() + " 取款 " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
}
}
public static void main(String[] args) throws InterruptedException {
UnsafeAccount account = new UnsafeAccount();
Thread t1 = new Thread(() -> account.withdraw(600), "线程1");
Thread t2 = new Thread(() -> account.withdraw(600), "线程2");
t1.start();
t2.start();
// 等待两个线程执行完毕
t1.join();
t2.join();
System.out.println("最终余额: " + account.balance);
}
}
最终余额可能为-200
问题根源:balance >= amount
的条件判断与balance -= amount
的操作非原子性,中间插入其他线程操作导致数据错误。
场景复现:
两个线程同时调用 withdraw(600)
方法,初始余额为1000元。以下是导致余额为-200的关键执行流程:
线程1(T1)启动
balance
值:1000balance >= 600
→ 通过balance -= 600
(但尚未完成)线程2(T2)同时启动
balance
前,T2也读取 balance
值:1000balance >= 600
→ 通过balance -= 600
操作交错执行
balance = 1000 - 600 = 400
balance
是1000,仍会执行 balance = 1000 - 600 = 400
balance
已经是400,T2的操作覆盖了T1的结果,最终 balance = 400 - 600 = -200
(假设业务允许透支)关键问题:
非原子操作:balance -= amount
并非一步完成,实际包含三步:
balance
值balance
线程切换时机:若多个线程在步骤1和步骤3之间切换,会导致共享数据被覆盖。
可视化流程:
时间线 | 线程1操作 | 线程2操作
---------------|--------------------------|--------------------------
t1 | 读取 balance=1000 |
t2 | | 读取 balance=1000
t3 | 计算 balance=1000-600=400 |
t4 | | 计算 balance=1000-600=400
t5 | 写入 balance=400 |
t6 | | 写入 balance=400-600=-200
修复方案:
通过同步机制(如 synchronized
)保证操作的原子性:
public class Account {
private int balance = 1000;
// 添加synchronized关键字
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount; // 现在线程安全
}
}
}
修复后的执行流程:
balance
已经是400,条件 balance >= 600
不成立,操作被拒绝。总结:
竞态条件的本质是多个线程对共享资源的非原子操作的交错执行。解决方法是:
synchronized
、ReentrantLock
)确保操作的原子性。AtomicInteger
)替代基础类型。ThreadLocal
)避免共享。
public class DeadlockExample {
public static void main(String[] args) {
// 定义两个锁对象
Object lockA = new Object();
Object lockB = new Object();
// 线程1:先获取lockA,再请求lockB
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + " 持有 lockA,等待 lockB...");
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + " 持有 lockA 和 lockB");
}
}
}, "Thread-1");
// 线程2:先获取lockB,再请求lockA
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + " 持有 lockB,等待 lockA...");
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + " 持有 lockB 和 lockA");
}
}
}, "Thread-2");
// 启动线程
t1.start();
t2.start();
}
}
线程执行顺序:
lockA
,然后休眠100ms,接着尝试获取 lockB
。lockB
,然后休眠100ms,接着尝试获取 lockA
。死锁发生条件:
lockA
和 lockB
不能同时被两个线程持有。lockB
,线程2等待线程1持有的 lockA
。典型输出(死锁发生时) :
Thread-1 持有 lockA,等待 lockB...
Thread-2 持有 lockB,等待 lockA...
使用 jstack
命令:
在终端运行 jstack <进程ID>
,查看线程状态:
Found one Java-level deadlock:
=============================
"Thread-2":
waiting to lock monitor locked by "Thread-1",
"Thread-1":
waiting to lock monitor locked by "Thread-2",
代码中主动检测死锁(可选):
ThreadMXBean tmBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = tmBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
System.out.println("死锁已发生!");
}
问题 | 原因 | 解决方案 |
---|---|---|
死锁 | 线程1持有 lockA 等待 lockB ,线程2持有 lockB 等待 lockA |
避免交叉请求锁,或按固定顺序请求锁 |
互斥 | 锁对象只能被一个线程持有 | 使用 ReentrantLock 支持超时机制 |
不可抢占 | 线程必须主动释放锁 | 使用 tryLock() 显式尝试获取锁 |
// 固定顺序请求锁(例如:总是先获取 lockA,再获取 lockB)
Thread t3 = new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { /* ... */ }
}
});
Thread t4 = new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { /* ... */ }
}
});
通过统一锁请求顺序,避免循环等待,彻底消除死锁风险。
Java 提供了多种线程同步机制
synchronized
关键字实例方法:锁定当前对象(this
)。
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
静态方法:锁定类的 Class
对象。
public static synchronized void staticMethod() {
// 类级同步
}
对象锁:指定任意对象作为锁。
private Object lock = new Object();
public void withdraw(int amount) {
synchronized (lock) {
if (balance >= amount) {
balance -= amount;
}
}
}
类锁:锁定类的 Class
对象。
public void method() {
synchronized (YourClass.class) {
// 类级同步
}
}
synchronized
的特性之前示例未同步结果:
优化解决:
package com.example.thread;
public class UnsafeAccount {
private int balance = 1000;
public synchronized void withdraw(int amount) {
// 人为添加延迟,放大竞态条件窗口
if (balance >= amount) {
try {
Thread.sleep(100); // 模拟耗时操作,增加其他线程介入的机会
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() + " 取款 " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
}
}
public static void main(String[] args) throws InterruptedException {
UnsafeAccount account = new UnsafeAccount();
Thread t1 = new Thread(() -> account.withdraw(600), "线程1");
Thread t2 = new Thread(() -> account.withdraw(600), "线程2");
t1.start();
t2.start();
// 等待两个线程执行完毕
t1.join();
t2.join();
System.out.println("最终余额: " + account.balance);
}
}
输出:最终余额为 400
,避免了线程竞争导致的负数问题。
这段代码中使用了 synchronized
关键字,这意味着在同一时间只有一个线程能够执行 withdraw
方法。
具体来说:
withdraw
方法时,它会获得 Account
对象的锁,其他任何线程在这段时间内尝试调用这个 withdraw
方法都会被阻塞,直到第一个线程完成并释放锁。t1
线程正在执行 withdraw
方法,t2
线程必须等待,直到 t1
线程执行完毕并释放 Account
对象的锁,然后 t2
才能开始执行 withdraw
方法。这样做的目的就是为了确保对 balance
的修改是线程安全的,避免了因并发访问导致的状态不一致问题。如果没有 synchronized
,两个线程可能会同时检查和修改 balance
,从而导致余额计算错误。
ReentrantLock
显式锁import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final ReentrantLock lock = new ReentrantLock();
private int sharedResource;
public void updateResource(int value) {
lock.lock(); // 显式获取锁
try {
sharedResource = value;
} finally {
lock.unlock(); // 确保释放锁
}
}
}
尝试加锁:非阻塞获取锁。
if (lock.tryLock()) {
try {
// 临界区代码
} finally {
lock.unlock();
}
}
超时加锁:在指定时间内尝试获取锁。
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 临界区代码
} finally {
lock.unlock();
}
}
条件变量(Condition) :替代 wait/notify
,实现更细粒度的线程协作。
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
public void waitForData() {
lock.lock();
try {
while (data.isEmpty()) {
notEmpty.await(); // 等待数据
}
} finally {
lock.unlock();
}
}
public class ProducerConsumer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private Queue<Integer> queue = new LinkedList<>();
private static final int CAPACITY = 10;
public void produce(int item) throws InterruptedException {
lock.lock();
try {
while (queue.size() >= CAPACITY) {
notFull.await(); // 等待队列不满
}
queue.add(item);
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待队列非空
}
int item = queue.remove();
notFull.signal(); // 通知生产者
return item;
} finally {
lock.unlock();
}
}
}
volatile
关键字public class FlagExample {
private volatile boolean isRunning = true;
public void stop() {
isRunning = false;
}
public void runTask() {
while (isRunning) {
// 执行任务
}
}
}
java.util.concurrent.atomic
)基于 CAS(Compare-And-Swap) 实现无锁并发,通过硬件指令(如 CAS
)确保原子性。
AtomicInteger
:原子整数操作。AtomicReference
:原子对象引用操作。AtomicLong
:原子长整型操作。import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子自增
}
public int getCount() {
return count.get();
}
}
wait
/ notify
/ notifyAll
wait()
:释放锁并进入等待状态,直到其他线程调用 notify
或 notifyAll
。notify()
:随机唤醒一个等待的线程。notifyAll()
:唤醒所有等待的线程。public class WaitNotifyExample {
public synchronized void waitMethod() {
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 进入 wait");
wait(); // 释放锁并等待
System.out.println("线程 " + Thread.currentThread().getName() + " 被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void notifyMethod() {
notify(); // 唤醒一个线程
System.out.println("线程 " + Thread.currentThread().getName() + " 调用 notify");
}
public static void main(String[] args) {
WaitNotifyExample example = new WaitNotifyExample();
Thread t1 = new Thread(() -> example.waitMethod(), "Thread-1");
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
example.notifyMethod();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread-2");
t1.start();
t2.start();
}
}
机制 | 特性 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|
synchronized |
隐式锁,可重入,互斥访问 | 简单同步需求 | 使用简单,JVM优化 | 无法控制锁粒度,性能较低 |
ReentrantLock |
显式锁,支持超时、尝试加锁、条件变量 | 复杂并发场景 | 灵活性高,性能优化 | 需手动管理锁,易出错 |
volatile |
保证可见性,禁止指令重排序 | 单写多读的状态标志 | 性能高 | 不保证原子性 |
原子类 | 基于CAS的无锁操作 | 高并发计数、更新操作 | 无需加锁,性能高 | 仅适用于简单数据类型 |
wait /notify |
线程间协作,需在同步代码块中调用 | 生产者-消费者、任务等待 | 实现线程通信 | 需谨慎处理条件检查,易出现虚假唤醒 |
java.util.concurrent
包中的线程安全类(如 ConcurrentHashMap
)。finally
块中释放锁。
Java线程同步是多线程编程的核心技能,合理选择同步机制可以显著提升程序的并发性能和稳定性。以下是关键总结:
synchronized
或原子类。ReentrantLock
和 Condition
。volatile
确保可见性。通过深入理解这些机制,开发者可以编写出高效、可靠的多线程程序,应对高并发场景的挑战。