前言
LockSupport 和 CAS 是Java并发包中很多并发工具控制机制(Lock和同步器框架的核心 AQS: AbstractQueuedSynchronizer)的基础,它们底层其实都是依赖Unsafe实现。
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。其主要的核心就是提供了park()和unpark()方法来实现阻塞线程和解除线程阻塞。
其基本原理类似于二元信号量(只有1个许可证"permit"可供使用),当执行park()的时候,如果这个唯一许可证还没有被占用,当前线程则获取该唯一许可继续往下执行,如果许可已经被占用,则当前线程阻塞,等待获取许可。当执行unpark()的时候,将释放对应线程的许可。
park/unpark方法详解
首先看LockSupport 中相关方法的源码(此处只列举了最基本的park方法,其他带超时时间参数的park方法就不再一一介绍)
public static void unpark(Thread thread) { if (thread != null) UNSAFE.unpark(thread); } public static void park() { UNSAFE.park(false, 0L); } public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(false, 0L); setBlocker(t, null); } 其他带超时时间的变种park方法便不再一一列举。
通过源码可以发现,park/unpark的底层都是通过调用unsafe类的相关实现,对于park的其他增加了超时时间的变种park方法也是根据unsafe的park方法的第一个参数是否是绝对时间来扩展的。
关于park/unpark方法在openJDK里的C++实现,主要是利用了Posix的mutex,condition来实现的,至于什么是Posix、什么是mutex,condition,就不在深入探索了,只要知道这种实现是和平台紧密相关的,大概就是借助了操作系统的某些实现。总之其内部维护了一个volatile修饰的int类型的_counter变量来记录所谓的“许可”。当park时,这个变量置为了0,当unpark时,这个变量置为1。
值得注意的是,由于park的底层调用的是unsafe的park实现,所以当调用LockSupport的park()方法时如果没有立即获得许可,那么当前线程阻塞之后,也只有出现如下几种情况才会退出阻塞状态,立即返回:
1)其他线程执行了当前线程的unpark()方法。2)其他线程打断了当前线程。3)如果park方法带的超时时间不为0,当超时时间到达时。4)无理由的虚假的唤醒(也就是传说中“Spurious wakeup”,和Object类的wait()方法类似)。
前三种情况都很好理解,第四种情况似乎有点让人无法接受,为什么会存在无缘无故的就被唤醒的情况?这样如何保证我们的应用不出现错误?Google了很多关于Spurious wakeup的文章,大概有如下几种解释:
第一种解释:通过分析源码发现底层的pthread_cond_wait方法并不是放在一个while循环中,而是if判断中,这样当pthread_cond_wait被唤醒之后,并不会再次进行条件判断,而是立即返回至上层应用。我认为这其实并不能称之为一种解释,最多算一种最肤浅最表面的原因,更重要的应该是为何pthread_cond_wait方法会在条件不成立的情况下返回。
第二种解释:这种解释认为这是出于性能考虑的原因“Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations”。但这看起来也像是一种模棱两可的解释。
第三种解释:认为这是操作系统本身的一种策略“Each blocking system call on Linux returns abruptly with EINTR
when the process receives a signal. ... pthread_cond_wait()
can't restart the waiting because it may miss a real wakeup in the little time it was outside the futex
system call.”
总而言之,这种无理由的虚假的唤醒是存在的,但是几率应该是比较少的,到底的出于什么原因导致的,我们可以不用深究。针对这种情况,如何保证我们的应用不受这种虚假唤醒的影响,网络上的答案到是一致的,那就是:将park()方法的调用置于循环检查是否满足条件的代码块中:
while (<condition does not hold>) LockSupport.park(); ... //执行适合条件的动作
park/unpark特性详解
1. 许可默认是被占用的,也就是说如果在没有先执行unpark的情况下,直接执行park()将获取不到许可,从而被阻塞。示例如下:
public static void main(String[] args) { LockSupport.park();//许可默认已经被占用,此处将阻塞 System.out.println("block.");//这里将不会得到执行 }
2. LockSupport不可重入,但unpark可以多次调用。
public static void main(String[] args) { Thread thread = Thread.currentThread(); LockSupport.unpark(thread);//释放许可 System.out.println("a"); LockSupport.unpark(thread);//再次释放许可,也是可以的。 System.out.println("b"); LockSupport.park();// 获取许可 System.out.println("c"); LockSupport.park();//不可重入,导致阻塞 System.out.println("d"); }以上代码中只会打印出:a,b,c。不会打印出c。因为第二次调用park的时候,线程无法获取许可从而导致阻塞。
3. 支持被中断
public static void main(String[] args) throws Exception { Thread t = new Thread(new Runnable() { private int count = 0; @Override public void run() { long start = System.currentTimeMillis(); long end = 0; while ((end - start) <= 1000) { count++; end = System.currentTimeMillis(); } System.out.println("before park.count=" + count); LockSupport.park();//被阻塞 System.out.println("thread over." + Thread.currentThread().isInterrupted()); } }); t.start(); Thread.sleep(5000); t.interrupt(); System.out.println("main over"); }当线程t执行 LockSupport.park()的时候,由于许可默认被占用,所以被阻塞,但是主线程在5秒只后对t线程进行了打断,导致LockSupport.park()被唤醒,打印出thread over.true。由此可见线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException 。
4. 唤醒信号不担心丢失
在wait/notify/notifyAll模式的阻塞/唤醒机制中,我们必须要考虑 notify和wait调用的时序性,避免在wait方法调用之前调用了notify,从而导致错过唤醒信号,使应用永远等待。而LockSupport的unpark()方法可以在park()方法调用之前、之后甚至同时执行,都可以达到唤醒线程的目的。并且park和Object.wait()本质实现机制不同,两者的阻塞队列并不交叉,object.notifyAll()不能唤醒LockSupport.park()阻塞的线程。
5. 方便线程监控与工具定位
在LockSupport类中存在parkBlocker的getter、setter方法, 可以看到它是通过unsafe运用Thread类的实例成员属性parkBlocker的偏移地址获取对应线程的parkBlocker成员属性的值。
private static final long parkBlockerOffset; static{ try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> tk = Thread.class; parkBlockerOffset = UNSAFE.objectFieldOffset (tk.getDeclaredField("parkBlocker")); } catch (Exception ex) { throw new Error(ex); } } public static Object getBlocker(Thread t) { if (t == null) throw new NullPointerException(); return UNSAFE.getObjectVolatile(t, parkBlockerOffset); } private static void setBlocker(Thread t, Object arg) { // Even though volatile, hotspot doesn't need a write barrier here. UNSAFE.putObject(t, parkBlockerOffset, arg); }
这个parkBlocker对象是用来记录线程被阻塞时被谁阻塞的。可以通过LockSupport的getBlocker获取到阻塞的对象.用于线程监控和分析工具来定位原因的。