Thread.join方法详解

一.场景

你可能会被经常问到如何线程有序执行,如何让当前线程等待其他线程执行完成,你可能会想到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使用的同步锁是否被其他线程占用,如果被占用,那么需要等待其他线程释放锁才能继续运行。

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值