• 【JDK1.8】JUC——LockSupport


    一、前言

    Basic thread blocking primitives for creating locks and other synchronization classes.

    用于创建锁定和其他同步类的基本线程阻塞原语(基础?)。

    上面这段话是Java Doc对LockSupport的描述,表明了该类在实现锁当中的重要意义。因此我们先来查看一下其中的源码,看看它是如何实现的。


    二、LockSupport成员变量分析

    public class LockSupport {
        private static final sun.misc.Unsafe UNSAFE;
        private static final long parkBlockerOffset;
        private static final long SEED;
        private static final long PROBE;
        private static final long SECONDARY;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> tk = Thread.class;
                parkBlockerOffset = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("parkBlocker"));
                SEED = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("threadLocalRandomSeed"));
                PROBE = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("threadLocalRandomProbe"));
                SECONDARY = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    }
    
    1. 首先要明确的就是sun.misc.Unsafe这个类,它是一个final class,里面有100多个方法,锁的实现也是依赖了这个类,其中基本上都是native方法。Java避免了程序员直接操作内存,但这不是绝对的,通过使用Unsafe类,我们还是能够操作内存。笔者尝试阅读里面的C++代码,奈何已经将知识都还给了老师。源码越看到后面,越觉得C和C++的伟大,膝盖瑟瑟打抖,有兴趣的园友们可以尝试着阅读以下:Unsafe C++源码

    2. parkBlockerOffset。从字面上看就是parkBlocker的偏移量,那么parkBlocker是干嘛的呢,从static代码块中可以看到,它属于Thread类,于是进去看看:

    /**
     * The argument supplied to the current call to
     * java.util.concurrent.locks.LockSupport.park.
     * Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
     * Accessed using java.util.concurrent.locks.LockSupport.getBlocker
     */
    volatile Object parkBlocker;
    

    从注释上看,就是给LockSupport的setBlocker和getBlocker调用。另外在LockSupport的java doc中也写到:

    This object is recorded while the thread is blocked to permit monitoring and diagnostic tools to identify the reasons that threads are blocked. (Such tools may access blockers using method [getBlocker(Thread).) The use of these forms rather than the original forms without this parameter is strongly encouraged. The normal argument to supply as a blockerwithin a lock implementation is this.

    大致是说,parkBlocker是当线程被阻塞的时候被记录,以便监视和诊断工具来识别线程被阻塞的原因。

    Unsafe类提供了获取某个字段相对 Java对象的“起始地址”的偏移量的方法objectFieldOffset,从而能够获取该字段的值。

    那么为什么记录该blocker在对象中的偏移量,而不是直接调用Thread.getBlocker(),这样不是更好,原因其实很好理解,当线程被阻塞(Blocked)的时候,线程是不会响应的。另外通过反射应该也可以拿到。


    三、LockSupport的重要方法

    类中的方法主要分为两类:park(阻塞线程)和unpark(解除阻塞)。

    首先强调的一点事park方法阻塞的是当前的线程,也就是说在哪个线程中调用,那么哪个线程就被阻塞(在没有获得许可的情况下)。

    重点讲其中的几个:

    3.1 park()解析

    public static void park() {
        UNSAFE.park(false, 0L);
    }
    

    UNSAFE.park的两个参数,前一个为true的时候表示传入的是绝对时间,false表示相对时间,即从当前时间开始算。后面的long类型的参数就是等待的时间,0L表示永久等待。

    根据java doc中的描述,调用park后有三种情况,能使线程继续执行下去:

    1. 有某个线程调用了当前线程的unpark。
    2. 其他线程中断(interrupt)了当前线程
    3. 该调用不合逻辑地(即毫无理由地)返回。

    验证一:

    public class UnparkTest {
        public static void main(String[] args) throws InterruptedException {
            Thread ut = new Thread(new UnparkThread(Thread.currentThread()));
            ut.start();
            System.out.println("I'm going to call park");
            // Thread.sleep(1000L);
            LockSupport.park();
            System.out.println("oh, I'm running again");
    
        }
    }
    
    class UnparkThread implements Runnable {
        private final Thread t;
        UnparkThread(Thread t) {
            this.t = t;
        }
    
        @Override
        public void run() {
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("I'm in unpark");
            LockSupport.unpark(t);
            System.out.println("I called unpark");
        }
    }
    

    结果:

    I'm going to call park
    I'm in unpark
    I called unpark
    oh, I'm running again
    

    另外值得一提的是,LockSupport对park和unpark的调用顺序并没有要求,将两个Thread.sleep(1000L);注释切换一下就可以发现,先调用unpark,再调用park,依旧可以获得许可,让线程继续运行。这一点与Object的 wait 和 notify 要求固定的顺序不同,其实现原理可以看这里


    验证二:

    public class LockSupportInterrupt {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(new InterruptThread());
            t.start();
            Thread.sleep(1000L);
            System.out.println("I'm going to interrupt");
            t.interrupt();
        }
    }
    
    class InterruptThread implements Runnable {
        @Override
        public void run() {
            System.out.println("I'm going to park");
            LockSupport.park();
            System.out.println("I'm going to again");
        }
    }
    

    运行结果:

    I'm going to park
    I'm going to interrupt
    I'm going to again
    

    LockSupport的park能够能响应interrupt事件,且不会抛出InterruptedException异常。


    3.2 park(Object blocker)

    park的另一个重载方法需要传入blocker对象:

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    

    在理解了parkBlocker的作用后,这个方法里的代码就很好理解了。

    1. 在调用park阻塞当前线程之前,先记录当前线程的blocker。
    2. 调用park阻塞当前线程
    3. 当前面提到的三个让线程继续执行下去的情况时,再将parkBlocker设置为null,因为当前线程已经没有被blocker住了,如果不设置为null,那诊断工具获取被阻塞的原因就是错误的,这也是为什么要有两个setBlocker的原因。

    再看一下setBlocker的代码:

    private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
    

    方法是私有的,嗯,为了保证正确性,肯定不能被其他类调用。

    另外就是利用了之前提到的偏移量以及unsafe对象将blocker值设置进了线程t当中。


    3.3 unpark(Thread thread)

    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    

    这就很简单了,判断是否为空,然后调用unsafe的unpark方法。由此更可见unsafe这个类的重要性。


    四、各种例子

    4.1 jstack查看parkBlocker

    前面提到parkBlocker提供了调试工具上面查找原因,所以我们来看一下在jstack上面是什么情况:

    public class JstackTest {
        public static void main(String[] args) {
            // 给main线程设置名字,好查找一点
            Thread.currentThread().setName("jstacktest");
            LockSupport.park("block");
        }
    }
    

    利用park(blocker)来阻塞main线程,传入string作为parkBlocker。

    运行之后,在shell里运行:

    > jps
    37137 Jps
    4860 
    37132 Launcher
    37133 JstackTest
    

    可以看到我们的java线程的pid,JstackTest这个类对应的是37133,然后再利用jstack来查看:

    > jstack -l 37133
    "jstacktest" #1 prio=5 os_prio=31 tid=0x00007f7f07001800 nid=0x2903 waiting on condition [0x0000700000901000]
       java.lang.Thread.State: WAITING (parking)
            at sun.misc.Unsafe.park(Native Method)
            - parking to wait for  <0x000000079582f5d0> (a java.lang.String)
            at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
            at lock.JstackTest.main(JstackTest.java:11)
    
       Locked ownable synchronizers:
            - None
    

    省略了一部分,可以看到jstacktest线程的状态是Waiting on condition(等待资源,或等待某个条件的发生),同事可以看到这样一句话:parking to wait for <0x000000079582f5d0> (a java.lang.String)。

    <0x000000079582f5d0>的类型是String,也就是之前传入park里的block字符串。而0x000000079582f5d0估计就是其地址(待验证)。


    4.2 利用LockSupport实现先进先出锁

    在来看一下java doc上提供的示例:

    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);
            while (waiters.peek() != current ||
                   !locked.compareAndSet(false, true)) {
                LockSupport.park(this);
                if (Thread.interrupted())
                    wasInterrupted = true;
            }
    
            waiters.remove();
            if (wasInterrupted)
                current.interrupt();
        }
    
        public void unlock() {
            locked.set(false);
            LockSupport.unpark(waiters.peek());
        }
    }
    

    先进先出锁就是先申请锁的线程最先获得锁的资源,实现上采用了队列再加上LockSupport.park。

    1. 将当前调用lock的线程加入队列
    2. 如果等待队列的队首元素不是当前线程或者locked为true,则说明有线程已经持有了锁,那么调用park阻塞其余的线程。
    3. 如果队首元素是当前线程且locked为false,则说明前面已经没有人持有锁,删除队首元素也就是当前的线程,然后当前线程继续正常执行。
    4. 执行完后调用unlock方法将锁变量修改为false,并解除队首线程的阻塞状态。此时的队首元素继续之前的判断。

    五、总结

    到这里,对LockSupport有了简单的认识,如果还想深入了话,就要开始阅读C++里面的代码了。后面有机会再重拾C++。最后谢谢各位园友观看,如果有描述不对的地方欢迎指正,与大家共同进步!




    参考:java-LockSupport详解

  • 相关阅读:
    【BZOJ5302】[HAOI2018]奇怪的背包(动态规划,容斥原理)
    【BZOJ5303】[HAOI2018]反色游戏(Tarjan,线性基)
    【BZOJ5304】[HAOI2018]字串覆盖(后缀数组,主席树,倍增)
    【BZOJ5305】[HAOI2018]苹果树(组合计数)
    【BZOJ5300】[CQOI2018]九连环 (高精度,FFT)
    【BZOJ5292】[BJOI2018]治疗之雨(高斯消元)
    【BZOJ5298】[CQOI2018]交错序列(动态规划,矩阵快速幂)
    【BZOJ5289】[HNOI2018]排列(贪心)
    Codeforces Round #539 Div1 题解
    【BZOJ5288】[HNOI2018]游戏(拓扑排序)
  • 原文地址:https://www.cnblogs.com/joemsu/p/8902745.html
Copyright © 2020-2023  润新知