文章目录
- 一、常见锁策略
-
- [1. 乐观锁 vs 悲观锁](#1. 乐观锁 vs 悲观锁)
- [2. 轻量级锁 vs 重量级锁](#2. 轻量级锁 vs 重量级锁)
- [3. 自旋锁 vs 挂起等待锁](#3. 自旋锁 vs 挂起等待锁)
- [4. 普通互斥锁 vs 读写锁](#4. 普通互斥锁 vs 读写锁)
- [5. 可重入锁 vs 不可重入锁](#5. 可重入锁 vs 不可重入锁)
- [6. 公平锁 vs 非公平锁](#6. 公平锁 vs 非公平锁)
- [7. synchronized锁策略详解](#7. synchronized锁策略详解)
-
- (1)锁升级
- [(2) 锁消除](#(2) 锁消除)
- [(3) 锁粗化](#(3) 锁粗化)
- 二、CAS
-
- [1. 工作原理](#1. 工作原理)
- [2. 典型应用](#2. 典型应用)
- [3. ABA问题](#3. ABA问题)
- [三、JUC 常用核心类(java.util.concurrent)](#三、JUC 常用核心类(java.util.concurrent))
-
- [1. Callable & FutureTask](#1. Callable & FutureTask)
- [2. ReentrantLock:可重入锁](#2. ReentrantLock:可重入锁)
- [3. Semaphore 信号量](#3. Semaphore 信号量)
- [4. CountDownLatch](#4. CountDownLatch)
- 四、线程安全集合类
-
- [1. List 集合](#1. List 集合)
- [2. 阻塞队列](#2. 阻塞队列)
- [3. Map 集合](#3. Map 集合)
一、常见锁策略
锁是解决多线程并发安全的核心,主流锁策略适用于各类编程语言,Java中synchronized、ReentrantLock均基于这些思想实现。
1. 乐观锁 vs 悲观锁
- 乐观锁 :默认冲突概率低,不加锁直接访问,仅在数据提交时检测冲突,冲突则失败重试,适合低并发场景。
- 悲观锁 :默认并发冲突概率高,访问数据前先加锁,其他线程阻塞等待,适合锁竞争激烈的场景。
JDK1.8的synchronized初始为乐观锁,锁竞争频繁时自动转为悲观锁。
2. 轻量级锁 vs 重量级锁
二者区别在于是否依赖操作系统内核互斥锁(mutex):
- 轻量级锁 :适用于乐观的场景下。尽量在用户态完成操作,减少用户态/内核态切换,开销更小,更高效。
- 重量级锁:适用悲观的场景。重度依赖OS的mutex,开销更大,更低效
synchronized无竞争时为轻量级锁,竞争加剧后膨胀为重量级锁。
3. 自旋锁 vs 挂起等待锁
- 自旋锁 :轻量级锁 的典型实现。获取锁失败后不阻塞,循环重试抢锁(也就是忙等)。因为在乐观场景下,出现锁竞争的概率比较小,即使出现锁竞争,短时间内很快就能拿到锁。
- 优点:锁释放后可立即获取,响应快;
- 缺点:锁持有时间久会持续消耗CPU。
- 挂起等待锁 :重量级锁的典型实现。抢锁失败后线程阻塞、释放CPU,由操作系统唤醒,无CPU消耗,但唤醒延迟高。
synchronized轻量级锁底层采用自适应自旋锁。
4. 普通互斥锁 vs 读写锁
1. 普通互斥锁: 像synchronized这样,只涉及加锁和解锁两种操作
2. 读写锁: 分为读锁和写锁。
多个线程多一个数据,是线程安全的;多个线程读取,一个或多个线程修改数据,就会涉及到线程安全问题。在大部分读操作,少部分写操作的场景下,使用synchronized就会有严重的锁冲突。
而读写锁确保读锁和读锁之间不互斥,读锁与写锁,写锁与写锁之间互斥
Java提供了读锁ReentrantReadWriteLock.ReadLock和写锁ReentrantReadWriteLock.WriteLock,都属于ReentrantReadWriteLock的内部类;synchronized并非读写锁。
5. 可重入锁 vs 不可重入锁
- 可重入锁 :同一线程可多次获取同一把锁 ,不会造成死锁,
synchronized、ReentrantLock均为可重入锁,通过线程标识+计数器实现。 - 不可重入锁:同一线程重复加锁会阻塞自身,引发死锁,Linux原生mutex就是不可重入锁。
可重入锁需要记录当前是哪个线程拿到了锁,并通过计数器记录当前加锁了多少次,在合适的时候进行解锁。
6. 公平锁 vs 非公平锁
- 公平锁 :遵循先来后到,线程按等待顺序获取锁,需额外结构记录排队顺序,性能略低。
- 非公平锁:不保证顺序,新线程可直接抢占锁,减少线程切换,默认锁均为非公平锁。
synchronized是非公平锁;ReentrantLock可手动开启公平模式。
7. synchronized锁策略详解
(1)锁升级
- 无锁:没有
synchronized修饰的代码块处于无锁的状态 - 偏向锁:刚进入
synchronized代码块内不会立即加锁,而是做一个简单的标记。如果没有其他线程来竞争这个锁,最终当前线程执行完毕解锁代码也只是简单的清楚标记。这个标记非常轻量,相比加锁解锁效率高很多。 - 轻量级锁:拿到偏向锁的线程运行过程中遇到了其他线程尝试抢这把锁,就会升级为轻量级锁。
- 重量级锁:JVM发现这把锁竞争非常激烈,就会升级成重量级锁。
(2) 锁消除
编译器会判定当前的代码逻辑是否真的需要锁。如果确实不需要这把锁,就会会把synchronized去掉。
(3) 锁粗化
锁的粒度:加锁和解锁之间,包含代码越多,就认为锁的粒度越粗;这里代码多不仅指行数,还要考虑实际执行的指令和时间
如果一块代码反复对细粒度的代码进行加锁,每一次解锁后再加锁都会涉及到竞争,编译器就可能优化成粗粒度的加锁,从而提高效率。
二、CAS
CAS就是指比较和交换(Compare and Swap),是CPU的一条指令。
1. 工作原理
伪代码:
cpp
boolean CAS(address,expectValue,swapValue){
if(address == expectValue){
&address = swapValue;
return true;
}
return false;
}
address表示内存地址,expectValue表示寄存器1的值,swapValue表示寄存器2的值。
- 比较内存值与寄存器1是否相等;
- 相等则把内存中的值和寄存器2进行交换。
一般情况只关心交换后内存中的值,不关心寄存器2的值,此处可以把交换的操作理解成赋值。本质上还是交换,基于交换实现了赋值。
通过上面的分析来看,虽然逻辑略显复杂,但是CAS是CPU的一条指令,而CPU是以指令为单位工作,也就是说CAS具有原子性的特性
2. 典型应用
CAS封装:CPU=>操作系统=>JVM=>Java 标准库=>Java程序员
原子类
java.util.concurrent.atomic包下所有类都是原子类,基于CAS实现无锁原子操作,避免加锁操作,性能更高。
AtomicInteger:原子类int
| 常用方法 | 功能说明 |
|---|---|
| get() | 获取当前值 |
| set(int val) | 直接设置值 |
| incrementAndGet() | 自增1,返回新值,类似++i |
| getAndIncrement() | 自增1,返回旧值 ,类似i++ |
| decrementAndGet() | 自减1,返回新值 |
| addAndGet(int delta) | 加上增量,返回新值 |
AtomicBoolean:原子类boolean
| 常用方法 | 功能说明 |
|---|---|
| AtomicBoolean | get()、set(boolean val) |
| compareAndSet(boolean expect, boolean update) | CAS更新布尔 |
getAndIncrement工作原理
伪代码:
java
class MyAtomicInteger{
private int value;
public int getAndIncrement(){
int oldValue = value;//理解成寄存器,把内存存到寄存器中
while(!CAS(value,oldValue,oldValue+1)){
oldValue = value;
}
return oldValue;
}
}
- 使用oldValue记录value,相当于把内存中的值存到寄存器中
- 调用CAS,如果value和oldValue相等,则让oldValue+1赋值给value,此时CAS返回true,循环条件不成立,就结束运行
- 调用CAS,如果value和oldValue不相等,CAS返回false,进入循环,更新寄存器中的结果。

即使上述代码存在线程切换,进行自增之前,先判断当前寄存器中的值是否准确,不过不正确,就会重新读取。
自旋锁
伪代码:
java
public class SpinLock{
//如果是null 说明锁是空闲的
//如果非null,说明已经被其他线程占用
private Thread owner = null;
public void lock{
//通过CAS判断当前这锁是否被某个线程持有
//如果被占有就自旋等待
//如果没有就尝试把owner设为当前尝试加锁的线程
while(CAS(this.owner,null,Thread.currentThread())){\
}
}
public void unlock(){
//单个的赋值操作就是原子的
this.owner = null;
}
}
由于lock循环体是空的,整个循环过程非常快(也就是忙等),一旦其他线程释放了锁,就可以立即获取到锁
3. ABA问题
- 问题:变量从A→B→A,CAS无法识别数据被修改过,导致逻辑异常。
- 解决方案 :引入版本号 ,每次修改数据版本号自增,CAS同时校验数据值+版本号;Java中使用
AtomicStampedReference实现带版本的CAS。
三、JUC 常用核心类(java.util.concurrent)
JUC包提供了大量并发工具,弥补synchronized灵活性不足的问题,是多线程开发主力。
1. Callable & FutureTask
Callable与 Runnable类似,都可以描述线程执行的任务。
Runnable没有返回值,不能抛出异常;Callable有泛型返回值,可以抛出异常,用于线程执行任务后获取结果。
Callable需要搭配FutureTask使用,FutureTask负责包装Callable任务,交给Thread执行,Callable的返回结果交给FuturnTask,可以通过get()方法获取。
get()就是获取到FutureTask的返回值,来自Callable的call方法。如果线程执行完毕就可以拿到结果,否则get()就会阻塞。
- 示例代码:
java
public class demo33 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
for(int i = 0;i<2000;i++){
result += 1;
}
return result;
}
};
FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
Thread t = new Thread(futureTask);
t.start();
System.out.println(futureTask.get());
}
}
创建线程的写法汇总:
- 继承Thread类:定义单独的类或匿名内部类
- 实现Runnable:定义单独的类或匿名内部类
- lambda表达式
- 实现Callable:定义单独的类或匿名内部类
- 线程池,ThreadFactory
2. ReentrantLock:可重入锁
与synchronized功能类似,属于显式锁,灵活性更强,二者对比:
synchronized是关键字、JVM隐式实现,自动释放锁;ReentrantLock是Java类,需手动调用lock()和unlock()。synchronized抢锁失败会永久等待;ReentrantLock支持tryLock()超时放弃。ReentrantLock默认是非公平锁,可实现公平锁ReentrantLock支持Condition精准唤醒指定线程,相比wait/notify 功能更强大。
使用规范:lock()放在try外,unlock()放在finally中,保证锁一定释放。
3. Semaphore 信号量
本质是计数器,表示"可用资源的个数",用来控制并发访问资源的线程数量,实现限流、资源池场景。
可以把信号量理解成停车场的指示牌,当驶入车辆,可用空位就会-1,驶出车辆,可用空位+1
- P操作:申请资源,可用资源为空时阻塞等待
- V操作:释放资源
PV操作都是原子的,可以在多线程环境下直接使用
特殊情况:信号量初始值为1,取值要么是1,要么是0,称为二元信号量,等价于锁。
4. CountDownLatch
多线程可以把一个大任务拆成多个子任务,CountDownLatch可以等待一组线程全部执行完毕后再继续主线程,类似"倒计时"。
- 构造方法中指定参数,描述拆成了多少个任务
- 每个任务执行完毕之后,调用一次countDown方法,计数器-1
- 主线程中调用await(),等待所有任务执行完毕.
- 代码示例
java
public class demo36 {
public static void main(String[] args) throws InterruptedException {
//十个子任务
CountDownLatch countDownLatch = new CountDownLatch(10);
ExecutorService executorService = Executors.newFixedThreadPool(4);
for(int i = 0;i<10;i++){
int id = i;
executorService.submit(()->{
System.out.println("子任务开始执行:"+id);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子任务结束执行:"+id);
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("所有任务执行完毕");
executorService.shutdown();
}
}
四、线程安全集合类
传统集合(ArrayList、HashMap)线程不安全,多线程环境需使用专用安全集合:
1. List 集合
Vector/Stack:古老线程安全类,全局加锁,性能差。Collections.synchronizedList:基于synchronized封装,全方法加锁,性能略差。CopyOnWriteArrayList:写时复制 ,读不加锁、写时复制新容器,适合读多写少场景;- 缺点是内存占用高、数据弱一致性;不适合多线程修改的情况
大多数情况还是需要手动上锁,自行判断哪些操作需要确保原子性。
2. 阻塞队列
常用于线程池、生产者-消费者模型:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue。
3. Map 集合
- Hashtable:对全局对象加锁,也就是说任意两个线程访问不同的元素,都要竞争。所有方法串行执行,并发效率极低,key/value不允许为null。
- ConcurrentHashMap :主流线程安全哈希表,优化点:
- 锁哈希桶(也即是链表的头节点),大幅降低锁竞争;
- 读操作无锁,利用
volatile保证可见性;写操作加synchronized,扩容多线程协作完成,效率极高;key不允许为null。 - 对于哈希表的长度size,使用原子类维护。