1. 线程创建与运行

1.1 线程创建的四种方式

方法1. 使用Thread

  1. @Slf4j(topic = "c.CreateThread1")
  2. public class CreateThread1 {
  3. public static void main(String[] args) {
  4. Thread t1 = new Thread("t1"){
  5. public void run(){
  6. log.debug("t1-running");
  7. }
  8. };
  9. t1.start();
  10. log.debug("main-running");
  11. }
  12. }
  1. 13:40:33.366 [main] DEBUG c.CreateThread1 - main-running
  2. 13:40:33.366 [t1] DEBUG c.CreateThread1 - t1-running

方法2.使用 Runnable 配合 Thread(推荐)

把【线程】和【任务】(要执行的代码)分开,Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)

  1. @Slf4j(topic = "c.CreateThread2")
  2. public class CreateThread2 {
  3. public static void main(String[] args) {
  4. // 任务对象
  5. Runnable runnable = new Runnable() {
  6. @Override
  7. public void run() {
  8. log.debug("running");
  9. }
  10. };
  11. // 线程对象
  12. Thread thread = new Thread(runnable, "t2");
  13. // 启动
  14. thread.start();
  15. }
  16. }
  1. 13:43:42.104 [t2] DEBUG c.CreateThread2 - running

小结

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

源码:

  • 创建线程为Thread类的init方法的重载,target对象为自定义的Runnable对象,

截屏2021-04-28 下午1.50.28.png
在执行Thread的run()方法时,他会判断是否target对象,如果有,就用。
截屏2021-04-28 下午1.52.36.png

方法3 FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。FutureTask 同时实现了Runnable, Future接口(FutureTask实现了RunnableFuture接口,该接口继承Runnable接口和Future接口)。Future接口可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。FutrueTask是Futrue接口的唯一的实现类。 它既可以作为 Runnable被线程执行,又可以作为Future得到Callable的返回值。
Thread类中没有一个方法是执行Callable的,所以需要外界帮助。通过适配器模式将Callable和Runnable接口建立联系。在FutureTask类中通过构造方法将实现了Callable接口的类放入。即对象适配器思想。一方面,FutureTask可以当做Runnable被Thread执行,另一方面实现了Future接口,可以用Future接口的get方法获取到Callable接口中call方法的计算结果)

  • Callable接口:与Runnable相比,功能更强
    • 相比run方法,可以有返回值
    • 方法可以抛出异常,被外面的操作捕获,获取异常信息
    • 支持泛型的返回值
    • 需要借助FutureTask类(是Future接口的唯一实现类,可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等)
  • future接口与Callable接口的关系

    • 可以用Future.get()来获取Callable接口返回的执行任务,
    • Futuren.isDone()来判断任务是否已经执行完成或者取消任务。
    • 如果call()方法还没执行完,调用get()方法的线程会被阻塞,直到call()方法返回结果,主线程才会恢复。
    • 可以认为Future是一个存储器,存储了call()这个任务的结果。

      1. @Slf4j(topic = "c.CreateThread3")
      2. public class CreateThread3 {
      3. public static void main(String[] args) throws ExecutionException, InterruptedException {
      4. // 创建FutureTask对象(作为线程要实现的内容),将实现了Callable接口的对象传入,
      5. FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
      6. @Override
      7. public Integer call() throws Exception {
      8. log.debug("running");
      9. Thread.sleep(1000);
      10. return 100;
      11. }
      12. });
      13. // 将task对象传入t1线程中
      14. Thread t1 = new Thread(task, "t1");
      15. t1.start();
      16. // 等待task返回结果
      17. Integer integer = task.get();
      18. log.debug("{}", integer);
      19. }
      20. }
      1. 14:24:25.084 [t1] DEBUG c.CreateThread3 - running
      2. 14:24:26.092 [main] DEBUG c.CreateThread3 - 100

      1.2 查看进程线程的方法

  • windows

    • 任务管理器可以查看进程和线程数,也可以用来杀死进程
    • tasklist 查看进程
    • taskkill 杀死进程
  • linux
    • ps -fe 查看所有进程
    • ps -fT -p 查看某个进程(PID)的所有线程
    • kill 杀死进程
    • top 按大写 H 切换是否显示线程
    • top -H -p 查看某个进程(PID)的所有线程
  • Java

    • jps 命令查看所有 Java 进程
    • jstack 查看某个 Java 进程(PID)的所有线程状态
    • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

      1.3 线程运行原理

      1.3.1 虚拟机栈与栈帧

      虚拟机栈描述的是Java方法执行的内存模型。每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,是属于线程的私有的。当java中使用多线程时,每个线程都会维护它自己的栈帧。
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

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

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

  • 线程的 cpu 时间片用完

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

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

1.4 Thread的常见方法

image.png

1.4.1 start() vs run()

1. start

  1. public static void main(String[] args) {
  2. Thread thread = new Thread(){
  3. @Override
  4. public void run(){
  5. log.debug("我是一个新建的线程正在运行中");
  6. FileReader.read(fileName);
  7. }
  8. };
  9. thread.setName("新建线程");
  10. thread.start();
  11. log.debug("主线程");
  12. }

输出:程序在 t1 线程运行,run()方法里面内容的调用是异步的

  1. 11:59:40.711 [main] DEBUG com.concurrent.test.Test4 - 主线程
  2. 11:59:40.711 [新建线程] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
  3. 11:59:40.732 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] start ...
  4. 11:59:40.735 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 3 ms

2. run

将上面代码的thread.start();改为thread.run();输出结果如下:程序仍在 main 线程运行,run()方法里面内容的调用还是同步的

  1. 12:03:46.711 [main] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
  2. 12:03:46.727 [main] DEBUG com.concurrent.test.FileReader - read [test] start ...
  3. 12:03:46.729 [main] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 2 ms
  4. 12:03:46.730 [main] DEBUG com.concurrent.test.Test4 - 主线程

3.小结

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

start方法对线程状态的影响

  1. @Slf4j(topic = "c.test1")
  2. public class ThreadTest1 {
  3. public static void main(String[] args) {
  4. Thread t1 = new Thread("t1"){
  5. public void run(){
  6. log.debug("running...");
  7. }
  8. };
  9. System.out.println(t1.getState());
  10. t1.start();
  11. System.out.println(t1.getState());
  12. }
  13. }
  1. NEW
  2. RUNNABLE
  3. 15:29:00.095 [t1] DEBUG c.test1 - running...

线程由NEW状态变为了RUNNABLE状态。

1.4.2 sleep() vs yield()

1. sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
  3. 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
  4. 建议用 TimeUnit 的sleep()代替 Thread 的sleep()来获得更好的可读性

    sleep的使用: Timed Waiting

  1. @Slf4j(topic = "c.ThreadTest2")
  2. public class ThreadTest2 {
  3. public static void main(String[] args) {
  4. Thread t1 = new Thread("t1"){
  5. @Override
  6. public void run() {
  7. try {
  8. // 让t1线程进入休眠状态
  9. Thread.sleep(2000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. };
  15. t1.start();
  16. // 打印t1状态,由于主线程创建之后,t1线程才创建出来,当主线程执行该方法时,t1线程可能还没有休眠
  17. log.debug("t1 state:{}" , t1.getState()); // RUNNABLE
  18. // 解决办法,给主线程休眠一下,
  19. try {
  20. Thread.sleep(500);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. log.debug("t1 state:{}" , t1.getState()); // TIMED_WAITING
  25. }
  26. }
  1. 15:39:04.488 [main] DEBUG c.ThreadTest2 - t1 state:RUNNABLE
  2. 15:39:04.997 [main] DEBUG c.ThreadTest2 - t1 state:TIMED_WAITING

sleep的使用:打断睡眠

  1. @Slf4j(topic = "c.ThreadTest3")
  2. public class ThreadTest3 {
  3. public static void main(String[] args) {
  4. Thread t1 = new Thread("t1"){
  5. @Override
  6. public void run() {
  7. log.debug("t1 enter sleep...");
  8. try {
  9. Thread.sleep(2000);
  10. } catch (InterruptedException e) {
  11. log.debug("wake up");
  12. e.printStackTrace();
  13. }
  14. }
  15. };
  16. t1.start();
  17. try {
  18. Thread.sleep(1000);
  19. // 打断
  20. log.debug("interrupt");
  21. t1.interrupt();
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. }
  1. 15:52:31.713 [t1] DEBUG c.ThreadTest3 - t1 enter sleep...
  2. 15:52:32.716 [main] DEBUG c.ThreadTest3 - interrupt
  3. 15:52:32.717 [t1] DEBUG c.ThreadTest3 - wake up
  4. java.lang.InterruptedException: sleep interrupted
  5. at java.lang.Thread.sleep(Native Method)
  6. at com.ll.ch2.ThreadTest3$1.run(ThreadTest3.java:14)

TimeUnit

  1. @Slf4j(topic = "c.ThreadTest4")
  2. public class ThreadTest4 {
  3. public static void main(String[] args) {
  4. try {
  5. log.debug("enter");
  6. TimeUnit.SECONDS.sleep(1);
  7. log.debug("end");
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }
  1. 15:55:27.245 [main] DEBUG c.ThreadTest4 - enter
  2. 15:55:28.255 [main] DEBUG c.ThreadTest4 - end

2. yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法想把cpu的使用权让出去,但是还是执行该线程)

    3.小结

    yield(Runnable状态)使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep(Time waiting状态)需要等过了休眠时间之后才有可能被分配cpu时间片

    4. sleep的应用

    在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其他程序。可以使用wait或条件变量达到类似效果;不同的是,后两种需要加锁,适用于同步场景;sleep适用于无需锁同步的场景。

    1.4.3 线程优先级

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

    设置优先级的源码

  1. public final void setPriority(int newPriority) {
  2. ThreadGroup g;
  3. checkAccess();
  4. if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
  5. throw new IllegalArgumentException();
  6. }
  7. if((g = getThreadGroup()) != null) {
  8. if (newPriority > g.getMaxPriority()) {
  9. newPriority = g.getMaxPriority();
  10. }
  11. setPriority0(priority = newPriority);
  12. }
  13. }

不使用yield

  1. @Slf4j(topic = "c.ThreadTest5")
  2. public class ThreadTest5 {
  3. public static void main(String[] args) {
  4. Runnable task1 = new Runnable() {
  5. @Override
  6. public void run() {
  7. int cnt = 0;
  8. for (;;){
  9. System.out.println("--->1 " + cnt++);
  10. }
  11. }
  12. };
  13. Runnable task2 = new Runnable() {
  14. @Override
  15. public void run() {
  16. int cnt = 0;
  17. for (;;){
  18. System.out.println(" --->2 " + cnt++);
  19. }
  20. }
  21. };
  22. Thread t1 = new Thread(task1, "t1");
  23. Thread t2 = new Thread(task2, "t2");
  24. t1.start();
  25. t2.start();
  26. }
  27. }

截屏2021-04-28 下午4.50.42.png
t1与t2势均力敌

使用yield

  1. @Slf4j(topic = "c.ThreadTest5")
  2. public class ThreadTest5 {
  3. public static void main(String[] args) {
  4. Runnable task1 = new Runnable() {
  5. @Override
  6. public void run() {
  7. int cnt = 0;
  8. for (;;){
  9. System.out.println("--->1 " + cnt++);
  10. }
  11. }
  12. };
  13. Runnable task2 = new Runnable() {
  14. @Override
  15. public void run() {
  16. int cnt = 0;
  17. for (;;){
  18. Thread.yield();
  19. System.out.println(" --->2 " + cnt++);
  20. }
  21. }
  22. };
  23. Thread t1 = new Thread(task1, "t1");
  24. Thread t2 = new Thread(task2, "t2");
  25. t1.start();
  26. t2.start();
  27. }
  28. }

截屏2021-04-28 下午4.51.55.png
可以看到t1明显比t2的数大,t2让步了。

加入优先级

  1. @Slf4j(topic = "c.ThreadTest5")
  2. public class ThreadTest5 {
  3. public static void main(String[] args) {
  4. Runnable task1 = new Runnable() {
  5. @Override
  6. public void run() {
  7. int cnt = 0;
  8. for (;;){
  9. System.out.println("--->1 " + cnt++);
  10. }
  11. }
  12. };
  13. Runnable task2 = new Runnable() {
  14. @Override
  15. public void run() {
  16. int cnt = 0;
  17. for (;;){
  18. //Thread.yield();
  19. System.out.println(" --->2 " + cnt++);
  20. }
  21. }
  22. };
  23. Thread t1 = new Thread(task1, "t1");
  24. Thread t2 = new Thread(task2, "t2");
  25. t1.setPriority(Thread.MIN_PRIORITY);
  26. t2.setPriority(Thread.MAX_PRIORITY);
  27. t1.start();
  28. t2.start();
  29. }
  30. }

截屏2021-04-28 下午4.56.32.png
效果不是很明显

1.4.4 join的使用

例子1:

  1. @Slf4j(topic = "c.JoinTest1")
  2. public class JoinTest1 {
  3. private static int r = 0;
  4. public static void main(String[] args) throws InterruptedException {
  5. log.debug("开始");
  6. Thread t1 = new Thread(() -> {
  7. try {
  8. log.debug("开始");
  9. Thread.sleep(1000);
  10. log.debug("结束");
  11. r = 10;
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }, "t1");
  16. t1.start();
  17. // t1.join();
  18. log.debug("结果为:{}", r);
  19. log.debug("结束");
  20. }
  21. }
  1. 18:03:36.497 [main] DEBUG c.JoinTest1 - 开始
  2. 18:03:36.567 [t1] DEBUG c.JoinTest1 - 开始
  3. 18:03:36.567 [main] DEBUG c.JoinTest1 - 结果为:0
  4. 18:03:36.570 [main] DEBUG c.JoinTest1 - 结束
  5. 18:03:37.569 [t1] DEBUG c.JoinTest1 - 结束

从结果中可以看到:r的值为0。因为t1的赋值在sleep方法之后进行的,但是主线程执行时,t1线程也开始执行了,主线程打印的时候t1线程还在sleep中,没有进行赋值。

解决办法

  • sleep时间不好把控
  • 使用join方法,等待线程运行结束 ```java @Slf4j(topic = “c.JoinTest1”) public class JoinTest1 { private static int r = 0;

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

    1. log.debug("开始");
    2. Thread t1 = new Thread(() -> {
    3. try {
    4. log.debug("开始");
    5. Thread.sleep(1000);
    6. log.debug("结束");
    7. r = 10;
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }, "t1");
    12. t1.start();
    13. t1.join();
    14. log.debug("结果为:{}", r);
    15. log.debug("结束");

    } }

  1. ```java
  2. 18:03:08.987 [main] DEBUG c.JoinTest1 - 开始
  3. 18:03:09.057 [t1] DEBUG c.JoinTest1 - 开始
  4. 18:03:10.062 [t1] DEBUG c.JoinTest1 - 结束
  5. 18:03:10.063 [main] DEBUG c.JoinTest1 - 结果为:10
  6. 18:03:10.064 [main] DEBUG c.JoinTest1 - 结束

主线程等待t1结束才运行,得到了赋值的结果10

线程的同步

同步:需要等待结果返回,才能继续运行;
异步:不需要等待结果返回,就能继续运行。
image.png

例子2

  1. @Slf4j( topic = "c.JoinTest2")
  2. public class JoinTest2 {
  3. private static int r1 = 0;
  4. private static int r2 = 0;
  5. public static void main(String[] args) throws InterruptedException {
  6. Thread t1 = new Thread(() -> {
  7. try {
  8. sleep(1000);
  9. r1 = 10;
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. });
  14. Thread t2 = new Thread(() -> {
  15. try {
  16. sleep(2000);
  17. r2 = 20;
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. });
  22. t1.start();
  23. t2.start();
  24. long start = System.currentTimeMillis();
  25. log.debug("join begin");
  26. t1.join();
  27. log.debug("t1 join end");
  28. t2.join();
  29. log.debug("t2 join end");
  30. long end = System.currentTimeMillis();
  31. log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
  32. }
  33. }
  1. 18:10:13.041 [main] DEBUG c.JoinTest2 - join begin
  2. 18:10:14.044 [main] DEBUG c.JoinTest2 - t1 join end
  3. 18:10:15.044 [main] DEBUG c.JoinTest2 - t2 join end
  4. 18:10:15.044 [main] DEBUG c.JoinTest2 - r1: 10 r2: 20 cost: 2005

从结果可以看出,等待时间为2秒钟,因为开始的时候主线程和t1、t2线程同时开始,所以只需等待2秒钟即可,不用等待3秒钟。(t2相较于t1晚1秒打印)

交换两个join

  1. @Slf4j( topic = "c.JoinTest2")
  2. public class JoinTest2 {
  3. private static int r1 = 0;
  4. private static int r2 = 0;
  5. public static void main(String[] args) throws InterruptedException {
  6. Thread t1 = new Thread(() -> {
  7. try {
  8. sleep(1000);
  9. r1 = 10;
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. });
  14. Thread t2 = new Thread(() -> {
  15. try {
  16. sleep(2000);
  17. r2 = 20;
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. });
  22. t1.start();
  23. t2.start();
  24. long start = System.currentTimeMillis();
  25. log.debug("join begin");
  26. t2.join();
  27. log.debug("t2 join end");
  28. t1.join();
  29. log.debug("t1 join end");
  30. long end = System.currentTimeMillis();
  31. log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
  32. }
  1. 18:18:02.773 [main] DEBUG c.JoinTest2 - join begin
  2. 18:18:04.775 [main] DEBUG c.JoinTest2 - t2 join end
  3. 18:18:04.775 [main] DEBUG c.JoinTest2 - t1 join end
  4. 18:18:04.776 [main] DEBUG c.JoinTest2 - r1: 10 r2: 20 cost: 2005

先对t2线程join,到t1线程join时,已经不需要等了,因为t1线程早已经结束了(t1与t2的打印时间相同)
截屏2021-04-28 下午6.15.01.png

限时同步

  1. @Slf4j(topic = "c.JoinTest3")
  2. public class JoinTest3 {
  3. private static int r1 = 0;
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread t1 = new Thread(() -> {
  6. try {
  7. sleep(2000);
  8. r1 = 10;
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. });
  13. long start = System.currentTimeMillis();
  14. t1.start();
  15. // 线程执行结束会导致 join 结束
  16. log.debug("join begin");
  17. // 设置join等待1500ms就不再等了
  18. t1.join(1500);
  19. long end = System.currentTimeMillis();
  20. log.debug("r1: {} cost: {}", r1, end - start);
  21. }
  22. }
  1. 18:30:12.519 [main] DEBUG c.JoinTest3 - join begin
  2. 18:30:14.025 [main] DEBUG c.JoinTest3 - r1: 0 cost: 1508

因为设置了join的等待时间,主线程等待1500毫秒就不再等了,而t1线程会在2000毫秒赋值,所以r1值为0

  1. @Slf4j(topic = "c.JoinTest3")
  2. public class JoinTest3 {
  3. private static int r1 = 0;
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread t1 = new Thread(() -> {
  6. try {
  7. Thread.sleep(2000);
  8. r1 = 10;
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. });
  13. long start = System.currentTimeMillis();
  14. t1.start();
  15. // 线程执行结束会导致 join 结束
  16. log.debug("join begin");
  17. // 设置join等待3000ms就不再等了
  18. t1.join(3000);
  19. long end = System.currentTimeMillis();
  20. log.debug("r1: {} cost: {}", r1, end - start);
  21. }
  22. }
  1. 18:31:41.227 [main] DEBUG c.JoinTest3 - join begin
  2. 18:31:43.228 [main] DEBUG c.JoinTest3 - r1: 10 cost: 2003

将等待时间设置为3000毫秒时,能获取到正常赋值,同时在t1结束的时候主线程也就结束了。

1.4.5 interrupt 方法详解

1.打断 sleep,wait,join 的线程

sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态,interrupt用于打断阻塞状态,同时会清除打断标记

以 sleep 为例

  1. @Slf4j(topic = "InterruptTest1")
  2. public class InterruptTest1 {
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t1 = new Thread("t1"){
  5. @Override
  6. public void run() {
  7. log.debug("sleep...");
  8. try {
  9. Thread.sleep(2000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. };
  15. t1.start();
  16. Thread.sleep(500);
  17. log.debug("interrupt");
  18. t1.interrupt();
  19. log.debug("打断标记:{}", t1.isInterrupted());
  20. }
  21. }
  1. 18:41:22.355 [t1] DEBUG InterruptTest1 - sleep...
  2. 18:41:22.856 [main] DEBUG InterruptTest1 - interrupt
  3. 18:41:22.857 [main] DEBUG InterruptTest1 - 打断标记:false
  4. java.lang.InterruptedException: sleep interrupted
  5. at java.lang.Thread.sleep(Native Method)
  6. at com.ll.ch2.InterruptTest1$1.run(InterruptTest1.java:13)

2. 打断正常运行的线程

打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();的返回值为true,可以判断Thread.currentThread().isInterrupted();的值来手动停止线程

  1. @Slf4j(topic = "InterruptTest2")
  2. public class InterruptTest2 {
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t1 = new Thread("t1"){
  5. @Override
  6. public void run() {
  7. while (true){
  8. boolean flag = Thread.currentThread().isInterrupted();
  9. if (isInterrupted()){
  10. log.debug("被打断了,退出循环");
  11. break;
  12. }
  13. }
  14. }
  15. };
  16. t1.start();
  17. Thread.sleep(1000);
  18. log.debug("打断t1");
  19. t1.interrupt();
  20. }
  21. }
  1. 11:42:52.312 [main] DEBUG InterruptTest2 - 打断t1
  2. 11:42:52.316 [t1] DEBUG InterruptTest2 - 被打断了,退出循环

两阶段终止模式

  • 使用线程对象的stop方法停止线程
    • stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁。
  • 使用System.exit(int) 方法停止线程
    • 目的仅是停止一个线程,这种做法会让整个程序停止。

Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁),监控线程在监控过程中停止监控了就可以采用这个方式。
如下所示:线程的isInterrupted()方法可以取得线程的打断标记,如果线程在睡眠sleep期间被打断,打断标记是不会变的,为false,但是sleep期间被打断会抛出异常,我们据此手动设置打断标记为true;如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true。处理好这两种情况那我们就可以放心地来料理后事啦!
image.png

  1. @Slf4j(topic = "c.TerminationModeTest")
  2. public class TerminationModeTest {
  3. public static void main(String[] args) throws InterruptedException {
  4. TwoPhaseTermination t = new TwoPhaseTermination();
  5. // 启动监控线程
  6. t.start();
  7. // 主线程等3.5秒后
  8. Thread.sleep(3500);
  9. // 优雅的停止监控线程
  10. t.stop();
  11. }
  12. }
  13. @Slf4j(topic = "c.TwoPhaseTermination")
  14. class TwoPhaseTermination {
  15. private Thread monitor;
  16. public void start(){
  17. monitor = new Thread("monitor"){
  18. @Override
  19. public void run() {
  20. while (true){
  21. // 拿到当前线程
  22. Thread current = Thread.currentThread();
  23. // 判断是否被打断
  24. if (current.isInterrupted()){
  25. // 被打断
  26. log.debug("处理后事");
  27. break;
  28. }
  29. try {
  30. Thread.sleep(1000);
  31. log.debug("监控记录");
  32. } catch (InterruptedException e) {
  33. // 睡眠期间被打断
  34. e.printStackTrace();
  35. // 重新设置打断标记
  36. current.interrupt();
  37. }
  38. }
  39. }
  40. };
  41. monitor.start();
  42. }
  43. public void stop(){
  44. monitor.interrupt();
  45. }
  46. }
  1. 11:34:43.983 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
  2. 11:34:44.990 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
  3. 11:34:45.995 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
  4. java.lang.InterruptedException: sleep interrupted
  5. at java.lang.Thread.sleep(Native Method)
  6. at com.ll.ch2.TwoPhaseTermination$1.run(TerminationModeTest.java:34)
  7. 11:34:46.483 [monitor] DEBUG c.TwoPhaseTermination - 处理后事

3.打断park线程

park线程让当前线程停下来

  1. @Slf4j(topic = "ParkTest")
  2. public class ParkTest {
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t1 = new Thread(()-> {
  5. log.debug("park....");
  6. LockSupport.park();
  7. log.debug("unpark.......");
  8. log.debug("打断状态{}", Thread.currentThread().isInterrupted());
  9. LockSupport.park();
  10. log.debug("unpark....");
  11. }, "t1");
  12. t1.start();
  13. Thread.sleep(1000);
  14. t1.interrupt();
  15. }
  16. }
  1. 11:49:54.913 [t1] DEBUG ParkTest - park....
  2. 11:49:55.916 [t1] DEBUG ParkTest - unpark.......
  3. 11:49:55.917 [t1] DEBUG ParkTest - 打断状态true
  4. 11:49:55.919 [t1] DEBUG ParkTest - unpark....

park()将t1线程挂起,主线程睡1秒后打断t1线程的park状态,此时t1线程继续执行,注意park一旦被打断就无法继续挂起线程。

4.isInterrupted() 与 interrupted() 比较

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

1.4.6 过时的方法

stop():停止线程运行
suspend():挂起(暂停)线程运行
resume():恢复线程运行
这些方法已过时,会破坏同步代码块,造成线程死锁。

1.4.7 守护线程

默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用t1.setDeamon(true);方法变成守护线程。

代码示例:

  1. @Slf4j(topic = "DaemonTest")
  2. public class DaemonTest {
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t1 = new Thread(()->{
  5. while (true){
  6. if (Thread.currentThread().isInterrupted()){
  7. break;
  8. }
  9. }
  10. log.debug("t1结束");
  11. }, "t1");
  12. //t1.setDaemon(true);
  13. t1.start();
  14. Thread.sleep(1000);
  15. log.debug("主线程结束");
  16. }
  17. }

截屏2021-04-29 下午1.17.46.png
可以看到主线程结束了,但是t1线程还没有结束,所以java进程不会 停止,还在运行中

守护线程

设置t1为守护线程

  1. @Slf4j(topic = "DaemonTest")
  2. public class DaemonTest {
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t1 = new Thread(()->{
  5. while (true){
  6. if (Thread.currentThread().isInterrupted()){
  7. break;
  8. }
  9. }
  10. log.debug("t1结束");
  11. }, "t1");
  12. t1.setDaemon(true);
  13. t1.start();
  14. Thread.sleep(1000);
  15. log.debug("主线程结束");
  16. }
  17. }

截屏2021-04-29 下午1.19.34.png
可以随着主线程结束,进程就终止了。守护线程t1不管结没结束,也就随着终止了。

注意

  • 垃圾回收器线程就是一种守护线程。进程结束了,垃圾回收也就停止。
  • Tomcat 中的 Acceptor(接收请求) 和 Poller (分发请求)线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

    2.线程状态

    2.1 线程的5种状态

    从操作系统层面划分,线程有5种状态
    截屏2020-07-12 下午5.16.18.png

  • 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联

  • 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
  • 运行状态,指线程获取了CPU时间片,正在运行
    • 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
  • 阻塞状态
    • 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
    • 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
  • 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

    2.2 线程的6种状态

    这是从 Java API 层面来描述的
    image.png

  • 1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

  • 2.运行(RUNNABLE):Java线程中将就绪、运行、阻塞(IO阻塞,java无法区分)三种状态笼统的称为“运行”。
    1. 阻塞(BLOCKED):表示线程阻塞于锁。
    1. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
    1. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
    1. 终止(TERMINATED):表示该线程已经执行完毕。


代码示例

  1. @Slf4j(topic = "c.StateTest")
  2. public class StateTest {
  3. public static void main(String[] args) throws IOException {
  4. Thread t1 = new Thread("t1") {
  5. @Override
  6. public void run() {
  7. log.debug("running...");
  8. }
  9. };
  10. Thread t2 = new Thread("t2") {
  11. @Override
  12. public void run() {
  13. while(true) { // runnable
  14. }
  15. }
  16. };
  17. t2.start();
  18. Thread t3 = new Thread("t3") {
  19. @Override
  20. public void run() {
  21. log.debug("running...");
  22. }
  23. };
  24. t3.start();
  25. Thread t4 = new Thread("t4") {
  26. @Override
  27. public void run() {
  28. synchronized (StateTest.class) {
  29. try {
  30. Thread.sleep(1000000); // timed_waiting
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. };
  37. t4.start();
  38. Thread t5 = new Thread("t5") {
  39. @Override
  40. public void run() {
  41. try {
  42. t2.join(); // waiting
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. }
  46. }
  47. };
  48. t5.start();
  49. Thread t6 = new Thread("t6") {
  50. @Override
  51. public void run() {
  52. synchronized (StateTest.class) { // blocked
  53. try {
  54. Thread.sleep(1000000);
  55. } catch (InterruptedException e) {
  56. e.printStackTrace();
  57. }
  58. }
  59. }
  60. };
  61. t6.start();
  62. try {
  63. Thread.sleep(500);
  64. } catch (InterruptedException e) {
  65. e.printStackTrace();
  66. }
  67. log.debug("t1 state {}", t1.getState());
  68. log.debug("t2 state {}", t2.getState());
  69. log.debug("t3 state {}", t3.getState());
  70. log.debug("t4 state {}", t4.getState());
  71. log.debug("t5 state {}", t5.getState());
  72. log.debug("t6 state {}", t6.getState());
  73. System.in.read();
  74. }
  75. }
  1. 13:42:08.467 [t3] DEBUG c.StateTest - running...
  2. 13:42:08.970 [main] DEBUG c.StateTest - t1 state NEW
  3. 13:42:08.973 [main] DEBUG c.StateTest - t2 state RUNNABLE
  4. 13:42:08.973 [main] DEBUG c.StateTest - t3 state TERMINATED
  5. 13:42:08.973 [main] DEBUG c.StateTest - t4 state TIMED_WAITING
  6. 13:42:08.973 [main] DEBUG c.StateTest - t5 state WAITING
  7. 13:42:08.973 [main] DEBUG c.StateTest - t6 state BLOCKED

t1是新建的一个线程,还没有开始执行;t2是正在执行的线程,由于有个死循环,所以他会一直执行;t3就执行了一次,由于主线睡了0.5秒,这个时候t3已经执行完了;t4睡的时间比主线程长;t5在等待t2执行完才能执行,且没有时间限制;t4线程加了锁,t6也对同一个加锁,等待t4释放锁。

3.结论

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