【权威说明】从JVM源码的角度带你看明白Thread.sleep() 是否占用CPU?

在面试的过程中,应该有很多小伙伴碰到过 Thread.sleep()是否占用cpu。在网上以及一些书籍中给出的答案是占用。 那到底是占用还是不占用,实战是检验真理的唯一标准,那么我们就通过源码看看到底是否占用。
首先我们先来看例子:

 @Test
    public void sleepForever(){
        //获取进程ID,避免去任务管理器查找
        System.out.println("进程ID="+getProcessID());
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static final int getProcessID() {
        //  ManagementFactory是一个在运行时管理和监控Java VM的工厂类
        //  它能提供很多管理VM的静态接口的运行时实例,比如RuntimeMXBean
        //  RuntimeMXBean是Java虚拟机的运行时管理接口.
        //  取得VM运行管理实例,到管理接口句柄
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        //  取得VM运行管理实例的名称,也是JVM运行实例的名称
        String jvmInstanceName = runtimeMXBean.getName();
        return Integer.valueOf(jvmInstanceName.split("@")[0]).intValue();
    }
}

在示例中把进程的ID打印出来了,打印出来的原因是可以使用jstack (pid 表示java进程id,可用jps命令查看) 查看所有线程的状态,jstack可以生成或导出jvm 虚拟机运行实例当前时刻的线程快照。线程快照是对当前JVM 实例内每一个线程正在执行的方法堆栈的集合,生成或导出线程快照的主要目的是用于定位线程出现长时间运行、停顿或者阻塞的原因。例如线程间的死锁、死循环、请求外部资源导致的长时间等待等。线程出现停顿的时候通过jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底后台做了什么事情,或者等待什么资源。
查看结果如下

C:\Users\tang_>jstack 9524
2022-09-29 11:02:17
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.25-b02 mixed mode):

"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x000000001b9ed000 nid=0x2e74 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001b95e800 nid=0x1f50 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001b959000 nid=0xfc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001b955000 nid=0x3e64 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001b94f800 nid=0x2a54 runnable [0x00000000157ee000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.read(SocketInputStream.java:150)
        at java.net.SocketInputStream.read(SocketInputStream.java:121)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x0000000781420520> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x0000000781420520> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:48)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x00000000141ea800 nid=0xba4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x00000000141a0800 nid=0x16bc runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000003051000 nid=0x2dbc in Object.wait() [0x00000000154ef000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000781106280> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142)
        - locked <0x0000000781106280> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000000304e000 nid=0x3a2c in Object.wait() [0x00000000153ef000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000781105cf0> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
        - locked <0x0000000781105cf0> (a java.lang.ref.Reference$Lock)

"main" #1 prio=5 os_prio=0 tid=0x0000000000fde800 nid=0xdc8 waiting on condition [0x0000000002dae000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at com.th.thread.SleepDemo.sleepForever(SleepDemo.java:60)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:483)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
        at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
        at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
        at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
        at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:221)
        at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

"VM Thread" os_prio=2 tid=0x0000000014154800 nid=0x2f04 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000002f78000 nid=0x2b08 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000002f79800 nid=0x31d4 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000002f7b000 nid=0x3ab0 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000002f7c800 nid=0x21ec runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001b9ef000 nid=0x5c8 waiting on condition

JNI global references: 57

我们要看的主要是main线程的状态为TIMED_WAITING (sleeping),如下图:
在这里插入图片描述

在Thread 类中,sleep是一个静态且本地方法,源码如下:

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

从源码可看出,sleep 是一个native方法,由jni注册声明,那么可同通过openJdk8 源码中找到native方法映射。在\src\java.basc\share\native\libjava\thread.c 文件中可看到Thread.sleep()方法对应是JVM_Sleep函数,如下图示
在这里插入图片描述

接下来再看JVM_Sleep 的具体实现,在hotspot源码下的src\share\vm\prims\jvm.cpp 文件中,

JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
 // JVM_ENTR宏定义
 //用于调试
  JVMWrapper("JVM_Sleep");

   //sleep(time)中的time<0时抛出llegalArgumentException 参数非法异常
  if (millis < 0) {
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }

//判断并清除线程中断状态,如果中断状态为true,抛出中断异常
  if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
    THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
  }

  // Save current thread state and restore it at the end of this block.
  // And set new thread state to SLEEPING.
  //用于修饰线程状态并做一些统计
  //当睡眠结束后,会修改后回线程状态,在ThreadSleepState的析构函数中修改
  JavaThreadSleepState jtss(thread);

#ifndef USDT2
  HS_DTRACE_PROBE1(hotspot, thread__sleep__begin, millis);
#else /* USDT2 */
  HOTSPOT_THREAD_SLEEP_BEGIN(
                             millis);
#endif /* USDT2 */

  if (millis == 0) {
    // When ConvertSleepToYield is on, this matches the classic VM implementation of
    // JVM_Sleep. Critical for similar threading behaviour (Win32)
    // It appears that in certain GUI contexts, it may be beneficial to do a short sleep
    // for SOLARIS
    //睡眠时间为0时,根据jvm中的参数ConvertSleepToYield做不同的处理
    //表示是否将sleep操作转为yield操作
    //分别调用os::yield()、os::sleep(thread, millis, true)
    if (ConvertSleepToYield) {
      os::yield();
    } else {
       //通过thread->osthread()->get_state()获取OSThread对象,
       //并将其状态设置为SLEEPING等到sleep结束后设置回原来状态。
      //获取并保存线程的旧状态
      ThreadState old_state = thread->osthread()->get_state();
      //将线程的状态设置为SLEEPING
      thread->osthread()->set_state(SLEEPING);
      //调用系统级别的sleep方法,此时只会睡眠最小时间间隔
      os::sleep(thread, MinSleepInterval, false);
     //恢复线程状态
      thread->osthread()->set_state(old_state);
    }
  } else {
    //如果睡眠时间大于0,也是做:sleep()操作,支持中断
    //获取并保存线程的旧状态
    ThreadState old_state = thread->osthread()->get_state();
     //将线程的状态设置为SLEEPING
    thread->osthread()->set_state(SLEEPING);
     //睡眠指定的毫秒数,并判断返回值
    if (os::sleep(thread, millis, true) == OS_INTRPT) {
      // An asynchronous exception (e.g., ThreadDeathException) could have been thrown on
      // us while we were sleeping. We do not overwrite those.
      if (!HAS_PENDING_EXCEPTION) {
#ifndef USDT2
        HS_DTRACE_PROBE1(hotspot, thread__sleep__end,1);
#else /* USDT2 */
        HOTSPOT_THREAD_SLEEP_END(
                                 1);
#endif /* USDT2 */
        // TODO-FIXME: THROW_MSG returns which means we will not call set_state()
        // to properly restore the thread state.  That's likely wrong.
        //抛出InterruptedException异常
        THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
      }
    }
    //恢复线程的状态
    thread->osthread()->set_state(old_state);
  }
#ifndef USDT2
  HS_DTRACE_PROBE1(hotspot, thread__sleep__end,0);
#else /* USDT2 */
  HOTSPOT_THREAD_SLEEP_END(
                           0);
#endif /* USDT2 */
JVM_END

从实现源码中可以看到这是一个JNI方法,JVM_Sleep 是JNI调用的宏定义。从JVM_Thread的源码中可看到在处理逻辑中,首先会做2个校验,分别是睡眠时间和线程的打断标记。

如果睡眠的时间为0,那么会调用系统级别的睡眠方法os::sleep(),睡眠时间为最小时间间隔。在睡眠之前会保存线程当前的状态,并将其设置为SLEEPING。在睡眠结束之后恢复线程状态。

接着就是sleep方法的重点,如果睡眠时间不为0,同样需要保存和恢复线程的状态,并调用系统级别的睡眠方法os::sleep()。当然睡眠的时间会变成指定的毫秒数。

最重要的区别是,此时会判断os::sleep()的返回值,如果是打断状态,那么就会抛出一个InterruptException!这里其实就是InterruptException产生的源头;

接下来在Hotspot目录src/os/aix/vm/os_aix.cpp 中查看os::sleep方法:


int os::sleep(Thread* thread, jlong millis, bool interruptible) {
 ....

// 获取thread中的_SleepEvent对象
  ParkEvent * const slp = thread->_SleepEvent;
  slp->reset();
  OrderAccess::fence();
  //如果允许被打断
  if (interruptible) {
  //记录下当前时间戳,这是时间比较的基准
    jlong prevtime = javaTimeNanos();

    // Prevent precision loss and too long sleeps
    jlong deadline = prevtime + millis * NANOSECS_PER_MILLISEC;

    for (;;) {
     //检查打断标记,如果打断标记为ture,则直接返回
      if (os::is_interrupted(thread, true)) {
        return OS_INTRPT;
      }

     //线程被唤醒后的当前时间戳
      jlong newtime = javaTimeNanos();

      assert(newtime >= prevtime, "time moving backwards");
      
       //睡眠毫秒数减去当前已经经过的毫秒数
      millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC;
       //如果小于0,那么说明已经睡眠了足够多的时间,直接返回
      if (millis <= 0) {
        return OS_OK;
      }

      // Stop sleeping if we passed the deadline
      if (newtime >= deadline) {
        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();
          //调用_SleepEvent对象的park方法,阻塞线程,此处为重点代码,睡眠代码
        slp->park(millis);

        // were we externally suspended while we were waiting?
        jt->check_and_wait_while_suspended();
      }
    }
  } else {
    OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);
    jlong prevtime = javaTimeNanos();

   
    jlong deadline = prevtime + millis * NANOSECS_PER_MILLISEC;
     //如果不能打断,除了不再返回OS_INTRPT以外,逻辑是完全相同的
    for (;;) {
     ....
    }
    return OS_OK;
  }
}

Thread.sleep的在jvm层面上是调用thread中SleepEvent对象的**park()**方法实现阻塞线程,在此过程中会通过判断时间戳来决定线程的睡眠时间是否达到了指定的毫秒。

Hotspot\src\share\vm\rumtime\park.hpp中找到ParkEvent方法:


class ParkEvent : public os::PlatformEvent {
  ...
} ;

在这个方法中没有park方法,接着在基类PlatformEvent(Hotspot目录src/os/aix/vm/os_aix.cpp)中查找:

class PlatformEvent : public CHeapObj<mtInternal> {
  private:
    ...
    pthread_mutex_t _mutex  [1];  //互斥锁
    pthread_cond_t  _cond   [1]; //条件变量
  ...
    
    void reset() { _Event = 0; }
    int  fired() { return _Event; }
    void park ();
    void unpark ();
    int  TryPark ();
    int  park (jlong millis);
    void SetAssociation (Thread * a) { _Assoc = a; }
};


PlatformEvent持有一把mutex互斥锁和一个cond条件变量,以及park (jlong millis)方法.接下来看下park (jlong millis)的实现, 由于代码过长,此处只放核心代码:


int os::PlatformEvent::park(jlong millis) {
 ....
  int ret = OS_TIMEOUT;
  //加互斥锁
  int status = pthread_mutex_lock(_mutex);
  assert_status(status == 0, status, "mutex_lock");
  guarantee (_nParked == 0, "invariant");
  ++_nParked;

  
//此处为核心代码
  while (_Event < 0) {
  //pthread_cond_timedwait方法进行条件限时等待,
  在 &abst 时间内等待条件时间发生,并且跟互斥锁_mutex有关
    status = pthread_cond_timedwait(_cond, _mutex, &abst);
    assert_status(status == 0 || status == ETIMEDOUT,
                  status, "cond_timedwait");
    if (!FilterSpuriousWakeups) break;         // previous semantics
    if (status == ETIMEDOUT) break;
    // We consume and ignore EINTR and spurious wakeups.
  }
  --_nParked;
  if (_Event >= 0) {
     ret = OS_OK;
  }
  _Event = 0;
  //解互斥锁
  status = pthread_mutex_unlock(_mutex);
  assert_status(status == 0, status, "mutex_unlock");
  assert (_nParked == 0, "invariant");
  return ret;
}


上面的代码有个套路,linux 线程互斥量pthread_mutex_t, 互斥量(mutex)就是一把锁,使用功能互斥量前先要初始化,使用的函数如下:

int pthread_mutex_init(pthread_mutex_t*restrict mutex,const pthread_mutexattr_t*restrict attr)
int pthread_mutex_destroy((pthread_mutex_t*) lock);

对互斥量加锁解锁的函数如下:

int pthread_mutex_lock(pthread_mutex+t*mutex);
int pthread_mutex_trylock(pthread_mutex+t*mutex);
int pthread_mutex_unlock(pthread_mutex+t*mutex);

Linux条件变量 pthread cond t的操作函数

*初始化一个条件变量 */
int pthread_cond_init (pthread_cond_t* cond, pthread_condattr_t *cond_attr);
* 销毁一个条件变量 *
int pthread_cond_destroy(pthread_cond_t* cond);
/* 令一个消费者等待在条件变量上 */
int pthread_cond_destroy(pthread_cond_t* cond);
/*生产者通知等待在条件变量上的消费者 *
int pthread_cond_signal(pthread_cond_t* cond);
/*生产者向消费者广播消息 */
int pthread_cond_broadcast(pthread_cond_t* cond);
/* 解锁->等 >加锁 * 
int pthread_cbnd_timedwait(&_cond, &_mutex, &__abstime)

pthread_cond_timedwait函数
在使用pthread_cond_timedwait()函数时,必须有三步:
(1)加互斥锁: pthread_mutex_lock(& mutex)
(2)等待:pthread_cond_timedwait(&cond,&mutex,& abstime)//解锁->等待->加锁
(3)解互斥锁:pthread_mutex_unlock(& mutex)

pthread_cond_timedwait的工作过程:

pthread_cond_timedwait在把线程放进阻塞队死后,自动对mutex进行解锁,使得其它线程可以获得加锁的权利。

pthread_cond_timedwait是一个阻塞操作,这意味着线程将睡眠,在它苏醒之前不会消耗CPU周期。

线程阻塞期间,这样其它线程能对mutex临界资源进行访问,并唤醒这个阻塞的进程。

当等待条件的发生,当收到信号或者等待的时间完成之后,pthread_cond_timedwait返回,又自动给mutex加锁。

实际上sleep/wait/park 都用到了PlatformEvent;sleep/wait/park 都是借助pthread_cond_timedwait实现阻塞,wait还需要结合objectMonitor使用。

总结:Thread.sleep()不会消耗CPU周期的,会放弃CPU。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 调优 JVM 的方法有很多,常用的方法如下: 1. 调整堆内存大小。可以使用 `-Xms` 和 `-Xmx` 参数来调整堆内存的初始大小和最大大小。如果堆内存过小,会导致频繁的 GC,从而导致 CPU 占用过高。 2. 调整 GC 策略。JVM 中有很多不同的 GC 算法,如 Serial GC、Parallel GC、CMS GC 等。每种算法都有自己的优缺点。可以使用 `-XX:+UseSerialGC` 或者 `-XX:+UseParallelGC` 等参数来选择不同的 GC 算法。 3. 调整 JVM 参数。JVM 还有很多其他的参数,比如 `-XX:+PrintGC` 可以在控制台输出 GC 的日志,`-XX:+PrintGCDetails` 可以输出详细的 GC 日志,方便我们分析问题。 4. 使用 profiler 分析 CPU 占用情况。有很多 profiler 工具可以帮助我们分析 CPU 占用情况。比如 Java Flight Recorder、JProfiler 等。使用这些工具可以帮助我们找到 CPU 占用过高的原因,从而调优 JVM。 ### 回答2: 要优化JVM来降低CPU占用过高的问题,可以从以下几个方面进行优化: 1. 调整内存设置:通过适当调整JVM的内存设置来避免内存压力过大,导致频繁的垃圾回收。可以通过修改-Xms和-Xmx参数来调整JVM的初始堆和最大堆大小,需要根据实际情况进行调整。 2. 减少对象创建:过多的对象创建会导致频繁的垃圾回收,从而造成CPU占用过高。可以通过优化代码,避免不必要的对象创建,使用对象池等方式减少对象的产生。 3. 优化算法和数据结构:选择合适的算法和数据结构可以减少CPU的使用率。对于一些耗时的操作,可以考虑使用更高效的算法来替代。 4. 多线程优化:合理的使用多线程可以充分利用多核CPU的性能,减少单个线程的CPU占用率。可以将一些独立、并行的任务放入不同的线程中进行处理。 5. 使用性能分析工具:可以使用性能分析工具来检测和分析CPU占用过高的原因,如JProfiler、VisualVM等。通过定位到具体的问题,可以有针对性地进行优化。 总之,JVM的优化需要综合考虑内存设置、对象创建、算法数据结构、多线程等多个因素。通过适当的调整和优化,可以降低CPU占用过高的问题,提高系统的性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

弯_弯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值