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;
}
}
}