一.场景
你可能会被经常问到如何线程有序执行,如何让当前线程等待其他线程执行完成,你可能会想到join方法,那join方法具体是怎么使用呢?我们看看下面的案例分析来研究一下join方法的使用。
二.案例分析
案例1:
public class JoinTest1 extends Thread{
public void run(){
System.out.println(Thread.currentThread().getName()+":begin");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":end");
}
public static void main(String[] args) throws InterruptedException {
JoinTest1 joinTest1 = new JoinTest1();
joinTest1.setName("childThread");
long a = System.currentTimeMillis();
joinTest1.start();
joinTest1.join();
long b = System.currentTimeMillis();
System.out.println("等待时间:"+(b-a));
System.out.println("end----------------");
}
}
执行结果:
childThread:begin
childThread:end
等待时间:2003
end----------------
Process finished with exit code 0
主线程等待子线程睡眠2秒钟之后继续执行,结果没有问题
案例2:
public class JoinTest2 extends Thread{
public void run(){
System.out.println(Thread.currentThread().getName()+":begin");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":end");
}
public static void main(String[] args) throws InterruptedException {
JoinTest2 joinTest2 = new JoinTest2();
joinTest2.setName("childThread");
long a = System.currentTimeMillis();
joinTest2.start();
joinTest2.join(500);
long b = System.currentTimeMillis();
System.out.println("等待时间:"+(b-a));
System.out.println("end----------------");
}
}
执行结果:
childThread:begin
等待时间:501
end----------------
childThread:end
Process finished with exit code 0
join(long millis)的方法在时间到了之后结束了等待,主线程继续执行
案例3:
public class JoinTest3 extends Thread{
public void run(){
System.out.println(Thread.currentThread().getName()+":begin");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":end");
}
public static void main(String[] args) throws InterruptedException {
JoinTest3 joinTest3 = new JoinTest3();
joinTest3.setName("childThread");
long a = System.currentTimeMillis();
joinTest3.start();
joinTest3.join(5000);
long b = System.currentTimeMillis();
System.out.println("等待时间:"+(b-a));
System.out.println("end----------------");
}
}
执行结果:
childThread:begin
childThread:end
等待时间:2005
end----------------
Process finished with exit code 0
主线程执行了joinTest3.join(5000),虽然设置的超时时间是5000ms,但是子线程在2000ms的时候已经执行结束,所以主线程不会继续阻塞。
案例4:
public class JoinTest4 extends Thread{
public void run(){
System.out.println(Thread.currentThread().getName()+":begin");
synchronized (this){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":end");
}
public static void main(String[] args) throws InterruptedException {
JoinTest4 joinTest4 = new JoinTest4();
joinTest4.setName("childThread");
long a = System.currentTimeMillis();
joinTest4.start();
joinTest4.join(500);
long b = System.currentTimeMillis();
System.out.println("等待时间:"+(b-a));
System.out.println("end----------------");
}
}
执行结果:
childThread:begin
childThread:end
等待时间:2005
end----------------
Process finished with exit code 0
这个时候可能会有疑问呢,为什么主线程明明调用了joinTest4.join(500)方法,正常应该是在500ms左右停止阻塞,可实际情况并非如此,不要急,我们先继续看下面的例子。
案例5:
public class JoinTest5 extends Thread{
private String lock = "111";
public void run(){
System.out.println(Thread.currentThread().getName()+":begin");
synchronized (lock){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":end");
}
public static void main(String[] args) throws InterruptedException {
JoinTest5 joinTest5 = new JoinTest5();
joinTest5.setName("childThread");
long a = System.currentTimeMillis();
joinTest5.start();
joinTest5.join(500);
long b = System.currentTimeMillis();
System.out.println("等待时间:"+(b-a));
System.out.println("end----------------");
}
}
执行结果:
childThread:begin
等待时间:501
end----------------
childThread:end
Process finished with exit code 0
这次的结果跟主线程只阻塞了500ms就继续往下执行了,我们对比下案例5和案例4,2者唯一的差别是synchronized锁住的对象不同,一个锁住的是this,一个锁住的是成员变量,看到这里,我们需要研究一下join的源码来仔细研究下。
三.join源码分析
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;
}
}
}
在join没有参数的时候执行的是join(0),否者执行的是join(time)。从代码里面可以看到最终的逻辑都是落在判断当前线程是否存活,如果存活则执行wait(time)方法。
我们继续来看wait方法,由于wait方法是native方法,我们直接查看wait方法的注释
找了一个中文注释如下:
描述:
1、调用该方法,导致当前线程(用T表示)阻塞,进入等待状态。
当前线程T会把自己放入 等待集合 中(等待 obj.wait()的obj对象)。
2、当前线程(T)程释放已获取obj的锁(别的线程可以获取了,如果有)。
3、什么时候唤醒(从wait set 删除)?以下4种事件任何一个发生,即会唤醒。
a, 其它的线程调用 obj.notify(),且当前线程T,正好是被选中唤醒的。
b, 其它的线程调用 obj.notifyAll()。
c.其它线程中断T。
d.指定的等待时间(timeout)超时,(时间精度会有些误差)。
4、当前线程T被唤醒后,它被移出等待集合,重新被调度。
它需要和其它线程平等的(没有任何特权)竞争,获取obj的锁。
从描述中可以看到首先进入wait方法之后,只能上述中的4个事件唤醒。那我们回到案例2,案例2满足情况d,那案例1呢,为什么在子线程执行完成之后,wait方法跳出阻塞了?通过网上查阅资料,最终发现以下内容:
jvm调用线程的源码
void JavaThread::run() {
...
thread_main_inner();
}
void JavaThread::thread_main_inner() {
...
this->exit(false);
delete this;
}
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
...
// Notify waiters on thread object. This has to be done after exit() is called
// on the thread (if the thread is the last thread in a daemon ThreadGroup the
// group should have the destroyed bit set before waiters are notified).
ensure_join(this);
...
}
static void ensure_join(JavaThread* thread) {
// We do not need to grap the Threads_lock, since we are operating on ourself.
Handle threadObj(thread, thread->threadObj());
assert(threadObj.not_null(), "java thread object must exist");
ObjectLocker lock(threadObj, thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
// Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
// Clear the native thread instance - this makes isAlive return false and allows the join()
// to complete once we've done the notify_all below
java_lang_Thread::set_thread(threadObj(), NULL);
lock.notify_all(thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}
在jvm虚拟机执行线程的方法中,线程的run方法执行完成之后,会调用 lock.notify_all(thread)唤醒当前线程对象上面的所有阻塞线程。这也满足了上面wait方法唤醒的条件b,所以join能够在监听的线程执行完成之后唤醒主线程。
我们现在继续分析案例4和案例5,为什么案例4中,join等待的时间已经到了,满足条件d缺没有继续执行主线程呢?
真正的原因是在join时间到了之后,wait线程其实已经被唤醒,但是因为这个wait方法是在join的同步方法中调用,需要获取当前对象的锁才能继续执行,这也是为什么案例5能够直接跳出阻塞的原因(非同一把锁)。
四.总结
join方法能够让线程有序执行,但是具体的使用过程需要注意一下几点
1.join方法是通过wait方法实现阻塞的。
2.wait方法停止阻塞的条件是别的线程调用当前对象的notify或者notifyall方法(join等待的线程会在执行完run方法之后调用notifyall)或者线程中断或者超时时间到期
3.wait方法被唤醒之后,能否继续执行join方法取决于join使用的同步锁是否被其他线程占用,如果被占用,那么需要等待其他线程释放锁才能继续运行。