【JavaEE初阶】Thread 类及常见方法、线程的状态

目录

[1、Thread 类及常见方法](#1、Thread 类及常见方法)

[1.1 Thread 的常见构造方法](#1.1 Thread 的常见构造方法)

[1.2 Thread 的几个常见属性](#1.2 Thread 的几个常见属性)

[1.3 启动⼀个线程 - start()](#1.3 启动⼀个线程 - start())

[1.4 中断⼀个线程](#1.4 中断⼀个线程)

[1.5 等待⼀个线程 - join()](#1.5 等待⼀个线程 - join())

[1.6 获取当前线程引用](#1.6 获取当前线程引用)

[1.7 休眠当前线程](#1.7 休眠当前线程)

2、线程的状态

[2.1 观察线程的所有状态](#2.1 观察线程的所有状态)

[2.2 线程状态和状态转移的意义](#2.2 线程状态和状态转移的意义)

[2.3 观察线程的状态和转移](#2.3 观察线程的状态和转移)


1、Thread 类及常见方法

Thread 类是 JVM 用来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。
每个执行流,也需要有⼀个对象来描述,类似下图所示,Thread 类的对象 就是用来描述⼀个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

1.1 Thread 的常见构造方法

代码示例:

java 复制代码
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

1.2 Thread 的几个常见属性

  • ID 是线程的唯⼀标识,不同线程不会重复,是JVM自动分配的身份标识
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的⼀个情况,下面我们会进⼀步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。

补充:

前台线程的运行会阻止进程的结束;后台线程的运行不会阻止进程的结束。

咱们代码创建的线程,默认就是前台线程,会阻止进程的结束,只要前台线程没执行完,进程就不会结束,即使main已经执行完毕了。

代码举例:

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) {
        Thread t= new Thread(()->{
            for (int i = 0;i < 10;i++) {
                System.out.println("线程工作");
                try{
                    Thread.sleep(3000);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

分析这个代码,当执行到t.start() 时,会创建一个新的线程,新的线程去执行循环,而main线程继续自己的后续代码的执行,此时后面已没有代码,则main线程执行完毕,可以通过jonsole工具进行查看,如图:

按照我们之前的理解,main执行完毕,进程就应该结束,但是很明显,该进程依然继续执行,我们可以根据上述代码的运行结果来看:

若我们把t 线程设为后台线程,结果又是如何呢?

设为后台线程的代码:

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) {
        Thread t= new Thread(()->{
            for (int i = 0;i < 10;i++) {
                System.out.println("线程工作");
                try{
                    Thread.sleep(1000);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.setDaemon(true);
        t.start();
    }
}

这时的运行结果如图,可知当线程 t 设为后台线程就不会阻止进程的结束了,当main执行完毕,进程就直接结束了。

  • 是否存活,即简单的理解为 run 方法是否运行结束了

补充:

isAlive()该方法表示了内核中的线程(PCB)是否还存在。

java代码中定义的线程对象(Thread)实例,虽然表示一个线程,但这个对象本身的生命周期与内核中的线程PCB生命周期是不完全一样的。

  1. 当实例化完一个对象 t 时,此时 t 对象有了,但内核pcb还没有创建,所以此时isAlive()是false的。
  2. 当执行完t.start(),这时就真正在内核中创建出pcb,此时isAlive()的值就是true了。
  3. 当线程run执行完了,此时内核中的线程就结束了(内核pcb就释放了),但是t对象可能还存在,isAlive()的值仍是false。
  • 线程的中断问题,下面我们进⼀步说明

下面是代码示例:

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还活着");
                            Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName()
                + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName()
                + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName()
                + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName()
                + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName()
                + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName()
                + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {}
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
    }
}

运行结果:

1.3 启动⼀个线程 - start()

之前我们已经看到了如何通过覆写 run 方法创建⼀个线程对象,但线程对象被创建出来并不意味着线 程就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start()方法,就是喊⼀声:"行动起来!",线程才真正独立去执行了

调用 start 方法, 才真的在操作系统的底层创建出⼀个线程. 对于同一个Thread对象来说,start只能调用一次。

经典面试题:start 和 run 的区别

用一个代码来说明:

java 复制代码
class MyThread8 extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello");
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo8 {
    public static void main(String[] args) {
        Thread t = new MyThread8();
        //t.run();   //此时是main方法调用run,没有创建新线程,后续的循环执行不到
        t.start();  //会创建新的线程,新线程执行run的循环,主线程main继续后续代码
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

当创建一个Thread类对象 t 时:

由 t 调用run方法时( t.run() ),并没有创建出一个新的线程,这个操作还是在主线程main中进行的,循环打印hello,此时代码就只能停留在run的循环中了,下方main中的循环执行不到。

若由 t 调用start,这时会创建出一个新的线程,去执行run循环;main线程则继续执行自己的后续循环。

总结:

作用功能不同:

  1. run方法的作用是描述线程具体要执行的任务;
  2. start方法的作用是真正的去申请系统线程

运行结果不同:

  1. run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
  2. start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

1.4 中断⼀个线程

接着上面图片张三、李四的例子,李四⼀旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加⼀些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停⽌转账,那张三该如何通知李四停止呢?这就涉及到我们的停⽌线程的方式了。
目前常见的有以下两种方式:

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

示例1: 使用自定义的变量来作为标志位,示例代码:

java 复制代码
public class ThreadDemo9 {
    private static boolean isQuit = false;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (!isQuit) {
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t结束");
        });
        t.start();

        try {
            Thread.sleep(3000);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("让t线程结束");
        isQuit = true;
    }
}

运行结果:

我们可以看到代码中,将自定义的变量标志位写成了类的静态成员变量,那是否可以写为main方法中的局部变量?

**不可以!**当你定义为局部变量时,会发现提示编译错误。

这是因为我们使用了创建线程的lambda表达式方法,lambda表达式有一个语法:变量捕获。

lambda表达式的变量捕获,本质上就是,把外面的变量当作参数传进来(参数是隐藏的)。这个捕获的变量得是 final 修饰的或者"事实final"(虽然没有写final,但是没有修改)。因为此处isQuit是确实要修改的,不能写成final,它也不是事实final,因此将标志位变量isQuit定义为局部变量是行不通的!

那为什么可以定义为成员变量呢?
lambda表达式,本质上是"函数式接口",匿名内部类。对于内部类,访问外部类的成员是完全可以的,这个事情不受到变量捕获的影响。
示例2: 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位。

Thread 内部包含了⼀个 boolean 类型的变量作为线程是否被中断的标记。

示例代码:

java 复制代码
public static void main(String[] args) {
        Thread t = new Thread(()->{
           while (!Thread.currentThread().isInterrupted()) {
               System.out.println("我是一个线程,正在工作");

               //interrupt会影响sleep,线程不会结束
                 try {
                    Thread.sleep(1000);
                }catch (InterruptedException e) {
                   e.printStackTrace();
                 }
              
           }
            System.out.println("线程结束");
        });

        t.start();
        try {
            Thread.sleep(3000);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("让t线程结束");
        t.interrupt();
    }
}

运行结果:

可以看到抛出异常,并且线程不会结束,继续往下循环输出。这是因为什么呢?

这是因为存在sleep,在执行sleep的过程中,调用interrupt。大概率sleep休眠时间还没到,被提前唤醒了。

sleep被提前唤醒,会做两件事:

  1. 抛出InterruptedException,紧接着就会被catch捕获到
  2. 清除Thread对象的isInterrupted标志位

对于线程不会结束,就是标志位被清除了。我们通过interrupt方法,已经把标志位设为true了,但是sleep提前唤醒操作,又把标志位清除,设为原来的false,所以线程不会结束。

如何解决呢?

要想线程结束,只需要在catch中加上break即可。但此时仍会抛异常,不想抛,就不写输出e.printStackTrace()。

java 复制代码
Thread t = new Thread(()->{
           while (!Thread.currentThread().isInterrupted()) {
               System.out.println("我是一个线程,正在工作");
               try {
                  Thread.sleep(1000);
               }catch (InterruptedException e) {
                  // e.printStackTrace();
                   break;  //加上break,仍会抛异常(不想抛,就不写上面输出e.printStackTrace()),线程会结束
              }
           }
            System.out.println("线程执行完毕");
        });

这时的结果:

thread 收到通知的方式有两种:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,并清除中断标志。当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法,可以选择忽略这个异常,也可以跳出循环结束线程。
  2. 否则,只是内部的⼀个中断标志被设置,thread 可以通过 Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,**不清除中断标志。**这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

1.5 等待⼀个线程 - join()

有时,我们需要等待⼀个线程完成它的⼯作后,才能进行自己的下⼀步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要⼀个方法明确等待线程的结束。

代码示例:

java 复制代码
public class ThreadDemo {
 public static void main(String[] args) throws InterruptedException {
     Runnable target = () -> {
         for (int i = 0; i < 10; i++) {
         try {
             System.out.println(Thread.currentThread().getName() 
         + ": 我还在⼯作!");
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
         System.out.println(Thread.currentThread().getName() + ": 我结束了!")
    };

    Thread thread1 = new Thread(target, "李四");
    Thread thread2 = new Thread(target, "王五");
    System.out.println("先让李四开始⼯作");
    thread1.start();
    thread1.join();
    System.out.println("李四⼯作结束了,让王五开始⼯作");
    thread2.start();
    thread2.join();
    System.out.println("王五⼯作结束了");
 }
}

这里是几个join方法:

1.6 获取当前线程引用

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
    }
}

1.7 休眠当前线程

也是我们比较熟悉⼀组方法,有⼀点要记得,因为线程的调度是不可控的,所以,这个方法只能保证 实际休眠时间是大于等于参数设置的休眠时间的。

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

2、线程的状态

2.1 观察线程的所有状态

线程的状态是⼀个枚举类型 Thread.State.

java 复制代码
public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}
  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的,又可以分成正在⼯作中和即将开始⼯作.
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了.

2.2 线程状态和状态转移的意义

大家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。

还是我们之前的例子:
刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
当李四、王五开始去窗口排队,等待服务,就进⼊到 RUNNABLE 状态。该状态并不表示已经被银行 工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为⼀些事情需要去忙,例如需要填写信息、回家取证件、发呆⼀会等等时,进⼊
BLOCKED 、 WATING 、 TIMED_WAITING 状态;
如果李四、王五已经忙完,为 TERMINATED 状态。
所以,我们上面内容介绍的 isAlive() 方法,可以认为是 处于不是 NEW 和 TERMINATED 的状态都是活着的。

2.3 观察线程的状态和转移

观察 1: 关注 NEW 、 RUNNABLE 、 TERMINATED 状态的转换

java 复制代码
public class ThreadStateTransfer {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 1000_0000; i++) {}
        }, "李四");
        System.out.println(t.getName() + ": " + t.getState());;
        t.start();
        while (t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());;
        }
        System.out.println(t.getName() + ": " + t.getState());;
    }
}

观察 2: 关注 WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换

java 复制代码
public static void main(String[] args) {
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (object) {
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        }, "t1");
    t1.start();
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println("hehe");
            }
        }}, "t2");
    t2.start();
}

使⽤ jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED
修改上面的代码, 把 t1 中的 sleep 换成 wait

java 复制代码
public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    try {
                        // [修改这⾥就可以了!!!!!]
                        // Thread.sleep(1000);
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1");
 ...
    }

使用户jconsole 可以看到 t1 的状态是 WAITING。
结论:

  • BLOCKED 表示等待获取锁,WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
  • TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
相关推荐
seventeennnnn3 分钟前
Java大厂面试真题:谢飞机的技术挑战
java·spring boot·面试·aigc·技术挑战·电商场景·内容社区
wkj00114 分钟前
接口实现类向上转型和向上转型解析
java·开发语言·c#
qqxhb14 分钟前
零基础设计模式——行为型模式 - 观察者模式
java·观察者模式·设计模式·go
寒士obj41 分钟前
类加载的过程
java·开发语言
无名之逆44 分钟前
大三自学笔记:探索Hyperlane框架的心路历程
java·开发语言·前端·spring boot·后端·rust·编程
Chuck1sn1 小时前
我把 Cursor AI 整合到 Ruoyi 中,从此让 Java 脚手架脱离人工!
java·vue.js·后端
水木石画室1 小时前
Spring Boot 常用注解面试题深度解析
java·spring boot·后端
hweiyu001 小时前
tomcat指定使用的jdk版本
java·开发语言·tomcat
百锦再1 小时前
.NET 类库开发详细指南c
java·log4j·.net·net·dot
黎䪽圓2 小时前
【Java多线程从青铜到王者】阻塞队列(十)
java·开发语言