线程常用方法你了解吗(join、yield、interrupt)
线程打断,打断调用者线程,但是 interrupt 方法和 stop 方法有本质区别,它并不会向 stop 方法一样直接中断一个线程的运行,调用 interrupt 方法的线程会继续运行下去,但是会设置一个中断标志 true,由调用者线程决定什么时候来来判执行线程中断,,,,,,,,,,,,,,,,,,。很明显使用 yield 方法,同样的逻辑处理,消耗的时间远大于使用了 yield 方法,证明
前言:
理解线程常用方法,有助于我们对多线程有更深入的理解,你都知道哪些线程常用的方法,各自又有什么作用?本文将对线程 join、yield、interrupt 方法进行详解。
常用方法:
- sleep:线程休眠。
- wait:线程等待。
- notify:单个线程唤醒。
- notifyAll:唤醒所有线程。
- join:线程强占。
- yield:线程让步。
- interrupt:线程打断。
线程常用方法你了解吗(sleep、wait、notify、notifyAll)
join 方法:
线程强占(谁调用 join 方法,谁就强占 cpu 资源,直到调用者执行结束),就是调用该方法的线程强占 cpu 时间片,join 方法 jdk 源码注释中描述的 等待该线程死亡,该线程值指的就是调用 join 方法的线程,也就是如果调用了 join 方法,那么 join 方法后面的代码,要等调用线程执行完毕后才可以得到执行。
源码简单分析:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
//判断线程状态 如果线程活着 就返回true 否则返回 false
public final native boolean isAlive();
分析:根据源码我们可以知道 join 方法底层还是调用的 wait 方法,同时 join 方法中有一个 isAlive 方法,判断调用者线程是否存活,当调用者线程结束后,返回 false,while 循环结束(被强占 cpu 时间片的线程可以恢复执行了),注意这里的 wait(0),不是等待 0 秒,而是一直等待,直到调用 join 方法的线程执行结束,同时join 方法支持传入时间,这时候就会走另外一个分支,等待指定时间,join 方方法中的 wait 方法无需唤醒操作。
代码演示如下:
public class ThreadDemo extends Thread {
public ThreadDemo() {
}
@Override
public void run() {
System.out.println("当前线程准备进入休眠,线程名称:" + Thread.currentThread().getName()+ ",当前时间:"+ System.currentTimeMillis() / 1000);
try {
//休眠 2 秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程休眠结束,线程名称:" + Thread.currentThread().getName()+ ",当前时间:"+ System.currentTimeMillis() / 1000);
}
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始运行,线程名称:" + Thread.currentThread().getName()+ ",当前时间:"+ System.currentTimeMillis() / 1000);
ThreadDemo threadDemoOne = new ThreadDemo();
threadDemoOne.start();
//join 强占 cpu
threadDemoOne.join();
System.out.println("主线程结束运行,线程名称:" + Thread.currentThread().getName()+ ",当前时间:"+ System.currentTimeMillis() / 1000);
}
}
没有使用 join 方法执行结果:
主线程开始运行,线程名称:main,当前时间:1711184107
主线程结束运行,线程名称:main,当前时间:1711184107
当前线程准备进入休眠,线程名称:Thread-0,当前时间:1711184107
当前线程休眠结束,线程名称:Thread-0,当前时间:1711184109
使用 join 方法执行结果:
主线程开始运行,线程名称:main,当前时间:1711184126
当前线程准备进入休眠,线程名称:Thread-0,当前时间:1711184126
当前线程休眠结束,线程名称:Thread-0,当前时间:1711184128
主线程结束运行,线程名称:main,当前时间:1711184128
使用 join 方法设置 join 时间的执行结果:
主线程开始运行,线程名称:main,当前时间:1711185453
当前线程准备进入休眠,线程名称:Thread-0,当前时间:1711185453
主线程结束运行,线程名称:main,当前时间:1711185454
当前线程休眠结束,线程名称:Thread-0,当前时间:1711185455
执行结果分析:注意观察执行结果中的时间(秒),根据执行结果可知,没有使用 join 时候,main 线程先执行结束,使用了 join,main 线程最后执行完,而设置了 join 时间的时候,我们发现时间到了,主线程就重新获得了 cpu 时间片,我们知道在很多情况下,都是通过主线程创建子线程的并启动子线程的,如果子线程中的业务需要比较长的时间,这时主线程往往会比子线程先结束,就会导致有时候主线程想获取子线程执行的业务结果,但是却获取不到,这个时候,就通过join方法来解决这个问题。
yield 方法:
线程让步,简单来说就是线程让出自己的 cpu 时间片,源码注释翻译为当前线程向调度程序申请让出对 cpu 处理器的使用,简单说就是主动放弃已经获取到的 cpu 时间片的执行权,执行 yield() 方法的线程状态会从 Running(运行中) 执行后会变为 Ready(就绪) 状态,此时其它处于 Ready 状态的线程可能获取到 CPU 时间片,也有可能是当前调用 yield() 方法的线程再次获得。
源码如下:
public static native void yield();
分析源码:
- public static :静态方法,是 Thread 类的方法,可以通过类名调用。
- native :yield 是一个 native 方法,简单理解就是操作系统的方法。
- yield 方法没有异常抛出。
- yield 方法结束后就转入就绪的状态。
- yield 只会给优先级相同或者优先级更高的线程让步。
yield 方法效果测试:
场景设计:yield 方法既然是让出自己的 cpu 时间片,那如果做一个相同的操作,直接执行完和让出 cpu 时间片,消耗的时间肯定不一样,按理论分析,不使用 yield 方法消耗的时间要小于使用 yield 方法消耗的时间,我们做如下测试。
测试代码:
public class ThreadDemo extends Thread {
public ThreadDemo() {
}
@Override
public void run() {
long start = System.currentTimeMillis() ;
System.out.println("当前线程准备进入计算,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis() );
for (int a = 0; a < 1000000000; a++) {
a = a + 1;
//Thread.yield();
}
System.out.println("当前线程计算结束,线程名称:" + Thread.currentThread().getName() + ",消耗时间:" + (System.currentTimeMillis() - start));
}
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始运行,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis() );
ThreadDemo threadDemoOne = new ThreadDemo();
threadDemoOne.start();
System.out.println("主线程结束运行,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis() );
}
}
没有使用 yield 方法测试结果:
主线程开始运行,线程名称:main,当前时间:1711187136105
主线程结束运行,线程名称:main,当前时间:1711187136106
当前线程准备进入计算,线程名称:Thread-0,当前时间:1711187136107
当前线程计算结束,线程名称:Thread-0,消耗时间:4
使用 yield 方法测试结果:
主线程开始运行,线程名称:main,当前时间:1711288301021
主线程结束运行,线程名称:main,当前时间:1711288301022
当前线程准备进入计算,线程名称:Thread-0,当前时间:1711288301022
当前线程计算结束,线程名称:Thread-0,消耗时间:37263
结果分析:
很明显使用 yield 方法,同样的逻辑处理,消耗的时间远大于使用了 yield 方法,证明 yield 方法生效,在实际的开发场景中,yield 方法的使用场景比较少。
interrupt 方法:
线程打断,打断调用者线程,但是 interrupt 方法和 stop 方法有本质区别,它并不会向 stop 方法一样直接中断一个线程的运行,调用 interrupt 方法的线程会继续运行下去,但是会设置一个中断标志 true,由调用者线程决定什么时候来来判执行线程中断。
使用 stop 方法中断线程会有什么问题?
使用 stop 方法虽然可以强行终止正在运行的线程,但使用 stop 方法是有隐患的,stop 方法杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁,这个后果就很严重了。
打断分类:
- 打断正在运行的线程,interrupt 方法去打断正在运行线程时,被打断的线程会继续运行,但是该线程的打断标记会更新为true,可以根据打断标记来作为判断条件使得线程停止。
- 打断处于阻塞状态的线程,interrupt 方法打断阻塞状态的线程时,会抛出异常,然后重置状态为 false,因此线程虽然被打断,但是打断标记依然为false。
打断正在运行的线程:
public class ThreadDemo extends Thread {
public ThreadDemo() {
}
@Override
public void run() {
System.out.println("执行业务,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis() / 1000);
System.out.println("循环开始前打断标记为:" + Thread.currentThread().isInterrupted());
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程死循环了,线程中断标记:" + Thread.currentThread().isInterrupted());
}
System.out.println("循环结束后打断标记为:" + Thread.currentThread().isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始运行,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis());
ThreadDemo threadDemoOne = new ThreadDemo();
threadDemoOne.start();
//睡眠1毫秒 让线程跑一会儿
Thread.sleep(1);
//打断线程
threadDemoOne.interrupt();
System.out.println("主线程结束运行,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis());
}
}
演示结果:
主线程开始运行,线程名称:main,当前时间:1711291731296
执行业务,线程名称:Thread-0,当前时间:1711291731
循环开始前打断标记为:false
线程死循环了,线程中断标记:false
线程死循环了,线程中断标记:false
。。。。。。
线程死循环了,线程中断标记:false
线程死循环了,线程中断标记:false
主线程结束运行,线程名称:main,当前时间:1711291731299
循环结束后打断标记为:true
结果分析:可以看到,我们启动子线程后,让主线程睡眠了 1 毫秒再执行子线程打断操作,结果也是如我们所料,还是现在执行了部分循环,后续自动停止了。
打断处于阻塞状态的线程:
public class ThreadDemo extends Thread {
public ThreadDemo() {
}
@Override
public void run() {
System.out.println("执行业务,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis() / 1000);
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程死循环了,线程中断标记:" + Thread.currentThread().isInterrupted());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("循环结束后打断标记为:" + Thread.currentThread().isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始运行,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis());
ThreadDemo threadDemoOne = new ThreadDemo();
threadDemoOne.start();
//睡眠1毫秒 让线程跑一会儿
Thread.sleep(1);
//打断线程
threadDemoOne.interrupt();
System.out.println("主线程结束运行,线程名称:" + Thread.currentThread().getName() + ",当前时间:" + System.currentTimeMillis());
}
}
演示结果:
结果分析:
- 这里要使用 debugger 调试,才可以打印出为 true 的结果。
- 可以很明显的看到,异常抛出之后,就无法获取到打断标志为 ture 的结果了,证实了抛出异常重置打断标注状态为 false。
终止线程的方式:
- 线程运行完毕正常结束。
- 共享变量结束线程,有些情况,线程需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程,我们使用某个变量,通过对变量值的判断来控制线程结束。
- 上面讲到的使用 interrupt 方法结束线程。
- 使用 stop 方法结束线程,不推荐使用,stop 方法结束线程有很大的风险,比如线程持有的锁没有被释放,被 stop 后会导致死锁发生。
如有错误的地方欢迎指出纠正。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)