Lock与ReentrantLock
Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time,TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
ReentrantLock实现了Lock接口 并提供了与synchronized相同的互斥性和内存可见性 在获取ReentrantLock时 有着与进入同步代码块相同的内存语义 在释放ReentrantLock时 同样有着与退出同步代码块相同的内存语义 此外 与synchronized一样 ReentrantLock还提供了可重入的加锁语义 ReentrantLock支持在Lock接口中定义的所有获取锁模式 并且与synchronized相比 它还为处理锁的不可用性问题提供了更高的灵活性
使用ReentrantLock来保护对象状态
Lock lock = new ReentrantLock();
...
lock.lock();
try {
//更新对象状态
//捕获异常 并在必要时恢复不变性条件
} finally {
lock.unlock();
}
如果没有使用finally来释放Lock 那么相当于启动了一个定时炸弹 当 炸弹爆炸 时 将很难追踪到最初发生错误的位置 因为没有记录应该释放锁的位置和时间 这就是ReentrantLock不能完全替代synchronized的原因:它更加 危险 因为当程序的执行控制离开被保护的代码块时 不会自动清除锁 虽然在finally块中释放锁并不困难 但也可能忘记
轮询锁与定时锁
可定时的与可轮询的锁获取模式是由tryLock方法实现的 与无条件的锁获取模式相比 它具有更完善的错误恢复机制 在内置锁中 死锁是一个严重的问题 恢复程序的唯一方法是重新启动程序 而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序 可定时的与可轮询的锁提供了另一种选择:避免死锁的发生
如果不能获得所有需要的锁 那么可以使用可定时的或可轮询的锁获取方式 从而使你重新获得控制权 它会释放已经获得的锁 然后重新尝试获取所有锁(或者至少会将这个失败记录到日志 并采取其他措施)
通过tryLock来避免锁顺序死锁
public class DeadlockAvoidance {
private static Random rnd = new Random();
public boolean transferMoney(Account fromAcct,
Account toAcct,
DollarAmount amount,
long timeout,
TimeUnit unit)
throws InsufficientFundsException, InterruptedException {
long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
long randMod = getRandomDelayModulusNanos(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);
}
}
private static final int DELAY_FIXED = 1;
private static final int DELAY_RANDOM = 2;
static long getFixedDelayComponentNanos(long timeout, TimeUnit unit) {
return DELAY_FIXED;
}
static long getRandomDelayModulusNanos(long timeout, TimeUnit unit) {
return DELAY_RANDOM;
}
static class DollarAmount implements Comparable<DollarAmount> {
public int compareTo(DollarAmount other) {
return 0;
}
DollarAmount(int dollars) {
}
}
class Account {
public Lock lock;
void debit(DollarAmount d) {
}
void credit(DollarAmount d) {
}
DollarAmount getBalance() {
return null;
}
}
class InsufficientFundsException extends Exception {
}
}
在实现具有时间限制的操作时 定时锁同样非常有用 当在带有时间限制的操作中调用了一个阻塞方法时 它能根据剩余时间来提供一个时限 如果操作不能在指定的时间内给出结果 那么就会使程序提前结束 当使用内置锁时 在开始请求锁后 这个操作将无法取消 因此内置锁很难实现带有时间限制的操作
带有时间限制的加锁
public class TimedLocking {
private Lock lock = new ReentrantLock();
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();
}
}
private boolean sendOnSharedLine(String message) {
/* send something */
return true;
}
long estimatedNanosToSend(String message) {
return message.length();
}
}
可中断的锁获取操作
正如定时的锁获取操作能在带有时间限制的操作中使用独占锁 可中断的锁获取操作同样能在可取消的操作中使用加锁
可中断的锁获取操作的标准结构比普通的锁获取操作略微复杂一些 因为需要两个try块 (如果在可中断的锁获取操作中抛出了InterruptedException 那么可以使用标准的try finally加锁模式)
可中断的锁获取操作
public class InterruptibleLocking {
private Lock lock = new ReentrantLock();
public boolean sendOnSharedLine(String message)
throws InterruptedException {
lock.lockInterruptibly();
try {
return cancellableSendOnSharedLine(message);
} finally {
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String message) throws InterruptedException {
/* send something */
return true;
}
}
非块结构的加锁
在内置锁中 锁的获取和释放等操作都是基于代码块的——释放锁的操作总是与获取锁的操作处于同一个代码块 而不考虑控制权如何退出该代码块 自动的锁释放操作简化了对程序的分析 避免了可能的编码错误 但有时候需要更灵活的加锁规则
通过降低锁的粒度可以提高代码的可伸缩性 锁分段技术在基于散列的容器中实现了不同的散列链 以便使用不同的锁 我们可以通过采用类似的原则来降低链表中锁的粒度 即为每个链表节点使用一个独立的锁使不同的线程能独立地对链表的不同部分进行操作 每个节点的锁将保护链接指针以及在该节点中存储的数据 因此当遍历或修改链表时 我们必须持有该节点上的这个锁 直到获得了下一个节点的锁 只有这样 才能释放前一个节点上的锁 称之为连锁式加锁(Hand-Over-Hand Locking)或者锁耦合(Lock Coupling)
性能考虑因素
当把ReentrantLock添加到Java5.0时 它能比内置锁提供更好的竞争性能 对于同步原语来说 竞争性能是可伸缩性的关键要素:如果有越多的资源被耗费在锁的管理和调度上 那么应用程序得到的资源就越少 锁的实现方式越好 将需要越少的系统调用和上下文切换 并且在共享内存总线上的内存同步通信量也越少 而一些耗时的操作将占用应用程序的计算资源
性能是一个不断变化的指标 如果在昨天的测试基准中发现X比Y更快 那么在今天就可能已经过时了
公平性
在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁 在公平的锁上 线程将按照它们发出请求的顺序来获得锁 但在非公平的锁上 则允许 插队:当一个线程请求非公平的锁时 如果在发出请求的同时该锁的状态变为可用 那么这个线程将跳过队列中所有的等待线程并获得这个锁 (在Semaphore中同样可以选择采用公平的或非公平的获取顺序) 非公平的ReentrantLock并不提倡 插队 行为 但无法防止某个线程在合适的时候进行 插队 在公平的锁中 如果有另一个线程持有这个锁或者有其他线程在队列中等待这个锁 那么新发出请求的线程将被加入队列中 在非公平的锁中 只有当锁被某个线程持有时 新发出请求的线程才会被放入队列中
在激烈竞争的情况下 非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟 假设线程A持有一个锁 并且线程B请求这个锁 由于这个锁已被线程A持有 因此B将被挂起 当A释放锁时 B将被唤醒 因此会再次尝试获取锁 与此同时 如果C也请求这个锁 那么C很可能会在B被完全唤醒之前获得 使用以及释放这个锁 这样的情况是一种 双赢 的局面:B获得锁的时刻并没有推迟 C更早地获得了锁 并且吞吐量也获得了提高
当持有锁的时间相对较长 或者请求锁的平均时间间隔较长 那么应该使用公平锁 在这些情况下 插队 带来的吞吐量提升(当锁处于可用状态时 线程却还处于被唤醒的过程中)则可能不会出现
在synchronized和ReentrantLock之间进行选择
与显式锁相比 内置锁仍然具有很大的优势 内置锁为许多开发人员锁熟悉 并且简洁紧凑 而且在许多现有的程序中都已经使用了内置锁——如果这两种机制混合使用 那么不仅容易令人困惑 也容易发生错误 ReentrantLock的危险性比同步机制要高 如果忘记在finally块中调用unlock 那么虽然代码表面上能正常运行 但实际上已经埋下了一颗定时炸弹 并很有可能伤及其他代码 仅当内置锁不能满足需求时 才可以考虑使用ReentrantLock
在一些内置锁无法满足需求的情况下 ReentrantLock可以作为一种高级工具 当需要一些高级功能时才应该使用ReentrantLock 这些功能包括:可定时的 可轮询的与可中断的锁获取操作 公平队列 以及非块结构的锁 否则 还是应该优先使用synchronized
读-写锁
ReentrantLock实现了一种标准的互斥锁:每次最多只有一个线程能持有ReentrantLock 但对于维护数据的完整性来说 互斥通常是一种过于强硬的加锁规则 因此也就不必要地限制了并发性 互斥是一种保守的加锁策略 虽然可以避免 写/写 冲突和 写/读 冲突 但同样也避免了 读/读 冲突 在许多情况下 数据结构上的操作都是 读操作 ——虽然它们也是可变的并且在某些情况下被修改 但其中大多数访问操作都是读操作 此时 如果能够放宽加锁需求 允许多个执行读操作的线程同时访问数据结构 那么将提升程序的性能 只有每个线程都能确保读取到最新的数据 并且在读取数据时不会有其他的线程修改数据 那么就不会发生问题 在这种情况下就可以使用读/写锁:一个资源可以被多个读操作访问 或者被一个写操作访问 但两者不能同时进行
ReadWriteLock接口
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
在读取锁和写入锁之间的交互可以采用多种实现方式 ReadWriteLock中的一些可选实现包括:
- 释放优先
- 读线程插队
- 重入性
- 降级
- 升级
当锁的持有时间较长并且大部分操作都不会修改被守护的资源时 那么读-写锁能提高并发性
用读-写锁来包装Map
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 remove(Object key) {
w.lock();
try {
return map.remove(key);
} finally {
w.unlock();
}
}
public void putAll(Map<? extends K, ? extends V> m) {
w.lock();
try {
map.putAll(m);
} finally {
w.unlock();
}
}
public void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
public V get(Object key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
public int size() {
r.lock();
try {
return map.size();
} finally {
r.unlock();
}
}
public boolean isEmpty() {
r.lock();
try {
return map.isEmpty();
} finally {
r.unlock();
}
}
public boolean containsKey(Object key) {
r.lock();
try {
return map.containsKey(key);
} finally {
r.unlock();
}
}
public boolean containsValue(Object value) {
r.lock();
try {
return map.containsValue(value);
} finally {
r.unlock();
}
}
}
小结
与内置锁相比 显式的Lock提供了一些扩展功能 在处理锁的不可用性方面有着更高的灵活性 并且对队列行有着更好的控制 但ReentrantLock不能完全替代synchronized 只有在synchronized无法满足需求时 才应该使用它
读-写锁允许多个读线程并发地访问被保护的对象 当访问以读取操作为主的数据结构时 它能提高程序的可伸缩性