小伙伴们,小叶秋最近看到一个面试题目,感觉挺有意思的,大意如下:

ok,大家看到这个题,可以先理解下,这里启动了两个线程,a 和 b,但是虽然说 a 在 b 之前 start,不一定就可以保证线程 a 的逻辑,可以先于线程 b 执行。

所以,这里的意思是,线程 a 和 b,执行顺序互不干扰,我们不应该假定其中一个线程可以先于另外一个执行。

另外,既然是面试题,那常规做法自然是不用上了,比如让 b 先 sleep 几秒钟之类的,如果真这么答,那可能面试就结束了吧。

好,我们下面开始分析解法。

可见性保证

程序里定义了一个全局变量,var = 1。

线程a会修改这个变量为2,线程b则在变量为2时,执行自己的业务逻辑。

那么,这里首先,我们要做的是,先讲var使用volatile修饰,保证多线程操作时的可见性。

public static volatile int var = 1;

解法分析

经过前面的可见性保证的分析,我们知道,要想达到目的,其实就是要保证:

a中的对var+1的操作,需要先于b执行。

但是,现在的问题是,两个线程同时启动,不知道谁先谁后,怎么保证 a 先执行,b 后执行呢?

让线程 b  先不执行,大概有两种思路:一种是阻塞该线程,一种是不阻塞该线程。阻塞的话,我们可以想想,怎么阻塞一个线程。

大概有下面这些方法:

  • synchronized,取不到锁时,阻塞

  • java.util.concurrent.locks.ReentrantLock#lock,取不到锁时,阻塞

  • object.wait,取到synchronized了,但是因为一些条件不满足,执行不下去,调用wait,将释放锁,并进入等待队列,线程暂停运行

  • java.util.concurrent.locks.Condition.await,和object.wait类似,只不过object.wait在jvm层面,使用c++实现,Condition.await在jdk层面使用java语言实现

  • threadA.join(),等待对应的线程threadA执行完成后,本线程再继续运行;threadA没结束,则当前线程阻塞;

  • CountDownLatch#await,在对应的state不为0时,阻塞

  • Semaphore#acquire(),在state为0时(即剩余令牌为0时),阻塞

  • 其他阻塞队列、FutureTask等等

如果不让线程进入阻塞,则一般可以让线程进入一个while循环,循环的退出条件,可以由线程a来修改,线程a修改后,线程b跳出循环。

比如:

volatile boolean stop = false;
while (!stop){...
}

上面也说了这么多了,我们实际上手写一写吧。

错误解法1--基于wait

下面的思路是基于wait、notify。

线程b直接wait,线程a在修改了变量后,进行notify。

public class Global1 {public static volatile int var = 1;public static final Object monitor = new Object();public static void main(String[] args) {Thread a = new Thread(() -> {// 1Global1.var++;// 2synchronized (monitor) {monitor.notify();}});Thread b = new Thread(() -> {// 3synchronized (monitor) {try {monitor.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 4if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});a.start();b.start();}
}

大家觉得这个代码能行吗?

实际是不行的。因为实际的顺序可能是:

线程a--1
线程a--2
线程b--1
线程b--2

在线程 a-2 时,线程 a 去 notify,但是此时线程 b 还没开始 wait,所以此时的 notify 是没有任何效果的:

没人在等,notify 个锤子。

怎么修改,本方案才行得通呢?

那就是,修改线程 a 的代码,不要急着 notify,先等等。

Thread a = new Thread(() -> {Global1.var++;try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}synchronized (monitor) {monitor.notify();}
});

但是这样的话,明显不合适,有作弊嫌疑,也不优雅。

错误解法2--基于condition的signal

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class Global1 {public static volatile int var = 1;public static final ReentrantLock reentrantLock = new ReentrantLock();public static final Condition condition = reentrantLock.newCondition();public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;final ReentrantLock lock = reentrantLock;lock.lock();try {condition.signal();} finally {lock.unlock();}});Thread b = new Thread(() -> {final ReentrantLock lock = reentrantLock;lock.lock();try {condition.await();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});a.start();b.start();}
}

这个方案使用了 Condition 对象来实现 object 的 notify、wait 效果。当然,这个也有同样的问题。

正确解法1--基于错误解法2进行改进

我们看看,前面问题的根源在于,我们线程 a,在去通知线程 b 的时候,有可能线程 b 还没开始 wait,所以此时通知失效。

那么,我们是不是可以先等等,等线程 b 开始 wait 了,再去通知呢?

Thread a = new Thread(() -> {Global1.var++;final ReentrantLock lock = reentrantLock;lock.lock();try {// 1while (!reentrantLock.hasWaiters(condition)) {Thread.yield();}condition.signal();} finally {lock.unlock();}
});

1 处代码,就是这个思想,在 signal 之前,判断当前 condition 上是否有 waiter 线程,如果没有,就死循环;如果有,才去执行 signal。

这个方法实测是可行的。

正确解法2

对正确解法 1,换一个 api,就变成了正确解法 2.

Thread a = new Thread(() -> {Global1.var++;final ReentrantLock lock = reentrantLock;lock.lock();try {// 1while (reentrantLock.getWaitQueueLength(condition) == 0) {Thread.yield();}condition.signal();} finally {lock.unlock();}
});

1 这里,获取 condition 上等待队列的长度,如果为 0,说明没有等待者,则死循环。

正确解法3--基于Semaphore

刚开始,我们初始化一个信号量,state 为 0。

线程 b 去获取信号量的时候,就会阻塞。

然后我们线程 a 再去释放一个信号量,此时线程 b 就可以继续执行。

public class Global1 {public static volatile int var = 1;public static final Semaphore semaphore = new Semaphore(0);public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;semaphore.release();});a.setName("thread a");Thread b = new Thread(() -> {try {semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});b.setName("thread b");a.start();b.start();}
}

正确解法4--基于CountDownLatch

public class Global1 {public static volatile int var = 1;public static final CountDownLatch countDownLatch = new CountDownLatch(1);public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;countDownLatch.countDown();});a.setName("thread a");Thread b = new Thread(() -> {try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});b.setName("thread b");a.start();b.start();}
}

正确解法5--基于BlockingQueue#

这里使用了 ArrayBlockingQueue,其他的阻塞队列也是可以的。

public class Global1 {public static volatile int var = 1;public static final ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<Object>(1);public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;arrayBlockingQueue.offer(new Object());});a.setName("thread a");Thread b = new Thread(() -> {try {arrayBlockingQueue.take();} catch (InterruptedException e) {e.printStackTrace();}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});b.setName("thread b");a.start();b.start();}
}

正确解法6--基于FutureTask

我们也可以让线程 b 等待一个 task 的执行结果。

而线程 a 在执行完修改 var 为 2 后,执行该任务,任务执行完成后,线程 b 就会被通知继续执行。

public class Global1 {public static volatile int var = 1;public static final FutureTask futureTask = new FutureTask<Object>(new Callable<Object>() {@Overridepublic Object call() throws Exception {System.out.println("callable task ");return null;}});public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;futureTask.run();});a.setName("thread a");Thread b = new Thread(() -> {try {futureTask.get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});b.setName("thread b");a.start();b.start();}
}

正确解法7--基于join

这个可能是最简洁直观的解法:

public class Global1 {public static volatile int var = 1;public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;});a.setName("thread a");Thread b = new Thread(() -> {try {a.join();} catch (InterruptedException e) {e.printStackTrace();}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});b.setName("thread b");a.start();b.start();}
}

正确解法8--基于CompletableFuture

这个和第 6 种类似。都是基于 future。

public class Global1 {public static volatile int var = 1;public static final CompletableFuture<Object> completableFuture =new CompletableFuture<Object>();public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;completableFuture.complete(new Object());});a.setName("thread a");Thread b = new Thread(() -> {try {completableFuture.get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});b.setName("thread b");a.start();b.start();}
}

非阻塞--正确解法9--忙等待

这种代码量也少,只要线程 b 在变量为 1 时,死循环就行了。

public class Global1 {public static volatile int var = 1;public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;});a.setName("thread a");Thread b = new Thread(() -> {while (var == 1) {Thread.yield();}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});b.setName("thread b");a.start();b.start();}
}

非阻塞--正确解法10--忙等待

忙等待的方案很多,反正就是某个条件不满足时,不阻塞自己,阻塞了会释放 cpu,我们就是不希望释放 cpu 的。

比如像下面这样也可以:

public class Global1 {public static volatile int var = 1;public static final AtomicInteger atomicInteger =new AtomicInteger(1);public static void main(String[] args) {Thread a = new Thread(() -> {Global1.var++;atomicInteger.set(2);});a.setName("thread a");Thread b = new Thread(() -> {while (true) {boolean success = atomicInteger.compareAndSet(2, 1);if (success) {break;} else {Thread.yield();}}if (Global1.var == 2) {//do something;System.out.println(Thread.currentThread().getName() + " good job");}});b.setName("thread b");a.start();b.start();}
}

暂时想了这么些,方案还是比较多的,大家可以开动脑筋,头脑风暴吧。

看看你还有什么骚操作,可以在评论区留言。


以上就是本期分享了。

一道很有意思的面试题目,快加入到这场头脑风暴游戏里吧~相关推荐

  1. hdu1426一道很有意思的题目:数独

    Problem Description 自从2006年3月10日至11日的首届数独世界锦标赛以后,数独这项游戏越来越受到人们的喜爱和重视. 据说,在2008北京奥运会上,会将数独列为一个单独的项目进行 ...

  2. 一道很有意思的面试题,考推理和逻辑能力

    问题: 一个屋子里面有五十个人,每个人领着一条狗,而这些狗中有一部分病狗(不少于一条).假定有如下条件: 一,狗的病不会传染,也不会不治而愈.也就是说病狗的数量一直不会改变: 二,狗的主人不能看见自己 ...

  3. 一道微软公司的面试题目的算法实现

    题目:        已知两个数字为1~30之间的数字,甲知道两数之和,乙知道两数之积,甲问乙:"你知道是  哪两个数吗?"乙说:"不知道".乙问甲:" ...

  4. 面试题目:2个鸡蛋100层楼问题

    一道非常经典的面试题目,给你两个鸡蛋,在一幢100层的大楼里面,至少扔几次可以测出让鸡蛋破碎的临界高度? 最朴素的做法是从100层一直扔到1层,不过这样明显不是最优的.这题目其实可以转化为dp求解,假 ...

  5. 一道面试题目引发的思考

    起因 多列布局是前端一个经典的反复被提及的面试题目,最典型的即两列,左列定宽菜单栏,右列变宽为内容区域. 通常得到的答案无外乎左列浮动定宽,然后右列或浮动,或设置外边距,或绝对定位等等.偶尔会有面试者 ...

  6. 请问面试官?一次很有意思的调查活动

    一天在群里看几个潜在的面试者在那里吵得不可开交,当然,我也加入了争吵. 具体的内容也记不清楚了,大致也就是 面试官必须会装逼 面试官更喜欢上过程序培训班的人 作品很重要 我不想再次讨论这些观点,当我们 ...

  7. 掌握赚钱最快的方法 其实赚钱很有意思 你可以什么都不会

    其实现在互联网赚钱真的很有意思 真正开始创业熬过初期你会发现赚钱很有意思 我上班的的时候,认为自己要去创业当老板要花很多钱,要融很多资金才能赚钱.当时认识了一个小伙子,也是喜欢创业,他就每天想办法从别 ...

  8. 免费刷题!初级软件测试面试题目和答案这个小程序很全

    有没有软件测试面试题库小程序?相信这是很多准备找工作的新手测试人都想要知道的吧! 今天,我就根据大家的需求,为大家整理了一些有关初级软件测试的面试题目以及一个可以免费刷题的题库,希望能帮助你们早日拿下 ...

  9. 基础知识很扎实 - 但是面试就是做不出来, 怎么办? (长, 慎入)

    http://www.newsmth.net/bbstcon.php?board=Microsoft&gid=48785 有人提问: 看了很多的面试题之类的,感觉虽然自己数据结构与算法基础知识 ...

最新文章

  1. Lesson 016 —— python 元组
  2. 非极大值抑制(Non-maximum suppression)在物体检测领域的应用
  3. MySQL存储引擎之InnoDB
  4. java桥接和装饰_设计模式:桥接模式和装饰模式
  5. 查询数据表结构并查出结构的结构信息
  6. pptx字体类的相关方法
  7. 全美首个AI本科专业今秋开课,CMU每年最多招35名新生
  8. ROS防止外网DDOS的最有效方法
  9. python源码中的学习笔记_第3章_range函数与循环结构
  10. CMMI认证适用的行业范围
  11. 王换招计算机组成与设计pdf,计算机组成原理答案(唐朔飞).pdf
  12. 2022华为软件精英挑战赛——梯度方法
  13. MySQL数据库常用命令大全
  14. steam错误代码100怎么办
  15. 什么是信用违约互换(信用违约掉期) - 债券市场中最常见的信用衍生品
  16. Python3批量修改图片像素
  17. 南邮 OJ 1128 An Industrial Spy
  18. dp:股票系列I II III IV 含冷冻期 含手续费
  19. GDAL添加ECW格式支持
  20. vim资源很全的一个网站

热门文章

  1. 海康经纬度 转换 GPS坐标
  2. 配置maven使用本地仓库
  3. Mac、win VLC播放器 快进的三个快捷键
  4. mysql导出结构及数据结构,mysql导出数据结构+导出数据
  5. android多边形拼图源码,【BMPinTu】一款拼图App的demo。多边形凹凸拼图
  6. Android 之 TranslateAnimation类:位移动画类(转载)
  7. 在UE商城购买了并安装的插件迁移方式
  8. 小学学计算机学flash动画,flash动画教学计划
  9. 【DOE】--方差、自由度、回归分析
  10. win10+ubuntu双系统安装踩坑