BAT面试官:你先手动用LockSupport实现一个先进先出的不可重入锁?吊炸天

引言

不知道你们面试的过程有没有遇到过吊炸天的面试官,一上来就说,你先手动实现一个先进先出的不可重入锁。惊不惊喜?激不激动?大展身手的时刻到了,来,咱们一块儿看看下面这个例子java

public class FIFOMutex {

    private final AtomicBoolean locked = new AtomicBoolean(false);
    private final Queue<Thread> waiters
        = new ConcurrentLinkedQueue<Thread>();

    public void lock() {
        boolean wasInterrupted = false;
        Thread current = Thread.currentThread();
        waiters.add(current);

        // 只有本身在队首才能够得到锁,不然阻塞本身
        //cas 操做失败的话说明这里有并发,别人已经捷足先登了,那么也要阻塞本身的
        //有了waiters.peek() != current判断若是本身队首了,为何不直接获取到锁还要cas 操做呢?
        //主要是由于接下来那个remove 操做把本身移除掉了额,可是他尚未真正释放锁,锁的释放在unlock方法中释放的
        while (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
            //这里就是使用LockSupport 来阻塞当前线程
            LockSupport.park(this);
            //这里的意思就是忽略线程中断,只是记录下曾经被中断过
            //你们注意这里的java 中的中断仅仅是一个状态,要不要退出程序或者抛异常须要程序员来控制的
            if (Thread.interrupted()) {
                wasInterrupted = true;
            }
        }
        // 移出队列,注意这里移出后,后面的线程就处于队首了,可是仍是不能获取到锁的,locked 的值仍是true,
        // 上面while 循环的中的cas 操做仍是会失败进入阻塞的
        waiters.remove();
        //若是被中断过,那么设置中断状态
        if (wasInterrupted) {
            current.interrupt();
        }

    }

    public void unlock() {
        locked.set(false);
        //唤醒位于队首的线程
        LockSupport.unpark(waiters.peek());
    }

}
复制代码

上面这个例子其实就是jdk中LockSupport 提供的一个例子。LockSupport 是提供线程的同步原语的很是底层的一个类,若是必定要深挖的话,他的实现又是借用了Unsafe这个类来实现的,Unsafe 类中的方法都是native 的,真正的实现是C++的代码程序员

经过上面这个例子,分别调用了如下两个方法面试

public static void park(Object blocker) 
    public static void unpark(Thread thread) 
复制代码

LockSupport的等待和唤醒是基于许可的,这个许可在C++ 的代码中用一个变量count来保存,它只有两个可能的值,一个是0,一个是1。初始值为0小程序

调用一次park

  1. 若是count=0,阻塞,等待count 变成1
  2. 若是count=1,修改count=0,而且直接运行,整个过程没有阻塞

调用一次unpark

  1. 若是count=0,修改count=1
  2. 若是count=1,保持count=1

屡次连续调用unpark 效果等同于一次

因此整个过程即便你屡次调用unpark,他的值依然只是等于1,并不会进行累加bash

源码分析

park

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        //设置当前线程阻塞在blocker,主要是为了方便之后dump 线程出来排查问题,接下来会讲
        setBlocker(t, blocker);
        //调用UNSAFE来阻塞当前线程,具体行为看下面解释
        UNSAFE.park(false, 0L);
        //被唤醒以后来到这里
        setBlocker(t, null);
    }
复制代码

这里解释下UNSAFE.park(false, 0L)。调用这个方法会有如下状况并发

  1. 若是许可值为1(也就是以前调用过unpark,而且后面没有调用过park来消耗许可),当即返回,而且整个过程不阻塞,修改许可值为0
  2. 若是许可值为0,进行阻塞等待,直到如下三种状况发生会被唤醒
    1. 其余线程调用了unpark 方法指定唤醒该线程
    2. 其余线程调用该线程的interrupt方法指定中断该线程
    3. 无理由唤醒该线程(就是耍流氓,下面会解析)

unpark

public static void unpark(Thread thread) {
        if (thread != null)
        //经过UNSAFE 来唤醒指定的线程
        //注意咱们须要保证该线程仍是存活的
        //若是该线程还没启动或者已经结束了,调用该方法是没有做用的
            UNSAFE.unpark(thread);
    }
复制代码

源码很是简单,直接经过UNSAFE 来唤醒指定的线程,可是要注意一个很是关键的细节,就是这里指定了唤醒的线程,这个跟Object 中的notify 彻底不同的特性,synchronized 的锁是加在对象的监视锁上的,线程会阻塞在对象上,在唤醒的时候没办法指定唤醒哪一个线程,只能通知在这个对象监视锁 上等待的线程去抢这个锁,具体是谁抢到这把锁是不可预测的,这也就决定了synchronized 是没有办法实现相似上面这个先进先出的公平锁。源码分析

park 和unpark 的调用不分前后顺序

先来个列子ui

public class LockSupportTest {

    private static final Logger logger = LoggerFactory.getLogger(LockSupportTest.class);

    public static void main(String[] args) throws Exception {
        LockSupportTest test = new LockSupportTest();
        Thread park = new Thread(() -> {
            logger.info(Thread.currentThread().getName() + ":park线程先休眠一下,等待其余线程对这个线程执行一次unpark");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info(Thread.currentThread().getName() + ":调用park");
            LockSupport.park(test);
            logger.info(Thread.currentThread().getName() + ": 被唤醒");
        });

        Thread unpark = new Thread(() -> {
            logger.info(Thread.currentThread().getName() + ":调用unpark唤醒线程" + park.getName());
            LockSupport.unpark(park);
            logger.info(Thread.currentThread().getName() + ": 执行完毕");
        });
        park.start();
        Thread.sleep(2000);
        unpark.start();
    }
}
复制代码

输出结果:this

18:52:42.065 Thread-0:park线程先休眠一下,等待其余线程对这个线程执行一次unpark
18:52:44.064 Thread-1:调用unpark唤醒线程Thread-0
18:52:44.064 Thread-1: 执行完毕
18:52:46.079 Thread-0:调用park
18:52:46.079 Thread-0:被唤醒

复制代码

从结果中能够看到,即便先调用unpark,后调用park,线程也能够立刻返回,而且整个过程是不阻塞的。这个跟Object对象的wait()和notify()有很大的区别,Object 中的wait() 和notify()顺序错乱的话,会致使线程一直阻塞在wait()上得不到唤醒。正是LockSupport这个特性,使咱们并不须要去关心线程的执行顺序,大大的下降了死锁的可能性。spa

支持超时

//nanos 单位是纳秒,表示最多等待nanos 纳秒,
//好比我最多等你1000纳秒,若是你还没到,就再也不等你了,其余状况跟park 同样
public static void parkNanos(Object blocker, long nanos) //deadline 是一个绝对时间,单位是毫秒,表示等待这个时间点就再也不等 //(好比等到今天早上9点半,若是你还没到,我就再也不等你了) ,其余状况跟park 同样 public static void parkUntil(Object blocker, long deadline) 复制代码

正是有了这个方法,因此咱们平时用的ReentrantLock 等各类lock 才能够支持超时等待,底层其实就是借用了这两个方法来实现的。这个也是synchronized 没有办法实现的特性

支持查询线程在哪一个对象上阻塞

public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }
复制代码

之前没看源码的时候有个疑问,线程都已经阻塞了,为何还能够查看指定线程的阻塞在相关的对象上呢?不该该是调用的话也是没有任何反应的的吗?直到看了源码,才知道它其实不是用该线程去直接获取线程的属性,而是经过UNSAFE.getObjectVolatile(t, parkBlockerOffset) 来获取的。这个方法的意思就是获取内存区域指定偏移量的对象

最佳实践

阻塞语句LockSupport.park() 须要在循环体,例如本文一开始的例子

while (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
            //在循环体内
            LockSupport.park(this);
            //唤醒后来到这里
            //忽略其余无关代码
        }
复制代码

若是不在循环体内会有什么问题呢?假如变成如下代码片断

if (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
            //在循环体内
            LockSupport.park(this);
            //唤醒后来到这里
            //忽略其余无关代码
        }
复制代码

这里涉及一个线程无理由唤醒的概念,也就是说阻塞的线程并无其余线程调用unpark() 方法的时候就被唤醒

假如前后来了两个线程A和B,这时候A先到锁,这个时候B阻塞。可是在A还没释放锁的时候,同时B被无理由唤醒了,若是是if,那么 线程B就直接往下执行获取到了锁,这个时候同时A和B均可以访问临界资源,这样是不合法的,若是是while 循环的话,会判断B不是 在队首或者CAS 失败的会继续调用park 进入阻塞。因此你们记得park方法必定要放在循环体内

LockSupport中的 park ,unpark 和Object 中的wait,notify 比较

  1. 他们均可以实现线程之间的通信
  2. park 和wait 均可以让线程进入阻塞状态
  3. park 和unpark 能够在代码的任何地方使用
  4. wait 和notify,notifyAll 须要和synchronized 搭配使用,必须在获取到监视锁以后才可使用,例如
synchronized (lock){
 lock.wait()
}
复制代码
  1. wait 和notify 须要严格控制顺序,若是wait 在notify 后面执行,则这个wait 会一直得不到通知
  2. park 和unpark 经过许可来进行通信,无需保证顺序
  3. park 支持超时等待,可是wait 不支持
  4. unpark 支持唤醒指定线程,可是notify 不支持
  5. wait 和park 均可以被中断唤醒,wait 会得到一个中断异常

思考题

LockSupport 本质上也是一个Object,那么调用LockSupport的unpark 能够唤醒调用LockSupport.wait() 方法的线程吗?请把你的答案写在留言区

看完两件事

若是你以为这篇内容对你挺有启发,我想邀请你帮我2个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「面试bat」,不按期分享原创知识,原创不易,请多支持(里面还提供刷题小程序哦)。

相关文章
相关标签/搜索