Java 中 LOCK
自旋锁(Spin Lock)
一)概念
自旋锁是一种基于忙等待(busy-waiting)的锁机制。当一个线程尝试获取锁时,如果锁已经被其他线程持有,该线程不会立即进入阻塞状态,而是在一个循环中不断地检查锁是否已经被释放,这个循环过程就称为自旋。自旋的目的是为了避免线程上下文切换带来的开销,因为线程上下文切换涉及到保存和恢复线程的执行状态,包括寄存器、程序计数器等信息,这是一个相对耗时的操作。
代码示例
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
// 自旋获取锁
while (!owner.compareAndSet(null, current)) {
// 空循环,等待锁释放
}
}
public void unlock() {
Thread current = Thread.currentThread();
// 释放锁
if (owner.compareAndSet(current, null)) {
// 成功释放锁
}
}
}
SpinLock
类使用AtomicReference
来实现自旋锁。lock
方法通过compareAndSet
操作尝试将owner
设置为当前线程,如果成功则表示获取锁;如果失败,则在while
循环中不断自旋等待。unlock
方法则将owner
设置为null
以释放锁。
优缺点
优点:
减少线程上下文切换的开销,对于那些持有锁时间较短的情况,自旋锁可以显著提高性能。因为如果线程在短时间内能够获取到锁,那么避免上下文切换的开销可能比进入阻塞状态等待唤醒更加高效。
缺点:
浪费CPU资源。如果自旋时间过长,即锁被其他线程长时间持有,那么自旋的线程会一直占用CPU资源进行空转,而这些CPU资源本可以用于其他有意义的计算任务,从而降低了系统的整体性能。
可重入锁(Reentrant Lock)
可重入锁是指同一个线程可以多次获取同一把锁而不会导致死锁。例如,在一个递归调用的方法中,如果该方法需要获取锁来访问共享资源,那么可重入锁允许该线程在递归调用时再次获取已经持有的锁,而不会被阻塞。可重入锁内部通过维护一个计数器来记录线程获取锁的次数,每次获取锁时计数器加 1,每次释放锁时计数器减 1,当计数器为 0 时,锁才真正被释放。
代码示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
public void outerMethod() {
lock.lock();
try {
System.out.println("外层方法获取锁");
innerMethod();
} finally {
lock.unlock();
}
}
public void innerMethod() {
lock.lock();
try {
System.out.println("内层方法获取锁");
} finally {
lock.unlock();
}
}
}
在上述示例中,ReentrantLockExample类演示了可重入锁的使用。outerMethod先获取锁,然后在内部调用innerMethod时,同一线程可以再次获取该锁,不会被阻塞。
乐观锁(Optimistic Lock)
概念
乐观锁基于一种乐观的假设,即认为在大多数情况下,多个线程对共享资源的访问不会产生冲突。它不会像悲观锁那样在访问共享资源之前就先加锁,而是在更新共享资源时才去检查是否有其他线程对资源进行了修改。如果发现有冲突,则根据具体的冲突处理策略来.
代码示例(基于版本号的乐观锁)
public class OptimisticLockExample {
private int version;
private int value;
public void updateValue(int newValue) {
// 先获取当前版本号和值
int currentVersion = version;
int currentValue = value;
// 模拟一些操作,这里假设在操作过程中其他线程可能修改了值
//...
// 更新时检查版本号是否一致
if (version == currentVersion) {
value = newValue;
version++;
} else {
// 版本号不一致,说明有冲突,处理冲突,这里可以选择重试或者抛出异常等
System.out.println("乐观锁检测到冲突");
}
}
}
OptimisticLockExample类通过维护一个version字段来实现乐观锁。在更新value时,先获取当前的version和value,在实际更新操作完成后,再次检查version是否与之前获取的一致,如果一致则说明没有其他线程修改过,更新成功;如果不一致,则表示有冲突。
悲观锁(Pessimistic Lock)
(一)概念
与乐观锁相反,悲观锁基于一种悲观的假设,即认为在多线程环境下,对共享资源的访问一定会产生冲突。所以,在任何对共享资源进行访问操作之前,都先获取锁,以防止其他线程同时访问。悲观锁的实现通常依赖于操作系统提供的互斥锁(mutex)或者Java中的synchronized关键字等机制。
代码示例(使用synchronized关键字实现悲观锁)
public class PessimisticLockExample {
private int value;
public synchronized void updateValue(int newValue) {
value = newValue;
}
public synchronized int getValue() {
return value;
}
}
分段锁(Segment Lock)
(一)概念
分段锁是一种将共享资源分成多个段(segment),每个段分别设置锁的机制。不同的线程可以同时访问不同段的资源,而只有当多个线程访问同一段资源时才会产生锁竞争。这种机制可以有效地提高并发性能,特别是在处理大规模数据结构时,如ConcurrentHashMap在JDK 1.7及之前版本中就使用了分段锁来实现高效的并发访问。
代码示例(简单模拟分段锁的概念)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SegmentLockExample {
// 假设将数据分为 3 个段,每个段有一个锁
private Lock[] segmentLocks = new Lock[3];
private Object[][] segmentData = new Object[3][];
public SegmentLockExample() {
for (int i = 0; i < 3; i++) {
segmentLocks[i] = new ReentrantLock();
}
}
public void updateSegmentData(int segmentIndex, int dataIndex, Object newValue) {
Lock lock = segmentLocks[segmentIndex];
lock.lock();
try {
segmentData[segmentIndex][dataIndex] = newValue;
} finally {
lock.unlock();
}
}
}
在上述示例中,SegmentLockExample类将数据分为 3 个段,每个段对应一个ReentrantLock。当更新某个段的数据时,先获取对应段的锁,然后进行操作,操作完成后释放锁。这样不同的线程可以同时更新不同段的数据,提高了并发性能。
优缺点
优点:
提高并发性能。在处理大规模数据时,将数据分段并分别加锁,可以减少锁竞争的范围,使得更多的线程能够同时进行操作,从而提高系统的整体吞吐量。
具有一定的扩展性。当数据量增加或者系统并发度提高时,可以通过增加段的数量来进一步优化性能,而不需要对整个锁机制进行大规模的修改。
缺点:
实现复杂度较高。相比于简单的全局锁机制,分段锁需要对数据进行合理的分段,并管理多个锁对象,这增加了代码的复杂性和维护难度。
可能存在段间不平衡的问题。如果数据分布不均匀,可能导致某些段的锁竞争非常激烈,而其他段的锁很少被使用,从而影响整体性能。