13、Java并发编程


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

概念

并发与并行

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

线程与进程

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

Java 线程

本章内容

  • 创建和运行线程
  • 查看线程
  • 线程 API
  • 线程状态

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

public class Sleeper {
    public static void sleep(int i) {
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sleep(double i) {
        try {
            TimeUnit.MILLISECONDS.sleep((int) (i * 1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

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

@Slf4j(topic = "c.FileReader")
public class FileReader {
    public static void read(String filename) {
        int idx = filename.lastIndexOf(File.separator);
        String shortName = filename.substring(idx + 1);
        try (FileInputStream in = new FileInputStream(filename)) {
            long start = System.currentTimeMillis();
            log.debug("read [{}] start ...", shortName);
            byte[] buf = new byte[1024];
            int n = -1;
            do {
                n = in.read(buf);
            } while (n != -1);
            long end = System.currentTimeMillis();
            log.debug("read [{}] end ... cost: {} ms", shortName, end - start);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

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()

public static void main(String[] args) {
    Thread t1 = new Thread("t1") {
        @Override
        public void run() {
            log.debug(Thread.currentThread().getName());
            FileReader.read(Constants.MP4_FULL_PATH);
        }
    };
    t1.run();

    //t1.start();
    log.debug("do other things ...");
}

输出

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

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

输出

19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
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 方法

public static void testJoin() throws InterruptedException {
    log.debug("开始");

    Thread t1 = new Thread(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        R = 10;
    });
    t1.start();		//1、主线程和t1并行执行
    //sleep(2);         //2、如果主线程sleep时间大于 t1线程 下面那行打印的R 是 10
    //sleep(0.2);     //3、否则 下面那行打印的R 是 0

    //t1.join();      //4、等t1线程结束后 主线程再往下执行

    log.debug("结果为:{}", R);
    log.debug("结束");
}

情况 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 大约多少秒?

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test2();
}
private static void test2() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1);
        r1 = 10;
    });
    Thread t2 = new Thread(() -> {
        sleep(2);
        r2 = 20;
    });
    long start = System.currentTimeMillis();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

分析如下
第一个 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
等够时间

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
    // 线程执行结束会导致 join 结束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

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

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test3();
}
public static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(2);
        r1 = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 线程执行结束会导致 join 结束
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

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

9、interrupt 方法

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

打断 sleep 线程

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

public static void testInterrupt(){
    Thread t1 = new Thread(()->{
        log.debug(" sleep前-当前线程状态: {}", Thread.currentThread().getState());
        sleep(1);
        log.debug(" sleep后-当前线程状态: {}", Thread.currentThread().getState());

    }, "t1");
    t1.start();

    sleep(0.5);
    t1.interrupt();
    log.debug(" 打断状态: {}", t1.isInterrupted());
}

输出

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

打断 park 线程

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

private static void testInteruptPark() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.debug("park...");
        LockSupport.park();
        log.debug("unpark...");
        log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
    }, "t1");
    t1.start();

    sleep(0.5);
    t1.interrupt();
}

输出

22:20:59.146 c.Sync [t1] - park...
22:20:59.655 c.Sync [t1] - unpark...
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 程序不会停下来。
举例:
需求:刘关张桃园三结义:不求同年同月同日生但求同年同月同日死,刘备志在复兴汉室 积劳而死,
作为他的结拜兄弟 关张二人也要践行诺言 一起陪大哥上路(虽然他们阳寿还未尽)

private static void testDaemon() throws InterruptedException {
    /*需求:刘关张桃园三结义:不求同年同月同日生但求同年同月同日死,刘备作为大哥 积劳而死,
         作为他的结拜兄弟 关张二人也要践行诺言 一起陪大哥上路(虽然他们阳寿还未尽)*/
    Thread liubei = new Thread(() -> {
        for (int i=0;i<3;i++){
            log.debug("刘备的寿命还有i="+i);
        }
    }, "刘备");

    Thread guanyu = new Thread(() -> {
        for (int i=0;i<80;i++){
            log.debug("关羽的寿命还有i="+i);
        }
    }, "关羽");

    Thread zhangfei = new Thread(() -> {
        for (int i=0;i<60;i++){
            log.debug("张飞的寿命还有i="+i);
        }
    }, "张飞");

    guanyu.setDaemon(true);
    zhangfei.setDaemon(true);
    liubei.start();
    guanyu.start();
    zhangfei.start();

    liubei.join();		//主线程要在这里等待liubei线程结束再运行
    log.debug("主线程结束...");
}

输出
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、应用之统筹(烧水泡茶)

共享模型之管程

共享模型之内存

共享模型之无锁

共享模型之不可变

共享模型之工具


文章作者: CoderXiong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CoderXiong !
  目录