09-Java并发编程的艺术.pdf

概念

并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)

    线程与进程

    进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多
    个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创
    建、运行到消亡的过程。
    线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程
    中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

    Java线程

    本章内容

  • 创建和运行线程

  • 查看线程
  • 线程 API
  • 线程状态

首先创建工具类:
就是 Thread.sleep()

  1. public class Sleeper {
  2. public static void sleep(int i) {
  3. try {
  4. TimeUnit.SECONDS.sleep(i);
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. public static void sleep(double i) {
  10. try {
  11. TimeUnit.MILLISECONDS.sleep((int) (i * 1000));
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }

读取文件(耗时的IO操作 效果等同于sleep)

  1. @Slf4j(topic = "c.FileReader")
  2. public class FileReader {
  3. public static void read(String filename) {
  4. int idx = filename.lastIndexOf(File.separator);
  5. String shortName = filename.substring(idx + 1);
  6. try (FileInputStream in = new FileInputStream(filename)) {
  7. long start = System.currentTimeMillis();
  8. log.debug("read [{}] start ...", shortName);
  9. byte[] buf = new byte[1024];
  10. int n = -1;
  11. do {
  12. n = in.read(buf);
  13. } while (n != -1);
  14. long end = System.currentTimeMillis();
  15. log.debug("read [{}] end ... cost: {} ms", shortName, end - start);
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }

1、创建和运行线程

1.1 直接使用 Thread

1.2 使用 Runnable 配合 Thread

1.3 FutureTask 配合 Thread

1.4 线程池

2、观察多个线程同时运行

主要是理解

  • 交替执行(就绪状态的线程Runnable 获取CPU执行权)
  • 谁先谁后,不由我们控制(即使加了优先权setPriority

    3、查看进程线程的方法

4、原理之线程运行

5、常见方法

6、start 与 run

直接调run()

  1. public static void main(String[] args) {
  2. Thread t1 = new Thread("t1") {
  3. @Override
  4. public void run() {
  5. log.debug(Thread.currentThread().getName());
  6. FileReader.read(Constants.MP4_FULL_PATH);
  7. }
  8. };
  9. t1.run();
  10. //t1.start();
  11. log.debug("do other things ...");
  12. }

输出

  1. 19:39:14 [main] c.TestStart - main
  2. 19:39:14 [main] c.FileReader - read [1.mp4] start ...
  3. 19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
  4. 19:39:18 [main] c.TestStart - do other things ...

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的
调用 start
将上述代码的 t1.run() 改为 t1.start();

输出

  1. 19:41:30 [main] c.TestStart - do other things ...
  2. 19:41:30 [t1] c.TestStart - t1
  3. 19:41:30 [t1] c.FileReader - read [1.mp4] start ...
  4. 19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms

程序在 t1 线程运行, FileReader.read() 方法调用是异步的
小结

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

    7、sleep、yield、priority优先级

    sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)(这时CPU也停下来了);
    2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException;
    3. 睡眠结束后的线程未必会立刻得到执行;
    4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性;

    yield

    yield是谦让的意思,会打断synchronize锁 让出CPU的执行权
    1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
    2. 具体的实现依赖于操作系统的任务调度器

    线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

举例:

8、join方法

  1. public static void testJoin() throws InterruptedException {
  2. log.debug("开始");
  3. Thread t1 = new Thread(() -> {
  4. log.debug("开始");
  5. sleep(1);
  6. log.debug("结束");
  7. R = 10;
  8. });
  9. t1.start(); //1、主线程和t1并行执行
  10. //sleep(2); //2、如果主线程sleep时间大于 t1线程 下面那行打印的R 是 10
  11. //sleep(0.2); //3、否则 下面那行打印的R 是 0
  12. //t1.join(); //4、等t1线程结束后 主线程再往下执行
  13. log.debug("结果为:{}", R);
  14. log.debug("结束");
  15. }

情况1:

因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 R=10 而主线程一开始就要打印 R 的结果,所以打印出 R=0

情况2:

主线程sleep时间大于 t1线程,在t1线程结束sleep后完成R=10 主线程才结束sleep,所有打印的R是10

情况3:

主线程sleep时间小于 t1线程,所以主线程打印R 在t1线程结束sleep后给 R赋值 之前,因此打印的R是0

情况4:

t1.join(),主线程执行到这里会停下来 等t1线程结束后 主线程再往下执行,因此打印的R是10

案例1:
以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

image.png
等待多个结果
问,下面代码 cost 大约多少秒?

  1. static int r1 = 0;
  2. static int r2 = 0;
  3. public static void main(String[] args) throws InterruptedException {
  4. test2();
  5. }
  6. private static void test2() throws InterruptedException {
  7. Thread t1 = new Thread(() -> {
  8. sleep(1);
  9. r1 = 10;
  10. });
  11. Thread t2 = new Thread(() -> {
  12. sleep(2);
  13. r2 = 20;
  14. });
  15. long start = System.currentTimeMillis();
  16. t1.start();
  17. t2.start();
  18. t1.join();
  19. t2.join();
  20. long end = System.currentTimeMillis();
  21. log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
  22. }

分析如下
第一个 join:等待 t1 时, t2 并没有停止, 而在运行
第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
如果颠倒两个 join 呢?
最终都是输出
20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005
image.png
有时效的 join
等够时间

  1. static int r1 = 0;
  2. static int r2 = 0;
  3. public static void main(String[] args) throws InterruptedException {
  4. test3();
  5. }
  6. public static void test3() throws InterruptedException {
  7. Thread t1 = new Thread(() -> {
  8. sleep(1);
  9. r1 = 10;
  10. });
  11. long start = System.currentTimeMillis();
  12. t1.start();
  13. // 线程执行结束会导致 join 结束
  14. t1.join(1500);
  15. long end = System.currentTimeMillis();
  16. log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
  17. }

输出
20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010
没等够时间

  1. static int r1 = 0;
  2. static int r2 = 0;
  3. public static void main(String[] args) throws InterruptedException {
  4. test3();
  5. }
  6. public static void test3() throws InterruptedException {
  7. Thread t1 = new Thread(() -> {
  8. sleep(2);
  9. r1 = 10;
  10. });
  11. long start = System.currentTimeMillis();
  12. t1.start();
  13. // 线程执行结束会导致 join 结束
  14. t1.join(1500);
  15. long end = System.currentTimeMillis();
  16. log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
  17. }

输出
20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502

9、interrupt方法

打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态

打断 sleep线程

会清空打断状态,以 sleep 为例

  1. public static void testInterrupt(){
  2. Thread t1 = new Thread(()->{
  3. log.debug(" sleep前-当前线程状态: {}", Thread.currentThread().getState());
  4. sleep(1);
  5. log.debug(" sleep后-当前线程状态: {}", Thread.currentThread().getState());
  6. }, "t1");
  7. t1.start();
  8. sleep(0.5);
  9. t1.interrupt();
  10. log.debug(" 打断状态: {}", t1.isInterrupted());
  11. }

输出

  1. 22:16:08.607 c.Sync [t1] - sleep前-当前线程状态: RUNNABLE
  2. java.lang.InterruptedException: sleep interrupted
  3. at java.base/java.lang.Thread.sleep(Native Method)
  4. at java.base/java.lang.Thread.sleep(Thread.java:337)
  5. at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
  6. at com.xjt.javase.juc.utils.MySleeper.sleep(MySleeper.java:8)
  7. at com.xjt.javase.juc.createThread.lambda$testInterrupt$1(createThread.java:107)
  8. at java.base/java.lang.Thread.run(Thread.java:832)
  9. 22:16:09.119 c.Sync [main] - 打断状态: true
  10. 22:16:09.120 c.Sync [t1] - sleep后-当前线程状态: RUNNABLE

打断 park线程

打断 park 线程, 不会清空打断状态

  1. private static void testInteruptPark() throws InterruptedException {
  2. Thread t1 = new Thread(() -> {
  3. log.debug("park...");
  4. LockSupport.park();
  5. log.debug("unpark...");
  6. log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
  7. }, "t1");
  8. t1.start();
  9. sleep(0.5);
  10. t1.interrupt();
  11. }

输出

  1. 22:20:59.146 c.Sync [t1] - park...
  2. 22:20:59.655 c.Sync [t1] - unpark...
  3. 22:20:59.655 c.Sync [t1] - 打断状态:true

10、不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名 static 功能说明
stop() 停止线程运行(推荐用interrupt停止)
suspend() 挂起(暂停)线程运行(推荐用wait 和 notify 暂停和唤醒线程)
resume() 恢复(唤醒)线程运行

11、守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。
有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
image.png
上图中,主线程运行结束了,但是t1线程中Thread.currentThread().isInterrupted() 没有被打断(这个值是false),一直在while循环中没有结束,所以java程序不会停下来。
举例:
需求:刘关张桃园三结义:不求同年同月同日生但求同年同月同日死,刘备志在复兴汉室 积劳而死,
作为他的结拜兄弟 关张二人也要践行诺言 一起陪大哥上路(虽然他们阳寿还未尽)

  1. private static void testDaemon() throws InterruptedException {
  2. /*需求:刘关张桃园三结义:不求同年同月同日生但求同年同月同日死,刘备作为大哥 积劳而死,
  3. 作为他的结拜兄弟 关张二人也要践行诺言 一起陪大哥上路(虽然他们阳寿还未尽)*/
  4. Thread liubei = new Thread(() -> {
  5. for (int i=0;i<3;i++){
  6. log.debug("刘备的寿命还有i="+i);
  7. }
  8. }, "刘备");
  9. Thread guanyu = new Thread(() -> {
  10. for (int i=0;i<80;i++){
  11. log.debug("关羽的寿命还有i="+i);
  12. }
  13. }, "关羽");
  14. Thread zhangfei = new Thread(() -> {
  15. for (int i=0;i<60;i++){
  16. log.debug("张飞的寿命还有i="+i);
  17. }
  18. }, "张飞");
  19. guanyu.setDaemon(true);
  20. zhangfei.setDaemon(true);
  21. liubei.start();
  22. guanyu.start();
  23. zhangfei.start();
  24. liubei.join(); //主线程要在这里等待liubei线程结束再运行
  25. log.debug("主线程结束...");
  26. }

输出
image.png
注意

  • 垃圾回收器线程就是一种守护线程(主线程停止了垃圾回收线程也会被强制停止);
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等

待它们处理完当前请求,这两个线程就会强制停止;

12、线程状态

五种状态

从 操作系统 层面来讲有五种状态
image.png

  • 【初始状态】仅是在语言层面创建了线程对象(new Thread),还未与操作系统线程关联;
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态

当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换

  • 【阻塞状态】

如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入
【阻塞状态】,等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

六种状态

这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
image.png

  • NEW

线程刚被创建,但是还没有调用 start() 方法

  • RUNNABLE

当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述

  • TERMINATED

当线程代码运行结束

13、应用之统筹(烧水泡茶)

共享模型之管程

共享模型之内存

共享模型之无锁

共享模型之不可变

共享模型之工具