不知道你们面试的过程有没有遇到过吊炸天的面试官,一上来就说,你先手动实现一个先进先出的不可重入锁。惊不惊喜?激不激动?大展身手的时刻到了,来,咱们一块儿看看下面这个例子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小程序
因此整个过程即便你屡次调用unpark,他的值依然只是等于1,并不会进行累加bash
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)。调用这个方法会有如下状况并发
public static void unpark(Thread thread) {
if (thread != null)
//经过UNSAFE 来唤醒指定的线程
//注意咱们须要保证该线程仍是存活的
//若是该线程还没启动或者已经结束了,调用该方法是没有做用的
UNSAFE.unpark(thread);
}
复制代码
源码很是简单,直接经过UNSAFE 来唤醒指定的线程,可是要注意一个很是关键的细节,就是这里指定了唤醒的线程,这个跟Object 中的notify 彻底不同的特性,synchronized 的锁是加在对象的监视锁上的,线程会阻塞在对象上,在唤醒的时候没办法指定唤醒哪一个线程,只能通知在这个对象监视锁 上等待的线程去抢这个锁,具体是谁抢到这把锁是不可预测的,这也就决定了synchronized 是没有办法实现相似上面这个先进先出的公平锁。源码分析
先来个列子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方法必定要放在循环体内
synchronized (lock){
lock.wait()
}
复制代码
LockSupport 本质上也是一个Object,那么调用LockSupport的unpark 能够唤醒调用LockSupport.wait() 方法的线程吗?请把你的答案写在留言区
若是你以为这篇内容对你挺有启发,我想邀请你帮我2个小忙: