JUC 并发编程概览

为什么要用JUC?

核心矛盾在于:传统的线程同步方式(如`synchronized`关键字)虽然简单,但存在以下局限性:

  1. 效率低下:锁的释放和获取方式单一,缺乏灵活性,容易导致死锁。
  2. 无法中断:一个试图获取锁的线程无法被中断,只能无限期等待。
  3. 单一条件:`synchronized`的等待/通知机制(`wait()`/`notify()`)绑定在对象监视器上,一个锁只能有一个等待条件,不够精细。

JUC包正是为了克服这些缺点而生,它提供了更灵活、性能更高、功能更丰富的并发控制工具。

JUC的核心组成部分

JUC包内容庞大,但我们可以将其分为几个核心部分:

  1. 原子类 (`java.util.concurrent.atomic`):提供了线程安全的、基于CAS操作的原子变量类(如`AtomicInteger`)。
  2. 锁框架(`java.util.concurrent.locks`):核心,提供了比`synchronized`更强大的锁机制,主要是`Lock`接口及其实现类(如`ReentrantLock`)。
  3. 并发集合 (`java.util.concurrent`):提供了线程安全的高性能集合类(如`ConcurrentHashMap`, `CopyOnWriteArrayList`)。
  4. 线程池(`java.util.concurrent`):强大的执行框架(`ExecutorService`, `ThreadPoolExecutor`)。
  5. 同步工具:提供特定的同步功能(如`CountDownLatch`, `CyclicBarrier`, `Semaphore`)。

锁机制深度解析

这是JUC最核心的部分,我们将重点对比`synchronized`和`Lock`接口,并深入其实现原理。

1. synchronized 内置锁

用法:关键字,用于修饰方法或代码块。
特性:

  1. 互斥性:同一时间只有一个线程可以进入同步代码块。
  2. 可重入性:同一个线程可以多次获取同一把锁(防止自身死锁)。
  3. 不可中断性:等待锁的线程会一直阻塞,无法被中断。
  4. 非公平性(默认):锁的获取不讲究先来后到,允许“插队”,性能好但可能导致线程饥饿。

底层原理:

  1. 依赖于JVM内置的`Monitor`(监视器锁)实现。
  2. 字节码中通过`monitorenter`和`monitorexit`指令来完成加锁和解锁。
  3. 锁的状态存储在Java对象头中的`Mark Word`里。

2. Lock 接口及其实现

`Lock`是一个接口,提供了比`synchronized`更丰富的操作。它的核心实现类是`ReentrantLock`。

Lock接口的核心方法:

  • `lock()`: 获取锁,如果锁被占用则等待。
  • `unlock()`: 释放锁,**必须**在`finally`块中调用以避免死锁。
  • `tryLock()`: **尝试非阻塞地**获取锁,立即返回成功与否。
  • `tryLock(long time, TimeUnit unit)`: **超时等待**获取锁,超时或被中断则返回。
  • `lockInterruptibly()`: 获取锁,但等待过程可以响应中断。

ReentrantLock 的核心特性:

可重入性 :与`synchronized`一样。
可中断的锁获取:使用`lockInterruptibly()`方法。
超时获取锁:使用`tryLock(long timeout, TimeUnit unit)`方法。
公平性/非公平性选择:
    *   **非公平锁**(默认):构造函数不传参或传`false`。吞吐量高,但可能饥饿。
    *   **公平锁**:构造函数传`true`。严格按照FIFO(先进先出)的顺序分配锁,保证不会饥饿,但上下文切换频繁,吞吐量相对较低。

**代码对比示例:**

```java
// 使用 synchronized
public synchronized void syncMethod() {
    // 临界区代码
}

// 使用 ReentrantLock
private final ReentrantLock lock = new ReentrantLock();

public void lockMethod() {
    lock.lock(); // 获取锁
    try {
        // 临界区代码
    } finally {
        lock.unlock(); // 确保锁被释放
    }
}

// 使用 tryLock 避免死锁
public void tryLockMethod() {
    if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试在1秒内获取锁
        try {
            // 获取锁成功,执行临界区代码
        } finally {
            lock.unlock();
        }
    } else {
        // 获取锁失败,执行其他逻辑(如记录日志、重试等)
    }
}
```

#### **3. 读写锁 (ReadWriteLock 和 ReentrantReadWriteLock)**

*   **解决痛点**:对于“读多写少”的场景,`synchronized`和`ReentrantLock`都是互斥锁,同一时间只允许一个线程访问,严重降低了读操作的并发性。
*   **设计原则**:
    *   **共享读**:多个读线程可以同时访问共享资源。
    *   **独占写**:写线程访问时,所有读线程和其他写线程均被阻塞。
*   **用法**:
    ```java
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    public String read() {
        readLock.lock();
        try {
            // 执行读操作
            return data;
        } finally {
            readLock.unlock();
        }
    }

    public void write(String newData) {
        writeLock.lock();
        try {
            // 执行写操作
            data = newData;
        } finally {
            writeLock.unlock();
        }
    }
    ```

---

### **第三部分:底层原理探秘 (AQS)**

**现在,我要问你一个关键问题:`ReentrantLock`和`ReentrantReadWriteLock`是如何实现上述强大功能的?**

答案就在于它们都依赖一个共同的基石——**AbstractQueuedSynchronizer (AQS)**,即抽象队列同步器。理解AQS是理解JUC锁机制的关键。

**AQS的核心思想:**
AQS使用一个整型的**volatile**变量(`state`)来表示同步状态,并通过一个内置的FIFO**双向链表队列**来管理获取锁失败的线程。

*   **状态 (`state`)**:
    *   对于`ReentrantLock`,`state`表示锁被重入的次数(0表示未锁定,1表示被一个线程锁定,>1表示被同一线程重入)。
    *   对于`ReentrantReadWriteLock`,高16位表示读锁的持有次数,低16位表示写锁的重入次数。
    *   对于`Semaphore`,`state`表示可用的许可数。
*   **CLH队列**:将暂时获取不到锁的线程封装成`Node`节点,加入队列并排队等待。
*   **模板方法模式**:AQS定义了获取和释放状态的模板方法(如`acquire()`和`release()`),具体的实现(如如何修改`state`)由子类(如`ReentrantLock`中的`Sync`内部类)完成。这就是为什么叫“抽象”队列同步器。

**以`ReentrantLock`的非公平锁为例,`lock()`方法的大致流程:**
1.  调用`lock()`方法。
2.  直接尝试用**CAS操作**快速设置`state`为1(尝试抢锁)。
3.  如果成功,则将当前线程设置为独占线程。
4.  如果失败,则调用AQS的`acquire(1)`方法。
5.  `acquire()`会再次尝试获取锁(`tryAcquire`),失败则将当前线程加入等待队列并挂起(`LockSupport.park()`)。
6.  当锁被释放时,会唤醒队列中的下一个线程来尝试获取锁。

**CAS (Compare-And-Swap)**:这是AQS乃至整个JUC包实现无锁并发操作的底层CPU指令。它是一种乐观锁,包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果V的值等于A,则用B更新V,否则什么都不做。整个操作是一个原子操作。

---

### **第四部分:总结与对比**

| 特性 | `synchronized` | `ReentrantLock` |
| :--- | :--- | :--- |
| **存在层次** | Java关键字,JVM级别 | JDK接口,API级别 |
| **实现机制** | 监视器模式,JVM实现 | AQS + CAS,Java代码实现 |
| **锁的释放** | 自动释放(代码块结束/异常) | **必须手动**调用`unlock()` |
| **灵活性** | 低 | **高**(可中断、超时、尝试) |
| **公平性** | 仅非公平 | **可公平**也可非公平 |
| **性能** | JDK6后优化很大,两者接近 | 在高竞争环境下可能更有优势 |
| **条件队列** | 单一 | **多个**(通过`Condition`) |

**如何选择?**
*   **优先使用`synchronized`**:除非你需要`ReentrantLock`提供的**高级功能**(可中断、超时、公平锁、多个条件等),否则由于其简洁性和JVM的持续优化,`synchronized`通常是更好的选择。
*   **考虑`ReentrantLock`**:当你的业务场景需要上述高级特性时,例如:
    *   处理一个可能发生死锁,需要尝试获取锁或响应中断的场景。
    *   需要实现公平的锁获取策略。
    *   需要**分离的等待条件**(使用`Condition`,例如生产者消费者模型中的“非空”和“非满”两个条件)。

---

### **面试官的最后提问**

好了,理论部分就讲到这里。现在,请你:

1.  简要概括一下AQS的工作原理。
2.  在什么场景下你会选择使用`ReentrantReadWriteLock`?
3.  `synchronized`的锁升级过程是怎样的?(如果你能回答这个问题,说明你对JVM优化也有深入了解)

不要紧张,思考一下,我们慢慢聊。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐