CAS 与 synchronized 原理

简介: 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. 不是读写锁.


关键锁策略 : 锁升级


a84e162f294749faa3bcd28b71c8c621.png


偏向锁不是真的 “加锁”, 只是打上一个 “标记”, 记录这个锁属于哪个线程.

如果后续没有其他线程来竞争该锁, 那么就不用进行其他操作了(避免了加锁解锁的开销)

如果后续其他线程来竞争这把锁了, 偏向锁就升级为自旋锁(轻量级锁), 如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会变为重量级锁.


其他锁优化


锁消除


编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.

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


比如 : StringBuilder 与 StringBuffer.

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


锁粗化


一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.

锁的粒度有粗有细, 粗就代表加锁的代码块代码多, 细就恰好相反.

当我们在一个线程里频繁加锁解锁时, 编译器就可能会将几个加锁操作和为一个, 就是将锁粗化, 毕竟频繁的加锁解锁很耗费资源.


相关文章
|
6天前
|
Java 编译器
synchronized原理
synchronized原理
|
6天前
|
应用服务中间件 Linux 调度
锁和原子操作CAS的底层实现
锁和原子操作CAS的底层实现
21 0
|
6天前
|
存储 安全 Java
Synchronized锁工作原理
Synchronized锁工作原理
|
9月前
|
Java 编译器 Linux
【多线程】锁策略、CAS、Synchronized
锁策略, cas 和 synchronized 优化过程
|
6天前
|
存储 安全 中间件
锁与原子操作CAS的底层实现
锁与原子操作CAS的底层实现
|
11月前
|
安全 Java 编译器
【JavaEE】多线程进阶问题-锁策略and死锁,CAS操作,Synchronized原理
JavaEE & 多线程进阶问题 & 锁策略and 死锁,CAS操作,Synchronized原理
44 0
|
Java
synchronized原理剖析
synchronized原理剖析
85 0
|
Java C++
Java并发 --- CAS解析(对比synchronized )
Java并发 --- CAS解析(对比synchronized )
|
安全 Java 对象存储
浅谈synchronized锁原理
保证线程安全的一个重要手段就是通过加锁的形式实现,今天盘点一下Java中锁的八股文
132 0
|
缓存 SpringCloudAlibaba 前端开发
JUC系列(九) CAS 与锁的理解
CAS 解决ABA问题 对于juc下的几个做类型的理解
JUC系列(九)  CAS 与锁的理解