Java 中的锁机制是并发编程的核心,不同场景需要选择不同类型的锁以平衡安全性和性能。以下从「锁的特性」和「具体实现」两个维度,详细介绍 Java 中常见的锁类型、特点及使用方式。

一、按锁的特性分类

1. 悲观锁 vs 乐观锁

核心区别:是否假设线程会发生冲突。

  • 悲观锁

    • 假设每次操作都会发生并发冲突,因此在操作前先获取锁,阻止其他线程访问。
    • 实现:synchronizedReentrantLock 等。
    • 适用场景:写操作频繁、冲突概率高的场景。
    // ReentrantLock 悲观锁示例
    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // 临界区操作(如修改共享变量)
    } finally {
        lock.unlock();
    }
    
  • 乐观锁

    • 假设冲突很少发生,操作时不加锁,仅在提交时检查是否有冲突(通过版本号或 CAS 实现)。
    • 实现:AtomicInteger(CAS)、数据库版本号机制。
    • 适用场景:读操作频繁、冲突概率低的场景。
    // AtomicInteger 基于 CAS 的乐观锁示例
    AtomicInteger count = new AtomicInteger(0);
    // 尝试自增,若失败则重试(底层通过 CAS 实现)
    count.incrementAndGet(); 
    
2. 独占锁 vs 共享锁

核心区别:是否允许多个线程同时获取锁。

  • 独占锁

    • 同一时刻只允许一个线程持有锁(如写操作)。
    • 实现:synchronizedReentrantLock(默认)。
  • 共享锁

    • 允许多个线程同时持有锁(如读操作)。
    • 实现:ReentrantReadWriteLock.ReadLockSemaphore(信号量)。
    // 读写锁示例:读锁共享,写锁独占
    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    Lock readLock = rwLock.readLock();  // 共享锁
    Lock writeLock = rwLock.writeLock(); // 独占锁
    
    // 读操作(可多线程同时执行)
    readLock.lock();
    try { /* 读取数据 */ } finally { readLock.unlock(); }
    
    // 写操作(仅单线程执行)
    writeLock.lock();
    try { /* 修改数据 */ } finally { writeLock.unlock(); }
    
3. 可重入锁 vs 不可重入锁

核心区别:同一线程是否可重复获取已持有的锁。

  • 可重入锁

    • 线程获取锁后,可再次获取同一把锁而不被阻塞(通过计数器记录重入次数)。
    • 实现:synchronizedReentrantLock(名称中的「Reentrant」即重入)。
    // synchronized 可重入示例
    public class ReentrantDemo {
        public synchronized void methodA() {
            System.out.println("进入 methodA");
            methodB(); // 同一线程可重入 methodB(同样被 synchronized 修饰)
        }
        public synchronized void methodB() {
            System.out.println("进入 methodB");
        }
    }
    
  • 不可重入锁

    • 线程获取锁后,再次获取同一把锁会被阻塞(可能导致死锁)。
    • Java 中无内置实现,需自定义(如简单的互斥锁)。
4. 公平锁 vs 非公平锁

核心区别:线程获取锁的顺序是否遵循「先到先得」。

  • 公平锁

    • 线程按请求锁的顺序获取锁,避免饥饿(长期等待的线程最终能获得锁)。
    • 实现:ReentrantLock 构造函数传入 true
    • 缺点:性能较低(需维护等待队列)。
  • 非公平锁

    • 线程获取锁的顺序不确定,新线程可能优先于等待队列中的线程获取锁(插队)。
    • 实现:synchronized(默认)、ReentrantLock(默认,构造函数传入 false)。
    • 优点:性能较高(减少线程切换开销)。
    // 公平锁示例
    Lock fairLock = new ReentrantLock(true); 
    // 非公平锁示例(默认)
    Lock unfairLock = new ReentrantLock(false); 
    
5. 自旋锁 vs 阻塞锁

核心区别:获取锁失败时线程是否阻塞。

  • 自旋锁

    • 线程获取锁失败时,不阻塞而是循环重试(自旋),适合锁持有时间短的场景。
    • 实现:AtomicInteger(CAS 自旋)、Unsafe 类的 CAS 操作。
    • 优点:避免线程上下文切换开销;缺点:自旋过久浪费 CPU。
  • 阻塞锁

    • 线程获取锁失败时,进入阻塞状态(放弃 CPU),适合锁持有时间长的场景。
    • 实现:synchronized(重量级锁模式)、ReentrantLock

二、Java 中具体的锁实现类

1. synchronized(内置锁)
  • 特性:悲观锁、独占锁、可重入锁、非公平锁(默认)。

  • 原理:基于 JVM 内置锁机制,通过 monitorenter/monitorexit 指令实现,Java 6 后引入锁升级(偏向锁 → 轻量级锁 → 重量级锁)。

  • 使用场景:简单同步场景,无需复杂功能(如中断、超时)。

    // 修饰方法
    public synchronized void syncMethod() { ... }
    
    // 修饰代码块
    public void syncBlock() {
        synchronized (this) { ... }
    }
    
2. ReentrantLock(可重入锁)
  • 特性:悲观锁、独占锁、可重入锁,支持公平 / 非公平模式。

  • 优势:相比 synchronized 更灵活,支持中断、超时获取锁、尝试获取锁(tryLock())。

  • 使用场景:需要精细控制锁的场景(如超时释放、中断等待)。

    Lock lock = new ReentrantLock();
    // 尝试获取锁,最多等待1秒
    try {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            // 获得锁,执行操作
        }
    } catch (InterruptedException e) {
        // 处理中断
    } finally {
        lock.unlock(); // 必须手动释放
    }
    
3. ReentrantReadWriteLock(读写锁)
  • 特性:读锁(共享锁)和写锁(独占锁)分离,支持重入。

  • 优势:读多写少场景下,允许多线程同时读,提高并发效率。

  • 注意:读锁不能升级为写锁(避免死锁),写锁可降级为读锁。

    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    // 读操作(共享)
    rwLock.readLock().lock();
    try { /* 读取数据 */ } finally { rwLock.readLock().unlock(); }
    
    // 写操作(独占)
    rwLock.writeLock().lock();
    try { /* 修改数据 */ } finally { rwLock.writeLock().unlock(); }
    
4. StampedLock( stamped 锁)
  • 特性:JDK 8 新增,支持乐观读模式,性能优于 ReentrantReadWriteLock

  • 优势:读操作默认不阻塞写操作(乐观读),仅在检测到冲突时才升级为悲观读。

  • 注意:不可重入,使用时需避免重入操作。

    StampedLock lock = new StampedLock();
    // 乐观读
    long stamp = lock.tryOptimisticRead();
    // 读取数据
    // 验证是否有写操作干扰
    if (!lock.validate(stamp)) {
        // 升级为悲观读锁
        stamp = lock.readLock();
        try { /* 重新读取数据 */ } finally { lock.unlockRead(stamp); }
    }
    
    // 写操作
    long writeStamp = lock.writeLock();
    try { /* 修改数据 */ } finally { lock.unlockWrite(writeStamp); }
    
5. Semaphore(信号量)
  • 特性:共享锁的一种,控制同时访问资源的线程数量(类似「许可证」机制)。

  • 使用场景:限流(如控制并发连接数)、资源池管理。

    // 允许3个线程同时访问
    Semaphore semaphore = new Semaphore(3);
    semaphore.acquire(); // 获取许可证,若满则阻塞
    try {
        // 访问受限资源
    } finally {
        semaphore.release(); // 释放许可证
    }
    
6. CountDownLatch(倒计时锁)
  • 特性:允许一个或多个线程等待其他线程完成操作后再执行。

  • 使用场景:协调多线程任务(如主线程等待所有子线程完成初始化)。

    CountDownLatch latch = new CountDownLatch(3); // 3个任务待完成
    // 子线程执行完任务后调用
    latch.countDown(); 
    // 主线程等待,直到计数器为0
    latch.await(); 
    

三、锁的选择原则

  1. 简单场景:优先用 synchronized(无需手动释放锁,不易出错)。
  2. 复杂控制:用 ReentrantLock(支持中断、超时、公平锁)。
  3. 读多写少:用 ReentrantReadWriteLock 或 StampedLock(提高读并发)。
  4. 限流场景:用 Semaphore(控制并发数量)。
  5. 线程协调:用 CountDownLatch 或 CyclicBarrier

合理选择锁类型可显著提升并发程序的性能和可靠性,实际开发中需结合业务场景(如冲突频率、读写比例、锁持有时间)综合判断。

贰、synchronized 与lock的区别 

synchronized 和 Lock 是 Java 中两种核心的同步机制,它们在底层实现、功能特性和使用场景上有显著差异。以下从底层原理到实际应用进行详细对比。

一、synchronized 的底层实现

synchronized 是 Java 内置的关键字,其底层依赖 JVM 实现,通过 对象头 和 监视器锁(Monitor) 机制保证同步。

1. 核心原理
  • 对象头(Mark Word):Java 对象在内存中的布局包含对象头,其中 Mark Word 存储了对象的锁状态信息(如偏向锁标记、轻量级锁指针、重量级锁指针等)。
  • 监视器锁(Monitor):每个 Java 对象都关联一个 Monitor(C++ 实现的 ObjectMonitor),它包含:
    • 等待队列(WaitSet):存放调用 wait() 后阻塞的线程。
    • -entry 列表(EntryList):存放等待获取锁的线程。
    • 所有者(owner):指向当前持有锁的线程。
2. 锁升级机制(Java 6+ 优化)

为减少锁开销,synchronized 引入了 锁升级 策略(不可逆):

  1. 无锁状态:对象刚创建时,Mark Word 无锁信息。
  2. 偏向锁:同一线程多次获取锁时,Mark Word 记录线程 ID,避免 CAS 操作(适用于单线程场景)。
  3. 轻量级锁:多线程交替获取锁时,通过 CAS 将 Mark Word 指向线程栈中的锁记录(适用于低竞争)。
  4. 重量级锁:多线程激烈竞争时,膨胀为重量级锁,依赖操作系统互斥量(Mutex)实现,线程会进入内核态阻塞(适用于高竞争)。
3. 字节码层面

synchronized 修饰方法或代码块时,字节码会插入特定指令:

  • 修饰代码块:monitorenter(获取锁)和 monitorexit(释放锁,异常时也会触发)。
  • 修饰方法:方法访问标记中包含 ACC_SYNCHRONIZED,JVM 会自动获取 / 释放锁。

二、Lock 的底层实现(以 ReentrantLock 为例)

Lock 是 Java 5 引入的接口,ReentrantLock 是最常用实现,底层基于 AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现。

1. AQS 核心结构

AQS 是并发工具的基础框架,通过 双向链表(等待队列) 和 状态变量(state) 实现同步:

  • 状态变量(state):用 volatile int 存储锁的状态(0 表示无锁,>0 表示重入次数)。
  • 等待队列:存放获取锁失败的线程,采用 CLH 队列(虚拟双向链表)实现,线程通过自旋 + CAS 竞争锁。
  • 条件队列:通过 Condition 实现,调用 await() 会将线程放入条件队列,signal() 唤醒并移至等待队列。
2. ReentrantLock 实现流程
  1. 获取锁:调用 lock() 时,通过 CAS 尝试修改 state(从 0→1),成功则获取锁;失败则进入等待队列。
  2. 重入机制:若当前线程已持有锁,只需增加 state 计数(state++)。
  3. 释放锁:调用 unlock() 时,减少 state 计数,当 state 变为 0 时,唤醒等待队列中的线程。
  4. 公平 / 非公平锁
    • 公平锁:线程需检查等待队列是否有前驱线程,有则排队。
    • 非公平锁:线程可直接尝试 CAS 获取锁,可能插队(默认策略,性能更高)。

三、synchronized 与 Lock 的核心区别

对比维度 synchronized Lock(以 ReentrantLock 为例)
底层实现 依赖 JVM 内置锁(Monitor + 对象头) 依赖 AQS 框架(状态变量 + 等待队列)
锁释放 自动释放(方法结束或异常) 必须手动释放(需在 finally 中调用 unlock()
锁类型 非公平锁(默认),不可切换 支持公平 / 非公平锁(构造函数指定)
功能灵活性 基础功能(获取 / 释放锁) 丰富功能:中断等待、超时获取、尝试获取锁等
条件变量 只有一个条件队列(通过 wait()/notify() 可创建多个 Condition,实现更精细的等待 / 唤醒
性能 低竞争场景下与 Lock 接近(锁升级优化后) 高竞争场景下性能更优(减少线程阻塞 / 唤醒开销)
可重入性 支持(隐式记录重入次数) 支持(显式通过 state 计数)
适用场景 简单同步场景,代码简洁 复杂同步场景(如超时控制、中断处理)

四、代码示例对比

1. synchronized 使用
public class SyncExample {
    private int count = 0;

    // 同步方法
    public synchronized void increment() {
        count++;
    }

    // 同步代码块
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
}
2. ReentrantLock 使用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须手动释放
        }
    }

    public void decrement() {
        lock.lock();
        try {
            count--;
        } finally {
            lock.unlock();
        }
    }
}
3. Lock 的高级特性(超时获取锁)
public boolean tryIncrement(long timeout) throws InterruptedException {
    // 尝试在指定时间内获取锁
    if (lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
        try {
            count++;
            return true;
        } finally {
            lock.unlock();
        }
    }
    return false; // 超时未获取锁
}

五、总结

  • synchronized:适合简单场景,无需手动管理锁释放,代码简洁,JVM 会持续优化其性能(如锁升级)。
  • Lock:适合复杂场景,提供中断、超时、多条件队列等高级功能,需手动释放锁(易出错但更灵活)。

选择原则:优先使用 synchronized(降低出错风险),当需要高级功能时再考虑 Lock。在高并发且竞争激烈的场景中,Lock 通常能提供更好的性能。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐