美文网首页
java.util.concurrent包学习之原子类

java.util.concurrent包学习之原子类

作者: 魂之挽歌w | 来源:发表于2019-09-28 22:50 被阅读0次

前言

  假设对共享变量只做赋值操作,那么可以将这些共享变量声明为volatile(volatile不能保证原子性)。但是很多情况下时必须要对共享变量进行操作的,java.util.concurrent.atomic包提供了13种原子类,这些类使用了很高效的机器级指令(而不是使用锁)来保证共享变量操作的原子性。原子类可以保证操作的原子性、有序性和可见性这三个并发必须了解的特性不了解可以看看https://www.jianshu.com/p/d917615b5b67,原理上实际是使用volatile确保可见性和有序性,使用Unsafe提供的CAS(CompareAndSet)方法确保原子性。
ps:CAS参考https://www.jianshu.com/p/a6305203e5cf
做个对比来看看保证原子性的重要性:
下面是不采用原子类,使用1000个线程来对一个整数a进行操作,每个线程对a做1000次a++(这个操作不是原子性的),逾期结果为:a =1000*1000

public static void main(String[] args) {
        long start_time = System.currentTimeMillis();
        for (int i =0 ;i<1000;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j=0 ;j<1000;j++){
                        a++;
                    }
                }
            }).start();
        }
        while (Thread.activeCount()>2){
            //有除主线程外加上Thread.activeCount还有一个守护线程,所以是2
        }
        System.out.println("使用时间="+(System.currentTimeMillis()-start_time));
        System.out.println("a="+a);
    }

使用时间=513
a=999117

分析:可以看到由于在进行a++操作时不能保证原子性,所有有一些线程读取到的是其他线程还未更新的旧值,所以最终结果不正确。下面来看看原子类

private static AtomicInteger b = new AtomicInteger();
    public static void main(String[] args) {
        long start_time = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        b.incrementAndGet();
                    }
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            //有除主线程外加上Thread.activeCount还有一个守护线程,所以是2
        }
        System.out.println("使用时间=" + (System.currentTimeMillis() - start_time));
        System.out.println("a=" + b.get());
    }

使用时间=886
a=1000000

结果分析:结果非常完美,但是我们发现,由于使用cas保证了原子性,耗时是比前者长的。

13种原子类基本介绍

一、原子更新基本类型

使用原子的方式更新基本类型,Atomic包提供了以下3个类。
  (1)AtomicBoolean: 原子更新布尔类型。
  (2)AtomicInteger: 原子更新整型。
  (3)AtomicLong: 原子更新长整型。
  以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
  (1)int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
  (2)boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
  (3)int getAndIncrement(): 以原子的方式将当前值加1,注意,这里返回的是自增前的值。
  (4)void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
  (5)int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。

二、原子更新数组

通过原子的方式更新数组里的某个元素,Atomic包提供了以下的4个类:
  (1)AtomicIntegerArray: 原子更新整型数组里的元素。
  (2)AtomicLongArray: 原子更新长整型数组里的元素。
  (3)AtomicReferenceArray: 原子更新引用类型数组里的元素。
  这三个类的最常用的方法是如下两个方法:
  (1)get(int index):获取索引为index的元素值。
  (2)compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值。

三、原子更新引用类型

Atomic包提供了以下三个类:
  (1)AtomicReference: 原子更新引用类型。
  (2)AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
  (3)AtomicMarkableReferce: 原子更新带有标记位的引用类型。
  这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象set进Atomic类,然后调用compareAndSet等一些方法去进行原子操作,原理都是基于Unsafe实现,但AtomicReferenceFieldUpdater略有不同,更新的字段必须用volatile修饰。

四、原子更新字段类

Atomic包提供了四个类进行原子字段更新:
  (1)AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
  (2)AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
  (3)AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
  (4)AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。
  这四个类的使用方式都差不多,示例代码如上一小节的AtomicReferenceFieldUpdater一样,要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段必须使用public volatile修饰。

注意:

  如果有大量线程要访问相同的原子值,性能会大幅度下降,因为乐观更新需要太多次重试。Java SE8提供了LongAdder和LongAccumulator来解决这个问题。LongAdder包括多个变量(加数),其总和为当前值。可以有多个线程更新不同的加数,线程个数增加会自动提供新的加数。通常情况下,只有当所有工作都完成之后,才需要总和的值,对于这种情况,这种方法会更高校,性能会有显著提升。

参考

相关文章

网友评论

      本文标题:java.util.concurrent包学习之原子类

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