等待线程执行终止的join方法
在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。Thread类中有一个join方法就可以做这个事情,前面介绍的等待通知方法是Object类中的方法,而join方法则是Thread类直接提供的。join是无参且返回值为void的方法。
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threadOne over! ");
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threadTwo over! ");
}
});
//启动子线程
threadOne.start();
threadTwo.start();
System.out.println("wait all child thread over! ");
//等待子线程执行完毕,返回
threadOne.join();
threadTwo.join();
System.out.println("all child thread over! ");
}
如上代码在主线程里面启动了两个子线程,然后分别调用了它们的join()方法,那么主线程首先会在调用
threadOne.join()
方法后被阻塞,等待threadOne执行完毕后返回。threadOne执行完毕后threadOne.join()就会返回,然后主线程(main方法线程)调用threadTwo.join()
方法后再次被阻塞,等待threadTwo执行完毕后返回。这里只是为了演示join方法的作用,在这种情况下使用后面会讲到的CountDownLatch是个不错的选择。
- 线程A调用线程B的join方法后会被阻塞,当其他线程调用了线程A的
interrupt()
方法中断了线程A时,线程A会抛出InterruptedException异常而返回。
示例加深理解:
public static void main(String[] args) throws InterruptedException {
//线程one
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("threadOne begin run! ");
for (; ; ) {//死循环
}
}
});
//获取主线程
final Thread mainThread = Thread.currentThread();
//线程two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
//休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断主线程
mainThread.interrupt();
}
});
// 启动子线程
threadOne.start();
//延迟1s启动线程
threadTwo.start();
try{//等待线程one执行结束
threadOne.join();
}catch(InterruptedException e){
System.out.println("main thread:" + e);
}
}
执行结果:
如上代码在
threadOne
线程里面执行死循环,主线程调用threadOne
的join
方法阻塞自己等待线程threadOne
执行完毕,待threadTwo
休眠1s后会调用主线程的interrupt()
方法设置主线程的中断标志,从结果看在主线程中的threadOne.join()
处会抛出InterruptedException
异常。这里需要注意的是,在threadTwo里面调用的是主线程的interrupt()
方法,而不是线程threadOne
的。
让线程睡眠的sleep方法
举例说明,线程在睡眠时拥有的监视器资源不会被释放
public class SleepTest2 {
// 创建一个独占锁
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 创建线程A
Thread threadA = new Thread(new Runnable() {
public void run() {
// 用于获取独占锁
//即线程在执行这行代码时会尝试获取指定的锁对象。锁的内容是指在多线程环境下需要进行互斥访问的共享资源或临界区。
lock.lock();
try {
System.out.println("child threadA is in sleep");
Thread.sleep(10000);
System.out.println("child threadA is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
});
// 创建线程B
Thread threadB = new Thread(new Runnable() {
public void run() {
// 获取独占锁
lock.lock();
try {
System.out.println("child threadB is in sleep");
Thread.sleep(10000);
System.out.println("child threadB is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
});
// 启动线程
threadA.start();
threadB.start();
}
}
执行结果:
首先创建了一个独占锁,然后创建了两个线程,每个线程在内部先获取锁,然后睡眠,睡眠结束后会释放锁。首先,无论你执行多少遍上面的代码都是线程A先输出或者线程B先输出,不会出现线程A和线程B交叉输出的情况。如果说没有锁,两个线程会竞争CPU然后执行,运行顺序不能保证。从执行结果来看,线程A先获取了锁,那么线程A会先输出一行,然后调用sleep方法让自己睡眠10s,在线程A睡眠的这10s内那个独占锁lock还是线程A自己持有,线程B会一直阻塞直到线程A醒来后执行unlock释放锁。
- 当一个线程处于睡眠状态时,如果另外一个线程中断了它,会不会在调用sleep方法处抛出异常。
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread thread = new Thread(new Runnable() {
public void run() {
try {
System.out.println("child thread is in sleep");
Thread.sleep(10000);
System.out.println("child thread is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
thread.start();
//主线程休眠2s
Thread.sleep(2000);
//主线程中断子线程
thread.interrupt();
}
子线程在睡眠期间,主线程中断了它,所以子线程在调用sleep方法处抛出了InterruptedException异常。
注意
- 如果在调用Thread.sleep(longmillis)时为millis参数传递了一个负数,则会抛出IllegalArgumentException异常。
如下所示
让出CPU执行权的yield方法
Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
- 简单一句话:你要CPU我听不见,你要让出CPU我欢迎
当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。
public class YieldTest implements Runnable {
YieldTest() {
//创建并启动线程
Thread t = new Thread(this);
t.start();
}
public void run() {
for (int i = 0; i < 5; i++) {
//当i=0时让出CPU执行权,放弃时间片,进行下一轮调度
if ((i % 5) == 0) {
System.out.println(Thread.currentThread() + "yield cpu...");
//当前线程让出CPU执行权,放弃时间片,进行下一轮调度
// Thread.yield();
}
}
System.out.println(Thread.currentThread() + " is over");
}
public static void main(String[] args) {
new YieldTest();
new YieldTest();
new YieldTest();
}
}
执行结果:
开启了三个线程,每个线程的功能都一样,都是在for循环中执行5次打印。运行多次后,上面的结果是出现次数最多的。
解开Thread.yield()注释再执行,结果如下:
Thread.yield()方法生效了,三个线程分别在i=0时调用了Thread.yield()方法,所以三个线程自己的两行输出没有在一起,因为输出了第一行后当前线程让出了CPU执行权。
注意
一般很少使用这个方法,在调试或者测试时这个方法或许可以帮助复现由于并发竞争条件导致的问题,其在设计并发控制时或许会有用途,后面在讲解java.util.concurrent. locks包里面的锁时会看到该方法的使用。
总结:
sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态(进入到CPU轮片队列,准备执行),线程调度器下一次调度时就有可能调度到当前线程执行。