#### 前言

在面试中,并发线程平安发问必然是不会短少的,那根底的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); }    }//减少1public 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操作。

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

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