Java多线程与并发
为什么需要多线程?
背景: 计算机系统中,CPU 速度远高于内存和 I/O 设备,为了充分利用 CPU 性能,需要平衡三者速度差异。
措施:
- CPU 缓存: 弥合 CPU 与内存速度差异 (引发可见性问题)。
- 操作系统进程/线程: 分时复用 CPU,平衡 CPU 与 I/O 设备速度差异 (引发原子性问题)。
- 编译优化: 优化指令执行顺序,更合理利用缓存 (引发有序性问题)。
并发问题根源:并发三要素
示例: 多个线程同时对共享变量 cnt
自增,最终结果可能小于预期值
并发三要素:
-
可见性 (Visibility): CPU 缓存引起
- 定义: 一个线程对共享变量的修改,其他线程能否立即看到。
- 原因: 线程修改变量时,可能只更新 CPU 缓存,未及时写入主内存,导致其他线程从主内存读取到旧值。
- 例子: 线程 1 修改
i
值为 10 (仅更新 CPU1 缓存),线程 2 读取i
值可能仍为 0 (从主内存读取)。 -
原子性 (Atomicity): 分时复用引起
-
定义: 一个或多个操作要么全部完成且不被打断,要么都不执行。
- 原因: CPU 分时复用 (线程切换) 可能在操作执行一半时切换线程,导致操作被打断,结果错误。
- 例子:
i += 1
非原子操作,包含:读取i
-> 加 1 -> 写入i
三个步骤。线程切换可能发生在步骤之间,导致最终结果错误。 -
有序性 (Ordering): 重排序引起
-
定义: 程序执行顺序应与代码顺序一致。
- 原因: 编译器和处理器为优化性能可能进行指令重排序 (编译器优化、指令级并行、内存系统重排序)。
- 例子: 语句
i = 1;
和flag = true;
代码顺序在前,但实际执行顺序可能被重排。
Java 如何解决并发问题:JMM (Java 内存模型)
JMM 本质: 规范 JVM 如何按需禁用缓存和编译优化,提供关键字和规则来解决并发问题。
核心方法:
- 关键字:
volatile
,synchronized
,final
- Happens-Before 规则: 定义操作间的先行发生关系,保证有序性。
JMM 维度:
- 原子性: JMM 仅保证基本类型变量的读写是原子性操作。更大范围原子性需使用
synchronized
或Lock
。 - 可见性:
volatile
关键字保证变量修改立即更新到主内存,读取时从主内存刷新。synchronized
和Lock
也可保证可见性。 - 有序性:
volatile
关键字和synchronized
、Lock
可保证一定程度有序性。JMM 通过 Happens-Before 规则保证。
关键字:volatile
, synchronized
, final
volatile
: 保证可见性和部分有序性,禁止指令重排序优化。synchronized
: 保证原子性、可见性和有序性,互斥同步,性能开销较大。final
: 保证不可变性,不可变对象天生线程安全。
Happens-Before 规则 (先行发生原则)
定义操作间的偏序关系,保证在并发环境下操作的可见性和有序性。
- 单一线程原则 (Single Thread Rule): 线程内,代码顺序靠前的操作先行发生于靠后的操作。
- 管程锁定规则 (Monitor Lock Rule):
unlock
操作先行发生于后续对同一个锁的lock
操作。 - volatile 变量规则 (Volatile Variable Rule):
volatile
变量的写操作先行发生于后续对该变量的读操作。 - 线程启动规则 (Thread Start Rule):
Thread.start()
调用先行发生于线程内的任何操作。 - 线程加入规则 (Thread Join Rule): 线程的所有操作先行发生于
join()
方法返回。 - 线程中断规则 (Thread Interruption Rule):
interrupt()
调用先行发生于被中断线程检测到中断事件。 - 对象终结规则 (Finalizer Rule): 对象构造函数执行结束先行发生于
finalize()
方法开始。 - 传递性 (Transitivity): 若 A happens-before B, B happens-before C, 则 A happens-before C。
线程安全级别
线程安全并非绝对,按安全程度由强到弱分为五类:
-
不可变 (Immutable): 绝对线程安全。对象一旦创建,状态不可变。如
final
基本类型、String
、枚举、部分Number
子类、不可变集合 (Collections.unmodifiableXXX()
)。推荐使用不可变对象保证线程安全。 -
绝对线程安全: 理想状态。无需任何额外同步措施即可在任何环境安全调用。 (实际中极少)
-
相对线程安全: 常见线程安全。单次操作线程安全,但连续调用可能需外部同步。如
Vector
,HashTable
,synchronizedCollection()
包装的集合。 -
线程兼容: 常见非线程安全。对象本身非线程安全,但可通过客户端同步保证并发安全。如
ArrayList
,HashMap
等 Java API 大部分类。 -
线程对立: 应避免。无论是否同步都无法在多线程环境安全使用。Java 中极少出现。
线程安全的实现方法
-
互斥同步 (悲观锁):
synchronized
: JVM 内置锁,使用方便,性能相对较低。ReentrantLock
: JDK 提供的可重入锁,功能更强大,需手动释放锁。-
非阻塞同步 (乐观锁):
-
CAS (Compare-and-Swap): 硬件层面原子操作,无锁,基于冲突检测重试。
- 原子类 (Atomic Classes): 如
AtomicInteger
,基于 CAS 实现,提供原子性操作。 - ABA 问题: CAS 的漏洞,值被改回原值后 CAS 无法检测到变化。
AtomicStampedReference
可解决,但互斥同步可能更高效。 -
无同步方案:
-
栈封闭: 方法局部变量线程私有,天然线程安全。
- 线程本地存储 (ThreadLocal): 将数据绑定到线程,线程内共享,线程间隔离。如
ThreadLocal
类。 - 可重入代码 (Reentrant Code/Pure Code): 不依赖共享数据和系统资源,状态由参数传入,可安全中断和重入。
多线程的出现是要解决什么问题的?
多线程出现主要是为了解决以下问题:
- 平衡性能差异:CPU、内存和I/O设备之间存在巨大的速度差异,多线程可以在I/O等待期间继续利用CPU处理其他任务
- 提高资源利用率:充分利用多核CPU并行处理能力
- 提高响应性:允许应用程序同时处理多个任务,如用户界面保持响应的同时执行后台任务
- 简化编程模型:将复杂系统分解为可管理的并发执行单元
本质上,多线程是为了最大化系统资源利用率,提高程序运行效率和响应速度。
线程不安全是指什么? 举例说明
线程不安全是指当多个线程同时访问共享数据时,由于缺乏适当的同步机制,导致数据出现不一致或不正确的情况。
public class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作
}
public int getCount() {
return count;
}
}
当1000个线程并发调用increment()方法时,最终结果可能小于1000,因为count++
实际包含三个操作:读取、增加、写回。线程切换可能发生在这些步骤之间,导致结果不正确。
并发出现线程不安全的本质是什么?
线程不安全的本质在于三个核心问题:
-
可见性问题:
- 源自CPU缓存机制
- 一个线程对变量的修改对其他线程不可见
- 例如:线程A修改了变量值,但线程B读取的仍是旧值,因为修改尚未从A的CPU缓存写回主存
-
原子性问题:
-
源自线程切换/分时复用
- 操作被分割成多个步骤执行,线程可能在中间步骤切换
- 例如:
i++
操作被分为读取、加1、写回三步,中途可能被打断 -
有序性问题:
-
源自指令重排序优化
- 代码执行顺序可能与书写顺序不同
- 例如:编译器可能调整语句
int i=0; flag=true;
的执行顺序优化
Java是怎么解决并发问题的?
Java通过以下机制解决并发问题:
-
三个关键字:
- volatile:保证可见性、部分有序性,不保证原子性
- synchronized:保证可见性、原子性、有序性
- final:保证不可变对象创建的安全性
-
Java内存模型(JMM):
-
规定了线程间如何通信和交互
- 规范了JVM实现在多线程环境下的行为
-
8个Happens-Before规则:
-
单一线程规则:同一线程中,前面操作先行发生于后续操作
- 管程锁定规则:unlock先行发生于后续同一锁的lock
- volatile变量规则:对volatile变量的写先行发生于后续读
- 线程启动规则:start()先行发生于线程内的任何操作
- 线程终止规则:线程中所有操作先行发生于其他线程检测到该线程已终止
- 线程中断规则:interrupt()先行发生于被中断线程检测到中断
- 对象终结规则:构造函数执行完先行发生于finalize()开始
- 传递性:如果A先行发生于B,B先行发生于C,则A先行发生于C
线程安全是不是非真即假?
线程安全不是非真即假的命题,而是有不同程度的安全级别:
- 不可变:最安全的级别,如String、final基本类型
- 绝对线程安全:任何情况下都安全,无需外部同步
- 相对线程安全:单独操作安全,但特定顺序的连续操作可能需额外同步,如Vector
- 线程兼容:对象本身不安全,但可通过正确同步使其安全,如ArrayList
- 线程对立:无论如何都无法在多线程环境使用
线程安全有哪些实现思路?
实现线程安全的主要思路:
-
互斥同步(悲观策略):
- synchronized关键字
- ReentrantLock等显式锁
- 阻塞线程直到获取锁
-
非阻塞同步(乐观策略):
-
CAS (Compare-And-Swap)操作
- 原子类(如AtomicInteger)
- 无锁算法和数据结构
-
无同步方案:
-
栈封闭:仅使用线程私有数据
- 线程本地存储(ThreadLocal):每个线程维护独立的变量副本
- 不可变对象:设计不可变的共享数据结构
如何理解并发和并行的区别?
并发(Concurrency):
- 逻辑上同时处理多个任务
- 在单核CPU上,通过时间片轮转交替执行多个任务
- 强调任务的交替执行
- 例如:一个人同时处理多个任务,不停地切换任务
并行(Parallelism):
- 物理上同时处理多个任务
- 在多核CPU上,同时执行多个任务
- 强调任务的同时执行
- 例如:多个人同时各自处理一个任务
简言之:并发是关于结构,并行是关于执行。并发是指程序的结构能够处理多个同时(或近似同时)发生的事件;并行是指多个事件或计算在同一时刻真正同时发生处理。