一、常用方法的基本使用
1、sleep
(1)调用sleep会让当前线程从Running 进入Timed Waiting(阻塞)状态
Thread t = new Thread(()->{
log.debug("running");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("end-sleep");
}
},"t1");
System.out.println(t.getState()); //线程刚创建时的状态 --- NEW
t.start();
System.out.println(t.getState()); //启动了线程,线程进入就绪状态 --- RUNNABLE
Thread.sleep(1000);
System.out.println(t.getState()); // 此处让main线程先休眠了1s,待t线程开始执行run方法,t线程休眠了2s,因此main线程休眠1s后查看t线程状态,可以得知t线程为TIMED_WAITING状态
(2)其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时sleep 方法会抛出InterruptedException
此处当t线程启动后,会首先运行Runnable中的run方法,打印running-1日志,之后t线程会休眠2s,主线程休眠1s,待主线程休眠结束,会打断t线程的休眠状态,抛出IllegalInterruptException异常,被InterruptException捕获到,执行打印end-sleep日志,这时由于提前打断了t线程的休眠抛出异常,直接进入catch语句,导致log.debug("running-2");没有执行。
【注意】:打断线程休眠后,抛出异常,如果线程后还有代码可以继续执行
Thread t = new Thread(()->{
log.debug("running-1");
try {
Thread.sleep(2000);
log.debug("running-2");
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("end-sleep");
}
log.debug("continue...");
},"t1");
t.start();
Thread.sleep(1000);
log.debug("interrupt");
t.interrupt();
此处演示,主线程休眠时间过长,没有执行打断t线程操作,t线程先执行结束并释放掉栈
由于此处t线程休眠没有打断,其自然醒后,会执行running-2方法,由于没有捕获到抛出的illegalInterruptException异常,因此该t线程正常结束,之后主线程休眠结束,执行log.debug("interrupt"); 而t.interrupt(),打断没有效果。
Thread t = new Thread(()->{
log.debug("running-1");
try {
Thread.sleep(2000);
log.debug("running-2");
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("end-sleep");
}
log.debug("continue...");
},"t1");
t.start();
Thread.sleep(4000);
log.debug("interrupt");
t.interrupt();
(3)睡眠结束后的线程未必会立刻得到执行
有可能线程结束睡眠后,当前CPU正在执行其他线程无法执行当前线程,所以该线程需要等待操作系统的任务调度器把新的时间片分给这个线程以后,它才能继续执行,不是说这个线程睡眠结束就会获得CPU的使用权并立马执行
(4)建议用TimeUnit的sleep 代替Thread的sleep 来获得更好的可读性
TimeUnit把时间单位划分的很清楚,还是比较建议使用TimeUnit
TimeUnit.SECONDS.sleep(1); //此处让线程休眠1s(SECOND限制时间单位为:秒)
TimeUnit内部也是调用了Thread.sleep()方法,只是相比于它多了单位的换算。
2、yield
(1)调用vield 会让当前线程从 Running(运行)状态进入Runnable (就绪)状态,然后调度执行其它同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
可以把yield理解成 礼让、让出,表示当前线程的CPU使用权让出,给其他线程有机会使用CPU
(2)具体的实现依赖于操作系统的任务调度器
如果不存在其他要使用的线程,但还是调用了yield,那么任务调度器还是会把时间片分给这个线程,让这个线程继续执行。
任务调度器不会把时间片分给阻塞状态中的线程,但可以分给就绪状态的。
3、线程优先级
- 线程优先级会提示 (hint)调度器优先调度该线程,但它仅仅是一个提示,真正决定的是任务调度器,甚至任务调度器可以忽略它;
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用;
线程优先级(1~10)
无论我们如何调整线程的优先级,实则都不是真正意义上能控制线程的执行顺序,主要是看任务调度器,无论是yield也好,setPriority设置优先级也罢,都只是起到提示作用而已。
4、sleep 使用案例(避免占用CPU资源)
对于单核CPU来说,如果执行循环的线程,会导致CPU占用过高,在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序
new Thread(()->{
while(true){
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
- 可以用 wait 或条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep适用于无需锁同步的场景
5、join方法
(1)join方法同步执行线程
join方法可以让指定线程执行完才执行其他线程
代码示例:
以下代码执行后,由于主线程和t线程是并行执行,t线程睡眠了1s,导致主线程先执行完了,t线程才睡眠结束执行a的赋值操作。
public class Test1 {
public static int a = 0;
public static void main(String[] args) {
Thread t = new Thread(()->{
log.debug("start1");
try {
Thread.sleep(1000);
a = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("end");
});
t.start();
log.debug("结果为:{}",a);
}
}
这时,就可以使用join方法,虽说用sleep让主线程休眠到t线程苏醒也可以实现该条件,但不建议这么干,毕竟有时你也不知道线程具体会执行多长时间
public class Test1 {
public static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("start1");
try {
Thread.sleep(1000);
a = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("end");
});
t.start();
t.join();
log.debug("结果为:{}",a);
}
}
以调用方角度来讲,如果
需要等待结果返回,才能继续运行就是同步
不需要等待结果返回,就能继续运行就是异步
(2)有时效的join方法
超过等待时间就不继续等待了,直接往下执行.
示例代码:
当等待时间结束,线程还没执行完,就不管该线程直接往下继续执行了.因此间隔时间为2s不是4s,且结果a没有被赋值.
public class Test1 {
public static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("start1");
try {
Thread.sleep(4000);
a = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("end");
});
long start = System.currentTimeMillis();
t.start();
t.join(2000);
log.debug("结果为:{}",a);
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
下述代码把join等待时间改为4s,但线程休眠时间只有2s,那么线程休眠结束执行完线程,也就释放返回了,无需再继续等待了线程.
public class Test1 {
public static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("start1");
try {
Thread.sleep(2000);
a = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("end");
});
long start = System.currentTimeMillis();
t.start();
t.join(4000);
log.debug("结果为:{}",a);
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
6、interrupt方法
interrupt方法可以打断 sleep,wait, join 的线程,这三个方法,都会让线程进入阻塞状态(操作系统的任务调度器不管这些状态的线程,也就不会主动分配时间片给他们);当然,interrupt方法也可以打断正在运行的线程
(1)interrupt打断阻塞线程
打断sleep 的线程,会清空打断状态,以sleep 为例:
注意: sleep、wait、join这三个方法使线程进入阻塞状态后,interrupt会打断对应阻塞状态的线程,由于阻塞状态的线程被打断后会抛出异常(它们是以异常的方式来表示被打断的),此时的打断标记就不再为true,会被置为false. 因此,我们可以通过是否抛出异常,来得知当前睡眠的线程是否被打断
public class Test1 {
public static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("start");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("end");
});
t.start();
Thread.sleep(1000);
log.debug("interrupt...");
t.interrupt();
log.debug("interrupt:{ "+t.isInterrupted()+" }");
}
}
(2)interrupt打断正常线程
使用interrupt打断正常状态的线程,无法让指定线程停止,只能对其起到提醒作用罢了.当一个线程打断另外一个正常状态的线程,只是提示指定线程要被打断,但不会真的被打断. 但是调用interrupt方法后,会让对应线程的置一个打断标记.我们可以根据打断标记去停止线程.
public class Test1 {
public static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("start...");
while(true){
log.debug("running...");
}
});
t.start();
log.debug("interrupt....");
t.interrupt();
}
}
从此处可以看出,interrupt确实没办法停止线程.
我们可以根据线程的打断标记,来判断是否停止结束线程的执行,以下述代码为例:
public class Test1 {
public static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("start...");
while(true){
log.debug("running...");
Thread temp = Thread.currentThread(); //当前线程
boolean interrupted = temp.isInterrupted(); //打断标记
if(interrupted){
break;
}
}
});
t.start();
log.debug("interrupt....");
t.interrupt();
}
}
7、🎉【设计模式】两阶段终止模式
Two Phase Termination
在一个线程T1中如何“优雅”终止线程T2?这里的[优雅]指的是给T2一个料理后事的机会。
- 错误思路
(1)使用线程对象的stop方法停止线程
stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
(2)使用System.exit(int)方法停止线程
目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止模式场景分析:
@Slf4j
public class Test2 {
public static void main(String[] args) throws InterruptedException {
DoublePhraseModes.start(); //启动监控线程
Thread.sleep(3500);
DoublePhraseModes.end();
}
}
@Slf4j
//监控线程
class DoublePhraseModes{
public static Thread thread;
public static void start(){
thread = new Thread(()->{
Thread currentThread = Thread.currentThread();
while(true){
if(currentThread.isInterrupted()){
//判断当前线程是否被打断
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("监控线程....");
} catch (InterruptedException e) {
//由于休眠时,抛出异常,没有设置打断标记为true,需要此处手动设置,否则下次循环还是会继续执行循环语句
e.printStackTrace();
log.debug("休眠时被打断");
currentThread.interrupt();
}
}
});
thread.start();
}
//中断线程
public static void end(){
thread.interrupt();
}
}
8、打断park线程
LockSupport.park()也是一种让线程打断的方法。其不会清空打断标记。
@Slf4j
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("running...");
LockSupport.park();
log.debug("park....");
});
t.start();
Thread.sleep(2000);
}
}
此处执行完LockSupport.park();该线程被中断后续不会继续执行(相当于该线程被停止往下执行)。如果使用了interrupt,可以打断park状态,从而继续执行线程的下述方法。
@Slf4j
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("running...");
LockSupport.park();
log.debug("park....");
System.out.println("打断标记:"+(Thread.currentThread()).isInterrupted());
});
t.start();
Thread.sleep(2000);
t.interrupt();
}
}
【注意】:如果在同一个线程中使用了两个park打断线程,由于第一次park打断,在main线程中使用了interrupt打断后,打断标记为true,下一次使用park打断线程时,会因为打断标记为true的原因,导致不执行park打断线程。线程还是会正常继续往后执行。
@Slf4j
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("running...");
LockSupport.park();
log.debug("park....");
System.out.println("打断标记:"+(Thread.currentThread()).isInterrupted());
LockSupport.park();
log.debug("last.park....");
});
t.start();
Thread.sleep(2000);
t.interrupt();
}
}
为了避免这种情况,可以使用interrupted()方法,在获取打断标记后,对打断标记进行清空。
@Slf4j
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("running...");
LockSupport.park();
log.debug("park....");
System.out.println("打断标记:"+(Thread.currentThread()).interrupted());
LockSupport.park();
log.debug("last.park....");
});
t.start();
Thread.sleep(2000);
t.interrupt();
}
}
9、不推荐停止线程的方法
以下不推荐使用的方法,这些方法已过时,容易破坏同步代码块,使得线程中得锁资源得不到释放,造成线程死锁
方法名 | 功能说明 | |
stop() | static | 停止线程运行 |
suspend() | static | 挂起(暂停)线程运行 |
resume() | static | 恢复线程运行 |
二、 主线程与守护线程及线程状态
1、主线程和守护线程
当java程序启动时,就会启动对应得main主线程,如果java程序中存在多个线程,那么即使main主线程运行完,其他线程没有执行完,那么java程序就不会结束。这些其他线程为默认为非守护线程。
而对于守护线程,即使守护线程没有执行完,main主线程执行完以及其他非守护线程执行完,java程序就会强制结束,不会等待守护线程执行结束才结束。
总的来说,默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
应用场景:
(1)java中的垃圾回收器就是守护线程,(当堆中的分配对象没有被其他对象引用时,在一定的时间后垃圾回收器就会对这些没有使用的对象进行垃圾回收。主线程运行结束,即使垃圾回收器没有运行结束,也会随着主线程执行的结束而结束执行,并释放资源)
2、线程的五种运行状态
以操作系统角度,划分线程的五种运行状态
- [初始状态]仅是在语言层面创建了线程对象,还未与操作系统线程关联
- [可运行状态] (就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行[运行状态]指获取了CPU 时间片运行中的状态
- ·当CPU 时间片用完,会从[运行状态] 转换至[可运行状态],会导致线程的上下文切换
- [阻塞状态]·如果调用了阻塞API,如 BIO 读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入[阻塞状态]
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至[可运行状态]
与[可运行状态]的区别是,对[阻塞状态]的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- [终止状态]表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
3、线程的六种运行状态
java中线程所存在的六种运行状态
- NEW :线程刚被创建,但是还没有调用start()方法
- RUNNABLE :当调用了 start()方法之后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的[可运行状态]、[运行状态]和[阻塞状态] (由于 BIO 导致的线程阻塞,在Java 里无法区分,仍然认为是可运行)
- BLOCKED,WAITING,TIMED_WAITING :都是JavaAPI层面对(阻塞状态]的细分
- TERMINATED:当线程代码运行结束