java多线程CAS的介绍

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

创新互联基于成都重庆香港及美国等地区分布式IDC机房数据中心构建的电信大带宽,联通大带宽,移动大带宽,多线BGP大带宽租用,是为众多客户提供专业遂宁联通机房报价,主机托管价格性价比高,为金融证券行业服务器托管,ai人工智能服务器托管提供bgp线路100M独享,G口带宽及机柜租用的专业成都idc公司。

1 CAS讲解

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

@Slf4j
public 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();
        }
    }

    结果如下:

java多线程CAS的介绍

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

@Slf4j
public 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();
        }
    }
}

   结果如下:

java多线程CAS的介绍

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

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

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

(2)令 j = k + 1。

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

java多线程CAS的介绍

java多线程CAS的介绍

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

java多线程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() = bbb
b: true
reference.getStamp() = 3
reference.getReference() = bbb
c = false
c为什么输出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的介绍这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是创新互联,小编将为大家推送更多相关知识点的文章,欢迎关注!


当前标题:java多线程CAS的介绍
本文路径:http://csdahua.cn/article/jdihip.html
扫二维码与项目经理沟通

我们在微信上24小时期待你的声音

解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流