Java16个原子类介绍

Java原子类实现原理分析

在谈谈java中的volatile一文中,我们提到过并发包中的原子类可以解决类似num++这样的复合类操作的原子性问题,相比锁机制,使用原子类更精巧轻量,性能开销更小,本章就一起来分析下原子类的实现机理。

悲观的解决方案(阻塞同步)

我们知道,num++看似简单的一个操作,实际上是由1.读取 2.加一 3.写入 三步组成的,这是个复合类的操作(所以我们之前提到过的volatile是无法解决num++的原子性问题的),在并发环境下,如果不做任何同步处理,就会有线程安全问题。最直接的处理方式就是加锁

synchronized(this){
    num++;
 }

使用独占锁机制来解决,是一种悲观的并发策略,抱着一副“总有刁民想害朕”的态势,每次操作数据的时候都认为别的线程会参与竞争修改,所以直接加锁。同一刻只能有一个线程持有锁,那其他线程就会阻塞。线程的挂起恢复会带来很大的性能开销,尽管jvm对于非竞争性的锁的获取和释放做了很多优化,但是一旦有多个线程竞争锁,频繁的阻塞唤醒,还是会有很大的性能开销的。所以,使用synchronized或其他重量级锁来处理显然不够合理。

乐观的解决方案(非阻塞同步)

 乐观的解决方案,顾名思义,就是很大度乐观,每次操作数据的时候,都认为别的线程不会参与竞争修改,也不加锁。如果操作成功了那最好;如果失败了,比如中途确有别的线程进入并修改了数据(依赖于冲突检测),也不会阻塞,可以采取一些补偿机制,一般的策略就是反复重试。很显然,这种思想相比简单粗暴利用锁来保证同步要合理的多。

  鉴于并发包中的原子类其实现机理都差不太多,本章我们就通过AtomicInteger这个原子类来进行分析。我们先来看看对于num++这样的操作AtomicInteger是如何保证其原子性的。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

 我们来分析下incrementAndGet的逻辑:

  1.先获取当前的value值

  2.对value加一

  3.第三步是关键步骤,调用compareAndSet方法来来进行原子更新操作,这个方法的语义是:

先检查当前value是否等于current,如果相等,则意味着value没被其他线程修改过,更新并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。

compareAndSet调用了Unsafe类的compareAndSwapInt方法

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

此方法是Java的native方法,并不由Java语言实现。

方法的作用是,读取传入对象o在内存中偏移量为offset位置的值与期望值expected(var4)作比较。

相等就把var5的值赋值给offset位置的值。方法返回true。不相等,就取消赋值,方法返回false。

这也是CAS的思想,及比较并交换。用于保证并发时的无锁并发的安全性。

CAS(Compare-and-Swap)

CAS算法是由硬件直接支持来保证原子性的,有三个操作数:内存位置V、旧的预期值A和新值B,当且仅当V符合预期值A时,CAS用新值B原子化地更新V的值,否则,它什么都不做。

CAS的ABA问题:当然CAS也并不完美,它存在"ABA"问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。 


关于原子类个数说明

在JDK7包括7之前,java原子类有12个,图片如下

Java16个原子类介绍_第1张图片

 在JDK8时出现了4个原子操作类,分别是如下图片所示

Java16个原子类介绍_第2张图片


原子更新基本类型类 

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

  • AtomicBoolean:  原子更新布尔类型。 
  • AtomicInteger:    原子更新整型。 
  • AtomicLong:       原子更新长整型。 

以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下: 

  • int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。 
  • boolean compareAndSet(int expect, int update): 如果当前值等于预期值,则以原子方式将当前值设置为更新的值。 
  • int getAndIncrement(): 以原子的方式将当前值加 1,注意,这里返回的是自增前的值,也就是旧值。 
  • void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 
  • int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。

代码示例

	static AtomicInteger ai =new AtomicInteger(1);
	public static void main(String[] args) {
 
		System.out.println(ai.getAndIncrement());
		System.out.println(ai.get());
     }

 输出结果

1

2

下面我们看看getAndIncrement() 是如何实现原子操作的 

 public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
 
 
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;
    }

代码解析

我们取得了旧值,然后把要加的数传过去,调用getAndAddInt () 进行原子更新操作,实际最核心的方法是 compareAndSwapInt(),使用CAS进行更新。我们Unsafe只提供了3中CAS操作,

另外注意,AtomicBoolean 是把Boolean转成整型,在使用 compareAndSwapInt 进行操作的。

/**
 * 如果当前数值是var4,则原子的将java变量更新成var5或var6
 * @return 如果更新成功返回true
 */
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
 
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
 
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

原子更新数组

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

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

这三个类的最常用的方法是如下两个方法: 

  • get(int index):获取索引为index的元素值。 
  • compareAndSet(int i, int expect, int update): 如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置为update值。 

下面以 AtomicReferenceArray 举例如下

static int[] value =new int[]{1,2};
static AtomicIntegerArray ai =new AtomicIntegerArray(value);

public static void main(String[] args) {
		ai.compareAndSet(0,1,5);
		System.out.println(ai.get(0));
		System.out.println(value[0]);
}

输出结果 

5

1

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

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

原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个值,如果更新多个值,比如更新一个对象里的值,那么就要用原子更新引用类型提供的类,Atomic包提供了以下三个类:

  • AtomicReference:  原子更新引用类型。 
  • AtomicReferenceFieldUpdater:  原子更新引用类型的字段。 
  • AtomicMarkableReferce:  原子更新带有标记位的引用类型,可以使用构造方法更新一个布尔类型的标记位和引用类型。 
  public static AtomicReference ai = new AtomicReference();
 
    public static void main(String[] args) {
 
        User u1 = new User("pangHu", 18);
        ai.set(u1);
        User u2 = new User("piKaQiu", 15);
        ai.compareAndSet(u1, u2);
        System.out.println(ai.get().getAge() + ai.get().getName());
 
    }
 
 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;
        }
    }
 

输出结果:piKaQiu 15

代码分析

我们把对象放到 AtomicReference 中,然后调用 compareAndSet () 原子操作替换,原理和 AtomicInteger 相同,只是调用的是 compareAndSwapObject()  方法。


原子更新字段类

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

  • AtomicIntegerFieldUpdater:  原子更新整型的字段的更新器。 
  • AtomicLongFieldUpdater:  原子更新长整型字段的更新器。 
  • AtomicStampedFieldUpdater:  原子更新带有版本号的引用类型。 
 //创建原子更新器,并设置需要更新的对象类和对象的属性
    private static AtomicIntegerFieldUpdater ai = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
 
    public static void main(String[] args) {
 
        User u1 = new User("pangHu", 18);
        //原子更新年龄,+1
        System.out.println(ai.getAndIncrement(u1));
        System.out.println(u1.getAge());
    }
 
 
 
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;
        }
    }

代码详解

要想原子地更新字段类需要两步。

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

输出结果:

18

19


JDK8新增原子类简介 

  • DoubleAccumulator
  • LongAccumulator
  • DoubleAdder
  • LongAdder

下面以 LongAdder 为例介绍一下,并列出使用注意事项

这些类对应把 AtomicLong 等类的改进。比如 LongAccumulator 与 LongAdder 在高并发环境下比 AtomicLong 更高效。

Atomic、Adder在低并发环境下,两者性能很相似。但在高并发环境下,Adder 有着明显更高的吞吐量,但是有着更高的空间复杂度。

LongAdder其实是LongAccumulator的一个特例,调用LongAdder相当使用下面的方式调用LongAccumulator。

sum()  方法在没有并发的情况下调用,如果在并发情况下使用会存在计数不准,下面有代码为例。

LongAdder不可以代替AtomicLong  ,虽然 LongAdder 的 add() 方法可以原子性操作,但是并没有使用 Unsafe 的CAS算法,只是使用了CAS的思想。

LongAdder其实是LongAccumulator的一个特例,调用LongAdder相当使用下面的方式调用LongAccumulator,LongAccumulator提供了比LongAdder更强大的功能,构造函数其中accumulatorFunction一个双目运算器接口,根据输入的两个参数返回一个计算值,identity则是LongAccumulator累加器的初始值。

private static ExecutorService executorService = Executors.newFixedThreadPool(5);
    public static void main(String[] args) {
        for (int i = 1; i <= 100; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    counter.add(2);
                }
            });
        }
        System.out.println(counter.sum());
        System.out.println(counter);
    }

输出结果

Java16个原子类介绍_第3张图片

如图LongAdder则是内部维护多个变量,每个变量初始化都0,在同等并发量的情况下,争夺单个变量的线程量会减少这是变相的减少了争夺共享资源的并发量,另外多个线程在争夺同一个原子变量时候如果失败并不是自旋CAS重试,而是尝试获取其他原子变量的锁,最后获取当前值时候是把所有变量的值累加后返回的。

Java16个原子类介绍_第4张图片

//构造函数
LongAdder()
    //创建初始和为零的新加法器。
 
//方法摘要
void    add(long x)    //添加给定的值。
void    decrement()    //相当于add(-1)。
double  doubleValue() //在扩展原始转换之后返回sum()as double。
float   floatValue()  //在扩展原始转换之后返回sum()as float。
void    increment()  //相当于add(1)。
int intValue()      //返回sum()作为int一个基本收缩转换之后。
long    longValue() //相当于sum()。
void    reset()    //重置将总和保持为零的变量。
long    sum()     //返回当前的总和。
long    sumThenReset()  //等同于sum()后面的效果reset()。
String  toString()   //返回。的字符串表示形式sum()。

你可能感兴趣的:(java,并发编程,java,html)