00JUC入门
线程入门
多线程的很多概念听起来就很难理解。比方说:
- 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
- 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。
总结来说,进程和线程都是操作系统用于并发执行的方式,但是它们在资源管理、独立性、开销以及影响范围等方面有所不同。
- 进程是操作系统分配资源的基本单位,线程是操作系统调度的基本单位。
- 进程拥有独立的内存空间,线程共享所属进程的内存空间。
- 进程的创建和销毁需要资源的分配和回收,开销较大;线程的创建和销毁只需要保存寄存器和栈信息,开销较小。
- 进程间的通信比较复杂,而线程间的通信比较简单。
- 进程间是相互独立的,一个进程崩溃不会影响其他进程;线程间是相互依赖的,一个线程崩溃可能影响整个程序的稳定性。
线程创建方法Thread.start()
根据知识库信息,Java中创建线程主要有以下四种方式,每种方式都有其适用场景和优缺点:
继承thread类
特点:
- 直接继承
Thread类,重写run()方法 - 代码简单直观,新手容易理解
- 无法实现多继承(Java单继承限制)
实现Runnable接口
特点:
- 实现
Runnable接口,重写run()方法 - 线程与任务解耦,任务逻辑独立于线程管理
- 支持Lambda表达式简化代码
- 实现Callable接口(带返回值)
callable
特点:
- 实现
Callable接口,重写call()方法 - 可返回任务执行结果(通过
FutureTask) - 可抛出受检异常
- 需要配合
FutureTask使用
示例代码:
1 | |
适用场景:
- 需要获取异步任务结果的场景
- 需要处理异常的场景
优缺点:
- ✅ 优点:可以返回执行结果,可抛出受检异常
- ❌ 缺点:需要额外使用
FutureTask,代码稍复杂,阻塞等待
使用线程池(最佳实践)
特点:
- 通过
ExecutorService创建线程池 - 避免频繁创建和销毁线程,资源管理高效
- 可控制并发数量,提高系统稳定性
示例代码:
1 | |
适用场景:
- 生产环境,需要高效管理线程的场景
- 高并发处理(如批量处理10万条订单数据)
- 需要超时重试的场景(如调用第三方接口)
优缺点:
- ✅ 优点:资源管理高效,系统稳定性高,可控制并发数量
[关于线程的一些疑问]
[1、为什么要重写 run 方法?]
这是因为默认的run()方法不会做任何事情。
为了让线程执行一些实际的任务,我们需要提供自己的run()方法实现,这就需要重写run()方法。
1 | |
在这个例子中,我们重写了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() 或超时 |
| 同步要求 | 不需要在同步块中 | 必须在同步块中 |

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 | |
New
处于 NEW 状态的线程此时尚未启动。这里的尚未启动指的是还没调用 Thread 实例的start()方法。
[关于 start 的两个引申问题]
反复调用同一个线程的 start 方法是否可行?
假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start 方法是否可行?
都不行,在调用 start 之后,threadStatus 的值会改变(
threadStatus !=0),再次调用 start 方法会抛出 IllegalThreadStateException 异常。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
终止状态。此时线程已执行完毕。
[线程状态的转换]

线程安全问题

线程安全问题:多个线程操作同一个共享整据,导致线程安全问题
并发编程三大问题
- 原子性:即不可分割性。比如 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 定义了线程和主存之间的抽象关系。
- 所有的共享变量都存在主存中。
- 每个线程都保存了一份该线程使用到的共享变量的副本。
- 如果线程 A 与线程 B 之间要通信的话,必须经历下面 2 个步骤:
- 线程 A 将本地内存 A 中更新过的共享变量刷新到主存中去。
- 线程 B 到主存中去读取线程 A 之前已经更新过的共享变量。
所以,线程 A 无法直接访问线程 B 的工作内存,线程间通信必须经过主存。
注意,根据 JMM 的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主存中读取。
回答话术
JMM 是 Java 的多线程内存规则,通过 happens-before 保证可见性(volatile/synchronized)、有序性(内存屏障)、原子性(Atomic类)。核心是让线程看到其他线程的最新修改。
Java 内存模型 (JMM) 是一个抽象的概念模型,它规定了线程如何通过主内存进行交互。而具体的“工作内存”与“主内存”之间的数据同步,其底层是依赖于硬件(CPU)的缓存一致性协议来完成的,其中最著名的是 MESI 协议
1 | |
什么是共享变量?
对于每一个线程来说,栈都是私有的,而堆是共有的。
也就是说,在栈中的变量(局部变量、方法定义的参数、异常处理的参数)不会在线程之间共享,也就不会有内存可见性的问题,也不受内存模型的影响。而在堆中的变量是共享的,一般称之为共享变量。
所以,内存可见性]针对的是堆中的共享变量。
如何保证内存可见性?
那么怎么知道这个共享变量的被其他线程更新了呢?这就是 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?