概述
现代操作系统在运行一个程序时,会为其创建一个进程,进程是分配系统资源的最小单位。例如,启动一个 Java 程序时,操作系统就会创建一个 Java 进程。现代操作系统调度 CPU 的最小单位是线程,一个进程可能包含一个或多个线程,这些线程都拥有各自的栈空间和局部变量等属性,并且能够访问共享的内存变量。
Java线程
Java 中的线程本质上就是一块内存(对象),不等于操作系统的线程。
下面是一段 Java 中的线程相关的代码:
1 | public static void main(String[] args) { |
对于 Java 语言来说,上述程序有两个动作,分别是创建线程和启动线程。但对于底层系统来说,Java 中的线程对象 Thread 执行 start() 方法后会间接调用操作系统函数库创建一个操作系统层面的线程并等待 CPU 调度。关于 Java 线程映射操作系统线程在下文会详细分析,我们先对 Java 线程的创建过程进行说明。
创建Java线程
1 | /** |
在创建 Thread 对象时会调用上面的 init 方法进行 Java 线程初始化。
启动Java线程
1 | /** |
我们知道的是,Java 线程调用 start
方法后就启动了,获取到 CPU 资源就可以执行任务。那么这和底层系统线程是怎么对应的呢?下面我们就来详细分析 Java 线程是怎么映射操作系统线程的。在分析 Java 线程是怎么映射到操作系统线程之前,我们需要先说明 JDK 的组成。JDK 的代码有两部分,一部分是 Java 语言库,另一部分是实现更底层功能的本地方法,他们由 C++ 实现,位于 openJdk 代码中。
映射系统线程
下面我们从调用本地方法 start0
出发,对执行链路进行跟踪,跟踪代码是 openJdk 中的源码。
- Thread.c可以看到,Thread 中的本地方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16static 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},
{"interrupt0", "()V", (void *)&JVM_Interrupt},
{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},
{"getThreads", "()[" THD, (void *)&JVM_GetAllThreads},
{"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
{"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};start0
维护在一个方法表中,对应的处理函数是 JVM_StartThread,下面我们看这个函数的作用。 - jvm.cpp可以看到,在执行本地方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JavaThread *native_thread = NULL;
bool throw_illegal_thread_state = false;
// 在 Thread::start 方法调用执行,必须先释放 Threads_lock
{
// 确保 C++ Thread 和 OSThread结构 在我们操作之前没有被释放。
MutexLocker mu(Threads_lock);
// 忽略
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
NOT_LP64(if (size > SIZE_MAX) size = SIZE_MAX;)
size_t sz = size > 0 ? (size_t) size : 0;
// 创建 JavaThread 对象
native_thread = new JavaThread(&thread_entry, sz);
}
// 创建完 JavaThread 后,启动 JavaThread
Thread::start(native_thread);
JVM_ENDstart0
后会创建一个 C++ JavaThread 对象,创建后才会启动 JavaThread 并执行 thread_entry 函数。 - jvm.cpp可以看到,JavaThread 执行的回调函数 thread_entry 是关联 Java 线程的直接入口,是通过 Java 虚拟机调用 Thread 对象的 run 方法的。至此,从 Java 线程到 C++ 线程就结束了,不过这两个都不是真正意义上的线程。下面我们继续跟,寻找创建操作系统线程的信息,其实操作系统线程的创建与启动的线索都在创建 C++ JavaThread 对象的逻辑中。
1
2
3
4
5
6
7
8
9
10
11
12
13static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
// thread_entry 中 JavaCalls::call_virtual 就是通过 JVM 调用 run 方法的
JavaCalls::call_virtual(&result,
obj,
vmClasses::Thread_klass(),
// vmSymbols::run_method_name() 映射的就是 run 方法
vmSymbols::run_method_name(),
vmSymbols::void_method_signature(),
THREAD);
} - thread.cpp哇,终于要见到了操作系统创建线程的流程了。在创建 C++ 的线程对象 JavaThread 时会根据不同的操作系统调用不同的函数创建线程。
1
2
3
4
5
6
7
8
9
10
11
12JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : JavaThread() {
_jni_attach_state = _not_attaching_via_jni;
// 1 为 JavaThread 设置运行函数 entry_point
set_entry_point(entry_point);
// Create the native thread itself.
// %note runtime_23
os::ThreadType thr_type = os::java_thread;
thr_type = entry_point == &CompilerThread::thread_entry ? os::compiler_thread :
os::java_thread;
// 2 根据不同的操作系统,调用对应底层函数创建线程
os::create_thread(this, thr_type, stack_sz);
} - os_linux.cpp这里以 Linux 操作系统为例,我们看到调用 Linux 的 pthread_create 库函数 创建线程并执行回调函数 thread_native_entry 。该回调用函数涉及到操作系统线程的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// thread->JavaThread
bool os::create_thread(Thread* thread, ThreadType thr_type,
size_t req_stack_size) {
// 创建 OSThread
OSThread* osthread = new OSThread(NULL, NULL);
if (osthread == NULL) {
return false;
}
// set the correct thread state
osthread->set_thread_type(thr_type);
// Initial state is ALLOCATED but not INITIALIZED
osthread->set_state(ALLOCATED);
// 为 JavaThread 设置 OSThread
thread->set_osthread(osthread);
ThreadState state;
{
ResourceMark rm;
pthread_t tid;
int ret = 0;
int limit = 3;
do {
// 我们的线程来了
// 调用 Linux 操作系统函数创建真正意义上的线程并运行函数 thread_native_entry,该函数参数是 JavaThread
ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
} while (ret == EAGAIN && limit-- > 0); - os_linux.cpp可以看到,Linux 在创建线程后先是将状态设置为了 INITIALIZED,然后阻塞自己,等待执行 os::start_thread() 唤醒自己。醒来后会间接调用 C++ 的 JavaThread 对象的回调函数,进而通过 JVM 调用 Java 的 Thread 对象的 run 方法。至此,C++ 的 JavaThread 对象创建完毕,接着执行它的 start 方法去唤醒阻塞的 OSThread。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43// 所有在 Linux 操作系统下新创建的线程运行的函数。
// 也就是当在 Java 中创建一个线程并执行 start 方法,后续会创建对应的操作系统线程并执行该方法
// thread->JavaThread
static void *thread_native_entry(Thread *thread) {
thread->record_stack_base_and_size();
thread->initialize_thread_current();
// 获取 JavaThread 中的 OSThread
OSThread* osthread = thread->osthread();
Monitor* sync = osthread->startThread_lock();
osthread->set_thread_id(os::current_thread_id());
if (UseNUMA) {
int lgrp_id = os::numa_get_group_id();
if (lgrp_id != -1) {
thread->set_lgrp_id(lgrp_id);
}
}
// initialize signal mask for this thread
PosixSignals::hotspot_sigmask(thread);
// initialize floating point control register
os::Linux::init_thread_fpu_state();
// handshaking with parent thread
{
MutexLocker ml(sync, Mutex::_no_safepoint_check_flag);
// notify parent thread
// 设置OSThread 为初始状态
osthread->set_state(INITIALIZED);
sync->notify_all();
// wait until os::start_thread()
// 如果 OSThread 是初始化状态,则让它阻塞。
while (osthread->get_state() == INITIALIZED) {
sync->wait_without_safepoint_check();
}
}
// OSThread 从阻塞中醒来,调用 JavaThread.call_run 方法
thread->call_run();
return 0;
} - thread.cpp可以看到,C++ 的 JavaThread 对象的启动方法 start 会调用 os::start_thread 方法通过设置 OSThread 的状态为 RUNNABLE 进而唤醒阻塞的 OSThread ,下面我们依次看更新状态和唤醒方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// thread -> JavaThread
void Thread::start(Thread* thread) {
// Start is different from resume in that its safety is guaranteed by context or
// being called from a Java method synchronized on the Thread object.
if (thread->is_Java_thread()) {
// Initialize the thread state to RUNNABLE before starting this thread.
// Can not set it after the thread started because we do not know the
// exact thread state at that time. It could be in MONITOR_WAIT or
// in SLEEPING or some other state.
java_lang_Thread::set_thread_status(JavaThread::cast(thread)->threadObj(),
JavaThreadStatus::RUNNABLE);
}
// 根据不同的操作系统,调用 start_thread 函数
// 注意 OS线程还在等待该函数执行
os::start_thread(thread);
} - os.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// thead->JavaThread
void os::start_thread(Thread* thread) {
// 从 JavaThread 中取出 OSThread
OSThread* osthread = thread->osthread();
// 设置 OSThread 状态为 RUNNABLE
osthread->set_state(RUNNABLE);
// 启动线程
pd_start_thread(thread);
}
void os::pd_start_thread(Thread* thread) {
OSThread * osthread = thread->osthread();
// 检查 OSThread 状态
Monitor* sync_with_child = osthread->startThread_lock();
MutexLocker ml(sync_with_child, Mutex::_no_safepoint_check_flag);
// 唤醒阻塞的OSThread (阻塞在 thread_native_entry 函数中)
sync_with_child->notify();
// OSThread 被唤醒后会执行 JavaThread.call_run 方法
} - thread.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void Thread::call_run() {
register_thread_stack_with_NMT();
MACOS_AARCH64_ONLY(this->init_wx());
JFR_ONLY(Jfr::on_thread_start(this);)
this->pre_run();
// JavaThread.run
this->run();
this->post_run();
}
void JavaThread::run() {
// initialize thread-local alloc buffer related fields
initialize_tlab();
_stack_overflow_state.create_stack_guard_pages();
cache_global_variables();
set_thread_state(_thread_in_vm);
// JavaThread.thread_main_inner
// 运行可以执行 Java 线程的函数
thread_main_inner();
} - thread.cpp以上方法用于执行创建 C++ 的 JavaThread 对象时指定的回调函数。目的只有一个,那就是利用 JVM 调用 Java 中我们创建的 Thread 对象的 run 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13void JavaThread::thread_main_inner() {
if (!this->has_pending_exception() &&
!java_lang_Thread::is_stillborn(this->threadObj())) {
{
ResourceMark rm(this);
this->set_native_thread_name(this->name());
}
HandleMark hm(this);
// 调用 JavaThread 的运行函数 thread_entry
this->entry_point()(this, this);
}
}
至此,Java 线程映射 OS 线程就介绍完毕了。下面我们进行简单小结:
- Java 中的 Thread 对象初始化完毕后调用 start 方法开始映射内核线程。注意,Thread 对象创建完毕后调用 start 方法之前和底层线程一毛钱的关系都没有。
- 通过调用本地方法 start0 创建 C++ 的 JavaThread 对象,并设置内核线程启动后触发的用于使用 JVM 执行的 Thread 对象的 run 方法的函数。
- JavaThread 会创建底层操作系统的 native thread
- 底层的 native thread 开始运行,通过 JVM 调用 Thread 的 run() 方法
- 当 Thread 的run()方法执行完毕返回后,或者抛出异常终止后,终止 native thread
可以看出,Java 中的线程是和内核线程一一对应的,几乎对线程的所有操作都需要借助系统调用完成。Java 线程映射内核线程的关系图如下:
线程生命周期
线程也有自己的生命周期,Java 中的线程有六种状态,具体如下:
1 | +--- Thread |
注意:Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 统称为阻塞状态(非 BLOCKED),这三种状态永远没有 CPU 的使用权。了解了线程的六个状态后,下面我们对状态之间的转换进行说明。
NEW 新建
NEW 新建表示线程被创建但尚未启动的状态,当我们 new Thread() 新建一个线程时,在线程运行 start() 方法之前,那么它的状态就是 NEW
。而一旦线程调用了 start() 方法,它的状态就会从 NEW
变成 RUNNABLE
。新建状态的展示如下图:
从 NEW 到 RUNNABLE 状态
NEW 状态的线程,不会被操作系统调度,因此不会执行。Java 线程要执行,就必须转换到 RUNNABLE 状态。从 NEW 状态转换到 RUNNABLE 状态很简单,只要调用线程对象的 start() 方法就可以了。Java 中的 RUNNABLE
状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready 。也就是说,Java 中处于 RUNNABLE
状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。因此,对于处于 RUNNABLE
状态的 Java 线程来说,即使当它的 CPU 时间片使用完了导致该线程不能使用,它的状态依然不会改变,还是 RUNNABLE
状态。转换图如下:
RUNNABLE 与 BLOCKED 的状态转换
从 RUNNABLE 状态进入到 BLOCKED 状态只有一种可能,就是线程等待 synchronized 的隐式锁。synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从 RUNNABLE 转换到 BLOCKED 状态。转换图如下:
想要从 BLOCKED 状态进入 RUNNABLE 状态,需要等待的线程获得 synchronized 隐式锁,获得锁后就又会从 BLOCKED 转换到 RUNNABLE 状态。转换图如下:
RUNNABLE 与 WAITING 的状态转换
线程进入 Waiting 状态有三种可能性,具体如下图:
下面对三种可能进行描述:
- Object.wait():获得 synchronized 隐式锁的线程,调用无参的 Object.wait() 方法,会进入到当前锁对象的 ObjectMonitor 中的 WaitSet 等待队列中,当前线程状态由 RUNNABLE 转换为 WAITING 状态。
- Thread.join():其中的 join() 是一种线程同步方法,如有一个线程对象 A,当调用 A.join() 的时候,执行这条语句的线程会等待 A 执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。当线程 A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE 。
- LockSupport.park():调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE 。
注意:从 WAITING 状态流转到其他状态则比较特殊,因为 WAITING 是不限时的,也就是说无论过了多长时间它都不会主动恢复。
这里分情况进行讨论:
- 执行了 LockSupport.unpark() 进入 RUNNABLE 状态
- join 的线程运行结束进入 RUNNABLE 状态。
- 被中断进入 RUNNABLE 状态。
- 其他线程调用 notify() 或 notifyAll()来唤醒它,它会直接进入 BLOCKED 状态。因为唤醒 WAITING 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有 synchronized 的隐式锁,所以处于 WAITING 状态的线程被唤醒时拿不到该锁,就会进入 BLOCKED 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 synchronized 的隐式锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 BLOCKED 状态回到 RUNNABLE 状态。
具体流转图如下:
RUNNABLE 与 TIMED_WAITING 的状态转换
线程进入 TIMED_WAITING 有五种可能,具体如下图:
下面对五种可能进行描述:
- 调用带超时参数的 Thread.sleep(long millis) 方法
- 获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法
- 调用带超时参数的 Thread.join(long millis) 方法
- 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法
- 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法
这里可以发现 TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数。
同样地,TIMED_WAITING 进入其它状态和 WAITING 类似,只是在其基础上增加了超时限制,也就是超时时间到达时将会返回到 RUNNABLE 状态,注意调用带超时参数的 Object.wait(long timeout) 方法,即使超时时间到了也是先进入阻塞状态(自行进入阻塞队列),获取到锁后才会返回到 RUNNABLE 状态。
具体流转图如下:
从 RUNNABLE 到 TERMINATED 状态
线程进入 TERMINATED 状态有两种情况,具体如下:
- 线程执行完 run() 方法后,会自动转换到 TERMINATED 状态
- 执行 run() 方法的时候异常抛出,也会导致线程终止
此外,有时候我们需要强制终止 run() 方法的执行,Java 的 Thread 类中提供了 stop() 方法,不过已经标记为 @Deprecated,所以不建议使用了。正确的姿势是利用线程中断进行交互。
状态小结
- 线程生命周期不可逆,一旦进入 RUNNABLE 状态就不能回到 NEW 状态
- 一旦被终止就不可能再有任何状态的变化
- 一个线程只能有一次 NEW 和 TERMINATED 状态,只有处于中间状态才可以相互转换,注意相互转换的方向和时机
操作系统线程状态
操作系统线程的状态可分为 初始状态
、可运行状态「就绪状态」
、运行状态
、休眠状态
以及 终止状态
,具体如下图:
初始状态
: 线程刚被创建,还不能分配 CPU;可运行状态「就绪状态」
: 线程等待系统分配 CPU,从而执行任务;运行状态
: 操作系统将 CPU 分配给线程,线程执行任务;休眠状态
: 运行状态下的线程如果调用阻塞 API,如读取文件,线程状态就将变成休眠状态;休眠状态下的线程会让出 CPU 使用权,休眠结束线程的状态将变成可运行状态;终止状态
: 线程执行结束或执行过程发生异常,那么线程会进入终止状态,这个状态下线程生命周期已经结束;
联系与区别
Java 线程没有可运行状态「就绪状态」
,它的 RUNNABLE
状态包括操作系统线程的 可运行状态「就绪状态」
与 运行状态
。也就是一个处于 RUNNABLE
状态下的 Java 线程,在操作系统层面状态可能为可运行状态「就绪状态」
,也可能为运行状态
。此外,Java 线程细分了操作系统休眠状态
,分成了 BLOCKED、WAITING、TIMED_WAITING 三种状态。
Java 线程进入阻塞状态,那么对应的操作系统线程就处于休眠状态
,这点很好理解。需要强调的是,当线程调用阻塞 API 进行 IO 操作时,操作系统层面线程会处于休眠状态
,而 Java 线程仍然处于 RUNNABLE
状态。因为从 JVM 来看,等待 CPU 使用权和等待 IO 是一样的,都是在等待资源,所以都归入 RUNNABLE
状态。
并发与并行
并发
如果某个系统支持两个或多个动作同时存在,那么这个系统就是一个并发系统。处理器在不同线程之间高速切换,让使用者感觉到这些线程在同时执行。
并行
如果某个系统支持两个或多个动作同时执行,那么这个系统就是一个并行系统。
并发系统与并行系统这两个定义之间的差异在于是同时存在还是同时执行。对于单核 CPU 并发执行任务,这些任务是同时存在的,CPU 会在不同任务之间进行切换,直到所有任务执行完成。并行意味着一定在多核 CPU 上,多个任务(线程)会被分配到独立的处理器上,因此可以同时运行。并行其实是并发的一个子集
理解中断
Java 中的中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。注意,中断某个线程,并不是说该线程就立即停止运行了,仅仅对该线程打了个标记。每个线程都关联了一个中断标识位,是一个 boolean 类型的变量,初始值为 false 。
关于线程中断,在 Thread 类中定义了以下几个方法:
1 | /** |
我们说中断线程,其实就是将线程的中断标识位置为 true ,至于被中断的线程怎么处理那就是线程自己的事了。线程通过检查自身是否被中断来进行响应,通过方法 isInterrupted()
判断是否被中断,通过调用静态方法 Thread.interupted()
对当前线程的中断标识位进行重置。如下代码:
1 | public static void main(String[] args) { |
需要注意的是,在 Java 提供的抛出 InterruptedException
方法在被调用抛出这个中断异常之前,Java 虚拟机会先把该线程的中断标识位清除然后抛出中断异常。
Object: wait() 系列方法
Thread: join() 系列方法、sleep() 系列方法
如上 JDK 提供的几个可中断方法,它们会让线程阻塞。如果线程阻塞在这些方法上,这个时候如果其他线程对这个线程进行了中断,那么这个线程会从这些方法中立即返回,抛出中断异常,同时重置中断状态为 false。日常开发中我们也可以根据需要,自定义可中断方法,在必要的时候通过线程中断实现特定功能。
此外,如果线程阻塞在 LockSupport.park(Object obj)
方法上,此时如果其他线程对该线程进行中断也会唤醒该阻塞线程,但是唤醒后不会重置中断状态。
安全地终止线程:线程中断是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。
小结
本篇文章对 Java 中的线程进行了介绍。先是从线程的创建与启动出发,分析了 Java 线程与内核线程的关系;然后对 Java 中的线程状态及流转进行了详细说明;最后对线程的中断进行了介绍,它是停止线程的正确姿势,也是线程通信的一种方式。
参考:
Java编发编程的艺术书籍
Java并发编程78讲专栏