00JUC入门

线程入门

多线程的很多概念听起来就很难理解。比方说:

  • 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
  • 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。

总结来说,进程和线程都是操作系统用于并发执行的方式,但是它们在资源管理、独立性、开销以及影响范围等方面有所不同。

  • 进程是操作系统分配资源的基本单位,线程是操作系统调度的基本单位。
  • 进程拥有独立的内存空间,线程共享所属进程的内存空间。
  • 进程的创建和销毁需要资源的分配和回收,开销较大;线程的创建和销毁只需要保存寄存器和栈信息,开销较小。
  • 进程间的通信比较复杂,而线程间的通信比较简单。
  • 进程间是相互独立的,一个进程崩溃不会影响其他进程;线程间是相互依赖的,一个线程崩溃可能影响整个程序的稳定性。

线程创建方法Thread.start()

根据知识库信息,Java中创建线程主要有以下四种方式,每种方式都有其适用场景和优缺点:

继承thread类

特点

  • 直接继承Thread类,重写run()方法
  • 代码简单直观,新手容易理解
  • 无法实现多继承(Java单继承限制)

实现Runnable接口

特点

  • 实现Runnable接口,重写run()方法
  • 线程与任务解耦,任务逻辑独立于线程管理
  • 支持Lambda表达式简化代码
  1. 实现Callable接口(带返回值)

callable

特点

  • 实现Callable接口,重写call()方法
  • 可返回任务执行结果(通过FutureTask
  • 可抛出受检异常
  • 需要配合FutureTask使用

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("Callable任务执行");
return "任务结果";
}
}

// 使用方式
Callable<String> callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();

// 获取结果
String result = futureTask.get(); // 阻塞等待结果

适用场景

  • 需要获取异步任务结果的场景
  • 需要处理异常的场景

优缺点

  • ✅ 优点:可以返回执行结果,可抛出受检异常
  • ❌ 缺点:需要额外使用FutureTask,代码稍复杂,阻塞等待

使用线程池(最佳实践)

特点

  • 通过ExecutorService创建线程池
  • 避免频繁创建和销毁线程,资源管理高效
  • 可控制并发数量,提高系统稳定性

示例代码

1
2
3
4
5
6
7
8
9
10
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);

// 提交任务
executor.submit(() -> {
System.out.println("线程池任务执行");
});

// 关闭线程池
executor.shutdown();

适用场景

  • 生产环境,需要高效管理线程的场景
  • 高并发处理(如批量处理10万条订单数据)
  • 需要超时重试的场景(如调用第三方接口)

优缺点

  • ✅ 优点:资源管理高效,系统稳定性高,可控制并发数量

[关于线程的一些疑问]

[1、为什么要重写 run 方法?]

这是因为默认的run()方法不会做任何事情。

为了让线程执行一些实际的任务,我们需要提供自己的run()方法实现,这就需要重写run()方法。

1
2
3
4
5
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread running");
}
}

在这个例子中,我们重写了run()方法,使其打印出一条消息。当我们创建并启动这个线程的实例时,它就会打印出这条消息。

[2、run 方法和 start 方法有什么区别?]

  • run():封装线程执行的代码,直接调用相当于调用普通方法。
  • start():启动线程,然后由 JVM 调用此线程的 run() 方法。

3、通过继承 Thread 的方法和实现 Runnable 接口的方式创建多线程,哪个好?

实现 Runable 接口好,原因有两个:

  • ♠①、避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。
  • ♠②、适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。

线程几种方法

1wait和sleep

wait是object的顺便用,sleep只能线程使用;wait只能在锁下使用,无锁环境用wait报错

sleep 和 wait 区别:

特性 Thread.sleep() Object.wait()
锁行为 不释放锁 释放锁
所属类 Thread 类的静态方法 Object 类的实例方法
唤醒方式 时间到自动唤醒 需要 notify()/notifyAll() 或超时
同步要求 不需要在同步块中 必须在同步块中

image-20251112145648703

2)join()

等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。

3)setDaemon()

将此线程标记为守护线程,准确来说,就是服务其他的线程,像 Java 中的垃圾回收线程,就是典型的守护线程。

4)yield()

yield() 方法是一个静态方法,用于暗示当前线程愿意放弃其当前的时间片,允许其他线程执行。然而,它只是向线程调度器提出建议,调度器可能会忽略这个建议。具体行为取决于操作系统和 JVM 的线程调度策略。

FutureTask

我们通过 Future 来获取 Callable 任务的执行结果,那么 Future 是什么呢?

Future 位于 java.util.concurrent 包下,它是一个接口:

Future 提供了三种功能:

  • 1)判断任务是否完成;
  • 2)能够中断任务;
  • 3)能够获取任务执行结果。

由于 Future 只是一个接口,如果直接 new 的话,编译器是会有一个 ⚠️ 警告的,它会提醒我们最好使用 FutureTask。

  • FutureTask本身:是一个线程安全的类,用于封装异步计算任务
  • FutureTask的get()方法:会阻塞等待任务完成
  • 不是阻塞等待结构,而是提供了一个阻塞等待机制(通过get()方法)来获取任务结果

简单来说,FutureTask是”任务容器”,而它的get()方法提供了”阻塞等待”功能。

线程的六种状态

Java 线程的 6 个状态:

1
2
3
4
5
6
7
8
9
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}

New

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

[关于 start 的两个引申问题]

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

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

  3. 都不行,在调用 start 之后,threadStatus 的值会改变(threadStatus !=0),再次调用 start 方法会抛出 IllegalThreadStateException 异常。

  4. threadStatus 为 2 代表当前线程状态为 TERMINATED(下面会讲)。

Runnable

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

Blocked

阻塞状态。处于 BLOCKED 状态的线程正等待(锁会在后面细讲)的释放以进入同步区。

waitting

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

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

  • Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
  • Thread.join():等待线程执行完毕,底层调用的是 Object 的 wait 方法;
  • LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。LockSupport 我们在后面会细讲。

timed_waiting

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

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

  • Thread.sleep(long millis):使当前线程睡眠指定时间;
  • Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
  • Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;
  • LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;LockSupport 我们在后面会细讲;
  • LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

terminated

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

[线程状态的转换]

img

线程安全问题

img

线程安全问题:多个线程操作同一个共享整据,导致线程安全问题

并发编程三大问题

  • 原子性:即不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要使用同步技术(sychronized)或者锁(Lock)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
  • **可见性:**每个线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改。**在 Java 中 volatile、synchronized 和 final 实现可见性。**volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。
  • **有序性:**java的有序性跟线程相关。一个线程内部所有操作都是有序的,如果是多个线程所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性,volatile和synchronized能保证指令不进行重排序。
    • volatile关键字本身就包含了禁止指令重排序的语义
    • synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入
    • 指令重排: 多线程环境中线程交替执行,由于编译器优化重排的存在,;两个线程中使用的变量能否保证–致性是无法确定的,结果无法预测

Java 内存模型

Java 内存模型(Java Memory Model,JMM)定义了 Java 程序中的变量、线程如何和主存以及工作内存进行交互的规则。它主要涉及到多线程环境下的共享变量可见性、指令重排等问题,是理解并发编程中的关键概念。

并发编程的线程之间存在两个问题:

  • 线程间如何通信?即:线程之间以何种机制来交换信息
  • 线程间如何同步?即:线程以何种机制来控制不同线程间发生的相对顺序

有两种并发模型可以解决这两个问题:

  • 消息传递并发模型
  • 共享内存并发模型

这两种模型之间的区别如下图所示:

两种并发模型的比较两种并发模型的比较

Java 使用的是共享内存并发模型

Java 线程之间的通信由 Java 内存模型(简称 JMM)控制,从抽象的角度来说,JMM 定义了线程和主存之间的抽象关系。

  1. 所有的共享变量都存在主存中。
  2. 每个线程都保存了一份该线程使用到的共享变量的副本。
  3. 如果线程 A 与线程 B 之间要通信的话,必须经历下面 2 个步骤:
    1. 线程 A 将本地内存 A 中更新过的共享变量刷新到主存中去。
    2. 线程 B 到主存中去读取线程 A 之前已经更新过的共享变量。

所以,线程 A 无法直接访问线程 B 的工作内存,线程间通信必须经过主存。

注意,根据 JMM 的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主存中读取

回答话术

JMM 是 Java 的多线程内存规则,通过 happens-before 保证可见性(volatile/synchronized)、有序性(内存屏障)、原子性(Atomic类)。核心是让线程看到其他线程的最新修改。

Java 内存模型 (JMM) 是一个抽象的概念模型,它规定了线程如何通过主内存进行交互。而具体的“工作内存”与“主内存”之间的数据同步,其底层是依赖于硬件(CPU)的缓存一致性协议来完成的,其中最著名的是 MESI 协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JMM
了解: 在cpu与内存之间 存在 一个缓冲区 目的是为了匹配二者之间运行速度不一致的问题
JMM是什么: java memory model
它规定了线程如何通过主内存进行交互 线程安全问题
JMM是JVM中对于线程安全保障的一种规范(接口)
JMM为什么-底层原理
JMM保证三大特性
原子性:通过synchronized和Lock保证 多线程并发操作不可分割
可见性 MESI协议 基于硬件的一个一致性协议 主内存与线程的工作内存之间的一致性
有序性 内存屏障 可以通过synchronize和volatile来实现禁止指令重排
JMM如何使用(实现类)
synchronized
Lock
volatile 中文翻译: 不稳定的

什么是共享变量?

对于每一个线程来说,栈都是私有的,而堆是共有的。

也就是说,在栈中的变量(局部变量、方法定义的参数、异常处理的参数)不会在线程之间共享,也就不会有内存可见性的问题,也不受内存模型的影响。而在堆中的变量是共享的,一般称之为共享变量。

所以,内存可见性]针对的是堆中的共享变量

如何保证内存可见性?

那么怎么知道这个共享变量的被其他线程更新了呢?这就是 JMM 的功劳了,也是 JMM 存在的必要性之一。JMM 通过控制主存与每个线程的本地内存之间的交互,来提供内存可见性保证

Java 中的 volatile 关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized 关键字不仅保证可见性,同时也保证了原子性(互斥性)。

在更底层,JMM 通过内存屏障来实现内存的可见性以及禁止重排序。为了程序员更方便地理解,设计者提出了 happens-before 的概念(下文会细讲),它更加简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则,以及这些规则的具体实现方法。

JMM 与 Java 运行时内存区域的区别

  • 区别

    两者是不同的概念。JMM 是抽象的,他是用来描述一组规则,通过这个规则来控制各个变量的访问方式,围绕原子性、有序性、可见性等展开。而 Java 运行时内存的划分是具体的,是 JVM 运行 Java 程序时必要的内存划分。

  • 联系

    都存在私有数据区域和共享数据区域。一般来说,JMM 中的主存属于共享数据区域,包含了堆和方法区;同样,JMM 中的本地内存属于私有数据区域,包含了程序计数器、本地方法栈、虚拟机栈。

指令重排

  • 指令重排是为了提高 CPU 性能,但是可能会导致一些问题,比如多线程环境下的内存可见性问题。

指令重排一般分为以下三种:

  • 编译器优化重排
  • 指令并行重排
  • 内存系统重排

指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。

happensbefore

happens-before 规则是 JMM 提供的强大的内存可见性保证,只要遵循 happens-before 规则,那么我们写的程序就能保证在 JMM 中具有强的内存可见性。

总之,如果操作 A happens-before 操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。

volatile

volatile 不稳定的
	volatile是什么
		JMM规范的一种实现 即是一种轻量级的同步机制 所谓轻量的一种理解就是不保证原子性
		只保证有序性和可见性
	volatile为什么(原理)
		可见性 MESI协议 基于硬件的一个一致性协议 主内存与线程的工作内存之间的一致性
		有序性 内存屏障 可以通过synchronize和volatile来实现禁止指令重排
	volatile怎么用
		volatile 只能修饰变量 不能修饰方法
		需要注意的是synchronized 不能修饰变量 它可以修饰方法和代码块
		为了解决volatile不保证原子性的缺陷,可以使用原子类 原子类的底层是CAS

[volatile 会禁止指令重排]

当我们使用 volatile 关键字来修饰一个变量时,Java 内存模型会插入内存屏障(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点:

  • 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。

  • 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。volatile 可以保证

    线程可见性且提供了一定的有序性,但是无法保证原子性。

    在 JVM 底层 volatile 是采用“内存屏障”来实现的。

    观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码就能发现,加入 volatile 关键字时,会多出一个 lock 前缀指令,lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供 3 个功能:

    • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    • 它会强制将对缓存的修改操作立即写入主存;
    • 如果是写操作,它会导致其他 CPU 中对应的缓存行无效。

Volatile和synchronized区别

特性 volatile synchronized
本质 变量访问修饰符 方法或代码块修饰符
原子性 不保证原子性(对单个读/写是原子的,但复合操作如i++不是) 保证原子性,整个同步块是一个不可分割的原子操作
可见性 保证可见性 保证可见性
有序性 保证有序性(禁止指令重排序) 保证有序性(一个unlock操作先于后面对同一个锁的lock操作)
线程阻塞 不会引起线程阻塞 引起线程阻塞(互斥同步,是悲观锁)
适用场景 单一变量的可见性、状态标志 多行代码的复合操作,需要保证原子性

end?


00JUC入门
http://example.com/2025/11/13/00JUC(线程-volatile)/
作者
無鎏雲
发布于
2025年11月13日
许可协议