Java的原子类无锁并发利器详解

 更新时间:2023年12月22日 09:34:21   作者:Java面试365  
这篇文章主要介绍了Java的原子类无锁并发利器详解,原子类同样能够解决互斥性问题、原子性问题除此之外,因为原子类是无锁操作,没有用互斥锁解决带来的加锁解决性能消耗,这种绝佳方案是怎么做到的呢,需要的朋友可以参考下

前言引入

当存在如下场景,两个线程同时去将count值累加一万次,那么如下代码是否存在线程安全问题呢?

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        TestCount testCount = new TestCount();
        Thread T1 = new Thread(()->{
            testCount.add10k();
        });
        Thread T2 = new Thread(()->{
            testCount.add10k();
        });
        T1.start();
        T2.start();
        T1.join();
        T2.join();
        System.out.println(testCount.count);
    }
}
class TestCount{
    long count = 0;
    void add10k(){
        int idx = 0;
        while (idx++ < 10000){
            count += 1;
        }
    }
}

显然是存在的,最终的结果显然是小于两万的,原因是count值存在可见性问题,count+=1也存在原子性的问题。

一般思路都是将count采用volatile修饰,count+=1原子性问题采用synchronized互斥锁解决。

class TestCount{
    volatile long count = 0;
    synchronized void add10k(){
        int idx = 0;
        while (idx++ < 10000){
            count += 1;
        }
    }
}

但是对于这种简单的互斥操作,需要采用synchronized这种比较重的互斥锁吗?有没有更优的解决办法呢?当然存在,采用原子类无锁方案能够极大的提升性能

class TestCount{
    AtomicLong count = new AtomicLong(0);
     void add10k(){
        int idx = 0;
        while (idx++ < 10000){
            count.getAndIncrement();
        }
    }
}

原子类同样能够解决互斥性问题、原子性问题除此之外,因为原子类是无锁操作,没有用互斥锁解决带来的加锁解决性能消耗,这种绝佳方案是怎么做到的呢?

无锁方案实现原理

无锁方案之所以能够保证原子性,主要还是硬件保证,CPU为了解决并发问题,提供了CAS(Compare And Swap)指令即比较并交换,CAS一般包含三个参数,共享变量的内存地址A,用于比较的期望值B,更新共享变量C,当共享变量的内存地址A的值和共享变量B的值相等时,才将共享变量的内存地址A处的值更新为共享变量C。

将场景语义化如下

class SimpleCAS{
    int count;
    public synchronized int cas(int expect,int newCount){
        // 读取count值
       int oldcount = count;
       // 读取的count值和期望值比较
       if (oldcount == expect){
           count = newCount;
       }
       // 返回老值
       return oldcount;
    }
}

CAS指令判断并不是一次性的,如果比较失败又会重新取最新的值和期望值判断直到成功。

class SimpleCAS{
    volatile int count;
    public synchronized int cas(int expect,int newCount){
        // 读取count值
       int oldcount = count;
       // 读取的count值和期望值比较
       if (oldcount == expect){
            count = newCount;
        }
       // 返回老值
       return oldcount;
    }
    // 自旋操作,执行cas方法
    public void addOne(){
        int newCount = 0;
        do {
            newCount = count + 1;
        }while (count != cas(count,newCount));
    }
}

ABA问题

原子类虽然好用,但是一定需要的坑就是ABA问题,假如存在共享变量A值为5,线程T1将共享变量A的值改为2,而线程T2将共享变量A改为3,线程T3又将共享变量A改为2,那么对于线程T1来讲共享变量A是没有变的吗?显然不是,可能大多数场景我们并不关心ABA问题,对于基础数据递增可能认为值不变就够了,并不关心值是否已经修改,但是对于引用类型呢,这就一定要注意ABA问题,两个A虽然相等,但是属性可能已经发生变化。

原子类提供工具类解决ABA问题AtomicStampedReference和AtomicMarkableReference

public static void main(String[] args) throws InterruptedException {
    // 初始化原子类 定义初始引用和标识
    // AtomicMarkableReference同理可得,只是将版本戳换成了boolean类型
    AtomicStampedReference<String> reference = new AtomicStampedReference<>("zhangsan",1001);
    /**
      * expectedReference 期望的引用
      * newReference     新的引用
      * expectedStamp    期望的版本戳
      * newStamp         新的版本戳
      * 只有当期望引用和期望版本戳都符合实际版本戳和引用才能替换成功
    */
    reference.compareAndSet("zhangsan","lisi",1002,1003);
    // zhangsan 替换失败的原因是期望版本戳和实际版本戳不匹配
    System.out.println(reference.getReference());
    reference.compareAndSet("zhangsan","lisi",1001,1002);
    // lisi 替换成功
    System.out.println(reference.getReference());
    reference.compareAndSet("lisi1","wangwu",1002,1003);
    // lisi 替换失败的原因是期望引用和实际引用不匹配
    System.out.println(reference.getReference());
}

getAndIncrement源码分析

/**
  * this 指当前对象
  * valueOffset 指内存地址偏移量
*/
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
  * this 指当前对象
  * valueOffset 指内存地址偏移量
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 就是读取主内存的值
        var5 = this.getIntVolatile(var1, var2);
        // this.compareAndSwapInt方法就是对应上诉的cas方法,不过返回值是boolean类型
        // var5读取的主内存值
        // 更新成功返回true,跳出循环
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

原子工具类总览

原子工具类是一个大家族,根据使用可以分为原子化基本数据类型、原子化的对象引用类型、原子化数组、原子化对象属性更新器和原子化的累加器,方法都基本类似不需要刻意去记,需要用到的时候再来查就可以,但是需要有个印象,如下图所示。

image-20220302143728329

到此这篇关于Java的原子类无锁并发利器详解的文章就介绍到这了,更多相关原子类无锁并发利器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Cloud Stream异常处理过程解析

    Spring Cloud Stream异常处理过程解析

    这篇文章主要介绍了Spring Cloud Stream异常处理过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-08-08
  • springboot简单实现单点登录的示例代码

    springboot简单实现单点登录的示例代码

    本文主要介绍了springboot简单实现单点登录的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • Spring Boot中定时任务Cron表达式的终极指南最佳实践记录

    Spring Boot中定时任务Cron表达式的终极指南最佳实践记录

    本文详细介绍了SpringBoot中定时任务的实现方法,特别是Cron表达式的使用技巧和高级用法,从基础语法到复杂场景,从快速启用到调试验证,再到常见问题的解决,涵盖了定时任务开发的全过程,感兴趣的朋友一起看看吧
    2025-03-03
  • Java 生成PDF文档的示例代码

    Java 生成PDF文档的示例代码

    这篇文章主要介绍了Java 生成PDF文档的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-08-08
  • Java实现设计模式之责任链模式

    Java实现设计模式之责任链模式

    责任链模式是一种行为设计模式,允许你将请求沿着处理链发送,然后处理者都可对其进行处理,完成后可以再将其传递给下一个处理者。下面将会举例说明什么是责任链模式,责任链模式该如何使用
    2022-08-08
  • Java使用Spire.PDF for Java实现旋转PDF页面

    Java使用Spire.PDF for Java实现旋转PDF页面

    在日常的文档处理工作中,我们经常会遇到 PDF 页面方向不正确的问题,本文将深入探讨如何利用 Java 实现 PDF 页面的旋转操作,感兴趣的小伙伴可以了解下
    2025-09-09
  • Java-文件File简单实用方法(分享)

    Java-文件File简单实用方法(分享)

    下面小编就为大家带来一篇Java-文件File简单实用方法(分享)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • java中http请求之restTemplate配置超时时间问题解决

    java中http请求之restTemplate配置超时时间问题解决

    这篇文章主要介绍了java中http请求之restTemplate配置超时时间,本文给大家分享三种解决方法,结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • java8 Stream list to Map key 重复 value合并到Collectio的操作

    java8 Stream list to Map key 重复 value合并到Collectio的操作

    这篇文章主要介绍了java8 Stream list to Map key 重复 value合并到Collectio的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • SpringBoot读取配置的6种方式

    SpringBoot读取配置的6种方式

    本文主要介绍了SpringBoot读取配置的6种方式,主要包括使用默认配置、使用application.properties文件、使用application.yml文件、使用@Value注解、使用Environment对象和使用ConfigurableEnvironment对象,感兴趣的可以了解一下
    2023-08-08

最新评论