菜鸟笔记
提升您的技术认知

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

类的构造函数

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

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

前面简单的介绍了一下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();
    }
}

结果:

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

我们通过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 、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();
    }
}

结果:
线程A被LockSupport.park()阻塞
线程B唤醒线程A
线程A被线程B LockSupport.unpark()唤醒

从上面可以看出使用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



三种方法的总结

方法 特点 缺点
wait/notify wait和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);
    }

上面的方法如何理解呢?
如果没有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);
    }

给指定的线程提供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);
}

说明: 调用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()方法中实现的。