多线程
多线程
1. 多线程相关概念
1.1 线程和进程
- 进程:是指内存运行的一个应用程序,是系统运行程序的基本单位,是程序的一次执行过程。
- 线程:是进程中的一个执行单元,负责当前进程中的任务的执行,一个进程会产生很多线程。
- 两者主要区别:每个进程都有独立内存空间。线程之间的堆空间和方法区共享,线程栈空间和程序计数器是独立的。线程消耗资源比进程小的多。

1.1.1 什么是并发与并行?
- 并发(Concurrency):同一时间段,多个任务都在执行,单位时间内不一定是同时执行。
- 并行(Parallel):单位时间内,多个任务同时执行,单位时间内一定是同时执行。
- 并发是一种能力,并行是一种手段。

1.1.2 线程上下文切换
一个 CPU 同一时刻只能被一个线程使用,为了提升效率 CPU 采用时间片算法将 CPU 时间片轮流分配给多个线程。在分配的时间片内线程执行,如果没有执行完毕,则需要挂起然后把 CPU 让给其他线程。
- CPU 切换线程,会把当前线程的执行位置记录下来,用于下次执行时找到准确位置
- 线程执行位置的记录与加载过程就叫做上下文切换
- 线程执行位置记录在程序计数器
上下文切换过程:
挂起线程01,将线程在CPU的状态(上下文)存储在内存
恢复线程02,将内存中的上下文在CPU寄存器中恢复
调转到程序计数器所指定的位置,继续执行之后的代码

1.2 线程的一生
1.2.1 线程从出生到死亡会出现六种状态
①New(新建)、②Runnable(可运行)、③Terminated(终止)
④Blocked(锁阻塞)、⑤Waiting(无限等待)、⑥Timed_Waiting(超时等待)

1.2.2 wait 与 sleep() 的区别:
- 主要区别:sleep() 方法没有释放锁,wait() 方法释放了锁
- 两者都可以暂停线程执行:wait() 常用于线程间交互/通信,sleep() 用于暂停线程执行
- wait() 方法被调用后,需要别的线程调用同一个对象的 notify 和 notifyAll。超时苏醒使用 wait(long) 方法
- sleep() 方法执行完成后,线程会自动苏醒。
1.2.3 多线程实现原理
Java线程是通过start()方法启动,启动后会执行run()方法

Thread究竟是如何执行run()方法呢?

流程小结:
- 线程类被 JVM 加载时会绑定 native 方法与对应的 C++ 方法
- start() 方法执行:
- start() → native start0() → JVM_Thread → 创建线程 JavaThread::JavaThread
- 创建OS线程,指定OS线程运行入口:
- 创建线程构造方法 → 创建OS线程 → 指定OS线程执行入口,就是线程的 run() 方法
- 启动OS线程,运行时会调用指定的运行入口run()方法。至此,实现一个的线程运行
- 创建线程的过程是线程安全的,基于操作系统互斥量(MutexLocker)保证互斥,所以说创建线程性能很差
1.3 线程的安全问题
1.3.1 什么是线程安全问题?
- 多个线程同时执行,可能会运行同一行代码,如果程序每次运行结果与单线程执行结果一致,且变量的预期值也一样,就是线程安全的,反之则是线程不安全。
1.3.2 引发线程安全问题的根本原因
多个线程共享变量
- 如果多个线程对共享变量只有读操作,无写操作,那么此操作是线程安全的
- 如果多个线程同时执行共享变量的写和读操作,则操作不是线程安全的
1.3.3 解决线程安全问题
同步机制 Synchronized
Volatile关键字:内存屏障
原子类:CAS
锁:AQS
并发容器
1.4 线程并发的三大特性
原子性:一个系列指令代码,要么全执行,要么都不执行,执行过程不能被打断
有序性:程序代码按照先后顺序执行
- 为什么会出现无序问题呢?因为指令重排
可见性:当多个线程访问同一个变量时,一个线程修改了共享变量的值,其他线程能够立即看到
- 为什么会出现不可见问题呢?因为 Java 内存模型( JMM )
1.5 指令重排序
1.5.1 什么是指令重排?
编译器和处理器会对执行指令进行重排序优化,目的是提高程序运行效率。现象是,我们编写的 Java 代码语句的先后顺序,不一定是按照我们写的顺序执行。
int count = 0;
boolean flag = false;
count = 1; //语句1
flag = true; //语句2
上述代码在执行过程中:语句1一定在语句2之前执行吗?不一定,因为有指令重排
按顺序执行不好么,为什么要指令重排执行?同步变异步,系统指令层面的优化
无论如何重排,不会影响最终执行结果,因为大部分指令并没有严格的前后执行顺序
在单线程情况下,程序执行遵循 as-if-serial 语义
1.5.2 什么是 as-if-serial 语义?
不管编译器和处理器怎么重排指令,单线程执行结果不受影响
int a = 10; //语句1
int b = 2; //语句2
a = a + 3; //语句3
b = a*a; //语句4
上面代码执行的顺序:语句2 ==> 语句1 ==> 语句3 ==> 语句4
不可能是:语句2 ==> 语句1 ==> 语句4 ==> 语句3
为什么?因为处理器在进行指令重排时,会考虑指令之间的数据依赖性
虽然重排序不会影响单线程程序正确执行,但是会影响多线程并发执行的正确性。
//线程1:
init = false
context = loadContext(); //语句1
init = true; //语句2
//线程2:
while(!init){//如果初始化未完成,等待
sleep();
}
execute(context);//初始化完成,执行逻辑
要想多线程程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
1.6 可见性
1.6.1 CPU 和缓存一致性
在多核 CPU 中每个核都有自己的缓存,同一个数据的缓存与内存可能不一致
为什么需要 CPU 缓存?
- 随着 CPU 技术发展,CPU 执行速度和内存读取速度差距越来越大,导致 CPU 每次操作内存都要耗费很多等待时间。为了解决这个问题,在 CPU 和物理内存上新增高速缓存。
程序在运行过程中会将运算所需数据从主内存复制到CPU高速缓存,当CPU计算直接操作高速缓存数据,运算结束将结果刷回主内存

1.6.2 Java 内存模型(Java Memory Model)
Java 为了保证满足原子性、可见性及有序性,诞生了一个重要的规范 JSR133,Java内存模型简称 JMM
JMM 定义了共享内存系统中多线程应用读写操作行为的规范
JMM 规范定义的规则,规范了内存的读写操作,从而保证指令执行的正确性
JMM 规范解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题
Java 实现了 JMM 规范因此有了 Synchronized、Volatile、锁等概念
JMM 的实现屏蔽各种硬件和操作系统的差异,在各种平台下对内存的访问都能保证效果一致
JMM 内存模型抽象结构示意图:

JMM 定义共享变量何时写入,何时对另一个线程可见
线程之间的共享变量存储在主内存
每个线程都有一个私有的本地内存,本地内存存储共享变量的副本
本地内存是抽象的,不真实存在,涵盖:缓存,写缓冲区,寄存器等
JMM线程操作内存基本规则:
- 线程操作共享变量必须在本地内存中,不能直接操作主内存的
- 线程间无法直接访问对方的共享变量,需经过主内存传递
1.6.3 内存可见性
可见性是一个线程对共享变量的修改,能够及时被其他线程看到
举个栗子:
线程 A 和线程 B 保证共享变量共享
线程 A 把本地内存 A 的共享变量副本值更新到主内存
线程 B 到主内存读取最新的共享变量

JMM 通过控制线程与本地内存之间的交互,来保证内存可见性
怎么解决可见性问题?
推翻 JMM,直接读取主内存共享变量?
使用 JMM:Synchronized,volatile
happens-before 规则:按需使用重排序和本地内存副本,前提是需要满足 happens-before 规则
1.6.4 happens-before规则
在 JMM 中使用 happens-before 规则约束编译器优化行为,Java 允许编译器优化,但是不能无条件优化。
如果一个操作的执行结果需要对另一个操作可见,那么这两个操作必须存在happens-before的关系!
需要关注的 happens-before 规则:
程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作
锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁
Volatile 变量规则:对一个 volatile 修饰变量的写,happens-before 于任意后续对这个变量的读
传递性:如果 A happens-before B,B happens-before C,那么 A happens-before C
happens-before规则 - 实现
- 处理器重排序规则
- 编译器重排序规则

JMM 设计 happens-before 规则的目标就是屏蔽编译器和处理器重排序规则的复杂性。不同的 CPU 架构,不同的 OS,不同的虚拟机实现皆不相同!
2. Synchronized
2.1 synchronized 简介
保证方法或代码块在多线程环境运行时,同一个时刻只有一个线程执行代码块。
JDK 1.6 之前,synchronized 的实现依赖于 OS 底层互斥锁的 MutexLock,存在严重的性能问题
JDK 1.6 之后,Java 对 synchronized 进行的了一系列优化,实现方式也改为 Monitor(管程)了
一句话:有了 Synchronized,就线程安全了,保证原子性、可见性、有序性
可以修饰方法(静态和非静态)和代码块:
同步代码块的锁:当前对象,字节码对象,其他对象
非静态同步方法:锁当前对象
静态同步方法:锁是当前类的 Class 对象
2.2 synchronized 原理剖析
如何解决可见性问题?Happens-before 规则
JMM 对于 Synchronized 的规定:
加锁前:必须把自己本地内存中共享变量的最新值刷到主内存
加锁时:清空本地内存中的共享变量,从主内存中读取共享变量最新的值
Synchronized是如何实现同步的呢?
- 同步操作主要是 monitorenter 和 monitorexit 两个 jvm 指令实现。背后原理是 Monitor(管程)
2.3 什么是 Monitor ?
Monitor 意译为管程,直译为监视器。所谓管程,就是管理共享变量及对共享变量操作的过程。让这个过程可以并发执行。
Java所有对象都可以做为锁,为什么?
- 因为每个对象都都有一个 Monitor 与之关联。然后线程对 monitor 执行 lock 和 unlock 操作,相当于对对象执行上锁和解锁操作。
Synchronized 里面不可以直接使用 lock 和 unlock 方法,但当我们使用了 synchronized 之后,JVM 会自动加入两个指令 monitorenter 和 monitorexit,对应的就是 lock 和 unlock 操作。
Monitor的实现原理:将共享变量和对共享变量的操作统一封装起来

2.4 锁优化
加了锁之后,不一定就是好的,盲目使用 Synchronized,虽然解决了线程安全问题,但也给系统埋下了迟缓的种子。
并发编程的几种情况:
- 只有一个线程运行
- 两个线程交替执行
- 多个线程并发执行
经过实践经验总结:前两种情况,可以针对性优化
JDK 1.6 基于这两个场景,设计了两种优化方案:偏向锁和轻量级锁
同步锁一共有四个状态:无锁,偏向锁,轻量级锁,重量级锁
JVM 会视情况来逐渐升级锁,而不是上来就加重量级锁,这就是 JDK 1.6 的锁优化
偏向锁:只有一个线程访问锁资源,偏向锁就会把整个同步措施消除
轻量级锁:只有两个线程交替竞争锁资源,如果线程竞争锁失败了不立即挂起,而是让它飞一会(自旋),在等待过程中可能锁就会被释放出来,这时尝试重新获取锁
2.5 锁信息存储
例如:锁类型,当前持有线程
| 偏向锁标记 | 锁状态标记 | 锁状态 |
|---|---|---|
| 0 | 01 | 无锁 |
| 1 | 01 | 偏向锁 |
| 无 | 00 | 轻量锁 |
| 无 | 10 | 重量锁 |
| 无 | 11 | GC 标记 |
同步锁锁定资源是对象,那无疑存储在对象信息中,由对象直接携带,是最方便管理和操作的。
32位操作系统的Markword

3. Volatile
3.1 Volatile 简介
Java 语言对 volatile 的定义:
- Java 语言允许线程访问共享变量,为了确保共享变量能被准确的一致地更新,线程应该确保通过互斥锁单独获取这个变量。Java 语言提供了 volatile,在某些情况下,它比锁要更方便。如果一个变量被声明成 volatile,JMM 确保所有线程看到这个变量的值是一致的。
- 一句话:volatile 可以保证多线程场景下共享变量的可见性、有序性。
- 可见性:保证对此共享变量的修改,所有线程的可见性
- 有序性:禁止指令重排序的优化,遵循 JMM 的 happens-before 规则
3.2 Volatile 实现原理剖析
内存屏障(Memory Barrier)是一种 CPU 指令,用于控制特定条件下的重排序和内存可见性问题。Java 编译器会根据内存屏障的规则禁止重排序。
Volatile 变量写操作时:在写操作后加一条 store 屏障指令,让本地内存中变量的值能够刷新到主内存
Volatile 变量读操作时:在读操作前加一条 load 屏障指令,及时读取到变量在主内存的值

JMM 内存屏障插入策略
在每个 volatile 写前,插入 StoreStore 屏障
在每个 volatile 写后,插入 StoreLoad 屏障
在每个 volatile 读前,插入 LoadLoad 屏障
在每个 volatile 读后,插入LoadStore屏障
| 屏障类型 | 示例 | 说明 |
|---|---|---|
| StoreStore | S01,StoreStore,S02 | 确保 S01 刷新数据到内存,先于 S02 及其后所有 Store 操作,对屏障前后的 Load 无影响 |
| StoreLoad | S01,StoreLoad,L02 | 全能型屏障:会屏蔽屏障前后所有指令的重排 |
| LoadLoad | L01,LoadLoad,L02 | 确保 load 动作 L01,先于 L02 及其后所有 Load 操作,对屏障前后 Store 无影响 |
| LoadStore | L01,LoadStore,S02 | 确保指令前的所有 load 操作,先于屏障后所有 Store 操作 |
重排序规则表:

- 当第一个操作是 volatile读时,不管第二个操作是什么,都不能重排序
- 确保 volatile 读到的是最新值:volatile 读之后的操作不会被编译器重排序到 volatile 读之前
- 当第一个操作是 volatile 写时,不管第二个操作是什么,都不能重排序
- 确保 volatile 写操作对之后的操作可见
- 当第二个操作是 volatile 写时,第一个操作是普通写时,不能重排序
3.3 Volatile 缺陷
存在原子性的问题:虽然 volatile 可以保证可见性,但是不能满足原子性
3.4 volatile 适合使用场景:
共享变量独立于其他变量和自己之前的值,这类变量单独使用的时候适合用 volatile
对共享变量的写入操作不依赖其当前值:例如 ++ 和 --,就不行
共享变量没有包含在有其他变量的不等式中
3.5 Volatile 和 Synchronized 特点比较:
| 特点 | Volatile | Synchronized |
|---|---|---|
| 加锁 | 否 | 是 |
| 阻塞线程 | 否 | 是 |
| 保证原子性 | 否 | 是 |
| 保证可见性 | 是 | 是 |
| 性能 | 很好 | 很差 |