在面试的过程中,应该有很多小伙伴碰到过 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。