回顾JAVA中的锁机制
Java中的锁机制
在Java中,锁机制是多线程编程里保障数据一致性与线程安全的关键技术。
1. 内置锁:synchronized关键字
synchronized
是Java的内置锁机制,能够保证在同一时刻,只有一个线程可以执行被其修饰的代码块或方法。
用法示例
public class SynchronizedExample {private int count = 0;// 同步方法public synchronized void increment() {count++;}// 同步代码块public void decrement() {synchronized(this) {count--;}}
}
实现原理
synchronized
是基于对象头中的Mark Word来实现的。当一个线程访问同步代码块时,会先查看对象的Mark Word。如果Mark Word显示该对象没有被锁定,那么这个线程就会将Mark Word设置为锁定状态,然后开始执行同步代码块。在这个线程执行同步代码块期间,如果其他线程也想访问这个同步代码块,它们会发现对象的Mark Word已经被设置为锁定状态,于是这些线程就会被阻塞,进入等待队列。
2. 显示锁:Lock接口
Lock
接口是Java 5引入的,它提供了比synchronized
更灵活、更强大的锁控制能力。
核心方法
lock()
:获取锁,如果锁不可用,则线程会被阻塞。unlock()
:释放锁,必须在finally
块中调用,以确保锁一定会被释放。tryLock()
:尝试获取锁,如果锁可用,则获取锁并返回true
;如果锁不可用,则立即返回false
,不会阻塞线程。
用法示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockExample {private final Lock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
}
3. ReentrantLock(可重入锁)
ReentrantLock
是Lock
接口的一个重要实现类,它支持可重入锁的特性。所谓可重入锁,就是指同一个线程可以多次获取同一把锁,而不会出现死锁的情况。
特性
- 公平性:
ReentrantLock
可以设置为公平锁或非公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则不保证这一点,有可能后请求的线程先获得锁。 - 可重入性:同一个线程可以多次获取同一把锁,每获取一次,锁的计数器就会加1,每释放一次,锁的计数器就会减1,当计数器为0时,锁才会被真正释放。
公平锁示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class FairLockExample {private final Lock fairLock = new ReentrantLock(true); // true表示公平锁public void performTask() {fairLock.lock();try {// 执行任务} finally {fairLock.unlock();}}
}
4. ReentrantReadWriteLock(读写锁)
ReentrantReadWriteLock
提供了读写分离的锁机制,它维护了一对锁,一个读锁和一个写锁。
特性
- 读锁:允许多个线程同时获取读锁,用于并发读取共享资源。
- 写锁:写锁是排他锁,同一时刻只允许一个线程获取写锁,用于修改共享资源。
- 读写互斥:读锁和写锁不能同时被获取,即当有线程获取了读锁时,其他线程不能获取写锁;当有线程获取了写锁时,其他线程不能获取读锁和写锁。
用法示例
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class Cache {private final ReadWriteLock rwLock = new ReentrantReadWriteLock();private Object data;public Object read() {rwLock.readLock().lock();try {return data;} finally {rwLock.readLock().unlock();}}public void write(Object newData) {rwLock.writeLock().lock();try {data = newData;} finally {rwLock.writeLock().unlock();}}
}
5. StampedLock(邮戳锁)
StampedLock
是Java 8引入的一种新的锁机制,它提供了比ReentrantReadWriteLock
更细粒度的锁控制。可以有效应对A-B-A问题。
特性
- 乐观读锁:乐观读锁是一种无锁机制,它允许在没有获取锁的情况下读取共享资源。读取完成后,需要验证资源是否在读取期间被修改过,如果没有被修改过,则读取有效;如果被修改过,则需要重新读取。
- 悲观读锁和写锁:与
ReentrantReadWriteLock
的读锁和写锁类似,但StampedLock
的悲观读锁和写锁是通过返回一个邮戳(stamp)来控制的。
用法示例
import java.util.concurrent.locks.StampedLock;public class Point {private double x, y;private final StampedLock sl = new StampedLock();public double distanceFromOrigin() {long stamp = sl.tryOptimisticRead(); // 尝试乐观读double currentX = x, currentY = y;if (!sl.validate(stamp)) { // 验证是否有写操作发生stamp = sl.readLock(); // 获取悲观读锁try {currentX = x;currentY = y;} finally {sl.unlockRead(stamp);}}return Math.sqrt(currentX * currentX + currentY * currentY);}
}
6. 锁的优化
锁粗化(Lock Coarsening)
锁粗化是指将多次连续的加锁和解锁操作合并为一次加锁和解锁操作,以减少锁的获取和释放带来的性能开销。
锁消除(Lock Elimination)
锁消除是指在编译时,Java编译器会对一些代码进行分析,如果发现某些锁是不必要的,就会将这些锁消除掉,从而提高代码的执行效率。
偏向锁(Biased Locking)
偏向锁是一种针对单线程环境的锁优化机制。当一个线程第一次获取锁时,锁会被标记为偏向锁,并记录该线程的ID。当该线程再次获取锁时,无需进行任何同步操作,直接获取锁,从而提高了单线程环境下的性能。
轻量级锁(Lightweight Locking)
轻量级锁是一种在多线程环境下,但线程竞争不激烈时的锁优化机制。当线程竞争不激烈时,轻量级锁可以避免线程的阻塞和唤醒操作,从而提高了性能。
自旋锁(Spin Lock)
自旋锁是指当一个线程获取锁失败时,它不会立即被阻塞,而是会在原地循环等待,直到锁被释放。自旋锁适用于锁的持有时间较短的场景,可以减少线程的阻塞和唤醒带来的性能开销。
7. 选择合适的锁
- synchronized:适用于简单的同步场景,代码简洁,由JVM自动管理锁的获取和释放。
- ReentrantLock:适用于需要更灵活的锁控制的场景,如可中断锁、公平锁等。
- ReentrantReadWriteLock:适用于读多写少的场景,可以提高并发读取的性能。
- StampedLock:适用于读多写少且读操作性能要求较高的场景,提供了乐观读锁机制,进一步提高了读操作的性能。
通过合理使用这些锁机制,你可以在多线程编程中实现高效且安全的并发控制。
重入锁和内置锁的选用
在Java里,重入锁(ReentrantLock)和内置锁(synchronized)都可用于实现线程同步,不过它们的适用场景存在差异。下面为你介绍选择使用重入锁还是内置锁的依据:
优先考虑内置锁的情况
- 语法简洁:内置锁是通过
synchronized
关键字来实现的,无需手动释放锁,JVM会自动处理锁的获取和释放,这样能降低因忘记释放锁而导致死锁的风险。public synchronized void method() {// 同步代码块 }
- 对性能要求不高:在JDK 1.6之后,Java对
synchronized
进行了一系列优化,像偏向锁、轻量级锁等,使得它的性能和ReentrantLock
相差不大。 - 锁的使用场景简单:若只是需要对方法或代码块进行简单的同步,内置锁完全可以满足需求。
优先考虑重入锁的情况
- 需要公平锁:重入锁可以通过构造函数指定使用公平锁(
new ReentrantLock(true)
),公平锁会按照线程请求锁的顺序来分配锁,能避免某些线程长时间等待锁的情况。而内置锁只能是非公平锁。 - 需要灵活的锁控制:重入锁提供了一些高级功能,如可中断锁、尝试锁(
tryLock()
)、带超时的锁获取等。Lock lock = new ReentrantLock(); try {// 尝试获取锁,若锁被其他线程持有,则当前线程可被中断lock.lockInterruptibly();// 执行同步操作 } catch (InterruptedException e) {Thread.currentThread().interrupt(); } finally {lock.unlock(); }
- 需要实现条件变量(Condition):重入锁可以和
Condition
接口配合使用,实现更灵活的线程等待和唤醒机制,比如实现生产者 - 消费者模式。Lock lock = new ReentrantLock(); Condition condition = lock.newCondition();// 等待条件 lock.lock(); try {while (!conditionMet()) {condition.await();} } finally {lock.unlock(); }// 唤醒等待的线程 lock.lock(); try {condition.signalAll(); } finally {lock.unlock(); }
性能方面的考量
- 低竞争场景:在竞争不激烈的情况下,内置锁和重入锁的性能差距不大。
- 高竞争场景:如果线程之间对锁的竞争非常激烈,重入锁的性能可能会略优于内置锁,因为重入锁提供了更多的锁优化选项,例如使用非公平锁可以减少线程的上下文切换。
总结
- 推荐优先使用内置锁:因为它的语法简洁,由JVM自动管理锁的获取和释放,降低了出错的概率。
- 在需要高级特性时使用重入锁:例如公平锁、可中断锁、条件变量等。
示例对比
内置锁示例
public class SynchronizedExample {private int count = 0;public synchronized void increment() {count++;}
}
重入锁示例
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
}
通过对比可以看出,内置锁的代码更加简洁,而重入锁则提供了更灵活的控制方式。你可以根据具体的需求来选择合适的锁机制。
理解各种锁的特性和适用场景
Java锁机制中的各类锁是根据不同场景和需求设计的,理解它们的核心概念、实现方式及适用场景对编写高效、线程安全的代码至关重要。以下从分类维度详细解析常见的锁类型:
一、按锁的特性分类
1. 乐观锁(Optimistic Locking)
- 核心思想:假设数据不会被其他线程修改,因此不阻塞访问,仅在更新时检查是否有冲突。
- 实现方式:
- 版本号机制:数据记录中增加
version
字段,更新时比较版本号是否一致(如数据库的UPDATE ... WHERE version = ?
); - CAS(Compare-And-Swap):Java中通过
Unsafe
类或Atomic
包实现(如AtomicInteger
的getAndIncrement()
)。
- 版本号机制:数据记录中增加
- 使用场景:读多写少、冲突概率低的场景(如缓存更新、统计计数)。
- 示例:
// AtomicInteger基于CAS实现乐观锁 private AtomicInteger counter = new AtomicInteger(0); public void increment() {counter.getAndIncrement(); // 内部使用CAS,无需显式加锁 }
2. 悲观锁(Pessimistic Locking)
- 核心思想:假设数据随时会被修改,因此访问时直接加锁,阻塞其他线程。
- 实现方式:
- synchronized关键字:Java内置的对象锁(如
synchronized(this)
); - ReentrantLock:可重入锁,功能更灵活(如支持公平锁、可中断锁)。
- synchronized关键字:Java内置的对象锁(如
- 使用场景:写多、冲突概率高的场景(如库存扣减、银行转账)。
- 示例:
// synchronized实现悲观锁 public synchronized void transferMoney() {// 操作共享资源 }
3. 自旋锁(Spin Lock)
- 核心思想:当锁被占用时,线程不挂起(不放弃CPU时间片),而是循环尝试获取锁,直到成功。
- 实现方式:
- 基于CAS实现(如
AtomicBoolean
的compareAndSet()
); - JVM内部的轻量级锁(偏向锁膨胀后可能升级为自旋锁)。
- 基于CAS实现(如
- 使用场景:锁持有时间短、线程不希望频繁挂起/唤醒的场景(如JDK内部的
ConcurrentHashMap
)。 - 优点:避免线程上下文切换,提升性能;
- 缺点:若锁长时间被占用,会浪费CPU资源。
- 示例:
// 自定义简单自旋锁 public class SpinLock {private AtomicBoolean locked = new AtomicBoolean(false);public void lock() {while (!locked.compareAndSet(false, true)) {// 循环等待,自旋}}public void unlock() {locked.set(false);} }
二、按JVM实现分类
4. 偏向锁(Biased Locking)
- 核心思想:在单线程环境下,锁偏向第一个获取它的线程,后续该线程无需再进行同步操作。
- 实现方式:
- 对象头中的
Mark Word
存储偏向线程ID; - 当有其他线程尝试竞争锁时,偏向锁会被撤销并升级为轻量级锁。
- 对象头中的
- 使用场景:只有一个线程访问同步块的场景(如单例模式的双重检查锁)。
- 优点:无竞争时几乎无开销,提升单线程性能。
5. 轻量级锁(Lightweight Lock)
- 核心思想:多线程交替执行同步块时,通过CAS避免重量级锁的线程挂起/唤醒操作。
- 实现方式:
- 线程进入同步块时,JVM在栈帧中创建锁记录(Lock Record);
- 通过CAS将对象头的
Mark Word
指向锁记录,成功则获取锁,失败则升级为重量级锁。
- 使用场景:线程竞争不激烈,锁持有时间短的场景。
6. 重量级锁(Heavyweight Lock)
- 核心思想:依赖操作系统的互斥量(Mutex)实现,线程竞争锁失败时会被挂起(进入内核态)。
- 实现方式:
- 基于操作系统的
pthread_mutex_t
(Linux)或CRITICAL_SECTION
(Windows); - 对象头的
Mark Word
指向重量级锁的指针,锁竞争时涉及用户态与内核态的切换。
- 基于操作系统的
- 使用场景:线程竞争激烈,锁持有时间长的场景。
- 缺点:性能开销大,因为涉及内核态与用户态的切换。
三、按API实现分类
7. 可重入锁(Reentrant Lock)
- 核心思想:允许同一个线程多次获取同一把锁,而不会被阻塞(通过计数器实现)。
- 实现方式:
synchronized
关键字(隐式可重入);ReentrantLock
(显式可重入,需调用lock()
和unlock()
)。
- 使用场景:方法嵌套调用同步块的场景(如递归函数)。
- 示例:
// ReentrantLock实现可重入锁 private ReentrantLock lock = new ReentrantLock(); public void outerMethod() {lock.lock();try {innerMethod();} finally {lock.unlock();} }public void innerMethod() {lock.lock(); // 同一线程可再次获取锁try {// 操作共享资源} finally {lock.unlock();} }
8. 公平锁(Fair Lock)
- 核心思想:锁的获取顺序按请求时间排序,先到先得(避免线程“饥饿”)。
- 实现方式:
ReentrantLock(true)
:构造函数传入true
启用公平锁;- 基于FIFO队列实现,线程竞争锁时会进入队列等待。
- 使用场景:对线程执行顺序敏感的场景(如资源分配)。
- 缺点:公平锁的性能通常低于非公平锁,因为需要维护队列。
9. 读写锁(ReadWrite Lock)
- 核心思想:将锁分为读锁和写锁,允许多个线程同时读,但写时互斥。
- 实现方式:
ReentrantReadWriteLock
:支持读锁(共享锁)和写锁(排他锁);- 读锁可被多个线程同时持有,写锁只能被一个线程持有,且写锁存在时禁止读锁。
- 使用场景:读多写少的场景(如缓存更新、配置读取)。
- 示例:
// 读写锁示例 private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private Lock readLock = rwLock.readLock(); private Lock writeLock = rwLock.writeLock();public void readData() {readLock.lock();try {// 读取共享数据} finally {readLock.unlock();} }public void writeData() {writeLock.lock();try {// 修改共享数据} finally {writeLock.unlock();} }
10. 分段锁(Striped Lock)
- 核心思想:将锁分段管理,不同段的锁相互独立,减少锁竞争。
- 实现方式:
ConcurrentHashMap
(JDK 7及以前):内部使用分段数组(Segment),每个Segment独立加锁;- JDK 8后改用CAS+Synchronized实现,但分段锁思想仍在其他场景中使用。
- 使用场景:大规模数据并发操作的场景(如分布式缓存)。
四、按锁的特性扩展
11. 可中断锁(Interruptible Lock)
- 核心思想:线程在等待锁的过程中可被其他线程中断。
- 实现方式:
ReentrantLock
的lockInterruptibly()
方法;synchronized
不支持可中断,只能通过Thread.interrupt()
标记中断状态。
- 使用场景:需要取消长时间等待的场景(如超时控制)。
- 示例:
ReentrantLock lock = new ReentrantLock(); try {lock.lockInterruptibly(); // 可中断的锁获取try {// 操作共享资源} finally {lock.unlock();} } catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态 }
12. 锁降级(Lock Degradation)
- 核心思想:将写锁降级为读锁,保持数据可见性。
- 实现方式:
- 先获取写锁,修改数据后获取读锁,再释放写锁。
- 使用场景:写操作后需要保证后续读操作可见性的场景(如缓存更新)。
- 示例:
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock writeLock = rwLock.writeLock(); Lock readLock = rwLock.readLock();public void updateAndRead() {writeLock.lock();try {// 修改数据readLock.lock(); // 获取读锁} finally {writeLock.unlock(); // 释放写锁,保留读锁(锁降级)}try {// 读取数据(确保数据可见性)} finally {readLock.unlock();} }
五、锁的选择策略
- 优先考虑无锁方案:如使用
Atomic
类、ConcurrentHashMap
等无锁数据结构。 - 读多写少→乐观锁/CAS:冲突概率低时性能最优。
- 写多冲突高→悲观锁:如
synchronized
或ReentrantLock
。 - 锁持有时间短→自旋锁:避免线程上下文切换。
- 公平性需求→公平锁:但需牺牲一定性能。
- 读写分离→读写锁:如
ReentrantReadWriteLock
。
理解各种锁的特性和适用场景,结合具体业务需求选择合适的锁,是编写高效并发代码的关键。