三、Thread类

1.常用构造方法

  • 代码演示Demo6:自定义线程名称
    package thread;
    
    public class Demo6 {
        public static void main(String[] args) {
            Thread t = new Thread(() -> {
                while(true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            },"自定义线程");
    
            t.start();
    
        }
    }
    
    • Q1:使用jconsole查看有哪些线程的时候为啥没有main线程
      • 首先mian线程是一定存在的
      • 只是main线程执行完了,t方法执行结束,也查看不到t线程
    • Q2:main线程需要重写run方法吗
      • 不需要,但是main线程是JVM中通过C++代码创建出来的,没有通过Thread类创建,也就不会重写run方法了

2.Thread的⼏个常⻅属性

  • ID:和PCB的id不一样
  • 状态:(重点:就绪 + 阻塞)
  • 优先级:仅供参考,还是系统调度决定的
  • 是否是后台线程(守护线程)
    • 前台线程:这样的线程如果不运行的结束的话,Java进程不一定会结束(大佬)
      后台线程:所有非后台线程结束,Java进程才结束(小透明)
    • Java线程默认都是前台线程,setDaemon方法可以把线程设置为后台线程
      • 代码示例Demo7
        package thread;
        
        public class Demo7 {
            public static void main(String[] args) {
                Thread t = new Thread(() -> {
                    for (int i = 0; i < 5; i++) {
                        System.out.println("hello thread");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
        
                //设置为后台线程
                t.setDaemon(true);
        
                t.start();
            }
        }
        
      • Q1:为什么啥什么都没打印/只打印了一次
        进程中,只有main是前台线程,main一结束,整个进程就结束了,t没打印或者才打印一个(抢占式执行),整个进程就结束了(最后一个前台线程结束,那么进程也结束)
      • Q2:如果start和setDaemon交换顺序,结果会怎样
        会报错,关于线程的各种属性设置,都得放到start之前,一旦线程启动,再设置就来不及了
    • 后台线程举例:gc线程,gc要周期性持续性执行,找垃圾是否需要回收,不可能主动结束
  • 5)内核线程PCB是否存活
    • Thread对象的生命周期和PCB的声明周期是不完全一样的
      PCB是内核中线程的体现;Java中通过Thread对象抽象的表示线程的概念

      • 创建过程:
        • 这个代码Thread对象(已经创建了Thread实例)已经出现,但是PCB还没出现

          start才真正创建线程,PCB才出现

      • 消耗过程:
        • 线程(PCB销毁)先结束,但是Thread还在

          由于t线程瞬间结束,线程和PCB先被消毁,但是在sleep结束之前,t引用指向的Thread对象,仍是存在的

      • PCB还存活,但是Thread对象以及被回收了

        线程还没结束,t指向的对象就被GC回收了


 3.Thread核心操作

  • start方法
    • 作用:启动线程,核心就是是否真的创建线程出来,每个线程都是独立调度执行的(相当于整个程序中多了一个执行流)
    • 注意:一个Thread对象,只能start一次
      要想再搞一个新的线程,就需要创建另一个Thread对象
  • 中断线程
    • 作用:其他线程只能提醒一下线程t是不是要终止,至于要不要终止,还是得t线程自己决定(t线程正在执行时)
      因为线程的调度是随机的,如果线程t正在做一个很重要的任务,强制终止可能会出bug
    • 实现方式:
      • (1)自己实现控制线程结束的代码
        核心思路:需要终止线程的入口方法尽快执行结束(跳出循环/return都可以)
package thread;

public class Demo10 {

    private static boolean isRunning = true;

    public static void main(String[] args) throws InterruptedException {
        //boolean isRunning = true;
        Thread t = new Thread(() -> {
            while (isRunning) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t线程 已经结束");
        });

        t.start();

        Thread.sleep(3000);

        //3s之后,主线程修改isRunning的值,从而通过t结束
        System.out.println("控制t 线程结束");
        isRunning = false;
    }
}
        • 最终决定权在t代码手里
        • Q1:把boolean isRunning = true;放到main函数内部会不会报错
        • 编译阶段就过不了:变量捕获:作为lambda表达式/匿名内部类,都能捕获到外面一层作用域中的变量名,就可以使用的(为了保证数据的一致性和避免数据竞争问题,Java 要求这些变量是有效最终变量,即final/”事实“final)
      • (2)Thread提供的interrupt/isInterruptted方法
        实际上就是Thread里内置了一个标志位,功能更强大

        • 引入:Demo10如果t线程sleep10s,main线程就无法
        • 代码示例:Demo11:

          • Thread.currentThread()方法:获取到当前线程(在哪个线程代码中用这个方法就是当前线程),获取t这个引用
            lambda表达式早就创建好了,所以t不能在lambda内部使用,编译不了
          • isInterrupted()方法:
            true:线程要终止;false:线程要继续执行
          • t.interrupt()方法
            • (1)设置标志位为true
              提醒线程要结束了
            • (2)唤醒sleep阻塞方法
              跟自己实现终止不同,interrupt可以让sleep抛出InterruptedException异常,不会再等待,立即唤醒
            • Q1:为什么打印完异常信息,t线程还在继续执行
              sleep在搞鬼:当有sleep+触发interrupt的时候线程正在sleep,那么线程被唤醒的同时,就会清除标志位(又变成了false)

              • 为什么sleep要清除标志位?
                目的就是把控制权转交给程序员自己
  • join方法:线程等待
    • 引入:为啥要线程等待:为了确定线程结束的先后顺序
      由于抢占式执行,哪个线程被调度不确定,多个线程谁先结束也不确定
    • 作用:main线程中调用t.join就是让main等待t【t先结束,main后结束】(只是结束的时候等,开始不用等)
      join是不会影响谁先开始的,而且main线程不能被join

      • eg:代码中有main线程,又有t1和t2,此时main调用t1.join,main阻塞,t1和t2都正常执行;如果是t1调用t2.join,就是t1阻塞,t2和main正常执行
        在哪个线程中调用join,谁就阻塞
    • 代码示例:Demo12
      package thread;
      
      public class Demo12 {
          public static void main(String[] args) throws InterruptedException {
              Thread t = new Thread(() -> {
                  for (int i = 0; i < 5; i++) {
                      System.out.println("hello thread");
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                  }
                  System.out.println("thread end");
              });
      
              t.start();
      
              for (int i = 0; i < 5; i++) {
                  System.out.println("hello main");
                  Thread.sleep(1000);
              }
              t.join();
              System.out.println("main end");
          }
      }
      
    • join的情况(t1.join得在t1.start之后)
      main调用join,t线程的情况下

      • (1)如果线程t已经结束,join就会立即返回
      • (2)如果t线程还没结束,join就会阻塞等待,一直等到t线程结束之后,join才能解除阻塞,继续执行
        阻塞:该线程暂时不参与CPU调度
    • 代码示例:Demo13主线程调用多个join
      package thread;
      
      public class Demo13 {
          public static void main(String[] args) throws InterruptedException {
              Thread t2 = new Thread(() -> {
                  for (int i = 0; i < 4; i++) { System.out.println("t2"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); Thread t1 = new Thread(() -> {
                  //t1一进来,就先等t2结束
                  try {
                      //这里加sleep->保证t2已经start
                      Thread.sleep(1000);
                      t2.join();
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
                  for (int i = 0; i < 3; i++) {
                      System.out.println("t1");
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                  }
              });
      
      
      
              t1.start();
              t2.start();
      
              t1.join();
              t2.join();
              System.out.println("main end");
          }
      }
      
      • 先执行t1.join,如果t1还没结束,main就等t1结束(此时和t2没有关系),t1结束后,main执行t2.join,main再等待t2结束
        其实两个join顺序无所谓,都是main等待t1+t2时间
    • 【主流方式】join(时间):加入等待的最大时间,超过这个时间,就不等了
      防止出现等待的线程挂了,出现死等的情况

      • 代码示例:Demo14
        package thread;
        
        public class Demo14 {
            public static void main(String[] args) throws InterruptedException {
                Thread t = Thread.currentThread();
                System.out.println(t.getName());
        
                Thread t2 = new Thread(() -> {
                    try {
                        t.join();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("t2结束");
                });
        
                t2.start();
        
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(1000);
                }
        
                System.out.println("main结束");
            }
        }
        

        main等3s就不等了(结果还是t最后一个结束)

  • currentThread方法
    • 获取到当前线程(在哪个线程代码中用这个方法就是当前线程),获取t这个引用
    • static方法:不需要对象,类直接调用
  • sleep方法
    • 让当前线程主动进入“阻塞”状态,主动放弃在CPU上的执行,时间到了,才会解除,重新被调度到CPU上执行
      native方法:本地方法,JVM实现,通过C++代码实现的
    • 注意:sleep控制的是”线程休眠的时间“,而不是”两个代码之间的间隔时间“
      • sleep设置的时间,是线程阻塞的时间:1000ms之内,线程一定不会去CPU上执行,当时间到了,线程从阻塞状态恢复到就绪状态,不代表线程立即就能去CPU上执行

    • interrupt中断会清除标记位

4.🔥【面试题】线程的状态(PCB的状态)

  • NEW
    Thread对象有了,还没调用start,系统内部的线程还没创建

    • 代码示例:Demo17
      package thread;
      
      public class Demo17 {
          public static void main(String[] args) throws InterruptedException {
              Thread t = new Thread(() -> {
                  System.out.println("hello");
              });
              System.out.println(t.getState());
              t.start();
              //使用等待,确保t执行完毕,再来获取t的状态
              t.join();
              System.out.println(t.getState());
          }
      }
      
  • TERMINATED
    线程已经终止,内核中线程已经销毁,但是Thread对象还在

  • RUNNABLE
    就绪状态:这个线程”随叫随到“

    • (1)这个线程正在CPU上执行
    • (2)这个线程虽然没在CPU上执行,但是随时可以调度到CPU上执行
  • 阻塞
    • WAITING
      死等 进入的阻塞

      • join无参数版本就是
      • 代码示例:Demo18
        package thread;
        
        public class Demo18 {
            public static void main(String[] args) throws InterruptedException {
                Thread t = new Thread(() -> {
                    while (true) {
                        System.out.println("hello thread");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
                t.start();
                //死等版本的join
                //t.join();
                //带有时间的等
                t.join(3000);
            }
        }
        
    • TIMED_WAITING
      带有超时时间的等

      • join有参数版等待的哪个线程的状态
      • 拓展:后续发现某个线程卡死了,就需要关注线程的状态,看线程是到哪一行卡住了(阻塞)
    • BLOCKED
      进行锁竞争的时候产生的阻塞
  • 线程转换

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。