关于java:面试为了进阿里又把并发CASCompare-and-Swap实现重新精读一遍

38次阅读

共计 3863 个字符,预计需要花费 10 分钟才能阅读完成。

#### 前言

在面试中,并发线程平安发问必然是不会短少的,那根底的 CAS 原理也必须理解,这样在面试中能力加分,那来看看面试可能会问那些问题:

  • 什么是乐观锁与乐观锁
  • 什么乐观锁的实现形式 -CAS(Compare and Swap),CAS(Compare and Swap)实现原理
  • 在 JDK 并发包中的应用
  • CAS 的缺点

1. 什么是乐观锁与乐观锁?

乐观锁

总是假如最坏的状况,每次读取数据的时候都默认其余线程会更改数据,因而须要进行加锁操作,当其余线程想要拜访数据时,都须要阻塞挂起。乐观锁的实现:

  • 传统的关系型数据库应用这种锁机制,比方行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁;
  • Java 外面的同步 synchronized 关键字的实现。

乐观锁

乐观锁,其实就是一种思维,总是认为不会产生并发问题,每次读取数据的时候都认为其余线程不会批改数据,所以不上锁,然而在更新的时候会判断一下在此期间别的线程有没有批改过数据,乐观锁实用于读操作多的场景,这样能够进步程序的吞吐量。实现形式:

  • CAS 实现:Java 中 java.util.concurrent.atomic 包上面的原子变量应用了乐观锁的一种 CAS 实现形式,CAS 剖析看下节。
  • 版本号管制:个别是在数据表中加上一个数据版本号 version 字段,示意数据被批改的次数,当数据被批改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若方才读取到的 version 值为以后数据库中的 version 值相等时才更新,否则重试更新操作,直到更新胜利

乐观锁实用于读多写少的状况下(多读场景),乐观锁比拟实用于写多读少场景


2. 乐观锁的实现形式 -CAS(Compare and Swap),CAS(Compare and Swap)实现原理

背景

在 jdk1.5 之前都是应用 synchronized 关键字保障同步,synchronized保障了无论哪个线程持有共享变量的锁,都会采纳独占的形式来拜访这些变量, 导致会存在这些问题:

  • 在多线程竞争下,加锁、开释锁会导致较多的上下文切换和调度延时,引起性能问题
  • 如果一个线程持有锁,其余的线程就都会挂起,期待持有锁的线程开释锁。
  • 如果一个优先级高的线程期待一个优先级低的线程开释锁,会导致优先级倒置,引起性能危险

为了优化乐观锁这些问题,就呈现了乐观锁:

假如没有并发抵触,每次不加锁操作同一变量,如果有并发抵触导致失败,则重试直至胜利。

CAS(Compare and Swap)原理

CAS 全称是 compare and swap(比拟并且替换),是一种用于在多线程环境下实现同步性能的机制,其也是无锁优化,或者叫自旋,还有自适应自旋。

在 jdk 中,CASvolatile 关键字作为实现并发包的基石。没有 CAS 就不会有并发包,java.util.concurrent 中借助了 CAS 指令实现了一种区别于 synchronized 的一种乐观锁。

乐观锁的一种典型实现机制(CAS):

乐观锁次要就是两个步骤:

  • 冲突检测
  • 数据更新

当多个线程尝试应用 CAS 同时更新同一个变量时,只有一个线程能够更新变量的值,其余的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并能够再次尝试。

在不应用锁的状况下保障线程平安,CAS 实现机制中有重要的三个操作数:

  • 须要读写的内存地位(V)
  • 预期原值(A)
  • 新值(B)

首先先读取须要读写的内存地位 (V),而后比拟须要读写的内存地位(V) 和预期原值(A),如果内存地位与预期原值的 A 相匹配,那么将内存地位的值更新为新值 B。如果内存地位与预期原值的值不匹配,那么处理器不会做任何操作。无论哪种状况,它都会在 CAS 指令之前返回该地位的值。具体能够分成三个步骤:

  • 读取(须要读写的内存地位(V))
  • 比拟(须要读写的内存地位 (V) 和预期原值(A))
  • 写回(新值(B))

3. CAS 在 JDK 并发包中的应用

在 JDK1.5 以上 java.util.concurrent(JUC java 并发工具包)是基于 CAS 算法实现的,相比于 synchronized 独占锁,梗塞算法,CAS 是非梗塞算法的一种常见实现,应用乐观锁 JUC 在性能上有了很大的晋升。

CAS 如何在不应用锁的状况下保障线程平安,看并发包中的原子操作类 AtomicInteger::getAndIncrement()办法(相当于 i ++ 的操作):

// AtomicInteger 中
//value 的偏移量
private static final long valueOffset; 
// 获取值
private volatile int value;
// 设置 value 的偏移量
static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {throw new Error(ex); }
    }
// 减少 1
public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
    }
  • 首先 value 必须应用了 volatile 润饰,这就保障了他的可见性与有序性
  • 须要初始化 value 的偏移量
  • unsafe.getAndAddInt 通过偏移量进行 CAS 操作,每次从内存中读取数据而后将数据进行 + 1 操作,而后对原数据,+ 1 后的后果进行 CAS 操作,胜利的话返回后果,否则重试直到胜利为止。

    //unsafe 中
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 应用偏移量获取内存中 value 值
            var5 = this.getIntVolatile(var1, var2);
           // 比拟并 value 加 +1
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

JAVA 实现 CAS 的原理,unsafe::compareAndSwapInt 是借助 C 来调用 CPU 底层指令实现的。上面是 sun.misc.Unsafe::compareAndSwapInt()办法的源代码:

public final native boolean compareAndSwapInt(Object o, long offset,
                                               int expected, int x);

4. CAS 的缺点

ABA 问题

在多线程场景下 CAS 会呈现 ABA 问题,例如有 2 个线程同时对同一个值 (初始值为 A) 进行 CAS 操作,这三个线程如下

线程 1,期望值为 A,欲更新的值为 B

线程 2,期望值为 A,欲更新的值为 B

线程 3,期望值为 B,欲更新的值为 A

  • 线程 1 领先取得 CPU 工夫片,而线程 2 因为其余起因阻塞了,线程 1 取值与冀望的 A 值比拟,发现相等而后将值更新为 B,
  • 这个时候呈现了线程 3,线程 3 取值与冀望的值 B 比拟,发现相等则将值更新为 A
  • 此时线程 2 从阻塞中复原,并且取得了 CPU 工夫片,这时候线程 2 取值与冀望的值 A 比拟,发现相等则将值更新为 B,尽管线程 2 也实现了操作,然而线程 2 并不知道值曾经通过了 A->B->A 的变动过程。

ABA 问题带来的危害:
小明在提款机,提取了 50 元,因为提款机问题,有两个线程,同时把余额从 100 变为 50
线程 1(提款机):获取以后值 100,冀望更新为 50,
线程 2(提款机):获取以后值 100,冀望更新为 50,
线程 1 胜利执行,线程 2 某种原因 block 了,这时,某人给小明汇款 50
线程 3(默认):获取以后值 50,冀望更新为 100,
这时候线程 3 胜利执行,余额变为 100,
线程 2 从 Block 中复原,获取到的也是 100,compare 之后,持续更新余额为 50!!!
此时能够看到,理论余额应该为 100(100-50+50),然而实际上变为了 50(100-50+50-50)这就是 ABA 问题带来的胜利提交。

解决办法

  • AtomicStampedReference:带有工夫戳的对象援用来解决 ABA 问题。这个类的 compareAndSet 办法作用是首先查看以后援用是否等于预期援用,并且以后标记是否等于预期标记,如果全副相等,则以原子形式将该援用和该标记的值设置为给定的更新值。
public boolean compareAndSet(
               V      expectedReference,// 预期援用
               V      newReference,// 更新后的援用
              int    expectedStamp, // 预期标记
              int    newStamp // 更新后的标记

)
  • version:在变量后面加上版本号,每次变量更新的时候变量的版本号都 +1,即 A ->B->A 就变成了 1A->2B->3A

循环工夫长开销大

自旋 CAS(不胜利,就始终循环执行,直到胜利)如果长时间不胜利,会给 CPU 带来极大的执行开销。

解决办法:

  • 限度自旋次数,避免进入死循环
  • JVM 能反对处理器提供的 pause 指令那么效率会有肯定的晋升,

只能保障一个共享变量的原子操作

当对一个共享变量执行操作时,咱们能够应用循环 CAS 的形式来保障原子操作,然而对多个共享变量操作时,循环 CAS 就无奈保障操作的原子性

解决办法:

  • 如果须要对多个共享变量进行操作,能够应用加锁形式 (乐观锁) 保障原子性,
  • 能够把多个共享变量合并成一个共享变量进行 CAS 操作。

    各位看官还能够吗?喜爱的话,动动手指导个????,点个关注呗!!谢谢反对!

欢送扫码关注,原创技术文章第一工夫推出

正文完
 0