本书最后一部分:“并发高级主题“,其内容如下:
- 第13章:显式锁ReentrantLock;
- 第14章:构建自定义同步器;
- 第15章:非阻塞同步器;
- 第16章:java内存模型
ReentrantLock
java 5之前,java语言唯一的线程同步手段就是 synchronized 和 volatile;Java 5增加了ReentrantLock,它不是java监视锁的替代品,而是在后者不满足需求时,提供更丰富的功能。
java为锁定义了一个通用的接口Lock,提供了相关锁操作方法如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
除了lock&unlock操作外,还提供了可中断加锁、尝试加锁、限时加锁操作(Condition下章讨论)。所有Lock实现类,提供了与监视锁相同的内存可见性保证(内存可见性在16章详细介绍),但在加锁、调度、性能特性方面各有不同。
ReentrantLock实现了Lock接口,它的加锁语义和监视器锁是一致的,它们之间的相同点如下:
- ReentrantLock和监视器锁都是可重入的;
- ReentrantLock.lock相当于进入synchronized代码块;
- ReentrantLock.unlock相当于离开synchronized代码块。
不同点在于ReentrantLock提供的额外功能:
- 线程调用ReentrantLock.lockInterruptibly加锁时如果被阻塞,可响应interrupt;
- ReentrantLock.tryLock尝试加锁而不阻塞线程(限时版本不永久阻塞,可响应interrupt),可避免死锁;
- 可以跨代码块使用ReentrantLock(一个方法加锁、另一个解锁),场景更加丰富。
ReentrantLock的这些新功能,使得它可以胜任某些监视器锁无法胜任的同步需求。
可中断加锁
在第7章讨论”异步任务取消“这个话题时,我们学习到,Excecutor框架通过java中断来取消正在执行中的任务,但成功与否取决于任务是否响应中断。而java监视器锁是不会响应中断,因此任务一旦使用了监视器锁,理论上存在不可中断的风险,ReentrantLock.lockInterruptibly则避免了此种风险。
还有就是当线程陷入死锁,如果死锁的是ReentrantLock.lockInterruptibly操作,还可以通过外部来中断该线程。要是监视器锁陷入死锁,就只有重启进程一条路了。
除非该锁有可能被长时间持有,否则,没有必要担心监视器锁不可中断的特性。
轮询&限时加锁
ReentrantLock.tryLock加锁失败立即返回而不陷入阻塞,提供了一种主动避免死锁的方式。下面的示例通过该特性来改进账户转账功能:
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);
}
}
上面代码的要点如下(避免死锁的技巧):
- 使用ReentrantLock.tryLock而不是监视器锁来保护账号对象;
- 由于tryLock在失败后立即返回,所以通过while循环来不断重试,知道超时(timeout参数);
- 先执行fromAcct.lock.tryLock(),再执行toAcct.lock.tryLock(),两者都成功才执行转账;如果第二个锁获取失败,立即释放第一个锁;
- 如果加锁失败,sleep一段随机的时间再重试(否则可能陷入上一章介绍的所谓“活锁“)
跨代码块加锁解锁
java监视器锁只能用在同一个代码块内,在更复杂的场景下,可能并不满足需求。第十一章展示的锁拆分&锁分离技术,一般来说,需要显式锁才能实现。
监视器锁 VS ReentrantLock
性能
在java 5引入ReentrantLock时,它的性能比监视器锁明显要好。对于同步机制而言,锁在发生竞争时的性能表现是程序可伸缩性的一个关键因素;如果处理锁竞争耗费时间更多,用于务执行的时间就更少,而且进一步加大锁竞争发生概率,使程序在高并发场景下的性能急剧下降。
不过Java 6对监视器进行了大幅优化,使得监视器锁和ReentrantLock的性能相差无几。由于监视器锁是JVM内置的,每代JVM都可能会对监视器锁进行优化,而且JVM还能动态进行锁粗化等运行时优化(对显式锁JVM很难进行运行时优化),因此Java7以上版本的监视器锁的性能已经超过ReentrantLock。
复杂性
transferMoney所展示的ReentrantLock用法想必令大家印象深刻,相比synchronized代码块,使用ReentrantLock代码的复杂度要大得多,也更难维护。更加重要的是,ReentrantLock必须手动释放,如果忘记或由于异常没有释放,就会造成死锁。
综上,除非需要ReentrantLock的额外功能,应该优先使用监视器锁。
锁的公平性
所谓锁的公平性是指,当多个线程都在等待同一把锁时,哪个线程优先获得锁的规则。如果能保证按着线程请求锁的顺序来获得锁,那么锁是公平的,否则锁是不公平的。不管锁是否公平的,等待锁的线程会形成一个队列,非公平锁并非刻意制造乱序,而是当某个线程尝试加锁时,发现锁是恰好是空闲的,会绕过队列直接成功加锁。因此,实际上不公平锁大体上也是公平的,只是不保证而已。
java监视器锁是不公平的,ReentrantLock可以指定公平性;但是公平锁的性能会差很多,可以推演一下A,B,C线程竞争同一个锁的场景:
- 假设线程A当前拥有锁,线程B正在等待锁;
- 下一个时刻线程C尝试获取锁,此刻刚好A释放了锁;
- 如果锁是非公平的,C加锁成功,继续运行,C释放锁后,B线程被唤醒并获得锁;
- 如果锁是公平的,C被挂起,B被唤醒,B加锁成功且运行完毕后释放锁,C再被唤醒并运行;
上面的分析揭示了,公平锁加锁过程相对更复杂,且导致额外的两次上下文切换;在高并发的情况下,上面的场景发生频率是很高的,所以公平锁对性能伤害不可忽视。
绝大多数情况下,只要所有线程最终能获得锁,绝对的公平性并没有那么重要;只有那些持有锁时间较长、加锁频率不高,且确实需要公平性的场景,才需要公平锁。
ReadWriteLock
ReentrantLock实现了标准的互斥锁,但是在有些场景下,并不需要如此强的互斥性。在对数据状态”读多写少“,且只需要将”写-写“、”写-读“串行化的场景,可使用ReadWriteLock。
ReadWriteLock是一个接口:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReadWriteLock实际包含一个读锁,一个写锁,读锁和写锁之间存在同步交互;ReadWriteLock的具体实现需要考虑以下几个方面:
- 写锁释放倾向性:当一个写锁释放时,如果同时有写线程和读线程在等待,优先策略如何定?
- 读锁抢占:如果当前有一个读锁,且有一个线程尝试对写锁加锁;此时又来一个线程尝试加读锁,应该立即成功还是等待?
- 重入性:读锁和写锁都是重入的吗?
- 降级:一个线程持有写锁,是否能继续获取读锁?如果允许,相当于将写锁降级为读锁;
- 升级:一个读锁是否能够升级为读锁?
ReentrantReadWriteLock是ReadWriteLock的实现者,与ReentrantLock一样,它也是可重入的,也可以指定公平性。如果ReentrantReadWriteLock是公平的,那么严格按照线程加锁的顺序来,否则效率优先。ReentrantReadWriteLock支持锁降级,但不支持升级,因为升级容易产生死锁。
ReadWriteLock在并发读频繁,并发写不频繁,且持有锁的时间相对较长的情况下能替代互斥锁改善性能;否则的话,由于ReadWriteLock自身更复杂一些,性能可能更差。在使用时,需要通过测试来对比,如果ReadWriteLock并没有优势,还是用ReentrantLock或监视器锁更好。
总结
相对监视器锁,显式的锁能够提供更多的功能,适应更灵活的场景。ReentrantLock与监视器锁语义一致,提供了几个额外功能:尝试加锁、可中断加锁、限时加锁、公平锁,且能够跨代码块加锁解锁。ReentrantLock并不是用来取代监视器锁的,仅在后者不满足需求时才考虑ReentrantLock。
ReadWriteLock是java定义的另一种显式锁,它在”读多写少“的场景下,可能提高程序可伸缩性,需要测试来证明效果。