原子操作知多少?

439 阅读13分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

| 作者:江夏

| CSDN:blog.csdn.net/qq_41153943

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

本文大概5877字,读完共需15分钟

前言

今天看到一个面试题是什么是原子操作及其实现,但是感觉自己记得并不是很清楚,所以想着写篇文章好好的整理一下关于原子操作的知识点。

一、概念

什么是原子操作?我们知道在化学和物理中原子是最小的元素不能够再被分割的粒子,我记得好像是最小的,不过这个不是重点哈。所以我们可以理解原子操作就是不可被分割的一个或多个系列的操作。比如有操作A和操作B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子操作。

二、实现

在java中可以通过锁机制的方式来实现原子操作,但是有时候需要更有效灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,因为synchronized关键字具有排他性,如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争,同时也会造成死锁的情况。而且锁的机制相当于其他轻量级的需求有点过于笨重,比如计数器。

实现原子操作还可以使用CAS实现原子操作,CAS的英文名全称是Compare and Swap,即比较再交换。jdk5增加了并发包java.util.concurrent.*,在其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,前面说到这是一种独占锁,也是是悲观锁。

CAS利用了处理器提供的CMPXCHG指令(比较并交换操作数)来实现的,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。

CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。

CAS实现线程安全其实并不是在语言层面进行解决的,而是交给CPU和内存,利用CPU的多处理能力,来实现硬件层面的阻塞,再加上volatile变量的特性(可见性,有序性)来实现基于原子操作的线程安全。

图片

三、CAS实现原子性操作的三大问题

在Java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,比如LinkedTransferQueue类的xfer方法。CAS虽然很高的解决了原子操作,但是CAS仍然存在三大问题。ABA问题、循环时间长开销大、以及只能保证一个共享变量的原子操作。

1、ABA问题

因为CAS需要在操作值的时候,检查值有没有发生变化,如果发生变化则更新,但是如果一个值为A,变成了B,又变成了A,那么使用CAS进行检查时就会发现它的值没有发生变化,但实际上发生变化了。ABA问题的解决思路就是使用版本号,在变量前边追加版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从java1.5开始,JDK提供了AtomicStampedReference、AtomicMarkableReference来解决ABA的问题,通过compareAndSet方法检查值是否发生变化以外检查版本号是否发生变化。compareAndSet方法首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部等于,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(
    v expectedReference, //预期引用
    V newReference,      //更新后的引用
  int expectedStamp,     //预期标志
  int newStamp           //更新后的标志
)

2、循环时间长开销大

自旋CAS如果长时间不动,会给CPU带来非常大的执行开销,如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令,是CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突而引起的CPU流水线被清空,从而提高内存CPU的执行效率。

3、只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

四、Jdk中相关原子操作类的使用

前面说到从jdk1.5开始,JDK的并发包里就提供了一些类来支持原子操作,如AtomicBoolean、AtomicInter。这些原子包装类还提供了简单、性能高效、线程安全有用的工具方法,并且 在并发代码来说是非常关键的。原子变量将发生在单个的变量上,粒度最细的情况。原子变量类有很多种,所以Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性。

更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong
更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型:原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic 包提供了以下3个类。AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

1、原子更新基本类型

使用原子方式更新基本类型,Atomic包提供了以下3个类。

AtomicBoolean:原子更新布尔值类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整形

以AtomicInteger为例,常用方法如下:

int get():获取当前值
void set(int newValue):设置为给定值
void lazySet(int newValue):最终设置为给定的值
int getAndSet(int newValue):以原子方式设置为给定值并返回旧值
boolean compareAndSet(int expect, int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
int getAndDecrement():以原子方式将当前值减1,注意,这里返回的是自减前的值。
int getAndAdd(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回旧值。
int incrementAndGet():以原子方式将当前值加1,注意,这里返回的是自增后的值。
int decrementAndGet():以原子方式将当前值减1,注意,这里返回的是自减后的值。
int decrementAndGet():以原子方式将当前输入值与实例中的值(AtomicInteger里的value)相加,并返回新值。——

栗子:

package Demo1;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {
    static AtomicInteger atomicInteger = new AtomicInteger(10);

    public static void main(String[] args) {
        System.out.println(atomicInteger.get()); // int get():获取当前值
        atomicInteger.set(12); // void set(int newValue):设置为给定值
        System.out.println(atomicInteger.get());
        atomicInteger.lazySet(15); // void lazySet(int newValue):最终设置为给定的值
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.addAndGet(24)); // int getAndSet(int newValue):以原子方式设置为给定值并返回旧值
        System.out.println(atomicInteger.compareAndSet(12,15)); // boolean compareAndSet(int expect, int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
        System.out.println(atomicInteger.getAndIncrement()); // int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
        System.out.println(atomicInteger.getAndDecrement()); // int getAndDecrement():以原子方式将当前值减1,注意,这里返回的是自减前的值。
        System.out.println(atomicInteger.getAndAdd(10)); // int getAndAdd(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回旧值。
        System.out.println(atomicInteger.incrementAndGet()); // int incrementAndGet():以原子方式将当前值加1,注意,这里返回的是自增后的值。
        System.out.println(atomicInteger.decrementAndGet()); // int decrementAndGet():以原子方式将当前值减1,注意,这里返回的是自减后的值。
        System.out.println(atomicInteger.addAndGet(10)); // int decrementAndGet():以原子方式将当前输入值与实力中的值(AtomicInteger里的value)相加,并返回新值。

    }
}

图片

2、原子更新数组

通过原子的方式更新数组里的某个元素,Atomic包提供了以下四个类。

AtomicIntegerArray:原子更新整型数组里的元素。
AtomicLongArray:原子更新长征信数组里的元素。
AtomicReferenceArray:原子更新引用类型数组里的元素。

以AtomicIntegerArray为例,主要是提供原子的方式更新数组里的整型,其常用方法如下:

int getAndSet(int i, int newValue):原子地将位置{@code i}的元素设置为给定的值并返回旧值。
int addAndGet(int i, int delta):原子地将给定的值添加到索引{@code i}处的元素并返回新值。
boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式将数组位置{@code i}的元素设置成update值。
int getAndIncrement(int i):原子地在索引{@code i}处的值自增1并返回自增前的值。
int incrementAndGet(int i):原子地在索引{@code i}处的值自增1并返回自增后的值。
int getAndDecrement(int i):原子地在索引{@code i}处的值自减1并返回自减前的值。
int decrementAndGet(int i):原子地在索引{@code i}处的值自减1并返回自减后的值。

栗子:

package Demo1;

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayDemo {
    static int[] value = new int[] { 1, 2 };
    static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
    public static void main(String[] args) {
        System.out.println( atomicIntegerArray.getAndSet(0, 3)); // int getAndSet(int i, int newValue):原子地将位置{@code i}的元素设置为给定的值并返回旧值。
        System.out.println(atomicIntegerArray.get(0));
        System.out.println(atomicIntegerArray.addAndGet(1,5)); // int addAndGet(int i, int delta):原子地将给定的值添加到索引{@code i}处的元素并返回新值。
        System.out.println(atomicIntegerArray.compareAndSet(0,3,30)); // boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式将数组位置{@code i}的元素设置成update值。
        System.out.println(atomicIntegerArray.getAndIncrement(0)); // int getAndIncrement(int i):原子地在索引{@code i}处的值自增1并返回自增前的值。
        System.out.println(atomicIntegerArray.incrementAndGet(0)); // int incrementAndGet(int i):原子地在索引{@code i}处的值自增1并返回自增后的值。
        System.out.println(atomicIntegerArray.getAndDecrement(0)); // int getAndDecrement(int i):原子地在索引{@code i}处的值自减1并返回自减前的值。
        System.out.println(atomicIntegerArray.decrementAndGet(0)); // int decrementAndGet(int i):原子地在索引{@code i}处的值自减1并返回自减后的值。

        System.out.println(Arrays.toString(value)); //

    }
}

图片

AtomicIntegerArray源码:
    /**     * 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();    }
所以原数组不会变化:因为数组value 通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当 AtomicIntegerArray 对内部的数组元素进行时,不会影响传入的数组。

3、原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,则需要使用这个原子引用类型提供的类。

AtomicReference:原子更新引用类型。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicMarkableReference:原子更新带有标记的引用类型,可以以原子更新一个布尔类型的标志位和引用类型。

以AtomicReference为例,常用方法如下:

V get():获取当前值
void set(V newValue):设置为给定值
V getAndSet(V newValue):原子地设置给定的值,并返回旧值。
boolean compareAndSet(V expect, V update):如果当前值等于预期值(expect),原子地将当前值设置为更新值(update)。

栗子:

package Demo1;

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {
    static AtomicReference<UserInfo> atomicUserRef;

    //定义一个实体类
    static class UserInfo {
        private volatile String name;
        private int age;

        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "UserInfo{" +"name='" + name + '\'' +", age=" + age +'}';
        }

        public static void main(String[] args) {
            UserInfo user1 = new UserInfo("Mark", 15);//要修改的实体的实例

            UserInfo user2 = new UserInfo("Mark001", 18);//要修改的实体的实例

            atomicUserRef = new AtomicReference(user1);

            UserInfo updateUser = new UserInfo("Bill", 17); // 修改为此实例的值

            System.out.println(atomicUserRef.compareAndSet(user1, updateUser)); // 更新成功,boolean compareAndSet(V expect, V update):如果当前值等于预期值(expect),原子地将当前值设置为更新值(update)。
            System.out.println(atomicUserRef.compareAndSet(user2, updateUser)); // 更新失败
            System.out.println(atomicUserRef.get()); // V get():获取当前值
            atomicUserRef.set(user2); // void set(V newValue):设置为给定值
            System.out.println(atomicUserRef.get());

            UserInfo user3 = new UserInfo("James001", 20);
            System.out.println(atomicUserRef.getAndSet(user3)); // V getAndSet(V newValue):原子地设置给定的值,并返回旧值。
            System.out.println(atomicUserRef.get()); // 获取更新后的值
            System.out.println(user1);
        }
    }
}

图片

代码中首先构建一个userinfo对象,然后把userinfo对象设置进AtomicReferenc中,最后调用 compareAndSet方法进行原子更新操作,实现原理同AtomicInteger里的compareAndSet方法。

4、原子更新字段类

如果需要原子更新某个类的字段需要使用更新字段类

AtomicIntegerFieldUpdater:原子更新整数型字段更新器
AtomicLongFieldUpdater:原子更新长整数字段更新器 
AtomicStampedReference:原子更新带有版本号的引用类型。

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

栗子:

package Demo1;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterDemo {
    // 创建原子更新器,并设置需要更新的对象类和对象的属性
    private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class,"old");
    public static void main(String[] args) {
        // 设置柯南的年龄是10岁
        User conan = new User("tim",10);
        // 柯南长了一岁,但是仍然会输出旧的年龄
        System.out.println(a.getAndIncrement(conan));
        // 输出柯南现在的年龄
        System.out.println(a.get(conan));
    }
    public static class User {
        private String name;
        public volatile int old;
        public User(String name,int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }
        public int getOld() {
            return old;
        }
    }
}

图片

总结

以上就是关于原子操作的一些原理和概念,也是平时笔面试题常见的了。上述也是我的一些见解和总结,如果有不正确的地方欢迎指出讨论。

相关推荐: