Skip to main content

第13章 显示锁

Java5.0增加了ReentrantLock

Lock与ReentrantLock

Lock提供了一种无条件的/可轮询的/定时的以及可中断的锁获取操作,所有加锁和解锁的方式都是显式的

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。

Lock lock = new ReentrantLock();
...
lock.lock();
try {

} finally {
lock.unlock();
}

轮询锁和定时锁

可定时/可轮询的锁获取时由tryLock方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制

// 通过tryLock来避免顺序死锁
public boolean transferMoney(Account fromAcct, Account to Acct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException {
long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
long randMod = getRandomDelayModulesNanos(timeout, unit);
long stopTime = System.nanoTime() + unit.toNanos(timeout);

while(true) {
if (fromAcct.lock.tryLock()) {
try {
if (toAcct.lock.tryLock()) {
try {
if (fromAcct.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
} else {
fromAcct.debit(amount);
toAcct.credit(amount);
return true;
}
} finally {
toAcct.lock.unlock();
}
}
} finally {
fromAcct.lock.unlock();
}
}
if (System.nanoTime() < stopTime) {
return false;
}
NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
}
}

带有时间限制的加锁

public boolean trySendOnSharedLine(String message, long timeout, TimeUnit unit) throws InterruptedException {
long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
if (!lock.tryLock(nanosToLock, NANOSECONDS)) {
return false;
}
try {
return sendOnSharedLine(message);
} finally {
lock.unlock();
}
}

可中断的锁获取操作

lockInterruptibly方法能够在获得锁的同时保持对中断的响应

public boolean sendOnSharedLine(String message) throws InterruptedException {
lock.lockInterruptibly();
try {
return cancellableSendOnSharedLine(message);
} finally {
lock.unlock();
}
}

private boolean cancellableSendOnSharedLine(String message) throws InterruptedException { ... }

非块结构的加锁

锁分段技术在基于散列的容器中实现了不同的散列链,以便使用不同的锁。

连锁式加锁(Hand-Over-Hand Locking)或者锁耦合(Lock Couping)

性能考虑因素

对于同步原语来说,竟争性能是可伸缩性的关键要素:如果有越多的资拥被浪费在锁的管理和调度上,那么应用程序得到的资源就越少。锁的实现方式越好,将需要越少的系统调用和上下文切换,并且在共享内存总线上的内存同步通信量越少,而一些耗时的操作将占用应用程序的计算资源。

性能是一个不断变化的指标,如果在昨天的测试基准中发现X比Y更快,那么在今天可能已经过时了

公平性

ReentrantLock 的构造函数中提供了两种公平性选择:创建一个非公平的锁〈默认〉或者一个公平的锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”; 当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁

公平性性把性能降低了约两个数量级。不必要的话,不要为公平性付出代价。

在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。ABC,A释放锁时,B被唤醒,C可能在B在唤醒之前获得/使用并释放这个锁。

当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。插队带来的吞吐量提升则可能不会出现

在synchronized和ReentrantLock之间进行选择

ReentrantLock 在加锁和内存上提供的语义与均内置锁相同,此外它还提供了一些其他功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁。

在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具 。当需要一些高级功能时应该使用ReentrantLock,可定时的,可轮询的,可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是有限使用synchronized

读-写锁

放宽加锁需求,允许多个执行读操作的线程同时访问数据结构,实现更高的并发性,提升程序性能

public interface ReadWriteLock{
Lock readLock();
Lock writeLock();
}
  • 释放优先 当写入操作释放写入锁时,并且队列中同时存在读线程和写线程,那么应该优先选择读线程,写线程,还是最先发出请求的线程?

  • 读线程插队 读线程插队到写线程之前,那么将提高并发性,但可能造成写线程发生饥饿问题

  • 重入性 读取所和写入锁是否可重入

  • 降级 写锁在不释放的的情况下获取读取锁,使得写锁降级为读锁

  • 升级 读取锁能否优于其他正在等待的读取线程的读线程而升级为一个写入锁

ReentrantReadWriteLock 为这两种锁都提供了可重入的加锁语义。

在公平的锁中,等待时间最长的线程将优先获得锁。如果这个锁由读线程持有,而另一个线程请求写入锁,那么其它读线程都不能获得读取锁,直到写线程使用完并释放了写入锁。在非公平的锁中,钱程获得许可的顺序是不确定的。写线程降级为读线程是可以的,但从读线程升级为写线程则是不可以的〈这样做会导致死锁〉。

public class ReadWriteMap<K,V> {
private final Map<K,V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
private final Lock w = lock.writeLock();

public ReadWriteMap(Map<K,V> map) {
this.map = map;
}

public V put(K key, V value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}

public V get(Object key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
}