暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

线程执行完isAlive方法返回true问题

马士兵 2021-07-08
480

今天我们来分享两道BT的多线程经典面试题,作者是黄俊老师。

黄俊老师,95后、23岁拿到阿里60w年薪,之后在美团拿到百万年薪,现在一心只想钻研技术。

由于文章篇幅较长,我们把两个问题分开来讲,避免有小伙伴搞不懂。

第一篇请点击线程唤醒问题

接下来我们来看第二个问题:线程执行完isAlive方法返回true问题

样例代码:

    public class ThreadAliveTest {


    public static void main(String[] args) throws InterruptedException {


    Thread t1 = new Thread(() -> {
    System.out.println("t1 start");
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("t1 end");
    });
    t1.start();
    Thread t2 = new Thread(() -> {
    synchronized (t1) {
    System.out.println("t2 start");
    try {
    Thread.sleep(5000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("t1 isAlive:" + t1.isAlive());
    }
    });
    t2.start();
    }
    }
    输出结果:
      t1 start
      t2 start
      t1 end
      t1 isAlive:true

      问题:

      为什么线程结束了,isAlive方法还返回true

      分析:

      我们首先看看执行流程,线程T1启动后将会睡眠2秒,随后2秒后执行结束,随后线程T2启动,T2首先获取到T1的对象锁,然后睡眠5秒,随后调用T1的isAlive方法判定线程是否存活,那么为什么会输出true呢?
      我们还得先看看isAlive方法如何实现的。我们来看源码。
        public final native boolean isAlive();
        首先看到isAlive方法由JNI方法实现。我们来看Hotspot源码。
          JVM_ENTRY(jboolean, JVM_IsThreadAlive(JNIEnv* env, jobject jthread))
          JVMWrapper("JVM_IsThreadAlive");
          oop thread_oop = JNIHandles::resolve_non_null(jthread);
          return java_lang_Thread::is_alive(thread_oop);
          JVM_END
          我们看到首先通过resolve_non_null方法将jthread转为oop对象thread_oop,随后调用java_lang_Thread的is_alive方法来判断是否存活,我们继续跟进。
            bool java_lang_Thread::is_alive(oop java_thread) {  JavaThread* thr = java_lang_Thread::thread(java_thread);  return (thr != NULL);}JavaThread* java_lang_Thread::thread(oop java_thread) {  return (JavaThread*)java_thread->address_field(_eetop_offset);}
            我们看到最后是通过获取java thread对象,也即java的Thread类中的eetop属性,如果该属性为null,那么表明线程已经销毁,也即返回false,如果eetop还在那么返回true,表明线程存活。
            那么什么是eetop呢?我们还得从线程创建方法入手。
              JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
              JVMWrapper("JVM_StartThread");
              JavaThread *native_thread = NULL;
              bool throw_illegal_thread_state = false; // 非法线程状态标识
              {
              // Threads_lock上锁,保证C++的线程对象和操作系统原生线程不会被清除。当前方法执行完,也就是栈帧释放时,会释放这里的锁,当然肯定会调用析构函数,而这个对象的析构函数中调用unlock方法释放锁
              MutexLocker mu(Threads_lock);
              if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) { // 如果线程不为空,则表明线程已经启动,则为非法状态
              throw_illegal_thread_state = true;
              } else {
              // 本来这里可以检测一下stillborn标记来看看线程是否已经停止,但是由于历史原因,就让线程自己玩了,这里就不玩了
              // 取得线程对象的stackSize的大小
              jlong size = java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
              // 开始创建C++ Thread对象和原生线程对象,使用无符号的线程栈大小,所以这里不会出现负数
              size_t sz = size > 0 ? (size_t) size :0;
              // 创建JavaThread,这里的thread_entry为传入的运行地址,也就是启动线程,需要一个入口执行点,这个函数地址便是入口执行点
              native_thread = new JavaThread(&thread_entry, sz);
              // 如果osthread不为空,则标记当前线程还没有被使用
              if (native_thread->osthread() != NULL) {
              native_thread->prepare(jthread);
              }
              }
              }
              // 如果throw_illegal_thread_state不为0,那么直接抛出异常
              if (throw_illegal_thread_state) {
              THROW(vmSymbols::java_lang_IllegalThreadStateException());
              }
              // 原生线程必然不能为空,因为线程是由操作系统创建的,所以没有OS线程,空有个JavaThread类有啥用0.0
              if (native_thread->osthread() == NULL) {
              delete native_thread; // 直接用C++的delete释放内存
              THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),"unable to create new native thread");
              }
              Thread::start(native_thread); // 一切准备妥当,开始启动线程
              JVM_END
              我们看到首先创建了JavaThread对象,该对象内部创建了OSThread对象,我们这么理解:JavaThread代表了C++层面的Java线程,而OSThread代表了操作系统层面的线程对象。
              随后调用了native_thread->prepare(jthread)方法为启动线程做准备。我们关注该方法。
                void JavaThread::prepare(jobject jni_thread, ThreadPriority prio) {
                // 包装当前Java线程对象
                Handle thread_oop(Thread::current(),
                JNIHandles::resolve_non_null(jni_thread));
                // 将Java层面的线程Oop对象与JavaThread C++层面的对象关联
                set_threadObj(thread_oop());
                java_lang_Thread::set_thread(thread_oop(), this);
                // 设置优先级
                if (prio == NoPriority) {
                prio = java_lang_Thread::priority(thread_oop());
                }
                Thread::set_priority(this, prio);
                // 将JavaThread类放入到全局线程列表中
                Threads::add(this);
                }
                我们注意看 java_lang_Thread::set_thread方法。我们跟进它的源码。
                  void java_lang_Thread::set_thread(oop java_thread, JavaThread* thread) {
                  // 将JavaThread C++层面的线程对象设置为Java层面的Thread oop对象的eetop变量
                  java_thread->address_field_put(_eetop_offset, (address)thread);
                  }
                  这下我们知道了eetop变量即使JavaThread对象的地址信息。
                  在了解完eetop如何被设置之后我们得继续看,eetop什么时候被取消。
                  当Java线程执行完Runnable接口的run方法最后一个字节码后,将会调用exit方法。
                  该方法完成线程对象的退出和清理操作,我们重点看ensure_join方法。
                    void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
                    ...
                    ensure_join(this);
                    ...
                    }
                    我们继续跟进ensure_join的源码实现。
                      static void ensure_join(JavaThread* thread) {
                      // 封装Java Thread线程oop对象
                      Handle threadObj(thread, thread->threadObj());
                      // 获取Java Thread线程oop对象锁
                      ObjectLocker lock(threadObj, thread);
                      // 清除未处理的异常信息
                      thread->clear_pending_exception();
                      // 将状态修改为TERMINATED
                      java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
                      // 将Java Thread线程oop对象与JavaThread C++对象解绑
                      java_lang_Thread::set_thread(threadObj(), NULL);
                      // 唤醒所有阻塞在线程对象的线程
                      lock.notify_all(thread);
                      // 如果以上代码期间发生异常,那么清理挂起的异常
                      thread->clear_pending_exception();
                      }
                      我们看到最终由ensure_join方法中的
                      java_lang_Thread::set_thread(threadObj(), NULL),将eetop变量设置为null。
                      当执行完这一步时,我们再通过isAlive方法判断线程是否存活时,将返回false,否则返回true。
                      而我们看到在操作该变量时需要获取线程对象锁。我们来看ObjectLocker的构造函数和析构函数的实现。
                        ObjectLocker::ObjectLocker(Handle obj, Thread* thread, bool doLock) {
                        _dolock = doLock;
                        _thread = thread;
                        if (_dolock) {
                        // 获取Java Thread线程oop对象锁
                        ObjectSynchronizer::fast_enter(_obj, &_lock, false, _thread);
                        }
                        }


                        ObjectLocker::~ObjectLocker() {
                        if (_dolock) {
                        // 释放Java Thread线程oop对象锁
                        ObjectSynchronizer::fast_exit(_obj(), &_lock, _thread);
                        }
                        }
                        我们看到当我们创建ObjectLocker对象时,会在构造函数中获取到线程对象锁,而当ensure_join方法执行完毕后,将会调用ObjectLocker的析构函数,在该函数中释放线程对象锁。

                        总结:

                        这下我们就可以通过以上知识来分析为何isAlive方法在线程执行完毕后仍然返回true了。
                        这是用于isAlive方法通过判断Java线程对象的eetop变量来判定线程是否存活,而当我们线程执行完毕后将会调用exit方法,该方法将会调用ensure_join方法,在该方法中将eetop甚至为null。
                        但是由于赋值前需要获取到Java线程的对象锁,而该对象的对象锁已经由线程T2持有,这时当前线程将会阻塞,从而造成eetop变量没有被清除,从而导致isAlive方法在T1线程执行完毕后仍然返回true。
                        读者也可以看看java Thread的源码,join函数也是通过对Thread对象获取锁然后调用isAlive来判定线程是否结束的。
                        这就意味着如果我们用别的线程持有了Java Thread的对象锁,那么这时调用join方法的线程也是会被阻塞的。
                        关于作者:
                        ——————
                        进行业交流群

                        👇推荐关注👇

                        有趣的行业资讯

                        干货技术分享

                        程序员的日常生活

                        ......

                        干就完了
                        文章转载自马士兵,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                        评论