前言
本篇介绍什么是CAS与synchronized的优化过程,如有错误,请在评论区指正,让我们一起交流,共同进步!
文章目录
- 前言
- 1. 什么是CAS?
- 2. CAS实现的操作
- 2.1 实现原子类
- 2.2 实现自旋锁
- 3. CAS的aba问题
- 4. synchronized的优化过程
- 总结
本文开始
1. 什么是CAS?
CAS:英文是compare and swap,比较和交换(一起执行,不能拆分的);
示例:有寄存器A , 寄存器B,内存C
比较寄存器A的值 和 内存C中的值,如果数值相同,就把寄存器B和内存C中的数值进行交换(给内存C赋值);
【注】与寄存器中的值相比,更加关注内存中的值;
图示:
本质:CAS操作,是一条CPU上的指令,通过一条指令完成上述代码功能(上述示例中交换的功能);
2. CAS实现的操作
CAS操作是一条CPU指令,这可以认为是原子的;
【注】原子指令:不加锁就能保证线程安全;
2.1 实现原子类
1.标准库里提供 AtomInteger 类 (原子类);
这样的原子类能够保证之前介绍的 ++,-- 的操作,线程是安全的;
了解原子类中的前置后置++,–方法;
//后置++; -》num++;
num.getAndIncrement();
//前置++;-》++num
num.incrementAndGet();
//前置--;-》--num;
num.decrementAndGet();
//后置--; -》num--;
num.getAndDecrement();
使用原子类实现两个线程,对num进行20000次++;
代码实现:
public static void main(String[] args) throws InterruptedException {
AtomicInteger num = new AtomicInteger(0);
Thread t = new Thread(() -> {
for (int i = 0; i < 20000; i++) {
//后置++; -》num++;
num.getAndIncrement();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 20000; i++) {
num.getAndIncrement();
}
});
t.start();
t2.start();
t.join();
t2.join();
//get 获取到数值
System.out.println(num.get());
}
在原子类中,++操作是如何执行的,来看一下伪代码;
注:oldValue 这里可以看作寄存器;value 看作内存中的值;
伪代码执行过程:
如果value 和 oldValue相同,oldValue + 1赋值给value中(相当于++),然后CAS返回true循环结束;
如果value 和 oldValue不相同,CAS直接返回false,再次循环,重新设置;
【注】上述代码中value, 两次都赋值给oldValue, 这种情况是在多线程情况下;value 是成员变量,多个线程同时调用 getAndIncrement方法;
CAS在自增中的作用:
确认当前value是否变过,如果没变过,才自增,如果变过,先更新,再自增;
2.2 实现自旋锁
自旋锁:反复检查当前锁状态,看锁是否解开;
伪代码看一下CAS实现自旋锁:
public class SpinLock {
private Thread owner = null;
//当前锁被那个线程持有;
public void lock(){
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
注:this.owner 是当前线程
CAS执行自旋锁执行过程:
1.比较this.owner 与 null;
2.比较this.owner==null,把当前线程引用设置到owner中,相当于加锁完成,循环结束;
3.this.owner != null, CAS直接返回false, 循环继续执行;(循环会不停访问锁是否释放)
【注】CAS:指令级,是读取内存的操作;内存可见性:编译器优化指令(调整指令),把 读内存 的指令调整成 读寄存器的指令;
3. CAS的aba问题
3.1 什么是aba问题 ?
cas的关键就是对比内存与寄存器中的值,通过对比检测内存是否改变过;如果对比的时候发现是相同的,但并不是没有变过,而是从a -》b -》a; 此时就会产生aba问题;
例子:买了一本书,你不能确定他是新书(没有被人买过看过),还是二手书(换了个包装);
【注】CAS只能对比值是否相同,不能确定值,在中间过程中是否改变过;
3.2 如何解决aba问题
解决方式:使用版本号解决aba问题;
数据即递增又递减,就需要使用 版本号变量 来控制;相当于每次CAS对比的时候就对比版本号,而不是数值;
【注】约定版本号只能增加,每次修改,都会增加一个版本号;
前提过程:给一个数num, 给一个版本号version,给一个old记录版本号;约定版本号只能递增;
执行:进行CAS操作,比较version与old是否一样,版本号一样old加1,num加1;
//示例:
int num = 10;
int version = 1;
old = version;
CAS(version, old, old+1,num++)
//每次比较版本号,如果一样,old加1,num加1;
4. synchronized的优化过程
4.1 synchronized 关键策略: 锁升级
锁升级过程:① -》② -》③ -》④(从左往右升级)
① 开始无锁状态
② 偏向锁: 开始加锁是偏向锁(程序运行时,jvm优化的手段);
③ 自旋锁:遇到锁竞争就是自旋锁;
④ 重量级锁:锁竞争激烈,就变成重量级锁(交给内核阻塞等待);
1.什么是偏向锁 ?
偏向锁:只是让线程对锁做个 “标记”;(只是标记,并不加锁)
【注】整个代码执行过程中,如果没有别的线程竞争这个锁,原来的线程就不会真加锁;一旦又其他线程尝试加锁,偏向锁立即升级为自旋锁(如果更激烈会再升级为重量级锁),其他线程只能等待;
2.为什么自旋锁要升级为重量级锁?
自旋锁:获取锁快,但是消耗大量CPU;
自旋锁升级为重量级锁,多于线程会在内核里阻塞等待;(阻塞等待:放弃CPU,不再销毁CPU资源,等待系统内核调度)
4.2 锁消除
锁消除:编译阶段做的优化手段;
锁消除的目的:
为了检测当前代码是否是多线程执行 / 是否有必要加锁;如果没有加锁,就会在编译过程自动把锁去掉;-》锁消除;
示例:
【注】synchronized不能滥用,见方法就加锁这是不适用的;
对于StringBuffer,它的关键方法都加synchronized关键字;如果是单线程使用,没有线程安全问题,编译器自动判断,没有必要加锁会自动去掉synchronized;
4.3 锁粒度
锁粒度:synchronized代码块中包含代码的多少;代码多,粒度大,代码少,粒度少;
多线程执行为了提高效率,尽量让串行的代码少,并发的代码多(并发代码越多,效率越高);
特殊情况:
某个场景需要频繁加锁 / 解锁,编译器优化操作为一个粗粒度锁;因为频繁的加锁 / 解锁都需要开销;
锁粗化开实际的情况进行粗化;
总结
✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!