CAS机制的讲解以及实际项目中的使用
2023-12-23 23:04:47
首先要明白cas解决的问题,它是乐观锁的一种解决方式,都是多线程并发情况下解决数据线程按全问题的一种手段-----无锁并发
为什么无锁效率高?
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歌,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
- 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
- 但无锁情况下,因为线程要保持运行,需要额外CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
CAS的特点
结合CAS和volatile 可以实现无锁并发,适用于线程数少、多核CPU的场景下。(cas要配合volatile使用,因为多线程操作下,为了保证线程修改完数据后,其他线程立刻可见)
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized是基于悲观锁的思想: 最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
- CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
- 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
下边列举几个java中使用cas实现的几个原子类
- 首先是几个基本类型的原子操作类AtomicInteger,AtomicBoolean等,它们实现线程安全的方式就是通过一个compareAndSet方法,第一个参数就是当前线程操作时首先获取的值,第二个是修改以后的值,它会通过当前线程获取的值在准备修改前去和当前存储的值做对比,如果在它一开始拿到值以后,有别的线程修改了它,那么在修改前比对一开始获取的值和现在的值就会发现不同,那么这个方法就不会去修改,然后返回false,表示修改失败,如果我们想实现修改,就可以给这个方法外套一层循环,除非修改成功,跳出循环,否则一直循环尝试修改,这几个原子类中好多的方法都是通过这样的方式实现的修改功能,例如下边的getAndSet
public final boolean getAndSet(boolean newValue) { boolean prev; do { prev = get(); } while (!compareAndSet(prev, newValue)); return prev; }
- 然后是引用类型操作类AtomicReference,比如String,Double各种时间类型等表示单个值的引用类型都可以使用它实现无锁并发且线程安全,以下以String类型为例子实现引用类型的多线程线程安全
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; public class AtomicTest { static AtomicReference<String> atomicReference=null; public AtomicTest(String list){ this.atomicReference=new AtomicReference<>(list); } public static void main(String[] args) throws InterruptedException { AtomicTest atomicTest = new AtomicTest(""); CountDownLatch countDownLatch = new CountDownLatch(100); ExecutorService executorService = Executors.newFixedThreadPool(200); for (int i = 1; i <=100 ; i++) { executorService.submit(()->{ try{ atomicTest.update(String.valueOf(1)); }finally { countDownLatch.countDown(); } }); } countDownLatch.await(); executorService.shutdown(); System.out.println(atomicReference.toString()); System.out.println(atomicReference.get().length()); } public void update(String H){ AtomicTest.atomicReference.updateAndGet(g->{ return g+H; }); } }
注意它们里边只有使用了compareAndSet才能保证操作的原子性和线程安全
-
AtomicStampedReference--上边的AtomicReference存在一个问题,比如现在有一个String类型,当前线程首先获取到了a,但是现在有其他线程先把a改成了b,又把b改成了a,如果我们要求,只要其他线程修改过这个值,我们就要修改失败,那么AtomicReference是没办法实现的,在AtomicStampedReference并没有帮我们实现类似于updateAndGet这样的方法,需要我们自己通过compareAndSet实现,见如下代码
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicTest { static AtomicStampedReference<String> atomicReference=null; public AtomicTest(String list){ this.atomicReference=new AtomicStampedReference<>(list,0); } public static void main(String[] args) throws InterruptedException { AtomicTest atomicTest = new AtomicTest(""); CountDownLatch countDownLatch = new CountDownLatch(100); ExecutorService executorService = Executors.newFixedThreadPool(200); for (int i = 1; i <=100 ; i++) { executorService.submit(()->{ try{ atomicTest.update(String.valueOf(1)); }finally { countDownLatch.countDown(); } }); } countDownLatch.await(); executorService.shutdown(); System.out.println(atomicReference.getReference()); System.out.println(atomicReference.getReference().length()); } public void update(String H){ while (true){ //获取最新值 String reference = atomicReference.getReference(); //获取版本号 int stamp = atomicReference.getStamp(); if (atomicReference.compareAndSet(reference,reference+H,stamp,stamp+1)){ break; } } } }
- ?AtomicMarkableReference--上边的在解决AtomicReference存在的问题是通过版本号实现的,如果我们只是想实现,别的线程修改过,则无法修改,可以通过一个boolean即可实现,那么AtomicMarkableReference就是实现了这样功能,我在这里就不一一介绍了,大家自己研究一下
文章来源:https://blog.csdn.net/weixin_59244784/article/details/135173620
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!