由一道面试题引发对LockSupport的使用思考,看看你中招没有

目录

一、前言

二、题目 

三、分析

四、关于LockSupport小计

五、解题流程

六、最终代码

七、总结


一、前言

 之前在看why博客时,看到了一篇有关面试的文章《遇到个面试题,还有点意思呢。》,心想着什么面试题这么有趣。于是点进去一看,是有关线程的面试题,初看时觉得不难,不就是控制线程执行顺序嘛,有什么难的,但当自己真正做的时候,就发现掉坑里了。

解题的方式有很多种,这里我只试验LockSupport的解法,其余的大家可以参考why的博客,以下附上地址:

遇到个面试题,还有点意思呢。

二、题目 

先把题目附上,再简单给大家讲讲要求。


private static int var = 1;

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

    Thread a = new Thread(() -> {
        TestController.var++;
    });

    Thread b = new Thread(() -> {
        if(TestController.var == 2){
            // dosomething
        }
    });


    a.start();
    b.start();
}

要求:

在保证

a.start();
b.start();

以上两行不动的情况下,如何任意修改程序,保证线程b中的do something一定可以执到呢?

三、分析

关于题目要求,想必大家已经很清楚了。因为是多线程,我们无法保证哪个线程先执行,此处有可能线程a先执行,线程b后执行;也有可能是线程b先执行,线程b后执行。但根据题目的要求,我们是需要保证线程b是后执行具体逻辑的。

那么,我们的解体思路可以是这样的:

1、线程a先执行,线程b后执行时

大家是不是以为这种情况不用考虑其中有需要解决的问题吗?no no no,当线程a先执行时,还没有执行完全,线程b次此时开始执行,而且就执行完了呢?那这是必定执行不到线程b的do something。这种情况我们需要让线程b去阻塞,等线程a执行完之后,再去唤醒线程b去执行。还有一种情况是线程a确确实实的执行完之后再去执行线程b。

2、线程b先执行,线程a后执行时

这种很简单,线程b先执行,那么就阻塞它,等线程a执行完之后,再去唤醒线程b。

所以,根据以上分析,我得出一个结论,不管线程b是先执行,还是后执行,我们都要阻塞它。等到线程a执行完之后,再去唤醒线程b。

那么此时就会有小可爱提问:那么,如果线程a先执行完了然后唤醒线程b(此时线程b是还没执行到的),然后线程b才开始执行,遇到阻塞,此时线程b不就永远阻塞下去了吗?

是的,确实会出现这种情况,但是如果我们使用LockSupport的话,就能避免这一种情况。

四、关于LockSupport小计

 关于LockSupport,这里我只简单介绍一下,因为我也没有深入了解过LockSupport,只是基于它的特性进行简单的使用,更为精深的知识,烦请小伙伴们去找相关知识学习了解。

以下是关于LockSupport的几个简单小知识:

1、使用LockSupport无须包裹在synchronized代码块,也无须在Lock包裹之中。

2、使用LockSupport进行阻塞时,如果该线程持有锁,阻塞并不会释放锁资源。

3、LockSupport.park()用于阻塞当前线程,LockSupport.unpark(Thread thread)用于唤醒某一个线程。

4、在阻塞线程之前,多个连续出现的LockSupport.unpark()可以抵消一个LockSupport.park()

LockSupport.unpark(Thread.currentThread());
LockSupport.unpark(Thread.currentThread());
LockSupport.unpark(Thread.currentThread());
LockSupport.park();

// 线程可以执行到该语句
System.out.println("这里");

控制台输出“”这里“。

5、在阻塞线程之前,多个连续出现的LockSupport.unpark()并不能抵消多个连续出现的LockSupport.park()

LockSupport.unpark(Thread.currentThread());
LockSupport.unpark(Thread.currentThread());
LockSupport.unpark(Thread.currentThread());
LockSupport.park();
// 线程在此处进行阻塞
LockSupport.park();

// 线程不可以执行到该语句
System.out.println("这里");

线程进行了阻塞,并不能看到有任何输出。

6、在使用LockSupport.unpark()之前,请确保线程已经创建成功了(这就是文章所描述的坑了),否则,在线程没有创建完毕就对其进行唤醒操作,唤醒将无效。

五、解题流程

根据上诉的分析之后,以下便是我的第一版代码

public static int var = 1;

    public static void main(String[] args){

        Thread b = new Thread(() -> {
            LockSupport.park();
            if(TestController.var == 2){
                System.out.println("线程b执行");
            }
        });

        Thread a = new Thread(() -> {
            TestController.var++;
            System.out.println("线程a执行");
            LockSupport.unpark(b);
        });
        
        a.start();
        b.start();
    }

以上简单地调整了线程a和线程b的声明顺序,但并不影响结果;

为了方便区分以及看出线程执行,在开启两个线程之间进行休眠,这样才能充分体现出任意线程先执行前后会出现的问题:

1、当线程a先执行,线程b后执行时,将以下代码作为修改:

a.start();
try {
    thread.sleep(2000);
}catch (Exception e){

}
b.start();

输出:

线程a执行

查看控制台,发现线程a能够正常执行,但是线程b阻塞了。

这里有小伙伴发出疑问:奇怪,怎么跟你说的不一样呢?LockSupport.unpark先执行了,但是并不能唤醒线程b啊!是啊,怎么回事呢,我一开始也怀疑是不是我记错了呢?但是没道理啊,我在同一个线程内进行唤醒和阻塞都没有问题的啊。等等,同一个线程里没有问题?那是不是不同线程就不能进行唤醒呢?当然不是啊,如果不同线程不行,那这个功能开发出来有什么用呢,于是我点击了unpark方法进行查看,并且发现了一些猫腻,请看:

/**
     * Makes available the permit for the given thread, if it
     * was not already available.  If the thread was blocked on
     * {@code park} then it will unblock.  Otherwise, its next call
     * to {@code park} is guaranteed not to block. This operation
     * is not guaranteed to have any effect at all if the given
     * thread has not been started.
     *
     * @param thread the thread to unpark, or {@code null}, in which case
     *        this operation has no effect
     */
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

直接看注释第一句,我直接人傻了,该方法需要我提供的线程是可用的?仔细一想,线程可用并不是在new之后就可用,而且在start之后创建出了一个可运行的线程。于是,线程a在唤醒线程b的时候,此时线程b没有创建出来,自然白唤醒了,那我们需要在线程a唤醒线程b的时候,去确认线程b存活的时候再去唤醒线程b,否则等。。。

将线程a唤醒的代码修改为如下:

while(!b.isAlive()){
    // 等待线程b创建成功
}
LockSupport.unpark(b);

再去测试,发现能够正常输出,并且结束线程了。

2、当线程b先执行,线程a后执行时,将以下代码作为修改:

b.start();
try{
    Thread.sleep(2000);
}catch (Exception e){

}
a.start();

输出:

线程a执行
线程b执行

六、最终代码

public static int var = 1;

    public static void main(String[] args){

        Thread b = new Thread(() -> {
            LockSupport.park();
            if(TestController.var == 2){
                System.out.println("线程b执行");
            }
        });

        Thread a = new Thread(() -> {
            TestController.var++;
            System.out.println("线程a执行");
            while(!b.isAlive()){
                // 等待线程b创建成功
            }
            LockSupport.unpark(b);

        });

        a.start();
        b.start();
    }

七、总结

1、在使用LockSupport,最好确保对同一个线程进行阻塞和唤醒时,park和unpark总是成对出现的。因为在异步线程中,如果不能确保线程执行顺序,多个park和unpark会导致线程状态混乱,它可能因为不成对的park和unpark而导致它可能处于阻塞或唤醒。

2、在对线程进行唤醒时,请确保线程是可用的(这是本文章主旨)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值