Java并发编程之CAS原理分析

Java并发编程之CAS原理分析

在并发编程中,我们经常需要处理多线程对共享资源的访问和修改。那么如何解决并发安全呢?


一. 解决并发安全问题的方案

最粗暴的方式就是使用 synchronized 关键字了,但它是一种独占形式的锁,属于悲观锁机制,性能会大打折扣。olatile 貌似也是一个不错的选择,但 volatile 只能保持变量的可见性,并不保证变量的原子性操作。

相比之下,CAS(Compare And Swap)是一种乐观锁机制。它不需要加锁,是一种无锁的原子操作。

二. 悲观锁和乐观锁
1. 悲观锁
对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

2. 乐观锁
对于乐观锁来说,它总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。

由于乐观锁假想操作中没有锁的存在,因此不太可能出现死锁的情况,换句话说,乐观锁天生免疫死锁。

3. 使用场景

乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;
悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

三、什么是CAS
CAS 是什么,它的英文全称是 Compare-And-Swap,中文叫做“比较并交换”,它是一种思想、一种算法。

CAS算法有3个基本操作数:

内存地址V
旧的预期值A
要修改的新值B
在并发场景下,各个代码的执行顺序不能确定,为了保证并发安全,我们可以使用普通的互斥锁,比如Java的 synchronized, ReentrantLock等。而CAS的特点是避免使用互斥锁,当多个线程并发使用CAS更新同一个变量时,只有一个可以操作成功,其他都会失败。而且用CAS更新失败的线程并不会阻塞,会快速失败并返回一个失败的状态,允许你再次尝试。


   四、CAS的原理
   CAS 的思想很简单:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false。

   CAS操作是原子的,即在同一时刻只有一个线程能够成功执行CAS操作。

 


  CAS的基本步骤如下:

  读取共享变量的当前值。
比较当前值与期望值。
如果相等,则使用新的值替换当前值。
如果比较失败,则说明其他线程已经修改了共享变量,需要重新读取当前值并重试操作。
CAS操作是一种无锁的操作,避免了传统锁机制中的竞争和阻塞,从而提高了并发性能。

java.util.concurrent 包很多功能都是建立在 CAS 之上,如 ReenterLock(可重入锁) 内部的 AQS,各种原子类,其底层都用 CAS来实现原子操作。

 

五、CAS的问题

CAS和锁都解决了原子性问题,和锁相比没有阻塞、线程上下文你切换、死锁,所以CAS要比锁拥有更优越的性能,但是CAS同样存在缺点。

1. 只能保证一个共享变量原子操作

CAS只能针对一个共享变量使用,如果多个共享变量就只能使用锁了,当然如果有办法把多个变量整成一个变量,利用CAS也不错,例如读写锁中state的高低位。

2. 自旋时间太长
当一个线程获取锁时失败,不进行阻塞挂起,而是间隔一段时间再次尝试获取,直到成功为止,这种循环获取的机制被称为自旋锁(spinlock)。
自旋锁的优点:
持有锁的线程在短时间内释放锁,那些等待竞争锁的线程就不需进入阻塞状态(无需线程上下文切换/无需用户态与内核态切换),它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户态和内核态的切换消耗。
自旋锁的缺点:
线程在长时间内持有锁,等待竞争锁的线程一直自旋,即CPU一直空转,资源浪费在毫无意义的地方,所以一般会限制自旋次数。
最后来说自旋锁的实现,实现自旋锁可以基于CAS实现,先定义lockValue对象默认值1,1代表锁资源空闲,0代表锁资源被占用,代码如下:

 1 public class SpinLock {
 2     
 3     //lockValue 默认值1
 4     private AtomicInteger lockValue = new AtomicInteger(1);
 5     
 6     //自旋获取锁
 7     public void lock(){
 8 
 9         // 循环检测尝试获取锁
10         while (!tryLock()){
11             // 空转
12         }
13 
14     }
15     
16     //获取锁
17     public boolean tryLock(){
18         // 期望值1,更新值0,更新成功返回true,更新失败返回false
19         return lockValue.compareAndSet(1,0);
20     }
21     
22     //释放锁
23     public void unLock(){
24         if(!lockValue.compareAndSet(1,0)){
25             throw new RuntimeException("释放锁失败");
26         }
27     }
28 
29 }

 

参考链接:https://segmentfault.com/a/1190000040042588

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章