|
| 1 | +全文 2929 字,剩下的是代码,P6 及以下阅读只需要 5 分钟,高P 请直接关闭 |
| 2 | + |
1 | 3 | # 手撕面试题:多个线程顺序执行问题 |
2 | 4 |
|
3 | | -最近换工作,除了一些常规算法题,还会遇到各种需要手写的题目,所以打算总结出来,给大家个参考。 |
| 5 | +大家在换工作面试中,除了一些常规算法题,还会遇到各种需要手写的题目,所以打算总结出来,给大家个参考。 |
4 | 6 |
|
5 | | -第一篇打算总结下阿里最喜欢问的多个线程顺序打印问题,我遇到的是机试,直接写出运行。同类型的题目,比如 |
| 7 | +第一篇打算总结下阿里最喜欢问的多个线程顺序打印问题,我遇到的是机试,直接写出运行。同类型的题目有很多,比如 |
6 | 8 |
|
7 | | -1. 三个线程分别打印A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串。 |
| 9 | +1. 三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串 |
8 | 10 | 2. 两个线程交替打印 0~100 的奇偶数 |
9 | 11 | 3. 通过 N 个线程顺序循环打印从 0 至 100 |
10 | 12 | 4. 多线程按顺序调用,A->B->C,AA 打印 5 次,BB 打印10 次,CC 打印 15 次,重复 10 次 |
11 | 13 | 5. 用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z |
12 | 14 |
|
13 | | -其实这类题目考察的都是**线程间的通信问题**,基于这类题目,做一个整理,方便日后手撕面试官,说的有点残暴,应该是手撕面试题。 |
14 | | - |
| 15 | +其实这类题目考察的都是**线程间的通信问题**,基于这类题目,做一个整理,方便日后手撕面试官,文明的打工人,手撕面试题。 |
15 | 16 |
|
| 17 | + |
16 | 18 |
|
17 | 19 | ## 使用 Lock |
18 | 20 |
|
19 | | -我们以第一题为例:三个线程分别打印A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串。 |
| 21 | +我们以第一题为例:三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串。 |
20 | 22 |
|
21 | 23 | 思路:使用一个取模的判断逻辑 **C%M ==N**,题为 3 个线程,所以可以按取模结果编号:0、1、2,他们与 3 取模结果仍为本身,则执行打印逻辑。 |
22 | 24 |
|
23 | | -main 方法启动后,3 个线程会抢锁,但是 state 的初始值为 0,所以第一次执行 if 语句的内容只能是 **线程 A**,然后还在 for 循环之内,此时 `state = 1`,只有 **线程 B** 才满足 `1% 3 == 1`,所以第二个执行的是 B,同理只有 **线程 C** 才满足 `2% 3 == 2`,所以第三个执行的是 C,执行完 ABC 之后,才去执行第二次 for 循环,所以要把 i++ 写在 for 循环里边,不能写成 `for (int i = 0; i < times;i++)` 这样。 |
24 | | - |
25 | 25 | ```java |
26 | 26 | public class PrintABCUsingLock { |
27 | 27 |
|
@@ -63,6 +63,8 @@ public class PrintABCUsingLock { |
63 | 63 | } |
64 | 64 | ``` |
65 | 65 |
|
| 66 | +main 方法启动后,3 个线程会抢锁,但是 state 的初始值为 0,所以第一次执行 if 语句的内容只能是 **线程 A**,然后还在 for 循环之内,此时 `state = 1`,只有 **线程 B** 才满足 `1% 3 == 1`,所以第二个执行的是 B,同理只有 **线程 C** 才满足 `2% 3 == 2`,所以第三个执行的是 C,执行完 ABC 之后,才去执行第二次 for 循环,所以要把 i++ 写在 for 循环里边,不能写成 `for (int i = 0; i < times;i++)` 这样。 |
| 67 | + |
66 | 68 |
|
67 | 69 |
|
68 | 70 | ## 使用 wait/notify |
@@ -120,7 +122,7 @@ public class PrintABCUsingWaitNotify { |
120 | 122 |
|
121 | 123 | 使用对象监视器实现,两个线程 A、B 竞争同一把锁,只要其中一个线程获取锁成功,就打印 ++i,并通知另一线程从等待集合中释放,然后自身线程加入等待集合并释放锁即可。 |
122 | 124 |
|
123 | | - |
| 125 | + |
124 | 126 |
|
125 | 127 | ```java |
126 | 128 | public class OddEvenPrinter { |
@@ -209,13 +211,13 @@ public class NumAndLetterPrinter { |
209 | 211 |
|
210 | 212 |
|
211 | 213 |
|
212 | | - |
213 | | - |
214 | 214 | ## 使用 Lock/Condition |
215 | 215 |
|
216 | | -还是以第一题为例,使用 Condition 的思路,其实和 wait/notify 的思路一样。 |
| 216 | +还是以第一题为例,使用 Condition 来实现,其实和 wait/notify 的思路一样。 |
217 | 217 |
|
218 | | -> Condition中的 `await()` 方法相当于 Object 的 `wait()` 方法,Condition 中的 `signal()` 方法相当于Object 的 `notify()` 方法,Condition 中的 `signalAll()` 相当于 Object 的 `notifyAll()` 方法。不同的是,Object 中的 `wait(),notify(),notifyAll()`方法是和`"同步锁"`(synchronized关键字)捆绑使用的;而Condition 是需要与`"互斥锁"/"共享锁"`捆绑使用的。 |
| 218 | +> Condition 中的 `await()` 方法相当于 Object 的 `wait()` 方法,Condition 中的 `signal()` 方法相当于Object 的 `notify()` 方法,Condition 中的 `signalAll()` 相当于 Object 的 `notifyAll()` 方法。 |
| 219 | +> |
| 220 | +> 不同的是,Object 中的 `wait(),notify(),notifyAll()`方法是和`"同步锁"`(synchronized关键字)捆绑使用的;而 Condition 是需要与`"互斥锁"/"共享锁"`捆绑使用的。 |
219 | 221 |
|
220 | 222 | ```java |
221 | 223 | public class PrintABCUsingLockCondition { |
@@ -273,14 +275,18 @@ public class PrintABCUsingLockCondition { |
273 | 275 |
|
274 | 276 |
|
275 | 277 |
|
| 278 | +> 以上几种方式,其实都会存在一个锁的抢夺过程,如果抢锁的的线程数量足够大,就会出现很多线程抢到了锁但不该自己执行,然后就又解锁或 wait() 这种操作,这样其实是有些浪费资源的。 |
| 279 | +
|
| 280 | + |
| 281 | + |
276 | 282 | ## 使用 Semaphore |
277 | 283 |
|
278 | 284 | > 在信号量上我们定义两种操作: 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。 |
279 | 285 | > |
280 | 286 | > 1. acquire(获取) 当一个线程调用 acquire 操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。 |
281 | 287 | > 2. release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。 |
282 | 288 |
|
283 | | -先解决第一题:三个线程循环打印A,B,C |
| 289 | +先看下如何解决第一题:三个线程循环打印 A,B,C |
284 | 290 |
|
285 | 291 | ```java |
286 | 292 | public class PrintABCUsingSemaphore { |
@@ -326,7 +332,7 @@ public class PrintABCUsingSemaphore { |
326 | 332 |
|
327 | 333 |
|
328 | 334 |
|
329 | | -如果题目中是多个线程循环打印的话,一般使用信号量解决是效率较高的方案,上一个线程持有下一个线程的信号量,通过一个信号量数组将全部关联起来,这种方式不会存在浪费资源,锁竞争的情况。 |
| 335 | +如果题目中是多个线程循环打印的话,一般使用信号量解决是效率较高的方案,上一个线程持有下一个线程的信号量,通过一个信号量数组将全部关联起来,这种方式不会存在浪费资源的情况。 |
330 | 336 |
|
331 | 337 | 接着用信号量的方式解决下第三题:通过 N 个线程顺序循环打印从 0 至 100 |
332 | 338 |
|
@@ -376,14 +382,93 @@ public class LoopPrinter { |
376 | 382 |
|
377 | 383 |
|
378 | 384 |
|
379 | | -## 写在最后 |
| 385 | +## 使用 LockSupport |
| 386 | + |
| 387 | +LockSupport 是 JDK 底层的基于 `sun.misc.Unsafe` 来实现的类,用来创建锁和其他同步工具类的基本线程阻塞原语。它的静态方法`unpark()`和`park()`可以分别实现阻塞当前线程和唤醒指定线程的效果,所以用它解决这样的问题会更容易一些。 |
| 388 | + |
| 389 | +(在 AQS 中,就是通过调用 `LockSupport.park( )`和 `LockSupport.unpark()` 来实现线程的阻塞和唤醒的。) |
| 390 | + |
| 391 | +```java |
| 392 | +public class PrintABCUsingLockSupport { |
| 393 | + |
| 394 | + private static Thread threadA, threadB, threadC; |
| 395 | + |
| 396 | + public static void main(String[] args) { |
| 397 | + threadA = new Thread(() -> { |
| 398 | + for (int i = 0; i < 10; i++) { |
| 399 | + // 打印当前线程名称 |
| 400 | + System.out.print(Thread.currentThread().getName()); |
| 401 | + // 唤醒下一个线程 |
| 402 | + LockSupport.unpark(threadB); |
| 403 | + // 当前线程阻塞 |
| 404 | + LockSupport.park(); |
| 405 | + } |
| 406 | + }, "A"); |
| 407 | + threadB = new Thread(() -> { |
| 408 | + for (int i = 0; i < 10; i++) { |
| 409 | + // 先阻塞等待被唤醒 |
| 410 | + LockSupport.park(); |
| 411 | + System.out.print(Thread.currentThread().getName()); |
| 412 | + // 唤醒下一个线程 |
| 413 | + LockSupport.unpark(threadC); |
| 414 | + } |
| 415 | + }, "B"); |
| 416 | + threadC = new Thread(() -> { |
| 417 | + for (int i = 0; i < 10; i++) { |
| 418 | + // 先阻塞等待被唤醒 |
| 419 | + LockSupport.park(); |
| 420 | + System.out.print(Thread.currentThread().getName()); |
| 421 | + // 唤醒下一个线程 |
| 422 | + LockSupport.unpark(threadA); |
| 423 | + } |
| 424 | + }, "C"); |
| 425 | + threadA.start(); |
| 426 | + threadB.start(); |
| 427 | + threadC.start(); |
| 428 | + } |
| 429 | +} |
| 430 | +``` |
| 431 | + |
| 432 | +理解了思路,解决其他问题就容易太多了。 |
| 433 | + |
| 434 | +比如,我们再解决下第五题:用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z |
| 435 | + |
| 436 | +```java |
| 437 | +public class NumAndLetterPrinter { |
| 438 | + |
| 439 | + private static Thread numThread, letterThread; |
| 440 | + |
| 441 | + public static void main(String[] args) { |
| 442 | + letterThread = new Thread(() -> { |
| 443 | + for (int i = 0; i < 26; i++) { |
| 444 | + System.out.print((char) ('A' + i)); |
| 445 | + LockSupport.unpark(numThread); |
| 446 | + LockSupport.park(); |
| 447 | + } |
| 448 | + }, "letterThread"); |
380 | 449 |
|
381 | | -实现的方式还有很多,可以用 LockSupport |
| 450 | + numThread = new Thread(() -> { |
| 451 | + for (int i = 1; i <= 26; i++) { |
| 452 | + System.out.print(i); |
| 453 | + LockSupport.park(); |
| 454 | + LockSupport.unpark(letterThread); |
| 455 | + } |
| 456 | + }, "numThread"); |
| 457 | + numThread.start(); |
| 458 | + letterThread.start(); |
| 459 | + } |
| 460 | +} |
| 461 | +``` |
| 462 | + |
| 463 | + |
| 464 | + |
| 465 | +## 写在最后 |
382 | 466 |
|
| 467 | +好了,以上就是常用的五种实现方案,多练习几次,手撕没问题。 |
383 | 468 |
|
| 469 | +当然,这类问题,解决方式不止是我列出的这些,还会有 join、CountDownLatch、也有放在队列里解决的,思路有很多,面试官想考察的其实只是对多线程的编程功底,其实自己练习的时候,是个很好的巩固理解 JUC 的过程。 |
384 | 470 |
|
385 | | -参考: |
| 471 | +> 以梦为马,越骑越傻。诗和远方,越走越慌。不忘初心是对的,但切记要出发,加油吧,程序员。 |
386 | 472 |
|
387 | | -https://www.jianshu.com/p/40078ed436b4 |
| 473 | +> 在路上的你,可以微信搜「 **JavaKeeper** 」一起前行,无套路领取 500+ 本电子书和 30+ 视频教学和源码,本文 **GitHub** [github.com/JavaKeeper](https://github.com/Jstarfish/JavaKeeper) 已经收录,服务端开发、面试必备技能兵器谱,有你想要的。 |
388 | 474 |
|
389 | | -https://www.throwable.club/2019/05/20/interview-problem-double-thread-print-odd-even-alternately/ |
|
0 commit comments