共计 3156 个字符,预计需要花费 8 分钟才能阅读完成。
写在后面
在 Java 中提供了 synchronized 关键字来保障只有一个线程可能拜访同步代码块。既然曾经提供了 synchronized 关键字,那为何在 Java 的 SDK 包中,还会提供 Lock 接口呢?这是不是反复造轮子,多此一举呢?明天,咱们就一起来探讨下这个问题。
再造轮子?
既然 JVM 中提供了 synchronized 关键字来保障只有一个线程可能拜访同步代码块,为何还要提供 Lock 接口呢?这是在反复造轮子吗?Java 的设计者们为何要这样做呢?让咱们一起带着疑难往下看。
为何提供 Lock 接口?
很多小伙伴可能会据说过,在 Java 1.5 版本中,synchronized 的性能不如 Lock,但在 Java 1.6 版本之后,synchronized 做了很多优化,性能晋升了不少。那既然 synchronized 关键字的性能曾经晋升了,那为何还要应用 Lock 呢?
如果咱们向更深层次思考的话,就不难想到了:咱们应用 synchronized 加锁是无奈被动开释锁的,这就会波及到死锁的问题。
死锁问题
如果要产生死锁,则必须存在以下四个必要条件,四者缺一不可。
- 互斥条件
在一段时间内某资源仅为一个线程所占有。此时若有其余线程申请该资源,则申请线程只能期待。
- 不可剥夺条件
线程所取得的资源在未应用结束之前,不能被其余线程强行夺走,即只能由取得该资源的线程本人来开释(只能是被动开释 )。
- 申请与放弃条件
线程曾经放弃了至多一个资源,但又提出了新的资源申请,而该资源已被其余线程占有,此时申请线程被阻塞,但对本人已取得的资源放弃不放。
- 循环期待条件
在产生死锁时必然存在一个过程期待队列 {P1,P2,…,Pn}, 其中 P1 期待 P2 占有的资源,P2 期待 P3 占有的资源,…,Pn 期待 P1 占有的资源,造成一个过程期待环路,环路中每一个过程所占有的资源同时被另一个申请,也就是前一个过程占有后一个过程所深情地资源。
synchronized 的局限性
如果咱们的程序应用 synchronized 关键字产生了死锁时,synchronized 要害是是无奈毁坏“不可剥夺”这个死锁的条件的。这是因为 synchronized 申请资源的时候,如果申请不到,线程间接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也开释不了线程曾经占有的资源。
然而,在大部分场景下,咱们都是心愿“不可剥夺”这个条件可能被毁坏。也就是说对于“不可剥夺”这个条件,占用局部资源的线程进一步申请其余资源时,如果申请不到,能够被动开释它占有的资源,这样不可剥夺这个条件就毁坏掉了。
如果咱们本人从新设计锁来解决 synchronized 的问题,咱们该如何设计呢?
解决问题
理解了 synchronized 的局限性之后,如果是让咱们本人实现一把同步锁,咱们该如何设计呢?也就是说,咱们在设计锁的时候,要如何解决 synchronized 的局限性问题呢?这里,我感觉能够从三个方面来思考这个问题。
(1)可能响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦产生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程可能响应中断信号,也就是说当咱们给阻塞的线程发送中断信号的时候,可能唤醒它,那它就有机会开释已经持有的锁 A。这样就毁坏了不可剥夺条件了。
(2)反对超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个谬误,那这个线程也有机会开释已经持有的锁。这样也能毁坏不可剥夺条件。
(3)非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是间接返回,那这个线程也有机会开释已经持有的锁。这样也能毁坏不可剥夺条件。
体现在 Lock 接口上,就是 Lock 接口提供的三个办法,如下所示。
`// 反对中断的 API
void lockInterruptibly() throws InterruptedException;
// 反对超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 反对非阻塞获取锁的 API
boolean tryLock();`
- lockInterruptibly()
反对中断。
- tryLock() 办法
tryLock() 办法是有返回值的,它示意用来尝试获取锁,如果获取胜利,则返回 true,如果获取失败(即锁已被其余线程获取),则返回 false,也就说这个办法无论如何都会立刻返回。在拿不到锁时不会始终在那期待。
- tryLock(long time, TimeUnit unit) 办法
tryLock(long time, TimeUnit unit) 办法和 tryLock() 办法是相似的,只不过区别在于这个办法在拿不到锁时会期待肯定的工夫,在工夫期限之内如果还拿不到锁,就返回 false。如果一开始拿到锁或者在期待期间内拿到了锁,则返回 true。
也就是说,对于死锁问题,Lock 可能毁坏不可剥夺的条件,例如,咱们上面的程序代码就毁坏了死锁的不可剥夺的条件。
`public class TansferAccount{private Lock thisLock = new ReentrantLock();
private Lock targetLock = new ReentrantLock();
// 账户的余额
private Integer balance;
// 转账操作
public void transfer(TansferAccount target, Integer transferMoney){boolean isThisLock = thisLock.tryLock();
if(isThisLock){
try{boolean isTargetLock = targetLock.tryLock();
if(isTargetLock){
try{if(this.balance >= transferMoney){
this.balance -= transferMoney;
target.balance += transferMoney;
}
}finally{targetLock.unlock}
}
}finally{thisLock.unlock();
}
}
}
}`
例外,Lock 上面有一个 ReentrantLock,而 ReentrantLock 反对偏心锁和非偏心锁。
在应用 ReentrantLock 的时候,ReentrantLock 中有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的偏心策略,如果传入 true 就示意须要结构一个偏心锁,反之则示意要结构一个非偏心锁。如下代码片段所示。
`// 无参构造函数:默认非偏心锁
public ReentrantLock() {sync = new NonfairSync();
}
// 依据偏心策略参数创立锁
public ReentrantLock(boolean fair){sync = fair ? new FairSync() : new NonfairSync();}`
锁的实现在实质上都对应着一个入口期待队列,如果一个线程没有取得锁,就会进入期待队列,当有线程开释锁的时候,就须要从期待队列中唤醒一个期待的线程。如果是偏心锁,唤醒的策略就是谁期待的工夫长,就唤醒谁,很偏心;如果是非偏心锁,则不提供这个偏心保障,有可能等待时间短的线程反而先被唤醒。而 Lock 是反对偏心锁的,synchronized 不反对偏心锁。
最初,值得注意的是,在应用 Lock 加锁时,肯定要在 finally{} 代码块中开释锁,例如,上面的代码片段所示。
`try{lock.lock();
}finally{lock.unlock();
}`
注:其余 synchronized 和 Lock 的具体阐明,小伙伴们自行查阅即可。