常见的锁策略、CAS、以及synchronized原理

目录

常见的锁策略

CAS

synchronized原理


常见的锁策略

一.乐观锁与悲观锁

乐观锁,也就是预测锁竞争不是很激烈的锁(做的工作可能相对更少);悲观锁,也就是预测锁竞争会很激烈的锁(做的工作可能会更多)。乐观和悲观的区分,主要就是看预测锁竞争激烈程度的结论。

二.轻量级锁和重量级锁

轻量级锁加锁和解锁的开销比较小,效率更高;重量级锁加锁开销比较大,效率更低。多数情况下,乐观锁也是一个轻量级锁;多数情况下,悲观锁也是一个重量级锁。

三.自旋锁和挂起等待锁

自旋锁是一种典型的轻量级锁;挂起等待锁是一种典型的重量级锁。当锁被释放的时候,自旋锁可以第一时间感知到并且有机会获取到锁,很明显,自旋锁占用了大量的系统资源。挂起等待锁则选择挂起,也就把CPU省下来了,就可以干别的事情。

四.互斥锁和读写锁

互斥锁就是像之前用过的像synchronized这样的锁。提供加锁和解锁两个操作,如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待。读写锁提供了三种操作:针对读加锁、针对写加锁、解锁。在Java标准库里面也提供了读写锁的具体实现(两个类,读锁类,写锁类)。

五.公平锁与非公平锁

此处把公平锁定义为“先来后到”,公平锁指的是,当锁被释放之后,就由等待队列中最早等待的来获取到锁。而非公平锁指的就是获取锁“凭自己实力”。操作系统和Java synchronized原生都是“非公平锁”操作系统这里的针对加锁的控制,本身就依赖于线程调度顺序的,这个顺序是随机的,就不会考虑到这个线程等待多久了。要想实现公平锁,就得在这个基础上能够引入一些额外的东西(引入一个队列,让这些加锁的线程去排队)。

六.可重入锁和不可重入锁

不可重入锁是指,一个线程针对一把锁,连续加锁两次,出现死锁。

可重入锁指的是,一个线程针对一把锁,连续加锁多次都不会死锁。

用synchronized举例子,

1.synchronized既是一个悲观锁,也是一个乐观锁(默认是乐观锁,但是如果发现当前锁竞争比较激烈,就会变成悲观锁)。

2.synchronized既是轻量级锁,也是一个重量级锁(默认是轻量级锁,如果发现当前的锁竞争比较激烈,就会转换成重量级锁)。

3.synchronized 中的轻量级锁,是基于自旋锁的方式实现的。

   synchronized中的重量级锁,是基于挂起等待锁的方式实现的。

4.synchronized 不是读写锁

5.synchronized是非公平锁

6.synchronized是可重入锁

以上这几种锁策略可以视为是“锁的形容词”。

CAS

CAS的全称Compare and swap,字面意思“比较并交换”。CAS做的是什么事情呢?我们假设内存中有原数据V,旧的预期值A和需要修改的新值B,CAS就是比较A与V是否相等(比较),如果比较相等,将B写入V(交换),再返回操作是否成功。用图来展示一下:

 上述交换过程中,大多数并不关心B后续的情况了,更关心的是V这个变量的情况,如果V和A的值不同,则无事发生。

上述操作最特别的地方,这个CAS的过程,并非是通过一段代码实现的,而是通过一条CPU指令完成的,所以CAS操作就是原子的!!可以一定程度上回避线程安全问题。解决线程安全问题除了加锁之外,又有一个新的方向了。CAS可以理解为是一个CPU给我们提供的一个特殊指令,通过这个指令就可以一定程度的处理线程安全问题了。

CAS的应用场景:

1.实现原子类:原子类是Java标准库中提供的类,通过原子类可以解决线程安全问题,伪代码如图:

通过一个例子来证明原子类是线程安全类:

 得到结果如图:

 这里就说明原子类的操作是线程安全的。

2.实现自旋锁:自旋锁的伪代码如图:

CAS的典型问题:ABA问题

CAS在运行中的核心,就是检查value和oldValue是否一致,如果一致,就视为value中途没有被修改过,所以进行下一步交换操作是没有问题的。这里的一致,可能是没改过,也可能是改过但是还原回来了。比如把value的值设置为A的话,CAS判定value为A,此时可能确实value始终是A,也可能是value本来是A,被改成了B,又被还原成了A。

ABA的问题就在于,买手机的时候,可能买到的是新机,也有可能买到的是翻新机。ABA大部分情况下是不会对代码/逻辑产生太大影响的。但是这并不能排除一些极端情况。举个例子:

 当然,上述场景出现的概率是非常低的,一方面,恰好滑稽在这边多按了几次,产生了多个扣款动作;另一方面,赶巧在这个很短的时间内,有人转账了一样的金额。这种问题一旦出现,都是不容易解决的。针对当前这种问题采取的方案就是加入一个版本号。想象成,初始版本号是1,每次修改之后版本号都+1,然后进行CAS的时候就不再以金额为准,而是以版本号为基准,版本号要是不变,那么就一定没有发生改变。

synchronized原理

两个线程,针对同一个对象加锁,就会产生阻塞等待。synchronized内部其实还有一些优化机制,存在的目的就是为了让这个锁更高效,更好用。

1.锁升级/锁膨胀

锁升级的过程:

1)无锁

2)偏向锁

3)轻量级锁

4)重量级锁

比如程序执行到这串代码时

加锁过程就可能会经历前面说的这几个阶段。这其中,偏向锁是一个陌生的名词。进行加锁的时候,首先会进入到偏向锁状态,偏向锁,并不是真正的加锁,而只是占个位置,有需要再真正加锁,没需要就算了。所以synchronized的时候并不是真正的加锁,而是先偏向锁状态,做一个标记(这个过程是非常轻量的)如果整个使用锁的过程中,都没有出现锁竞争,在synchronized执行完之后,取消偏向锁即可。但是,如果使用过程中,另一个线程也尝试加锁,在它加锁之前,迅速的把偏向锁升级为真正的加锁状态!另一个线程就只能阻塞等待了。当synchronized发生锁竞争的时候,就会从偏向锁升级为轻量级锁,此时,synchronized相当于通过自旋的方式来进行加锁的。如果要是很快别人就释放锁了,自旋是划算的,如果迟迟拿不到锁,自旋到一定程度后,就会再次升级为重量级锁(挂起等待锁)。此时如果线程进行了重量级锁的加锁,并且发生锁竞争,此时线程就会被放到阻塞队列中,暂时不参与CPU调度了。直到锁被释放之后,这个线程才有机会被调度到,才有机会获取到锁。

2.锁消除

锁消除指的是,编译器会智能的判定,看当前的代码是否真的需要加锁,如果这个场景不需要加锁,程序猿也加了,就会自动把锁消除。比如之前的StringBuffer类,关键方法都带有synchronized,但是如果在单线程中使用StringBuffer,synchronized加了也白加,此时编译器就会直接把这些加锁操作给消除了。

3.锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗;包含的代码越少,粒度就越细,通常情况下,认为锁的粒度细一点是比较好的。加锁的部分的代码,是不能并发执行的。锁的粒度越细,能并发的代码就越多;反之就越少。

如有错误,欢迎指正。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晚报大街-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值