• 【JavaEE初阶】多线程 _ 基础篇 _ Thread类的使用、线程的几个重要操作和状态


    ☕导航小助手☕

        🍱写在前面

            🧇一、Thread类的常见构造方法

            🍚二、Thread 的几个常见属性

            🍛三、和线程相关的几个重要的操作

                        🍞🍞3.1 启动线程 - start()

                        🍣🍣3.2 中断线程

                        🍤🍤3.3 等待线程 - join()

                        🥩🥩3.4 获取到线程引用

                        🧀🧀3.5 休眠线程 - sleep()

            🍜四、线程的状态

                        🍰🍰4.1 Java 线程中的基本状态

                        🥡🥡4.2 线程之间的状态是如何转换的


    写在前面

    这篇博客,仍然来介绍关于多线程基础篇的知识~

    其主要介绍的内容是:Thread类 的常见构造方法和属性、和线程相关的几个重要操作 以及 Java 线程中的几种状态~

    下面,正文开始 ......

     

    一、Thread类的常见构造方法

    构造方法说明
    Thread()创建线程对象
    Thread(Runnable target)使用 Runnable对象 创建线程
    Thread(String name)创建线程对象,并命名
    Thread(Runnable target)使用 Runnable对象 创建线程,并命名
    【了解】Thread(ThreadGroup group,Runnable target)
    线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可

    Thread():

    默认无参构造方法,如 :Thread t1 = new Thread();


    Thread(Runnable target):

    使用 Runnable 创建一个任务,再把 任务 放到线程里面,

    如 Thread t2 = new Thread(new MyThread());


    Thread(String name):

    给线程起一个名字,线程 在操作系统内核里 没有名字,只有一个身份标识~

    但是 Java中 为了让程序猿 调试的时候方便理解,这个线程是谁~

    就在 JVM 里给对应的 Thread对象 加了个名字(JVM中的 Thread对象 和 操作系统内核里面的线程 一一对应)~

    这个名字对于程序的执行没影响,但是对于程序猿调试来说还是挺有用的~

    如果不手动设置,也会有默认的名字,形如 Thread-0、Thread-1、......


    Thread(Runnable target):

    使用 Runnable对象 创建线程,并命名:

    1. package thread;
    2. public class Demo7 {
    3. public static void main(String[] args) {
    4. Thread t = new Thread(new Runnable() {
    5. @Override
    6. public void run() {
    7. while (true) {
    8. System.out.println("hello thread");
    9. try {
    10. Thread.sleep(1000);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. }
    15. }
    16. },("我的线程"));
    17. t.start();
    18. while(true){
    19. System.out.println("hello main");
    20. try {
    21. Thread.sleep(1000);
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. }
    25. }
    26. }
    27. }

    程序运行,我们可以找到 jconsole.exe 去看一看:

     

    二、Thread 的几个常见属性

    属性获取方法
    IDgetId()
    名称getName()
    状态getState()
    优先级getPriority()
    是否后台进程isDaemon()
    是否存活isAlive()
    是否被中断isInterrupted()

    getId():获取到的是 线程在 JVM 中的身份标识~

    线程中的身份标识是有好几个的:内核的 PCB 上有标识;到了用户态线程库里 也有标识(pthread,操作系统提供的线程库);到了 JVM 里又有一个标识(JVM Thread类 底层也是调用操作系统的 pthread 库) ~

    三个标识各不相同,但是目的都是作为身份的区分~

    由于 介绍的是 Java程序,所以我们只需要知道 JVM 中的身份标识即可~


    getName():

    在 Thread 构造方法里,自己所起的名字~


    getState():

    PCB 里面有状态,此处得到的状态是 JVM 里面设立的状态体系,这个状态比操作系统内置的状态要更丰富一些~


    getPriority():

    获取到优先级~


    isDaemon():

    daemon 称为 "守护线程",也可以理解为 "后台线程"~

    类似于 手机app,打开一个app,此时这个app就来到了 "前台",当按到 "回到菜单" 这样的按钮,此时app就切换到 "后台"~
    线程也分成 前台线程 和 后台线程(这个是可以自己来设置的),创建线程出来默认为是 前台线程,前台线程 会阻止进程结束;换句话说,进程会保证所有的前台线程都执行完了 才会退出~

    当然,main 这个线程就是一个前台线程~

    后台线程不会阻止进程结束,所以 进程退出的时候 不关后台进程是否执行完 就退出了~

    如:

    1. package thread;
    2. public class Demo7 {
    3. public static void main(String[] args) {
    4. Thread t = new Thread(new Runnable() {
    5. @Override
    6. public void run() {
    7. while (true) {
    8. System.out.println("hello thread");
    9. try {
    10. Thread.sleep(1000);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. }
    15. }
    16. },("我的线程"));
    17. t.start();
    18. }
    19. }

    执行结果:

    分析:

    由于该线程是一个前台线程,所以需要等待 其运行结束,进程才会结束,所以会一直执行下去~

    再如:我们可以把他手动设置成 后台线程:

    1. package thread;
    2. public class Demo7 {
    3. public static void main(String[] args) {
    4. Thread t = new Thread(new Runnable() {
    5. @Override
    6. public void run() {
    7. while (true) {
    8. System.out.println("hello thread");
    9. try {
    10. Thread.sleep(1000);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. }
    15. }
    16. },("我的线程"));
    17. //手动设置成 后台线程
    18. t.setDaemon(true);
    19. t.start();
    20. }
    21. }

    运行结果:

    分析:

    通过 setDaemon(true) 可以把线程设置为后台线程,等到主线程执行完,进程就结束了~

    需要注意的是,先要 设置线程,然后再启动线程~


    示例:

    1. package thread;
    2. public class Demo8 {
    3. public static void main(String[] args) {
    4. Thread t = new Thread(() ->{
    5. while (true) {
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. },"我的线程");
    13. t.start();
    14. System.out.println("线程Id:" + t.getId());
    15. System.out.println("线程名称:" + t.getName());
    16. System.out.println("线程状态:" + t.getState());
    17. System.out.println("线程优先级:" + t.getPriority());
    18. System.out.println("是否后台线程(true/false):" + t.isDaemon());
    19. System.out.println("是否存活:" + t.isAlive());
    20. }
    21. }

     运行结果:

    这些操作都是获取到的是 一瞬间的状态,而不是持续性的状态~ 

     

    三、和线程相关的几个重要的操作

    3.1 启动线程 - start()

    创建 Thread实例,并没有真的在操作系统内核里创建出线程(仅仅只是在安排任务,"跑步时 的 '各就各位,预备' ")!!!

    而是 调用 start,才是真正创建出线程("发令枪响")!!!

    这个在上面和上一篇的博客也提到过,这里就不做过多演示了~

     

    3.2 中断线程

    怎么让线程执行结束,其实方法很简单:只要让线程的 入口方法 执行完了,线程就随之结束了~

    主线程 的入口方法,就可以视为 mian方法~

    入口方法:其实就是 上一篇博客所介绍的 创建线程 的代码~

    🚪上一篇博客的传送门🚪

    所谓的 "中断线程",就是让线程尽快把 入口方法执行结束~


    方法一:直接自己定义 标志位 来区分线程是否要结束~

    1. package thread;
    2. public class Demo9 {
    3. //用一个布尔变量来表示 线程是否要结束
    4. //这个变量是一个成员变量,而不是局部变量
    5. private static boolean isQuit = false;
    6. public static void main(String[] args) throws InterruptedException {
    7. Thread t = new Thread(() -> {
    8. while (!isQuit) {
    9. System.out.println("线程运行中......");
    10. try {
    11. Thread.sleep(1000);
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. }
    16. System.out.println("新线程执行结束!");
    17. });
    18. t.start();
    19. Thread.sleep(5000);
    20. System.out.println("控制新线程退出!");
    21. isQuit = true;
    22. }
    23. }

    运行结果:

    这个代码中,控制线程结束,主要是这个线程里有一个循环,这个循环执行完,就算结束了~

    很多时候创建线程都是 让线程完成一些比较复杂的任务,往往都有一些循环,正是因为有循环,执行的时间才可能比较长;如果线程本身执行的很快,刷的一下就结束了,那么也就没有提前控制它的必要了~ 


    方法二:使用 Thread类 中自带的标志位~ 

    这种方法是可行的~

    Thread 其实内置了一个标志位,不需要咱们去手动创建标志位

    1. Thread.currentThread().isInterrupted()
    2. --currentThread() 是 Thread类的静态方法,获取到当前线程的实例,这个方法中有一个线程会调用它
    3. --线程1 调用这个方法,就能返回线程1 的 Thread对象;
    4. --线程2 调用这个方法,就能返回线程2 的 Thread对象~
    5. --类似于 JavaSE 里面的 this~
    6. --isInterrupted() 为判定内置的标志,可以获取到标志位的值,为 true 表示线程要被中断~
    1. package thread;
    2. public class Demo10 {
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t = new Thread(() ->{
    5. while (!Thread.currentThread().isInterrupted()) {
    6. System.out.println("线程运行中......");
    7. try {
    8. Thread.sleep(1000);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. });
    14. t.start();
    15. Thread.sleep(5000);
    16. System.out.println("控制新线程退出!");
    17. t.interrupt();
    18. }
    19. }

    运行结果:

    说明:

    调用了 interrupt,产生了一个异常~

    异常虽然出现了,但是线程仍然在继续运行~ 

    注意理解 interrupt方法 的行为:

    1. 如果 t 线程 没有处在阻塞状态,此时 interrupt 就会修改内置的标志位~
    2. 如果 t 线程 正在处于阻塞状态,此时 interrupt 就让线程内部产生阻塞的方法,例如 sleep 抛出异常~

    上述循环代码中,正好异常被捕获了~

    而捕获之后,啥也没有干 只是打印了一个调用栈:

      

    而如果 把上述代码中的  e.printStackTrace(); 给注释掉,那么就啥也不打印,运行结果调用栈也不打印了,直接忽略异常了~

    正是因为这样的捕获操作,程序员就可以自行控制线程的退出行为了:

    即 在里面可以自主的微操作了~

     


    当然,除了 Thread.currentThread().isInterrupted(),Thread类还自带了一个静态方法 interrupted() 也可以访问标志位~

    使用 Thread.interrupted() 即可~

    1. 如果当前线程处于运行的状态,就是修改标志位
    2. 如果当前线程处于阻塞的状态,则是触发一个异常,线程内部就会通过这个异常被唤醒

    这里也不做过多叙述了,一般掌握一个就可以了~

     

    3.3 等待线程 - join()

    我们已经知道,线程之间的执行顺序完全是随机的,看系统的调度~

    一般情况下写代码,其实是比较讨厌这种随机性的,更需要能够让顺序给确定下来~

    join() 就是一种确定线程执行顺序的 辅助手段~

    如:咱们不可以确定两个线程的开始执行顺序,但是可以通过 join() 来控制两个线程的结束顺序~

    如:上一篇博客曾举了一个例子:

    1. private static void concurrency() {
    2. //concurrency 的意思是 "并发"
    3. long begin = System.currentTimeMillis();
    4. Thread t1 = new Thread(() ->{
    5. int a = 0;
    6. for(long i = 0; i < count; i++) {
    7. a++;
    8. }
    9. });
    10. Thread t2 = new Thread(() ->{
    11. int a = 0;
    12. for(long i = 0; i < count; i++) {
    13. a++;
    14. }
    15. });
    16. t1.start();
    17. t2.start();
    18. try {
    19. t1.join();
    20. t2.join();
    21. } catch (InterruptedException e) {
    22. e.printStackTrace();
    23. }
    24. long end = System.currentTimeMillis();
    25. System.out.println("多线程并发执行的时间:" + (end-begin) + "毫秒");
    26. }

    我们知道,main、t1、t2 三个线程是随机调度执行的~

    但是此处的需求是,希望让 t1、t2 都执行完了之后,main再进行计时,来统计执行时间~

    就是希望 t1、t2 先执行完,main 后执行完~

    咱们无法干预 调度器的行为(调度器还是该咋随机咋随机),但是可以让 main 线程进行等待~

    就在 main 里分别调用了 t1.join() 和 t2.join() ~

    t1.join() —> main阻塞,等待 t1 执行完~

    t2.join() —> main阻塞,等待 t2 执行完~ 

    当 t1、t2 都执行完了以后,main 解除阻塞,然后才能继续往下执行,才能执行完~

    main 阻塞了,就不参与调度了,但是 t1、t2 仍然参与调度,它们的执行还是会 随机调度、交替执行的~ 


    main 有点特殊,不太方便 join()~

    一般情况下,想让谁阻塞,谁就调用 join() 即可 ~

    如:要实现让 t2 等待 t1,main 等待 t2,就可以 main 去调用 t2.join(),t2 调用 t1.join() 即可~

    如:

    1. package thread;
    2. public class Demo11 {
    3. private static Thread t1 = null;
    4. private static Thread t2 = null;
    5. public static void main(String[] args) throws InterruptedException {
    6. System.out.println("main begin");
    7. t1 = new Thread(() -> {
    8. System.out.println("t1 begin");
    9. try {
    10. Thread.sleep(3000);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. System.out.println("t1 end");
    15. });
    16. t1.start();
    17. t2 = new Thread(() -> {
    18. System.out.println("t2 begin");
    19. try {
    20. t1.join();
    21. } catch (InterruptedException e) {
    22. e.printStackTrace();
    23. }
    24. System.out.println("t2 end");
    25. });
    26. t2.start();
    27. t2.join();
    28. System.out.println("main end");
    29. }
    30. }

    运行结果:


    如:

    1. package thread;
    2. //控制 main 先运行 t1,t1 执行完 再执行 t2
    3. public class Demo12 {
    4. public static void main(String[] args) throws InterruptedException {
    5. System.out.println("main begin");
    6. Thread t1 = new Thread(() -> {
    7. System.out.println("t1 begin");
    8. try {
    9. Thread.sleep(1000);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. System.out.println("t1 end");
    14. });
    15. t1.start();
    16. //等待 t1 结束
    17. t1.join();
    18. Thread t2 = new Thread(() -> {
    19. System.out.println("t2 begin");
    20. try {
    21. Thread.sleep(1000);
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. }
    25. System.out.println("t2 end");
    26. });
    27. t2.start();
    28. t2.join();
    29. System.out.println("main end");
    30. }
    31. }

    运行结果:


    join() 的行为:

    1. 如果被等待的线程 还没执行完,就阻塞等待~
    2. 如果被等待的线程 已经执行完了,就直接返回~

    当然,join() 还有其他的版本~

    方法说明
    public void join()
    等待线程结束(死等)
    public void join(long millis)
    等待线程结束,最多等 millis 毫秒(设定了等待的最大时间)
    public void join(long millis, int nanos)
    同理,但可以更高精度(设定了等待的最大时间)

    在实际的开发过程中,在一般情况下,都不会使用 "死等" 的方式~

    因为 "死等" 的方式有风险~

    万一代码出了 bug 没控制好,就很容易让服务器 "卡死",无法继续工作~

    更多的情况下是 等待的时候预期好最多等多久,超过时间了就需要做出一些措施~

    3.4 获取到线程引用

    为了对线程进行操作,就需要获取到线程的引用~

    这里的操作,就是指:线程等待、线程中断、获取各种线程的属性~

    如果是 继承 Thread类,重写 run方法,可以直接在 run 中使用 this 即可获取到线程的实例~

    但是如果是 Runnable 或者 Lambda,this 就不行了(指向的就不是 Thread实例)~

    更通用的方法是,使用 Thread.currentThread() ,currentThread() 是一个静态方法,其特点是 哪个线程来调用这个方法,得到的线程就是哪个实例~

    3.5 休眠线程 - sleep()

    sleep() 能够让线程休眠一会儿~

    前面已经所介绍了 sleep() 的使用方法,现在来画图介绍一下 sleep() 到底是如何使用的~


     注意:实际上应该是 双向链表 连接,不过为了简单,所以画的就是 单向链表 了~

    注意:

    如果写了一个 sleep(1000),那么也不一定 就会在1000ms 之后就上 CPU 运行~

    1000ms 之后只是把这个 PCB 放回了就绪队列!!!至于说这个线程啥时候执行,还得看调度器的心情~

    因此,sleep(1000) 意味着休眠时间不小于 1000ms,实际的休眠时间会略大于 1000ms,这个误差精度在 10ms 以下~ 

    四、线程的状态

    4.1 Java 线程中的基本状态

    操作系统中 进程的状态 有 阻塞状态、就绪状态、执行状态~

    而在 Java/JVM里 线程中也有一些状态,更是对此做出了一些细分~

    New:安排了工作,还未开始行动,创建了 Thread对象,但是还没有调用 start方法,系统内核里面没有线程~

    Runnable:就绪状态,包含了两个意思:

    1. 正在 CPU上运行
    2. 还没有在 CPU 上运行,但是已经准备好了

    Blocked:等待锁~

    Waiting:线程中调用了 wait方法~

    Time Waiting:线程中通过 sleep方法 进入的阻塞~

    Terminated:工作完成了,系统里面的线程已经执行完毕,销毁了(相当于线程的 run方法 执行完了),但是 Thread对象 还在~

     Blocked、Waiting、Time Waiting 三种状态都是 阻塞状态,只不过是产生阻塞状态的原因不一样,Java里面使用不同的状态来表示了~


    1. package thread;
    2. public class Demo13 {
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t = new Thread(() -> {
    5. System.out.println("hello thread");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. });
    12. //在 t.start() 之前获取,获取到的是 线程还未创建的状态
    13. System.out.println(t.getState());
    14. t.start();
    15. t.join();
    16. //在 t.join() 之后获取,获取到的是线程已经结束之后的状态
    17. System.out.println(t.getState());
    18. }
    19. }

     

    运行结果:

     

     

     

    4.2 线程之间的状态是如何转换的

    主干道是:New => Runnable => Terminated ~

    在 Runnable状态 会根据特定的代码进入支线任务,这些 "支线任务" 都是 "阻塞状态"~

    这三种 "阻塞状态",进入的方式不一样,同时阻塞的时间也不同,被唤醒的方式也不同~

    如:sleep() 等到时间到了自动唤醒,至于 wait() 和 synchronized() 是如何唤醒的以后会介绍的~

    这一篇的博客就到此为止了,下一篇博客将会介绍到 多线程安全的问题 ~

    如果感觉这一篇博客对你有帮助的话,可以一键三连走一波,非常非常感谢啦 ~

     

     

     

  • 相关阅读:
    SR和GBN的区别
    基于java+SpringBoot+HTML+Mysql传统工艺品销售网站
    接口测试方法论——WebSocket一点通
    预处理详解
    Python-爬虫 (BS4数据解析)
    Android 多平台AR SDK 集成使用
    chatGPT的前世今生
    时序预测 | MATLAB实现BiLSTM时间序列未来多步预测
    多元函数的二阶泰勒展开推导
    SpringBoot实现分页查询
  • 原文地址:https://blog.csdn.net/qq_53362595/article/details/126197053