Java并发编程之原子性-Atomic的使用


Posted in Java/Android onMarch 16, 2022

线程安全

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类时线程安全的。

线程安全主要体现在以下三个方面

  • 原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作
  • 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

JUC中的Atomic包详解

Atomic包中提供了很多Atomicxxx的类:

Java并发编程之原子性-Atomic的使用

它们都是CAS(compareAndSwap)来实现原子性。

先写一个简单示例如下:

@Slf4j
public class AtomicExample1 { 
    // 请求总数
    public static int clientTotal = 5000; 
    // 同时并发执行的线程数
    public static int threadTotal = 200; 
    public static AtomicInteger count = new AtomicInteger(0); 
    public static void main(String[] args) throws Exception {

        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }
 
    private static void add() {
        count.incrementAndGet();
    }
}

可以发下每次的运行结果总是我们想要的预期结果5000。说明该计数方法是线程安全的。

我们查看下count.incrementAndGet()方法,它的第一个参数为对象本身,第二个参数为valueOffset是用来记录value本身在内存的编译地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较,第三个参数为常量1

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
 
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
 
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
 
    private volatile int value; 
 
    ... 此处省略多个方法...
 
    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
}

AtomicInteger源码里使用了一个Unsafe的类,它提供了一个getAndAddInt的方法,我们继续点看查看它的源码:

public final class Unsafe {
    private static final Unsafe theUnsafe;
 
    ....此处省略很多方法及成员变量.... 
 
 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 
        return var5;
    } 
 
 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 
 public native int getIntVolatile(Object var1, long var2);
}

可以看到这里使用了一个do while语句来做主体实现的。而在while语句里它的核心是调用了一个compareAndSwapInt()的方法。它是一个native方法,它是一个底层的方法,不是使用Java来实现的。

假设我们要执行0+1=0的操作,下面是单线程情况下各参数的值:

Java并发编程之原子性-Atomic的使用

Java并发编程之原子性-Atomic的使用

更新后:

Java并发编程之原子性-Atomic的使用

compareAndSwapInt()方法的第一个参数(var1)是当前的对象,就是代码示例中的count。此时它的值为0(期望值)。第二个值(var2)是传递的valueOffset值,它的值为12。第三个参数(var4)就为常量1。方法中的变量参数(var5)是根据参数一和参数二valueOffset,调用底层getIntVolatile方法得到的值,此时它的值为0 。compareAndSwapInt()想要达到的目标是对于count这个对象,如果当前的期望值var1里的value跟底层的返回的值(var5)相同的话,那么把它更新成var5+var4这个值。不同的话重新循环取期望值(var5)直至当前值与期望值相同才做更新。compareAndSwap方法的核心也就是我们通常所说的CAS。

Atomic包下其他的类如AtomicLong等的实现原理基本与上述一样。

这里再介绍下LongAdder这个类,通过上述的分析,我们已经知道了AtomicLong使用CAS:在一个死循环内不断尝试修改目标值直到修改成功。如果在竞争不激烈的情况下,它修改成功概率很高。反之,如果在竞争激烈的情况下,修改失败的概率会很高,它就会进行多次的循环尝试,因此性能会受到一些影响。

对于普通类型的long和double变量,jvm允许将64位的读操作或写操作拆成两个32位的操作。LongAdder的核心思想是将热点数据分离,它可以将AtomicLong内部核心数据value分离成一个数组,每个线程访问时通过hash等算法映射到其中一个数字进行计数。而最终的计数结果则为这个数组的求和累加,其中热点数据value,它会被分离成多个单元的cell,每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成。这样,热点就进行了有效的分离,提高了并行度。LongAdder相当于在AtomicLong的基础上将单点的更新压力分散到各个节点上,在低并发的时候对base的直接更新可以很好的保障跟Atomic的性能基本一致。而在高并发的时候,通过分散提高了性能。但是如果在统计的时候有并发更新,可能会导致统计的数据有误差。

在实际高并发计数的时候,可以优先使用LongAdder。在低并行度或者需要准确数值的时候可以优先使用AtomicLong,这样反而效率更高。

下面简单的演示下Atomic包下AtomicReference简单的用法:

@Slf4j
public class AtomicExample4 { 
    private static AtomicReference<Integer> count = new AtomicReference<>(0); 
    public static void main(String[] args) {
        count.compareAndSet(0, 2); 
        count.compareAndSet(0, 1); 
        log.info("count:{}", count.get());
    }
}

compareAndSet()分别传入的是预期值跟更新值,只有当预期值跟当前值相等时,才会将值更新为更新值;

上面的第一个方法可以将值更新为2,而第二个步中无法将值更新为1。

下面简单介绍下AtomicIntegerFieldUpdater 用法(利用原子性去更新某个类的实例):

@Slf4j
public class AtomicExample5 { 
    private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
 
    @Getter
    private volatile int count = 100; 
    public static void main(String[] args) { 
        AtomicExample5 example5 = new AtomicExample5();
 
        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 1, {}", example5.getCount());
        }
 
        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 2, {}", example5.getCount());
        } else {
            log.info("update failed, {}", example5.getCount());
        }
    }
}

它可以更新某个类中指定成员变量的值。

注意:修改的成员变量需要用volatile关键字来修饰,并且不能是static描述的字段。

AtomicStampReference这个类它的核心是要解决CAS的ABA问题(CAS操作的时候,其他线程将变量的值A改成了B,接着又改回了A,等线程使用期望值A与当前变量进行比较的时候,发现A变量没有变,于是CAS就将A值进行了交换操作。

实际上该值已经被其他线程改变过)。

ABA问题的解决思路就是每次变量变更的时候,就将版本号加一。

看一下它的一个核心方法compareAndSet():

public class AtomicStampedReference<V> { 
    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
 
   ... 此处省略多个方法 ....
 
   public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
}

可以看到它多了一个stamp的比较,stamp的值是由每次更新的时候进行维护的。

再介绍下AtomicLongArray,它维护了一个数组。在该数组下,我们可以选择性的已原子性操作更新某个索引对应的值。

public class AtomicLongArray implements java.io.Serializable {
    private static final long serialVersionUID = -2308431214976778248L;
 
    private static final Unsafe unsafe = Unsafe.getUnsafe();
 
    ...此处省略....
 
 
    /**
     * Atomically sets the element at position {@code i} to the given value
     * and returns the old value.
     *
     * @param i the index
     * @param newValue the new value
     * @return the previous value
     */
    public final long getAndSet(int i, long newValue) {
        return unsafe.getAndSetLong(array, checkedByteOffset(i), newValue);
    }
 
    /**
     * Atomically sets the element at position {@code i} to the given
     * updated value if the current value {@code ==} the expected value.
     *
     * @param i the index
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int i, long expect, long update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }
}

最后再写一个AtomcBoolean的简单使用:

@Slf4j
public class AtomicExample6 { 
    private static AtomicBoolean isHappened = new AtomicBoolean(false);
 
    // 请求总数
    public static int clientTotal = 5000;
 
    // 同时并发执行的线程数
    public static int threadTotal = 200;
 
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened.get());
    }
 
    private static void test() {
        if (isHappened.compareAndSet(false, true)) {
            log.info("execute");
        }
    }
}

总结

以上就是Atomic包的基本原理及主要的使用方法。它是使用CAS来保证原子性操作,从而达到线程安全的目的。

仅为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Java/Android 相关文章推荐
Java方法重载和方法重写的区别到底在哪?
Jun 11 Java/Android
Java实战之用Swing实现通讯录管理系统
Jun 13 Java/Android
SpringBoot2 参数管理实践之入参出参与校验的方式
Jun 16 Java/Android
Java生成读取条形码和二维码的简单示例
Jul 09 Java/Android
dubbo集成zipkin获取Traceid的实现
Jul 26 Java/Android
简述Java中throw-throws异常抛出
Aug 07 Java/Android
SpringMVC 整合SSM框架详解
Aug 30 Java/Android
关于maven依赖 ${xxx.version}报错问题
Jan 18 Java/Android
springboot用户数据修改的详细实现
Apr 06 Java/Android
Java实现带图形界面的聊天程序
Jun 10 Java/Android
springboot读取resources下文件的方式详解
Jun 21 Java/Android
Java实现字符串转为驼峰格式的方法详解
Jul 07 Java/Android
Java9新特性之Module模块化编程示例演绎
Mar 16 #Java/Android
JVM的类加载器和双亲委派模式你了解吗
Java生成日期时间存入Mysql数据库的实现方法
Mar 03 #Java/Android
Java设计模式之享元模式示例详解
解析探秘fescar分布式事务实现原理
关于ObjectUtils.isEmpty() 和 null 的区别
Feb 28 #Java/Android
java objectUtils 使用可能会出现的问题
Feb 28 #Java/Android
You might like
PHP基础学习之流程控制的实现分析
2013/04/28 PHP
PHP捕获Fatal error错误的方法
2014/06/11 PHP
PIGCMS 如何关闭聊天机器人
2015/02/12 PHP
浅谈Yii乐观锁的使用及原理
2017/07/25 PHP
基于PHP实现邮箱验证激活过程详解
2020/10/28 PHP
枚举JavaScript对象的函数
2006/12/22 Javascript
jquery JSON的解析方式
2009/07/25 Javascript
jQuery实现鼠标移到元素上动态提示消息框效果
2013/10/20 Javascript
JQuery插件fancybox无法在弹出层使用左右键的解决办法
2013/12/25 Javascript
jquery的ajax和getJson跨域获取json数据的实现方法
2014/02/04 Javascript
用html+css+js实现的一个简单的图片切换特效
2014/05/28 Javascript
javascript根据时间生成m位随机数最大13位
2014/10/30 Javascript
初步认识JavaScript函数库jQuery
2015/06/18 Javascript
谈谈对JavaScript原生拖放的深入理解
2016/09/20 Javascript
Vue.js实战之Vuex的入门教程
2017/04/01 Javascript
Bootstrap实现基于carousel.js框架的轮播图效果
2017/05/02 Javascript
微信小程序 选项卡的简单实例
2017/05/24 Javascript
原生JS实现的双色球功能示例
2018/02/02 Javascript
Vue2.0 实现移动端图片上传功能
2018/05/30 Javascript
Vue源码探究之状态初始化
2018/11/14 Javascript
微信小程序实现通过双向滑动缩放图片大小的方法
2018/12/30 Javascript
js如何获取图片url的Blob值并预览示例代码
2019/03/07 Javascript
Vue中父子组件的值传递与方法传递
2020/09/28 Javascript
[00:32]2018DOTA2亚洲邀请赛VG出场
2018/04/03 DOTA
[49:40]2018DOTA2亚洲邀请赛小组赛 A组加赛 TNC vs Newbee
2018/04/03 DOTA
Python挑选文件夹里宽大于300图片的方法
2015/03/05 Python
Python压缩解压缩zip文件及破解zip文件密码的方法
2015/11/04 Python
Python读大数据txt
2016/03/28 Python
python爬虫之百度API调用方法
2017/06/11 Python
python 实现将多条曲线画在一幅图上的方法
2019/07/07 Python
Python MySQLdb 执行sql语句时的参数传递方式
2020/03/04 Python
一文读懂Python 枚举
2020/08/25 Python
vue.js刷新当前页面的实例讲解
2020/12/29 Python
对于没有初始化的变量的初始值可以作怎样的假定
2014/10/12 面试题
大学校务公开实施方案
2014/03/31 职场文书
导游词之任弼时故居
2020/01/07 职场文书