LockSupport的使用

LockSupport

一、为什么需要LockSupport类

来看下在没有LockSupport之前,是怎么实现让线程等待/唤醒的。在没有LockSupport之前,线程的挂起和唤醒咱们都是通过Object的wait和notify/notifyAll方法实现。

那如果咱们换成LockSupport呢?简单得很,看代码:

public static void main(String[] args) throws Exception {
    Thread A = new Thread(() -> {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            sum += i;
        }
        LockSupport.park();
        System.out.println(sum);
    });
    A.start();
    //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
    Thread.sleep(1000);
    LockSupport.unpark(A);
}

二、LockSupport灵活性

首先LockSupport在使用起来比Object的wait/notify简单,而且最重要的是灵活性。

上边的例子代码中,主线程调用了Thread.sleep(1000)方法来等待线程A计算完成进入wait状态。如果去掉Thread.sleep()调用:

public static void main(String[] args) throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(() -> {
            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);
        });
        A.start();
        //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
	   // Thread.sleep(1000);
        synchronized (obj) {
            obj.notify();
        }
    }

多次执行后,我们会发现:有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。

原因就在于主线程调用完notify后,线程A才进入wait方法,导致线程A一直阻塞住。由于线程A不是后台线程,所以整个程序无法退出。

那如果换做LockSupport呢?LockSupport就支持主线程先调用unpark后,线程A再调用park而不被阻塞吗?是的,没错。代码如下

public static void main(String[] args) throws Exception {
        Thread A = new Thread(() -> {
            int sum = 0;
            for (int i = 0; i < 10; i++) {
                sum += i;
            }
            LockSupport.park();
            System.out.println(sum);
        });
        A.start();
        //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
	    // Thread.sleep(1000);
        LockSupport.unpark(A);
    }

不管你执行多少次,这段代码都能正常打印结果并退出。这就是LockSupport最大的灵活所在。

小结一下,LockSupport比Object的wait/notify有两大优势:

  1. LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
  2. unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
  3. notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。

三、原理

那么原理是什么呢?

3.1、park方法

blocker是用来记录线程被阻塞时被谁阻塞的。用于线程监控和分析工具来定位原因的。setBlocker(t, blocker)方法的作用是记录t线程是被broker阻塞的。因此我们只关注最核心的方法,也就是UNSAFE.park(false, 0L)。

UNSAFE是一个非常强大的类,他的的操作是基于底层的,也就是可以直接操作内存,因此我们从JVM的角度来分析一下:

每个java线程都有一个Parker实例:

我们换一种角度来理解一下park和unpark,可以想一下,unpark其实就相当于一个许可,告诉特定线程你可以停车,特定线程想要park停车的时候一看到有许可,就可以立马停车。

现在有了这个概念,我们体会一下上面JVM层面park的方法,这里面counter字段,就是用来记录所谓的“许可”的。

当调用park时,先尝试直接能否直接拿到“许可”,即_counter>0时,如果成功,则把_counter设置为0,并返回。

如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,unlock mutex并返回:

否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0,unlock mutex并返回:

总结来说,画个图来表示一下:

3.2、unpark方法

还是先来看一下JDK源码:

上面注释的意思是给线程生产许可证。

当unpark时,则简单多了,直接设置_counter为1,再unlock mutext返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程:

ok,现在我们已经对源码进行了分析,整个过程其实就是生产许可和消费许可的过程。而且这个生产过程可以反过来。也就是先生产再消费。

3.3、上面案例回顾分析

public static void main(String[] args) throws Exception {
        Thread A = new Thread(() -> {
            int sum = 0;
            for (int i = 0; i < 10; i++) {
                sum += i;
            }
            LockSupport.park();
            System.out.println(sum);
        });
        A.start();
        //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
	    // Thread.sleep(1000);
        LockSupport.unpark(A);
    }

这里是不能够确定哪个线程是先执行,哪个后执行的。

1、如果park先执行,发现counter=0,那么等待;在进行unpark的时候,发现=0,先去阻塞的线程执行;

2、如果unpark先执行,那么直接赋值counter=1,而park在执行的时候,发现counter=1之后,就立马执行。

四、LockSupport使用

4.1、park住的线程没有被中断,只会阻塞

public class ThreadWattingTestOne {
    public static void main(String[] args) {
        final Thread thread1 = new Thread(() -> {
            LockSupport.unpark(Thread.currentThread());
            System.out.println("hello,world");
            LockSupport.park();
            System.out.println("hello,park1");
            LockSupport.park();
            System.out.println("hello,park2");
        });
        thread1.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

查看控制台输出:

hello,world
hello,park1

先执行unpark,直接赋值counter=1,然后第一次park执行时,发现counter=1之后,执行之后并将counter设置为0;但是第二次的时候发现counter=0之后,阻塞。所以输出不出来park2

4.2、park住的线程被正常唤醒

public class LockSupportTestTwo {
    public static void main(String[] args) {
        final Thread thread1 = new Thread(() -> {
            for (;;){
                System.out.println("hello,world");
                LockSupport.park();
                System.out.println("hello,park");
            }
        });
        thread1.start();
        LockSupport.unpark(thread1);
    }

}

控制台打印:

hello,world
hello,park
hello,world

1、如果park先执行,那么第一次就会被阻塞住;而在unpark执行之后,会唤醒阻塞住的线程;第二次循环执行的时候,发现还是为0,那么阻塞;

2、如果unpark先执行,那么counter=1,park消费完成之后,counter=0,然后又会阻塞。

二者执行效果上是一样的。

4.3、park线程被中断唤醒

public class LockSupportTestFour {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            System.out.println(String.format("当前线程的名称是:%s", Thread.currentThread().getName()));
            LockSupport.park();
            System.out.println("hello,locksupport");
            LockSupport.park();
            System.out.println("hello,locksupport");
        });
        thread1.start();
        Thread.sleep(3000);
        thread1.interrupt();
    }
}

控制台输出:

当前线程的名称是:Thread-0
hello,locksupport
hello,locksupport

也就是说interrupt会中断掉park方法,让其停止阻塞。

再来一下循环的例子会更加直观明显:

public class LockSupportTestThree {
    public static void main(String[] args) {
        final Thread thread1 = new Thread(() -> {
            for (;;){
                System.out.println("hello,world");
                LockSupport.park();
                System.out.println("hello,park");
            }
        });
        thread1.start();
        thread1.interrupt();
    }
}

控制台循环执行。线程中断打断了让其进入到休眠,所以线程一直处于活跃状态。也就是说线程中断让其许可一直处于可用状态。

因为从Thread中的State状态中的描述可看到,LockSupport.park()会让线程进入到WAITING状态,而线程中断会打破线程休眠。

4.4、park住的线程和Thread.interupted()结合

/**
 * @Description 测试LockSupport.park();,只会park住中断标记为false的,不会打断中断标记为true的
 * @Author liguang
 * @Date 2022/06/20/17:51
 */
public class ParkInteruptTestOne {
    private static Logger logger = LoggerFactory.getLogger(ParkInteruptTestOne.class);


    public static void main(String[] args) throws InterruptedException {
        testParkTwo();
    }

    /**
     * park的线程因为中断而唤醒之后,将无法再park住
     * @throws InterruptedException
     */
    private static void testParkTwo() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            // park住的线程是因为中断唤醒的,那么将无法再次park住
            logger.info("当前线程正在执行........");
            logger.info("当前线程是因为中断而被唤醒的");
            LockSupport.park();
            // 但是重置标记
            Thread.interrupted();
            logger.info("当前线程中断标记是:{}",Thread.currentThread().isInterrupted());
            logger.info("因为当前线程标记为false,所以会再次阻塞住,不会再次来打印下面的代码");
            LockSupport.park();
            logger.info("是否会再次来打印下面的代码?");
        }, "threadA");

        threadA.start();
        TimeUnit.SECONDS.sleep(2);
        // 发送中断信号之后,再也无法进行park住了
        threadA.interrupt();
    }
}

五、总结

调用LockSupport.park()/LockSupport.park(this)代码只是会在线程标记状态为TRUE的时候,不会阻塞住当前线程;

侧重点:在于不满足某个条件的时候让当前线程阻塞住,当满足了条件之后,再让当前线程运行起来。

posted @ 2022-03-10 00:35  雩娄的木子  阅读(147)  评论(0编辑  收藏  举报