Java线程(二):线程特点、状态、终止开始控制(

hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹

在Java中,线程的知识非常多,博主预计写九篇博客左右,来解析线程知识,这篇博客是线程部分的第二篇,这篇博客主要解析藏线程的特点,线程的运行和终止控制,线程的状态几方面来进一步解析线程,使用jdk监视台来查看线程状态的代码在上篇线程博客中有提到,还不会的铁汁们可以看下第一篇线程博客
🎆个人主页:洋不写bug的博客
🎆所属专栏:JavaEE学习
🎆铁汁们对于JavaEE的各种常用核心语法,都可以在上面的前端专栏学习,专栏正在持续更新中🏀,有问题可以写在评论区或者私信我哦~

1,线程命名

如果创建了很多线程,每个线程有不同的任务,这时候在线程显示工具看(具体怎么使用上篇博客有提到),就是thread-0,thread-1,thread-2...就很容易分不清楚

线程中有很多属性,就有name这个属性,我们就可以给线程命名,这个命名可以是中文,也可以是英文
使用最简单的lambda方法来创建线程,在lambda方法最后加上名字,这里给这个线程命名为"我的线程",代码如下所示:

java 复制代码
public class Demo05 {
    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) {
                     e.printStackTrace();
                }
            }
    },"我的线程");
        t.start();
       
}
}

这时候在监视平台上就能看到我们创建的这个线程(具体方法在上篇线程入门博客中有提到)

不知道铁汁们有没有发现一个问题,监视平台中没有主线程,只有我们创建的这个线程,那这是为什么呢?
这是因为start方法执行完,创建完新的线程,main方法就自动结束了,那对应的主线程就会自动销毁,想要主线程不销毁,只需要在mian方法中写一个无限循环即可

如下所示:

java 复制代码
public class Demo05 {
    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) {
                     e.printStackTrace();
                }
            }
    },"我的线程");
        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
}
}

2,前台和后台线程

线程有几个常见的属性,如下图:

之前进程博客中提到的:进程状态,进程优先级,进程上下文,进程的记账信息...,这些东西其实准确来说,都是线程中的

前台线程和后台线程的概念也很简单:

  • 前台线程:线程没运行完,进程就不会结束(线程能够阻止进程的结束)

  • 后台线程:线程没运行完,进程可以结束(线程不能阻止进程的结束),后台线程又称为守护线程,守护的英语就是daemon

  • 铁汁们一看,就知道这个前台线程比后台线程厉害

    在酒桌文化中,领导就类似于前台线程,其他人喝好了,领导还想继续喝,那酒局就不会结束🐵

    就拿刚刚的测试来举例子(如下图),这个进程中有这么多线程,只有我们程序的两个线程是前台线程,其他都是后台线程'

    在程序中可以使用isDaemon方法来看线程是前台线程还是后台线程,后台线程方法返回true,前台线程方法返回false,代码如下所示:

    sql 复制代码
            System.out.println("是否是后台线程" + t.isDaemon());

    当然,也可以把创建的线程设置为后台线程,使用setDaemon方法来创建,设置线程类型要写在创建线程之前(也就是写在start方法前面),如下所示:

    java 复制代码
    public class Demo05 {
        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) {
                         e.printStackTrace();
                    }
                }
        },"我的线程");
            t.setDaemon(true);
            t.start();
            System.out.println("是否是后台线程" + t.isDaemon());
            while (true){
                System.out.println("hello main");
                Thread.sleep(1000);
            }
    }
    }

    前台线程和后台线程的使用情况如下:

  • 如果一个线程的任务很重要,这个任务必须要做完的,那这个线程就是前台线程

  • 如果一个线程做的任务无关紧要/周期性无期限的执行,就应该设置为后台线程 就比如JVM的垃圾回收机制,python日志收集线程 ------ 这些线程不执行核心业务逻辑,只是默默为执行核心逻辑的用户线程 "保驾护航",因此后台线程又称为守护线程

3,线程运行特点

启动start的动作是非常快的,执行完start后,就会立刻向下执行,不会有任何的阻塞等待,如下所示:

java 复制代码
public class Demo_05 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            System.out.println("hello thread");
        });
        t.start();
        System.out.println("hello main");
    }
}

运行这个代码大部分情况下都是先打印main,但start之后,main线程和t线程是并发执行的,所以少部分情况下会先打印thread

另外,一个线程对象只能start一次,下面这段代码start两次,就会抛出非法线程状态异常,如下所示:

java 复制代码
public class Demo_05 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            System.out.println("hello thread");
        });
        t.start();
        t.start();
        System.out.println("hello main");
    }
}

第一次执行了start之后,线程就是就绪/阻塞状态了,start方法中会对线程状态进行了判断,如果线程是就绪/阻塞状态,就不能再start了

4,终止线程

正常情况下,一个线程需要把入口方法执行完,才能够使线程结束,但是有时候,我们会希望线程提前结束(尤其是线程在sleep中),就需要配合线程本身和代码来实现

最简单的就是搞个boolean变量,来控制线程的开始和终止

java 复制代码
public class Demo_06 {

    private static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while (flag){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        System.out.println("hello main");
        Thread.sleep(3000);
        flag = false;
        System.out.println("t线程结束");

    }
}

更多使用的还是直接使用线程内置的标志位 isInterruptted(),也就是在Thread对象中,包含了一个boolean变量,如果为false,说明没人去尝试终止这个线程,如果为true,说明有人尝试去终止
但是这里在lambda表达式的while中直接使用却会报错,因为这个lambda表达式的定义,是在new Thread之前的,所以这个t对象在while中调用的时候还没有被初始化呢,如下所示:

解决这个问题,就需要换种写法,使用Thread类的静态方法 currentThread()
哪个线程调用currentThread方法,这个方法就返回哪个线程的引用

线程对象还有个interrupt方法,调用后可以将线程的中断状态修改为true,进而while循环就会跳出,线程就会结束,终止线程经常会使用这样的办法,代码如下所示:

java 复制代码
package Thread;

public class Demo_07 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
            }
        });
        t.start();
        Thread.sleep(2000);
        t.interrupt();
    }
}

有的铁汁可能觉得刚刚打印的hello thread太多了,那我们在while方法中加个sleep,让线程1s打印一次,我们预测这里是打印大概三次hello thread,然后线程结束

但是运行代码却会报出异常,并且会无限的打印hello thead,这是为什么呢?

java 复制代码
package Thread;

public class Demo_07 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(2000);
        t.interrupt();
    }
}

在线程终止这里,有个这样的设定:

如果线程t正在sleep,调用interrupt方法后,sleep就会被提前唤醒,sleep提前唤醒之后,就会抛出异常 ,这里代码中捕获异常后,打印异常的信息

无限打印是因为当sleep方法被提前唤醒后,就会把isInterrupted标志位给重置为false,后面标志位一直为flase,while语句就会一直打印hello thread

有使用IDEA编程的铁汁,自动补全catch语句,那catch异常后就会抛出一个RuntimeException(e),这个RuntimeException(e)是非检查异常,也就是一旦抛出
程序就会终止,就会强制杀死线程,这种写法在Java中是非常不推荐的
,代码如下:

java 复制代码
package Thread;

public class Demo_07 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Thread.sleep(2000);
        t.interrupt();
    }
}

例如线程在操作数据库,如果在操作到一半的时候强行终止这个线程,那就会导致对数据库的修改只进行了一半,产生"脏数据"。

这个地方的异常处理部分也可以使用break来跳出while循环,进而结束线程,通常会在break前加善后程序,这就相当于善后完成后再终止线程,比直接强硬终止线程要好的多

java 复制代码
package Thread;

public class Demo_07 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //添加善后程序
                    break;
                }
            }
        });
        t.start();
        Thread.sleep(2000);
        t.interrupt();
    }
}

总结下,控制线程的结束就是通过interrupt方法,使用这个方法分为两种情况:

  1. t线程没有执行sleep等阻塞操作,t的isInterrupted()方法返回true,通过循环条件来结束t线程
  2. t线程中进行了sleep等阻塞操作,如果sleep被提前唤醒,就会抛出InterruptedException,同时也会把isInterrupted()的返回结果改为flase,我们catch异常后,决定执行怎样的操作

5,线程等待

在前面的线程运行特点中,提到了同时调用两个线程后,哪个先执行是不确定的(无法确定到底是先打印hello thread 还是hello main),这种随机的事情对于程序员写代码时不太好的,我们有时希望线程的执行顺序能够固定下来
这时候就可以使用线程等待操作,让一个线程先执行,一个线程后执行,后执行的线程必须等到先执行的线程结束后再执行
线程等待使用的就是join方法,join方法会造成阻塞(凡是会造成阻塞的都会抛出这个异常),抛出 InterruptedException ,需要处理一下

在t线程中每1s打印一次hello thread,打印5次,在t.start()后面加上个t.join()
执行到t.start时,t线程和主线程同时执行,接着主线程继续向下执行,执行到t.join()的时候,就需要阻塞等待t线程,等待t线程结束之后,主线程才能执行剩余的部分,这种情况就是main线程等待t线程

代码如下所示:

java 复制代码
public class Demo_07 {
    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) {
                    break;
                }
            }
        });
        t.start();
        System.out.println("主线程等待之前");
        t.join();
        System.out.println("主线程等待之后");
    }

}

当然,如果t线程早就已经结束了,那这个join方法就相当于没什么用了,例如延迟6s再执行main线程,如下所示:

java 复制代码
public class Demo_07 {
    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) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(6000);
        System.out.println("主线程等待之前");
        t.join();
        System.out.println("主线程等待之后");
    }
}

总结下

  • 哪个线程的引用调用join方法,哪个线程就是被等的一方,例如t线程调用join方法,那main线程就要到t线程执行结束,才能执行
  • 哪个线程中调用join,哪个线程就是等待的一方,在上面的代码中,join方法是在主线程中调用的,那主线程就需要等待

也可以让t线程等main线程,首先在main方法中用currentThread()方法拿到一个main线程的引用,接着在t线程中使用main线程的引用来调用join方法,这时候t线程就需要阻塞,等待main线程执行结束后,t线程再继续执行,代码如下所示

java 复制代码
public class Demo_08 {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();
        Thread t = new Thread(() ->{
            System.out.println("t线程等待之前");
            try {
                mainThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t线程等待之后");

        });
        t.start();
        for(int i = 0;i < 10;i++){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

join默认情况是死等,只要前一个线程结束,后一个线程就会一直等待,那如果第一个线程会一直循环,那第二个线程就一直不会执行,我们有时候并不想这样

因此,join还有重载版本,传入时间参数,指定等待的最大时间,超过这个时间,等待的线程就不等了,直接执行,在实际开发中,使用join基本上都是带着参数的
join含参重载有两个版本,一个是精确到ms,一个是精确到纳秒,基本上使用精确到ms的就行,精确到纳秒也不准,因为计算机本身调用线程就会有几ms的误差😂😅

可以通过时间戳来看下误差,这里的误差是2ms(不同的计算机误差也不同,有的误差会有10ms左右,都是正常的)

java 复制代码
public class Demo_08 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(1000);
        System.out.println(System.currentTimeMillis());
    }
}

含参数的join代码如下所示,这里主线程等待t线程最多就是3000ms,超过了主线程就开始往下执行

java 复制代码
public class Demo_07 {
    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) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        System.out.println("主线程等待之前");
        t.join(3000);
        System.out.println("主线程等待之后");
    }
}

6,线程的状态

线程的状态可以分为以下6种

  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了.

NEW状态就是Thread对象创建了,但是还没有start,可以用getState方法来查看线程的状态,代码如下所示:

java 复制代码
public class Demo_09 {
    public static void main(String[] args) {
        Thread  t = new Thread(()->{
            System.out.println("hello thread");
        });
        System.out.println(t.getState());
        t.start();
    }
}

TERMINATED就是线程的工作完成了,但是Thread对象还在(线程的生命周期和Thread对象的生命周期是不一样的),可以在main线程中使用join方法,这样就能保证后面对于t线程状态的查看是在t线程已经结束的情况下

java 复制代码
public class Demo_09 {
    public static void main(String[] args) throws InterruptedException {
        Thread  t = new Thread(()->{
            System.out.println("hello thread");
        });
        System.out.println(t.getState());
        t.start();
        t.join();
        System.out.println(t.getState());
    }
}

线程还有个方法叫做isAlive(),用来判断线程是否在存活状态,结果是true和false,NEW状态和TERMINATED状态一个还没开始执行,一个执行完了,它们都不处于存活状态

在六种状态中,只有NEW状态和TERMINATED状态的线程是非存活的,isAlive()方法的返回值是flase,其他都是true,测试代码如下所示:

java 复制代码
public class Demo_09 {
    public static void main(String[] args) throws InterruptedException {
        Thread  t = new Thread(()->{
            System.out.println("hello thread");
        });
        System.out.println(t.getState() + "\tisAlive()" + t.isAlive());
        t.start();
        t.join();
        System.out.println(t.getState() + "\tisAlive()" + t.isAlive());
    }
}

Runnable状态:就绪状态和正在执行的状态的结合

代码中只要不触发阻塞操作,执行的线程就都是Runnable状态,如下所示:

java 复制代码
public class Demo_09 {
    public static void main(String[] args) throws InterruptedException {
        Thread  t = new Thread(()->{
            System.out.println("hello thread");
        });
        t.start();
        System.out.println(t.getState());
    }
}

接下来就是三种阻塞状态,本篇博客中只解析TIMED_WAITING状态和WAITING状态,BlOCKED会在后面博客提到锁的时候再解析

  • TIMED_WAITING:有超时时间的阻塞,如Thread.sleep(时长)、join(时长)
  • WAITING:无超时时间的阻塞,如join()、wait()(无参)
  • BlOCKED:加锁产生的阻塞(这个阻塞会在后面博客中解析)

执行下面这段程序,结果显示t的状态就是TIMED_WAITING(程序判断阻塞只看有没有超时时间的阻塞,这里程序就是根据线程t中的sleep加了超时时间,并不会看里面的while循环)

java 复制代码
public class Demo_09 {
    public static void main(String[] args) throws InterruptedException {
        Thread  t = new Thread(()->{
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
        t.join();
    }
}

这段程序中,main线程的状态就是WAITING状态(因为t.join并没有加超时时间,只要t线程不结束,main线程就要一直等待),只是不好加上打印代码来看,这时候就可以把代码运行上,通过监视平台来看,Thread-0(t)线程的状态是TIMED_WAITING,main线程的状态是WAITING

结语💕💕

线程的第一篇和第二篇解析博客,都是线程中最最基础的内容,我们之所以学习线程的状态,是为了以后在程序出现bug的时候方便调试,现在AI编程大大提高了我们的编程效率,我们写代码的时间减少了,调试代码的时间增加了
AI对于复杂程序bug的调试,迄今为止 AI 不能直接 "自动调用" 本地 / 远程的调试器(如 Java 的 jdb、IDEA 调试器) 主动调试代码,只能通过静态的分析代码,因此一些项目的程序调试还是要靠我们自己

以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

相关推荐
ZTLJQ2 小时前
挖掘金矿:Python数据解析库完全解析
开发语言·python
山上三树2 小时前
C++ 回调函数(Callback Function)详解
开发语言·c++
lay_liu2 小时前
QoS质量配置
开发语言·智能路由器·php
sonnet-10292 小时前
拓扑排序的实现
java·c语言·开发语言·笔记·算法
SuperEugene2 小时前
Vue3 Pinia 状态管理规范:何时用 Pinia 何时用本地状态|状态管理与路由规范篇
开发语言·前端·javascript·vue.js·前端框架
ONE_SIX_MIX2 小时前
lancedb 表名 编解码 与 转译 python
开发语言·python
Rabbit_QL2 小时前
【Claude Code 循环登录】浏览器显示成功,CLI 永远 Not logged in
开发语言
C++ 老炮儿的技术栈2 小时前
Qt 开发机器人客户端程序
c语言·开发语言·c++·windows·qt·机器人