Java 中提供了多种锁机制,用于实现多线程之间的同步和互斥。
1. 乐观锁&悲观锁
1.1 特点
乐观锁:假定多个事务之间很少发生冲突,操作不加锁。发生错误的时候进行回滚或重试。
悲观锁:假定冲突可能频繁发生,先加锁,阻止其他事务发生,操作后释放锁。
实现机制
乐观锁:实现方式是利用版本号(versioning)或时间戳(Timestamp),在进行更新的时候检查版本号或时间戳是否仍然匹配。
悲观锁:使用传统的锁机制,如
synchronize
关键字或ReetrantLock
,执行完成释放锁
1.2 性能
-
乐观锁:适合读多写少,在高并发写入的情况下,可能出现多次重试,导致性能降低。
-
悲观锁:时候写多读少,在高并发写入的情况下,可能会有较多的线程竞争,导致性能降低。
1.3 示例
乐观锁:
AtomicInteger version = new AtomicInteger();
public void updateData(Object newData){
int currentVersion = version.get();
if (currentVersion == version.get()){
// 执行更新
update(newData);
// 增加版本
version.incrementAndGet();
}else {
throw new OptimisticLockingFailureException("concurrent modification detected");
}
}
悲观锁:
// 使用ReentrantLock实现悲观锁
ReentrantLock lock = new ReentrantLock();
// 更新操作
public void updateData(Object newData) {
lock.lock(); // 获取锁
try {
// 执行更新
update(newData);
} finally {
lock.unlock(); // 释放锁
}
}
2. 自旋锁 & 适应性自旋锁
自旋锁(Spin Lock)和适应性自旋锁是两种不同的锁实现,用于在多线程环境中同步访问共享资源。
2.1 特点
自旋锁:
- 一直自旋,当线程尝试获取锁,如锁已被其他线程占用,一直自旋等待直到获取到锁为止。
- 不考虑等待时间。
适应性自旋锁:
- 动态自适应,根据锁的历史信息进行动态调整自旋时间。
- 考虑等待时间。
优点:
自旋锁:低开销,自旋锁通常比较轻量,适用于锁竞争不激烈的情况。
适应性自旋锁:适应性 ,适应性自旋锁能够在锁的竞争激烈时自适应地减少自旋等待时间,提高效率。
缺点:
自旋锁:高竞争下效率低,在锁竞争激烈的情况下,自旋会导致线程不断自旋,浪费 CPU 资源。
适应性自旋锁:复杂性 ,实现适应性自旋锁的算法较为复杂,可能会引入一些额外的开销。
2.2 性能
-
自旋锁: 适用于锁竞争不激烈的情况,且期望锁的开销较小。
-
适应性自旋锁:适用于锁的竞争激烈、存在较多争用的情况,能够根据实际情况调整自旋等待时间。
2.3 示例
自旋锁
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread currentThread = Thread.currentThread();
while (!owner.compareAndSet(null, currentThread)) {
// 自旋等待
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
owner.compareAndSet(currentThread, null);
}
}
适应性自旋锁
见 JDK 并发包中的 ReentrantLock
3. 无锁 & 偏向锁 & 轻量级锁 & 重量级锁
这四种锁结构都是针对synchronized来说的。锁的升级是由于竞争的激烈程度导致的,竞争越大,锁结构越重量化。
锁升级实现流程实现
3.1 特点
无锁: 不使用任何锁机制,通过一些算法或硬件原语实现多线程间的同步,通常在并发度较高的情况下表现优越。
偏向锁: 当一个线程访问同步块并获取锁时,会在对象头上的Mark Word中记录锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行额外的操作。
轻量级锁: 当一个线程尝试获取锁时,如果没有竞争,将在对象头的Mark Word中存储指向锁记录的指针;如果有竞争,会膨胀为重量级锁。
重量级锁: 当一个线程尝试获取锁时,如果存在竞争,将通过操作系统的互斥原语(如操作系统的 Mutex)来实现锁。
优点:
无锁:无竞争开销: 适用于低竞争、高并发的情况,减少锁的开销。
偏向锁:减少竞争: 适用于大多数情况下都是单线程访问同步块的场景,减少无谓的竞争。
轻量级锁:低竞争: 适用于线程间竞争不激烈的情况,减少了传统重量级锁的性能开销。
重量级锁:适应高竞争: 在多线程间存在激烈竞争的情况下,重量级锁能够确保线程安全。
缺点:
无锁:复杂性: 无锁算法相对复杂,实现较为困难。
偏向锁:切换成本: 如果存在多线程竞争,会导致锁膨胀为轻量级锁,引入额外的切换成本。
轻量级锁:自旋等待: 线程可能会进行短暂的自旋等待,一旦竞争激烈,可能会升级为重量级锁。
重量级锁:性能开销: 在竞争激烈的情况下,频繁地切换锁的所有者,可能导致性能下降。
3.2 性能
- 无锁: 适用于高并发度、低竞争的场景,算法实现较为复杂。
- 偏向锁: 适用于单线程频繁访问同步块的场景,减少无谓的竞争。
- 轻量级锁: 适用于线程间竞争不激烈的情况,减少了传统重量级锁的性能开销。
- 重量级锁: 适用于多线程竞争激烈、需要确保线程安全的场景,但可能会引入较大的性能开销。
4. 公平锁 & 非公平锁
锁可以分为公平锁和非公平锁,它们主要区别于获取锁的顺序。
公平锁 / 非公平锁:
前提:已存在多个线程在排队获取锁,存在阻塞队列。
条件:新来一个线程需要获取资源。
公平锁:在阻塞队列后面排队。
非公平锁:先竞争锁,竞争失败了才到阻塞队列后面排队。
4.1 特点
-
公平锁
- 获取锁顺序: 公平锁按照请求锁的顺序获取锁,即先到先得的原则。
- 等待队列: 线程在等待锁时会进入一个先进先出(FIFO)的等待队列。
- 实现机制: 公平锁通常使用队列(如
ReentrantLock
中的FairSync
)来维护等待队列。
-
非公平锁
- 获取锁顺序: 非公平锁在尝试获取锁时不考虑等待队列中其他线程的顺序,可能会插队成功获取锁。
- 等待队列: 线程在等待锁时可能会插队直接获取锁,不一定按照请求的顺序。
- 实现机制: 非公平锁通常使用一种较为简单的机制,避免了公平锁的性能开销。
优点
公平锁:公平性: 所有线程都有机会获取锁,不会出现饥饿现象。
非公平锁:性能: 在高并发的情况下,非公平锁通常具有更好的吞吐量,因为它允许线程插队。
缺点
公平锁:性能开销: 实现公平锁通常需要维护一个等待队列,可能引入额外的性能开销。
非公平锁:可能导致饥饿: 一些线程可能会被其他线程一直插队,导致某些线程一直无法获取锁,可能会出现饥饿现象。
4.2 性能
- 公平锁: 由于需要维护等待队列,可能在高并发场景下性能相对较低。
- 非公平锁: 通常具有更好的吞吐量,因为它允许一些线程插队,降低了竞争。
4.3 示例
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
5. 可重入锁 & 非可重入锁
锁可以分为可重入锁和非可重入锁,它们主要区别于同一个线程是否可以多次获得同一个锁。
ReentrantLock
和synchronized
关键字都是可重入锁的实现。
5.1 特点
-
可重入锁
- 可重入性: 同一个线程可以多次获得同一个锁,每次获得锁都会给锁计数器加1,需要相同次数的释放操作才能释放锁。
- 递归调用: 在递归调用中,线程不会被自己持有的锁所阻塞。
- 实现机制: Java 中的
ReentrantLock
和synchronized
关键字都是可重入锁的实现。
-
非可重入锁
- 不可重入性: 同一个线程获得锁后,再次尝试获得时会被阻塞,造成死锁。
- 不支持递归调用: 在递归调用中,线程会被自己持有的锁所阻塞,无法再次获取。
6. 独享锁(排他锁) & 共享锁
锁可以分为独享锁(排他锁)和共享锁,它们主要用于多线程之间对共享资源的访问控制
6.1 特点
-
独享锁
- 获取方式: 独享锁是一种独占式的锁,一次只能被一个线程持有。
- 互斥性: 当一个线程持有独享锁时,其他线程无法获取该锁,需要等待释放。
- 操作原子性: 独享锁通常用于保护临界区或关键代码段,确保操作的原子性。
-
共享锁
- 获取方式: 共享锁是一种共享式的锁,可以被多个线程同时持有。
- 允许并发: 多个线程可以同时获取共享锁,允许并发读取共享资源。
- 读写分离: 共享锁通常用于读多写少的场景,提高读操作的并发性。
优点
独享锁:数据安全: 适用于需要保护临界区或写操作的场景,确保数据的一致性和完整性。
共享锁:高并发: 适用于读操作较多的场景,能够提高系统的并发性。
缺点
独享锁:竞争激烈: 在高并发场景下,独享锁的竞争可能较为激烈,导致性能下降。
共享锁:不适用于写操作: 对于写操作,需要等待其他线程释放共享锁,可能导致写操作的延迟。
6.2 性能
- 独享锁: 适用于需要保护临界区或关键代码段,确保操作的原子性,写操作较为频繁的场景。
- 共享锁: 适用于读操作较为频繁的场景,提高系统的并发性,对数据的一致性要求相对较低。
6.3 示例
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 共享锁
public int readData() {
readWriteLock.readLock().lock(); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " is reading data: " + data);
return data;
} finally {
readWriteLock.readLock().unlock(); // 释放读锁
}
}
// 独享锁
public void writeData(int newData) {
readWriteLock.writeLock().lock(); // 获取写锁
try {
System.out.println(Thread.currentThread().getName() + " is writing data: " + newData);
data = newData;
} finally {
readWriteLock.writeLock().unlock(); // 释放写锁
}
}