[JUC] LockSupport 相关原理和使用的学习

一、什么是LockSupport

LockSupport是java.util.concurrent.locks包下面的一个类。在java并发包下各种同步组件的底层实现中,LockSupport的身影处处可见。JDK中的定义为用来创建锁和其他同步类的线程阻塞原语。我们可以使用它来阻塞和唤醒线程,功能和wait,notify有些相似,但是LockSupport比起wait,notify功能更强大,也好用的多。

并发组件和并发工具大都是基于AQS来实现的, AQS就是AbstractQueuedSynchronizer, 队列同步器(简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态(许可),通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

而AQS中的控制线程又是通过LockSupport类来实现的,因此可以说,LockSupport是Java并发基础组件中的基础组件

二、LockSupport核心方法

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。LockSupport提供的阻塞和唤醒方法如下:

public static void park(Object blocker); // 与park()方法同义,但它多传入的参数为阻塞对象。
public static void parkNanos(Object blocker, long nanos); // 与parkNanos(long)同义,但指定了阻塞对象。
public static void parkUntil(Object blocker, long deadline); // 与parkUntil(long)同义,但指定了阻塞对象。
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方法时提供的阻塞器对象,如果没有被阻塞,则返回null。返回的值只是一个瞬间快照——线程可能已经解除阻塞或阻塞在不同的阻塞对象上。

使用park和unpark进行线程的阻塞和唤醒操作,park和unpark底层是借助系统层(C语言)方法pthread_mutex和pthread_cond来实现的,通过pthread_cond_wait函数可以对一个线程进行阻塞操作,在这之前,必须先获取pthread_mutex,通过pthread_cond_signal函数对一个线程进行唤醒操作。(C语言范畴,不做过多解释,有需要了解底层的可自行搜索)

在核心方法解释中多处提到许可一词,这个就是我们说的那一个int成员变量表示同步状态。其中有许可用和无许可分别用1和0来表示。每个线程都对应一个信号变量,当线程调用park时其实就是去获取许可,如果能成功获取到许可则能够往下执行,否则则阻塞直到成功获取许可为止。而当线程调用unpark时则是释放许可,供线程去获取。

三、LockSupport 例子

线程1启动后,如果message为空则一直park等待。线程1启动后主线程沉睡3秒后线程2启动。线程2中给message赋值,之后 unpark唤醒线程1,线程1打印出message。

public class LockSupportDemo {

    private static String message;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() ->  {
            while (message == null) {
                System.out.println("Thread1 is pending!!!");
                LockSupport.park();
            }
            System.out.println(message);
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread2 trying to change message");
            message = "thread2 wakes up thread1";
            LockSupport.unpark(thread1);
        });

        thread1.start();
        Thread.sleep(3000);
        thread2.start();
    }
}

四、park 与 unpark 的顺序 

1. wait / notify的例子

这个就是LockSupport与wait, notify最不同的地方。也是优于Object本身阻塞方法的地方。

正常来说一般是先park线程阻塞,然后unpark来唤醒该线程。但是如果先执行unpark再执行park,会不会park就永远没办法被唤醒了呢?在往下分析之前我们先来看看wait/notify方式的执行顺序的例子。如果主线程分别创建了thread1和thread2,然后启动它们。由于带有wait方法的thread1会睡眠三秒,所以先由thead2执行了notify去唤醒阻塞在锁对象的线程,而在三秒后thread1才会执行wait方法,此时它将一直阻塞在那里。所以wait/notify方式的执行顺序会影响到唤醒。

public class LockSupportDemo2 {

    private static String message;

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(message);
        });
        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                message = "thread2 wakes up thread1";
                lock.notify();
            }
        });
        thread1.start();
        thread2.start();
    }
}

2. park / unpark的例子

类似地,我们对park和unpark进行分析。这个例子与上面的wait/notify例子逻辑几乎相同,唯一不同的是阻塞和唤醒操作改为了park和unpark。实际上这个例子能正确运行,且最终输出了“thread2 wakes up thread1”。从此可以看出park/unpark方式的执行顺序不影响唤醒,这是因为park/unpark是使用了许可机制,如果先调用unpark去释放许可,那么调用park时就直接能获取到许可而不必等待。

public class LockSupportDemo3 {

    private static String message;

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                try {
                    LockSupport.park();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println(message);
        });
        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                message = "thread2 wakes up thread1";
                LockSupport.unpark(thread1);
            }
        });
        thread1.start();
        thread2.start();
    }
}

3. park方法对中断的响应

park方法支持中断,也就是说一个线程调用park方法进入阻塞后,如果该线程被中断则能够解除阻塞立即返回。但需要注意的是,它不会抛出中断异常,所以我们不必去捕获InterruptedException。下面是一个中断的例子,thread1启动后调用park方法进入阻塞状态,然后主线程睡眠一秒后中断thread1,此时thread1将解除阻塞状态并输出null。接着主线程睡眠三秒后启动thread2,thread2将调用unpark,但thread1已经因中断而解除阻塞了。

public class LockSupportDemo4 {

    private static String message;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            LockSupport.park();
            System.out.println(message);
        });

        Thread thread2 = new Thread(() -> {
            message = "thread2 wakes up thread1";
            LockSupport.unpark(thread1);
        });

        thread1.start();
        Thread.sleep(1000);
        thread1.interrupt();
        Thread.sleep(3000);
        thread2.start();
    }
}

4. park是否释放锁

当线程调用LockSupport的park方法时是否会释放该线程所持有的锁资源呢?答案是不会。我们看下面的例子来理解,thread1和thread2两个线程都通过synchronized(lock)来获取锁。由于thread1先启动所以获得锁,而且它调用park方法使得thread1进入阻塞状态,但是thread1却不会释放锁lock。对于thread2这边,因为一直无法获取锁而无法进入同步块,也就没办法执行unpark操作。最终的结果就是造成了死锁,thread1在等thread2的unpark,而thread2却在等thread1释放锁。

public class LockSupportDemo5 {

    private static String message;

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                LockSupport.park();
                System.out.println(message);
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                message = "thread2 wakes up thread1";
                LockSupport.unpark(thread1);
            }
        });

        thread1.start();
        Thread.sleep(3000);
        thread2.start();
    }
}

五、LockSupport的底层实现

上面也说了,他是借助系统层(C语言)方法pthread_mutex和pthread_cond来实现的。但是从Java层面说,他是间接调用Unsafe的方法。Unsafe类提供了很多底层的和不安全的操作,主要用了它park、unpark和putObject三类方法,park和unpark方法可以看出是对底层线程进行操作,而putObject方法则是将对象设置为Thread对象里面的parkBlocker属性,getObjectVolatile方法则是获取Thread对象里面的parkBlocker属性值。unpark方法则是间接调用Unsafe对象的unpark方法。

下面是LockSupport JDK源码:

    //直接调用UNSAFE的park方法。传入false,偏移量为0
    public static void park() {
        UNSAFE.park(false, 0L);
    }    

    //setBlocker调用UNSAFE方法的putObject将传入对象置入,然后执行park,
    //之后再调用setBlocker将对象置空
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

    //调用UNSAFE的park方法 ,传入等待时长
    public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }

    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }

    //传入等待时常,如果超过后自动释放
    public static void parkUntil(long deadline) {
        UNSAFE.park(true, deadline);
    }

    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

    //调用UNSAFE的unpark方法直接将所需释放线程传入
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

    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);
    }

   

六、两次唤醒两次阻塞会怎样

我们在上文知道了LockSupport的park和unpark方法无需像wait/notify一样有严格的执行顺序,但是如果我们首先做两次唤醒,然后在执行两次阻塞会怎样呢?

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("结束线程阻塞");
            }
        }
    }
}

从上面的代码里面我们可以看到,首先我们在线程中park了两次 ,然后开启线程后我们又循环两次的unpark唤醒。但是输出结果中我们看到的却是线程一起挂起没法停止。照理说两次park对应两次unpark没毛病。但是从结果我们可以得出一个匪夷所思的结论,先唤醒线程,在阻塞线程,线程不会真的阻塞;但是先唤醒线程两次再阻塞两次时就会导致线程真的阻塞。那么这到底是为什么?

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

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

总结

LockSuppor主要的功能就是实现线程的阻塞和唤醒,而且使用了许可机制。它的核心方法包括六种park方法和unpark方法,然后通过例子我们了解了LockSupport的基本用法,接着探讨了park与unpark的执行顺序,然后继续分析了park对中断的响应和park时是否会释放锁,同时也介绍了阻塞对象Blocker以及它的作用。最后从源码角度剖析了LockSupport类的实现,至此我们已经完全掌握了LockSupport的机制原理。

参考文章: https://blog.51cto.com/u_15057823/2631850

                   https://www.cnblogs.com/takumicx/p/9328459.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值