> 文章列表 > 多线程 之 CAS与synchronized的优化过程

多线程 之 CAS与synchronized的优化过程

多线程 之 CAS与synchronized的优化过程


前言

本篇介绍什么是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与synchronized的优化过程

本质:CAS操作,是一条CPU上的指令,通过一条指令完成上述代码功能(上述示例中交换的功能);

2. CAS实现的操作

 CAS操作是一条CPU指令,这可以认为是原子的;
【注】原子指令:不加锁就能保证线程安全;

2.1 实现原子类

1.标准库里提供 AtomInteger 类 (原子类);
这样的原子类能够保证之前介绍的 ++,-- 的操作,线程是安全的;
了解原子类中的前置后置++,–方法;

               //后置++; -》num++;num.getAndIncrement();//前置++;-》++numnum.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());}

在原子类中,++操作是如何执行的,来看一下伪代码;

多线程 之 CAS与synchronized的优化过程

注: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代码块中包含代码的多少;代码多,粒度大,代码少,粒度少;

多线程执行为了提高效率,尽量让串行的代码少,并发的代码多(并发代码越多,效率越高);
特殊情况:
某个场景需要频繁加锁 / 解锁,编译器优化操作为一个粗粒度锁;因为频繁的加锁 / 解锁都需要开销;

多线程 之 CAS与synchronized的优化过程

锁粗化开实际的情况进行粗化;


总结

✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!

天赋娱乐音乐