在 Java19 之前,线程是 Java 运行的最小单元,线程作为 Java 的核心功能之一,在 Java 的发展史上起着举足轻重的作用,因此,今天我们就来聊聊 Java 线程的相关知识。
申明:本文基于 jdk-11.0.15,操作系统基于 Linux,JVM 基于 hotspot 源码,hotspot 源码下载地址: https://github.com/openjdk/jdk
本文会从 线程定义、线程创建、线程状态、线程工作原理、线程组、线程优先级、线程通信 7 个部分对线程进行全面分析。
一、什么是线程
维基百科中文版对线程的解释如下:

通过维基百科的描述,我们可以知道线程是操作系统执行的最小单元,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在 UnixSystem V 及 SunOS 中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(userthread)称为线程。
那么,Java中的线程是什么呢?
在 Java 中,线程对应的是 java.lang.Thread 类,下面是 Thread 类的源码描述:
A thread is a thread of execution in a program. The Java Virtual Machine allows an application to have multiple threads of execution running concurrently.
Every thread has a priority. Threads with higher priority are executed in preference to threads with lower priority. Each thread may or may not also be marked as a daemon. When code running in some thread creates a new Thread object, the new thread has its priority initially set equal to the priority of the creating thread, and is a daemon thread if and only if the creating thread is a daemon.
When a Java Virtual Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:
The exit method of class Runtime has been called and the security manager has permitted the exit operation to take place.
All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method.
因此,在 Java 中,线程是指程序中的执行线程,每个线程都有一个优先级,高优先级的线程执行要优先于低优先级线程,在 Java 虚拟机中,允许应用程序同时运行多个执行线程,当 JVM 启动时,通常会维护一个 非守护线程(main 方法对应的线程)。
二、线程创建方式
在 Java 中,创建线程有 3 种方式:继承 Thread 类、实现 Runnable 接口、Callable/Future。
2.1、继承 Thread 类
创建线程的第一种方法是将类声明为 Thread 的子类。该子类需要重写 Thread 类的 run()方法,然后创建子类的实例,最后调用实例的 start()方法启动线程。示例代码如下:
public class MyThread extends Thread { @Override public void run() { System.out.println("MyThread extends Thread!"); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } }
代码执行结果如下:
MyThread extends Thread!
2.2、实现 Runnable 接口
创建线程的第二种方法是声明一个实现 Runnable 接口的类。然后在类中实现 run()方法,并且将该类的实例作为参数传递给一个线程类,最后,调用 Thread.start()启动。示例代码如下:
public class MyRunnable implements Runnable { @Override public void run() { System.out.println("MyRunnable implements Runnable!"); } public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); } }
代码执行结果如下:
MyRunnable implements Runnable!
2.3、Callable/Future
创建线程的第三种方法是实现 Callable 接口,然后通过 Future 获取结果值。一般来说 Callable 需要配合线程池工具类 ExecutorService 来使用,如下代码,定义一个 MyTask 类,然后让该类去实现 Callable 接口,并且实现 call()方法,最后通过 executor.submit()提交任务。
public class MyThread implements Callable { @Override public Object call() throws Exception { return "MyThread implements Callable"; } public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newCachedThreadPool(); MyTask task = new MyTask(); Future<Integer> future = executor.submit(task); // get()方法会阻塞当前线程,直到得到结果。 System.out.println(future.get()); } }
代码执行结果如下:
MyThread implements Callable!
接下来,我们看看 Callable 的源码:

通过 Callable 的源码可以得知, Callable 是一个函数式接口,并且只有一个 call() 抽象方法,Callable 接口与 Runnable 类似,不过,Runnable 不返回结果,也不能抛出检查异常。
我们再看下 Future.get()是如何获取数据的?

通过源码可以看出,get()是一个阻塞方法,会一直等待直到处理完成返回结果或者等待超时中断。
Thread、Runnable、Callable/Future 比较
- 由于 Java 是单继承,因此 Runnable 接口比 Thread 更灵活;
- Runnable 接口更符合面向对象编程;
- Thread 类的方法比较丰富,本身也实现了 Runnable 接口,而 Runnable 接口更为轻量;
- Thread、Runnable 都无法返回值,Callable/Future 可以拿到线程执行的结果值;
- Future.get()是通过阻塞方法来获取结果值;
三、线程的状态
在 Java 中,线程的状态也叫做线程生命周期,主要有 6 种,其源码如下:
public class Thread implements Runnable { public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; } }
3.1、NEW
NEW:尚未启动的线程处于此状态,我们可以通过下面的代码来验证 NEW:
@Test public void testStateNew(){ Thread thread = new Thread(()->{}); System.out.println(thread.getState()); // 输出 NEW }
3.2、RUNNABLE
RUNNABLE:处于 RUNNABLE(可运行)状态的线程在 Java 虚拟机中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。我们可以通过下面的代码来验证 RUNNABLE:
@Test public void testStateNew(){ // 只创建一个线程,并没有调用 start()方法 Thread thread = new Thread(()->{}); thread.start(); System.out.println(thread.getState()); // 输出 RUNNABLE }
3.3、BLOCKED
BLOCKED:处于阻塞状态的线程正在等待监视器锁进入同步块/方法或调用 Object.wait 后重新进入同步块/方法。
3.4、WAITING
WAITING:等待状态。调用以下方法,线程就会处于等待状态:
- Object.wait()
- Thread.join()
- LockSupport.park()
处于 WAITING 等待状态的线程,需要其他线程对其对象执行下面任一方法才能切换成 RUNNABLE 状态:
- notify()
- notifyAll()
- LockSupport.unpark()
3.5、TIMED_WAITING
TIMED_WAITING:超时等待状态。线程等待给定的时间后会被自动唤醒。调用以下方法会使线程进入超时等待状态:
- Thread.sleep(long millis):使当前线程睡眠指定时间;
- Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify()/notifyAll()唤醒;
- Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;
- LockSupport.parkNanos(long nanos):除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
- LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
我们通过下面的代码来验证状态:
public class ThreadBlocked extends Thread { @Override public void run() { while (true) { try { // 线程进入 TIMED_WAITING 状态 TimeUnit.SECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread threadBlocked = new Thread(new ThreadBlocked(), "ThreadBlocked"); threadBlocked.start(); } }
在上述代码中,让线程 sleep 睡眠 100s,然后可以通过 jstack pid 指令查看堆栈信息,从而观测线程状态:

3.6、TERMINATED
TERMINATED:已终止状态,代表线程已完成执行。
总结
通过上文的分析,我们可以把 Java线程的 6 种状态及其转换关系总结如下图:

四、线程的工作原理
4.1、启动线程
在上文示例代码中,通过 Thread.start()方法就能启动线程,那么 start()方法是如何实现的呢?
首先,我们来分析 Thread和 Runnable的源码,如下:


通过 Runnable 源码可以看出,Runnable 是一个函数式接口,内部只有一个抽象方法 run(),任何实现该接口的线程类都必须实现 run()方法,而 Thread 源码显示,Thread 本身实现了 Runnable 接口,并且实现了 Runnable接口的run()方法,Thread.start()最终调用了native start0()方法,也就是说 start0()是在 JVM 中实现的,因此,继续查阅 JVM 的源码,看看 start0()方法到底做了什么。
从 JVM的Thread.c 源码可以看出,start0()会映射到 JVM 中的 JVM_StartThread 方法,映射关系如下图:

在 jvm.cpp 源码中,JVM_StartThread 方法源码实现如下图:

通过源码,可以 JVM_StartThread 实现逻辑总结为下列步骤:
- 1. 判断 Java 线程是否已经启动,如果启动过,则抛异常;
- 2. 如果 Java 线程没有启动过,则通过 new JavaThread()创建 Java 线程;
- 3. 调用 Thread::start(native_thread); 启动步骤 2 创建的线程;
接下来要分析JVM中线程是如何创建的,源码位于 javaThread.cpp 中,通过 new JavaThread()创建线程实际是会调用 os::create_thread()方法来创建 Java 线程对应的内核线程,其源码截图如下:

os::create_thread()是如何实现的呢?我们将目光转向操作系统内核源码,这里以Linux内核的os_linux.cpp 源码为例,os::create_thread() 是利用 pthread_create()来创建内核线程,方法共 4 个参数。

到此,线程已经创建完成,接下来就是如何启动线程,在 JVM中调用 的是Thread::start(native_thread),start()方法会调用平台启动线程的方法: os::start_thread(thread);,最终转向 thread.cpp 文件中的 JavaThread::run()方法,源码截图如下:

在 thread.cpp 源码中,thread->call_run() 调用了 thread_main_inner()。

在 javaThread.cpp 源码中,thread_main_inner()方法中的 this->entry_point(this, this)返回的其实就是在 new JavaThread(&thread_entry, sz) 时传入的 thread_entry。因此,thread_main_inner()就相当于调用了 thread_entry(this,this)。

在 jvm.cpp 源码中,thread_entry 方法中,JVM 通过 JavaCalls 模块调用 call_virtual方法,而 JavaCalls 是 C++调用 Java代码的一个功能模块,所以,通过JavaCalls模块最后能调用到 Java 中的 run()方法,源码截图如下:

通过上文对 JDK 和 JVM 的源码分析我们可以知道:start()方法先是 JDK通过 JNI的方式调用 JVM本地C++方法,然后 JVM 再和 os操作系统进行交互,最后 JVM 再回调 Java的run()方法,完成了整个执行过程,整个过程整理成下图:

上文描述了线程的启动过程,那么,线程是如何停止的呢?
4.2、停止线程
说起停止线程,想必大家很自然会想到 Thread.stop() 或者 Thread.suspend(),不过,很遗憾的是,Thread.stop() 和 Thread.suspend()从 JDK 1.2就已经被废弃了,主要是因为这些方法不安全,使用它们来停止线程会导致已锁定的监视器会被解锁,因此推荐使用 Thread.interrupt()来中断线程。
使用 interrupt()方法有两个重要作用:
- 设置一个线程终止的标记(共享变量的值 true)
- 唤醒处于阻塞状态下的线程
如下代码,在 run()方法中让线程睡眠 300s,然后在线程睡眠过程中调用线程中断方法:
public class MyThread extends Thread { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 调用interrupt thread.interrupt(); } @Override public void run() { try { TimeUnit.SECONDS.sleep(300); } catch (InterruptedException e) { // 打印出 java.lang.InterruptedException: sleep interrupted e.printStackTrace(); } } }
执行上述代码,可以发现线程被正常中断,catch中会打印出以下堆栈信息:java.lang.InterruptedException: sleep interrupted
那么,Thread.interrupt() 是如何中断线程的?
我们从 Thread.interrupt() 进行分析,发现 interrupt()调用了 native interrupt0(),从而转向了 JVM的实现,interrupt()源码如下:

在 JVM 的 Thread.c 源码中,我们可以看到 interrupt0()映射到 JVM_Interrupt方法,源码截图如下:

接着我们进入 JVM_Interrupt 方法的源码类jvm.cpp, 在 JVM_Interrupt中会判断线程是否存活,如果存活,才会调用 JavaThread.interrupt()方法进行线程中断操作,否则直接结束方法,源码截图如下:

最后,我们进入 javaThread.cpp ,interrupt() 方法针对线程不同的状态,会采取相应的方法来进行中断,具体实现逻辑如下图:

为什么interrupt()方法需要使用 unpark()呢?这是因为在演示代码中使用的是Thread.sleep()方法进行线程睡眠,而 sleep()最终会调用park 函数,源码截图如下:


所以 interrupt()需要使用unpark()唤醒睡眠的线程,park()方法其实是调用系统层面的锁挂起线程,而unpark()方法调用系统层面的唤醒条件变量达到唤醒线程的目的,Sleep()方法检测到线程中断标识,抛出 sleep interrupted异常。

4.3、start() 和 run()比较
start()和 run()方法是实际工作中比较容易搞混的两个方法,因此这里特别拿出来进行比较,我们先看一个示例代码:
public class MyThread extends Thread { @Override public void run() { System.out.println("ThreadName:" + Thread.currentThread().getName()); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.run(); // 输出 ThreadName:main thread.run(); // 输出 ThreadName:main thread.start(); // 输出 ThreadName:Thread-0 thread.start(); // 抛出 java.lang.IllegalThreadStateException 异常 } }
通过上面的代码执行结果我们可以得出:
- start(): 会启动一个新线程,在新线程里会执行相应的 run()方法;start()不能被重复调用,JVM源码会判断线程是否已经启动过。
- run() : 直接调用实例的 run()方法, run()就是一个普通方法,不会启动新线程,而是直接在当前线程中执行;run()可重复调用。
方法 | 说明 |
---|---|
native void yield() | 当前线程将放弃对处理器的使用,一般调试和测试中比较常用 |
native void sleep() | 线程睡眠指定的时长,线程不会释放对监视器的所有权 |
synchronizedvoid join() | 最多等待指定毫秒后让线程终止。0:永远等待 |
void setDaemon() | 用将线程设置为守护线程或用户线程,需在线程启动前设置 |
对照 JVM源码可以发现,在 Thread类中的很多方法最终都会映射到 JVM内部的 C++方法,甚至还需要和 OS操作系统进行交互。
五、Java线程组
ThreadGroup:线程组,在 Java中,每个线程必须属于一个组,不能独立存在,如果在 new Thread()时没有显式指定线程组,那么默认将父线程(当前执行new Thread()的线程)线程组设置为自己的线程组。public static void main(String[] args)方法的线程组默认为 main。
我们以一个实例来感受下线程组,如下代码,定义一个线程类,然后获取线程对应的线程组名称。
public class MyThread extends Thread { @Override public void run() { System.out.println("MyThread extends Thread!"); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 输出 threadGroup:main System.out.println("threadGroup:" + thread.getThreadGroup().getName()); } }
上述示例中,我们并没有给 MyThread线程实例设置过线程组信息,那么,获取到的线程组是在什么时候设置的呢?
首先,看看 Thread类在创建线程实例时做了什么,源码如下:
public class Thread implements Runnable { // 无参构造器 创建线程 public Thread() { this(null, null, "Thread-" + nextThreadNum(), 0); } // 指定线程组,Runnable和线程名来 创建线程 public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { this(group, target, name, stackSize, null, true); } // 最底层线程创建逻辑 private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { /* Determine if it's an applet or not */ /* If there is a security manager, ask the security manager what to do. */ if (security != null) { g = security.getThreadGroup(); } /* If the security manager doesn't have a strong opinion on the matter, use the parent thread group. 如果当前线程没有设置组,则获取父线程的线程组 */ if (g == null) { g = parent.getThreadGroup(); } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkAccess(); /* * Do we have the required permissions? */ if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission( SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this.target = target; setPriority(priority); if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ this.tid = nextThreadID(); } } }
通过Thread源码可以看出,如果创建线程时未指定线程组,那么会将父类的线层组信息设置为自己的线程组,接着就来看下 ThreadGroup源码类:

通过源码的描述可以得知:ThreadGroup(线程组)简单来说就是线程的集合,它是一个树形结构,并呈父子关系,除了 system 线程组(只有子线程组),一个线程组既可以有父线程组,同时也可以有子线程组。但是,线程只能访问本线程组的信息,不能访问其父类或者其他线程组的信息。在 Java 中线程组的树状结构如下图:

- system 线程组,它是用来处理 JVM 系统任务的线程组,比如对象销毁等;
- main 线程组,它是 system 线程组的直接子线程组,这个线程组至少包含一个 main 线程,用于执行 main 方法;
- sub 线程组,它是 main 线程组的子线程组,是应用程序创建的线程组;
所以,ThreadGroup 线程组是一个标准的向下引用的树状结构,这样设计的目的是防止”上级”线程被”下级”线程引用而无法有效地被 GC 回收。
ThreadGroup 其他一些重要方法:
方法 | 说明 |
---|---|
void checkAccess() | 判断线程能否访问该线程组 |
int activeCount() | 返回线程组及其子组中活动线程数的估计值 |
void destroy() | 销毁线程组及子线程组 |
void interrupt() | 中断线程组中所有线程 |
void stop() | 停止线程组中所有线程 |
void suspend() | 挂起线程组中的所有线程 |
ThreadLocal.ThreadLocalMap threadLocals | 存放线程变量副本,和 ThreadLocal 配合使用 |
六、Java线程优先级
在 Thread 类中,关于线程优先级的源码如下,通过源码可以看出,线程的优先级在 1~10,Java 默认的线程优先级为 5,可以通过 setPriority()进行优先级的设置,通常情况下,高优先级的线程会比低优先级的线程更优先执行,但是最终执行的顺序还是由操作系统的调度器来决定。所以说,Java 中的线程的优先级来是一个参考值,它只是给操作系统一个建议,最终的调用顺序,是由操作系统的线程调度算法决定的。
public class Thread implements Runnable { private int priority; /** * The minimum priority that a thread can have. */ public static final int MIN_PRIORITY = 1; /** * The default priority that is assigned to a thread. */ public static final int NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. */ public static final int MAX_PRIORITY = 10; }
上文我们对 Thread 的工作原理,线程组等相关知识进行了讲解,那么,线程作为 Java19 之前的最小运行单元,他们之间是如何通信的呢?
七、Java线程通信
在 Java 中,线程间通信的方式主要有 2 种:共享内存 和 消息传递。
7.1、共享内存
线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,比如:volatile 和 synchronized 关键字。
volatile 关键字保证了共享变量的可见性,也就是说,任何线程更新了主内存,其他线程就必须失效本地工作内存,然后从主内存中获取最新值同步到自己的工作内存,这样线程之间通过共享内存实现通信。
synchronized 关键字通过向对象施加 Monitor 锁,从而确保同一时间只能有一个线程能拿到锁,保证了线程访问的可见性和排他性。
7.2、消息传递
线程之间通过明确的发送消息来显式进行通信,在 Java 中典型的消息传递方式就是 wait()、notify()、notifyAll()。
当线程 A 调用了对象 Object 的 wait()方法后,会释放 Object 的 monitor 所有权,然后进入等待状态,当另外一个线程 B 调用了同一个对象的 notify()或者 notifyAll()方法,线程 A 收到通知后会继续抢占运行需要的资源,因此,线程 A,B 就通过消息传递的方式实现了通信。
八、总结
到此,本文的分析就全部结束了,因为文章篇幅有限,文章无法对线程的每个知识点展开分析,因此,建议大家利用空余时间去下载 JVM 源码(C++),对照本文给的源码查看线索,结合 JDK源码,然后再从 JVM 和操作系统的角度全面深入掌握线程的运行机制,相信你的投人一定会受益匪浅。