【高并发】面试官:Java中提供了synchronized,为何还要提供Lock呢?

写在前面

在Java中提供了synchronized关键字来保证只有一个线程可以访问同步代码块。既然已经提供了synchronized关键字,那为什么在Java的SDK包中,还会提供Lock接口呢?这是否是重复造轮子,画蛇添足呢?今天,咱们就一块儿来探讨下这个问题。

再造轮子?

既然JVM中提供了synchronized关键字来保证只有一个线程可以访问同步代码块,为什么还要提供Lock接口呢?这是在重复造轮子吗?Java的设计者们为什么要这样作呢?让咱们一块儿带着疑问往下看。java

为什么提供Lock接口?

不少小伙伴可能会据说过,在Java 1.5版本中,synchronized的性能不如Lock,但在Java 1.6版本以后,synchronized作了不少优化,性能提高了很多。那既然synchronized关键字的性能已经提高了,那为什么还要使用Lock呢?函数

若是咱们向更深层次思考的话,就不难想到了:咱们使用synchronized加锁是没法主动释放锁的,这就会涉及到死锁的问题。性能

死锁问题

若是要发生死锁,则必须存在如下四个必要条件,四者缺一不可。优化

在这里插入图片描述

  • 互斥条件

在一段时间内某资源仅为一个线程所占有。此时如有其余线程请求该资源,则请求线程只能等待。this

  • 不可剥夺条件

线程所得到的资源在未使用完毕以前,不能被其余线程强行夺走,即只能由得到该资源的线程本身来释放(只能是主动释放)。spa

  • 请求与保持条件

线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其余线程占有,此时请求线程被阻塞,但对本身已得到的资源保持不放。线程

  • 循环等待条件

在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,造成一个进程等待环路,环路中每个进程所占有的资源同时被另外一个申请,也就是前一个进程占有后一个进程所深情地资源。设计

synchronized的局限性

若是咱们的程序使用synchronized关键字发生了死锁时,synchronized关键是是没法破坏“不可剥夺”这个死锁的条件的。这是由于synchronized申请资源的时候, 若是申请不到, 线程直接进入阻塞状态了, 而线程进入阻塞状态, 啥都干不了, 也释放不了线程已经占有的资源。code

然而,在大部分场景下,咱们都是但愿“不可剥夺”这个条件可以被破坏。也就是说对于“不可剥夺”这个条件,占用部分资源的线程进一步申请其余资源时, 若是申请不到, 能够主动释放它占有的资源, 这样不可剥夺这个条件就破坏掉了。blog

若是咱们本身从新设计锁来解决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的详细说明,小伙伴们自行查阅便可。

相关文章
相关标签/搜索