目录
一、前言
之前在看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、在对线程进行唤醒时,请确保线程是可用的(这是本文章主旨)。