并发编程 11:Atomic原子操作类总结

大家好,我是一天喝 3 升水的七哥,今天继续卷。

我们为什么一定要学习 Atomic 包下的这些原子操作类呢?下面告诉你原因。

Java中有那么一些类,是以Atomic开头的。这一系列的类我们称之为原子操作类。

以最简单的类AtomicInteger为例,它相当于一个int变量,我们执行Int的 i++ 的时候并不是一个原子操作。而使用AtomicInteger的incrementAndGet却能保证原子操作。更新变量这种场景下效果和 synchronized 相同,却要简单高效的多。

这篇文章大家带着下面的三个问题出发,相信你会有所收获的。

  1. Atomic包中的原子操作类具体有什么?
  2. 如何使用?
  3. 如何实现的?

文章中的示例代码,我们要尽量多动手去实践操作,自己写出来和仅仅看懂是不一样的。知识和技能是不一样的,看会的那叫知识,动手实践掌握的才叫技能

并发编程 11:Atomic原子操作类总结

看完上面的图,我们会发现原子操作类分为4种类型的原子更新方式,那为什么要提供这么多种分类呢?显而易见,是因为我们Java中的变量有很多种类型,Atomic包是为了让我们在不同的场景选择更加实用的原子操作类。

原子更新基本类型

我们Java中的基本数据类型是用的非常多的,比如说计算 人数、金额、更新条件变量布尔值等。Atomic包针对基本数据类型提供了3个类,想知道他们具体是什么,如何使用?那么接着往下看,你会有所收获。

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以上三个类,看名字就能发现它们是分别用来原子更新布尔、整形、Long整形的。具体该如何使用呢?我们先来看下它们提供的方法。

并发编程 11:Atomic原子操作类总结

我们只要知道了原子更新基本数据类型的类,然后点进去看,通过名字和代码注释就知道怎么用了。除去一些 getter/setter等基础方法,常用的也不多,这里给大家列举下。

由于Atomic包下针对基本数据类型提供的这三个类方法基本上都是一样的,名称都一样,知道一个的意思,其他的套用即可。

我这里列举 AtomicInteger 这个最常用的类来展示其提供的常用方法以及含义:

  • 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
   /**
  * Atomically adds the given value to the current value.
  * 原子地将给定值添加到当前值。
  * @param delta the value to add
  * @return the updated value
  */

 public final int addAndGet(int delta) {
     return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
 }
  • 如果输入的数值等于预期值,则以原子方 式将该值设置为输入的值
/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 * @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 expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
  • 以原子的方式将当前值+1,注意,这里是返回增加之前的原有值,其实看方法名就可以知道,getAndIncrement  是要先 get 然后再 increment
/**
 * Atomically increments by one the current value.
 * 将当前值原子递增1。
 * @return the previous value
 */

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
  • 懒设置,即最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值,这是基于性能方面考虑,因为在多个CPU缓存间同步一个内存的代价是很昂贵的。
/**
 * Eventually sets to the given value.
 * 最终设置为给定值。
 * @param newValue the new value
 * @since 1.6
 */

public final void lazySet(int newValue) {
    unsafe.putOrderedInt(this, valueOffset, newValue);
}
  • 以原子方式设置为newValue的值,并返回旧值,也是通过名字就知道是先 get ,所以返回时设置新值之前的值
/**
 * Atomically sets to the given value and returns the old value.
 *
 * @param newValue the new value
 * @return the previous value
 */

public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

上面这些常见的方法,使用也很简单。在需要原子操作的场景比使用 synchronized 可简单、高效很多的,下面就来看看具体的使用场景。

问题:假设我们有两个线程,分别将全局整形变量 i 进行加1。每个线程执行5000次,按照传统的int使用方式,代码如下:

public static void main(String[] args) throws InterruptedException 
    CountDownLatch cdl 
new CountDownLatch(2);
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 5000; j++) {
                m++;
            }
            cdl.countDown();
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 5000; j++) {
                m++;
            }
            cdl.countDown();
        }
    });
    t1.start();
    t2.start();
    cdl.await();
    System.out.println("result=" + m);
}

当我运行的时候,结果却不是10000,你知道为什么?

原因就在于 m++, 因为 m++ 不是一个原子操作, 它是要先读取再自增,然后赋值,在读取自增未赋值的时候就会产生线程安全问题,如果你没想明白,这里为什么线程不安全,欢迎给我留言。

那这个问题要怎么解决呢?我们最容易想到的就是,线程安全问题,那就上大杀器 synchronized ,没有解决不了的并发问题。当然这个没有错了,只要在 m++ 这行代码加上关键字 synchronized就能解决,不过 synchronized 过于沉重,今天我们要用原子操作类来解决,我看来看下使用原子操作类如何解决。

public static void main(String[] args) throws InterruptedException {
    CountDownLatch cdl = new CountDownLatch(2);
    AtomicInteger i = new AtomicInteger(0);

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 5000; j++) {
                i.incrementAndGet();
            }
            cdl.countDown();
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 5000; j++) {
                i.incrementAndGet();
            }
            cdl.countDown();
        }
    });
    t1.start();
    t2.start();

    cdl.await();
    System.out.println("result=" + i.get());
}

现在我们无论执行多少次,结果总是10000。

上面我们已经看了 AtomicInteger 可以在并发场景中保证变量更新是线程安全的,接下来我们再看下 getAndIncrement() 方法的使用。

@Test
public void testAtomicIntegerTest() {
    AtomicInteger atomicInteger = new AtomicInteger(0);
    int andIncrement = atomicInteger.getAndIncrement();
    System.out.println(andIncrement);
    System.out.println(atomicInteger.get());
}

通过上面的学习,你先看下上面的代码,思考下输出值是多少呢?

答案我就不贴了,动手验证下,你会掌握的更加牢固,明白 incrementAndGet()getAndIncrement() 之间的差异点。

还是忍不住告诉你答案吧:

并发编程 11:Atomic原子操作类总结

实现原理

通过上面的示例代码以及练习,你是否会思考这样一个问题,没错,就是 实现原理

接下来我们就拿上面刚刚练习的 getAndIncrement() 来一起分析下实现原理。

看源码呢,这里七哥给大家分享下我的方法,不是直接去看对应类的所有成员变量和方法,而是直接进入我们使用的方法,因为我们研究原理的前提肯定是已经会用了,通过写的示例代码,直接进入对应方法查看,比如 getAndIncrement() , 进入后源代码如下:

/**
 * Atomically increments by one the current value.
 * 将当前值原子递增1。
 * @return the previous value
 */

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

发现没有,居然没有逻辑,就一行代码,使用了 Unsafe 来实现,这个类是JDK提供的一个工具类,里面有很多 native 方法,即本地方法,是 JVM 使用 C 帮我们实现的,我们只要记住他提供了一些安全的操作方法,是直接操作内存的。

不过我们发现, unsafe.getAndAddInt(this,valueOffset,1) 方法中的三个参数,第一个this不用多说,就是指我们当前操作的 AtomicInteger 对象, 第3个参数是1,这个也好理解,因为我们这个方法就是用来自增的嘛,每次增加1,是增量值。重要在于第二个参数 valueOffset,这是一个成员变量,我们看下其在类中的定义:

// value成员属性的内存地址相对于对象内存地址的偏移量
private static final long valueOffset;

我已经写了注释,相信不难理解,这里我们会发现他定义修饰为常量,使用了 static final 描述,这又是为什么呢?要解答这个问题,我们要看下 valueOffset 赋值的地方:

static {
    try {
        //初始化valueOffset,通过unsafe.objectFieldOffset方法获取成员属性value内存地址相对于对象内存地址的偏移量
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

在静态代码块中赋值的,这里就要考验大家的基本功了,静态代码块的执行时机是在什么时候呢?

这也是面试常考的知识点,通常会和子类继承父类的场景结合在一起,考验我们的基本功,我梳理了一张图,分享给大家,这个绝对值得保存,因为我不相信你不会忘。

并发编程 11:Atomic原子操作类总结

通过上图我们知道了 在我们创建 AtomicInteger 对象时,会先执行静态代码块,为 valueOffset 进行赋值,调用的是 unsafe.objectFieldOffset 方法,这个方法也是 native 的,上面说了 native 方法是 JVM 调用 C 函数实现的,我们这里只要清楚他是用来获取 AtomicInteger 这个 对象中 value 属性 的内存偏移量的,这样就能根据对象的内存地址找到属性值的内存地址。

搞清楚了 unsafe.getAndAddInt(this, valueOffset, 1) 方法参数的含义,我们接着往里看:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 通过对象和字段偏移量找到当前的预期值
        var5 = this.getIntVolatile(var1, var2);
        
     /**
     * CAS操作,现代CPU已广泛支持,是一种原子操作;
     * 简单地说,当期待值var5与valueOffset地址处的值相等时,设置为预期值+1,完成自增
     */

    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

注释我也写的很明白了,要补充一点就是,这里使用了 do-while 循环,其实也就是乐观锁,非阻塞的同步方式,一直尝试进行原子操作更新,直到成功。

讲到这里,原理就了解的差不多了,其他类似 AtomicLong、AtomicBoolean 都是利用 Unsfae实现的,按照我上面讲的方法你可以自行查看。一定要自己多去看看,动手操作下,我们是程序员,这个自觉性一定要有。

掌握了原理,我们在一起看看 其他的原子操作类的方法和使用。

原子更新数组

我们接着来看 Atomic 包下提供的原子更新类。

通过原子的方式更新数组里的某个元素,即操作数组元素是线程安全的,Atomic包提供了以下3个类:

  • AtomicIntegerArray:原子更新整型数组里的元素

  • AtomicLongArray:原子更新长整型数组里的元素

  • 原子更新引用类型数组里的元素

上述几个类提供的方法几乎一样,我们以AtomicIntegerArray类为例讲解,其常用方法如下:

  • int addAndGet(int i , int delta):以原子方式将输入值与数组中索引 i 的元素相加

  • boolean compareAndSet(int i , int expect , int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值

@Test
public void testAtomicIntegerArray() {
    int[] value = new int[]{1,2};
    AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
    int andSet = atomicIntegerArray.getAndSet(03);
    // 以原子方式设置为newValue的值,并返回旧值
    Assert.assertEquals(4,atomicIntegerArray.addAndGet(0,1));
    Assert.assertEquals(1,andSet);
    Assert.assertEquals(4,atomicIntegerArray.get(0));
    // AtomicIntegerArray会将传入的数组复制一份,不会影响原数组
    Assert.assertEquals(1,value[0]);
}

上面的测试都是通过的,你能看懂就说明掌握了对应方法的使用。

需要注意的是,数组value通过构造方法传递进去,然后 AtomicIntergerArray 会将当前数组复制一份,所以当 AtomicIntergerArray 对内部的数组元素进行修改时,不会影响传入的数组。

构造方法:

/**
 * Creates a new AtomicIntegerArray with the same length as, and
 * all elements copied from, the given array.
 *
 * @param array the array to copy elements from
 * @throws NullPointerException if array is null
 */

public AtomicIntegerArray(int[] array) {
    // Visibility guaranteed by final field guarantees
    this.array = array.clone();
}

原子更新引用

上面我们学会了如何原子更新基本类型,可是平时工作中复杂的业务场景使用最多的还是应用类型,这个要如何更新呢?

这就需要使用原子更新引用类型提供的类,Atomic包提供了以下3个类:

  • AtomicReference:原子更新引用类型
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段
  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是 AtomicMarkableReference(V initialRef,boolean initialMark)

以上几个类提供的方法几乎一样,所以此处我们仅以AtomicReference为例进行讲解,AtomicReference的使用示例代码如下:

public static void main(String[] args) {
    User user = new User("张三"20);
    atomicReference.set(user);
    User updateUser = new User("李四",25);
    atomicReference.compareAndSet(user,updateUser);
    System.out.println(atomicReference.get());
}

static class User{
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

运行结果:
User{name='李四', age=25}

其实现原理是依靠了 unsafe.compareAndSwapObject 方法。

public final boolean compareAndSet(V expect, V update) {
    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

原子更新属性(字段)

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新:

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器

  • AtomicLongFieldUpdater:原子更新长整型字段的更新器

  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题

要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段.

以上3个类提供的方法几乎一样,此处仅以 AtomicIntegerFieldUpdater 为例进行讲解,AtomicIntegerFieldUpdater的 示例代码如下:

private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public static void main(String[] args) {
    User user = new User("张三",10);
    System.out.println(updater.addAndGet(user,2));
    System.out.println(updater.get(user));
}
static class User{
    private String name;
    public volatile int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}
运行结果:
12
12

思考一个问题

当我们看了 Unsafe 类后,发现他只提供了三种CAS方法:compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong。但是Java的基本类型里还有char、float和double等。那么问题来了,如何原子的更新其他的基本类型呢?

并发编程 11:Atomic原子操作类总结
/**
* 如果当前数值是expected,则原子的将Java变量更新成x
@return 如果更新成功则返回true
*/

public final native boolean compareAndSwapObject(Object o,long offset , Object expected , Object x );
public final native boolean compareAndSwapInt(Object o , long offset , int expected, int x );
public final native boolean compareAndSwapLong(Object o , long offset , long expected ,long x );

这个问题,我们直接通过 AtomicBoolean 源码来看看:

// AtomicBoolean
public final boolean compareAndSet(boolean expect, boolean update) {
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

发现没?它是先把Boolean转换成整型,再使用 compareAndSwapInt 进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现。

// AtomicDouble
public final boolean compareAndSet(double expect, double update) {
    return updater.compareAndSet(this,Double.doubleToRawLongBits(expect),Double.doubleToRawLongBits(update));
}

至此,今天这篇文章想写的Java原子操作类就基本差不多了,给大家小结一下:

  1. JDK中从1.5 开始提供了Atomic包下的原子操作类,他们都是 Atomic 开头的,可以便捷、高效、安全的使用在需要更新变量的场景;
  2. Atomic 包下提供的原子操作类涵盖:原子更新基本类型、原子更新数组、原子更新引用、原子更新属性;
  3. 原子更新操作类都是使用Unsafe提供的三个CAS方法结合死循环实现的,也就是乐观锁。



码字不易,觉得内容对你有帮助,希望你能花0.1秒点个赞哟~ 你小小的 点赞 永远是我持续创作的动力,谢谢你(疯狂比心)~

作者介绍: 七哥,非科班转行程序员,靠努力学习一步步成功,写文章也经常拍视频,专注编程技术与个人成长干货,愿望是陪家人平淡快乐的度过一生!


并发编程 11:Atomic原子操作类总结


留个个人微信二维码

欢迎做个点赞之交

原文始发于微信公众号(七哥聊编程):并发编程 11:Atomic原子操作类总结

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/37104.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!