Java 并发编程(六)|并发进阶高频:CAS、锁升级

并发进阶

前言

本篇承接上一篇《四大并发实战:单例、阻塞队列、定时器与线程池》,深挖 Java 并发底层锁机制与无锁 CAS 编程,覆盖面试必考的各类锁区分、CAS 原子操作、synchronized 三大优化(锁升级 / 消除 / 粗化),全部配套伪代码 + 可运行实战案例,吃透底层原理类面试核心考点。

常见的锁策略

乐观锁 VS 悲观锁

这是"锁的一种特性"。

此处的悲观和乐观,是对后续锁冲突是否激烈给出的预测。

  1. 乐观锁:预测接下来锁冲突的概率不大,就可以少做一些工作。
  2. 悲观锁:预测接下来锁冲突的概率很大,就应该多做一些工作。

重量级锁 VS 轻量级锁

  1. 重量级锁:锁的开销比较大。
  2. 轻量级锁:锁的开销比较小。

乐观锁,通常是轻量级的锁;悲观锁,通常是重量级的锁。

自旋锁 VS 挂起等待锁

  1. 自旋锁:一种轻量级锁的典型实现。
    (1)往往在纯用户态实现。
    (2)比如一个while循环,不停检查当前锁是否被释放,若没有,就继续循环;释放了就获取到锁,从而结束循环。忙等,消耗CPU,换来更快的响应速度。
  2. 挂起等待锁:一种重量级锁的典型实现。
    (1)要借助系统API实现。
    (2)一旦出现锁竞争,就会在内核中触发一系列的动作,比如让这个线程进入阻塞状态,暂时不参与CPU调度。阻塞的开销很大。

读写锁

读写锁把加锁分成两种:读加锁、写加锁

  1. 读加锁:读的时候,可以读,但是不可以写。
  2. 写加锁:写的时候,不可以读,也不可以写。
  3. 两个线程加锁过程中:
    (1)读锁和读锁之间,不会产生竞争;
    (2)读锁和写锁之间,有竞争;
    (3)写锁和写锁之间,有竞争。

可重入锁 VS 不可重入锁

  1. 可重入锁:一个线程针对同一把锁,连续加锁两次,不会死锁。
  2. 不可重入锁:一个线程针对同一把锁,连续加锁两次,会死锁。

公平锁 VS 非公平锁

当很多线程尝试加同一把锁时,一个线程能够拿到锁,其他线程阻塞等待,一旦第一个线程释放锁之后,接下来哪个线程能够拿到锁?

  1. 公平锁:按照先来后到的顺序。
  2. 非公平锁:剩下的线程以均等的概率来重新竞争锁。

操作系统提供的加锁API默认是非公平锁。

synchronized 锁的策略

  1. 乐观锁 VS 悲观锁:自适应;
  2. 轻量级锁 VS 重量级锁:自适应;
  3. 自旋锁 VS 挂起等待锁:自适应。
  4. 自适应:
    (1)初始情况下,synchronized会预测当前的锁冲突的概率不大,此时以乐观锁模式运行(轻量级锁,基于自旋锁的方式实现)
    (2)在实际使用过程中,如果发现锁冲突的情况比较多,synchronized就会升级成悲观锁(重量级锁,基于挂起等待的方式实现)
  5. 不是读写锁,是可重入锁,是非公平锁

CAS

认识CAS

  1. CAS(Compare and swap):比较交换的是内存和寄存器。
  2. 比如有一个内存M,两个寄存器A,B
  3. CAS(M,A,B):如果M和A的值相同,就把M和B里的值进行交换,同时整个操作返回true;否则,无事发生,同时整个操作返回false;交换的本质是为了把B赋值给M。
  4. CAS其实是一个CPU指令。单个CPU指令是原子的,就可以使用CAS完成一些操作,进一步替代加锁。
  5. 基于CAS实现线程安全的方式为"无锁编程"。

应用CAS

实现原子类

java 复制代码
public class Demo {
	
	public static AtomicInteger count=new AtomicInteger(0);

	public static void main(String[] args) throws InterruptedException {
		// TODO 自动生成的方法存根
		Thread t1=new Thread(()->{
			for(int i=0;i<50000;i++) {
				count.getAndIncrement();//count++
			}
		});
		Thread t2=new Thread(()->{
			for(int i=0;i<50000;i++) {
				count.getAndIncrement();
			}
		});
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(count.get());
	}

}

原子类里面是基于CAS实现的。

伪代码实现:

java 复制代码
class AtomicInteger {
 private int value;
 public int getAndIncrement() { 
 	int oldValue = value; 
 	while ( CAS(value, oldValue, oldValue+1) != true) { //这里的判定就是在判断是否有别的线程穿插过来
 		oldValue = value; 
 	} 
 	return oldValue;
 }
}

加锁是通过阻塞的方式避免穿插执行,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; 
 }
}

CAS的ABA问题

CAS进行操作的关键:通过值"没有发生变化"来作为"没有其他线程穿插执行"的判定依据,但这种判定方式不严谨,极端情况下,可能会有另一个线程穿插进来,发生将值从A->B->A,针对第一个线程,虽然值没变,但是实际上已经被穿插执行。

解决方法:

  1. 让判定的数值,按照一个方向增长,不要反复横跳(有增有减,就会发生ABA)。
  2. 引入一个额外的变量(版本号),约定每次修改,版本号就自增一次,此时在使用CAS判定时就不是判定值了,而是判定版本号,看版本号是否变化了,若版本号没变,就代表没有线程穿插执行。

synchronized 原理

锁升级

synchronized的状态变化:无锁 -> 偏向锁 -> 自旋锁(轻量级锁) -> 重量级锁。

  1. 锁升级的过程是单向的,不能再降级了。
  2. 偏向锁:不是真正加锁,只是做了一个标记,完全是运行时的优化策略。当锁冲突出现时,偏向锁就升级成轻量锁,就真正加锁了。
  3. 锁升级的过程就是在性能和线程安全之间尽量进行权衡。

锁消除

  1. 编译器会自动针对当前写的加锁的代码,做出判定,如果编译器觉得这个场景,不需要加锁,此时就会把写的synchronized优化掉。
  2. 例如:StringBuilder 不带synchronized;StringBuffer 带有synchronized;
    写了synchronized也不一定线程安全;
    若在单个线程中使用StringBuffer,编译器就会把synchronized优化掉;
  3. 编译器只会在自己非常有把握时,才进行锁消除
  4. 锁消除:编译期锁消除;运行时锁消除。
  5. 保守保留锁:编译器针对synchronized锁的处理策略。
  6. 核心:编译期无法预判运行时的线程竞争情况,为了保证程序正确性,不会擅自删除或修改代码逻辑,只会把锁的语义完整保留到字节码中

锁粗化

锁的粒度:synchronized中,代码越少,就认为锁的粒度越粗;代码越少,锁的粒度越细。

java 复制代码
for(...) {
	sync(lock){
		n++ ;
	} //锁粒度细
}
 
sync(lock){
	for(...) {
		n++ ;
	} //锁粒度粗
}

锁的粒度细的时候,能够并发执行的逻辑更多,更有利于充分利用CPU资源。

若粒度细的锁,被反复进行加锁解锁,可能实际效果还不如粒度粗的锁(涉及到频繁的锁竞争)。

全文总结

本文完整梳理并发底层核心面试考点:

  1. 六类锁策略区分,掌握不同锁适用业务场景;
  2. CAS 无锁编程底层 CPU 指令原理、原子类实战、手写自旋锁,以及 ABA 问题解决方案;
  3. synchronized 底层三大核心优化:单向锁升级、编译期锁消除、锁粗化,理解 JVM 锁性能优化逻辑。

后续会更新 JUC 工具类、线程安全集合全套实战内容,欢迎点赞收藏,评论区交流面试学习心得!

🔗 系列文章导航

本篇是「Java并发编程系列」的连载内容,点击链接查看完整系列:

🔹 上一篇:Java 并发编程(五)|四大并发实战:单例、阻塞队列、定时器与线程池

👉 点击直达「Java并发编程」专栏合集

相关推荐
techdashen1 小时前
Arborium:把 tree-sitter 语法高亮打包成 Rust 文档生态的基础设施
开发语言·后端·rust
要开心吖ZSH2 小时前
MVCC 进阶:快照读 vs 当前读、幻读与 Next-Key Lock
java·数据库·sql·mysql·mvcc
京韵养生记2 小时前
【无标题】
java·服务器·前端
会周易的程序员2 小时前
microLog 后端开发指南
开发语言·c++·物联网·设计模式·日志·iot·aiot
小强库计算机毕业设计2 小时前
源码分享Spring Boot + Vue3 的校园社团管理系统
java·spring boot·后端·计算机毕业设计
Esaka_Forever2 小时前
Python 完整内存管理机制详解
开发语言·python·spring
星空露珠2 小时前
迷你世界UGc3.0脚本Wiki[剧情动画模块管理接口 Timeline]
开发语言·数据结构·算法·游戏·lua
格子软件2 小时前
2026年分布式GEO代理流量调度:源码级状态机防重挂实战
java·vue.js·人工智能·spring boot·分布式·vue
hj2862512 小时前
Docker 容器化技术标准化笔记
java·笔记·docker