zoukankan      html  css  js  c++  java
  • 关于LockSupport

    一.LockSupport是什么

    LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。

    Doug Lea 的神作concurrent包是基于AQS (AbstractQueuedSynchronizer)框架,AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)。

    因此,LockSupport可谓构建concurrent包的基础之一。LockSupport类是Java6(JSR166-JUC)引入的一个类。

    二.LockSupport能做什么

    LockSupport是JDK中用来实现线程阻塞和唤醒的工具。

    使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。

    JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。

    LockSupport的优点:

    (1)单个线程的阻塞唤醒非常方便,避免了Synchronized、wait、notify等编写,简介代码;

    (2)其park()、unpark()方法没有顺序概念,先调哪个后调哪个都可以,但是一定要成对出现。

    三.LockSupport原理

    LockSupport调用的Unsafe中的native代码:

    public native void unpark(Thread jthread); 
    public native void park(boolean isAbsolute, long time); 

    两个函数声明清楚地说明了操作对象:park函数是将当前Thread阻塞,而unpark函数则是将另一个Thread唤醒。

    与Object类的wait/notify机制相比,park/unpark有两个优点:

    (1)以thread为操作对象更符合阻塞线程的直观定义;

    (2)操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

    在上面的文字中,使用了阻塞和唤醒,是为了和wait/notify做对比。

    其实park/unpark的设计原理核心是“许可”。park是等待一个许可。

    unpark是为某线程提供一个许可。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。

    有一点比较难理解的,是unpark操作可以再park操作之前。也就是说,先提供许可。当某线程调用park时,已经有许可了,它就消费这个许可,然后可以继续运行。这其实是必须的。

    考虑最简单的生产者(Producer)消费者(Consumer)模型:

    Consumer需要消费一个资源,于是调用park操作等待;Producer则生产资源,然后调用unpark给予Consumer使用的许可。

    非常有可能的一种情况是,Producer先生产,这时候Consumer可能还没有构造好(比如线程还没启动,或者还没切换到该线程)。

    那么等Consumer准备好要消费时,显然这时候资源已经生产好了,可以直接用,那么park操作当然可以直接运行下去。如果没有这个语义,那将非常难以操作。

    LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。

    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.suspend和Thread.resume所可能引发的死锁” 问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。

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

    需要特别注意的一点:park 方法还可以在其他任何时间“毫无理由”地返回,因此通常必须在重新检查返回条件的循环里调用此方法。

    从这个意义上说,park 是“忙碌等待”的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高效。

    三种形式的 park 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。

    (这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。在锁实现中提供的作为 blocker 的普通参数是 this。

    四.LockSupport使用

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

    代码:

    public class WaitNotifyTest {
        private static Object obj = new Object();
        public static void main(String[] args) {
            new Thread(new WaitThread()).start();
            new Thread(new NotifyThread()).start();
        }
        static class WaitThread implements Runnable {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("start wait!");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("end wait!");
                }
            }
        }
        static class NotifyThread implements Runnable {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("start notify!");
                    obj.notify();
                    System.out.println("end notify");
                }
            }
        }
    }

    运行结果:

    start wait!
    start notify!
    end notify
    end wait!

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

    (1)由上面的例子可知,wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,这限制了其使用场合:只能在同步代码块中。

    (2)另一个缺点可能上面的例子不太明显,当对象的等待队列中有多个线程时,notify只能随机选择一个线程唤醒,无法唤醒指定的线程。

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

    2、使用LockSupport阻塞唤醒线程

    代码:

    public class LockSupportTest {
    
        public static void main(String[] args) {
            Thread parkThread = new Thread(new ParkThread());
            parkThread.start();
            System.out.println("开始线程唤醒");
            LockSupport.unpark(parkThread);
            System.out.println("结束线程唤醒");
    
        }
    
        static class ParkThread implements Runnable{
    
            @Override
            public void run() {
                System.out.println("开始线程阻塞");
                LockSupport.park();
                System.out.println("结束线程阻塞");
            }
        }
    }

    运行结果:

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

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

    该方法还有个变体LockSupport.park(Object blocker),指定线程阻塞的对象blocker,该对象主要用来排查问题。方法LockSupport.unpark(Thread thread)用来唤醒线程,

    因为需要线程作参数,所以可以指定线程进行唤醒。

    3、可以先唤醒线程再阻塞线程

    代码:

    public class LockSupportTest {
    
        public static void main(String[] args) {
            Thread parkThread = new Thread(new ParkThread());
            parkThread.start();
            System.out.println("开始线程唤醒");
            LockSupport.unpark(parkThread);
            System.out.println("结束线程唤醒");
    
        }
    
        static class ParkThread implements Runnable{
    
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("开始线程阻塞");
                LockSupport.park();
                System.out.println("结束线程阻塞");
            }
        }
    }

    运行结果:

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

    先唤醒指定线程,然后阻塞该线程,但是线程并没有真正被阻塞而是正常执行完后退出了。这是怎么回事?在改动下代码,先唤醒线程两次,在阻塞线程两次,看看会发生什么。

    4、先唤醒线程两次再阻塞两次会发生什么

    public class LockSupportTest {
    
        public static void main(String[] args) {
            Thread parkThread = new Thread(new ParkThread());
            parkThread.start();
            for(int i=0;i<2;i++){
                System.out.println("开始线程唤醒");
                LockSupport.unpark(parkThread);
                System.out.println("结束线程唤醒");
            }
        }
    
        static class ParkThread implements Runnable{
    
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for(int i=0;i<2;i++){
                    System.out.println("开始线程阻塞");
                    LockSupport.park();
                    System.out.println("结束线程阻塞");
                }
            }
        }
    }

    运行结果:

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

    可以看到线程被阻塞导致程序一直无法结束掉。对比上面的例子,可以得出一个匪夷所思的结论,先唤醒线程,在阻塞线程,线程不会真的阻塞;

    但是先唤醒线程两次再阻塞两次时就会导致线程真的阻塞。

    总结:

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

    (1)当调用park()方法时,会将_counter置为0,同时判断前值,小于1说明前面被unpark过,则直接退出,否则将使该线程阻塞。

    (2)当调用unpark()方法时,会将_counter置为1,同时判断前值,小于1会进行线程唤醒,否则直接退出。

    形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;

    但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。

    (3)为什么可以先唤醒线程后阻塞线程?

    因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。

    (4)为什么唤醒两次后阻塞两次会阻塞线程。

    因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。

  • 相关阅读:
    GDI 设备环境句柄(2)
    GDI 像素(5)
    Api+Mvc增删查改
    sql语句全
    Mvc 导出
    触发器、事务
    计算时间戳的差
    SQL行转列经典例子(转载)
    Socket (套接字)通信
    MVC上传图片
  • 原文地址:https://www.cnblogs.com/ZJOE80/p/12909661.html
Copyright © 2011-2022 走看看