从1996到2025——细说Java锁的30年进化史

序章:并发时代的黎明

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();
    }
});

不变的真理

三十年过去了,有些原则从未改变:

  1. 最小化临界区:无论什么锁,持有时间越短越好
  2. 避免锁嵌套:减少死锁风险
  3. 优先不可变:最好的并发控制是不需要控制
  4. 理解成本:每种锁都有其适用场景和代价
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到今天的虚拟线程友好的并发原语,每一步都是对性能和易用性的追求。这三十年的历史,不仅仅是技术的演进,更是一代代工程师智慧的结晶。

当你在代码中写下synchronizedlock.lock()时,请记住:这背后是三十年的进化,是无数个深夜里对性能的优化,是对并发本质的不断探索。

锁的故事还在继续。在虚拟线程、结构化并发的新时代,我们有理由相信,Java的并发编程将迎来下一个三十年的辉煌。


"并发不是关于速度,而是关于结构。"
------ Rob Pike

相关推荐
努力努力再努力wz2 小时前
【Linux进阶系列】:线程(上)
java·linux·运维·服务器·数据结构·c++·redis
极客柒2 小时前
Unity 协程GC优化记录
java·unity·游戏引擎
我要去腾讯2 小时前
Springcloud核心组件之Sentinel详解
java·spring cloud·sentinel
czhc11400756632 小时前
Java117 最长公共前缀
java·数据结构·算法
喵个咪2 小时前
开箱即用的GO后台管理系统 Kratos Admin - 数据脱敏和隐私保护
后端·go·protobuf
我是天龙_绍3 小时前
Java Object equal重写
后端
张永清-老清3 小时前
每周读书与学习->JMeter主要元件详细介绍(四)再谈取样器
学习·jmeter·性能优化·性能调优·jmeter性能测试·性能分析·每周读书与学习
weixin_307779133 小时前
AWS Elastic Beanstalk 实现 Java 应用高可用部署指南
java·开发语言·云计算·aws·web app
虎子_layor3 小时前
实现异步最常用的方式@Async,快速上手
后端·spring