Java 并发编程笔记 第二章:进程与线程、第三章:Java线程

网课链接: 黑马程序员java并发.

第二章 进程与线程

章节总结

  1. 计算资源收益最大化: 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu

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

    • 有些任务,经过精心设计可以进行任务拆分, 并行执行, 可以提高程序的运行效率。
    • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
  3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

1. 进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • **进程就可以视为程序的一个实例。**大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
  • Java 中,线程作为小调度单位,进程作为资源分配的最小单位。

两者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
    • 进程拥有共享的资源,如内存空间等
    • 供其内部的线程共享
      进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication)
  • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
    • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

2. 并发与并行

并发是一个CPU在不同的时间去不同线程中执行指令
并行是多个CPU同时处理不同的线程

引用 Rob Pike 的一段描述:

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

3. 应用

同步和异步的概念

  • 需要等待结果返回才能继续运行的话就是 同步
  • 不需要等待就是 异步
  1. 设计
    多线程可以使方法的执行变成异步的

比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,那么 cpu 只能等 5 秒,啥都不能做。

  1. 结论
    Tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 Tomcat 的工作线程.
    UI 程序中,开线程进行其他操作,避免阻塞 UI 线程.

第三章 Java 线程

章节总结

  1. 线程的创建
  2. 线程重要的 API,如 start、run、sleep、yield、join、interrupt 等
  3. 线程的状态
  4. 原理方面,线程的运行流程,栈、栈帧、上下文切换、程序计数器等知识
  5. Thread 两种创建线程的源码
  6. 使用 interrupt 来编写两阶段终止

1. 线程创建与运行

方法1, Thread

public static void main(String[] args) {
        // 匿名内部类方式创建 Thread
        Thread t = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running");
            }
        };
        
        t.start();
        log.debug("running");
    }

方法2, Runnable + Thread

public static void main(String[] args) {
        // 使用 lambda 表达式,因为 Runnable 接口标注了 @FunctionalInterface 这个注解,
        // 表示是一个函数式接口,可以使用 lambda 表达式
        Runnable r = () -> log.debug("running");
        new Thread(r, "t1").start();
    }

方法1 vs 方法2

方法 1 是把线程和任务合并在了一起
方法 2 是把线程和任务分开了,用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。
通过查看源码可以发现,方法二本质上还是通过使用 Thread 类中的 run 方法执行的!

方法3, Future + Thread

public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 使用 FutureTask 传入 Callable 接口方式创建
        FutureTask<Integer> future = new FutureTask<Integer>(() -> {
            log.debug("running...");
            Thread.sleep(2000); // 休眠
            return 100;
        });
        // 2. 传入 future, 因为 FutureTask 这个类是实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口
        Thread t1 = new Thread(future, "t1");
        t1.start();
        // 3. 获取返回结果时
        // 当主线程获取 t1 线程的返回值时, 需要等 2 秒,此时主线程进入阻塞状态
        log.debug("{}",  future.get());
    }

Future API

Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。

public interface Future<V> {
	// 取消任务
	boolean cancel(boolean mayInterruptIfRunning);
	// 获取任务执行结果
	V get() throws InterruptedException, ExecutionException;
	// 获取任务执行结果,带有超时时间限制
	V get(long timeout, TimeUnit unit) throws InterruptedException,                             ExecutionException,  TimeoutException;
	// 判断任务是否已经取消
	boolean isCancelled();
	// 判断任务是否已经结束
	boolean isDone();
}

2、线程运行原理

栈与栈帧

内存模型:
每个方法被执行的时候都会同时创建一个**栈帧(stack frame)**用于存储

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口等信息,是属于线程的私有的。

当Java中使用多线程时, 每个线程都会维护它自己的栈帧!每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法,当方法执行完会来到栈帧中的方法出口地址位置,然后从栈中 pop 出栈帧。

线程上下文切换(Thread Context Switch)

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

  • 被动原因:
    • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
    • 垃圾回收
    • 有更高优先级的线程需要运行
  • 主动原因:
    • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

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

3. Thread 的常见方法

Non-Static

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

Static

方法名功能说明备注
interrupted()判断当前线程是否被打断会清除 打断标记
currentThread()获取当前正在执行的线程
Sleep(long)让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
yield()提示线程调度器让出当前线程对CPU的使用主要是为了测试和调试

1. Start () vs Run()

run()

public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.info(Thread.currentThread().getName() + " running....");
            }
        }, "t1");

        // 测试通过 Thread 类实例 t1 对象直接调用 run 方法
        t1.run();

        log.info(Thread.currentThread().getName() + " running...");
    }

start()

public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.info(Thread.currentThread().getName() + " running....");
            }
        }, "t1");

        // 测试通过 Thread 类实例 t1 对象直接调用 run 方法
//        t1.run();
        // 调用 start 方法
        t1.start();

        log.info(Thread.currentThread().getName() + " running...");
    }

Start和Run的运行方式

使用 start 方式:

CPU 会为创建的线程分配时间片,线程进入运行状态,然后线程调用 run 方法执行逻辑。

使用 run 的方式:

虽然会创建了线程,但是它是直接调用方法,而不是像 start 方式那样触发的,这个线程对象会处一直处在新建状态,从结果上也可以看出,run 方法是 main 线程调用,而不是 t1 线程。

2. Sleep() vs Yield()

sleep (使线程阻塞)

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

yield (让出当前线程)

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

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

3. Join()

用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。
如在主线程中调用ti.join(),则是主线程等待t1线程结束,join 采用同步。

Thread t1 = new Thread();
//等待 t1 线程执行结束
t1.join();
// 最多等待 1000ms,如果 1000ms 内线程执行完毕,则会直接执行下面的语句,不会等够 1000ms
t1.join(1000);

4. Interrupt()

interrupt 打断线程有两种情况:

  • 如果一个线程在在运行中被打断,打断标记会被置为 true 。
  • 如果是打断因sleep wait join 方法而被阻塞的线程,会将打断标记置为 false 。

isInterrupted() 与 interrupted() :
isInterrupted 是实例方法,interrupted 是静态方法,它们的用处都是查看当前打断的状态,但是 isInterrupted 方法查看线程的时候,不会将打断标记清空,也就是置为 false,interrupted 查看线程打断状态后,会将打断标志置为 false,也就是清空打断标记,简单来说,interrupt() 方法类似于 setter 设置中断值,isInterrupted() 类似于 getter 获取中断值,interrupted() 类似于 getter + setter 先获取中断值,然后清除标志。

终止模式之两阶段终止模式

Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)

Interrupt()可以打断阻塞和运行中两种状态的进程

5. sleep(), yield(), wait(), join()

sleep:Thread类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU(阿里面试题 Sleep释放CPU,wait 也会释放cpu,因为cpu资源太宝贵了,只有在线程running的时候,才会获取cpu片段),提供其他线程运行的机会且不考虑优先级,但如果有同步锁则sleep不会释放锁即其他线程无法获得同步锁 可通过调用interrupt()方法来唤醒休眠线程。

yield让出CPU调度,Thread类的方法,类似sleep只是不能由用户指定暂停多长时间 ,并且yield()方法只能让同优先级的线程有执行的机会。 yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳。

wait:Object类的方法(notify()、notifyAll() 也是Object对象),必须放在循环体和同步代码块中,执行该方法的线程会释放锁,进入线程等待池中等待被再次唤醒(notify随机唤醒,notifyAll全部唤醒,线程结束自动唤醒)即放入锁池中竞争同步锁

join:一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。

等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。

不推荐: stop(), suspend(), resume()

6. 守护线程

主线程与守护线程: main threads vs damon threads

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。

有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

利用setDamon来设置守护线程

最常见的运用就是垃圾回收器

4. 线程状态

五种状态

在这里插入图片描述

操作系统层面去定义的状态

  • [初始状态] 仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • [可运行状态] (就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • [运行状态] 指获取了 CPU 时间片运行中的状态
    • 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • [阻塞状态]
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
  • [终止状态] 表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

六种状态

Java线程实现层面去定义的状态

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 [可运行状态], [运行状态] 和[阻塞状态] (由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING ,TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
  • TERMINATED 当线程代码运行结束

版权声明:本文为weixin_41225956原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。