锁、synchronized 与lock的区别
Java 中的锁机制是并发编程的核心,不同场景需要选择不同类型的锁以平衡安全性和性能。以下从「锁的特性」和「具体实现」两个维度,详细介绍 Java 中常见的锁类型、特点及使用方式。
一、按锁的特性分类
1. 悲观锁 vs 乐观锁
核心区别:是否假设线程会发生冲突。
-
悲观锁
- 假设每次操作都会发生并发冲突,因此在操作前先获取锁,阻止其他线程访问。
- 实现:
synchronized
、ReentrantLock
等。 - 适用场景:写操作频繁、冲突概率高的场景。
// 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 共享锁
核心区别:是否允许多个线程同时获取锁。
-
独占锁
- 同一时刻只允许一个线程持有锁(如写操作)。
- 实现:
synchronized
、ReentrantLock
(默认)。
-
共享锁
- 允许多个线程同时持有锁(如读操作)。
- 实现:
ReentrantReadWriteLock.ReadLock
、Semaphore
(信号量)。
// 读写锁示例:读锁共享,写锁独占 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 不可重入锁
核心区别:同一线程是否可重复获取已持有的锁。
-
可重入锁
- 线程获取锁后,可再次获取同一把锁而不被阻塞(通过计数器记录重入次数)。
- 实现:
synchronized
、ReentrantLock
(名称中的「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();
三、锁的选择原则
- 简单场景:优先用
synchronized
(无需手动释放锁,不易出错)。 - 复杂控制:用
ReentrantLock
(支持中断、超时、公平锁)。 - 读多写少:用
ReentrantReadWriteLock
或StampedLock
(提高读并发)。 - 限流场景:用
Semaphore
(控制并发数量)。 - 线程协调:用
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):指向当前持有锁的线程。
- 等待队列(WaitSet):存放调用
2. 锁升级机制(Java 6+ 优化)
为减少锁开销,synchronized
引入了 锁升级 策略(不可逆):
- 无锁状态:对象刚创建时,
Mark Word
无锁信息。 - 偏向锁:同一线程多次获取锁时,
Mark Word
记录线程 ID,避免 CAS 操作(适用于单线程场景)。 - 轻量级锁:多线程交替获取锁时,通过 CAS 将
Mark Word
指向线程栈中的锁记录(适用于低竞争)。 - 重量级锁:多线程激烈竞争时,膨胀为重量级锁,依赖操作系统互斥量(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
实现流程
- 获取锁:调用
lock()
时,通过 CAS 尝试修改state
(从 0→1),成功则获取锁;失败则进入等待队列。 - 重入机制:若当前线程已持有锁,只需增加
state
计数(state++
)。 - 释放锁:调用
unlock()
时,减少state
计数,当state
变为 0 时,唤醒等待队列中的线程。 - 公平 / 非公平锁:
- 公平锁:线程需检查等待队列是否有前驱线程,有则排队。
- 非公平锁:线程可直接尝试 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
通常能提供更好的性能。
更多推荐
所有评论(0)