侧边栏壁纸
博主头像
牧云

怀璧慎显,博识谨言。

  • 累计撰写 183 篇文章
  • 累计创建 18 个标签
  • 累计收到 8 条评论

目 录CONTENT

文章目录

Java 并发编程:一文读懂 Java 中的各种锁

秋之牧云
2026-05-12 / 0 评论 / 0 点赞 / 6 阅读 / 0 字

在多线程并发编程中,锁(Lock) 是保证线程安全、解决资源竞争的核心机制。Java 提供了丰富多样的锁实现,从底层的 synchronized 到 JUC 包下的 ReentrantLockReadWriteLock 等。

很多开发者对锁的理解停留在“加锁解锁”的层面,但实际上,理解锁的分类、特性以及底层优化机制,对于编写高性能并发代码至关重要。本文将结合原理与代码,系统梳理 Java 中的各种锁。


一、 锁的分类维度

Java 中的锁并非单一概念,而是可以从不同维度进行分类。理解这些分类有助于我们在不同场景下选择最合适的锁。

1. 乐观锁 vs 悲观锁

这是基于对并发冲突的态度进行的分类 。

  • 悲观锁 (Pessimistic Lock)
    • 核心思想:假设每次访问数据时都会有其他线程修改数据,因此每次操作前都先加锁。
    • 典型实现synchronizedReentrantLock
    • 适用场景:写多读少,竞争激烈。
  • 乐观锁 (Optimistic Lock)
    • 核心思想:假设数据一般不会发生冲突,只在更新时检查数据是否被修改过(通常通过 CAS 或版本号机制)。
    • 典型实现AtomicIntegerLongAdder
    • 适用场景:读多写少,竞争较少。

2. 公平锁 vs 非公平锁

这是基于线程获取锁的顺序策略进行的分类 。

  • 公平锁 (Fair Lock)
    • 机制:严格按照线程申请锁的时间顺序(FIFO)来获取锁。
    • 优点:所有线程都能获得锁,不会饥饿。
    • 缺点:吞吐量低,因为需要维护队列和上下文切换。
    • 实现new ReentrantLock(true)
  • 非公平锁 (Non-Fair Lock)
    • 机制:新来的线程可以直接尝试获取锁(插队),如果失败再排队。
    • 优点:吞吐量高,减少了线程唤醒的开销。
    • 缺点:可能导致某些线程长期等待(饥饿)。
    • 实现synchronized(仅支持非公平)、new ReentrantLock()(默认非公平)。

为什么默认是非公平锁?
因为在实际场景中,刚释放锁的线程很可能再次请求锁,或者新线程正在 CPU 运行,直接让它们获取锁可以避免昂贵的上下文切换,从而提升整体性能 。

3. 独享锁 vs 共享锁

这是基于锁能否被多个线程持有进行的分类 。

  • 独享锁 (Exclusive Lock):同一时刻只能被一个线程持有。例如 synchronizedReentrantLock、读写锁中的写锁
  • 共享锁 (Shared Lock):同一时刻可以被多个线程持有。例如读写锁中的读锁

4. 可重入锁 vs 不可重入锁

  • 可重入锁 (Reentrant Lock):同一个线程可以多次获取同一把锁而不会死锁。synchronizedReentrantLock 都是可重入锁。其原理是在锁内部维护一个计数器,获取锁时+1,释放时-1,归零时真正释放 。
  • 不可重入锁:线程必须释放锁后才能再次获取,否则死锁。Java 标准库中极少使用。

二、 synchronized 的锁升级机制

synchronized 是 Java 中最基础的内置锁。在 JDK 1.6 之前,它直接依赖操作系统的 Mutex Lock,性能较差。JDK 1.6 之后,JVM 引入了锁升级机制,根据竞争程度动态调整锁的状态,以平衡性能与安全性。

锁的状态由对象头中的 Mark Word 记录,升级过程如下:

1. 无锁 (No Lock)

对象创建初期,未被任何线程锁定。Mark Word 存储对象的 HashCode、分代年龄等信息。

2. 偏向锁 (Biased Locking)

  • 场景:只有一个线程访问同步块。
  • 原理:JVM 将对象头的 Mark Word 标记为偏向模式,并记录当前线程 ID。当该线程再次进入同步块时,只需比较线程 ID,无需进行 CAS 或原子操作,性能极高 。
  • 注意:如果对象调用了 hashCode(),偏向锁通常会失效,因为 HashCode 需要占用 Mark Word 空间。

3. 轻量级锁 (Lightweight Locking)

  • 场景:存在轻微竞争,多个线程交替执行同步块。
  • 原理
    1. 线程在栈帧中创建锁记录 (Lock Record)
    2. 通过 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针。
    3. 如果 CAS 成功,获得锁;如果失败,说明有竞争,线程进行自旋 (Spin) 尝试获取 。
  • 优势:避免了用户态到内核态的切换,比重量级锁轻。

4. 重量级锁 (Heavyweight Locking)

  • 场景:竞争激烈,自旋超过阈值或有多个线程同时竞争。
  • 原理
    1. 轻量级锁膨胀为重量级锁。
    2. Mark Word 指向堆中的 ObjectMonitor 对象。
    3. 未获取锁的线程进入阻塞状态,依赖操作系统 Mutex 实现,涉及用户态与内核态切换,开销最大 。

重要提示:锁升级是单向不可逆的(无锁 -> 偏向 -> 轻量 -> 重量),一旦升级为重量级锁,即使后续无竞争,也不会降级 。


三、 JUC 包下的常用锁实战

除了 synchronizedjava.util.concurrent.locks 包提供了更灵活的锁实现。

1. ReentrantLock:灵活的手动锁

ReentrantLocksynchronized 的功能增强版,支持公平/非公平、可中断、超时获取等特性 。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock(); // 默认非公平
    private int count = 0;

    public void increment() {
        lock.lock(); // 手动加锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须在 finally 中释放,防止死锁
        }
    }
    
    // 尝试获取锁,支持超时
    public boolean tryIncrement() {
        if (lock.tryLock()) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;
    }
}

2. ReentrantReadWriteLock:读写分离

适用于读多写少的场景。读锁是共享锁,写锁是独享锁 。

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private String data = "Initial";

    // 多个线程可同时读
    public String read() {
        rwLock.readLock().lock();
        try {
            return data;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // 同一时刻只能有一个线程写,且不能有读
    public void write(String newData) {
        rwLock.writeLock().lock();
        try {
            this.data = newData;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

3. 乐观锁实战:AtomicInteger

利用 CAS 机制实现无锁并发,性能在高并发下通常优于锁 。

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        // CAS 自旋更新,无需阻塞
        count.incrementAndGet();
    }
}

四、 总结与选型建议

锁类型特点适用场景
synchronizedJVM 内置,自动加解锁,支持锁升级简单同步场景,代码量少,竞争不极端激烈
ReentrantLockAPI 级别,支持公平/非公平、可中断、超时需要高级控制(如超时、中断)或公平性要求
ReentrantReadWriteLock读写分离,读共享写独占读多写少,且读操作耗时较长
Atomic 类CAS 无锁编程,高吞吐简单的计数器、状态标志更新

选型指南:

  1. 首选 synchronized:代码简洁,JVM 优化越来越好,大多数场景足够用。
  2. 需要灵活控制选ReentrantLock:如需要尝试获取锁、响应中断、公平锁等。
  3. 读多写少选ReadWriteLock:大幅提升读并发性能。
  4. 简单原子操作选Atomic:避免锁开销,极致性能。

理解锁的本质不是目的,目的是根据业务场景选择最合适的工具,在安全性性能之间找到最佳平衡点。

0

评论区