美文网首页
Java1.8-Atomic包简介(二)

Java1.8-Atomic包简介(二)

作者: 骑着乌龟去看海 | 来源:发表于2019-01-19 21:31 被阅读31次

    概述

      看过JDK源码的童鞋都知道,Java的concurrent包下又包含了两个包:locks和atomic,locks包下主要是实现线程安全的Lock类相关的接口与实现,而atomic包则是Java中原子类相关的实现类,今天就来学习下这个包下所包含的类。

    介绍

    1. 介绍下原子性

      所谓Atomic,翻译过来就是原子,学习数据库的时候我们了解过原子。原子被认为是操作中最小的单位,一段代码如果是原子的,则表示这段代码在执行过程中,要么执行成功,要么执行失败,不会有中间状态。原子操作一般都是底层通过CPU的指令来实现。而atomic包下的这些类,则可以让我们在多线程环境下,通过一种无锁的原子操作来实现线程安全。

    2. 实现基础

      atomic包下的类基本上都是借助Unsafe类,通过CAS操作来封装实现的。Unsafe这个类简单说下,这个类不属于Java标准,或者说这个类是Java预留的一个后门类,JDK中,有关提升性能的concurrent或者NIO等操作,大部分都是借助于这个类来封装操作的。
      我们都知道,Java这种编译型语言,不像C语言能支持操作内存,正常情况下都是由JVM进行内存的创建回收等操作,但这个类提供了一些直接操作内存相关的底层操作,使得我们也可以手动操作内存,但从类的名字就可以看出,这个类不是安全的,官方也是不建议我们使用的,说不定哪天有可能废弃掉。

    3. 类介绍

    在JDK8的atomic包下,大概有16个类,按照原子的更新方式,大概可以分为4类:原子更新普通类型,原子更新数组,原子更新引用,原子更新字段。接下来我们来挨个看下。

    3.1 更新基本类型

      atomic包下提供了三种基本类型的原子更新,分别是AtomicBooleanAtomicIntegerAtomicLong,这几个原子类对应于基础类型的布尔,整形,长整形,至于Java中其他的基本类型,如float等,如果需要,我们可以参考这几个类的源码自行实现。首先,我们来看下AtomicInteger这个类的常用方法:

    // 以原子方式递增给定的值,也就是将给定的值与现有值进行相加
    public final int addAndGet(int delta)
    // 原子更新,如果输入的值等于预期值,则以原子方式将该值设置为输入的值
    public final boolean compareAndSet(int expect, int update)
    
    // 以原子方式递增,将当前值加1,相当于i++,返回的是自增前的值
    public final int getAndIncrement()
    // 以原子方式递减,将当前值减1,相当于i--,返回的是递减前的值
    public final int getAndDecrement()
    // 以原子方式递增,相当于++i,返回的是自增后的值
    public final int incrementAndGet()
    // 以原子方式递减,相当于--1,返回的是递减后的值
    public final int decrementAndGet()
    
    // 返回当前值
    public final int get()
    // 设置当前值为给定的值
    public final void set(int newValue)
    // 顾名思义,设置并返回旧值
    public final int getAndSet(int newValue)
    // 添加给定值,并返回旧值
    public final int getAndAdd(int delta)
    // 添加给定值,返回新的值
    public final int addAndGet(int delta)
    
    // JDK8引入,设置并返回旧值,设置的值是使用一个函数式表达式的结果
    public final int getAndUpdate(IntUnaryOperator updateFunction)
    // 和上面一样,返回新的值
    public final int updateAndGet(IntUnaryOperator updateFunction)
    
    // 更新,返回旧的值,更新值为 当前值与给定值通过表达式进行计算的结果
    public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)
    // 和上面类似,返回新的值
    public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)
    
    // 设置值,以一种延迟的方式;设置值后,不保证该值的修改能被其他线程立刻看到;
    // 也就是说,其他线程有可能读到的还是旧的值;
    public final void lazySet(int newValue)
    

    这里面的方法都比较正常,除了两个方法,一个是lazySet方法,有关这个方法的介绍可以参考:AtomicLong.lazySet是如何工作的?,另一个方法是weakCompareAndSet,这个方法的实现和compareAndSet的方法一模一样,暂时不知道是做什么用的。

    另外,简单提供一个小例子来测试下:

    AtomicInteger atomicInteger = new AtomicInteger(1);
    System.out.println(atomicInteger.incrementAndGet());                       // 2
    System.out.println(atomicInteger.getAndIncrement());                       // 2
    System.out.println(atomicInteger.getAndAccumulate(2, (i, j) -> i + j));    // 3
    System.out.println(atomicInteger.get());                                   // 5
    System.out.println(atomicInteger.addAndGet(5));                            // 10
    

    而有关AtomicBoolean这个类的操作就更简单了,这个类中的方法不多,基本上上面都介绍了,而内部的计算则是先将布尔转换为数字0/1,然后再进行后续计算。而AtomicLong则是和AtomicInteger一样,就不多说了。

    3.2 更新数组

      atomic包下提供了三种数组相关类型的原子更新,分别是 AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray,对应于整型,长整形,引用类型,要说明的一点是,这里说的更新是指更新数组中的某一个元素的操作。由于方法和更新基本类型方法相同,这里只简单看下AtomicIntegerArray这个类的几个方法,其他的方法类似。

    // i代表数组的下标
    public final boolean compareAndSet(int i, int expect, int update)
    
    // 递增对应下标处的值
    public final int incrementAndGet(int i)
    public final int decrementAndGet(int i)
    

    AtomicInteger相比,方法多了一个数组下标的参数,其他的都相同,方法内部调用也是一样的,举个简单的例子来看下:

    AtomicIntegerArray array = new AtomicIntegerArray(5);
    array.set(0, 1);                                        // 设置数组第一个值为1
    System.out.println(array.getAndDecrement(0));         // 1
    System.out.println(array.addAndGet(0, 5));      // 5
    

    至于AtomicReferenceArray,是一个支持泛型的原子类,操作是相似的:

    AtomicReferenceArray<String> array = new AtomicReferenceArray<>(5);
    array.set(0, "hello");
    System.out.println(array.getAndSet(0, "hello world")); //  hello
    System.out.println(array.get(0));                                  //  hello world
    
    3.3 更新引用类型

      更新引用类型的原子类包含了AtomicReference(更新引用类型),AtomicReferenceFieldUpdater(抽象类,更新引用类型里的字段),AtomicMarkableReference(更新带有标记的引用类型)这三个类,这几个类能同时更新多个变量。来看下简单的例子(代码来自:http://ifeve.com/java-atomic/),首先来看下AtomicReference的使用:

    public static void main(String[] args) {
        AtomicReference<User> reference = new AtomicReference<>();
        User user = new User("MrZhang", 20);
        reference.set(user);
        
        User user1 = new User("MrLi", 22);
        // User{name='MrZhang', age=20}
        System.out.println(reference.getAndSet(user1));  
        // User{name='MrLi', age=22}        
        System.out.println(reference.get());
    }
    static class User {
        private String name;
        private int age;
        // 省略get,set,构造方法,toString方法
    }
    

    然后来看下AtomicReferenceFieldUpdater,这个类是个抽象类,用于更新某个实例对象的某一个字段,但该字段有一些限制:

    • 字段的类型必须是volatile类型的,在线程之间共享变量时保证可见性;
    • 只能是实例变量且可修改,不能是类变量,也即是不能是static修饰,也不能是final修饰的变量;
    • 调用者必须能够直接操作对象的字段,所以对象字段的访问修饰符需要与调用者保持一致;

    该抽象类提供了静态方法newUpdater来创建它的实现类,该方法接受三个参数:包含该字段的对象的类,要被更新的对象的类,要被更新的字段的名称,来看一下简单实现:

    public static void main(String[] args) {
        User user = new User("MrZhang", 20);
        AtomicReferenceFieldUpdater<User, String> referenceFieldUpdater =
                AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
    
        referenceFieldUpdater.compareAndSet(user, user.name, "MrLi");
        // output : MrLi
        System.out.println(referenceFieldUpdater.get(user));
    
    }
    static class User {
        volatile String name;
        private int age;    
    }
    

    AtomicMarkableReference则是为了解决CAS过程中的ABA问题,AtomicMarkableReference可以让我们了解到对象在进行CAS的过程中是否被修改过,而与之对应的是AtomicStampedReference,这个类可以给引用加上版本号,让我们了解到对象都是经过怎样的修改,以及修改了多少次,这里就不举例了。

    3.4 更新字段类型

      这个其实和上面说的有点相似,如果我们更新的时候只更新对象中的某一个字段,则可以使用atomic包提供的更新字段类型:AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicStampedReference,前两个顾名思义,就是更新int和long类型,最后一个是更新引用类型,该类提供了版本号,用于解决通过CAS进行原子更新过程中,可能出现的ABA问题。前面这两个类和上面我们介绍的AtomicReferenceFieldUpdater有些相似,都是抽象类,都需要通过newUpdater方法进行实例化,并且对字段的要求也是一样的。

    public static void main(String[] args) {
        User user = new User("MrZhang", 20);
        AtomicIntegerFieldUpdater<User> updater = 
                AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
        // output : 30
        System.out.println(updater.get(user));
    }
    
    static class User {
        volatile String name;
        volatile int age;
    }
    
    3.5 JDK8之后引入的类型

      在JDK8之前,针对原子操作,我们基本上可以通过上面提供的这些类来完成我们的多线程下的原子操作,不过在并发高的情况下,上面这些单一的CAS+自旋操作的性能将会是一个问题,所以上述这些类一般用于低并发操作,而针对这个问题,JDK8又引入了下面几个类:DoubleAdderLongAdderDoubleAccumulatorLongAccumulator,这些类是对AtomicLong这些类的改进与增强,这些类都继承自Striped64这个类。我们来简单看下LongAdder这个类,先来看下这个类的方法:

    // 将当前值与给定值相加
    public void add(long x)
    // 自增
    public void increment()
    // 自减
    public void decrement()
    // 求和
    public long sum()
    // 重置
    public void reset()
    // 上面两个方法合到一块
    public long sumThenReset()
    

    简单实例:

    LongAdder adder = new LongAdder();
    adder.add(12L);
    adder.increment();
    System.out.println(adder.sum()); // 13
    

    拿LongAdder来说,该类维护了一个数组变量Cell[],这些变量的和就是要以原子方式更新的long型变量,当更新方法add在线程间竞争时,该组变量可以动态增长,从而减少竞争,这种操作类似于分流,也就是分段锁机制。而方法sum则返回当前在cells变量上的总和。

    本文参考自:
    《Java并发编程实战》
    Java中的Atomic包使用指南
    源码阅读:全方位讲解LongAdder

    相关文章

      网友评论

          本文标题:Java1.8-Atomic包简介(二)

          本文链接:https://www.haomeiwen.com/subject/stftjqtx.html