面试官:谈谈ReentrantLock与synchronized的区别?

这篇文章是并发编程系列第9集,上一次并发系列第八集通过ReentrantLock 独占锁,讲解了AQS 的基本实现原理,这次第10集开个支线,讲讲面试经常会被问的基于 AQS实现的 ReentrantLock 与 synchronized 的区别。

暖场热身

上海某核心商务楼里,正发生着一起求职者和面试官的较量。

面试官:你先自我介绍一下。

安琪拉:面试官你好,我是草丛三婊,最强中单(妲己不服),草地摩托车车手,第21套广播体操推广者,火的传人安琪拉,这是我的简历,可能有点沉,您拿好了,也就三十多页。

面试官:看你简历上写熟悉并发编程,熟悉到什么程度?

安琪拉:精通。对。。。问就是“精通”,头铁。

面试官:谈谈ReentrantLock与synchronized的区别?_第1张图片 嘿嘿

面试官:用过 synchronized 的吧?

安琪拉:用过,巴拉巴拉讲一大堆来之前背好的八股。

面试官:不错不错,ReentrantLock 用过吧?

安琪拉:用过,巴拉巴拉讲一大堆(这里不清楚的去背第8集)。

正式对决

面试官:那你一般synchronized 用的多还是 ReentrantLock用的多?

安琪拉:用的都多

面试官:它们2 个有区别吗?什么时候用 synchronized, 什么时候用 ReentrantLock?

安琪拉:有区别,synchronized 属于非公平锁,不支持锁共享,功能比较单一。

ReentrantLock 功能比较丰富,比如需要用到公平锁、尝试加锁,带超时时间的获取锁,获取锁支持响应中断我就会用ReentrantLock。

面试官:【心想,小伙子基本功可以的】,那你能分别讲讲刚才你说的这几种的详细用法吗? 最好说说具体用的方法。

安琪拉:【问的还有点深。。。】你想先听哪个?

面试官:【有点东西】 那你讲讲ReentrantLock 支持的可以尝试加锁是意思?

安琪拉: 如果把加锁比作俘获女孩子的芳心。

synchronized 就是个痴心专一男,一旦发起加锁流程,就一心等待,即使这时女孩有男朋友(其他线程抢占),他还是会一直等待。

ReentrantLock 不一样,他有个技能,叫 tryLock,尝试获取女孩子的心,发现一旦有其他竞争对象,他立马折返撤退。

而且这个tryLock 还有个特殊技能,获取锁支持带超时时间,如果女孩在指定时间内没给机会,就会返回。

说的好听,ReentrantLock 很灵活应变,不好听,ReentrantLock 有点渣。

面试官:【陷入沉思中,原来synchronized 竟是我自己。。。】

安琪拉: 面试官,面试官,你怎么啦?

面试官:【回过神来】哦哦,你继续说,那你说下尝试加锁的实现机制。

安琪拉:很简单啦,上代码。返回尝试加锁是否成功。

public boolean tryLock() {
 return sync.nonfairTryAcquire(1);
}

这里很有意思,刚开始看源码的时候,我刚开始不理解为什么 nonfairTryAcquire(int acquires) 这个方法放在公共的 Sync 类里面,而不是放在非公平锁里面,如下图

面试官:谈谈ReentrantLock与synchronized的区别?_第2张图片

函数名毕竟是 nonfair 打头的,后来看了 tryLock 的代码就懂了,无论是公平锁还是非公平锁,tryLock 都是调用的 Sync 的 nonfairTryAcquire 这个方法。

所以把 nonfairTryAcquire 翻译成非公平的尝试获取锁不是很恰当,因为它不是NonfairSync 的私有方法。

面试官:那你说下ReentrantLock 公平锁的实现机制。

安琪拉:ok。ReentrantLock 公平锁, 请看代码

//初始化ReentrantLock 的时候传入指定锁类型的boolean 值
ReentrantLock lock = new ReentrantLock(true);

//构造函数,内部可以区分公平非公平
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

面试官:能具体讲讲公平锁和非公平锁在实现机制上的区别吗?

安琪拉:可以,区别其实很简单。

公平锁,加锁前会先看下前面有没有其他线程在排队,如果前面有其他线程排队,就乖乖排在队尾。

但是非公平锁的流程是他不管前面有没有线程在排队,都先尝试获取一下锁,获取成功就直接占用。

下面是公平锁和非公平锁的二段获取锁的代码,区别很简单,公平锁多了个 hasQueuedPredecessors() 调用,先看一下队列里面是不是有其他排队者。

//公平锁
protected final boolean tryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
  int c = getState();
  if (c == 0) {
    // !hasQueuedPredecessors() 代表队列没有其他等待获取锁的排队线程
    if (!hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
      setExclusiveOwnerThread(current);
      return true;
    }
  }
  else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0)
      throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
  }
  return false;
}

//非公平锁
final boolean nonfairTryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
  int c = getState();
  if (c == 0) {
    //没有这个方法,直接 CAS 获取锁
    if (compareAndSetState(0, acquires)) {
      setExclusiveOwnerThread(current);
      return true;
    }
  }
  else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0) // overflow
      throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
  }
  return false;
}

面试官:那你再说说ReentrantLock 带超时时间的获取锁的原理?

安琪拉:可以,先把超时时间转换成nano 时间单位,然后调用 doAcquireNanos 方法。

public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

面试官:谈谈ReentrantLock与synchronized的区别?_第3张图片

这个方法的主要流程和第8 集说的获取锁流程类似,只是加了一个超时时间的判断,函数入口会先用当前时间加超时间隔时间得到一个到期时间,然后在获取锁的for 循环体中判断是否超时。

大家注意到调用shouldParkAfterFailedAcquire 方法判断是否需要park的时候,如果返回true 会再判断nanosTimeout > spinForTimeoutThreshold

spinForTimeoutThreshold 这个参数是什么意思?

这个值是自旋的最小阈值,这里被Doug Lea 设置成了1000,表示1000纳秒,也就是说如果剩余的时间不足1000纳秒,则不需要park。

为什么不需要park,因为离到期时间太短,阻塞之后再唤醒的时间可能都不够(这里是<1000纳秒),代价还很高。

所以最好的方式是下一次for 循环要么获取到锁,要么滚蛋,不浪费一次pack 再 unpack的代价。

面试官:那你再讲讲 ReentrantLock 支持响应中断什么意思?

安琪拉:ok。ReentrantLock 的lockInterruptibly 支持中断响应,什么意思?

我们来比较一下支持中断和不支持中断的获取锁代码区别。

面试官:谈谈ReentrantLock与synchronized的区别?_第4张图片


这里是如果应该park,并且发生了中断,设置中断标识信息(interrupted字段),获取到锁之后返回这个中断标识信息。

面试官:谈谈ReentrantLock与synchronized的区别?_第5张图片


parkAndCheckInterrupt 这个方法会让线程park(阻塞),同时会调用 Thread.interrupted() 返回线程是否发生过中断,这个方法会清除线程的中断标识位,是否发生过中断的状态保持在 interrupted 里面,后面返回了。

我们再来看下响应中断的实现方式:

面试官:谈谈ReentrantLock与synchronized的区别?_第6张图片

只要发生了中断,直接抛 InterruptedException 异常,获取锁流程不再继续。

- 所以说ReentrantLock 如果使用 lockInterruptibly,如果发生了中断是会立即停止获取锁的流程,是响应中断的,

- 但是如果使用 lock 是不响应中断,实际上是把中断吃掉了,延迟中断(中断标识返回后主动中断)。

总结:synchronized 获取锁的过程中是不能被中断,ReentrantLock 支持。

面试官:除了你说的这几点:

  • 公平锁

  • 尝试加锁,获取锁带超时时间

  • 获取锁响应中断

还有ReentrantLock 和 synchronized 还有别的区别吗?

安琪拉:【还有????你这个面试官坏的很,我知道你想问啥了。。。】

有有有,

底层实现 上,synchronized 是 JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。

锁的对象:synchronzied 锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock 是根据volatile 变量 state 标识锁的获得/争抢。

实现机制上:synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向内核态申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile 保证数据可见性以实现锁的功能。

释放锁方式上:synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动释放锁;ReentrantLock 需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock() 和unlock() 方法配合 try/finally 语句块来完成。

带条件的锁:synchronized不能绑定条件;ReentrantLock 可以绑定Condition 结合await()/singal() 方法实现线程的精准唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll() 方法要么随机唤醒一个线程要么唤醒全部线程。

面试官:那你把这几个底层实现到带条件的锁都给我一一讲清楚原理,【哈哈,你快讲,我面试要用】

安琪拉:今天讲太多,不想讲了。

面试官:那我只能表示非常遗憾了,这样吧,您先回去,后续进展会通知您的,这边出门左转。

安琪拉:【又是悲伤的一次面试经历】

总结:

ReentrantLock 与 synchronized的区别:

  • synchronized 属于非公平锁,ReentrantLock 支持公平锁、非公平锁;

  • synchronized 不支持尝试加锁,ReentrantLock 支持尝试加锁,支持带超时时间的尝试加锁;

  • synchronized 不支持响应中断的获取锁,ReentrantLock 提供了响应中断的加锁方法;

  • synchronized 不支持带条件,ReentrantLock支持;

  • 其他底层实现上、实现机制上、锁的对象上、释放锁的方式上也有区别。

你可能感兴趣的:(队列,java,多线程,面试,android)