千家信息网

java多线程CAS的介绍

发表于:2025-01-24 作者:千家信息网编辑
千家信息网最后更新 2025年01月24日,这篇文章主要讲解了"java多线程CAS的介绍",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"java多线程CAS的介绍"吧!1 CAS讲解在工作中,我
千家信息网最后更新 2025年01月24日java多线程CAS的介绍

这篇文章主要讲解了"java多线程CAS的介绍",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"java多线程CAS的介绍"吧!

1 CAS讲解

在工作中,我们往往需要面对多线程计数的问题,我们第一反应,使用"synchronized",控制并发。

@Slf4jpublic class CASDemo extends Thread{    private static  int  t = 0;    @Override    public void run() {        increment();        log.info("-----------{}------------",t);    }    private synchronized  static  void increment() {        t++;    }    public static void main(String[] args){        for (int i = 0 ; i < 100 ; i++){            CASDemo casDemo = new CASDemo();            casDemo.start();        }    }

结果如下:

从上面结果来看符合我们的预期,100个线程执行了1次,最后t为100。但是这是最好的实现机制吗?我们知道"synchronized"是同步锁,即使jdk6以后已经对其进行了优化(具体可以见另一篇文章:java多线程基础知识之synchronized原理分析),只是用来计数,是否太大材小用了。有没有一种更加优雅的解决方案?

@Slf4jpublic class CASDemo extends Thread{    private static AtomicInteger t = new AtomicInteger(0);    @Override    public void run() {        log.info("-----------{}------------",t.incrementAndGet());    }    public static void main(String[] args){        for (int i = 0 ; i < 100 ; i++){            CASDemo casDemo = new CASDemo();            casDemo.start();        }    }}

结果如下:

从结果上来看,第一种和第二种结果是一样的。为啥推荐第一种方法呢?因为第二种使用了无锁式的"compareAndSwap"即"CAS",既然"CAS"是无锁的,那么是怎么样保证其实线程安全的。

"compareAndSwap"从字面上看,这一过程肯定包含了比较和替换两个动作,具体步骤如下:

(1)线程从内存中读取 i 的值,假如此时 i 的值为 0,我们把这个值称为 k 吧,即此时 k = 0。

(2)令 j = k + 1。

(3)用 k 的值与内存中i的值相比,如果相等,这意味着没有其他线程修改过 i 的值,我们就把 j(此时为1) 的值写入内存;如果不相等(意味着i的值被其他线程修改过),我们就不把j的值写入内存,而是重新跳回步骤 1,继续这三个操作。具体源码如下:

同时,整个"CAS"是原子的,对应操作系统的一条硬件操作指令,尽管看似有很多操作在里面,但操作系统能够保证它是原子执行的。

通过上面的流程讲解,我们可以发现其不可能从内存中同时取到相同的K值,并且分别+1然后提交到内存中,从而保证的线程安全。

但是我们仍然面临一个问题:谁偷偷更改了我的值。

举个例子,当线程A即将要执行第三步的时候,线程 B 把 i 的值加1,之后又马上把 i 的值减 1,然后,线程 A 执行第三步,这个时候线程 A 是认为并没有人修改过 i 的值,因为 i 的值并没有发生改变。而这,就是我们平常说的ABA问题。对于基本类型的值来说,这种把数字改变了在改回原来的值是没有太大影响的,但如果是对于引用类型的话,就会产生很大的影响了。

怎么解决这个问题呢?--版本控制(参考乐观锁)。

例如,每次有线程修改了引用的值,就会进行版本的更新,虽然两个线程持有相同的引用,但他们的版本不同,这样,我们就可以预防 ABA 问题了。Java 中提供了 AtomicStampedReference 这个类,就可以进行版本控制了。

给个demo.

//构造方法, 传入引用和戳public AtomicStampedReference(V initialRef, int initialStamp)//返回引用public V getReference()//返回版本戳public int getStamp()//如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存public boolean compareAndSet(V   expectedReference,                                 V   newReference,                                 int expectedStamp,                                 int newStamp)//如果当前引用 等于 预期引用, 将更新新的版本戳到内存public boolean attemptStamp(V expectedReference, int newStamp)//设置当前引用的新引用和版本戳public void set(V newReference, int newStamp)


public static void main(String[] args) {        String str1 = "aaa";        String str2 = "bbb";        AtomicStampedReference reference = new AtomicStampedReference(str1,1);        reference.compareAndSet(str1,str2,reference.getStamp(),reference.getStamp()+1);        System.out.println("reference.getReference() = " + reference.getReference());        boolean b = reference.attemptStamp(str2, reference.getStamp() + 1);        System.out.println("b: "+b);        System.out.println("reference.getStamp() = "+reference.getStamp());        boolean c = reference.weakCompareAndSet(str2,"ccc",4, reference.getStamp()+1);        System.out.println("reference.getReference() = "+reference.getReference());        System.out.println("c = " + c);    }输出:reference.getReference() = bbbb: truereference.getStamp() = 3reference.getReference() = bbbc = falsec为什么输出false呢, 因为版本戳不一致啦

2jdk8对CAS的优化

由于采用这种 CAS 机制是没有对方法进行加锁的,所以,所有的线程都可以进入 increment() 这个方法,假如进入这个方法的线程太多,就会出现一个问题:每次有线程要执行第三个步骤的时候,i 的值老是被修改了,所以线程又到回到第一步继续重头再来。

而这就会导致一个问题:由于线程太密集了,太多人想要修改 i 的值了,进而大部分人都会修改不成功,白白着在那里循环消耗资源。

为了解决这个问题,Java8 引入了一个 cell[] 数组,它的工作机制是这样的:假如有 5 个线程要对 i 进行自增操作,由于 5 个线程的话,不是很多,起冲突的几率较小,那就让他们按照以往正常的那样,采用 CAS 来自增吧。但是,如果有 100 个线程要对 i 进行自增操作的话,这个时候,冲突就会大大增加,系统就会把这些线程分配到不同的 cell 数组元素去,假如 cell[10] 有 10 个元素吧,且元素的初始化值为 0,那么系统就会把 100 个线程分成 10 组,每一组对 cell 数组其中的一个元素做自增操作,这样到最后,cell 数组 10 个元素的值都为 10,系统在把这 10 个元素的值进行汇总,进而得到 100,二这,就等价于 100 个线程对 i 进行了 100 次自增操作。

总之,jdk8对于高并发的情况下,采用了类似减少锁粒度方法来提高性能。



感谢各位的阅读,以上就是"java多线程CAS的介绍"的内容了,经过本文的学习后,相信大家对java多线程CAS的介绍这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

0