写在后面

在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接口提供的三个办法,如下所示。

`// 反对中断的APIvoid lockInterruptibly() throws InterruptedException;// 反对超时的APIboolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 反对非阻塞获取锁的APIboolean 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的具体阐明,小伙伴们自行查阅即可。