Java EE\] 多线程初阶请看下面文章
[https://blog.csdn.net/Boop_wu/article/details/155076865?fromshare=blogdetail\&sharetype=blogdetail\&sharerId=155076865\&sharerefer=PC\&sharesource=Boop_wu\&sharefrom=from_linkhttps://blog.csdn.net/Boop_wu/article/details/155076865?fromshare=blogdetail\&sharetype=blogdetail\&sharerId=155076865\&sharerefer=PC\&sharesource=Boop_wu\&sharefrom=from_link](https://blog.csdn.net/Boop_wu/article/details/155076865?fromshare=blogdetail&sharetype=blogdetail&sharerId=155076865&sharerefer=PC&sharesource=Boop_wu&sharefrom=from_link "https://blog.csdn.net/Boop_wu/article/details/155076865?fromshare=blogdetail&sharetype=blogdetail&sharerId=155076865&sharerefer=PC&sharesource=Boop_wu&sharefrom=from_link")
> 本文介绍了Java多线程编程中的核心概念与技术,主要包括:
>
> 1. 常见锁策略:包括乐观锁/悲观锁、重量级锁/轻量级锁、自旋锁/挂起等待锁等,以及synchronized的自适应锁机制
> 2. synchronized原理:详细解析其锁升级过程(无锁→偏向锁→轻量级锁→重量级锁)和优化策略(锁消除、锁粗化)
> 3. CAS机制:深入讲解比较交换操作的实现原理、ABA问题及解决方案
> 4. JUC工具类:包括Callable接口、ReentrantLock、原子类、信号量等并发工具的使用
> 5. 线程安全集合:对比分析各种线程安全集合的实现原理和适用场景
>
> 文章系统性地讲解了Java并发编程的核心技术,帮助开发者深入理解多线程编程的底层机制和最佳实践
## 一.常见的锁策略
### 1.乐观锁和悲观锁
描述的是加锁时遇到的场景
**悲观锁** : 加锁的时候 , 预测接下来的锁竞争会非常激烈 , 就需要针对这种情况进行一些额外的工作
**乐观锁** : 加锁的时候 , 预测接下来的锁竞争情况不激烈 , 就不需要额外的工作
**注意** : Synchronized 初始使用乐观锁策略 , 但发现竞争比较**频繁**的时候 , 就会自动切换成悲观锁策略
### 2.重量级锁和轻量级锁
遇到加锁之后的解决方案

**重量级锁**: 加锁机制重度依赖于 OS 的 mutex ; 会有大量的内核态用户切换 , 容易引发线程的调度(一般是用来应对悲观场景下的锁 , 需要付出更多的代价)
**轻量级锁** : 加锁机制尽量不再使用 mutex , 而是在用户态代码完成 , 实在搞不定了 , 再使用 mutex ; 会有少量的内核态用户切换 , 不太容易引发线程调度
**用户态和内核态** : 是操作系统为权限隔离 , 保障安全而划分的两种 CPU 运行级别, 核心区别在于对硬件/系统资源的访问权限 , 所有进程运行都会在这两种状态之间切换
**注意** : synchronized 开始是一个轻量级锁 , 如果锁冲突比较**严重** , 就会变成重量级锁
### 3.挂起等待锁和自旋锁
**挂起等待锁** : 获取锁失败后 , 线程放弃 CPU资源 并进入阻塞队列挂起 , 只有锁被释放后才被唤醒(重量级锁的典型实现)
**自旋锁** : 获取锁失败后 , 线程不放弃 CPU 并 进入自旋(循环)检测锁是否被释放(轻量级锁的典型实现) (自旋锁的特点是不阻塞线程 , 而是通过循环消耗 CPU 资源来等待锁释放 , 适用于锁竞争不激烈 , 持有时间短的场景)
**注意** : synchronized 中的轻量级锁策略大概率是通过自旋锁的方式实现的
### 总结 :
悲观锁=\>重量级锁=\>挂起等待锁
乐观锁=\>轻量级锁=\>自旋锁
synchronzied 针对上述的锁策略是**自适应的**
### 4.互斥锁和读写锁
**互斥锁** : 任意时刻仅允许一个线程访问临界区(读/写均互斥)
**读写锁** : ① 读锁 : 多线程可同时持有(读并发) ; ② 写锁 : 仅一个线程持有(写互斥) ; 可以提高效率 , 减少互斥的机会
> Java 标准库中提供了 ReentrantReadWriteLock 类 , 实现了读写锁
>
> * ReenTrantReadWriteLock.ReadLock 类表示一个**读锁** , 这个对象提供了 lock/unlock 方法
> * ReenTrantReadWriteLock.WriteLock 类表示一个**写锁**, 实现了lock/unlock 方法
> * 其中 : ① 读加锁和读加锁之间 , 不互斥 ; ② 写加锁和写加锁之间 , 互斥 ; ③ 读加锁和写加锁之间 , 互斥
**注意** : synchronized 不是读写锁
### 5.可重入锁和不可重入锁
**可重入锁** : 允许同一线程多次获取同一把锁(递归锁) ; 内部机制 : 维护 \[ 线程归属标记+锁计数器 \] , 解锁需要计数器归 0
**不可重入锁** : 同一线程持有锁时 , 再次请求锁会阻塞/死锁
> 在 Java 中只要以 Reentrant 开头命名的锁都是可重入锁 , 而且 JDK 提高的所有现成的 Lock 实现类 , 包括 synchronized 关键字锁都是可重入的 ; 而 Linux 系统中提供的 mutex 时不可重入的
**注意** : synchronized 是可重入锁
### 6.公平锁和非公平锁(针对插队现象的公平和非公平)
**公平锁** : 按照线程等待的**先后顺序获取锁** , 先等待的线程先执行
**非公平锁**: 线程请求锁时直接尝试抢锁 , 抢不到再进入等待队列(机会均等)
> Java 标准库中
>
> 公平锁 : ReentrantLock(**true**) ,
>
> 非公平锁 : ReentrantLock(false)/synchronized
**注意** :
synchronized 是非公平锁
> 操作系统内部的线程调度就可以视为随机的 , 如果不做任何额外的限制 , 锁就是非公平锁 , 如果想要实现公平锁 , 就需要依赖额外的数据结构 , 来记录线程的先后顺序
## 二.synchronized 原理
### 1.核心特性
可重入 , 非公平 , 隐式加锁/解锁(JVM 自动处理)

### 2.锁升级
无锁 =\> 偏向锁 =\> 轻量级锁=\> 重量级锁
**注意** : 锁升级是单项的(只能从低到高) , 无法降级(不会从重量级到轻量级)
#### ① 无锁
此时没有锁竞争 , 无需加锁
#### ② 偏向锁(Biased Lock) -- 单线程无竞争场景
**目标** : 消除单线程重复加锁的开销(仅第一次加锁有少量开销)
* **加锁逻辑**:

* **解锁逻辑**:

#### ③ 轻量级锁(Lightweight Lock) -- 多线程交替竞争场景
**目标** : 用 CAS 自旋代替内核态阻塞 , 减少上下文切换开销
**触发条件**: 有多个线程竞争偏向锁 , JVM 撤销偏向锁 , 升级为轻量级锁
* **加锁逻辑**:

* **解锁逻辑**:

#### ④ 重量级锁(Heavuweight Lock) -- 多线程持续竞争场景
**目标**: 通过操作系统内核态的监视器锁保证互斥 , 牺牲性能换取稳定性
**触发条件**: 轻量级锁自旋次数达到阈值 , 或多个线程同时自旋 , JVM 升级为重量级锁
* **加锁逻辑**:

* **监视器锁** :

### 3.其他锁优化
#### 1.锁消除(Lock Elimination) - 移除"无竞争的锁"
> JVM 的 JIT 编译器通过逃逸分析判断 : 若锁对象是线程私有的 , 则该线程不存在线程并发竞争 , JIT 会在编译阶段自动移除该锁的加锁操作
**代码示例**:
##### ① 局部变量作为锁对象
```java
public void lockEliminationDemo() {
// 锁对象:局部变量 lockObj,仅当前线程可见(无逃逸)
Object lockObj = new Object();
synchronized (lockObj) { // JIT 会消除此锁
System.out.println("无竞争的同步代码块");
}
}
```
**优化后** :
```java
public void lockEliminationDemo() {
Object lockObj = new Object();
System.out.println("无竞争的同步代码块"); // 锁被消除
}
```
##### ②JDK 内置类的隐式锁消除
> `StringBuffer` 的 `append()` 方法是同步方法(加了 `synchronized`),但如果 `StringBuffer` 是局部变量(无逃逸),JIT 会消除锁:
```java
public String stringBufferDemo() {
// sb 是局部变量,无逃逸
StringBuffer sb = new StringBuffer();
sb.append("Java"); // 同步方法,锁被消除
sb.append("Lock"); // 同步方法,锁被消除
return sb.toString();
}
```
> `sb` 仅在当前方法内使用,无其他线程竞争,JIT 消除 `append()` 方法的 `synchronized` 锁,性能等同于 `StringBuilder`
#### 2. 锁粗化(Lock Coarsening) - 合并细粒度锁
> 当 JIT 检测到同一个锁对象被频繁,连续地加锁解锁 (如循环内加锁 , 连续调用同步方法) , 会见多次加解锁操作合并为一次 , 减少锁操作对底层的开销
**代码示例**:
##### ① 循环内细粒度的锁
```java
'public class demo38 {
private static int count = 0;
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (locker){
count++;
}synchronized (locker){
count++;
}synchronized (locker){
count++;
}
}
});
}
}
```
优化后
```java
public class demo38 {
private static int count = 0;
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (locker){
count++;
count++;
count++;
}
}
});
}
}
```
> **注意** : JIT 不会无限制的粗化锁 : 若合并后的锁持有时间过长 , 可能导致其他线程长期阻塞 , JIT 会平衡 加解锁次数 和 持有锁时间 , 仅对短时间内连续的锁操作进行粗优化
|--------|------------------|----------------------------|
| **维度** | **synchronized** | **ReentrantLock** |
| 锁类型 | 隐式锁(JVM 自动管理) | 显式锁(手动 lock ()/unlock ()) |
| 公平性 | 仅非公平锁 | 可配置公平 / 非公平锁 |
| 锁升级 | 自动(无锁→偏向→轻量→重量) | 无锁升级,默认非公平,底层依赖 CAS+AQS |
| 功能扩展 | 无(仅基础加锁 / 解锁) | 支持可中断锁、超时锁、条件变量(Condition) |
| | | |
## 三.CAS(Compara And Swap)
> CAS (比较和交换)是 Java 实现无锁并发编程的核心底层机制 , 属于乐观锁的实现 , 通过 CPU 原子指令保证操作的原子性 , 无需传统锁(如 synchronized)的阻塞/唤醒开销
### 1.核心操作
> * 一个 CAS 涉及到的操作 : 若内存地址 V 的值 == 预期值 A , 则将 V 更新为 B , 返回 true ; 否则不操作 , 返回 false
> * 硬件层面的 CPU 原子指令 , JVM 通过 Unsafe 类调用底层指令 , 保证"比较 - 交换" , 不可中断 ; 当多个线程同时对某个资源进行 CAS 操作时 , 只能有一个线程操作成功 , 但是并不会阻塞其他线程 , 其他线程只会收到操作失败的信号
1. check and set
2. read and update
#### 一个伪代码
**参数 : 内存地址(要操作变量的内存地址) , 预期值(变量的旧值) , 新值**
```java
booleanCAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
```
### 2.应用
#### ① 是java.util.concurrent.atomic 包
```java
import java.util.concurrent.atomic.AtomicInteger;
public class demo39 {
private static AtomicInteger count = new AtomicInteger(0);//赋初始值为0
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();//count++;
//count.incrementAndGet();//++count;
//count.addAndGet(4);//count+=4;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();//count++;
//count.incrementAndGet();//++count;
//count.addAndGet(4);//count+=4;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " +count);
}
}
```

#### ② 基于 CAS 实现自旋锁
伪代码
```java
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
```
若 owner 为 null(锁未被持有) , 则将其设为当前线程 ; 若锁已经被其他线程持有 , 则通过 while 循环自旋等待
### 3.ABA 问题
#### ① 问题描述 :
线程 1 操作是 \[初始值A =\> 经过修改操作值为B\] ; 线程 2 的操作是 \[数是指为 A =\> 修改为 C =\> 又退回 A\] ; 此时线程 1 执行 CAS 时 , 检测到变量仍然为 A , 误以为未被修改 , 会再次将其更新为 B
#### ② 问题核心 :
**CAS 仅校验最终值 , 忽略中间修改**
#### ③ 解决方案 :
**引入版本号**, 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期
当真正修改的时候 , 如果当前版本号 与 读到的版本号相同 , 则修改数据 , 并把版本号+1 , 如果当前版本号高于读到的版本号 , 就操作失败(认为数据已经被修改过了)
### 4.仅支持单个变量原子操作 问题
#### ① 问题描述 :
CAS 只能保证单个变量操作的原子性 , 无法直接实现多变量的原子更新(如同时更新 a 和 b)
#### ② 解决方案 :
##### **合并变量**: 将多个变量合并为一个对象 , (通过 AtomicReference 操作对象引用);
```java
import java.util.concurrent.atomic.AtomicReference;
class MulNum{
int a;
int b;
public MulNum(int a,int b){
this.a = a;
this.b = b;
}
}
public class demo40 {
private static AtomicReference
本质是对集合所有方法加 synchronized
java
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
注意 : 迭代时需要手动加锁 , 否则抛异常(ConcurrentMidificationException)
java
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class demo1 {
public static void main(String[] args) throws InterruptedException {
//多线程下的ArraryList
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.add("Hello");
syncList.add("World");
new Thread(new Runnable() {
@Override
public void run() {
syncList.add("Java");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
syncList.add("Thread");
}
}).start();
Thread.sleep(1000);
synchronized (syncList) {//必须锁定集合对象本身
for (String s : syncList) {
System.out.println(s);
}
}
}
}
③ 使用 JUC 包 , CopyOnWriteArrayList
核心实现 :
- 写操作 : (add/remove/set) 复制一份新的数组 , 在数组上修改 , 修改完成后替换原数组 , 全程加锁
- 读操作 : (get/iterator) 直接读原数组 , 无锁 , 性能极高
常见问题 :
问题 1 : 写操作性能地 , 内存开销大
解决方案 : 仅用于都铎写少的场景 ; 当频繁场景时改用 ReentrantLock 手动加锁的普通 List
问题 2 : 迭代过程中其他线程的修改不会反映到迭代器中
解决方案 : 需要加锁 ; 或者每次迭代前重新获取集合
3.多线程使用队列
① ArrayBlockingQueue
基于数组实现的阻塞队列
② LinkedBlockingQueue
基于链表实现的阻塞队列
③ PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
④ TransferQueue
最多只包含一个元素的阻塞队列
4.多线程使用哈希表
在多线程下使用哈希表(键值对储存) , 核心问题是解决线程安全和并发性能--普通 HashMap 线程不安全 , 并发读写会导致数据错乱 , 死循环 , ConcurrentModificationException 等问题
① 线程安全哈希表
|---------|--------------------------------------------|------------------------------------------|
| 类型 | 代表类 | 实现原理 |
| 全局锁哈希表 | Hashtable/ Collections.synchronizedMap | 所有方法加 synchronized;全局锁 |
| 精细化锁哈希表 | ConcurrentHashMap | JDK1.7:分段锁;JDK1.8:CAS + 局部synchronized |
| 写时复制哈希表 | ConcurrentSkipListMap(有序) | 跳表 + CAS,无锁设计 |
②Hashtable(低效)
只是简单的把关键的方法加上了 synchronized
这相当于直接对 Hashtable 对象本身加锁
- 如果多线程访问同一个 Hashtable 就会直接造成锁冲突
- 一旦触发扩容 , 就由该线程完成整个扩容过程
注意 : Hashtable 是 JDK1.0 的老旧类 , 性能差 , 已被ConcurrentHashMap 完全替代 , 仅作为了解
②ConcurrentHashMap(首选)
核心原理: 抛弃了 JDK1.7 的分段锁(把这些链表分成几组 , 每个组安排一个锁) , 改用更细粒度的锁机制 (锁桶)
- 读操作 : 无锁 , 通过 volatile 保证数据可见性
- 写操作 : 1)对空桶 : CAS 原子操作写入 , 无锁 ; 2) 对非空桶 : 对桶首届点加 synchronized 锁 , 仅阻塞当前桶的读写 , 其他桶可并发
- 当桶元素超过 8 个转为红黑树 , 提升查找性能
java
private static final Map<String, Integer> map = new ConcurrentHashMap<>();
扩容机制 : 化整为零
- 发现扩容的线程只创建新数组,搬几个元素 : 1) 触发扩容的线程先标记 , 并且创建新数组(容量翻倍) ; 2) 按步长 (16)拆分原数组 , 该线程仅迁移自己负责的一小段桶(目的:避免的那现场一次迁移所有元素导致长时间阻塞)
- 扩容期间新老数组同时存在 : 1)新数组作为全局变量 , 扩容全程与老数组共存 ; 2)迁移完成的桶做标记 , 未迁移的桶仍在老数组中(保证连续性)
- 后续线程搬家 , 各搬一小部分 : 1)任何线程执行 put/remove 时 , 若检测到扩容中 , 会先暂停自身操作 , 协助迁移 ; 2)每个线程仅迁移自己"认领的桶段"(避免重复)
- 搬完最后一个元素删除老数组 : 1)所有桶迁移完成 , 主线程将老数组替换 成新数组 , 并删除 (释放内存); 2)更新扩容阈值 , 并标记扩容完成
- 插入只往新数组加 : 扩容期间 , put 操作先检查桶是否已经迁移 , 若已迁移 : 直接写入新数组 , 若未迁移 : 先协助迁移 , 再写入新数组(防止老数组数据冗余)
- 查找需同时查找新老数组 : get 操作 , 先查老数组 , 若桶已迁移完(标记) , 则跳转到行数则查询 ; 若桶未迁移 : 直接查询老数组 ; 若新老数组都查不到 , 再返回 null(保证扩容期间读取数据不丢失 , 不遗漏)