疫情居家隔离期间,在网上看了几个技术教学视频,意在查漏补缺,虽然网上这些视频的水平鱼龙混杂,但也有讲得相当不错的,这是昨晚看到的马老师讲的一道面试题,记录一下:

如上图,有2个同时运行的线程,一个输出ABCDE,一个输出12345,要求交替输出,即:最终输出A1B2C3D4E5,而且要求thread-1先执行。

主要考点:二个线程如何通信?通俗点讲,1个线程干到一半,怎么让另1个线程知道我在等他?

方法1:利用LockSupport

import java.util.concurrent.locks.LockSupport;

public class Test01 {

    //这里一定要初始化成null,否则在线程内部无法引用,会提示未初始化
static Thread t1 = null, t2 = null; public static void main(String[] args) {
char[] cA = "ABCDEFG".toCharArray();
char[] cB = "1234567".toCharArray(); t1 = new Thread(() -> {
for (char c : cA) {
System.out.print(c);
//解锁T2线程(注:unpark线程t2后,t2即使再调用LockSupport.park也锁不住)
LockSupport.unpark(t2);
//再把自己T1卡住(直到T2为它解锁)
LockSupport.park(t1);
}
}, "t1"); t2 = new Thread(() -> {
for (char c : cB) {
//先把T2自己卡住(直到T1为它解锁)
LockSupport.park(t2);
System.out.print(c);
//再把T1解锁
LockSupport.unpark(t1);
} }, "t2"); t1.start();
t2.start();
}
}

优点:逻辑清晰,代码简洁,可认为是最优解。 

方法2:模拟自旋锁的做法,利用标志位不断尝试

import java.util.concurrent.atomic.AtomicInteger;

public class Test02a {

    public static void main(String[] args) {
char[] cA = "ABCDEFG".toCharArray();
char[] cB = "1234567".toCharArray(); //AtomicInteger保证线程安全,值1表示t1可继续 ,值2表示t2可继续
AtomicInteger flag = new AtomicInteger(1); new Thread(() -> {
for (char c : cA) {
//不断"自旋"重试
while (flag.get() != 1) {
}
System.out.print(c);
//标志位指向t2
flag.set(2);
}
}, "t1").start(); new Thread(() -> {
for (char c : cB) {
while (flag.get() != 2) {
}
System.out.print(c);
//标志位指向t1
flag.set(1);
}
}, "t2").start();
}
}

优点:思路纯朴无华,容易理解。缺点:自旋尝试比较占用cpu,如果有更多线程参与竞争,cpu可能会较高。

这个方法还有一个变体,不借助并发包下的AtomicInteger,可以改用static valatile + enum变量保证线程安全:

public class Test02b {

    enum ReadyToGo {
T1, T2
} static volatile ReadyToGo r = ReadyToGo.T1; public static void main(String[] args) {
char[] cA = "ABCDEFG".toCharArray();
char[] cB = "1234567".toCharArray(); new Thread(() -> {
for (char c : cA) {
while (!r.equals(ReadyToGo.T1)) {
}
System.out.print(c);
r = ReadyToGo.T2;
}
}).start(); new Thread(() -> {
for (char c : cB) {
while (!r.equals(ReadyToGo.T2)) {
}
System.out.print(c);
r = ReadyToGo.T1;
}
}).start();
}
}

  

方法3:利用ReentrantLock可重入锁及Condition条件

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class Test03 { public static void main(String[] args) {
char[] cA = "ABCDEFG".toCharArray();
char[] cB = "1234567".toCharArray(); Lock lock = new ReentrantLock();
Condition cond1 = lock.newCondition();
Condition cond2 = lock.newCondition(); CountDownLatch latch = new CountDownLatch(1); new Thread(() -> {
//保证t1先执行
latch.countDown(); lock.lock();
try {
for (char c : cA) {
System.out.print(c);
//"唤醒"满足条件2的线程t2
cond2.signal();
//卡住满足条件1的线程t1
cond1.await();
}
//输出最后1个字符后,把t2也唤醒(否则t2一直await永远退出不了)
cond2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start(); new Thread(() -> {
try {
//先把t2卡住,保证t1先输出
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
} lock.lock();
try {
for (char c : cB) {
System.out.print(c);
//"唤醒"满足条件1的线程t1
cond1.signal();
//卡住满足条件2的线程t2
cond2.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start(); }
}

方法4:利用阻塞队列BlockingQueue

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; public class Test04 { public static void main(String[] args) {
char[] cA = "ABCDEFG".toCharArray();
char[] cB = "1234567".toCharArray(); BlockingQueue<Boolean> q1 = new LinkedBlockingQueue<>(1);
BlockingQueue<Boolean> q2 = new LinkedBlockingQueue<>(1); new Thread(() -> {
for (char c : cA) {
System.out.print(c);
try {
//放行t2
q2.put(true);
//阻塞t1
q1.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start(); new Thread(() -> {
for (char c : cB) {
try {
//先阻塞t2
q2.take();
System.out.print(c);
//再放行t1
q1.put(true);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2").start(); }
}

点评:巧妙利用了阻塞队列的特性,思路新颖

方法5:利用IO管道输入/输出流

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream; public class Test05 { public static void main(String[] args) throws IOException {
char[] cA = "ABCDEFG".toCharArray();
char[] cB = "1234567".toCharArray(); PipedInputStream input1 = new PipedInputStream();
PipedInputStream input2 = new PipedInputStream();
PipedOutputStream output1 = new PipedOutputStream();
PipedOutputStream output2 = new PipedOutputStream(); input1.connect(output2);
input2.connect(output1); //相当于令牌(在2个管道中流转)
String flag = "1"; new Thread(() -> { byte[] buffer = new byte[1];
for (char c : cA) {
try {
System.out.print(c);
//将令牌通过output1->input2给到t2
output1.write(flag.getBytes());
//从output2->input1读取令牌(没有数据时,该方法会block,即:相当于卡住自己)
input1.read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}, "t1").start(); new Thread(() -> {
byte[] buffer = new byte[1];
for (char c : cB) {
try {
//读取t1通过output1->input2传过来的令牌(无数据时,会block住自己)
input2.read(buffer);
System.out.print(c);
//将令牌通过output2->input1给到t1
output2.write(flag.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}, "t2").start();
}
}

效率极低,纯属炫技。主要利用了管道流read操作,无数据时,会block的特性,类似阻塞队列。

方法6:利用synchronized/notify/wait

import java.util.concurrent.CountDownLatch;

public class Test06 {

    public static void main(String[] args) {
char[] cA = "ABCDEFG".toCharArray();
char[] cB = "1234567".toCharArray(); Object lockObj = new Object(); CountDownLatch latch = new CountDownLatch(1); new Thread(() -> {
//保证t1先输出
latch.countDown(); synchronized (lockObj) {
for (char c : cA) {
System.out.print(c);
//通知等待锁释放的其它线程,即:交出锁,然后通知t2去抢
lockObj.notify();
try {
//自己进入等待锁的队列(即:卡住自己)
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//输出完后,把自己唤醒,以便线程能结束
lockObj.notify();
} }, "t1").start(); new Thread(() -> {
try {
//先卡住t2,让t1先输入
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
} synchronized (lockObj) {
for (char c : cB) {
System.out.print(c);
//通知等待锁释放的其它线程,即:交出锁,然后通知t1去抢
lockObj.notify();
try {
//自己进入等待锁的队列(即:卡住自己)
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// lockObj.notify();
}
}, "t2").start();
}
}

这是正统解法,原理是先让t1抢到锁(这时t2在等待锁),然后输出1个字符串后,通知t2抢锁,然后t1开始等锁,t2也是类似原理。

假期充电: 一道并发java面试题的N种解法的更多相关文章

  1. 一道百度的java面试题的几种解法

    考试结束,班级平均分只拿到了年级第二,班主任于是问道:大家都知道世界第一高峰珠穆朗玛峰,有人知道世界第二高峰是什么吗?正当班主任要继续发话,只听到角落默默想起来一个声音:”乔戈里峰” 前言 文章出自: ...

  2. 一道百度java面试题的多种解法

    下面是我在2018年10月11日二面百度的时候的一个问题: java程序,主进程需要等待多个子进程结束之后再执行后续的代码,有哪些方案可以实现? 这个需求其实我们在工作中经常会用到,比如用户下单一个产 ...

  3. Java并发编程面试题 Top 50 整理版

    本文在 Java线程面试题 Top 50的基础上,对部分答案进行进行了整理和补充,问题答案主要来自<Java编程思想(第四版)>,<Java并发编程实战>和一些优秀的博客,当然 ...

  4. Java并发--Java线程面试题 Top 50

    原文链接:http://www.importnew.com/12773.html 不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题.Java语言一个重要的特点就是内置了对并发的支持,让Ja ...

  5. 2019年Java并发精选面试题,哪些你还不会?(含答案和思维导图)

    Java 并发编程 1.并发编程三要素? 2.实现可见性的方法有哪些? 3.多线程的价值? 4.创建线程的有哪些方式? 5.创建线程的三种方式的对比? 6.线程的状态流转图 7.Java 线程具有五中 ...

  6. 8月份21道最新Java面试题剖析(数据库+JVM+微服务+高并发)

    前言 纵观几年来的Java面试题,你会发现每家都差不多.你仔细观察就会发现,HashMap的出现几率未免也太高了吧!连考察的知识点都一样,什么hash碰撞啊,并发问题啊!再比如JVM,无外乎考内存结构 ...

  7. Java并发多线程面试题 Top 50

    不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题.Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎.大多数待遇丰厚的Java开发职位都要求开发者精通多线程 ...

  8. 一道简单的面试题,难倒各大 Java 高手!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 最近栈长在我们的<Java技术栈知识星球>上分享的一道 Java 实战面试题,很有意思,现在拿出来和大家分享下, ...

  9. Java【并发】面试题

    精尽 Java[并发]面试题 以下面试题,基于网络整理,和自己编辑.具体参考的文章,会在文末给出所有的链接. 如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 Java[并发]面试题的大保健. ...

  10. 一道非常棘手的 Java 面试题:i++ 是线程安全的吗

    转载自  一道非常棘手的 Java 面试题:i++ 是线程安全的吗 i++ 是线程安全的吗? 相信很多中高级的 Java 面试者都遇到过这个问题,很多对这个不是很清楚的肯定是一脸蒙逼.内心肯定还在质疑 ...

随机推荐

  1. Axure RP仿抖音短视频APP交互原型图模板

    Axure RP仿抖音短视频APP高保真交互原型模板,原型图设计灵感来自于抖音段视频APP,在预览里你可以看到抖音的影子.本素材包含登录.首页推荐.同城.直播间.消息.朋友.发布.我的.搜索等主要模块 ...

  2. vue3 基础-父子组件间如何通过事件通信

    前几篇讨论的父子组件间如何进行传数据的话题. 即父组件在调用子组件的时候, 通过自定义属性 (v-bind) 的方式传递数据, 同时子组件通过 props 属性进行接收. 子组件可以对数据进行各种校验 ...

  3. Unity ML-Agents实战指南:构建多技能游戏AI训练系统

    引言:游戏AI训练的技术演进 在<赛博朋克2077>的动态NPC系统到<Dota 2>OpenAI Five的突破性表现中,强化学习正在重塑游戏AI边界.本文将通过Unity ...

  4. C# Environment.CurrentDirectory和AppDomain.CurrentDomain.BaseDirectory的区别

    Environment.CurrentDirectory 和 AppDomain.CurrentDomain.BaseDirectory 都是C#中用于获取当前应用程序的目录路径的方法,但是它们的用途 ...

  5. MySQL建表时,五种日期和时间类型选择

      MySQl中有多种表示日期和时间的数据类型.其中YEAR表示年份,DATE表示日期,TIME表示时间,DATETIME和TIMESTAMP表示日期和实践.它们的对比如下: 日期时间类型 占用空间 ...

  6. 20244104 实验二《Python程序设计》实验报告

    课程:<Python程序设计> 班级: 2441 姓名: 陈思淼 学号:20244104 实验教师:王志强 实验日期:2025年4月5日 必修/选修: 公选课 1.实验内容 设计并完成一个 ...

  7. Flume+Kafka获取MySQL数据

    摘要 MySQL被广泛用于海量业务的存储数据库,在大数据时代,我们亟需对其中的海量数据进行分析,但在MySQL之上进行大数据分析显然是不现实的,这会影响业务系统的运行稳定.如果我们要实时地分析这些数据 ...

  8. C/C++中的volatile

    C/C++中的volatile 约定 Volatile 这个话题,涉及到计算机科学多个领域多个层次的诸多细节.仅靠一篇博客,很难穷尽这些细节.因此,若不对讨论范围做一些约定,很容易就有诸多漏洞.到时误 ...

  9. 杂七杂八系列----C#代码如何影响CPU缓存速度?

    CPU与RAM的隔阂 CPU与RAM是两个独立的硬件,并非集成在一起.所以他们两个之间一定会存在一个连接的桥梁,这个桥梁的名字叫做内存总线. 内存总线由三部分组成: 地址总线(Address Bus) ...

  10. OceanBase 中的身外身法 —— Auto DOP(自适应并行)使用技巧分享

    首先为大家推荐这个 OceanBase 开源负责人老纪的公众号 "老纪的技术唠嗑局",会持续更新和 OceanBase 相关的各种技术内容.欢迎感兴趣的朋友们关注! Part 1 ...