Java-多线程-Thread相关方法

版权声明:欢迎转载,请注明作者和出处 https://blog.csdn.net/baichoufei90/article/details/85159160

Java-多线程-Thread相关方法

摘要

Thread 相关方法,是JDK和用户编码中大量使用的最基本方法,本文会简单介绍常用的sleep, join, yield等方法,并介绍其实现原理。

0x01 sleep

1.1 基本概念

  • sleep方法如其名,就是让线程休息下,直到指定时间耗尽。
  • 最大的特点就是阻塞过程中,不释放线程拥有的对象锁(ObjectMonitor)。
  • sleep过程,会让出CPU时间片给其他线程执行。
  • 底层使用linux系统的pthread_cond_timedwait方法实现。
  • sleep方法可被中断

1.2 源码浅析

代码如下:

 /**
  * 使得当前调用该方法的线程暂停指定时长,具体取决于系统计时器和调度程序的精度和准确性。
  * 注意该方法不会放弃对象锁。
  *
  * @param  millis
  *         休眠时长,毫秒级
  *
  * @throws  IllegalArgumentException
  *          millis为负
  *
  * @throws  InterruptedException
  *          如果任何其他线程中断当前线程,就会抛出该异常,注意此时会清理中断状态
  */
public static native void sleep(long millis) throws InterruptedException;

1.3 实现原理

1.3.1 JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))

该方法代码位于jdk8/hotspot/src/share/vm/prims/jvm.cpp,部分核心代码如下:

// 第三个参数就是sleep的毫秒数
JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
  // sleep毫秒数不能小于0
  if (millis < 0) {
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }

  // 如果已经中断,就抛InterruptedException
  if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
    THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
  }

  // 保存当前线程状态并在结束阻塞时恢复
  JavaThreadSleepState jtss(thread);

  if (millis == 0) {
  // 休眠毫秒为0,永久休眠
    // 如果开启了ConvertSleepToYield转换,就调用os::yield()
    if (ConvertSleepToYield) {
      os::yield();
    } else {
      // 保存当前线程状态
      ThreadState old_state = thread->osthread()->get_state();
      // 新的线程状态设为SLEEPING
      thread->osthread()->set_state(SLEEPING);
      // 调用os::sleep方法,且是不可中断模式
      os::sleep(thread, MinSleepInterval, false);
      // sleep阻塞完后恢复旧状态
      thread->osthread()->set_state(old_state);
    }
  } else {
    ThreadState old_state = thread->osthread()->get_state();
    thread->osthread()->set_state(SLEEPING);
    // 调用os::sleep,且是可中断模式
    if (os::sleep(thread, millis, true) == OS_INTRPT) {
       ...
      }
    }
    // sleep阻塞完后恢复旧状态
    thread->osthread()->set_state(old_state);
  }

1.3.2 os::sleep

我们来看看linux系统的该方法版本,代码位于jdk8/hotspot/src/os/linux/vm/os_linux.cpp,核心代码如下:

int os::sleep(Thread* thread, jlong millis, bool interruptible) {
  ParkEvent * const slp = thread->_SleepEvent ;
  slp->reset() ;
  if (interruptible) {
  // 可中断
    // 记录当前时间
    jlong prevtime = javaTimeNanos();
    for (;;) {
    // for死循环
      if (os::is_interrupted(thread, true)) {
      // 检测到中断标记为为true,就返回了。注意,此时会自动清除中断标记位
        return OS_INTRPT;
      }
      // 当次循环时间
      jlong newtime = javaTimeNanos();

      if (newtime - prevtime < 0) {
        // time moving backwards, should only happen if no monotonic clock
        // not a guarantee() because JVM should not abort on kernel/glibc bugs
        assert(!Linux::supports_monotonic_clock(), "time moving backwards");
      } else {
        // 更新还需阻塞的时间
        millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC;
      }

      if(millis <= 0) {
      // 剩余阻塞时间耗尽,就正常返回
        return OS_OK;
      }
      // 否则更新前一次循环的时间为此次循环时间
      prevtime = newtime;

      {
        assert(thread->is_Java_thread(), "sanity check");
        JavaThread *jt = (JavaThread *) thread;
        ThreadBlockInVM tbivm(jt);
        OSThreadWaitState osts(jt->osthread(), false /* not Object.wait() */);

        jt->set_suspend_equivalent();
        // 
        slp->park(millis);

        // were we externally suspended while we were waiting?
        jt->check_and_wait_while_suspended();
      }
    }
  } else  // 不再展开,因为我们一般都用的可中断sleep

1.3.3 os::PlatformEvent::park(jlong millis)

核心代码摘录如下:

int status = pthread_mutex_lock(_mutex);
while (_Event < 0) {
    // 调用safe_cond_timedwait
    status = os::Linux::safe_cond_timedwait(_cond, _mutex, &abst);
  }
status = pthread_mutex_unlock(_mutex);
1.1.3.4 os::Linux::safe_cond_timedwait
int os::Linux::safe_cond_timedwait(pthread_cond_t *_cond, pthread_mutex_t *_mutex, const struct timespec *_abstime)
{
   if (is_NPTL()) {
      return pthread_cond_timedwait(_cond, _mutex, _abstime);
   } else {
      // 6292965: LinuxThreads pthread_cond_timedwait() resets FPU control
      // word back to default 64bit precision if condvar is signaled. Java
      // wants 53bit precision.  Save and restore current value.
      int fpu = get_fpu_control_word();
      int status = pthread_cond_timedwait(_cond, _mutex, _abstime);
      set_fpu_control_word(fpu);
      return status;
   }
}

可以看到 ,是通过pthread_cond_timedwait方法实现的阻塞。

1.4 Sleep对比Wait

经常面试会问这个问题,往往我们都是网上查资料死记硬背。现在我们都看完了源码(wait源码点Java-多线程-wait/notify,可以得出以下结论

  • wait会释放ObjectMonitor控制权;sleep不会
  • wait逻辑复杂,需要首先调用synchronized获取ObjectMonitor控制权,才能调用wait,且wait后还有放入WaitSet逻辑,唤醒时还有一系列复杂操作;而sleep实现简单,不需要别的线程唤醒
  • wait与sleep都能被中断(除了sleep(0),当然对他中断没有意义)

0x02 yield

2.1 基本概念

  • 该方法是给调度器一个提示,当前线程愿意放弃占有的CPU使用权。但注意,调度器可以忽略该提示。
  • yield只是一个探索式的尝试,期望改善多线程场景下某些线程过度使用CPU的情况。该方法的使用应经过长期性能测试,以确保它实际上具有所需的效果。
  • 用户编码中很少能正确使用该方法。因为可能在调试或测试的时候能达到预期,但在生产环境高并发环境下有可能导致bug!
  • 该方法在jdk的如JUC并发包内被用设计来做并发控制

2.2 源码浅析

// 就是一个native方法,底层使用
public static native void yield();

2.3 实现原理

2.3.1 JVM_ENTRY(void, JVM_Yield(JNIEnv *env, jclass threadClass))

该方法代码位于jdk8/hotspot/src/share/vm/prims/jvm.cpp,部分核心代码如下:

// 默认关闭了ConvertYieldToSleep配置
if (ConvertYieldToSleep) {
 os::sleep(thread, MinSleepInterval, false);
} else {
 os::yield();
}

2.3.2 os::yield

void os::yield() {
  sched_yield();
}

2.3.3 sched_yield

这个是linux系统级别的函数了。

  • 在man手册里对他的描述翻译如下:
    可让调用该方法的线程放弃CPU。该线程会被移动到队列的末尾以获得其静态优先级,此时新线程可以运行了
  • 返回值如下:
    成功时,返回0;失败时,返回-1。
  • 出错情况
    在linux系统中,调用该方法总是成功不会出错。
  • 注意
    如果调用该方法的线程是唯一的最高优先级的队列,那该线程不会让出CPU而是继续运行。
  • 使用场景
    策略性的调用sched_yield方法可以给与其他线程执行机会,特别是在如互斥锁这类的资源竞争的场景下,可以提升执行效率。但前提是调用线程已经确定释放了其他线程需要获得的资源。否则此时执行sched_yield方法只会白白造成线程切换的开销,因为其他线程还是没有获得正常运行所需要的资源。诸如此类错误的使用yield,反而会造成系统效率降低。

2.4 Sleep对比Yield

  • 主要是使用场景不同。Sleep主要用来指定睡眠时间让出CPU,并在时间到后再被系统分配CPU资源,进行调度执行;而Yield是用来主动释放CPU给其他线程执行,但无法精确控制是否释放。

0x03 join

3.1 基本概念

join方法主要用来等待其他线程运行结束,再继续运行自己的线程代码。

3.2 源码浅析

join方法有两个版本,分为是否带超时时间参数:

3.2.1 无参数join

先看没有参数的版本:

/**
 * 一直等待目标线程结束
 *
 * @throws  InterruptedException
 *          如果任何其他线程中断当前线程,就会抛出该异常,注意此时会清理中断状态
 */
public final void join() throws InterruptedException {
    join(0);
}

3.2.2 带超时参数join

再看带参数版本之前,先看其会用到的一个判断当前线程是否还活着的方法isAlive

/**
 * 测试当前线程是否还存活并运行着
 *
 * @return  <code>true</code> if this thread is alive;
 *          <code>false</code> otherwise.
 */
public final native boolean isAlive();

现在我们看看带时间参数的join方法:

/**
 * 一直等待目标线程结束,或是指定的时间已经到达。
 * 0表示永远等待。
 * 
 * 注意synchronized使用在了方法上,也就是说会获取该thread对象的对象锁
 * 但这种直接wait在thread对象上的方法是不推荐的
 * 
 * @param  millis
 * @throws  IllegalArgumentException
 * @throws  InterruptedException
 */
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) {
        // isAlive表示目标线程活着
        while (isAlive()) {
            // 这里就是调用目标Thread对象的wait方法,等待时间为0就永久等待
            wait(0);
        }
    } else {
        // 下面这个循环的意义是当目标线程还存活时就wait
        // 在唤醒时如果目标线程还是存活就更新超时时间,继续wait
        while (isAlive()) {
            // 还需要等待的时间
            long delay = millis - now;
            if (delay <= 0) {
                // 超时时间已到就退出,结束join
                break;
            }
            // 否则继续wait一段时间
            wait(delay);
            // 更新已经消耗的时间
            now = System.currentTimeMillis() - base;
        }
    }
}

0xFF 参考文档

man2/sched_yield

猜你喜欢

转载自blog.csdn.net/baichoufei90/article/details/85159160