掌握并发编程核心:synchronized、AQS、ReentrantLock 实用攻略

51 阅读7分钟

一、synchronized 关键字

1. 基本用法

  • 修饰实例方法:锁是当前对象实例。

    public synchronized void method() {
        // 同步代码
    }
    
  • 修饰静态方法:锁是当前类的 Class 对象。

    public static synchronized void staticMethod() {
        // 同步代码
    }
    
  • 修饰代码块:显式指定锁对象。

    public void blockMethod() {
        synchronized (lockObject) {
            // 同步代码
        }
    }
    

2. 底层原理

  • 对象头与 Monitor

    • 每个Java对象在堆中都有一个对象头,包含 Mark Word(存储锁状态和线程ID)。
    • Monitor(管程)是JVM实现的互斥锁机制,通过 monitorenter 和 monitorexit 指令控制锁的获取和释放。
  • 锁升级过程(JDK6优化):

    1. 无锁:初始状态。
    2. 偏向锁:仅一个线程访问时,通过CAS记录线程ID。
    3. 轻量级锁:多个线程竞争但未冲突时,通过CAS自旋尝试获取锁。
    4. 重量级锁:竞争激烈时,升级为操作系统级互斥锁(涉及内核态切换)。

3. 特性与限制

  • 可重入性:同一线程可重复获取同一锁。
  • 非公平锁:不保证等待线程的获取顺序。
  • 自动释放:代码块结束或异常时自动释放锁。

4. 适用场景

  • 简单的临界区保护(如单例模式)。
  • 低竞争环境(避免重量级锁的性能损耗)。

二、AQS(AbstractQueuedSynchronizer)

1. 核心设计

  • 作用:Java并发包(java.util.concurrent.locks)的基础框架,用于构建锁和同步器(如 ReentrantLockSemaphore)。

  • 核心组件

    • 状态变量(state :通过CAS操作控制同步状态(如锁的持有次数)。
    • CLH队列:双向链表实现的等待队列,管理竞争线程。
  • 模板方法模式:子类需实现 tryAcquiretryRelease 等钩子方法。 AQS(AbstractQueuedSynchronizer)是 Java 并发包中的一个核心类,它大量运用了模板方法模式。以下是关于 AQS 中模板方法模式的详细介绍:

模板方法模式概述

模板方法模式是一种行为设计模式,它在一个抽象类中定义了一个算法的骨架,而将一些步骤延迟到子类中实现。这样可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

AQS 中的模板方法模式体现

  • AQS 中的抽象模板方法

    • acquire(int arg):这是 AQS 提供的一个模板方法,用于获取锁等同步状态。它的实现依赖于其他几个方法,如tryAcquire(int arg)addWaiter(Node mode)acquireQueued(Node node, int arg)等。acquire方法定义了获取同步状态的整体流程框架,而具体的获取逻辑由子类实现的tryAcquire方法来完成。
    • release(int arg):用于释放锁等同步状态的模板方法。它同样依赖于tryRelease(int arg)等方法,release方法定义了释放同步状态的通用流程,而具体的释放逻辑由子类实现的tryRelease方法来决定。
  • AQS 中的具体模板方法执行流程

    • acquire方法为例

      • 首先,调用tryAcquire方法尝试获取同步状态。这个方法是留给子类实现的,不同的子类可以根据自身的需求来定义具体的获取逻辑。
      • 如果tryAcquire方法返回false,表示获取同步状态失败,则会通过addWaiter方法将当前线程包装成一个节点,并添加到等待队列中。
      • 然后,通过acquireQueued方法让线程在队列中等待,不断尝试获取同步状态,直到获取成功或者线程被中断等情况发生。
    • release方法为例

      • 首先,调用tryRelease方法尝试释放同步状态,这也是由子类来实现具体逻辑。
      • 如果tryRelease方法返回true,表示释放成功,则会从等待队列中唤醒一个等待的线程,让其有机会获取同步状态。

AQS 中模板方法模式的作用

  • 提高代码复用性:AQS 通过模板方法模式,将获取和释放同步状态等通用的逻辑封装在抽象类中,子类只需要实现具体的获取和释放逻辑,避免了大量重复代码的编写。
  • 增强可扩展性:当需要实现新的同步器时,只需要继承 AQS 并实现相应的抽象方法即可,符合开闭原则,即对扩展开放,对修改关闭。
  • 实现统一的同步框架:使得 Java 并发包中的各种同步器,如ReentrantLockSemaphoreCountDownLatch等,都能够基于 AQS 提供的统一模板方法来实现,保证了同步器的一致性和稳定性。

2. 工作流程

  1. 获取锁

    • 调用 acquire(int arg),先尝试 tryAcquire
    • 失败后,将线程加入CLH队列并阻塞。
  2. 释放锁

    • 调用 release(int arg),先执行 tryRelease
    • 唤醒队列中的后继线程。

3. 典型实现

  • ReentrantLock:基于AQS的可重入锁。
  • CountDownLatch:基于AQS的倒计数器。

三、ReentrantLock

1. 核心特性

  • 显式锁:需手动 lock() 和 unlock(),配合 try-finally 使用。

    ReentrantLock lock = new ReentrantLock();
    lock.lock();
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
    
  • 可重入性:同一线程可多次获取锁(通过 state 记录重入次数)。

  • 公平性:支持公平锁(按等待顺序获取)和非公平锁(默认)。

    ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
    
  • 可中断性:通过 lockInterruptibly() 响应中断。

  • 条件变量:通过 Condition 实现线程等待/唤醒。

    Condition condition = lock.newCondition();
    condition.await();     // 等待
    condition.signal();    // 唤醒
    

2. 与 synchronized 对比

特性synchronizedReentrantLock
锁获取方式隐式(自动释放)显式(需手动释放)
公平性非公平支持公平和非公平
可中断性不支持支持(lockInterruptibly()
条件变量通过 wait()/notify()通过 Condition 实现更灵活控制
性能JDK6优化后接近高竞争场景下更灵活

3. 适用场景

  • 需要细粒度控制(如超时、公平性、条件等待)。
  • 高并发竞争环境(通过自旋和CAS减少阻塞)。

四、锁的底层实现对比

锁类型实现机制性能特点
synchronizedJVM内置锁(Monitor + 对象头)低竞争时性能优,重量级锁有性能损耗
ReentrantLock基于AQS + CAS + CLH队列高竞争时更灵活,支持公平锁

五、常见问题与解决方案

1. 死锁

  • 场景:多个线程互相等待对方释放锁。

  • 排查工具

    • jstack:查看线程堆栈,识别死锁链。
    • Arthasthread -b 直接定位死锁。
  • 解决:按固定顺序获取锁,或使用 tryLock() 超时机制。

2. 锁竞争性能瓶颈

  • 场景:高并发下锁竞争导致吞吐量下降。

  • 优化手段

    • 减小锁粒度:如 ConcurrentHashMap 的分段锁。
    • 无锁编程:使用 CAS(如 AtomicInteger)。
    • 读写锁ReentrantReadWriteLock 分离读/写锁。

3. 锁饥饿

  • 场景:公平锁中低优先级线程长期无法获取锁。
  • 解决:合理设置线程优先级,或使用非公平锁。

六、最佳实践

  1. 优先使用 synchronized

    • 简单场景下,利用JVM优化(锁升级)。
  2. 复杂场景选择 ReentrantLock

    • 需要可中断、超时、公平性等高级特性时。
  3. 避免锁嵌套

    • 减少死锁风险,保持锁顺序一致。
  4. 锁分离与合并

    • 读写分离(ReadWriteLock)、锁粗化(合并相邻同步块)。

七、示例代码

1. 使用 ReentrantLock 实现线程安全计数器

public class SafeCounter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

2. 条件变量实现生产者-消费者模型

public class ProducerConsumer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
    private Queue<Integer> queue = new LinkedList<>();
    private int maxSize = 10;

    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == maxSize) {
                notFull.await();
            }
            queue.add(value);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            int value = queue.poll();
            notFull.signal();
            return value;
        } finally {
            lock.unlock();
        }
    }
}

总结

  • synchronized:简单易用,适合低竞争场景,依赖JVM优化。
  • AQS:Java并发包的核心框架,提供灵活的同步器扩展能力。
  • ReentrantLock:功能丰富,支持公平锁、可中断锁和条件变量,适合高竞争复杂场景。
  • 选择建议:根据业务需求权衡易用性、性能和功能扩展性,优先选择最简方案。