JUC全称java.util.concurrent 处理并发的工具包(线程管理、同步、协调)
一.并发基础
多线程要解决什么问题?本质是什么?
- CPU、内存、I/O的速度是有极大差异的,为了合理利用CPU的高性能,平衡三者的速度差异,解决办法如下:
- 计算机体系结构CPU增加了缓存,以均衡与内存的速度差异。//导致可见性问题
- 操作系统增加了进行、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异;//导致原子性问题。
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。//导致有序性问题
(1)CPU增加缓存导致的可见性问题
- 原因:CPU的计算速度远远大于内存的访问速度。因此,CPU引入了缓存(L1,L2,L3),线程运行时,数据通常会先加载到CPU缓存中,计算结束后再同步到贮存。
- 可见性:不同线程可能运行在不同的CPU核心上,各自有独立的缓存。这导致线程在某些时刻无法及时感知其他线程对共享变量的修改。
- 例子:修改主存的值,但是其他线程可能还在读取缓存的数值。
(2)操作系统增加进程、线程导致的原子性问题
- 原因
- 为了充分利用CPU资源,操作系统通过进程和线程来分时复用CPU,但在多线程环境下,线程切换可能打断对共享变量的修改,使得某些操作无法完整执行,破坏了操作的原子性。
- 问题
- 某些操作需要多个步骤完成,而线程切换可能在中间插入,导致共享数据处于不一致的状态。线程切换破坏了连续操作的完整性,现代CPU支持指令级并行和多核架构,线程切换可能使对共享资源的访问被中断。
(3) 编译指令优化导致的有序性问题
- 原因:为了提升程序运行效率,编译器和CPU可能对指令执行顺序进行优化(指令重排序)。但这些优化可能导致程序在多线程环境下的执行顺序与代码的书写顺序不一致。
- 问题:在多线程中,指令重排序可能导致线程看到的执行顺序与预期不符,从而破坏程序逻辑。
Java怎么解决并发问题的?
- Java内存模型(java Memory Model)是个很复杂的规范,具体看Java内存模型详解
理解的第一个维度:核心知识点
- JMM(Java Memory model)本质上可以理解为,规范了JVM如何提供按需禁用缓存和编译优化的方法。
- 方法包括
- volatile、synchronized和final三个关键字
- Happens-Before规则
理解的第二个维度:原子性,有序性,可见性
原子性
- 在Java中,对基本数据类型的变量的读取和赋值(数字赋值,变量之间的相互赋值不属于)属于原子性操作,这些操作不可被中断。要么执行,要么不执行。
- 如果想实现更大范围操作的原子性,通过synchronized和Lock实现。因为这能够保证任一时刻只有一个线程执行该代码块,那么自然不存在原子性问题了。
可见性
- volatile关键字
- 当一个共享变量被volatile修饰时,会保证修改的值立即被更新到主存,当有其他线程需要读取时,会去内存中读取新值。
- 通过synchronized和Lock实现,能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会对变量的修改刷新到主存当中,因此可以保证可见性。
有序性
- volatile关键字
- synchronized和Lock也可以,保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。JMM是通过Happens-Before规则来保证有序性的。
二、并发关键字 volatile和synchronized
1.volatile
- 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。
- 禁止进行指令重排序
- volatile不保证原子性
参考:https://note.youdao.com/ynoteshare/index.html?id=0a2692da5a11099e198dcfe179c3c655&type=note&_time=1736243887397
可见性问题详解
- 过程
- 每个线程从主内存中将变量拷贝到本地内存作为副本并操作变量副本
- 写回主内存,会通过CPU总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
- 总之,当一个线程修改了volatile修饰的变量,当修改后的变量写回内存时,其他线程能立即看到最新值。
2.synchronized
参考:https://note.youdao.com/ynoteshare/index.html?id=a9d1d098bc64bef9c3e458f0000d37d4&type=note&_time=1736245618633
- 注意:volatile只能保证可见性和有序性,synchronized能保证可见性、有序性和原子性。
实现什么样类型的锁?
- 悲观锁:每次访问共享资源时都会上锁
- 非公平锁:线程获取锁的顺序并不一定是按照线程堵塞的顺序
- 可重入锁:已经获取锁的线程可以再次获取锁
- 独占锁或排他锁:该锁只能被一个线程所持有,其他线程均被堵塞。
关键字的使用方式
- 修饰普通同步方法
- 修饰静态同步方法
- 修饰同步方法块
3.CAS(Compare-And-Swap)
- 无锁的并发操作,比较和交换,在高并发场景下进行线程安全的变量更新,避免了传统加锁操作中的堵塞问题。
- 工作原理
- CAS 操作需要三个值:
- 内存值 V :当前变量的实际值。
- 预期值 A :当前线程认为变量的期望值。
- 新值 B :希望更新为的值。
- 操作流程如下:
- 如果
V == A
,则将V
更新为B
。 - 如果
V != A
,说明变量已经被其他线程修改,当前线程放弃更新。
- 如果
- CAS 操作需要三个值:
- 存在三大问题
- ABA问题
- 不适用于竞争激烈的情形下
- 只能保证一个共享变量的原子操作
三、JUC全局观
- Lock框架和Tools类
- Collection 并发集合
- Atomic 原子类
- Executors 线程池
1.JUC原子类
- java并发包(java.util.concurrent.atomic)提供的一组基于CAS(Compare-And-Swap)操作的线程安全类。对基本数据类型或对象的变量进行原子操作,而无需加锁,提供了一种高效的方式来实现线程安全。
- 主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。
分类
- 基本类型原子类 :
AtomicInteger
:对int
类型变量进行原子操作。AtomicLong
:对long
类型变量进行原子操作。AtomicBoolean
:对boolean
类型变量进行原子操作。
- 引用类型原子类 :
AtomicReference<T>
:对引用对象进行原子操作。AtomicStampedReference<T>
:解决 CAS 的 ABA 问题,结合版本号进行原子操作。AtomicMarkableReference<T>
:通过布尔标记和引用值一起操作,解决特定场景的并发问题。
- 数组原子类 :
AtomicIntegerArray
:对int
数组的元素进行原子操作。AtomicLongArray
:对long
数组的元素进行原子操作。AtomicReferenceArray<T>
:对引用类型数组的元素进行原子操作。
- 字段更新原子类(反射实现) :
AtomicIntegerFieldUpdater
:对类中int
类型字段进行原子更新。AtomicLongFieldUpdater
:对类中long
类型字段进行原子更新。AtomicReferenceFieldUpdater
:对类中引用类型字段进行原子更新。
- 累加器 :
LongAdder
和DoubleAdder
:对高并发场景下的累加操作进行优化。