java JUC并发编程详解
JUC 并发编程概览
为什么要用JUC?
核心矛盾在于:传统的线程同步方式(如`synchronized`关键字)虽然简单,但存在以下局限性:
- 效率低下:锁的释放和获取方式单一,缺乏灵活性,容易导致死锁。
- 无法中断:一个试图获取锁的线程无法被中断,只能无限期等待。
- 单一条件:`synchronized`的等待/通知机制(`wait()`/`notify()`)绑定在对象监视器上,一个锁只能有一个等待条件,不够精细。
JUC包正是为了克服这些缺点而生,它提供了更灵活、性能更高、功能更丰富的并发控制工具。
JUC的核心组成部分
JUC包内容庞大,但我们可以将其分为几个核心部分:
- 原子类 (`java.util.concurrent.atomic`):提供了线程安全的、基于CAS操作的原子变量类(如`AtomicInteger`)。
- 锁框架(`java.util.concurrent.locks`):核心,提供了比`synchronized`更强大的锁机制,主要是`Lock`接口及其实现类(如`ReentrantLock`)。
- 并发集合 (`java.util.concurrent`):提供了线程安全的高性能集合类(如`ConcurrentHashMap`, `CopyOnWriteArrayList`)。
- 线程池(`java.util.concurrent`):强大的执行框架(`ExecutorService`, `ThreadPoolExecutor`)。
- 同步工具:提供特定的同步功能(如`CountDownLatch`, `CyclicBarrier`, `Semaphore`)。
锁机制深度解析
这是JUC最核心的部分,我们将重点对比`synchronized`和`Lock`接口,并深入其实现原理。
1. synchronized 内置锁
用法:关键字,用于修饰方法或代码块。
特性:
- 互斥性:同一时间只有一个线程可以进入同步代码块。
- 可重入性:同一个线程可以多次获取同一把锁(防止自身死锁)。
- 不可中断性:等待锁的线程会一直阻塞,无法被中断。
- 非公平性(默认):锁的获取不讲究先来后到,允许“插队”,性能好但可能导致线程饥饿。
底层原理:
- 依赖于JVM内置的`Monitor`(监视器锁)实现。
- 字节码中通过`monitorenter`和`monitorexit`指令来完成加锁和解锁。
- 锁的状态存储在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优化也有深入了解)
不要紧张,思考一下,我们慢慢聊。
更多推荐




所有评论(0)