Java Thread.sleep/Thread.join/Th

作者: 小鱼人爱编程 | 来源:发表于2021-10-20 23:38 被阅读0次

    前言

    线程并发系列文章:

    Java 线程基础
    Java 线程状态
    Java “优雅”地中断线程-实践篇
    Java “优雅”地中断线程-原理篇
    真正理解Java Volatile的妙用
    Java ThreadLocal你之前了解的可能有误
    Java Unsafe/CAS/LockSupport 应用与原理
    Java 并发"锁"的本质(一步步实现锁)
    Java Synchronized实现互斥之应用与源码初探
    Java 对象头分析与使用(Synchronized相关)
    Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
    Java Synchronized 重量级锁原理深入剖析上(互斥篇)
    Java Synchronized 重量级锁原理深入剖析下(同步篇)
    Java并发之 AQS 深入解析(上)
    Java并发之 AQS 深入解析(下)
    Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
    Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
    Java 并发之 ReentrantReadWriteLock 深入分析
    Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
    Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
    最详细的图文解析Java各种锁(终极篇)
    线程池必懂系列

    前面几篇文章深入分析了Thread、synchronized、AQS等相关知识,基础打好了,接下来就来分析常见的几个方法的应用、原理及其容易混淆的地方。
    通过本篇文章,你将了解到:

    1、Thread.sleep 应用及原理
    2、Thread.yield 应用及原理
    3、Thread.join 应用及原理
    4、Object.wait 应用及原理
    5、Condition.await 应用及原理
    6、总结

    1、Thread.sleep 应用及原理

    Thread.sleep 应用

    Thread.sleep 作用:

    调用 Thread.sleep后,线程进入休眠状态并让出CPU,等待超时时间耗尽,再次被CPU 调度运行。

    先来看看Thread.java 里的定义:

        public static native void sleep(long millis) throws InterruptedException;
    

    可以看出,其接受一个毫秒为单位的超时时间,并可能抛出InterruptedException 异常。当然也可以通过:

     public static void sleep(long millis, int nanos)
        throws InterruptedException{...}
    

    追加纳秒时间。
    来看看Demo:

        private void testSleep() {
            try {
                Thread.sleep(-1);//----->(1)
                Thread.sleep(0);//------>(2)
                Thread.sleep(1);//------>(3)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    

    上述代码标注的(1)/(2)/(3),分别传入负数、0、正数,结果分别是什么呢?
    (1)
    会抛出IllegalArgumentException 异常。
    (2)
    正常运行。
    (3)
    休眠1ms后线程得以继续执行。

    Thread.sleep 原理

    接着从源码的角度查看Thread.sleep(xx)接收不同参数以及为什么可以响应中断。
    Thread.sleep(xx)为native方法,先找到其对应的jni函数映射:

    #Thread.c
    static JNINativeMethod methods[] = {
        {"start0",           "()V",        (void *)&JVM_StartThread},
        {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
        {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
        {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
        {"resume0",          "()V",        (void *)&JVM_ResumeThread},
        {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
        {"yield",            "()V",        (void *)&JVM_Yield},
        {"sleep",            "(J)V",       (void *)&JVM_Sleep},
        {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
        {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
        {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
        {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
        {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
        {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
        {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
        {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
    };
    

    再找到C++函数实现:

    #jvm.cpp
    JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
      JVMWrapper("JVM_Sleep");
    
      if (millis < 0) {
        //如果传入的时间为负数,则抛出IllegalArgumentException 异常
        THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
      }
    
      //如果发生中断,则将中断标记位置为false,然后抛出InterruptedException 异常
      if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
        THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
      }
      ...
      if (millis == 0) {
        //传入的时间为0
        //ConvertSleepToYield 表示是否将sleep转为yield,在x86下是true
        if (ConvertSleepToYield) {
          //调用系统的yield()函数
          os::yield();
        } else {
          ...
          //调用系统的sleep()函数,并指定最小的时间MinSleepInterval(1ms)
          os::sleep(thread, MinSleepInterval, false);
          ...
        }
      } else {
        //传入的时间>0
        if (os::sleep(thread, millis, true) == OS_INTRPT) {
            //睡眠醒过来后,发现发生了中断,抛出中断异常
            THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
          }
        }
        thread->osthread()->set_state(old_state);
      }
      if (event.should_commit()) {
        event.set_time(millis);
        event.commit();
      }
    #ifndef USDT2
    JVM_END
    

    分析到此处,上面的疑惑基本解开了。

    1、Thread.sleep(-1),传参是负数,则抛出异常。
    2、Thread.sleep(0),传参是0,则调用Thread.yield()。
    3、不是1、2两点的值,则调用ParkEvent.park(xx)-->os::Linux::safe_cond_timedwait()-->NPTL.pthread_cond_timedwait(xx) 按照限时时间挂起线程。
    4、Thread.sleep(xx)过程中,若是发生中断,则抛出中断异常,并将中断标记位置为false。

    2、Thread.yield 应用及原理

    Thread.yield 应用

    Thread.yield 作用:

    调用Thread.yield后,线程让出CPU,CPU调度优先级比当前线程更高或者优先级相等的线程,若没有则Thread.yield调用立刻返回。
    在并发场景下,可以提高CPU利用率。

    先来看看Thread.java 里的定义:

    public static native void yield();
    

    Thread.yield 虽然平时很少用到,但不代表没用,回顾之前分析AQS源码时判断节点是否在同步同队列里时的做法:

    #AbstractQueuedSynchronizer.java
        final boolean transferAfterCancelledWait(Node node) {
            ...
            //不断检测是否在同步队列里
            while (!isOnSyncQueue(node))
                //没有无休止地轮训,而是yield一会
                Thread.yield();
            return false;
        }
    

    比如:线程A往同步队列里放节点,线程B查询,因为A、B并不是严格意义上的同步关系,因此无需用等待/通知机制,B知道A放节点速度很快,因此B仅仅只需要Thread.yield()简单让出CPU即可。

    Thread.yield 原理

    #jvm.cpp
    JVM_ENTRY(void, JVM_Yield(JNIEnv *env, jclass threadClass))
      JVMWrapper("JVM_Yield");
      //不支持yield,直接返回
      //默认是支持的
      if (os::dont_yield()) return;
      //ConvertYieldToSleep 默认false
      if (ConvertYieldToSleep) {
        os::sleep(thread, MinSleepInterval, false);
      } else {
        //调用系统yield
        os::yield();
      }
    JVM_END
    #os_linux.cpp
    void os::yield() {
      //系统调度
      sched_yield();
    }
    

    可以看出,Thread.yield 不响应中断。

    3、Thread.join 应用及原理

    Thread.join 应用

    Thread.join 作用:

    调用Thread.join后,调用者线程阻塞等待直到目标线程停止运行后再返回。
    可以用来做线程间简单同步,比如A线程(调用者线程)等待B(目标线程)执行结束后再做其它动作。

    在Thread.java 里的定义:

    public final void join() throws InterruptedException(){...}
    

    当然也可以设置超时时间,若是超时时间耗尽后,目标线程还没执行完毕Thread.join也是可以提前返回的。

    //超时单位毫秒
    public final synchronized void join(long millis){...}
    

    来看看Demo:

    public class TestThread {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1 执行到末尾了");
                }
            });
            t1.start();
            System.out.println("等待 t1 执行完毕");
            t1.join();
            System.out.println("t1 执行结束了");
        }
    }
    

    主线程调用Thread.join等待t1执行完毕。

    Thread.join 原理

    #Thread.java
        public final void join() throws InterruptedException {
            //超时时间0
            join(0);
        }
    
        public final synchronized void join(long millis)
        throws InterruptedException {
            long base = System.currentTimeMillis();
            long now = 0;
    
            if (millis < 0) {
                //超时时间必须>=0
                throw new IllegalArgumentException("timeout value is negative");
            }
            if (millis == 0) {
                while (isAlive()) {
                    //此处的wait就是this.wait(0),也就是thread.wait()
                    //一直等待,直到收到notify
                    wait(0);
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        //超时过了,直接返回
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }
    

    可以看出,Thread.join借助Object.wait/Object.notify 来实现线程间同步功能的。而且Thread.join 没有显式处理中断的逻辑,但却声明需要抛出中断异常,实际上中断异常是Object.wait抛出的。
    既然有Thread.wait在等待,那么想要Thread.wait返回,那么需要调用Thread.notify,这个在哪调用的呢?只能是线程结束时调用。

    #thread.cpp
    //线程结束,调用该方法
    void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
      ...
      ensure_join(this);
      ...
    }
    
    static void ensure_join(JavaThread* thread) {
      ...
      ObjectLocker lock(threadObj, thread);
      ...
      //唤醒等待在thread对象上的线程
      lock.notify_all(thread);
      ...
    }
    
    #ObjectSynchronizer.hpp
    void notify_all(TRAPS)      { ObjectSynchronizer::notifyall(_obj,    CHECK); }
    

    可以看出,线程结束后最终调用了thread.notify_all唤醒所有等待它执行完成的线程。
    因为Thread.join里调用Object.wait,而Object.wait需要抛出中断异常,因此Thread.join也需要抛出中断异常。

    4、Object.wait 应用及原理

    Object.wait 应用

    Object.wait 作用:

    调用Object.wait 后,线程被阻塞,等待其它线程唤醒它。
    用来做线程间的同步。

    来看看Object.java里的定义:

    public final void wait() throws InterruptedException
    

    可以看出该方法可能会抛出中断异常。
    当然wait(xx)可以设置超时时间。

    public final native void wait(long timeout) throws InterruptedException;
    

    超时时间单位为毫秒(ms)。

    看看Demo:

    public class TestThread {
        public static Object object = new Object();
        public static volatile boolean flag = false;
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (object) {
                        try {
                            System.out.println("flag is:" + flag + " t1 wait");
                            while (!flag)
                                object.wait();
                            System.out.println("flag is:" + flag + " t1 continue");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            t1.start();
    
            Thread.sleep(2000);
            synchronized (object) {
                System.out.println("flag is:" + flag + " main change flag");
                flag = true;
                object.notify();
                System.out.println("flag is:" + flag + " main notify t1");
            }
        }
    }
    

    t1发现flag==false,于是调用wait等待flag变为true,主线程修改flag为true,调用notify唤醒t1,这就完成了一次简单的线程间通信(同步)。

    Object.wait 原理

    Object.wait 源码在之前的Java Synchronized 重量级锁原理深入剖析下(同步篇)已经分析过,本次着重分析它是如何响应中断的。

    #ObjectMonitor.cpp
    void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
       //若是发生了中断,则抛出中断异常
       if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
         ...
         THROW(vmSymbols::java_lang_InterruptedException());
         return ;
       }
       ...
       //释放锁
       exit (true, Self) ; 
       { 
         {
           if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {
              //中断
           } else
           if (node._notified == 0) {
             //挂起线程,阻塞于此
             if (millis <= 0) {
                Self->_ParkEvent->park () ;
             } else {
                ret = Self->_ParkEvent->park (millis) ;
             }
           }
         }
       }
       //唤醒之后,先获取锁
          if (v == ObjectWaiter::TS_RUN) {
             ...
         } else {
             //重新获取锁
             ReenterI (Self, &node) ;
             node.wait_reenter_end(this);
         }
    
       if (!WasNotified) {
         if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
           //唤醒后,发现曾经发生过中断,于是抛出异常
           THROW(vmSymbols::java_lang_InterruptedException());
         }
       }
    }
    

    由此可见,调用Object.wait()后:

    1、线程释放锁。
    2、线程调用ParkEvent.park(xx)-->os::Linux::safe_cond_timedwait()-->NPTL.pthread_cond_timedwait(xx) 挂起线程。
    3、线程被唤醒后继续争抢锁。
    4、若在1、2、3 阶段发生中断,则重置中断标记位,并抛出中断异常。

    5、Condition.await 应用及原理

    Condition.await 应用

    Condition.await 作用:
    与Object.wait 作用一样。

    看看Condition.java里的定义:

    void await() throws InterruptedException;
    

    可以看出,可能会抛出中断异常。
    当然await(xx)可以设置超时时间。

    boolean await(long time, TimeUnit unit) throws InterruptedException;
    

    超时时间单位可选毫秒/纳秒等。

    看看Demo:

    public class TestThread {
        public static Lock myLock = new ReentrantLock();
        public static Condition condition = myLock.newCondition();
        public static volatile boolean flag = false;
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    myLock.lock();
                    try {
                        System.out.println("flag is:" + flag + " t1 await");
                        while (!flag)
                            condition.await();
                        System.out.println("flag is:" + flag + " t1 continue");
                    } catch (InterruptedException e) {
                    } finally {
                        myLock.unlock();
                    }
                }
            });
            t1.start();
    
            Thread.sleep(2000);
            myLock.lock();
            try {
                System.out.println("flag is:" + flag + " main change flag");
                flag = true;
                condition.signal();
                System.out.println("flag is:" + flag + " main signal t1");
            } catch (Exception e) {
            } finally {
                myLock.unlock();
            }
        }
    }
    

    与Object.wait Demo类似,只是Object.wait需要配合synchronized使用,而Condition.await需要配合Lock使用。

    Condition.await 原理

    原理请移步:Java并发之 AQS 深入解析(下)

    6、总结

    Thread.sleep/Thread.join/Thread.yield 和锁没有任何关系,而Object.wait调用前需要先获取synchronized锁,Condition.await调用前需要先获取Lock锁,因此它们和锁有关系。

    之所以容易把它们几个弄混,是因为表面上看调用这些方法后都会使得线程阻塞。除去Thread.yield外,其它方法阻塞线程都是通过调用底层的NPTL对应的函数。

    最后,用一张图总结本篇分析的内容:


    image.png

    此处需要说明的是:

    我们关注与锁有无关系是基于外部上锁,临界区执行Thread.sleep/Thread.yield/Thread.join/Object.wait/Condition.await 等方法是否与锁有关系。因此Thread.join 是与锁没有关系,只是内部使用了synchronized+Object.wait。

    下篇分析ReentrantLock、ReentrantReadWriteLock 原理及其应用。
    本文基于jdk1.8。

    您若喜欢,请点赞、关注,您的鼓励是我前进的动力

    持续更新中,和我一起步步为营系统、深入学习Java/Android

    相关文章

      网友评论

        本文标题:Java Thread.sleep/Thread.join/Th

        本文链接:https://www.haomeiwen.com/subject/zvgxaltx.html