Java 并发锁实战手册:各类锁的特性、适用场景与选择方法论
Java开发中的锁
摘要:在Java开发中,锁是处理多线程并发安全的核心工具。不同类型的锁适用于不同场景,理解它们的特性和适用场景能帮助我们写出高效且安全的并发代码。以下是Java中常见锁的分类及实际开发中的使用场景
一、基于锁的获取方式:隐式锁 vs 显式锁
1. 隐式锁(synchronized)
- 特点:JVM原生支持,无需手动释放,自动实现锁的获取与释放;可重入、非公平锁(默认);在JDK 1.6后进行了大量优化(偏向锁、轻量级锁、重量级锁升级机制)。
- 使用方式:修饰方法(锁对象为
this
或类对象)、修饰代码块(指定锁对象)。// 修饰方法 public synchronized void method1() { ... }// 修饰代码块 public void method2() {synchronized (this) { ... } }
- 实际场景:
- 并发量较低的简单场景(如工具类的线程安全方法)。
- 不需要复杂锁控制(如中断、超时、公平性)的场景。
- 优势:使用简单,不易因忘记释放锁导致死锁,JVM优化成熟,性能接近显式锁。
- 注意:锁粒度不宜过大(避免影响并发效率),如尽量锁代码块而非整个方法。
2. 显式锁(java.util.concurrent.locks.Lock)
需手动调用lock()
获取锁、unlock()
释放锁(通常在finally
中释放),功能更灵活。
(1)ReentrantLock(可重入锁)
- 特点:可重入、支持公平/非公平锁(构造函数指定)、可中断获取锁、超时获取锁、尝试获取锁(
tryLock()
)。 - 使用方式:
Lock lock = new ReentrantLock(); // 非公平锁(默认) // Lock lock = new ReentrantLock(true); // 公平锁public void method() {lock.lock();try {// 业务逻辑} finally {lock.unlock(); // 必须释放,否则可能死锁} }
- 实际场景:
- 需要灵活控制锁的场景:如超时获取锁(避免永久阻塞)、中断等待锁的线程(
lockInterruptibly()
)。 - 需要公平锁的场景(如对执行顺序有严格要求,避免线程饥饿)。
- 复杂的同步逻辑(如多个条件变量配合
Condition
使用)。 - 示例:线程池任务中,需要控制某资源的并发访问,且允许超时放弃。
- 需要灵活控制锁的场景:如超时获取锁(避免永久阻塞)、中断等待锁的线程(
(2)ReadWriteLock(读写锁)
- 特点:拆分为读锁(
ReadLock
)和写锁(WriteLock
),多线程可同时获取读锁(共享),但写锁与读锁/写锁互斥(排他);适合“读多写少”场景。 - 使用方式:
ReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock();// 读操作 public void read() {readLock.lock();try { ... } finally { readLock.unlock(); } }// 写操作 public void write() {writeLock.lock();try { ... } finally { writeLock.unlock(); } }
- 实际场景:
- 缓存系统(如本地缓存,大量线程读缓存,少量线程更新缓存)。
- 配置文件读取(多数情况读配置,少数情况更新配置)。
- 注意:读锁不能升级为写锁(避免死锁),写锁可降级为读锁。
(3)StampedLock( stamped 锁)
- 特点:JDK 8引入,比
ReadWriteLock
更灵活,支持三种模式:写锁、悲观读锁、乐观读;乐观读无需加锁,通过版本号判断是否有写入,适合“读远多于写”场景。 - 使用方式:
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); }
- 实际场景:
- 高频读、低频写的场景(如统计数据展示,极少更新但频繁查询)。
- 对性能要求极高,且读操作占绝对主导的场景。
- 注意:不支持重入,使用时需避免重入调用导致死锁。
二、基于锁的竞争策略:乐观锁 vs 悲观锁
1. 悲观锁
- 特点:认为并发冲突会频繁发生,每次操作前必须获取锁,阻塞其他线程(如
synchronized
、ReentrantLock
)。 - 适用场景:写操作频繁、并发冲突严重的场景(如库存扣减、转账)。
2. 乐观锁
- 特点:认为并发冲突少,操作时不加锁,仅在提交时校验是否有冲突(通常基于CAS机制或版本号)。
- Java中的实现:
Atomic
系列类(如AtomicInteger
,基于CAS)。- 自定义版本号机制(如数据库表的
version
字段)。
- 使用方式:
AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 原子自增(CAS实现)
- 实际场景:
- 并发冲突少的场景(如计数器、序列号生成)。
- 读多写少,且写入冲突概率低的场景(如用户积分更新)。
- 注意:CAS存在“ABA问题”,可通过版本号解决(如
AtomicStampedReference
)。
三、基于锁的公平性:公平锁 vs 非公平锁
- 公平锁:线程获取锁的顺序与请求顺序一致,避免饥饿,但性能稍差(需维护等待队列)。
- 适用场景:对顺序性要求高的场景(如生产环境的任务调度,需按提交顺序执行)。
- 非公平锁:线程获取锁的顺序不固定,可能“插队”,吞吐量更高(默认选择)。
- 适用场景:多数并发场景(如普通业务逻辑),优先追求性能。
四、基于锁的粒度:偏向锁、轻量级锁、重量级锁(synchronized底层优化)
这三种是synchronized
的底层实现,随竞争升级:
- 偏向锁:无实际竞争时,锁偏向第一个获取它的线程,减少CAS操作(适用于单线程反复获取锁的场景)。
- 轻量级锁:轻度竞争时,通过CAS自旋获取锁,避免线程阻塞(适用于短时间持有锁的场景)。
- 重量级锁:重度竞争时,膨胀为操作系统级别的互斥锁,线程进入内核态阻塞(适用于长时间持有锁的场景)。
实际开发中无需手动控制,JVM自动升级,理解其机制可帮助优化锁的使用(如减少锁持有时间,避免频繁升级)。
五、选择建议
- 优先使用
synchronized
:简单、安全,JVM优化成熟,适合大多数低至中并发场景。 - 需要灵活控制(超时、中断、公平性)时,用
ReentrantLock
。 - 读多写少场景,用
ReadWriteLock
;读远多于写,用StampedLock
。 - 并发冲突少,用乐观锁(
Atomic
类);冲突多,用悲观锁。 - 尽量减小锁粒度(如锁对象用局部变量而非全局对象),减少锁竞争。
根据具体业务的并发特点选择合适的锁,平衡安全性与性能。