多线程与高并发(6)——CAS详解(包含ABA问题)

一、乐观锁和悲观锁

乐观锁和悲观锁都是用于解决并发场景下的数据竞争问题,不局限于某种编程语言或数据库。

1、乐观锁:

就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据
乐观锁的实现方式:
主要有两种,一种是CAS机制,一种是版本号机制。
(1)版本号机制:在数据中增加一个version字段用来表示该数据的版本号,每当数据被修改版本号就会加1。当某个线程查询数据的时候,会将该数据的版本号一起读取出来,之后在该线程需要更新该数据的时候,就将之前读取的版本号与当前版本号进行比较,如果一致,则执行操作,如果不一致,则放弃操作。
(2)CAS: 我们下面详解。

2、悲观锁

在操作数据的时候比较悲观,悲观地认为别人一定会同时修改数据,因此悲观锁在操作数据时是直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能操作数据。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。 相反,如果经常发生冲突,上层应用会不断进行 retry,这样反而降低了性能,所以这种情况下用悲观锁比较合适。
读取频繁使用乐观锁,写入频繁使用悲观锁。

二、什么是CAS

CAS,compare and swap,比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值或者叫期望值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
多线程与高并发(6)——CAS详解(包含ABA问题)_第1张图片
真实的CAS操作是由CPU完成的,CPU会确保这个操作的原子性。CAS是CPU指令级别的操作,中间不能被打断,是靠CPU原语实现的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。 调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS操作避免了多线程的竞争锁,上下文切换和进程调度。

三、CAS原理

CAS是靠CPU原语实现原子性的,那具体是什么呢?CPU提供了下面两种方式:
总线锁定和缓存锁定。
总线锁定: 指CPU使用了总线锁,所谓总线锁就是使用CPU提供的LOCK#信号,当CPU在总线上输出LOCK#信号时,其他CPU的总线请求将被阻塞。
缓存锁定: 总线锁定方式虽然保证了原子性,但是在锁定期间,会导致大量阻塞,增加系统的性能开销,所以现代CPU通过锁定范围缩小的思想设计出了缓存行锁定(缓存行是CPU高速缓存存储的最小单位)。
所谓缓存锁定是指CPU对缓存行进行锁定,当缓存行中的共享变量回写到内存时,其他CPU会通过总线嗅探机制感知该共享变量是否发生变化,如果发生变化,让自己对应的共享变量缓存行失效,重新从内存读取最新的数据,缓存锁定是基于缓存一致性机制来实现的,因为缓存一致性机制会阻止两个以上CPU同时修改同一个共享变量(现代CPU基本都支持和使用缓存锁定机制)。

四、java中的CAS

CAS在Java中的应用,即并发包中的原子操作类(Atomic系列),jdk提供了java.util.concurrent.atomic包,在该包中提供了许多基于CAS实现的原子操作类,如图所示:
多线程与高并发(6)——CAS详解(包含ABA问题)_第2张图片
而atomic包底层是调用了unsafe类,Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法,都直接调用操作系统底层资源执行相应任务。如下所示:
多线程与高并发(6)——CAS详解(包含ABA问题)_第3张图片

五、CAS缺陷

1、自旋时间长

当一个线程获取锁时失败,不进行阻塞挂起,而是间隔一段时间再次尝试获取,直到成功为止,这种循环获取的机制被称为自旋锁(spinlock)。
自旋锁好处是,持有锁的线程在短时间内释放锁,那些等待竞争锁的线程就不需进入阻塞状态(无需线程上下文切换/无需用户态与内核态切换),它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户态和内核态的切换消耗。
自旋锁坏处显而易见,线程在长时间内持有锁,等待竞争锁的线程一直自旋,即CPU一直空转,资源浪费在毫无意义的地方,所以一般会限制自旋次数。
实现自旋锁可以基于CAS实现。锁升级过程中,最开始也是自旋锁。
自旋时间长,开销大,如下所示:
多线程与高并发(6)——CAS详解(包含ABA问题)_第4张图片
getAndAddInt方法执行时,do while会一直尝试。所以自旋要限制次数。

2、只能保证一个共享变量原子操作

CAS只能针对一个共享变量使用,如果多个共享变量就只能使用锁了,如果可以把多个变量整成一个变量,就可以利用CAS,例如读写锁中state的高低位。

3、ABA问题

什么是ABA问题呢?
如果一个值原来是A,变成了B,然后又变成了A,在compare 的时候会发现没有被修改。这就是ABA问题。
当然如果是基本数据类型,肯定没有问题,但是如果是对象呢?
如果有两个线程t1和t2,一个对象A,而对象A中有各种属性甚至是引用了其他对象。t1先引用了A,然后把A中的属性或者其他引用给更改了,而t2再去引用A的时候,只是对比内存值,也就是A的引用地址,会发现没有任何改变。就造成了ABA问题。
解决ABA问题方法很简单,加上版本号就行,比如1A2B3A,看它有没有改变。
atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
多线程与高并发(6)——CAS详解(包含ABA问题)_第5张图片

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