
序章:并发时代的黎明
1996年,当James Gosling和他的团队将Java 1.0带到这个世界时,互联网正在经历第一次浪潮。那是一个单核处理器统治的时代,但Java的设计者们已经预见到了多线程的未来。在JDK 1.0的源码中,synchronized关键字静静地躺在语言规范里,成为Java为并发编程埋下的第一粒种子。
这是一个简单而粗暴的开始。
第一代:synchronized的独舞(1996-2004)
重量级的守护者
早期的synchronized就像一位忠诚但笨重的守门人。每一次加锁都需要向操作系统请求互斥量(mutex),每一次解锁都伴随着系统调用的开销。这种重量级锁的实现,在那个CPU时钟频率还在以百兆赫计数的年代,显得尤为沉重。
java
// 1996年的并发代码,朴素而直接
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
这段代码的每一次方法调用,都要经历用户态到内核态的切换。线程在竞争锁时会被挂起,陷入阻塞状态,等待操作系统的调度。这种机制虽然安全可靠,但代价高昂------一次加锁操作可能需要数百个时钟周期。
synchronized的两种形态
从一开始,synchronized就展现了两种形态:方法锁和代码块锁。
java
// 方法锁:锁住整个方法
public synchronized void methodLock() {
// 临界区代码
}
// 对象锁:更细粒度的控制
public void blockLock() {
synchronized(this) {
// 临界区代码
}
}
// 类锁:守护静态资源
public static synchronized void classLock() {
// 静态临界区
}
据说,那个年代的程序员们形成了一种共识------能不用synchronized就不用,因为它太重了。但当你不得不用时,它又是唯一的选择。这种矛盾,持续了近十年。
第二代:Doug Lea的革命(2004-2006)
java.util.concurrent的诞生
2004年,Java 5的发布标志着一个新纪元的开启。Doug Lea,这位计算机科学家,将他多年研究的并发框架带入了Java标准库。java.util.concurrent包的出现,就像在synchronized统治的单一世界里,突然打开了一扇通往多元宇宙的大门。
ReentrantLock:可重入的艺术
java
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须在finally中释放
}
}
// 可重入的魔法
public void nestedLock() {
lock.lock();
try {
increment(); // 同一线程可以再次获取锁
} finally {
lock.unlock();
}
}
}
ReentrantLock带来了synchronized不具备的能力:
java
// 尝试加锁:不再傻等
if (lock.tryLock()) {
try {
// 获取锁成功
} finally {
lock.unlock();
}
} else {
// 获取失败,做其他事情
}
// 超时机制:等待,但不是永远
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 1秒内获取到了锁
} finally {
lock.unlock();
}
}
// 可中断:响应中断信号
try {
lock.lockInterruptibly();
try {
// 临界区代码
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 在等待锁时被中断
}
ReadWriteLock:读者与写者的协议
java
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache {
private final Map<String, Object> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key); // 多个读者可以同时进入
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
cache.put(key, value); // 写者独占
} finally {
rwLock.writeLock().unlock();
}
}
}
这种读写分离的设计,在读多写少的场景下,性能提升了数倍甚至数十倍。
第三代:synchronized的逆袭(2006-2011)
2006年,当Java 6发布时,一场静悄悄的革命在JVM内部发生了。HotSpot团队对synchronized进行了彻底的改造,引入了锁优化的概念。这次进化如此成功,以至于许多场景下,synchronized的性能甚至超过了ReentrantLock。
偏向锁:一个人的舞台
在现实世界的大多数程序中,一个锁常常只被一个线程反复获取。既然如此,为何还要每次都进行完整的加锁操作?
java
public class BiasedLockExample {
private Object lock = new Object();
public void frequentOperation() {
// 第一次加锁:在对象头记录线程ID
synchronized(lock) {
// 后续加锁:只需检查线程ID,无需CAS操作
// 性能接近无锁状态
}
}
}
偏向锁的核心思想:当一个线程第一次获取锁时,在对象头中记录这个线程的ID。后续该线程再次进入同步块时,只需要简单地检查对象头中的线程ID是否是自己,无需任何原子操作。
这是一种大胆的假设:大部分锁在生命周期内,只会被一个线程持有。而统计数据证明,这个假设在80%以上的场景中都是正确的。
轻量级锁:CAS的华尔兹
当第二个线程出现,偏向锁升级为轻量级锁:
java
// 轻量级锁使用CAS操作
// 线程在自己的栈帧中创建Lock Record
// 通过CAS操作尝试将对象头的Mark Word替换为指向Lock Record的指针
synchronized(lock) {
// 如果CAS成功,获取锁
// 如果CAS失败,自旋等待
}
轻量级锁基于一个观察:锁竞争不激烈时,持有锁的时间很短。此时让线程自旋等待,比挂起线程更高效。
重量级锁:最后的防线
当自旋达到一定次数仍未获取锁,或者有两个以上线程竞争时,锁最终膨胀为重量级锁,回到最初的操作系统互斥量实现。
scss
偏向锁 -> 轻量级锁 -> 重量级锁
(无竞争) (轻度竞争) (激烈竞争)
这种锁膨胀的设计,让synchronized在不同场景下都能保持合理的性能。
锁消除与锁粗化
JVM的JIT编译器还引入了更激进的优化:
java
// 锁消除:编译器发现锁对象不会逃逸,直接消除同步
public String concatString(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1); // StringBuffer内部有synchronized
sb.append(s2); // 但sb不会逃逸,JIT会消除这些锁
return sb.toString();
}
// 锁粗化:合并连续的同步块
synchronized(lock) {
// 操作1
}
synchronized(lock) {
// 操作2
}
// 编译器会合并为一个同步块
第四代:无锁并发(2011-2015)
CAS:Compare And Swap的魔法
在锁的进化史上,有一条平行的道路:无锁编程。其核心是CAS(Compare And Swap)操作。
java
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
// 无锁的递增
count.incrementAndGet();
}
// CAS的本质
public void casIncrement() {
int oldValue, newValue;
do {
oldValue = count.get();
newValue = oldValue + 1;
// 如果期望值等于当前值,则更新为新值
} while (!count.compareAndSet(oldValue, newValue));
}
}
CAS操作由CPU指令直接支持(如x86的CMPXCHG),具有原子性。它的思想是乐观的:假设没有冲突,直接更新;如果发现冲突,则重试。
Atomic家族的壮大
java
// 基本类型
AtomicInteger, AtomicLong, AtomicBoolean
// 数组类型
AtomicIntegerArray array = new AtomicIntegerArray(10);
array.compareAndSet(0, 1, 2); // 原子地更新数组元素
// 引用类型
AtomicReference<User> userRef = new AtomicReference<>();
// 字段更新器
private volatile int status;
private static final AtomicIntegerFieldUpdater<MyClass> statusUpdater =
AtomicIntegerFieldUpdater.newUpdater(MyClass.class, "status");
ABA问题:历史的幽灵
CAS有一个著名的陷阱:ABA问题。
java
// ABA问题示例
// 线程1读取值A,准备更新为B
int oldValue = ref.get(); // 读到A
// 此时线程2将A改为B,再改回A
ref.set(B);
ref.set(A);
// 线程1继续,CAS成功,但中间状态被忽略了
ref.compareAndSet(oldValue, newValue); // 成功,但不知道中间变化
解决方案是AtomicStampedReference,引入版本号:
java
AtomicStampedReference<Integer> ref =
new AtomicStampedReference<>(100, 0);
int stamp = ref.getStamp();
ref.compareAndSet(100, 101, stamp, stamp + 1);
// 只有值和版本号都匹配才会更新
第五代:现代并发的百花齐放(2015-2021)
StampedLock:读写锁的继任者
Java 8引入了StampedLock,提供了比ReadWriteLock更灵活的机制:
java
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 乐观读:不加锁,事后验证
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 乐观读
double currentX = x;
double currentY = y;
if (!sl.validate(stamp)) { // 验证期间是否有写入
stamp = sl.readLock(); // 乐观读失败,降级为悲观读锁
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 写锁
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 锁升级:读锁升级为写锁
public void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp); // 尝试升级
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock(); // 升级失败,直接获取写锁
}
}
} finally {
sl.unlock(stamp);
}
}
}
StampedLock的乐观读是一种革命性的设计:在读多写少的场景下,读操作几乎不需要任何同步开销。
LongAdder:高并发计数器
当多个线程频繁更新同一个计数器时,AtomicLong会成为瓶颈。LongAdder通过分段的思想解决了这个问题:
java
import java.util.concurrent.atomic.LongAdder;
public class HighConcurrencyCounter {
private LongAdder counter = new LongAdder();
public void increment() {
// 内部维护多个Cell,线程更新不同的Cell
// 减少竞争
counter.increment();
}
public long sum() {
// 读取时汇总所有Cell的值
return counter.sum();
}
}
LongAdder的核心思想:用空间换时间,将热点分散到多个内存位置。
第六代:虚拟线程时代来临(2021-2025)
Project Loom:并发的范式转变
2021年,Project Loom进入预览阶段,带来了虚拟线程(Virtual Threads)。这是Java并发模型的一次根本性变革。
java
// 传统线程:重量级,创建成本高
Thread thread = new Thread(() -> {
synchronized(lock) {
// 传统线程阻塞会占用OS线程
}
});
// 虚拟线程:轻量级,可以创建百万级别
Thread vThread = Thread.ofVirtual().start(() -> {
synchronized(lock) {
// 虚拟线程阻塞不会占用OS线程
// 但synchronized可能会pin住carrier thread!
}
});
synchronized的新挑战:Pinning问题
虚拟线程给synchronized带来了新的挑战:
java
// 不推荐:synchronized会"pin"住carrier thread
Thread.ofVirtual().start(() -> {
synchronized(lock) {
// 虚拟线程无法挂载到其他carrier thread
// 限制了并发度
blockingIO();
}
});
// 推荐:使用ReentrantLock
Thread.ofVirtual().start(() -> {
lock.lock();
try {
// ReentrantLock支持虚拟线程的正确挂载
blockingIO();
} finally {
lock.unlock();
}
});
Structured Concurrency:结构化的并发
Java 19引入的结构化并发,改变了我们思考并发的方式:
java
import java.util.concurrent.StructuredTaskScope;
public class StructuredConcurrencyExample {
public Response handle(Request request) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 启动多个子任务
var user = scope.fork(() -> fetchUser(request.userId()));
var order = scope.fork(() -> fetchOrder(request.orderId()));
scope.join(); // 等待所有任务完成
scope.throwIfFailed(); // 如果有失败则抛出
// 所有子任务都成功
return new Response(user.resultNow(), order.resultNow());
} // scope自动关闭,取消所有未完成的任务
}
}
这种结构化的方式,让并发代码的生命周期管理变得清晰和安全。
Scoped Values:线程局部变量的继任者
java
import java.lang.ScopedValue;
public class ScopedValueExample {
private static final ScopedValue<User> CURRENT_USER =
ScopedValue.newInstance();
public void handleRequest(User user) {
ScopedValue.where(CURRENT_USER, user).run(() -> {
// 在这个scope内,CURRENT_USER绑定到user
processRequest();
// 虚拟线程创建的子线程会继承这个值
Thread.ofVirtual().start(() -> {
User u = CURRENT_USER.get(); // 可以访问父线程的user
});
});
// scope结束后,绑定自动解除
}
}
相比ThreadLocal,ScopedValue更适合虚拟线程:不可变、自动清理、支持跨线程传递。
尾声:三十年的沉淀
从1996年到2025年,Java的锁经历了从简单到复杂,再从复杂到优雅的进化:
yaml
1996-2004: synchronized的蛮荒时代
↓
2004-2006: Doug Lea的显式锁革命
↓
2006-2011: JVM的锁优化魔法
↓
2011-2015: 无锁并发的崛起
↓
2015-2021: 现代并发原语的完善
↓
2021-2025: 虚拟线程时代的重构
选择的智慧
今天,当我们面对并发问题时,有了更多的选择:
java
// 简单场景:synchronized依然是最佳选择
public synchronized void simpleMethod() {
// JVM优化后性能优异,代码简洁
}
// 需要高级特性:ReentrantLock
if (lock.tryLock(timeout, TimeUnit.SECONDS)) {
try {
// 超时、中断、公平性
} finally {
lock.unlock();
}
}
// 读多写少:StampedLock的乐观读
long stamp = sl.tryOptimisticRead();
// 几乎零开销的读操作
// 高并发计数:LongAdder
counter.increment(); // 分段降低竞争
// 简单原子操作:Atomic类
atomicInt.incrementAndGet(); // 无锁CAS
// 虚拟线程时代:避免synchronized的pinning
Thread.ofVirtual().start(() -> {
lock.lock(); // 使用ReentrantLock
try {
// ...
} finally {
lock.unlock();
}
});
不变的真理
三十年过去了,有些原则从未改变:
- 最小化临界区:无论什么锁,持有时间越短越好
- 避免锁嵌套:减少死锁风险
- 优先不可变:最好的并发控制是不需要控制
- 理解成本:每种锁都有其适用场景和代价
java
// 永恒的建议
public class BestPractice {
// 1. 不可变对象:无需加锁
private final ImmutableList<String> items;
// 2. 局部变量:线程安全
public void method() {
int local = 0; // 每个线程独立
}
// 3. 合理使用volatile:可见性保证
private volatile boolean flag;
// 4. 选择合适的工具
private final ConcurrentHashMap<String, String> map; // 已优化的并发容器
private final AtomicInteger count; // 简单原子操作
private final ReentrantLock lock; // 复杂场景
}
后记:向未来致敬
Java的锁,从笨重的synchronized到今天的虚拟线程友好的并发原语,每一步都是对性能和易用性的追求。这三十年的历史,不仅仅是技术的演进,更是一代代工程师智慧的结晶。
当你在代码中写下synchronized或lock.lock()时,请记住:这背后是三十年的进化,是无数个深夜里对性能的优化,是对并发本质的不断探索。
锁的故事还在继续。在虚拟线程、结构化并发的新时代,我们有理由相信,Java的并发编程将迎来下一个三十年的辉煌。
"并发不是关于速度,而是关于结构。"
------ Rob Pike