• LockSupport从入门到深入理解


    LockSupport 常见面试题

    1、为什么LockSupport也是核心基础类? AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作) 2、写出分别通过wait/notify和LockSupport的park/unpark实现同步?
    3、LockSupport.park()会释放锁资源吗? 那么Condition.await()呢?
    4、Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别?
    5、 重点 如果在wait()之前执行了notify()会怎样?
    6、如果在park()之前执行了unpark()会怎样?

    一、LockSupport 是什么?

    LockSupport是用来创建锁和其他同步工具类的基本线程阻塞原语。
    java锁和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通过调用 LockSupport .park()和 LockSupport .unpark()实现线程的阻塞和唤醒 的。 LockSupport 很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继 续 执行;如果许可已经被占用,当前线 程阻塞,等待获取许可。

    LockSupport 类的属性

    public class LockSupport {
        // Hotspot implementation via intrinsics API
        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实例
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                // 线程类类型
                Class<?> tk = Thread.class;
                // 获取Thread的parkBlocker字段的内存偏移地址
                parkBlockerOffset = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("parkBlocker"));
                // 获取Thread的threadLocalRandomSeed字段的内存偏移地址
                SEED = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("threadLocalRandomSeed"));
                // 获取Thread的threadLocalRandomProbe字段的内存偏移地址
                PROBE = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("threadLocalRandomProbe"));
                // 获取Thread的threadLocalRandomSecondarySeed字段的内存偏移地址
                SECONDARY = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    类的构造函数

    // 私有构造函数,无法被实例化
    private LockSupport() {}
    
    
    • 1
    • 2
    • 3

    二、三种让线程等待和唤醒的方法

    前面简单的介绍了一下LockSupport定义。接下来我们介绍java中三种阻塞和唤醒机制,并总结它们的优缺点。

    方法一:使用Object中的wait()方法让线程等待,使用Object的notify()方法唤醒线程,结合synchronized;
    方法二:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程;
    方法三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程;

    我们用具体实例演示三种方法

    方法一 使用wait()和notify():

    public class ObjectWait {
    
        public static void main(String[] args) {
            Object o = new Object();
    
    
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
    
                    System.out.println("线程A被o.wait()阻塞前");
                    synchronized(o){
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                    }
                    System.out.println("线程A被线程B o.notify()唤醒");
    
                }
            },"A");
    
            t.start();
    
    
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程B唤醒线程A");
                    synchronized (o){
                        o.notify();
                    }
                }
            },"B").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    结果:

    线程A被o.wait()阻塞前
    线程B唤醒线程A
    线程A被线程B o.notify()唤醒
    
    • 1
    • 2
    • 3

    我们通过o.wait()将线程A阻塞,再通过线程B中运行o.notify()方法将线程A唤醒.
    注意:1、wait和notify都需要在同步块或者同步方法里,也就是要使用到synchronized,将资源类锁住,且必须成对出现。 2、使用时必须先wait 在notify,否则wait不会被唤醒的情况,从而导致线程一直阻塞。
    这里我没有演示 先notify再wait 会出现的wait不会唤醒的情况,大家可以自行测试。

    方法二: Lock .condition

    public class ConditionAwait {
    
        public static void main(String[] args) {
    
            Lock lock = new ReentrantLock();
            Condition condition = lock.newCondition();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
    
                    System.out.println("线程A被condition.await()阻塞前");
    
                    try {
                        lock.lock();
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                    System.out.println("线程A被线程B condition.signl()唤醒");
                }
            }, "A").start();
    
    
            new Thread(new Runnable() {
                @Override
                public void run() {
    
    
    
                    try {
                        lock.lock();
    
                        System.out.println("线程B中使用condition.signal()唤醒线程A");
                        condition.signal();
                    }catch (Exception e){
    
    
                    }finally {
                        lock.unlock();
                    }
    
                }
            }, "B").start();
    
        }
    }
    
    结果:
    线程A被condition.await()阻塞前
    线程B中使用condition.signal()唤醒线程A
    线程A被线程B condition.signl()唤醒
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    注意:1 、Condition中的线程等待和唤醒一定要先获得锁。
    2、一定要先await,再signal,不能反了

    方法三,使用LockSupport

    public class LockSupportDemo {
    
        public static void main(String[] args) {
    
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
    
                    System.out.println("线程A被LockSupport.park()阻塞");
                    LockSupport.park();
    
                    System.out.println("线程A被线程B LockSupport.unpark()唤醒");
    
                }
            },"A");
    
            t.start();
            
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程B唤醒线程A");
                    // 唤醒指定线程t,也就是A
                    LockSupport.unpark(t);
                }
            },"B").start();
        }
    }
    
    结果:
    线程ALockSupport.park()阻塞
    线程B唤醒线程A
    线程A被线程B LockSupport.unpark()唤醒
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    从上面可以看出使用LockSupport 进行线程阻塞和唤醒可以在线程的任意地方执行,并且可以通过unpart(thread)唤醒指定的线程。作为工具类LockSupport的使用,也降低了代码的耦合性。

    使用interrupt() 中断park()阻塞

    package CompleteFuture;
    
    import java.util.concurrent.locks.LockSupport;
    
    public class LockSupportDemo {
    
        public static void main(String[] args) {
    
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
    
                    System.out.println("before park");
                    LockSupport.park();
                    System.out.println("after park");
    
                }
            },"A");
    
            t.start();
    
    
           //确保 park()执行
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    //        System.out.println("线程t是否被阻塞: "+t.isInterrupted());
            System.out.println("before interrupted");
            t.interrupt();
            System.out.println("after interrupted");
    
        }
    }
    
    结果:
    before park
    before interrupted
    after interrupted
    after park
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    三种方法的总结

    方法特点缺点
    wait/notifywait和notify都需要在同步块或者同步方法里,也就是要使用到synchronized,将资源类锁住,且必须成对出现。 2、使用时必须先wait 在notify,否则wait不会被唤醒的情况,从而导致线程一直阻塞。需要借助synchronized
    condition需要结合lock 和unlock ,可以精准唤醒指定线程(示例没有展示),大家自行研究它的底层其实还是使用的LockSupport
    LockSupport使用park 和unpark唤醒指定线程 ,不关系是先执行 unpark 还是park,只要是成对出现线程都将被释放多次调用unpark也只能释放一次

    三、LockSupport 源码分析

    LockSupport中方法如下:
    在这里插入图片描述

    3.1 park() 源码分析

    /**Disables the current thread for thread scheduling purposes unless the permit is available.
    If the permit is available then it is consumed and the call returns immediately; otherwise the current thread becomes disabled for thread scheduling purposes and lies dormant until one of three things happens:
    Some other thread invokes unpark with the current thread as the target; or
    Some other thread interrupts the current thread; or
    The call spuriously (that is, for no reason) returns.
    This method does not report which of these caused the method to return. Callers should re-check the conditions which caused the thread to park in the first place. Callers may also determine, for example, the interrupt status of the thread upon return. 
    */
    public static void park() {
            UNSAFE.park(false, 0L);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上面的方法如何理解呢?
    如果没有permit许可,那么调用该方法后,当前线程立马停止执行计划(阻塞),直到有一下3中情况发生:
    1、其他线程调用unpark(被阻塞线程引用)方法,参数为需要唤醒的线程;
    2、其他线程中断当前线程;
    3、调用虚假(即无缘无故)返回;

    UNSAFE.park(isAbsolute,timeout)的理解,阻塞一个线程直到unpark出现、线程

    • 被中断或者timeout时间到期。如果一个unpark调用已经出现了,

    • 这里只计数。timeout为0表示永不过期.当isAbsolute为true时,

    • timeout是相对于新纪元之后的毫秒。否则这个值就是超时前的纳秒数。这个方法执行时

    • 也可能不合理地返回(没有具体原因)
      深入理解sun.misc.Unsafe原理

    3.2 unpark(Thread thread)

     public static void unpark(Thread thread) {
            if (thread != null)
                UNSAFE.unpark(thread);
        }
    
    • 1
    • 2
    • 3
    • 4

    给指定的线程提供unblock凭证。如果指定的线程使用了park(),则线程变成非阻塞。如果没有使用park,则线程下一次使用park时,怎线程不会阻塞。

    park(blocker) 锁定指定对象

    public static void park(Object blocker) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 设置Blocker
        setBlocker(t, blocker);
        // 获取许可
        UNSAFE.park(false, 0L);
        // 重新可运行后再此设置Blocker
        setBlocker(t, null);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    说明: 调用park函数时,首先获取当前线程,然后设置当前线程的parkBlocker字段,即调用setBlocker函数,之后调用Unsafe类的park函数,之后再调用setBlocker函数。那么问题来了,为什么要在此park函数中要调用两次setBlocker函数呢? 原因其实很简单,调用park函数时,当前线程首先设置好parkBlocker字段,然后再调用Unsafe的park函数,此后,当前线程就已经阻塞了,等待该线程的unpark函数被调用,所以后面的一个setBlocker函数无法运行,unpark函数被调用,该线程获得许可后,就可以继续运行了,也就运行第二个setBlocker,把该线程的parkBlocker字段设置为null,这样就完成了整个park函数的逻辑。如果没有第二个setBlocker,那么之后没有调用park(Object blocker),而直接调用getBlocker函数,得到的还是前一个park(Object blocker)设置的blocker,显然是不符合逻辑的。总之,必须要保证在park(Object blocker)整个函数执行完后,该线程的parkBlocker字段又恢复为null。所以,park(Object)型函数里必须要调用setBlocker函数两次。setBlocker方法如下。

    五、更深入的理解

    5.1 Thread.sleep()和Object.wait()的区别 Thread.sleep()不会释放占有的锁,Object.wait()会释放占有的锁;

    • Thread.sleep()必须传入时间,Object.wait()可传可不传,不传表示一直阻塞下去;
    • Thread.sleep()到时间了会自动唤醒,然后继续执行;
    • Object.wait()不带时间的,需要另一个线程使用Object.notify()唤醒;
    • Object.wait()带时间的,假如没有被notify,到时间了会自动唤醒,这时又分好两种情况,一是立即获取到了锁,线程自然会继续执行;二是没有立即获取锁,线程进入同步队列等待获取锁;
    • 其实,他们俩最大的区别就是Thread.sleep()不会释放锁资源,Object.wait()会释放锁资源。

    5.2 Object.wait()和Condition.await()的区别

    • Object.wait()和Condition.await()的原理是基本一致的,不同的是Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。
    • 实际上,它在阻塞当前线程之前还干了两件事,一是把当前线程添加到条件队列中,二是“完全”释放锁,也就是让state状态变量变为0,然后才是调用LockSupport.park()阻塞当前线程。

    5.3 Thread.sleep()和LockSupport.park()的区别

    • LockSupport.park()还有几个兄弟方法——parkNanos()、parkUtil()等,我们这里说的park()方法统称这一类方法。
    • 从功能上来说,Thread.sleep()和LockSupport.park()方法类似,都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;
    • Thread.sleep()没法从外部唤醒,只能自己醒过来;
    • LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;
    • Thread.sleep()方法声明上抛出了InterruptedException中断异常,所以调用者需要捕获这个异常或者再抛出;
    • LockSupport.park()方法不需要捕获中断异常;
    • Thread.sleep()本身就是一个native方法; LockSupport.park()底层是调用的Unsafe的native方法;

    5.4 Object.wait()和LockSupport.park()的区别 二者都会阻塞当前线程的运行,他们有什么区别呢?

    • 经过上面的分析相信你一定很清楚了,真的吗? 往下看!
    • Object.wait()方法需要在synchronized块中执行; LockSupport.park()可以在任意地方执行;
    • Object.wait()方法声明抛出了中断异常,调用者需要捕获或者再抛出
    • LockSupport.park()不需要捕获中断异常;
    • Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;
    • LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;
    • park()/unpark()底层的原理是“二元信号量”,你可以把它相像成只有一个许可证的Semaphore,只不过这个信号量在重复执行unpark()的时候也不会再增加许可证,最多只有一个许可证。

    5.5 如果在wait()之前执行了notify()会怎样?

    • 如果当前的线程不是此对象锁的所有者,却调用该对象的notify()或wait()方法时抛出IllegalMonitorStateException异常;
    • 如果当前线程是此对象锁的所有者,wait()将一直阻塞,因为后续将没有其它notify()唤醒它。

    5.6 如果在park()之前执行了unpark()会怎样?

    线程不会被阻塞,直接跳过park(),继续执行后续内容

    LockSupport.park()会释放锁资源吗?

    不会,它只负责阻塞当前线程,释放锁资源实际上是在Condition的await()方法中实现的。
    参考文章

    https://blog.csdn.net/u013851082/article/details/70242395

  • 相关阅读:
    centos7.9系统安装cuda+cudnn+pytorch+python
    PostgreSQL索引篇 | GIN索引 (倒排索引)
    算法的时间复杂度
    .NET 6.0中使用Identity框架实现JWT身份认证与授权
    js中函数多参数的简化
    我的第一个Spring Boot应用实现步骤以及遇到的问题
    使用C语言+USRP B210从零开始实现无线通信(5) 纠错与输出
    Java版本spring cloud + spring boot企业电子招投标系统源代码
    助力工业物联网,工业大数据之客户回访事实指标需求分析【二十三】
    拓端tecdat|R语言社区主题检测算法应用案例
  • 原文地址:https://blog.csdn.net/u010445301/article/details/125437214