Java并发包基础元件LockSupport

前言

LockSupport 和 CAS 是Java并发包中很多并发工具控制机制(Lock和同步器框架的核心 AQS: AbstractQueuedSynchronizer)的基础,它们底层其实都是依赖Unsafe实现。

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。其主要的核心就是提供了park()和unpark()方法来实现阻塞线程和解除线程阻塞。

其基本原理类似于二元信号量(只有1个许可证"permit"可供使用),当执行park()的时候,如果这个唯一许可证还没有被占用,当前线程则获取该唯一许可继续往下执行,如果许可已经被占用,则当前线程阻塞,等待获取许可。当执行unpark()的时候,将释放对应线程的许可。

 

park/unpark方法详解

首先看LockSupport 中相关方法的源码(此处只列举了最基本的park方法,其他带超时时间参数的park方法就不再一一介绍)

 1 public static void unpark(Thread thread) {  
 2         if (thread != null)  
 3             UNSAFE.unpark(thread);  
 4 }  
 5   
 6 public static void park() {  
 7         UNSAFE.park(false, 0L);  
 8 }  
 9   
10 public static void park(Object blocker) {  
11         Thread t = Thread.currentThread();  
12         setBlocker(t, blocker);  
13         UNSAFE.park(false, 0L);  
14         setBlocker(t, null);  
15 } 

通过源码可以发现,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()方法的调用置于循环检查是否满足条件的代码块中:

1 while (<condition does not hold>)  
2      LockSupport.park();  
3     ... //执行适合条件的动作   

park/unpark特性详解

1. 许可默认是被占用的,也就是说如果在没有先执行unpark的情况下,直接执行park()将获取不到许可,从而被阻塞。示例如下:

1 public static void main(String[] args)  
2 {  
3      LockSupport.park();//许可默认已经被占用,此处将阻塞  
4      System.out.println("block.");//这里将不会得到执行  
5 } 

2. LockSupport不可重入,但unpark可以多次调用。 

 1 public static void main(String[] args)  
 2 {  
 3      Thread thread = Thread.currentThread();  
 4      LockSupport.unpark(thread);//释放许可  
 5      System.out.println("a");  
 6      LockSupport.unpark(thread);//再次释放许可,也是可以的。  
 7      System.out.println("b");  
 8      LockSupport.park();// 获取许可  
 9      System.out.println("c");  
10      LockSupport.park();//不可重入,导致阻塞  
11      System.out.println("d");  
12 } 

 以上代码中只会打印出:a,b,c。不会打印出c。因为第二次调用park的时候,线程无法获取许可从而导致阻塞。

3. 支持被中断

 1 public static void main(String[] args) throws Exception {  
 2         Thread t = new Thread(new Runnable() {  
 3             private int count = 0;  
 4   
 5             @Override  
 6             public void run() {  
 7                 long start = System.currentTimeMillis();  
 8                 long end = 0;  
 9   
10                 while ((end - start) <= 1000) {  
11                     count++;  
12                     end = System.currentTimeMillis();  
13                 }  
14   
15                 System.out.println("before park.count=" + count);  
16   
17                 LockSupport.park();//被阻塞  
18                 System.out.println("thread over." + Thread.currentThread().isInterrupted());  
19   
20             }  
21         });  
22   
23         t.start();  
24   
25         Thread.sleep(5000);  
26   
27         t.interrupt();  
28   
29         System.out.println("main over");  
30 } 

当线程t执行 LockSupport.park()的时候,由于许可默认被占用,所以被阻塞,但是主线程在5秒只后对t线程进行了打断,导致LockSupport.park()被唤醒,打印出thread over.true。由此可见线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException 。 注意:如果在执行park之前就执行了中断操作将线程打断,在接下来调用park时将会立即返回,并且不会清除中断状态。

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成员属性的值。

 1 private static final long parkBlockerOffset;  
 2   
 3 static{  
 4 try {  
 5             UNSAFE = sun.misc.Unsafe.getUnsafe();  
 6             Class<?> tk = Thread.class;  
 7             parkBlockerOffset = UNSAFE.objectFieldOffset  
 8                 (tk.getDeclaredField("parkBlocker"));  
 9         } catch (Exception ex) { throw new Error(ex); }  
10   
11 }  
12 public static Object getBlocker(Thread t) {  
13         if (t == null)  
14             throw new NullPointerException();  
15         return UNSAFE.getObjectVolatile(t, parkBlockerOffset);  
16 }  
17   
18 private static void setBlocker(Thread t, Object arg) {  
19         // Even though volatile, hotspot doesn't need a write barrier here.  
20         UNSAFE.putObject(t, parkBlockerOffset, arg);  
21 }  

这个parkBlocker对象是用来记录线程被阻塞时被谁阻塞的。可以通过LockSupport的getBlocker获取到阻塞的对象.用于线程监控和分析工具来定位原因的。

 

nextSecondarySeed方法

 1 static final int nextSecondarySeed() {  
 2     int r;  
 3     Thread t = Thread.currentThread();  
 4     if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {  
 5         r ^= r << 13;   // xorshift  
 6         r ^= r >>> 17;  
 7         r ^= r << 5;  
 8     }  
 9     else if ((r = java.util.concurrent.ThreadLocalRandom.current().nextInt()) == 0)  
10         r = 1; // avoid zero  
11     UNSAFE.putInt(t, SECONDARY, r);  
12     return r;  
13 }  

LockSupport类中还提供了上面这个叫nextSecondarySeed的方法,它其实操作的是Thread类中的一些关于伪随机数的成员属性:threadLocalRandomSeed、threadLocalRandomProbe、threadLocalRandomSecondarySeed

 1 private static final long SEED;  
 2 private static final long PROBE;  
 3 private static final long SECONDARY;  
 4 static {  
 5     try {  
 6         UNSAFE = sun.misc.Unsafe.getUnsafe();  
 7         Class<?> tk = Thread.class;  
 8         SEED = UNSAFE.objectFieldOffset  
 9             (tk.getDeclaredField("threadLocalRandomSeed"));  
10         PROBE = UNSAFE.objectFieldOffset  
11             (tk.getDeclaredField("threadLocalRandomProbe"));  
12         SECONDARY = UNSAFE.objectFieldOffset  
13             (tk.getDeclaredField("threadLocalRandomSecondarySeed"));  
14     } catch (Exception ex) { throw new Error(ex); }  
15 } 

从nextSecondarySeed方法的实现,可以看出它主要是为了生成随机数,并且把随机数保存到当前线程对象的成员属性作为下一次生成随机数的因子,在其中还用到了并发包特别提供的随机数生成工具ThreadLocalRandom,其实在 ThreadLocalRandom类中也提供了一个几乎一模一样的nextSecondarySeed方法,为什么不使用Random而要重新创建一个ThreadLocalRandom?这主要是为了解决Random类在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的问题,至于其深层次的内部原理,会在专门的ThreadLocalRandom分析文章中进行分析介绍。

 

posted @ 2021-05-11 16:02  莫待樱开春来踏雪觅芳踪  阅读(276)  评论(0)    收藏  举报