【剧前爆米花--爪哇岛寻宝】java--CAS和synchronized的加锁过程以及一些优化策略

作者:困了电视剧

专栏:《JavaEE初阶》

文章分布:这是一篇关于CAS的实现以及java中synchronized的加锁过程和优化策略的文章,希望对你有所帮助!

 

目录

CAS

什么是CAS

CAS是怎样进行实现的

CAS的应用

实现原子类 

CAS的ABA问题及其引起的bug

什么是ABA问题

如何解决ABA问题

Synchronized原理

加锁过程——锁不断升级

偏向锁

轻量级锁

重量级锁

其他的优化操作

锁消除

锁粗化


CAS

什么是CAS

CAS: 全称 Compare and swap ,字面意思 :” 比较并交换 ,一个 CAS 涉及到以下操作:
我们假设内存中的原数据 V ,旧的预期值 A ,需要修改的新值 B
1. 比较 A V 是否相等。(比较)
2. 如果比较相等,将 B 写入 V 。(交换)
3. 返回操作是否成功。

以下是CAS在实现过程中的伪代码,不过CAS是一个原子性的操作,下面的伪代码不是。

   

CAS是怎样进行实现的

CAS的实现是基于计算机的硬件实现的,换言之:是因为硬件予以了支持,软件层面才能做到。 

所以由此观之,CAS更像一个乐观锁,当某一线程正在执行CAS操作并成功后,其他线程并不会进行阻塞,而是进行判断,发现检测出预期值和原先不同了,所以只会收到操作失败的信号。

CAS的应用

实现原子类 

标准库中提供了 java.util.concurrent.atomic , 里面的类都是基于这种方式来实现的 .
典型的就是 AtomicInteger . 其中的 getAndIncrement 相当于 i++ 操作 .

这是伪代码的实现方式: 

 

 当有一个线程实现了++的功能后,此时value的值就会发生改变不等于oldValue的值,所以其他线程执行++的操作就会失败。

CAS的ABA问题及其引起的bug

什么是ABA问题

CAS是通过判定老值和此时内存中的值是否相等来进行判定的,那么有没有一种可能,内存中的值通过两次以上的改变又变回了原来的值呢?

举个栗子:

有一个存有100元的账户,现在有两个线程去做同一件任务——从这个账户中取50元,A线程先通过预期值,发现账户中就是100元,于是取走了50,现在账户中只剩50元,然后B线程发现账户中只剩50元知道该任务已经被执行过了,于是就不再执行一次。

但是,如果A线程取走50后,其他线程给这个账户又打了50元,使这个账户又变成了100元,那B线程就无法进行正确的判定,于是这个账户又被取走了50元。

bug就这样产生了。

如何解决ABA问题

要解决这种ABA问题很简单,只需要设置一个版本号即可,CAS操作在读旧值的时候同时读版本号,在执行操作时,匹配值是否相等的同时,也匹配版本号是否相等,都相等则进行CAS操作,同时,版本号加一,以防ABA的情况造成bug。

Synchronized原理

在之前的文章中介绍了锁策略

【剧前爆米花--爪哇岛寻宝】常见的锁策略——乐观锁、读写锁、重量级锁、自旋锁、公平锁、可重入锁等_困了电视剧的博客-CSDN博客

那java中的synchronized遵循了哪些锁策略呢?

1. 开始时是乐观锁 , 如果锁冲突频繁 , 就转换为悲观锁 .
2. 开始是轻量级锁实现 , 如果锁被持有的时间较长 , 就转换成重量级锁 .
3. 实现轻量级锁的时候大概率用到的自旋锁策略
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁

加锁过程——锁不断升级

偏向锁

第一个尝试加锁的线程,优先进入偏向锁状态。

偏向锁不是真的 " 加锁 ", 只是给对象头中做一个 " 偏向锁的标记 ", 记录这个锁属于哪个线程 .
如果后续没有其他线程来竞争该锁 , 那么就不用进行其他同步操作了 ( 避免了加锁解锁的开销 )
如果后续有其他线程来竞争该锁 ( 刚才已经在锁对象中记录了当前锁属于哪个线程了 , 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态 , 进入一般的轻量级锁状态 .
偏向锁本质上相当于 " 延迟加锁 " . 能不加锁就不加锁 , 尽量来避免不必要的加锁开销 .
但是该做的标记还是得做的 , 否则无法区分何时需要真正加锁 .

锁加了但没完全加,只要没有竞争,那我加不加就没人知道,偏向锁就是这种,节省了资源。 

轻量级锁

随着其他线程进入竞争 , 偏向锁状态被消除 , 进入轻量级锁状态 ( 自适应的自旋锁 ).
此处的轻量级锁就是通过 CAS 来实现 .
通过 CAS 检查并更新一块内存 ( 比如 null => 该线程引用 )
如果更新成功 , 则认为加锁成功
如果更新失败 , 则认为锁被占用 , 继续自旋式的等待 ( 并不放弃 CPU).
自旋操作是一直让 CPU 空转 , 比较浪费 CPU 资源 .
因此此处的自旋不会一直持续进行 , 而是达到一定的时间 / 重试次数 , 就不再自旋了 .
也就是所谓的 " 自适应 "

轻量级锁就是当线程被占用的时候,其他线程并不进入阻塞状态,因为进入阻塞状态会发生用户态和内核态的转换等消耗大量资源的操作。

重量级锁

如果竞争进一步激烈 , 自旋不能快速获取到锁状态 , 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex .
1.执行加锁操作 , 先进入内核态 .
2.在内核态判定当前锁是否已经被占用
3.如果该锁没有占用 , 则加锁成功 , 并切换回用户态 .
4.如果该锁被占用 , 则加锁失败 . 此时线程进入锁的等待队列 , 挂起 . 等待被操作系统唤醒 .
5.经历了一系列的沧海桑田 , 这个锁被其他线程释放了 , 操作系统也想起了这个挂起的线程 ,       于是唤醒这个线程, 尝试重新获取锁 .

到了重量级锁就不会继续在往上升级了

总结:

刚开始进行加锁操作的时候,先加偏向锁这个锁并不是真的加锁,而是加一个标记,因为此时还没有竞争,当其他线程加入竞争的时候,就会升级成轻量级锁,此时该锁会不断地进行自旋,虽然自旋也会消耗资源但其“自适应”的特性会让该消耗的资源小于阻塞所需的资源,再接着,随着竞争的越发激烈,这个轻量级锁就会变成重量级锁,重量级锁就需要进行状态的转换,消耗的资源就会大大增加。 

其他的优化操作

除了上述的锁进化的操作以最大程度节约资源之外,synchronized还有其他的优化策略。

锁消除

编辑器和jvm会判断锁是否可以消除,如果可以,就直接消除,这就进一步节约了资源。

举个栗子:

StringBuffer是StringBuilder的线程安全版,即StringBuffer在一些重要的方法上都加上了synchronized关键字,当我们在单线程——不会出现线程安全问题的地方使用StringBuffer时,这个synchronized在执行的时候就会自动消除,以节约资源。

锁粗化

一段逻辑中如果出现多次加锁和解锁的操作,那么编辑器和jvm就会自动进行锁粗化,即将一段整体加上锁。

因为每次加锁和解锁的操作都会造成资源的消耗,所以,这样做很有必要,会大大节约资源。

举个栗子:

一个职工向领导汇报事情A,B,C。他一次只汇报一件,汇报三次,和他一次性就将这三个事件全部汇报完毕,一定是一次全部汇报的效率更高。

以上就是本篇文章的全部内容,如有疏漏还请指正!

 

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值