开场

上海漕河泾某栋写字楼里,因为一个Reentrantlock,引发了一场求职者和面试官的battle。

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

安琪拉:面试官你好,我叫安琪拉,草丛三婊,最强中单,草地摩托车车手,第21套广播体操推广者,火球拥有者、不焚者,安琪拉,这是我的简历,请过目。

面试官:看你简历上写熟悉多线程编程,用过ReentrantLock吗?

安琪拉:用过的。

面试官:ReentrantLock 为什么叫 ReentrantLock?

安琪拉:【心想:这什么鬼问题?】

并发编程系列第七集-Reentrantlock_加锁

ReentrantLock 可以拆成二部分,


  • 在英文当中,前缀 re- 有"再"、"重新"、"重复"的意思。例如:review(复习)restart(重新开始),reentrant是可重入的意思
  • Lock就是锁
    加在一块就是可重入锁。

你看我简历上不是写了英语过四级嘛,这种问题就不要拿来考我,OK?

面试官:那可重入是什么意思?

安琪拉:跟我这咬文嚼字呢。。

面试官:可重入,是个动词,指的是谁可重入呢?

面试官:指的是线程,当一个线程获取锁之后,这个线程可以再次获取锁,可重入从字面上就是可重新进入,进哪里?当然是临界区。

面试官:什么是临界区?

安琪拉临界区指的是一个访问共用资源的程序片段,这个共用资源又有个特性。它不希望同时被多个线程访问。当有线程进入程序片段(临界区)的时候,其他线程必须等待,清楚了吧。。

正题

面试官:【心想,看样子来之前背了八股文,不过问题不大,我早有准备(提前从安琪拉的博客上找了些题)】

synchronized 用过的吧?

安琪拉:用过。

面试官:那你说说同样是锁,ReentrantLock 和 synchronized 的区别?嘿嘿

并发编程系列第七集-Reentrantlock_公平锁_02

安琪拉:可以从几个方面来说:

  1. 锁分类

  • synchronized 是非公平锁
  • ReentrantLock 可自定义锁是公平还是非公平(具体原理后面解释)

  • 灵活性

  • synchronized 使用形式非常固定,就是单纯的加锁
  • ReentrantLock 可以尝试获取锁(tryLock)、带超时时间的获取锁、可响应中断(lockInterruptibly)

  • 可重入(线程可重复多次加锁)
  • synchronized、ReentrantLock 都是可重入的。
  • 使用方式

  • synchronized 修饰方式加锁,自动释放锁(没有显示的unlock操作)
  • ReentrantLock 显示加锁,显示释放锁(有lock、unlock操作)。

  • 实现原理

  • synchronized 基于monitor(监视器)模式
  • ReentrantLock 基于 AQS

面试官:你刚才说使用方式上的差异,能写段代码看下吗?简单写写就好,加锁解锁过程。

安琪拉:笔递给我一下,给我张A4纸。

先说下synchronized,三种作用范围如下, 以及可重入的示例:

public class SynchronizedTest {

    private static final Object monitor = new Object();

    private static int money = 0;

    // 1.作用于静态方法
    public synchronized static void add(int income) {
        money += income;
    }

    // 2.作用于非静态方法
    public synchronized void achieveMoney(int income) {
        money += income;
    }

    public void achieveCoin(int income) {
        // 3.作用域代码快
        synchronized (monitor) {
            money += income;
        }
    }

    // 4.可重入
    public void achieve(int income) {
        for (int i = 0; i < 10; i++) {
            synchronized (monitor) {
                money +income;
            }
        }
    }
}

面试官:ReentrantLock 的用法呢?也一起写一写。

安琪拉

public class ReentrantLockTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public void add(int income) {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                //doSomeThing();
            }finally {
                lock.unlock();
            }
        }
    }
}

上面代码是带时间的尝试获取锁,获取成功,才会执行后续逻辑。

安琪拉:对了,能帮我倒杯水吗?讲了这么久有点渴。

面试官:ReentrantLock 有什么特点,使用场景是怎样的?

安琪拉:ReentrantLock 有很多特点,很适合按照特定场景来用,


  • 可尝试获取锁

    比如我有个定时任务,每间隔5秒钟执行一次暴打凯的任务。但是为了防止前一次暴打凯任务还没结束,后一次任务又触发了,怕凯受不住,我就在每次任务执行前先尝试获取一下锁,调用 ​​lock.tryLock()​​,如果获取锁失败,直接跳过这次任务,等下一次任务执行。

  • 公平锁

    比如亚瑟、凯、孙悟空都希望能跟安琪拉一起中路蹲草,但是中路草丛有限,挤不下这么多人,只能选一个,那就大家按照先来先到原则,从ReentrantLock获取到锁的才允许蹲草,获取不到,需要等在后面,ReentrantLock是公平的。

    如果是非公平锁的话,可能草丛已经有亚瑟跟凯排队了,孙悟空从野区来直接排在他俩前面了。

  • 带条件等待

    使用synchronized 结合Object上的 wait和 notify方法可以实现线程间的等待通知机制。但是ReentrantLock结合Condition接口同样可以实现这个功能。而且相比synchronized 使用起来更清晰。

    我们来写个代码:

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

      lock.lock();
      //启动子线程
      new Thread(new SubThread()).start();
      System.out.println("主线程等待通知");
      try {
        //主线程进入等待队列,释放锁
        condition.await();
      } finally {
        lock.unlock();
      }
      System.out.println("主线程恢复运行");
    }

    static class SubThread implements Runnable {

      @Override
      public void run() {
        //获取锁
        lock.lock();
        try {
          //唤醒条件等待队列
          condition.signal();
          System.out.println("子线程通知");
        } finally {
          lock.unlock();
        }
      }
    }


程序输出:

并发编程系列第七集-Reentrantlock_可重入_03

面试官:既然你这里讲到带条件的等待,那你通过Reentrantlock 写一个生产者消费者模型,纸笔给你

安琪拉

ReentrantLock lock = new ReentrantLock();
/**
     * 库存
     */
LinkedList<Integer> list = new LinkedList<>();
/**
     * 容量
     */
private int capacity;

public Factory(int capacity) {
  this.capacity = capacity;
}

Condition notFull = lock.newCondition();//队列满时的等待条件
Condition notEmpty = lock.newCondition();//队列空时的等待条件

public void produce(int ele) throws InterruptedException {
  lock.lock();
  try {
    //这个地方容易写成if,注意应该是while,在多线程编程里面死循环很常见
    while (list.size() == capacity) {
      //库存满了,等库存空余(不满)的条件
      notFull.await();
    }
    System.out.println("生产元素:" + ele);
    list.add(ele);
    //生产了新商品,通知消费
    notEmpty.signal();
  } finally {
    lock.unlock();
  }
}

public void consume() throws InterruptedException {
  lock.lock();
  try {
    //同上
    while (list.size() == 0) {
      //库存空了,等不为空条件出现
      notEmpty.await();
    }
    System.out.println("消费元素:" + list.poll());
    notFull.signal();
  } finally {
    lock.unlock();
  }
}

//测试验证
public static void main(String[] args) {
  Factory factory = new Factory(2);
  for (int i = 0; i < 10; i++) {
    int data = i;
    new Thread(() -> {
      try {
        factory.produce(data);
      } catch (InterruptedException e) {

      }
    }).start();
  }
  for (int i = 0; i < 10; i++) {
    new Thread(() -> {
      try {
        factory.consume();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }).start();
  }
}

面试官:那你跟我讲讲ReentrantLock 如何区分公平还是非公平的?

安琪拉:直接上代码吧

//默认: 非公平锁,
public ReentrantLock() {
    sync = new NonfairSync();
}
//公平锁, fair 传入true
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

非公平锁的吞吐量比公平锁高很多,所以默认使用非公平锁。

面试官:那你跟我说说获取锁的流程?

安琪拉:先说非公平锁的获取流程。

final void lock() {
    //直接尝试获取锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

非公平锁直接上来先尝试CAS 抢占一把,如果没有线程占锁,直接获取,否则进入 ​​acquire(1)​​ 流程。

​acquire(1)​​ 是公平锁的处理流程,这些都是AQS 提供的能力。

面试官:等你半天了,终于说到我想听的了,对于AQS, 讲讲你的理解,顺着这样的思路来讲:Why - What - How。为什么会有这个东西?这个东西是什么?怎么做到的?

尤其是和synchronized作一下对比,可能觉得已经有synchronized锁了,还需要AQS吗?

安琪拉

Why: synchronized 只解决了有和没有的问题,但是锁的场景和多样性方面还很欠缺,例如: 带超时时间的获取锁、获取锁非阻塞(尝试获取锁)、带等待条件的请求锁。其实这些你通过synchronized 加手动封装也能实现,但是需要些功力而且还容易出错,所以Doug Lea就写了功能更加丰富的AQS以及一些一系列多线程组件,方便大家按需扩展。AQS 是JDK1.5 引入的,那个时候synchronized 还没做优化,没有偏向锁和轻量级锁,所以AQS 的 CAS(Reentrantlock) 自旋就很有必要了,毕竟有时间竞争并不激烈。

What: AQS是AbstractQueuedSynchronizer 的缩写,抽象队列同步器,我们很多同步工具ReentrantLock、Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier 都是基于AQS 实现的。

How: AQS实现机制是什么呢?大家可以先不往下看,如果让你来实现一个多线程控制访问共享资源的工具,你会如何写?考虑这么几个问题:


  1. 当有多个线程竞争的时候,运行线程排队等待获取资源,如何做?
  2. 当某个线程使用完资源,如何通知正在排队等待的资源?
  3. synchronized获取锁是阻塞的,也就是线程获取锁的时候一定会进入等待,但是如果希望实现一个线程过来访问,发现已经有其他线程持有锁了,直接返回,不希望产生锁竞争,怎么实现?

AQS 基本的原理是它提供了一套共享资源的访问的规范,通过CLH(一个双向链表)的方式把线程等待管理起来。

并发编程系列第七集-Reentrantlock_可重入_04CLH

它底层采用的是状态标志位(state变量)+FIFO队列的方式来记录获取锁、释放锁、竞争锁等一系列锁操作;

对于AQS而言,其中的state变量可以看做是锁,队列采用的是先进先出的双向链表,state共享状态变量表示锁状态,内部使用CAS对state进行原子操作修改来完成锁状态变更(锁的持有和释放)。

面试官:那你给我讲讲AQS中锁获取、释放的流程?

安琪拉:AQS东西太对了,您看要不这样子,到二面面试官的时候,我再深入介绍AQS。

面试官:这样子啊。那今天先到这吧,你回去等我们通知。

安琪拉:嗯,通知我的时候记得不要打电话,不方便,发微信就好了。