并发编程学习笔记之显示锁(十)
ReentrantLock(重进入锁)并不是作为内部锁(synchronized)机制的替代,而是当内部锁被证明受到局限时,提供可选择的高级特性.
1. 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();
}
与内部加锁机制不同,Lock提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显示的.
Lock的实现必须提供具有与内部加锁相同的内存可见性的语义.但是加锁的语义、调度算法、顺序保证,性能特性这些可以不同.
ReentrantLock实现了Lock接口,提供了与synchronized相同的互斥和内存可见性的保证.
获得ReentrantLock的锁与进入synchronized块有着相同的内存语义,释放ReentrantLock锁与退出synchronized块有相同的内存语义.
ReentrantLock提供了与synchronized一样的可重入加锁的语义.ReentrantLock支持Lock接口定义的所有获取锁的模式.
一句话synchronized能做的,ReentrantLock都能做,但是ReentrantLock为处理不可用的锁提供了更多灵活性(好吧,ReentrantLock写起来比较麻烦)
为什么要使用显示锁
内部锁在大部分情况下都能很好地工作,但是有一些功能上的局限--不能中断那些正在等待获取锁的线程,并且在请求锁失败的情况下,必须无限等待.
内部锁必须在获取他们的代码块中被释放:这很好地简化了代码,与异常处理机制能够良好的互动,但是在某些情况下,一个更灵活的加锁机制提供了更好的活跃度和性能.
public class LockTest {
Lock lock = new ReentrantLock();
public void testLock(){
lock.Lock();
try {
// 需要加锁的代码..
}finally {
lock.unlock();
}
}
}
这个模式在某种程度上比使用内部锁更加复杂:锁必须在finally块中释放.
另一方面,如果锁守护的代码在try块之外抛出了异常,它将永远都不会被释放了;
如果对象能够被置于不一致的状态,可能需要额外的try-catch,或try-finally块.
显示的lock的缺点
使用lock之后必须unlock释放锁,这也是ReentrantLock不能完全替代synchronized的原因.
它更加危险,因为当程序的控制权离开了守护的块时,不会自动清除锁.
1.1 可轮询和可定时的锁请求
可定时的与可轮询的锁获取模式,是由tryLock方法实现,与无条件的锁获取相比,它具有更完善的错误恢复机制.
使用内部锁,发生死锁时唯一的恢复方法是重启程序,唯一的预防方法是在构建程序时不要出错,所以不可能允许不一致的锁顺序.
可定时的与可轮询的锁提供了另一个选择,可以规避死锁的发生.
使用方式:
public class LockSample {
//创建一个锁的实例
Lock lock = new ReentrantLock();
public void methodA(){
lock.lock();
try {
System.out.println("执行了方法A");
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void methodB(){
lock.lock();
try {
System.out.println("执行了方法B");
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String [] args){
LockSample lockSample = new LockSample();
lockSample.methodA();
//methodB()方法必须在锁可用的时候才会执行
lockSample.methodB();
}
}
使用tryLock能解决第九篇博客死锁,提到过的动态的顺序死锁问题.
public class LockTest {
public static void main(String [] args){
LockTest lockTest = new LockTest();
Account fromAccount = new Account();
Account toAccount = new Account();
Account account = new Account();
//开启一个新线程,获取两个用户的锁,这个方法是假设,对象的锁已经被获得用的.
new Thread(){
@Override
public void run(){
//这两个方法的内部实现就是Thread.sleep()将代码阻塞住.
fromAccount.credit(account);
toAccount.dedit(account);
}
}.start();
lockTest.transferMoney(fromAccount,toAccount,account);
}
public void transferMoney(Account fromAccount,Account toAccount,Account account){
while(true){
// lock.tryLock()返回一个布尔值,告诉你当前的锁是否可用,如果可用往下走
if(fromAccount.lock.tryLock()){
try {
if (toAccount.lock.tryLock()){
try {
//走到这里,证明两个锁都可用,可以进行转账操作.
fromAccount.credit(account);
toAccount.dedit(account);
}finally {
toAccount.lock.unlock();
}
}
}finally {
fromAccount.lock.unlock();
}
}
}
}
}
Account的内部实现:
public class Account {
public Lock lock = new ReentrantLock();
public void credit(Account account) {
lock.lock();
try {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
public void dedit(Account account) {
lock.lock();
try {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
定时锁可以在时间预算内设定相应的超时,如果活动子啊期待的时间内没能获得结果,这个机制是程序能够提前返回.
而使用内部锁一旦开始请求,锁就不能停止了,所以内部锁为实现具有时限的活动带来了风险.
.tryLock方法还有一个重载版本,可以设定等待的时间:
lock.tryLock(4, TimeUnit.SECONDS)
1.2 可中断的锁获取操作
lock.lockInterruptibly上的锁,是可以响应中断的:
public class LockSample {
//创建一个锁的实例
Lock lock = new ReentrantLock();
public void testInterruptibly(){
try {
lock.lockInterruptibly();
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void test(){
System.out.println("lock.tryLock() = " + lock.tryLock());
try {
System.out.println("lock.tryLock(4,TimeUnit.SECONDS) = " + lock.tryLock(4, TimeUnit.SECONDS));;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodA(){
lock.lock();
try {
System.out.println("执行了方法A");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void methodB(){
lock.lock();
try {
System.out.println("执行了方法B");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String [] args){
Long startTime = System.nanoTime();
LockSample lockSample = new LockSample();
Thread thread = Thread.currentThread();
new Thread(){
@Override
public void run(){
try {
//休眠两秒,执行中断
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
//这里本来是休眠5秒的,因为上面直接中断了,可以看下面的endtime是两秒,证明了可以被中断
lockSample.testInterruptibly();
Long endTime = startTime - System.nanoTime();
System.out.println("endTime = " + endTime);
}
}
2. 对性能的考量
ReentrantLock提供的竞争上的性能要远远优于内部锁.
对于同步原语而言,竞态时的性能是可伸缩性的关键:若果有越多的资源花费在锁的管理和调度上,那程序执行的时间就越少.
在Java5.0中,ReentrantLock相比于synchronized能给吞吐量带来相当不错的提升,但是在Java6中,这两者非常接近.
也就是说之前选择显示锁,还有性能方面的考量,但是现在显示锁和synchronized已经差不多了.
3. 公平性
ReentrantLock构造函数提供了两种公平性的选择:
- 创建非公平锁(默认)
- 公平锁
公平锁:如果锁已经被其他线程占有,新的请求线程会加入到等待队列,或者已经有一些线程在等待锁了;
非公平锁: 非公平锁允许闯入,当请求这样的锁时,如果锁的状态变为可用,线程的请求可以在等待线程的队列中向前跳跃,获得该锁.(Semaphore同样提供了公平和非公平的获取顺序).在非公平的锁中,线程只有当锁正在被占用时才会等待.
为什么要使用不公平锁
当发生加锁的时候,公平会因为挂起和重新开始线程的代价带来巨大的性能开销.
在多数情况下,非公平锁的优势超过了公平的排队.
在竞争激烈的情况下,闯入锁比公平锁性能好的原因之一是:挂起的线程重新开始,与它真正开始运行,两者之间会产生严重的延迟.
比较公平锁和非公平锁,使用的例子:
假设线程A持有一个锁,线程B请求该锁.因为此时锁正在使用中,线程B被挂起,当A释放锁后,B重新开始.与此同时,如果C请求锁,那么C得到了很好的机会获得这个锁,使用它,并且甚至可能在B被唤醒前就已经释放该锁了.
在这样的情况下,各方面都获得了成功:B并没有比其他任何线程晚得到锁,C更早的得到了锁,吞吐量得到了改进.
如果持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么使用公平锁是比较好的.
4. 在synchronized和ReentrantLock之间进行选择
在内部锁不能够满足使用时,ReentrantLock才被作为更高级的工具,当你需要以下高级特性时,才应该使用:
可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁,否则,请使用synchronized.
5. 读-写锁
读-写锁:一个资源能够被多个读者访问,或者被一个写者访问,两者不能同时进行.
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
ReadWriteLock暴露了两个Lock对象,一个用来读,另一个用来写.读取ReadWriteLock锁守护的数据,你必须首先获得读取的锁,当需要修改ReadWriteLock守护的数据时,你必须首先获得写入的锁.
读-写锁实现的加锁策略允许多个同时存在的读者,但是只允许一个写者.
读-写锁的设计是用来进行性能改进的,使得特定情况下能够有更好的并发性.
多处理器系统中,频繁的访问主要为读取数据结构的时候,读-写锁能够改进性能;
在其他情况下运行的情况比独占的锁要稍差一些,这归因于它更大的复杂性.
ReentrantReadWriteLock也能被构造为非公平(默认)或公平的.
公平: 在公平的锁中,选择权交给等待时间最长的线程;如果锁由读者获得,而一个线程请求写入锁,那么不在允许读者获得读取锁,直到写者被受理,并且已经释放了写入锁.
非公平: 线程允许访问的顺序是不定的.由写者降级为读者是允许的;从读者升级为写者是不允许的(尝试这样的行为会导致死锁).
使用读写锁的情况
当锁被持有的时间相对较长,并且大部分操作都不会改变锁守护的资源,那么读-写锁能够改进并发性.
使用读-写锁包装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();
}
}
//remove(),putAll(),clear()使用w.lock
public V get(Object key){
r.lock();
try {
return map.get(key);
}finally {
r.unlock();
}
}
//其他的只读map使用r.lock
}
总结
显示的Lock与内部锁相比提供了一些扩展的特性,包括处理不可用的锁时更好的灵活性,以及对队列行为更好的控制,但是ReentrantLock不能完全替代synchronized;只有当你需要synchronized没能提供的特性时才应该使用.
读-写锁允许多个读者并发访问被守护的对象,当访问多为读取数据结构的时候,它具有改进可伸缩性的能力.
喜欢我的博客就请点赞+【关注】一波