Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析

目录

1.概述

2.LockSupport的park-unpark与Object的wait-notitfy的对比

3.LockSupport的源码分析

3.1 成员变量

3.2 park方法

3.3 unpark方法


1.概述

  • 当我要分析AQS,Condition的时候,发现它们当中都使用到了LockSupport,所以先在这里对LockSupport进行学习
  • LockSupport是一个工具类,主要作用是挂起和唤醒线程(park-unpark),它是创建锁和其他同步类的基础。

2.LockSupport的park-unpark与Object的wait-notitfy的对比

在没有LockSupport之前,线程的挂起和唤醒咱们都是通过Object的wait和notify/notifyAll方法实现。

wait-notify代码案例1:

package LockSupport;

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    obj.wait();
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        }).start();

        //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
        Thread.sleep(1000);
        obj.notify();
    }
}

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第1张图片

  • wait和notify底层是基于ObjectMonitor的,而ObjectMonitor只有在使用了synchronized时才能获取,即只有synchronized修饰的代码块或者方法中才能使用wait和notify,所以上述出现java.lang.IllegalMonitorStateException
  • 所以对于上述的改进,我们只需要加入synchronized修饰即可

wait-notify代码案例1改进:

package LockSupport;

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    synchronized (obj){
                        obj.wait();
                    }

                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        }).start();

        //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
        Thread.sleep(1000);
        synchronized (obj){
            obj.notify();
        }
    }
}

当我们换成LockSupport的park和unpark后,如下

park-unpark代码案例1:

package LockSupport;

import java.util.concurrent.locks.LockSupport;

public class TestLockSupport {

    public static void main(String[] args)throws Exception {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    //注意:LockSupport中所有的方法都是static的,所以直接使用类来调用即可
                    LockSupport.park();

                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        });
        thread.start();

        //睡眠一秒钟,保证线程A已经计算完成,阻塞在park方法
        Thread.sleep(1000);
        LockSupport.unpark(thread);
    }
}
  • 调用LockSupport.park()阻塞当前线程,调用LockSupport.unpark(Thread)来唤醒指定线程thread

我们分别将上述两段代码的注释

wait-notify代码案例2:

package LockSupport;

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    synchronized (obj){
                        obj.wait();
                    }

                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        }).start();

        //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
        //Thread.sleep(1000);
        synchronized (obj){
            obj.notify();
        }
    }
}

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第2张图片

  • 可以发现线程进行了阻塞
  • 原因就在于:主线程调用完notify后,创建的线程才进入wait方法,导致线程一直阻塞住。由于线程不是守护线程,所以整个程序无法退出。

而LockSupport就支持主线程先调用unpark后,线程A再调用park而不被阻塞

park-unpark代码案例2:

package LockSupport;

import java.util.concurrent.locks.LockSupport;

public class TestLockSupport02 {

    public static void main(String[] args)throws Exception {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    //注意:LockSupport中所有的方法都是static的,所以直接使用类来调用即可
                    LockSupport.park();

                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        });
        thread.start();

        //睡眠一秒钟,保证线程A已经计算完成,阻塞在park方法
        //Thread.sleep(1000);
        LockSupport.unpark(thread);
    }
}

  • 不管你执行多少次,这段代码都能正常打印结果并退出

总结:LockSupport比Object的wait/notify的优势

  • 1.LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
  • 2.unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
  • 3.LockSupport的unpark方法可以唤醒指定线程,而notify只会唤醒它所在的synchronized获得的锁对象上wait的线程

3.LockSupport的源码分析

3.1 成员变量

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

关于SEED和PROBE的作用可以看我的另一篇博客:https://blog.csdn.net/qq_34805255/article/details/102384212

在我们第一次主动使用LockSupport类(如调用LockSupport的某个静态方法)的时候,进行类的初始化的时候,会执行上述代码

3.2 park方法

  • LockSupport类与每个使用他的线程都会关联一个许可证,默认调用LockSupport 类的方法的线程是不持有许可证的,所以调用park方法由于没有许可证,就会阻塞,只有调用unpark得到许可证的时候才会马上返回
  • 而如果先调用unpark方法,就会让调用线程拥有许可证,而在此时调用park方法,会立即返回

首先说明Thread类中有个parkBlocker字段,就是会在park方法中使用

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第3张图片

park方法横向的其实有两种

  • 一种是设置线程字段parkBlocker的方法,下述的Object类型的参数就是要赋值给parkBlocker的值

  • 另一种是不设置parkBlocker字段的方法

我们来对比一下它们的源码:

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第4张图片

可以发现,

  • 其实它们的底层都是使用Unsafe的park方法
  • 第一种比第二种多的就是在它前后调用setBlocker方法

  • 而我们可以看到setBlocker就是将传入的Object类型的参数赋值给当前线程对象的parkBlocker字段

那么设置此Blocker有什么作用呢?

  • 当线程因为没有持有许可证的情况下调用park  被阻塞挂起的时候,这个blocker对象会被记录到该线程内部。使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取该blocker对象的,所以JDK推荐我们使用带有blocker参数的park方法,并且blocker设置为this,这样当内存dump排查问题时候就能知道是哪个类被阻塞了

为什么要前后调用两次setBlocker呢?

  • 为了保证在park(Object blocker)整个函数执行完后,该线程的parkBlocker字段又恢复为null。
  • 调用park函数时,当前线程首先设置好parkBlocker字段,然后再调用Unsafe的park函数,此后,当前线程就已经阻塞了,等待该线程的unpark函数被调用,所以后面的一个setBlocker函数无法运行,unpark函数被调用,该线程获得许可后,就可以继续运行了,也就运行第二个setBlocker,把该线程的parkBlocker字段设置为null,这样就完成了整个park函数的逻辑。

示例:

没有传入Blocker:

package LockSupport;

import java.util.concurrent.locks.LockSupport;

public class TestPark {

    public static void main(String[] args) {
        LockSupport.park();
    }
}

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第5张图片

使用jps和jstack工具配合查看

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第6张图片

当传入Blocker:

package LockSupport;

import java.util.concurrent.locks.LockSupport;

public class TestPark {

    public static void main(String[] args) {
        TestPark testPark = new TestPark();
        LockSupport.park(testPark);
    }
}

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第7张图片

  • 可以看到打印出来的日志,就指明了TestPark类的park方法正在阻塞

我们接下来再纵向的比较一下三种不同参数的park方法

1.park()

  •  如果调用 park() 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,否者调用线程会被禁止参与线程的调度,也就是会一直被阻塞挂起

  • 在看到其他线程调用 unpark(Thread thread) 方法并且当前线程作为参数时候,调用park方法被阻塞的线程会返回,
  • 另外其他线程调用了阻塞线程的interrupt()方法,设置了中断标志时候或者由于线程的虚假唤醒原因后阻塞线程也会返回,所以调用 park() 最好也是用循环条件判断方式。

需要注意的是调用park()方法被阻塞的线程被其他线程中断后阻塞线程返回时候并不会抛出InterruptedException 异常。

示例:

package LockSupport;

import java.util.concurrent.locks.LockSupport;

/**
 * 调用park方法的线程只有中断才能使park方法返回
 */
public class LockSupportTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程 park start!");
                // 调用park方法,挂起自己,只有中断才会退出循环
                while (!Thread.currentThread().isInterrupted()) {
                    LockSupport.park();

                }
                System.out.println("子线程 退出!");
            }
        });

        //启动子线程
        thread.start();
        //主线程休眠1S
        Thread.sleep(1000);
        System.out.println("主线程 interrupt start!");
        //中断子线程
        thread.interrupt();
    }
}

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第8张图片

  • 此段代码中只有当子线程被中断后,才会设置中断标志进入循环,并且由于中断,park方法返回,子线程才会运行结束,如果子线程不被中断,即使你调用unpark(Thread) 子线程也不会结束。

2.parkNanos(long nanos)

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第9张图片

  • 和 park 类似,如果调用 park 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,不同在于如果没有拿到许可调用线程会被挂起 nanos 时间后在返回。

3.parkUntil(long deadline)

  • UNSAFE的park方法传入true,代表使用绝对时间,
  • 所以deadline是一个绝对时间,单位为ms,该时间是从1970年到某一个时间点的毫秒值

3.3 unpark方法

  • 当一个线程调用了 unpark 时候,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有。如果 thread 之前调用了 park() 被挂起,则调用 unpark 后,该线程会被唤醒。

示例1:

package LockSupport;

import java.util.concurrent.locks.LockSupport;

public class LockSupportTest {
    public static void main( String[] args ) {
        System.out.println( "park start!" );
        //使当前线程获取到许可证
        LockSupport.unpark(Thread.currentThread());
        //再次调用park
        LockSupport.park();
        System.out.println( "park stop!" );
    }
}

Java多线程(14)——JUC——locks系列(1)——LockSupport使用以及源码分析_第10张图片

示例2:

package LockSupport;

import java.util.concurrent.locks.LockSupport;

public class LockSupportTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程 park start!");
                //由于线程默认没有许可,调用park方法,挂起自己
                LockSupport.park();
                System.out.println("子线程 unpark!");
            }
        });

        //启动子线程
        thread.start();
        //主线程休眠1S
        Thread.sleep(1000);
        System.out.println("主线程 unpark start!");
        //调用unpark让thread线程持有许可证,然后park方法会返回
        LockSupport.unpark(thread);
    }
}

本文参考:

https://www.cnblogs.com/qingquanzi/p/8228422.html

https://blog.csdn.net/liangwenmail/article/details/82151614

你可能感兴趣的:(#,Java多线程)