Skip to content

JUC 并发编程 1——多线程基础

· 75 min

【跳转到下一篇:JUC并发编程——Java内存模型与底层同步机制】

一、基础#

线程与进程#

最初的计算机只能接受一些特定的指令,用户每输入一个指令,计算机就做出一个 操作。当用户在思考或者输入时,计算机就在等待。这样效率非常低下,在很多时候,计算机都处在等待状态。

后来有了批处理操作系统,把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。

批处理操作系统在一定程度上提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以批处理操作效率也不高。

批处理操作系统的瓶颈在于内存中只存在一个程序,那么内存中能不能存在多个程序呢?于是提出了进程。 进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。

b040eadb-8aa1-4b2a-b587-2c0a6b4efa0b

使用进程+CPU时间片轮转方式的操作系统,在宏观上看起来同一时间段执行多个任务,换句话说,进程让操作系统的并发成为了可能。虽然并发从宏观上看有多个任务在执行,但在事实上,对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源。

image-20221004132729868

在早期的计算机中,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。但是,如果我希望两个任务同时进行,就必须运行两个进程,由于每个进程都有一个自己的内存空间,进程之间的通信就变得非常麻烦(比如要共享某些数据)而且执行不同进程会产生上下文切换,非常耗时,那么能否实现在一个进程中就能够执行多个任务呢?

image-20221004132700554

于是提出了线程,一个进程可以有多个线程,线程是程序执行中一个单一的顺序控制流程,现在线程才是程序执行流的最小单元,各个线程之间共享程序的内存空间(也就是所在进程的内存空间),上下文切换速度也高于进程。

总之,进程和线程的提出极大的提高了操作系统的性能。进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

多进程的方式也可以实现并发,为什么我们要使用多线程?

多进程方式确实可以实现并发,但使用多线程,有以下几个好处:

  • 进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
  • 进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。

进程和线程的区别

进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):

  • 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位

上下文切换#

上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程) 切换到另一个进程(或线程)

上下文是指某一时间点 CPU 寄存器和程序计数器的内容

CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。

但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。

并发与并行#

1、顺序执行实际上就是我们同一时间只能处理一个任务,所以需要前一个任务完成之后,才能继续下一个任务,依次完成所有任务。

2、并发执行也是同一时间只能处理一个任务,但是可以每个任务轮着做(时间片轮转)。

3、并行执行就突破了同一时间只能处理一个任务的限制,同一时间可以做多个任务。

比如要进行一些排序操作,就可以用到并行计算,只需要等待所有子任务完成,最后将结果汇总即可。包括分布式计算模型MapReduce,也是采用的并行计算思路。

  • 并发(Concurrency) 的核心特征:
    • 微观串行:在任意一个瞬间,CPU(尤其是单核)只能执行一条指令,多个线程/任务是交替执行的(通过时间片轮转、上下文切换)。
    • 宏观并行:从用户或程序整体视角看,多个任务似乎同时在运行(比如一边下载文件一边播放音乐)。
  • 多核CPU下,每个核(core)都可以调度运行线程,这时候线程可以是并行的。

注意:并发≠并发调用

应用#

1、多线程可以让方法执行变为异步调用,但不代表单线程就不能实现异步,只是比较麻烦。

组合示例
单线程的同步调用普通方法调用
单线程的异步调用JavaScript 事件循环(Event Loop) + 回调队列(Callback Queue)
多线程的同步调用synchronized 方法被多个线程调用
多线程的异步调用CompletableFuture.supplyAsync()

Tomcat的异步servlet也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞Tomcat的工作线程。

2、提高效率

查看进程、程序相关线程的方法#

每个Java程序都有一个默认的主线程,就是通过JVM启动的第一个线程main线程。除此之外,还有很多其他线程比如守护线程(Daemon),守护线程默认的优先级比较低。如果某线程是守护线程,那如果所有的非守护线程都结束了,这个守护线程也会自动结束。

方法1:通过 Thread.getAllStackTraces().keySet() 获取线程对象

public class EmptyMain {
public static void main(String[] args) throws InterruptedException {
Thread.getAllStackTraces().keySet().forEach(t ->
System.out.println(t.getName() + " | " + t.getThreadGroup() + " | daemon=" + t.isDaemon()));
}
}
Terminal window
Reference Handler | java.lang.ThreadGroup[name=system,maxpri=10] | daemon=true
Common-Cleaner | java.lang.ThreadGroup[name=InnocuousThreadGroup,maxpri=10] | daemon=true
main | java.lang.ThreadGroup[name=main,maxpri=10] | daemon=false
Finalizer | java.lang.ThreadGroup[name=system,maxpri=10] | daemon=true
Signal Dispatcher | java.lang.ThreadGroup[name=system,maxpri=10] | daemon=true
Attach Listener | java.lang.ThreadGroup[name=system,maxpri=10] | daemon=true

Thread.getAllStackTraces().keySet() 返回的是 真实的 Thread 对象集合Set<Thread>),可以直接调用所有 Thread 的 public 方法

1、t.getName()线程名称

2、t.getThreadGroup()线程组对象

🔍 maxpri=10 表示该线程组允许的最大优先级为 10(Java 线程优先级范围 1~10)。

3、t.isDaemon()是否为守护线程

方法2:通过 ThreadMXBean 获取线程信息

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.lang.management.ThreadInfo;
public class EmptyMain {
public static void main(String[] args) {
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] ids = bean.getAllThreadIds();
ThreadInfo[] infos = bean.getThreadInfo(ids);
for (ThreadInfo info : infos) {
System.out.println(info.getThreadName());
}
}
}
image-20260101125328508
方法作用
ManagementFactory.getThreadMXBean()获取 JVM 线程管理器
bean.getAllThreadIds()获取所有线程 ID
bean.getThreadInfo(ids)获取线程详细信息快照
info.getThreadName()获取线程名称

具体线程数量和名称可能因 JVM 版本、GC 算法、操作系统、是否启用调试/JFR 等略有不同。

二、Java多线程类和接口#

juc

Thread类和Runnable接口#

JDK提供了 Thread 类和 Runnable 接口来让我们实现自己的“线程”类。Thread 类是一个 Runnable 接口的实现类。

@FunctionalInterface
public interface Runnable {
/**
* 当对象被用作线程的任务时,该方法会被线程调用。
* 此方法中的代码将在新线程中执行。
*/
public abstract void run();
}

查看 Thread 类的构造方法,发现其实是简单调用一个私有的 init 方法来实现初始化。

public class Thread implements Runnable {
// 线程名称前缀和编号计数器
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
// 所属线程组
private ThreadGroup group;
// 要执行的 Runnable 任务
private Runnable target;
// 线程名称
private String name;
// 栈大小(0 表示使用默认值)
private long stackSize;
// 用于权限检查的上下文
private AccessControlContext inheritedAccessControlContext;
// ThreadLocal 相关的两个 map
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// ------------------ 构造函数 ------------------
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 其他构造函数略...
// ------------------ init 方法(核心初始化逻辑)------------------
private void init(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) {
if (security != null) {
g = security.getThreadGroup();
} else {
g = parent.getThreadGroup();
}
}
g.checkAccess(); // 安全检查
this.group = g;
this.target = target;
this.priority = parent.getPriority(); // 继承优先级
this.daemon = parent.isDaemon(); // 继承守护状态
// 设置 AccessControlContext
if (security == null || isCCLOverridden(getClass())) {
this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
} else {
this.inheritedAccessControlContext = parent.inheritedAccessControlContext;
}
// 处理栈大小
if (stackSize == 0) {
stackSize = parent.stackSize;
}
this.stackSize = stackSize;
// 处理 inheritableThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 将新线程加入线程组
g.addUnstarted();
}
// init 的重载版本(供公共构造函数调用)
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
}

init 的方法传入的变量:

实际情况下大多是直接调用下面两个构造方法:

自定义线程类#

1、继承 Thread 类,并重写 run 方法

public class Demo {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}

2、实现 Runnable 接口的 run 方法

Runnable 接口只有一个未实现方法 run,因此可以直接使用 Java 8 的lambda表达式来简化代码。

public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
// Java 8 函数式编程,可以省略MyThread类
new Thread(() -> {
System.out.println("Java 8 匿名内部类");
}).start();
}
}

Thread类常用方法(基于 Java21)#

一、线程控制与状态管理#

1、start()

2、run()

3、join(), join(long millis), join(long millis, int nanos)

二、线程中断(协作式取消)#

4、interrupt()

5、isInterrupted()

6、Thread.interrupted()(静态方法!)

💡 最佳实践:捕获 InterruptedException 后,通常应恢复中断状态。

以下这段代码是 Java 并发编程中处理线程中断的标准范式,体现了“协作式中断”的核心思想:

try {
Thread.sleep(1000); // ① 当前线程休眠 1 秒
} catch (InterruptedException e) { // ② 如果休眠被中断,会抛出此异常
Thread.currentThread().interrupt(); // ③ 恢复中断状态
return; // ④ 提前退出方法(或任务)
}

❓为什么要恢复中断?

  • 中断是一种跨方法/组件的协作信号,可能由上层逻辑发起(如用户点击“取消”)。

  • 如果你在方法中“吞掉”了中断(不恢复),后续代码就无法感知中断请求,导致无法正确取消任务。

  • 恢复中断后,调用栈上层的代码仍可通过 isInterrupted() 或再次捕获 InterruptedException 来响应中断。

  • 除非你明确要“消费”中断(比如在最外层任务中处理取消逻辑),否则都应该恢复中断状态。
三、线程休眠与让步#

7、sleep(long millis), sleep(long millis, int nanos)(静态方法)

8、yield()(静态方法)

示例:

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("线程1开始运行!");
for (int i = 0; i < 50; i++) {
if(i % 5 == 0) {
System.out.println("让位!");
Thread.yield();
}
System.out.println("1打印:"+i);
}
System.out.println("线程1结束!");
});
Thread t2 = new Thread(() -> {
System.out.println("线程2开始运行!");
for (int i = 0; i < 50; i++) {
System.out.println("2打印:"+i);
}
});
t1.start();
t2.start();
}

观察结果,我们发现,在让位之后,尽可能多的在执行线程2的内容。

四、线程信息与属性#

9、getName() / setName(String name)

10、getId()

11、getPriority() / setPriority(int priority)

12、getState()

// Thread.getState方法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
// sun.misc.VM 源码:
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}

13、isAlive()

14、isDaemon() setDaemon(boolean on)

五、线程上下文相关(高级)#

15、getContextClassLoader() / setContextClassLoader(ClassLoader cl)

16、holdsLock(Object obj)(静态方法)

💡注意:

  • stop(), suspend(), resume()不安全(可能导致死锁、数据不一致)已在 Java 1.2 废弃。
  • 线程的停止必须协作(通过中断或 volatile 标志)。
  • 避免直接操作线程,优先使用 ExecutorService 等高级并发工具。

Callable、Future接口与FutureTask类#

Runnable 和裸 Thread 确实实现了“异步执行”(即并发、非阻塞地运行任务),但它们无法以标准、安全、便捷的方式返回计算结果或传递异常,必须通过显式的线程间同步机制(如 volatilesynchronizedCountDownLatch 等)来协调。

JDK提供了 Callable 接口与 Future 接口为我们解决这个问题,这也是所谓的异步模型(有返回值,可取消,可查询状态)。

更现代的异步方式:CompletableFuture(Java 8+)、虚拟线程(Virtual Threads,Java 21+ Project Loom)

Callable接口#

Callable 与 Runnable 类似,同样是只有一个抽象方法的函数式接口。不同的是, Callable 提供的方法是有返回值的,而且支持泛型。

@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}

Callable 一般配合线程池工具 ExecutorService 来使用:

Future接口#

Future 接口表示一个异步计算的结果。它提供了以下能力:

public abstract interface Future<V> {
public abstract boolean cancel(boolean mayInterruptIfRunning);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
取消任务#

cancel 方法是试图取消一个线程的执行。

所以为了让任务能够取消,就使用 Callable/Runnable + Future(一般用 Callable) 来代替 Thread。因为把 Callable 提交给 ExecutorService 可以得到一个 Future 对象,进而使用 cancel 方法。

为什么不用 Thread.interrupt()

  • Thread.interrupt() 确实可以用于取消线程,但有局限性,它是协作式取消。
  • 仅靠 interrupt() 无法保证任务能被及时、可靠地取消,除非任务本身配合检查中断。
  • 比如若线程在执行纯计算任务(比如一个大循环做数学运算),而代码中没有主动检查中断状态Thread.currentThread().isInterrupted()),那么 interrupt()完全无效——线程会一直运行下去。

有些后台任务本质上是“fire-and-forget”类型——比如定期清理缓存、发送心跳、轮询等。如果为了可取消性而使用 Future 但又不产生有意义的返回值,则可以声明 Future<?> 形式类型(表示某种未知类型的 Future,强调不关心结果类型),并返回 null 作为底层任务的结果

// ExecutorService 接口
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);

FutureTask类#

Future 接口有唯一实现类 FutureTask,帮助我们实现了 Future 接口的各种方法。

FutureTask 是实现的 RunnableFuture 接口的,而 RunnableFuture 接口同时继承了 Runnable 接口和 Future 接口。

之前的 demo 中 submit 方法会自动创建 FutureTask 的实例。

FutureTask 提供了 2 个构造器:

public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}

三、线程组和优先级#

线程组#

Java中用ThreadGroup来表示线程组,我们可以使用线程组对线程进行批量控制。

每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main() 方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。

示例代码:

public class Demo {
public static void main(String[] args) {
Thread testThread = new Thread(() -> {
System.out.println("testThread当前线程组名字:" +
Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread线程名字:" +
Thread.currentThread().getName());
});
testThread.start();
System.out.println("执行main所在线程的线程组名字: " + Thread.currentThread().getThreadGroup().getName());
System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
}
}

输出结果:

Terminal window
执行main所在线程的线程组名字: main
执行main方法线程名字:main
testThread当前线程组名字:main
testThread线程名字:Thread-0

ThreadGroup管理着它下面的Thread,ThreadGroup是一个标准的向下引用的树状结构,这样设计的原因是防止”上级”线程被”下级”线程引用而无法有效地被GC回收

线程的优先级#

Java中线程优先级可以指定,范围是1~10(默认为5),有些操作系统只支持3级划分:低、中、高。

现代 JVM(如 HotSpot)使用的是 1<1> 线程模型每一个 Java 线程都直接映射到一个 OS 原生线程(如 Linux 的 pthread、Windows 的 kernel thread),这意味着Java 线程的调度 完全由操作系统调度器决定,Java 优先级会映射到操作系统的优先级,而结果并不一定尊重原有的优先级。

所以Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级和执行顺序还是由操作系统的调度算法决定。

Java 线程优先级常量:

1、可以使用方法 Thread 类的 setPriority() 实例方法来设定线程的优先级。

public class Demo {
public static void main(String[] args) {
Thread a = new Thread();
System.out.println("我是默认线程优先级:"+a.getPriority());
Thread b = new Thread();
b.setPriority(10);
System.out.println("我是设置过的线程优先级:"+b.getPriority());
}
}

输出结果:

Terminal window
我是默认线程优先级:5
我是设置过的线程优先级:10

2、线程组可以设置一个“最大优先级”(max priority),该组内所有线程的优先级都不能超过这个值。

ThreadGroup.setMaxPriority(int pri)

注意:永远不要依赖线程优先级来保证程序正确性或性能,Java中的线程优先级并不可靠。

通过代码来验证一下:

import java.util.stream.IntStream;
public class Demo {
public static class T1 implements Runnable {
@Override
public void run() {
// 打印当前执行线程的名称和优先级
System.out.println(String.format(
"当前执行的线程是:%s,优先级:%d",
Thread.currentThread().getName(),
Thread.currentThread().getPriority()
));
}
}
public static void main(String[] args) {
// 创建并启动 9 个线程,优先级从 1 到 9
IntStream.range(1, 10).forEach(i -> {
Thread thread = new Thread(new T1()); // 传入 Runnable 任务
thread.setPriority(i); // 设置线程优先级
thread.start(); // 启动线程
});
}
}

某次输出:

Terminal window
当前执行的线程是:Thread-17,优先级:9
当前执行的线程是:Thread-1,优先级:1
当前执行的线程是:Thread-13,优先级:7
当前执行的线程是:Thread-11,优先级:6
当前执行的线程是:Thread-15,优先级:8
当前执行的线程是:Thread-7,优先级:4
当前执行的线程是:Thread-9,优先级:5
当前执行的线程是:Thread-3,优先级:2
当前执行的线程是:Thread-5,优先级:3

创建/启动顺序 ≠ 执行顺序

  • start() 被调用的顺序确实是 1→2→…→9,但并不意味着线程会立刻执行 run() 方法,实际上是多个线程在几乎同一时刻进入就绪(Runnable)状态,等待 CPU 调度器分配时间片来真正运行。

四、线程的状态和转换#

操作系统OS和Java中的线程状态转换#

维度操作系统(OS)线程状态Java 线程状态(java.lang.Thread.State
定义者操作系统内核(如 Linux、Windows)Java 虚拟机(JVM)
目的决定 CPU 调度、资源分配、睡眠/唤醒等向 Java 程序员提供逻辑上的线程生命周期视图
可见性通过 top -Hps -Thtop 等 OS 工具查看通过 thread.getState() 在 Java 代码中获取
粒度底层、物理(真实 CPU 执行状态)高层、逻辑(抽象的程序行为)

操作系统线程状态(以 Linux 为例)

在支持多线程的操作系统中,CPU 调度的基本单位是线程,每个线程有独立的调度状态(如 Linux 的 R/S/D/T/Z)。

OS 状态含义对应场景
R (Running / Runnable)正在 CPU 上运行,或在可运行队列中等待 CPUJava 的 RUNNABLE / R(自旋阶段)
S (Sleeping)可中断睡眠(等待事件,如 I/O、锁、sleep)Java 的 WAITING / TIMED_WAITING / BLOCKED(挂起后)
D (Disk Sleep)不可中断睡眠(通常在等磁盘 I/O)很少由 Java 直接引起
T (Stopped)被信号暂停(如 SIGSTOP调试时可能见到
Z (Zombie)已退出但父进程未回收Java 线程不会出现(JVM 管理线程生命周期)

⚠️ 注意:Windows、macOS 的状态命名不同,但概念类似。

关键映射关系(以 HotSpot JVM on Linux 为例)

Java 状态可能对应的 OS 状态说明
NEW无(OS 线程尚未创建)调用 start() 后才创建 OS 线程
RUNNABLER正在运行或可运行
BLOCKEDR(自旋阶段)S(挂起后)⭐ 最易混淆!轻量级锁时自旋(OS 是 R),重量级锁时挂起(OS 是 S)
WAITINGSwait() 会调用 OS 的 futex_wait,进入睡眠
TIMED_WAITINGSsleep() 会让 OS 线程睡眠指定时间
TERMINATED线程已销毁(无状态)OS 线程被回收

Java线程的六种状态和转换#

image-20260106002627819

// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}

1、处于NEW状态的线程此时尚未启动。这里的尚未启动指的是还没调用Thread实例的start()方法。

private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 输出 NEW
}

1、反复调用同一个线程的start()方法是否可行?

// start()的源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}

2、假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?

两个问题的答案都是不行。在start()内部有一个threadStatus的变量。在调用一次start()之后,它的值会改变不再为0;TERMINATED状态下它的值为2,此时再次调用start()方法会抛出IllegalThreadStateException异常。

2、RUNNABLE表示当前线程正在运行中。在Java虚拟机中运行,也有可能在等待CPU分配资源。

当CPU给予的运行时间结束时,会从运行状态回到就绪(可运行)状态,等待下一次获得CPU资源。

Thread源码里RUNNABLE状态的定义:

/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/

Java线程的RUNNABLE状态其实包括了传统操作系统进程的ready和running两个状态。

3、处于BLOCKED阻塞状态的线程正等待锁的释放以进入同步区。

根据 Java 官方文档:

A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling Object.wait().

也就是说,线程处于 BLOCKED 是因为:

注意:BLOCKED 不包括 wait()sleep()、I/O 等情况(那些是 WAITINGTIMED_WAITING

4、处于WAITING等待状态的线程变成RUNNABLE状态需要其他线程唤醒。

调用如下3个方法会使线程进入等待状态:

5、超时等待状态TIMED_WAITING。线程等待一个具体的时间,时间到后会被自动唤醒。

调用如下方法会使线程进入超时等待状态:

6、TERMINATED 终止状态。此时线程已执行完毕。

当线程的 run() 方法执行完毕,或抛出未被捕获的异常/错误时,线程将进入 TERMINATED 状态。

目前在Java里还没有安全直接的方法来停止线程,但是Java提供了线程中断机制来处理需要中断线程的情况。

线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。

五、多线程的问题#

线程安全问题(数据正确性)和线程同步#

1 原子性(Atomicity)#

  • 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 原子操作:即不会被线程调度机制打断的操作,没有上下文切换。

在并发编程中很多操作都不是原子操作:

int i = 0; // 操作1
i++; // 操作2
int j = i; // 操作3
i = i + 1; // 操作4

在单线程环境下上述四个操作都不会出现问题,但是在多线程环境下,如果不加锁的话,可能会得到意料之外的值。

public class YuanziDeo {
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
int numThreads = 2;
int numIncrementsPerThread = 100000;
Thread[] threads = new Thread[numThreads];
for (int j = 0; j < numThreads; j++) {
threads[j] = new Thread(() -> {
for (int k = 0; k < numIncrementsPerThread; k++) {
i++;
}
});
threads[j].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final value of i = " + i);
System.out.println("Expected value = " + (numThreads * numIncrementsPerThread));
}
}

输出如下:

Terminal window
Final value of i = 102249
Expected value = 200000

解决工具

注意:volatile 不能保证原子性!

2 可见性(Visibility)与 volatile 关键字#

class Test {
int i = 50;
int j = 0;
public void update() {
// 线程1执行
i = 100;
}
public int get() {
// 线程2执行
j = i;
return j;
}
}
thread-bring-some-problem-d91ca0c2-4f39-4e98-90e2-8acb793eb983

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

解决工具

3 有序性(Ordering)#

问题:编译器/CPU 重排序导致 a=1; flag=true; 被重排为 flag=true; a=1;,破坏逻辑。

JMM 定义了一套happens-before(先行发生)规则,用来保证跨线程的操作可见性和有序性

规则说明
1. 程序顺序规则在同一个线程内,按照代码顺序,前面的操作 happens-before 后面的操作
2. 监视器锁规则对一个锁的解锁 happens-before 后续对这个锁的加锁
3. volatile 变量规则对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作
4. 线程 start 规则Thread.start() happens-before 该线程的任何操作
5. 线程 join 规则线程中的所有操作 happens-before 其他线程对该线程的 join() 返回
6. 中断规则interrupt() happens-before 被中断线程检测到中断
7. finalizer 规则对象构造完成 happens-before finalize() 开始
8. 传递性如果 A hb B,B hb C,则 A hb C

volatile 不仅提供可见性(写后立即刷主存,读后立即读主存),更重要的是它通过插入内存屏障(Memory Barrier)禁止特定类型的重排序

由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序。

image-20260108033326901

屏障类型指令示例说明
LoadLoadLoad1;LoadLoad;Load2保证Load1的读取操作在Load2及后续读取操作之前执行
StoreStoreStore1;StoreStore;Store2在Store2及其后的写操作执行前,保证Store1的写操作已刷新到主内存
LoadStoreLoad1;LoadStore;Store2在Store2及其后的写操作执行前,保证Load1的读操作已读取结束
StoreLoadStore1;StoreLoad;Load2保证load1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

变量的线程安全分析#

1、成员变量和静态变量是否线程安全?

2、局部变量是否线程安全?

示例1:

public static void test1() {
int i = 10;
i++;
}

每个线程调用test1()方法时,局部变量i会在每个线程的栈帧内存中被创建多份,因此不存在共享,使用 javap -v 命令查看test1()的字节码:

Terminal window
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10 // 将整数10推送到操作数栈顶。
2: istore_0 // 将操作数栈顶的整数值存储到局部变量表的索引为0的位置(即将10存储到局部变量i)
3: iinc 0, 1 // 将局部变量表中索引为0的位置的整数值增加1。
Start Length Slot Name Signature
3 4 0 i I

示例2:

常见的线程安全类#

  1. String
  2. Integer
  3. StringBuffer
  4. Random
  5. Vector
  6. Hashtable
  7. java.util.concurrent包下的类

这里说它们是线程安全的是指,当多个线程调用它们同一个实例的某个方法时,是线程安全的,可以理解为:

Hashtable table = new Hashtable();
new Thread(() -> table.put("key", "value"), "t1").start();
new Thread(() -> table.put("key", "value"), "t2").start();

注意:虽然它们的每个方法都是原子的,但多个方法的组合不是原子的,比如

Hashtable table = new Hashtable();
// 两个线程同时执行
if (table.get("key") == null) {
table.put("key", value);
}

不可变类的线程安全性

线程同步#

在多线程环境中,为了保证线程安全,我们需要通过线程同步来解决。

**同步:**协调多个进程或线程对共享资源的访问,以确保程序的正确性、一致性和可预测性。

为什么需要同步?—— 举个例子

场景:银行账户转账

int balance = 1000;
// 线程A:取款500
balance = balance - 500;
// 线程B:取款600
balance = balance - 600;

理想结果:余额不能为负,第二次取款应失败。

但实际可能:

  1. A 读取 balance = 1000
  2. B 也读取 balance = 1000(此时A还没写回)
  3. A 计算 1000 - 500 = 500,写回
  4. B 计算 1000 - 600 = 400,写回 ❌

最终余额是 400,而不是正确的 500 或 -100(取决于业务逻辑)。

CAUTION

临界区(Critical Section)是多线程中一个非常重要的概念,指的是在代码中访问共享资源的那部分,且同一时刻只能有一个线程能访问的代码。多个线程同时访问临界区的资源如果没有任何同步(加锁)操作,会导致资源的状态不可预测和不一致,从而产生所谓的“竞态条件”(Race Condition)。在许多并发控制策略中,例如互斥锁 synchronized,目标就是确保任何时候只有一个线程进入临界区。

解决方法:用同步机制保证“读-改-写”操作不可分割(原子性)

同步机制:

1、锁(Lock)

2、信号量(Semaphore)

3、条件变量(Condition)

4、屏障(Barrier)

所在包说明
CyclicBarrierjava.util.concurrent可重用的屏障,适用于多轮同步
Phaserjava.util.concurrent更灵活、可动态注册/注销参与者的屏障

⚠️ 不要和 CPU/编译器层面的“内存屏障(Memory Barrier)” 混淆! 那个是 JMM 底层机制(如 volatile 插入的 StoreLoad 屏障),程序员不直接操作。 而这里的 Barrier 是应用层同步工具,程序员主动使用

5、原子类(AtomicXXX)

活跃性问题(程序能否 progress)#

活跃性是指某件正确的事情最终会发生,但当某个操作无法继续下去的时候,就会发生活跃性问题。

问题成因解决方案
死锁多线程循环等待资源避免嵌套锁、按固定顺序加锁、使用 tryLock(timeout)
活锁线程不断响应变化但无进展引入随机退避,打破对称性
饥饿低优先级线程长期得不到资源(ReentrantLock() 默认是非公平锁)公平锁(ReentrantLock(true))、合理调度
资源泄漏线程池未关闭、Future 未处理try-with-resources、executor.shutdown()

死锁#

其实死锁的概念在操作系统中也有提及,它是指两个线程相互持有对方需要的锁,但是又迟迟不释放,导致程序卡住:

image-20221004205058223

我们发现,线程A和线程B都需要对方的锁,但是又被对方牢牢把握,由于线程被无限期地阻塞,因此程序不可能正常终止。我们来看看以下这段代码会得到什么结果:

public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (o1){
try {
Thread.sleep(1000);
synchronized (o2){
System.out.println("线程1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (o2){
try {
Thread.sleep(1000);
synchronized (o1){
System.out.println("线程2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}

所以在编写程序时一定要避免这种死锁的情况。

那么如何去检测死锁呢?可以利用 jstack 命令来检测死锁,首先利用 jps 找到java进程:

Terminal window
nagocoler@NagodeMacBook-Pro ~ % jps
51592 Launcher
51690 Jps
14955
51693 Main
nagocoler@NagodeMacBook-Pro ~ % jstack 51693
...
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.test.Main.lambda$main$1(Main.java:46)
- waiting to lock <0x000000076ad27fc0> (a java.lang.Object)
- locked <0x000000076ad27fd0> (a java.lang.Object)
at com.test.Main$$Lambda$2/1867750575.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.test.Main.lambda$main$0(Main.java:34)
- waiting to lock <0x000000076ad27fd0> (a java.lang.Object)
- locked <0x000000076ad27fc0> (a java.lang.Object)
at com.test.Main$$Lambda$1/396873410.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.

jstack 自动帮助我们找到了一个死锁,并打印出了相关线程的栈追踪信息,同样的,使用 jconsole 也可以进行监测。

问题背景:转账中的死锁风险

假设有两个账户:

两个线程同时执行转账:

死锁发生过程:

  1. 线程1 成功锁住 A
  2. 线程2 成功锁住 B
  3. 线程1 尝试锁 B → 被阻塞(B 被线程2 占着)
  4. 线程2 尝试锁 A → 被阻塞(A 被线程1 占着)
  5. 双方互相等待 → 死锁!

这就是经典的“循环等待”死锁条件。

解决方案:按账户 ID 固定顺序加锁

核心思想:

无论谁转账,都必须按照“账户 ID 从小到大”的顺序加锁。

这样就消除了加锁顺序的不确定性,从根本上避免循环等待。

具体实现(Java 示例)

class Account {
final int id;
volatile int balance;
public Account(int id, int balance) {
this.id = id;
this.balance = balance;
}
}
public class TransferService {
public void transfer(Account from, Account to, int amount) {
// 关键:按 ID 顺序确定加锁顺序
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized (first) {
synchronized (second) {
// 执行转账
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
} else {
throw new IllegalArgumentException("Insufficient funds");
}
}
}
}
}

场景演示:A(1) ↔ B(2) 双向转账

情况1:A → B

情况2:B → A

无论方向如何,加锁顺序永远一致!

哲学家就餐问题#

有五位哲学家,分别是苏格拉底、柏拉图、亚里士多德、赫拉克利特、阿基米德,围坐在圆桌旁

模拟一下这个场景

1、筷子类

public class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}

2、哲学家类

@Slf4j(topic = "c.Philosopher")
public class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
private void eat() {
log.debug("我踏马吃吃吃");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
while (true) {
// 拿左手筷子
synchronized (left) {
// 拿右手筷子
synchronized (right) {
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}

3、就餐

public class Test04 {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}

活锁#

**举例:**A 和 B 是两个朋友,每人钱包里都有 10 块钱

他们约定:

“转账前先看对方有没有欠我钱,如果有,我就转;但如果我发现我自己的钱不够,或者对方正在操作,我就放弃这次,等一会儿再试。”

现在,两人同时开始操作

时间A 的动作B 的动作结果
t=0看自己有 10 元 → 够转看自己有 10 元 → 够转都决定要转
t=1扣自己 10 元(A=0)扣自己 10 元(B=0)钱都扣了
t=2准备加给 B → 但发现 B 的账户正在被修改(因为 B 也在转)准备加给 A → 但发现 A 的账户正在被修改双方都警觉:“状态不一致!”
t=3A 说:“算了,这次作废,我把 10 块退回来”(A=10)B 说:“我也作废,退回来”(B=10)回到初始状态
t=4两人休息 10 毫秒,又同时重试……
⏳ 又一轮同样的操作
class Account {
volatile int balance;
// 尝试从 this 转账到 target
boolean tryTransfer(Account target, int amount) {
// 第一步:检查余额(读)
if (this.balance < amount) {
return false; // 钱不够,失败
}
// 第二步:模拟“准备转账”——这里没有原子性!
this.balance -= amount; // 先扣自己的钱
// 此刻如果被打断,target 还没收到钱!
// 第三步:检查 target 是否“可用”(比如是否被锁、是否状态异常)
// 假设这里有个并发检查:如果 target.balance 被另一个线程改了,就认为不安全
if (/* 检测到 target 状态异常 */) {
// 回滚!把钱加回来
this.balance += amount;
return false;
}
// 第四步:加钱给对方
target.balance += amount;
return true;
}
}

问题:没有使用锁或原子操作来保护整个转账过程,导致中间状态暴露给其他线程,引发误判和反复回滚。

解决:

1、加锁保证原子性(推荐):

synchronized (getLockFor(a, b)) { // 按固定顺序加锁
if (a.balance >= 10) {
a.balance -= 10;
b.balance += 10;
}
}

→ 整个转账要么全做,要么不做,

不会出现“扣了钱但没加”的中间态。

2、如果必须重试,加入随机延迟

while (!success) {
if (tryTransfer(...)) success = true;
else Thread.sleep(new Random().nextInt(50)); // 打破同步节奏
}

→ 让两个线程不太可能每次都同时重试,一方先成功,另一方下次就能看到稳定状态。

🔧 诊断工具jstack 查死锁、ThreadMXBean.findDeadlockedThreads()

性能问题(吞吐量 & 延迟)#

多线程并发不一定比单线程串行执行快,因为多线程有创建线程和线程上下文切换的开销。

瓶颈优化手段对应工具
线程创建开销大复用线程ThreadPoolExecutor, Executors
锁竞争严重降低粒度 / 无锁ConcurrentHashMap, ReadWriteLock, StampedLock
上下文切换频繁控制线程数合理设置线程池 core/maxSize
阻塞式 I/O异步非阻塞CompletableFuture, 虚拟线程(Loom)
批量任务效率低按完成顺序处理CompletionService

现代趋势

  • 虚拟线程(Java 21+):极大降低线程创建成本,简化异步编程
  • 无锁数据结构ConcurrentLinkedQueue, Disruptor(高性能队列)