CAS 与 synchronized 原理

CAS

什么是CAS

CAS: 全称Compare and swap,字面意思:“比较并交换”.

先来看它的伪代码 :

boolean CAS(M, A, B) {
	if (&M == A) {
		&M = B;
		return true;
	}
	return false;
}

寄存器A的值 与 内存M存放的值 进行比较, 如果值相同, 就把寄存器B的值给到内存M.
这段代码是非原子的, 运行过程中随着线程调度可能会产生问题.

注意: CAS操作是一条CPU指令, 并非上述代码, 这一条指令就能完成上述代码功能.(CAS操作是原子的)

CAS 的应用

1. 实现原子类

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.(这里就是通过CAS实现线程安全, 而没有用到锁)

import java.util.concurrent.atomic.AtomicInteger;
public class Test {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1); 
        atomicInteger.getAndIncrement();  
        System.out.println(atomicInteger);  //输出:2
    }
}

来看下它的伪代码 :

class AtomicInteger {  
	private int value;
	public int getAndIncrement() {
		int oldValue = value;
		while ( CAS(value, oldValue, oldValue+1) != true) {
			oldValue = value;
		}
		return oldValue;
	}
}

如果发现 value 的值与 oldValue 的值相同, 则将 oldValue+1 放到 value 中, 相当于++了, 然后返回 true ,循环结束. 反之如果 value 的值与 oldValue 的值不相同, 则放回 false, 进入循环, 将 value 的值重新赋给 oldValue, 再来比较.

这个操作不涉及阻塞等待, 比加锁方案快得多.

注意 : 如果是第二次进入循环判定, 也就是将 value 赋值给 oldValue 后, 再进入CAS比较是否相等时, 这时 value 和 oldValue 的值一定相等.(赋值操作后面就是CAS操作, 都是一条指令 执行非常快)

CAS在这里的作用是什么呢?
其实就是在确定, 看当前的 value 是否变过, 如果没变过 则自增, 否则先更新 再自增.

2. 实现自旋锁

反复检测当前锁状态, 看是否解开了.

自旋锁伪代码:

public class SpinLock {
	private Thread owner = null;  //记录当前锁被那个对象持有, 现在为null 表示没有人持有
	public void lock(){    // 加锁操作
		// 通过 CAS 看当前锁是否被某个线程持有.
		// 如果这个锁已经被别的线程持有, 那么就继续循环.
		// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
		while(!CAS(this.owner, null, Thread.currentThread())){
		}
	}
	public void unlock (){  // 解锁操作
		this.owner = null;
	}
}

优点 : 当这个锁被别的线程持有时, 循环就会一直执行, 一直获取锁状态, 一旦锁释放了 就能立刻获取到锁.
同时这也是它的缺点, 它如果没获取到锁就会一直占着CPU忙等, 消耗资源.

一般来说, 乐观锁发生锁冲突的概率很低, 比较适合实现自旋锁.

CAS 的 ABA 问题 (面试经典问题)

什么是 ABA 问题

CAS 操作关键是比较 内存 和 寄存器 的值是否相同, 如果相同, 则进行赋值操作.
假设内存的值改变过, 只是最后又变回来了, 这时候值确实是相同的, 但可能会出问题.
(比如内存的值由 A 变为 B, 然后又变回 A, 这时候进入CAS)

两个值都一样了, 为什么还会出现问题呢?
大部分的情况下, 是没有影响的. 但是不排除一些特殊情况, 比如 : 我去某鱼上买电脑, 本以为是个新电脑, 结果是别人翻新了的, 外表和新电脑没差别, 但用起来就出问题了.
同样的, 两个值虽然相同, 但它里面的东西可能就不一样了.

解决方法

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当
前版本号比之前读到的版本号大, 就认为操作失败.

synchronized 原理

基本特点

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

关键锁策略 : 锁升级

在这里插入图片描述
偏向锁不是真的 “加锁”, 只是打上一个 “标记”, 记录这个锁属于哪个线程.
如果后续没有其他线程来竞争该锁, 那么就不用进行其他操作了(避免了加锁解锁的开销)
如果后续其他线程来竞争这把锁了, 偏向锁就升级为自旋锁(轻量级锁), 如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会变为重量级锁.

其他锁优化

锁消除

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
在有些场景中, 比如单线程代码加锁, 这显然是没必要的, 这个时候编译器就会判断出当前状态不会引发线程安全问题, 就不会加锁了.

比如 : StringBuilder 与 StringBuffer.
二者相比, StringBuffer 是线程安全的, 它的关键方法都加上了 synchronized 关键字, 如果我们在单线程情况下使用 StringBuffer, 编译器就会自动把锁去掉, 提高代码效率.

锁粗化

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.
锁的粒度有粗有细, 粗就代表加锁的代码块代码多, 细就恰好相反.
当我们在一个线程里频繁加锁解锁时, 编译器就可能会将几个加锁操作和为一个, 就是将锁粗化, 毕竟频繁的加锁解锁很耗费资源.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

随风的浪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值