zoukankan      html  css  js  c++  java
  • LockSupport详解

      我们知道,concurrent包是基于AQS (AbstractQueuedSynchronizer)框架,AQS框架借助于两个类:Unsafe(提供CAS操作) 和 LockSupport(提供park/unpark操作)。因此,LockSupport可谓构建concurrent包的基础之一。理解concurrent包,就从这里开始。

    LockSupport 简介

      LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。归根结底,LockSupport调用的Unsafe中的native代码。

      LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。

      park() 和 unpark()不会有 Thread.suspendThread.resume 所可能引发的死锁问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。

      如果调用线程被中断,则park方法会返回。同时park也拥有可以设置超时时间的版本。

    public static void park(Object blocker); // 暂停当前线程
    public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
    public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
    public static void park(); // 无期限暂停当前线程
    public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
    public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
    public static void unpark(Thread thread); // 恢复当前线程
    public static Object getBlocker(Thread t);
    

      为什么叫park呢,park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。

      我们可以使用它来阻塞和唤醒线程,功能和wait,notify有些相似,但是LockSupport比起wait,notify功能更强大,也好用的多。

    示例

    1、使用 wait,notify 阻塞唤醒线程

    @Test
    public void testWaitNotify() {
        Object obj = new Object();
        Thread waitThread = new Thread(() -> {
            synchronized (obj) {
                System.out.println("start wait!!!");
                try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}
                System.out.println("end wait!!!");
            }
        });
        Thread notifyThread = new Thread(() -> {
            synchronized (obj) {
                System.out.println("start notify");
                obj.notify();
                System.out.println("end notify!!!");
            }
        });
        waitThread.start();
        notifyThread.start();
    }

    结果如下所示:

    start wait!!!
    start notify
    end notify!!!
    end wait!!!
    

    使用 wait,notify 来实现等待唤醒功能至少有两个缺点:

    • 1. 由上面的例子可知,wait 和 notify 都是 Object 中的方法,在调用这两个方法前必须先获得锁对象,这限制了其使用场合:只能在同步代码块中。
    • 2. 另一个缺点可能上面的例子不太明显,当对象的等待队列中有多个线程时,notify只能随机选择一个线程唤醒,无法唤醒指定的线程。

    而使用LockSupport的话,我们可以在任何场合使线程阻塞,同时也可以指定要唤醒的线程,相当的方便。

    2、使用 LockSupport 阻塞唤醒线程

    @Test
    public void testLockSupport() {
        Thread parkThread = new Thread(() -> {
            System.out.println("开始线程阻塞");
            LockSupport.park();
            System.out.println("结束线程阻塞");
        });
        parkThread.start();
        System.out.println("开始线程唤醒");
        LockSupport.unpark(parkThread);
        System.out.println("结束线程唤醒");
    }

    结果如下所示:

    开始线程阻塞
    开始线程唤醒
    结束线程阻塞
    结束线程唤醒
    

      LockSupport.park(); 可以用来阻塞当前线程,park是停车的意思,把运行的线程比作行驶的车辆,线程阻塞则相当于汽车停车,相当直观。

      该方法还有个变体 LockSupport.park(Object blocker),指定线程阻塞的对象 blocker,该对象主要用来排查问题。方法 LockSupport.unpark(Thread thread) 用来唤醒线程,因为需要线程作参数,所以可以指定线程进行唤醒。

    许可

      上面的这个“许可”是不能叠加的,“许可”是一次性的。

      比如线程B 连续调用了三次 unpark函数,当线程A 调用 park函数就使用掉这个“许可”,如果线程A 再次调用 park,则进入等待状态。

      注意,unpark函数 可以先于 park 调用。比如线程B 调用 unpark函数,给线程A 发了一个“许可”,那么当线程A 调用 park 时,它发现已经有“许可”了,那么它会马上再继续运行。

      可能有些朋友还是不理解“许可”这个概念,我们深入HotSpot的源码来看看。每个java线程都有一个Parker实例,Parker类是这样定义的:

    class Parker : public os::PlatformParker {  
    private:  
      volatile int _counter ;  
      ...  
    public:  
      void park(bool isAbsolute, jlong time);  
      void unpark();  
      ...  
    }  
    class PlatformParker : public CHeapObj<mtInternal> {  
      protected:  
        pthread_mutex_t _mutex [1] ;  
        pthread_cond_t  _cond  [1] ;  
        ...  
    }

    LockSupport就是通过控制变量 _counter 来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。

    • 当调用 park()方法时,会将 _counter 置为 0,同时判断前值 < 1 说明前面被 unpark过,则直接退出,否则将使该线程阻塞。
    • 当调用 unpark()方法时,会将 _counter 置为 1,同时判断前值 < 1 会进行线程唤醒,否则直接退出。
      形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用 park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而 unpark则相反,它会增加一个凭证,但凭证最多只能有1个。
    • 为什么可以先唤醒线程后阻塞线程?
      因为 unpark获得了一个凭证,之后调用 park因为有凭证消费,故不会阻塞。
    • 为什么唤醒两次后阻塞两次会阻塞线程。
      因为凭证的数量最多为 1,连续调用两次 unpark 和 调用一次 unpark 效果一样,只会增加一个凭证;而调用两次 park却需要消费两个凭证。

    总结

      正如文章开始处提到的,是不是觉得 LockSupport.park() 和 unpark() 和 object.wait() 和 notify() 很相似,那么它们有什么区别呢?
    1. 面向的主体不一样。LockSuport 主要是针对 Thread 进行阻塞处理,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait() 是以对象为纬度,阻塞当前的线程和唤醒单个或所有线程。
    2. 实现机制不同。两者的阻塞队列并不交叉。也就是说 unpark 不会对 wait 起作用,notify 也不会对 park 起作用。object.notifyAll() 不能唤醒 LockSupport 的阻塞 Thread。
      LockSupport 是 JDK 中用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
  • 相关阅读:
    Something broke! (Error 500)——reviewboard
    linux内核--自旋锁的理解
    I.MX6 mkuserimg.sh hacking
    I.MX6 DNS 查看、修改方法
    I.MX6 android mkuserimg.sh
    I.MX6 AW-NB177NF wifi HAL 调试修改
    I.MX6 wpa_supplicant_8 编译问题
    I.MX6 MAC Address hacking
    I.MX6 MAC地址修改
    I.MX6 U-boot imxotp MAC address 写入
  • 原文地址:https://www.cnblogs.com/liang1101/p/12785496.html
Copyright © 2011-2022 走看看