1. 进程与线程

1.1 进程与线程

1.1.1 进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

1.1.2 线程

一个进程之内可以分为一到多个线程

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行

Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

1.1.3 两者对比

进程基本上相互独立的,而线程存在于进程内,是进程的一个子集

进程拥有共享的资源,如内存空间等,供其内部的线程共享

进程间通信较为复杂

  • 同一台计算机的进程通信称为 IPC(Inter-process communication)
  • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP

线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量

线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

1.2 并行与并发

单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行

一般会将这种 线程轮流使用 CPU 的做法称为并发,concurrent

单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活

多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的

  • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率
  • 但不是所有计算任务都能拆分,也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义

IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

引用 Rob Pike 的一段描述: 并发(concurrent)是同一时间应对(dealing with)多件事情的能力 并行(parallel)是同一时间动手做(doing)多件事情的能力

家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待) 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行

2. Java 线程

2.1 创建和运行线程

  1. public static void main(String[] args) {
  2. new Thread("t1") {
  3. @Override
  4. public void run() {
  5. log.info("t1 run");
  6. }
  7. }.start();
  8. }
  1. public static void main(String[] args) {
  2. Runnable runnable = new Runnable() {
  3. @Override
  4. public void run() {
  5. log.info("t2 run");
  6. }
  7. };
  8. new Thread(runnable, "t2").start();
  9. }
  1. public static void main(String[] args) throws Exception {
  2. FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
  3. @Override
  4. public Integer call() throws Exception {
  5. return 100;
  6. }
  7. });
  8. // 启动线程
  9. new Thread(futureTask, "t3").start();
  10. // 主线程阻塞
  11. Integer result = futureTask.get();
  12. log.info("结果是 {}", result);
  13. }

2.2 线程上下文切换

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

    2.3 常见方法

    | 方法名 | static | 功能说明 | 注意 | | —- | —- | —- | —- | | start() | | 启动一个新线程,在新的线程运行 run() 方法中的代码 | start 方法只是让线程进入就绪,代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start 方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException | | run() | | 新线程启动会调用的方法 | 如果在构造 Thread 对象时传入了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象来覆盖默认行为 | | join() | | 等待线程运行结束 | | | join(long) | | 等待线程运行结束,最多等待多少毫秒 | | | getId() | | 获取线程长整型的 id | id 唯一 | | getName() | | 获取线程名称 | | | setName(String) | | 修改线程名称 | | | getPriority() | | 获取线程优先级 | | | setPriority(int) | | 修改线程优先级 | Java 中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的频率 | | getState() | | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | | isInterrupted() | | 判断是否被打断 | 不会清除打断标记 | | isAlive() | | 线程是否存活 | | | interrupt() | | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记
    打断正在运行的线程,会设置打断标记不清除状态
    打断 park 的线程,会设置打断标记不清除状态 | | interrupted() | 是 | 判断当前线程是否被打断 | 会清除打断标记 | | currentThread() | 是 | 获取正在执行的线程 | | | sleep(long) | 是 | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | | | yield() | 是 | 提示线程调度器让出当前线程的对 CPU 的使用 | |

2.4 start 和 run

直接调用 run 是在主线程中执行了 run,没有启动新的线程

使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

  1. public static void main(String[] args) {
  2. Thread thread = new Thread("t1") {
  3. @Override
  4. public void run() {
  5. log.info("t1 start");
  6. }
  7. };
  8. // 这里是 main 线程打印日志
  9. thread.run();
  10. // 这里是 t1 新线程打印日志
  11. thread.start();
  12. }

2.5 sleep 和 yield

sleep

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

yield

  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器

    2.6 join

    ```java @Slf4j public class ThreadJoin {

    static int result = 1;

    public static void main(String[] args) throws Exception {

    1. Thread thread = new Thread(() -> result = 10, "t1");
    2. thread.start();
    3. // 确保线程运行结束, 将结果改成 10
    4. thread.join();
    5. log.info("result {}", result);

    }

}

  1. ```java
  2. @Slf4j
  3. public class ThreadMultiJoin {
  4. static int r1 = 0;
  5. static int r2 = 0;
  6. public static void main(String[] args) throws InterruptedException {
  7. Thread t1 = new Thread(() -> {
  8. try {
  9. TimeUnit.SECONDS.sleep(1);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. r1 = 10;
  14. });
  15. Thread t2 = new Thread(() -> {
  16. try {
  17. TimeUnit.SECONDS.sleep(2);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. r2 = 20;
  22. });
  23. long start = System.currentTimeMillis();
  24. t1.start();
  25. t2.start();
  26. // 等待 t1 时, t2 仍然在运行
  27. t1.join();
  28. // 因此在等待 t2 时, 只需要在等待 1s 即可
  29. t2.join();
  30. long end = System.currentTimeMillis();
  31. log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
  32. }
  33. }

2.7 interrupt

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

  1. public static void main(String[] args) throws Exception {
  2. Thread t1 = new Thread(() -> {
  3. try {
  4. TimeUnit.SECONDS.sleep(1);
  5. } catch (InterruptedException e) {
  6. log.error(e.getMessage(), e);
  7. }
  8. }, "t1");
  9. t1.start();
  10. TimeUnit.MILLISECONDS.sleep(100);
  11. // t1 interrupt 状态是 false
  12. log.info("t1 interrupt 状态是 {}", t1.isInterrupted());
  13. t1.interrupt();
  14. // t1 interrupt 状态是 true
  15. log.info("t1 interrupt 状态是 {}", t1.isInterrupted());
  16. }
  1. public static void main(String[] args) throws Exception {
  2. Thread t2 = new Thread(() -> {
  3. while (true) {
  4. Thread current = Thread.currentThread();
  5. boolean interrupted = current.isInterrupted();
  6. if (interrupted) {
  7. // 打断状态: true
  8. log.debug(" 打断状态: {}", interrupted);
  9. break;
  10. }
  11. }
  12. }, "t2");
  13. t2.start();
  14. TimeUnit.MILLISECONDS.sleep(100);
  15. t2.interrupt();
  16. }
  1. public static void main(String[] args) throws Exception {
  2. Thread t3 = new Thread(() -> {
  3. LockSupport.park();
  4. // 打断状态 true
  5. log.info("打断状态 {}", Thread.currentThread().isInterrupted());
  6. }, "t1");
  7. t3.start();
  8. TimeUnit.MILLISECONDS.sleep(100);
  9. t3.interrupt();
  10. }
  1. public static void main(String[] args) throws Exception {
  2. Thread t3 = new Thread(() -> {
  3. for (int i = 0; i < 3; i++) {
  4. // 如果线程已经被打断, 这里的 park() 方法会失效,会一直循环 3 次
  5. LockSupport.park();
  6. // 打断状态 true
  7. log.info("打断状态 {}", Thread.currentThread().isInterrupted());
  8. }
  9. }, "t1");
  10. t3.start();
  11. TimeUnit.MILLISECONDS.sleep(100);
  12. t3.interrupt();
  13. }

2.8 不推荐的方法

方法名 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行

2.9 主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束

  1. public static void main(String[] args) throws Exception {
  2. Thread t1 = new Thread(() -> {
  3. log.info("子线程运行开始");
  4. try {
  5. TimeUnit.SECONDS.sleep(2);
  6. } catch (InterruptedException e) {
  7. log.error(e.getMessage(), e);
  8. }
  9. log.info("子线程运行结束");
  10. }, "t1");
  11. // 设置该线程为守护线程
  12. t1.setDaemon(true);
  13. t1.start();
  14. TimeUnit.MILLISECONDS.sleep(100);
  15. log.info("主线程运行完成");
  16. }

2.10 线程状态

2.10.1 五种状态(操作系统)

image.png
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

【运行状态】指获取了 CPU 时间片运行中的状态

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

【阻塞状态】

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

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

2.10.2 六种状态(Java API)

image.png
【NEW】 线程刚被创建,但是还没有调用 start() 方法

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

【BLOCKED、WAITING、TIMED_WAITING】 都是 Java API 层面对【阻塞状态】的细分

【TERMINATED】 当线程代码运行