温馨提示:PC端观看,效果更佳!
本文两大部分:介绍一些Thread线程常用的方法,和解析线程的六大状态!
目录
一、线程常用的方法
介绍Thread类
1.开启线程
2.获取线程引用
3.休眠线程
4.中断线程
3.等待线程
二、线程状态
1.NEW
2.TERMINATED
3.RUNNABLE
4.WAITING
5.TIMED_WAITING
6.BLOCKED
7.状态总结(超级重要)
一、线程常用的方法
介绍Thread类
(1)这是一个线程类,用来实例化线程对象
(2)该类中,有很多方法。普通成员方法,可以使用对象的引用进行调用,像statr()方法等等;而静态方法,则需要通过类名去调用,如:Thread.sleep()等等。
(3)我们通过Thread来实例化对象,产生的对象,我们默认称为“线程对象”,start()之后,线程才产生。因此,类中拥有的各种属性方法,我们后面就默认称为该线程的属性和方法。
(4)线程,有哪些状态、属性,方法,本质上都是Thread中的;因此我们实例化出的线程,也就具有了。后续所说的线程,都是指 线程对象
1.开启线程
(1)方法:start()
(2)作用:当线程对象调用该方法之后,真正的线程才真正的被创建出来并且执行。
(3)实例
public static void main(String[] args) throws InterruptedException {Thread t = new Thread("张三线程") {@Overridepublic void run() {System.out.println("111");}};t.start();}
在:该操作,只是创造出了线程对象
Thread t = new Thread("张三线程")
只有:start()之后,该线程才是真正创建并运行了起来(真正在系统中创造了PCB)
t.start();
(4)start()注意事项
每一个线程对象,只可以start()一次。第二次start()操作就会出现异常
异常:非法的线程状态
该线程状态:
原因:线程start()之后,被调度到cpu上执行,当线程中的任务执行完之后,就会被销毁了;此时线程(PCB)已经不存在了,但是Thread对象(t引用指向的对象)还存在。
2.获取线程引用
(1)作用:返回当前线程对象的引用(在哪个线程中调用,就返回哪个线程的)
(2)方法全貌:public static native Thread currentThread()
(3)返回自定义线程的引用
public static void main(String[] args) {Thread t = new Thread(()->{System.out.println(Thread.currentThread().getState());System.out.println("开启线程成功!");}) {};t.start();}
获取t线程的引用,并且调用该线程的状态
(4)获取主线程的引用
public static void main(String[] args) {Thread t = Thread.currentThread();//此时t中存的是main线程的引用System.out.println("线程名字:"+t.getName());}
3.休眠线程
(1)方法:Thread.sleep()。很明显,这是一个静态方法,需要传参。
参数是整数,单位是ms。1000ms等于一秒
(2)作用:让线程休眠
(3)实例
public static int m = 4;public static void main(String[] args) {Thread t = new Thread(()->{while (true) {try {Thread.sleep(1000);m--;} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(m+"秒后结束t线程");if(m == 1) break;}System.out.println("t线程结束");});t.start();}
作用:每循环一次,就会休眠一秒,当休眠三秒后,线程就会结束。
(4)使用的注意实现1
1)在重写的run方法中
原因:在方法重写的时候,要求方法前面是一样的,run方法的父类是没有抛出异常的,所以run方法也不能抛出异常。所以sleep异常处理选择时,就不能抛出异常,只能捕获异常。
2)在外面
(5)sleep的注意事项2
sleep控制的是线程的休眠时间,而不是两个代码之间的时间
在时间上,并不是1000ms.
得出一个小结论:sleep结束之后,线程不一定会马上被调度会cpu上执行。
4.中断线程
更加合理的说法是说,终止一个线程。按理来说,当线程开启的时候,就会一直执行,直到线程的任务结束。但是有时候,需要停止线程,就需要一些操作去执行了。而终止线程,只是起到提醒的作用,线程是否要终止,还要看线程本身。
目前有两种方式:
(1)通过共享的标记来进行沟通(2)调用interrupt()方法来通知
2.1.第一种实现
(1)标志正确写法
public static boolean isRunning = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (isRunning) {System.out.println("线程正在执行!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("线程结束了");});t.start();System.out.println("3s后结束线程");Thread.sleep(1000);isRunning = false;}
实现手段:通过改变循环的条件来终止循环,进而结束线程。
(2)缺点
1)该标志变量要定义在main方法外面,也就是全局变量;如果定义在里面,会被内部类捕获到,从而报错。
2)这种终止线程写法的缺点 ,即使循环条件不符合了,需要结束循环,但是由于上一次循环进入时,线程正在sleep中,导致无法即使终止线程。
2.2.第二种实现
(1)简介:使用Thread的三个方法来配合实现。下面介绍
方法 | 作用 |
public void interrupt() | 修改标志位(哪个线程调用就修改) |
public static native Thread currentThread() | 获取当前线程的引用 |
public boolean isInterrupted() | 线程中自带的标志位,默认值为false |
(2)代码实现
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (!Thread.currentThread().isInterrupted()) {System.out.println("线程正在执行!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("线程结束了");});t.start();System.out.println("3s后结束线程");Thread.sleep(1000);t.interrupt();}
代码是结束了,但是抛出了异常。是因为唤醒sleep等阻塞的方法,即使线程在sleep中,也能即使结束线程(就是相比于第一种方法的优势)。设置成false之后,则会让sleep抛出一个InterruptedException异常,不需要等待sleep,进而结束了。
如果更改sleep抛出的异常,进而可以控制线程。
(3)提醒线程终止,但是不想结束
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (!Thread.currentThread().isInterrupted()) {System.out.println("线程正在执行!");try {Thread.sleep(1000);} catch (InterruptedException e) {//throw new RuntimeException(e);e.printStackTrace();}}System.out.println("线程结束了");});t.start();System.out.println("3s后结束线程");Thread.sleep(1000);t.interrupt();}
代码没有结束,抛出一个异常后继续执行了代码。
原因:在触发interruptd的时候,而线程正在sleep的时候,会唤醒正在sleep,并且把interrupt修改的标志位又修改回来(又改回false)。前面因为没有修改抛出的异常,而直接把线程终止了。
(4)提醒线程终止,马上终止
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (!Thread.currentThread().isInterrupted()) {System.out.println("线程正在执行!");try {Thread.sleep(1000);} catch (InterruptedException e) {//throw new RuntimeException(e);//e.printStackTrace();System.out.println("自愿结束线程");break;}}System.out.println("线程结束了");});t.start();System.out.println("3s后结束线程");Thread.sleep(1000);t.interrupt();}
(5)提醒线程终止,等会结束
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (!Thread.currentThread().isInterrupted()) {System.out.println("线程正在执行!");try {Thread.sleep(1000);} catch (InterruptedException e) {System.out.println("我先打印完1234再结束");System.out.println(1);System.out.println(2);System.out.println(3);System.out.println(4);break;}}System.out.println("线程结束了");});t.start();System.out.println("3s后结束线程");Thread.sleep(1000);t.interrupt();}
3.等待线程
等待线程有三个重载的方法,前面的两个比较常用
方法 | 说明 |
public void join() | 等待线程结束(死等) |
public void join(long millis) | 等待线程结束,最多等millis毫秒 |
public void join(long millis,int nanos) | 同2,但可以更高精度(因此容易出错) |
线程等待的意义:多线程中,调度顺序是无序的,因此每个线程的执行时间和结束时间是不确定的。引用线程等待,就可以控制线程结束的先后顺序
下面以join()为主要介绍。
3.1.join()
(1)没有等待时
public static void main(String[] args) {Thread t = new Thread(()->{while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t");}});t.start();System.out.println("main线程");}
此时,是main线程已经结束,但是t线程没有结束。
(2)引入等待
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t");}});t.start();t.join();System.out.println("main线程");}
通过代码的结果可以看到,没有执行后面的打印语句。这样的意思是:main线程需要等待t线程结束后才能结束,也就是结束t.join()语句才行。
此时,main线程在等待t线程,也就是等待状态。代码无法直接观看,借助调用栈来观看
(3)等待解析
哪个线程调用join,哪个线程就是要先执行结束的。准确的来说:在A线程里面,B线程调用join,那么线程结束的顺序就是 B-->A,也就是线程A会等待线程B先结束
(4)深入理解
上面的代码中,在main中调用t.join(),有三种情况。
1)如果线程t此时已经结束了,此时的join就会立即返回,相当于join没气作用
2)如果线程t还没结束,此时main线程就会阻塞等待t线程结束,直到t线程结束之后,join才会解除阻塞,继续执行main
3)当执行t.join时,t线程还没开始,就会直接跳过t.join(),相当于没起到作用
下面介绍上面三种情况,顺便多介绍两种情况。
第一种情况:t.join时,t线程已经结束了(只有t线程和main线程)
当代码执行到t.join()时,t线程已经被销毁了,所以执行join时就直接返回了,相当于没有
第二种情况:正常起到作用(只有t线程和main线程)
当执行到t.join()时,t线程还没结束,此时就会main线程就会阻塞等待,直到t线程结束之后才会继续执行
第三种情况:t线程还没开始(只有t线程和main线程)
这就是当执行到t.join()时,t线程还没开始执行
第四种情况:三个线程嵌套等待(t1线程、t2线程、main线程)
预期结束顺序:t2 -> t1 -> main
public static void main(String[] args) throws InterruptedException {Thread t2 = new Thread(()->{for (int i = 0; i < 3; i++) {System.out.println("t2");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t1 = new Thread(()->{try {Thread.sleep(500);//确保t2线程此时已经启动t2.join();//t1线程阻塞等待t2线程结束后再执行} 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();//main线程阻塞等待t1线程结束再继续执行System.out.println("main end");}
得出一个小结论:在A线程中使用B.join(),那么一定是B线程先结束,A线程后结束(join阻塞A线程);但是A线程中同时使用B.join()和C.join(),B线程和C线程之间是并发执行,A阻塞等待的时间是Math.max(B线程的时间,C线程的时间)
执行结果:
第五种情况:main线程也可以join()操作
要想做到main线程先结束,就要拿到主线程的引用
public static void main(String[] args) throws InterruptedException {Thread mainT = Thread.currentThread();//拿到main线程的引用System.out.println(mainT.getName());Thread t = new Thread(()->{try {mainT.join();//main线程先结束,t线程才会继续执行} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t end");});t.start();Thread.sleep(2000);System.out.println("main end");}
由此可见,main线程也是可以join的
由于join()是一个死等的方法,只有t线程不结束,还没的代码就不会执行。这种写法很不友好。
3.2.join(long millis)
(1)这是一种带有时间的等待,参数就是需要等待的时间,如果超过了这个时间就不会继续执行下去,进而去执行后面的代码。
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t");}});t.start();t.join(3000);System.out.println("main end");}
t线程是一个死循环线程,永远不会结束,如果是死等的join,那么main线程永久不会结束。但是这是一个带有时间的超时等待,过了这个时间就不会再继续等待下去
(2)如果join等待3秒,而t线程执行2秒,也会提前结束join,并不是真的会等待3秒
4.方法小结(超级重要)
注:方法括号中的ms代表毫秒
方法名称 | 方法 | 方法作用 |
开始一个线程 | start() | 自定义的线程调用后,才代表该线程启动 |
获取当前线程的引用 | currentThread() | 可以获取当前线程的一个引用 |
休眠线程 | sleep(ms) | 让当前线程休眠指定时间 |
中断线程 | isInterrupted()、interrupted() | 提醒线程是否要终止 |
等待线程 | join()--无参方法 | 调用该方法的线程提前结束,阻塞 |
join(ms)--带有参数方法 | 在嵌套该线程的那个线程 |
二、线程状态
线程的状态,也是PCB中的状态。粗分的话有四个状态,细分是六个状态。下面我们以六个状态来介绍。
状态 | 名字 |
NEW | 新建状态 |
TERMINATED | 终止态 |
RUNNABLIE | 就绪态 |
WAITING | 死等状态 |
TIMED_WAITING | 带有超时时间的等待 |
BLOCKED | 因锁竞争的阻塞 |
后面三种,大的方向都是阻塞状态
下面每个状态一一介绍,产生状态的原因。每种状态都会用代码使用,和在调用堆栈中观察,如果可以使用代码观察(使用getState()查看线程状态),也会一起展示。
1.NEW
(1)小写:new 中文名字:新建态
(2)产生时间/原因:在Thread对象产生后,为start()之前
(3)线程状态解释:此时的线程并未真正的产生,只存在硬盘中,当start()后,线程才真正产生(处于cpu上)
(4)代码观察状态
public static void main(String[] args) {Thread t = new Thread();System.out.println(t.getState());
}
(5)使用调用堆栈观看
public static void main(String[] args) throws InterruptedException {Thread t = new Thread("ttttt");System.out.println(t.getState());Thread.sleep(30000);}
让main线程休眠30秒,好让观察
此时,只能看到main线程,而看不到ttttt线程,原因就是此时只是创造出Thread对象,线程处于新建态,还未调度到cpu上执行
2.TERMINATED
(1)小写:terminated 中文:终止态
(2)产生原因:线程开始后,线程完成了任务之后,线程从pcb中被销毁
(3)代码观察
1)第一种
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{System.out.println("执行完这个语句t线程就结束");});System.out.println(t.getState());t.start();Thread.sleep(3000);System.out.println(t.getState());}
等待三秒后再去获取t线程的状态
2)第二种实现
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{System.out.println("执行完这个语句t线程就结束");});System.out.println(t.getState());t.start();t.join();System.out.println(t.getState());}
使用t.join(),就可以保证在t线程结束之后再去执行后面的语句(推荐)
(4)使用调用堆栈观察
使用堆栈无法观察,因为这个状态是线程终止态,线程已经被销毁了,不复存在。
3.RUNNABLE
(1)小写:runnable 中文:就绪状态
(2)产生原因有两种
1)这个线程正在CPU上执行
2)这个线程不在CPU上执行,但是可以随时调度到CPU上执行
下面我们只去研究第一种,因为第二种我们无法干预,也无法展示
(3)代码观察
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{try {Thread.sleep(20000);} catch (InterruptedException e) {throw new RuntimeException(e);}});System.out.println(t.getState());t.start();System.out.println(t.getState());}
此时线程还是休眠中,任务还没结束,因此展示出来的就是RUNNABKE
(4)在调用堆栈中
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true) {}});System.out.println(t.getState());t.start();System.out.println(t.getState());}
在t线程中,我们使用死循环的写法。如果是休眠时间,但调用堆栈中会是:带有超时时间的等待状态。
4.WAITING
(1)小写:waiting 名称:等待
(2)原因:由于死等进入的等待状态。其中有两个典型的例子
1)由于join()进入的死等状态
2)由于wait()进入的死等状态(在线程安全问题部分会有学习)
(3)因join()进入的WAITING状态
由于要执行完t.join()才会去执行后面的语句,所以代码层面观察不到线程的状态
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("1");}});t.start();t.join();System.out.println(Thread.currentThread().getState());}
使用调用堆栈观察:
(4)因wait()进入的等待状态
这个方法居然做什么的,目前先不做具体的解释
可见,线程的WAITING状态是由于两种死等的方法造成的
5.TIMED_WAITING
(1)名称:带有超时时间的等待
(2)产生原因:由带参数的:join()、sleep()、waiting()方法而进入的带有超时时间的等待。
(3)第一种:由sleep()进入的等待
(4)第二种:带有时间的join()
(5)第三种:带有时间的wait()
以上就是进入TIMED_WAITING状态的三种方式
6.BLOCKED
(1)原因由来:由于锁竞争产生的阻塞
(2)代码展示
public static void main(String[] args) {String str = "520";Thread t1 = new Thread(()->{synchronized (str) {while (true) {}}});t1.start();Thread t2 = new Thread(()->{synchronized (str) {System.out.println("1");}});t2.start();System.out.println(t2.getState());}
由于t1线程已经对str对象上锁,当t2再次想对str对象加锁时,就会产生互斥,从而进入阻塞状态。
7.状态总结(超级重要)
7.1.部分总结
(1)RUNNABLE状态
原因1:这个线程正在cpu上执行,也就是线程正在执行
原因2:这个线程暂时未在cpu上执行,但是可以随时调度到cpus上执行
(2)阻塞状态
阻塞状态的作用:进入阻塞状态的线程,暂时不能调度到cpu上执行,直到满足一定的条件,才会重新进入就绪状态。
阻塞状态有三个:WAITING、TIMED_WAITING、BLOCKED
三种状态都是阻塞状态,但是产生的原因不一样
7.2.表格总结
状态 | 产生原因1 | 产生原因2 | 产生原因3 |
---|---|---|---|
NEW | Thread对象创建时 | ||
TERMINATED | 内核中的线程(PCB)被销毁(线程结束时) | ||
RUNNABLE | 线程正在cpu上运行时 | 线程可以随时调度到cpu运行(不可干预) | |
WAITING | join() | wait() | |
TIMED_WAITING | sleep(参数) | join(参数) | wait(参数) |
BLOCKED | 对同一个锁竞争 |