目录
前言
1、创建一个线程
1.1、 体会多线程的执行
1.2、体会单线程的执行
1.3、sleep方法(休眠)
1.4、通过第三方程序来观察线程详情
1.5、创建线程的方式
1.5.1、继承Thread类,重写run方法来创建线程
1.5.2、实现Runnable接口,重写run方法来创建线程
1.5.3、继承Thread类,使用匿名内部类
1.5.4、继承Runnable接口,使用匿名内部类(定义在类里面的类)
1.5.5、使用lambda表达式来创建线程
2、Thread类及常见方法
2.1、Thread的常见构造方法
2.2、Thread类的几个常见属性
3、启动一个线程(start)
4、中断一个线程
4.1、设置结束标志位
5、等待一个线程(join)
6、获取当前线程的引用
7、休眠当前线程
8、线程的状态
8.1、线程状态之间的转换
前言
Java标准库中Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装。
1、创建一个线程
Java标准库中提供了一个类Thread能够表示一个线程,但是我们在示例化的时候,并不是直接创建Thread这个类的对象,而是创建一个Thread类的子类,重写run(线程的入口方法)。创建子类的对象,用父类的引用指向这个子类对象。
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello world");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
//启动线程
t.start();
//启动线程之后,相当于在工厂(进程)中开启了一个新的流水线(执行流/线程),新的流水线开始并发的执行另外一个逻辑了
}
}
点击运行程序,其实是idea创建了一个新的Java进程,这个Java线程来执行我们自己写的代码。
这个Java进程中存在两个线程。
- main方法所对应的线程(一个线程里至少存在一个线程)也可以称为主线程
- 通过t.start创建的新的线程。
1.1、 体会多线程的执行
上述的代码我问并不能体会到“每个线程是一个独立的执行流”,我们将上述代码改动以下,在两个类中加上一个死循环,来观察他们的执行结果。
//体会多线程
//这个代码按理来说,执行了start方法之后,进入到MyThread对象中执行run方法内容,会死循环出不去了。也就是说在打印的时候只能输出hello t,现在我们来观察以下它的执行结果。
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello t");
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
//启动线程
t.start();//这个start的作用是启动了一个新的线程,来执行MyThread中的内容。
while(true){
System.out.println("hello main");
}
}
}
结果中,两个死循环中的打印结果都有,从这里我们可以看出来,多线程是并发(并发+并行)执行的。start方法用来开启新的线程。
1.2、体会单线程的执行
在main中直接调用run方法,run方法不会创建新的线程,run方法的执行是在main方法的线程中执行的。
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello t");
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
//start 会创建一个线程
// t.start();
//run不会创建新的线程,run是在main 中执行的
t.run();
while(true){
System.out.println("hello main");
}
}
}
此处的代码中没有使用start方法创建新的线程,两个死循环是在同一个线程中的,执行第一个死循环的时候,进入之后就出不来了,无法进入到第二个循环中。
❗❗❗总结:
- 这里的run方法可以这样理解,main方法是主线程的入口方法,对象使用start方法创建新的线程,新的线程也需要一个入口方法来进入执行。所以run可以和main方法的这个作用类比。让CPU核心执行这个线程的入口。
- start方法会调用操作系统的API,创建新的线程。新的线程里调用run方法。通过多线程的代码来看,t对象在调用了start这个方法之后,并没有调用run方法,但是程序在执行的时候,run方法中的结果被打印出来了。这里的重写的方法是不需要我们手动调用的,已经有其他代码来调用了。
1.3、sleep方法(休眠)
sleep是Thread类的静态方法,直接使用Thread直接调用。sleep方法的参数单位时ms。1s = 1000ms
上述两个代码运行起来打印结果的速度太快,我们可以使用sleep方法,在程序执行时,让其休眠一段时间,有利于我们的观察。
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello t");
try{
Thread.sleep(1000);
}catch(InterruptedException e){//Interrupted表示打断/中断 —— 意思就是sleep在睡眠过程中,还没有到时间,就被唤醒
e.printStackTrace();
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
//start 会创建一个线程
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
可以看到这里的打印速度就会慢很多,有利于观察结果。
此处的交替也不是严格意义上的交替,每一秒过后,是先打印main还是先打印t,是不确定的。多个线程在CPU上调度执行的顺序是不确定的(随机的)。
1.4、通过第三方程序来观察线程详情
我们找到jdk安装的地方,然后bin目录下找到这个程序,用这个jdk提供的工具,就能够给我们查看出Java进程里面的线程的详情。
当我们的代码运行起来之后,点击这个程序,就可以看见当前本地的一些进程。
❗❗❗注意:jconsole只能分析Java进程,不能识别非Java写的进程。
点击我们程序中的main和Thread-0,来查看这个线程执行的具体情况。这里我们主要关注的就是堆栈跟踪。堆栈跟踪描述的就是当前这个两个线程中的代码执行到哪里了。之后我们在写这些多线程代码的时候出现bagel,需要调试的情况下,参考调用栈是一个有效的方法。
1.5、创建线程的方式
1.5.1、继承Thread类,重写run方法来创建线程
上述的代码中使用的都是进程Thread类,重写run方法的方式来创建线程的,所以这里不在进程过多的说明了。
1.5.2、实现Runnable接口,重写run方法来创建线程
Runnable字面意思是可运行的。使用Runnable描述一个具体的任务,Runnable接口使用run方法来描述。
第一种写法是使用Thread的run来描述线程的入口
第二种写法是使用Runnable interface来描述线程的入口
虽然描述线程入口的方式存在差别,但是起到的效果是一样的。
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();//这里创建一个实现Runnable接口的类的对象
//这里直接创建Thread类的对象,不用实现Thread的子类。
Thread t = new Thread(runnable);//将这个对象作为Thread类的构造方法的参数
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
第一种写法和第二种写法之间,没有本质的区别,只是描述线程入口的方式不同而已,这两种写法也没有高下之分,使用那种都可以。
1.5.3、继承Thread类,使用匿名内部类
我们通过在创建Thread类的时候,在后边写一个大括号({}),大括号中来写属性或者方法。这就是在创建类的时候,使用匿名内部类。
public class ThreadDemo3 {
public static void main(String[] args) {
//new 表示创建一个对象这个动作
//Thread(),对象名 表示这个匿名内部类继承了这个类
Thread t = new Thread(){
@Override
public void run() {
while(true){
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.5.4、继承Runnable接口,使用匿名内部类(定义在类里面的类)
内部类是定义在类里面的有名字的类,匿名内部类是定义在类里面没有名字的类。
public class ThreadDemo4 {
public static void main(String[] args) {
//此处实现的匿名内部类是实现了Runnable接口的
//这里的new表示的是创建对象的动作,
//Runnable(),接口名称就是匿名内部类需要实现的那个接口
//{..}表示的是匿名内部类的内容
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.5.5、使用lambda表达式来创建线程
上述的四种创建线程的方法,在实际中并不是使用最多的方法,而是lambda表达式是使用最多的。使用lambda表达式,是最简单最直观的写法。
- lambda表达式本质就是一个匿名函数(没有名字的函数,这种一般都是一次性的),Java里面方法是无法脱离类的,在Java里边lambda就是一个例外。
- ❗❗❗lambda表达式的基本写法:()->{方法体};()中可以放多个参数,如果只有一个参数,()可以省略。{}中如果只有一行代码,也可以省略。
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、Thread类及常见方法
Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联。
每个执行流(线程),需要有一个对象来描述,而Thread类的对象就是用来描述一个线程执行流的JVM会将这些Thread对象组织起来,用于线程调度,线程管理。
2.1、Thread的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name ) | 创建线程,并命名 |
Thread(Running target,String name) | 使用Runnable对象创建对象,并命名。 |
前两个构造方法,在之前的代码中已经使用过了,不做解释,这里我们来了解一下后面两个构造方法。后面两个两个构造方法中的参数name的作用就是给线程起了一个名字,这里的名字不影响线程的执行,只是方便咱们再调试的时候,快速找到咱们关心的线程。
package threading;
public class ThreadDemo6 {
public static void main(String[] args) {
//将这个lambda表达式作为Thread的一个参数
Thread t = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"我的名字");
t.start();
}
}
可以看见这些线程中,我们对上述Java代码所在的线程进行了命名。
2.2、Thread类的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台运行 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID:是线程的唯一标识,每个线程都有自己的ID
- 名称:就像上述我们给线程命名
- 是否为后台线程:true 表示为后台线程,false 表示为前台线程,前台线程会阻止Java进程结束,必须得Java进程中所有的前台线程都执行完,Java进程才能结束。后台线程不阻止Java进程结束,哪怕后台线程还没执行完,Java进程该结束就结束了。我们创建的线程默认是前台的,可以通过setDaemon设置成后台的。
public class ThreadDemo8 { public static void main(String[] args) { Thread t = new Thread(()->{ while(true){ } }); //默认是前台线程,也就是设为false //此时这个线程会阻止进程结束 //t.setDaemon(false); //改成true编程后台线程,不影响进程的结束 t.setDaemon(true); t.start(); } }
- 线程是否存活:描述的是系统内核里的那个线程是否存活。一种是线程的入口方法执行完毕,此时系统中的对应线程就没了,此时调用该线程的isALive就是false,表示这个线程已经不再存活了。另一种是线程对象已经创建好了,但是还没有执行调用start,那么这个时候它的isALive也是false.这个线程也是不存活的状态。
public class ThreadDemo7 { public static void main(String[] args) { Thread t = new Thread(()->{ System.out.println("hello t"); }); t.start(); try { //上述t线程没有进行任何循环和sleep,意味着里面的代码会迅速执行完毕, //也就意味着,main线程如果sleep结束,此时t基本上就是已经执行完了的状态,此时t对象还在 //但是在系统中对应的线程已经结束了 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //在main线程中过一秒在调用isALive方法,则这个线程已经结束,结果为false System.out.println(t.isAlive()); } }
上述中说到main线程sleep结束,此时t基本上就是已经执行完了的状态,这里为什么是基本上,因为他可能存在极端情况,主线程sleep结束了,t线程仍然还没有去CPU上执行。这里主要是系统对于线程的调度是随机的。比如我们有些时候,在使用机器的时候,点击某个窗口的时候,提示"未响应"。
3、启动一个线程(start)
- start方法:他是开启/创建一个线程。
- run方法:表示的是新线程的入口方法,这个方法是被系统自动调用起来的,程序员不需要调用这个方法。
4、中断一个线程
中断线程这里表示的意思就是让一个线程终止,本质上来说,让一个线程终止,办法就一种,让该线程的入口方法执行完毕。让一个线程入口方法执行完毕的方法有很多。
4.1、设置结束标志位
1️⃣第一种手动给线程中设定一个结束标志位。
public class ThreadDemo9 {
//将isQuit作为线程结束标志位
public static boolean isQuit = false;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(!isQuit){
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t 线程结束");
});
t.start();
//在主线程,修改isQuit
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isQuit = true;
}
}
❓❓❓如果将上述代码中的isQuit从成员变量改成局部变量(放到main方法中),这个代码还能正常运行吗?
❗❗❗不能 这里就涉及到了lambda表达式的知识点 变量捕获
lambda表达式能否访问外面的局部变量?可以——变量捕获语法规则
Java要求变量捕获,捕获的是变量必须是被final或者"实际final"修饰。被final修饰变量会变成常量,实际final就是变量没有用final修饰,但是代码中并没有做出修改。
上述代码中,isQuit并不是被final修饰的局部变量,或者是实际final变量。因为我们的isQuit在主线程中进行了修改,所以我们直接将isQuit写成了成员变量,成员变量不受上述语法规则的限制。
上述的代码是我们自己创建了一个变量来控制循环,其实Thread类内部内置了一个标志位,让我们更方便的实现上述效果——设置标志位。
2️⃣通过Thread类内置的方法设置结束标志位
public class ThreadDemo10 {
public static void main(String[] args) {
Thread t = new Thread(()->{
//currentThread()是获取到当前线程的实例
//此处currentThread得到的对象就是t
//isInterrupted()可以理解为t对象里自带的一个标志位,起始为false
while(!Thread.currentThread().isInterrupted()) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
//打印出异常方法的调用栈
e.printStackTrace();
}
//把t内部的标志位给设置成true
t.interrupt();
}
}
此时我们看代码的执行结果,发现,主线程中睡眠了3s后,调用t.interrupt方法的时候,线程并没有真的结束,而是打印了异常这个星系,又继续执行了。
❗❗❗这里我们就需要对interrupt这个方法有一个了解。他又两个作用。
- 设置标志位为true
- 如果该线程正在阻塞中(比如正在执行sleep),此时就会把这个阻塞状态唤醒,通过抛出异常的方式让sleep立即结束。
换言之,当两个线程,主线程走到interrupt这个方法这里,另一个线程走到打印"hello t"这里,那么当interrupt被执行,另一个线程中,会直接设置标志位。但是当主线程里走到interrupt,另一个线程走到sleep函数,当主线程中interrupt被执行之后,sleep会被直接中断,让休眠结束。通过抛异常的方式唤醒让sleep结束。
❗❗❗又存在要给问题,interrupt执行之后标志位变为true,为什么在显示的结果中抛出异常之后,程序还会继续被执行。
这是因为sleep在被唤醒的时候,sleep会自动的把isInterrupted标志位给清空(将true还原为false)
❓❓❓说到这里有的同学就会问,在whlie循环中,第一次执行到sleep是被唤醒,抛出了异常,而循环到第二次为什么没有排除异常??
- 这个问题的产生,是因为我们在思考上述代码的时候,是以单线程的思路来考虑的,而不是以多线程的思路考虑的。
- 我么要注意多线程在执行的时候是以并发(并行+并发)的方式运行的,以上述代码为例,上述代码中存在两个线程,可以理解为两个线程是同时运行的(电脑对线程的调度非常快速,我们人类是感知不到的)多线程的执行顺序是兵分两路,自己执行自己的代码。
- 主线程中的interrupt只执行了一次,主线程结束了,只剩一个线程在运行了。也就是interrupt对标志位只修改了一次,然后剩下的线程在运行的时候while循环中的条件只能是true,也就是说isInterrupted标志位只能是false.
❓❓❓为什么sleep要清空标志位呢?
❗❗❗目的就是为了让线程自身能够对于线程何时结束,有一个明确的控制。
当前interrupt方法,效果不是让线程立即结束,而是告诉他,你该结束了,至于他是否真的要立即结束还是等会结束,都是代码来灵活控制的,interrupt只是单纯的通知而不是命令。
1️⃣无视要求
就上述的代码而言,我们将try-catch中的打印异常方法调用栈这行注释掉。那么我们的代码就将interrupt的通知无视掉了。
public class ThreadDemo10 { public static void main(String[] args) { Thread t = new Thread(()->{ //currentThread()是获取到当前线程的实例 //此处currentThread得到的对象就是t //isInterrupted()可以理解为t对象里自带的一个标志位 while(!Thread.currentThread().isInterrupted()) { System.out.println("hello t"); try { Thread.sleep(1000); } catch (InterruptedException e) { // e.printStackTrace(); } } }); t.start();
2️⃣立即执行
这种是在上述代码中try-catch 中加上一个break。
public class ThreadDemo10 { public static void main(String[] args) { Thread t = new Thread(()->{ //currentThread()是获取到当前线程的实例 //此处currentThread得到的对象就是t //isInterrupted()可以理解为t对象里自带的一个标志位 while(!Thread.currentThread().isInterrupted()) { System.out.println("hello t"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); break;//将break放在这里,是当sleep抛出异常之后,执行catch中的代码,在这里结束。 } //break;如果将break放在这里,会导致while循环执行一次就结束。 } }); t.start();
3️⃣等会执行
在catch中再写一个try-catch。
public class ThreadDemo10 { public static void main(String[] args) { Thread t = new Thread(()->{ //currentThread()是获取到当前线程的实例 //此处currentThread得到的对象就是t //isInterrupted()可以理解为t对象里自带的一个标志位 while(!Thread.currentThread().isInterrupted()) { System.out.println("hello t"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); try { Thread.sleep(1000); } catch (InterruptedException ex) { ex.printStackTrace(); } } } });
❗❗❗总结:
- interrupt方法和sleep不是搭配使用,这里sleep只是用作举例 ,Thread这里但凡是涉及到阻塞功能的方法都和sleep有类似的效果。
- 多线程的执行方式是,兵分多路,每个线程都同时执行自己线程中的代码。
- 使用isInterruped方法和interrupt方法设置结束标志位,当线程发生阻塞时,interrupt将阻塞(例如sleep)唤醒时,相应的isInterrupted的标志位就会被清空,将true——》false. interrupt将标志位由false改为true。
- interrupt存在两种作用,改变标志位的值,唤醒阻塞(sleep)。
- sleep存在两种作用,休眠,清空标志位。
5、等待一个线程(join)
线程之间时并发执行的,操作系统对于线程的调度是无序的,无法判定两个线程谁先执行结束,时候执行结束。
public class ThreadDemo11 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello t");
});
t.start();
System.out.println("hello main");
}
}
这两个线程到底是先输出hello main还是hello t,我们无法确定。
这个代码实际执行的时候,大部分情况下都是先执行出hello main(因为线程创建也有开销),但是不排除特定情况下,主线程hello main没有立即执行到。谁先执行谁后执行,这些都是不确定的,那么结束的顺序也是不确定的。所以为了明确结束顺序,这个时候我们就要说到join方法。
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
System.out.println("hello t");
});
t.start();
t.join();
System.out.println("hello main");
}
}
❗❗❗注意:
- 这里的谁等谁很关键,这里的join是在main中被调用,所以是main线程等t线程先结束,再往下执行。
- 如果再t.join执行的时候,t线程还没结束 ,main线程就会阻塞等待(代码走到这一行就停下来了)。当前这个main线程暂时不参与cpu的调度执行了
- 这里调用join之后,调用join的线程停止,别的线程(后台线程)不受影响,就比如处理调试信息或者垃圾回收的线程都不受影响。
❗❗❗ 理解t.join()
- main线程调用t.join()的狮虎,如果t还在执行,此时main线程阻塞,知道t执行完毕(t的run执行完了),main才从阻塞中解除,才继续执行
- main线程调用t.join的时候,如果t已经结束了,此时join不会阻塞,就会立即往下执行。
join方法的两种写法,带参数和不带参数。
方法 | 说明 |
public void join() | 等待线程结束(死等) |
public void join(long millis) | 等待线程结束,(有时间限制,最多等多长时间) |
6、获取当前线程的引用
方法 | 说明 |
public static Thread currentThread() | 返回当前线程对象的引用 |
这个方法在上面的设置结束标志位的时候已经用过了。
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
作用就是在那个线程中调用,获取的就是那个线程的实例。
7、休眠当前线程
这个方法大家都相当熟悉了,在这篇博客中,每个代码中都有它的身影。所以我么不做过多的说明。sleep方法,只能保证实际休眠时间是大于等于参数设置的休眠时间的,因为线程的调度是不可控制的。
8、线程的状态
操作系统里面的线程,自身是有一个状态的,但是Java Thread是对系统线程的封装,把这里的状态又进一步的精细化了。
1️⃣NEW状态:系统中的线程还没有创建出来呢,只是有个Thread对象。
public class ThreadDemo12 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
System.out.println("hello");
});
//在启动之前,获取线程状态,NEW
System.out.println(t.getState());
t.start();
}
}
2️⃣ TERMINATED状态:系统中的线程已经执行完了,Thread对象还在。
public class ThreadDemo12 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
System.out.println("hello");
});
t.start();
//这里休眠2秒,确保t线程已经执行完了
Thread.sleep(2000);
System.out.println(t.getState());
}
}
3️⃣RUNNABLE状态:就绪状态。存在两种可能(1、正在CPU上运行。2、准备好随时可以去CPU上运行)
public class ThreadDemo12 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(true){
//为了防止hello把线程状态冲没了,先注释掉
//System.out.println("hello");
}
});
//在启动之前,获取线程状态,NEW
System.out.println(t.getState());
t.start();
Thread.sleep(2000);
System.out.println(t.getState());
}
}
4️⃣TIMED_WAITING:指定时间等待就会触发这个状态(比如sleep方法)
public class ThreadDemo12 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(true){
//为了防止hello把线程状态冲没了,先注释掉
//System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//在启动之前,获取线程状态,NEW
System.out.println(t.getState());
t.start();
Thread.sleep(2000);
System.out.println(t.getState());
}
}
5️⃣BLOCKED:表示等待锁出现的状态。
6️⃣WAITING:使用wait方法时出现的状态。
8.1、线程状态之间的转换
一条主线,三条支线。