CAS、Synchronized 原理

    • 什么是CAS
    • CAS应用
      • 原子类
      • 自旋锁
      • CAS的ABA问题
    • Synchronized 原理
      • 锁升级优化
      • 锁消除优化
      • 锁粗化优化

什么是CAS

什么是CAS?Compare and swap :比较和交换

一个CAS操作涉及:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。
    CAS、Synchronized 原理_第1张图片
    上述过程:如果V和A不同;啥问题也没有;不操作;CAS的特殊之处;把上述操作通过一个CPU指令完成;效率非常高;这个操作原子性的。可以认为CPU提供的特殊指令;回避线程安全问题。
    可以理解:比较是一个原子性操作;交换也是一个原子性操作。如果比较相等那么比较和交换的整体也是一个原子性操作。
    当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可以认为是乐观锁的一种实现。

CAS应用

原子类

CAS实现原子类;例如:Java标准库提供的原子类AtomicInteger
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.getAndIncrement();//相当于++操作;多线程在++操作就是安全的

假设在多线程进行这个++操作:伪代码
CAS、Synchronized 原理_第2张图片
执行过程:
1:先把内容读取到自己的栈内存上(cpu寄存器)
CAS、Synchronized 原理_第3张图片

2:假设线程1先执行CAS操作.由于oldValue和value的值相同,直接进行对value赋值。CAS是直接读写内存的,而不是操作寄存器;CAS的读内存,比较,写内存操作是一条硬件指令,是原子性。

3::写完后的结果如下
CAS、Synchronized 原理_第4张图片
4:线程2再执行CAS操作,第一次CAS的时候发现写入失败;因为oldValue和value不相等。因此需要进入循环;在循环里重新读取value的值赋给oldValue。重复上述流程
上述使用一个原子类.不需要使用重量级锁,就可以高效的完成多线程的自增操作.

自旋锁

伪代码:
通过 CAS 操作检查当前锁是否被其他线程持有。如果锁已经被持有,CAS 操作会失败,进入自旋等待状态。
如果锁没有被其他线程持有,CAS 操作成功,将当前线程设为锁的持有者(owner)。
这是一个典型的自旋锁实现,会一直循环检测直到成功获取锁。

public class SpinLock {
    private Thread owner = null;

    public void lock() {
        // 通过CAS(Compare and Swap)检查当前锁是否被某个线程持有。
        // 如果这个锁已经被其他线程持有,则自旋等待。
        // 如果这个锁没有被其他线程持有,则将owner设为当前尝试加锁的线程。
        while (!CAS(this.owner, null, Thread.currentThread())) {
            // 自旋等待,直到成功获取锁
        }
    }

    public void unlock() {
        // 释放锁,将owner设为null
        this.owner = null;
    }
}

CAS的ABA问题

CAS 在运行中的核心,检查 value 和 oldvalue 是否一致如果一致;就视为 vaue 中途没有被修改过,所以进行下一步交换操作是没问题的。
上述的一致有两种可能:这里 一致,可能是没改过。也可能是,改过,但是还原回来了(ABA问题)。

ABA问题是特殊情况;但是仍然是致命的;假设有CAS方式扣款;判断这个钱扣了没;你银行卡有1000块钱要取500。
现在两个线程读到你余额是1000情况;然后你取的时候有人给你转了500。这会就会线程1这个扣完;然后线程3又充500;线程2接着扣。

怎么解决呢?
想象成,初始版本号是 1,每次修改版本号都 + 1然后进行 CAS 的时候不是以金额为基准了,而是以版本号为基准。版本号只能升;不能降;只要版本号没变就一定没发生改变;这个问题得我们自己去避免。基于版本号实现的乐观锁是一种典型的实现方式。

Synchronized 原理

jdk1.8下

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁
    JVM 将 synchronized 锁分为无锁、偏向锁、轻量级锁、重量级锁状态。会根据情况,进行依次升级。有很多内部优化机制让这个锁更高效和好用。

锁升级优化

synchronized (locker) { }过程:
1:无锁状态;初始阶段,对象的标记为无锁状态。当第一个线程进入synchronized代码块时,它会尝试获取锁,此时这个对象的标记变为偏向锁。
2:偏向锁;进行加锁的时候,首先先会进入到偏向锁状态;偏向锁,并不是真正的加锁,而只是占个位置;有需要再真加锁,如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)。跟做个标记一样;然后清空标记。
3:轻量级锁状态: 当发生锁竞争,偏向锁会升级为轻量级锁。synchronized相当于自旋的方式加锁(跟上述CAS伪代码逻辑一样),并尝试使用CAS来争夺锁。
4:重量级锁状态: 如果要是很快别人就释放锁了,自旋是划算的,但是如果迟迟拿不到锁,一直自旋,并不划算.synchronized 自旋不是无休止的自旋,自旋到一定程度之后,就会再次升级成 重量级锁(挂起等待锁;)
可以想象是有个计数器记录循环多少次;循环多久;然后到一定程度结束循环执行重量级锁的逻辑;让其先放弃cpu;当前线程一旦被切换cpu就是比较低效的事情;因为即使对方释放锁了;你这边也得要等待到你调度的时候;谁能保证猴年马月。

注意:锁只能这样子升级;不会降级;重入的情况呢?synchronized对重入的情况是特殊处理;不加锁;只是计数。

锁消除优化

编译器智能的判定,看当前的代码是否是真的要加锁,如果这个场景不需要加锁,程序猿也加了,就自动把锁给去掉。
比如:StringBuffer sb = new StringBuffer();是线程安全的;底层是synchronized保证的。但是单线程你并不会用到线程安全问题;所以就是我们使用StringBuffer和StringBudder通常混用不区分。
sb.append(“a”);
sb.append(“b”);
sb.append(“c”);
sb.append(“d”);

锁粗化优化

锁的粒度: synchronized 包含的代码越多,粒度就越粗包含的代码越少,粒度就越细。有时候并不是锁的粒度越细越好。
一般情况下认为是锁的粒度细一点好;这样子并发的块就比较多;但是有些两次加锁解锁之间,间隙非常小此时莫不如就直接一次大锁好;因为反复的加锁和解锁会消耗更多资源。(间隙特别小;就算并发也作用不大;并发减少的时间;不如加锁的开销大。)

你可能感兴趣的:(多线程,java,开发语言)