关于Atomic

一.Atomic是什么

所谓 Atomic,翻译过来就是原子。原子被认为是操作中最小的单位,一段代码如果是原子的,则表示这段代码在执行过程中,要么执行成功,要么执行失败。

原子操作一般都是底层通过 CPU 的指令来实现。而 atomic 包下的这些类,则可以让我们在多线程环境下,通过一种无锁的原子操作来实现线程安全。

atomic 包下的类基本上都是借助 Unsafe 类,通过 CAS 操作来封装实现的。Unsafe 这个类不属于 Java 标准,或者说这个类是 Java 预留的一个后门类,

JDK 中,有关提升性能的 concurrent 或者 NIO 等操作,大部分都是借助于这个类来封装操作的。

Java 是种编译型语言,不像 C 语言能支持操作内存,正常情况下都是由 JVM 进行内存的创建回收等操作,但这个类提供了一些直接操作内存相关的底层操作,

使得我们也可以手动操作内存,但从类的名字就可以看出,这个类不是安全的,官方也是不建议我们使用的。

二.Atomic能做什么

在多线程或者并发环境中,常常会遇到这种情况 int i=0; i++ 。稍有经验的同学都知道这种写法是线程不安全的。

为了达到线程安全的目的,通常会用synchronized来修饰对应的代码块。现在我们有了新的方法,就是使用J.U.C包下的atomic类。

三.Atomic原理

一句话来说,atomic类是通过自旋CAS操作volatile变量实现的。

CAS是compare and swap的缩写,即比较后(比较内存中的旧值与预期值)交换(将旧值替换成预期值)。它是sun.misc包下Unsafe类提供的功能,需要底层硬件指令集的支撑。

使用volatile变量是为了多个线程间变量的值能及时同步。

按理来说,使用synchroized已经能满足功能需求了。为什么还会有这个类呢?那肯定是性能的问题了。

在JDK1.6之前,synchroized是重量级锁,即操作被锁的变量前就对对象加锁,不管此对象会不会产生资源竞争。这属于悲观锁的一种实现方式。

而CAS会比较内存中对象和当前对象的值是否相同,相同的话才会更新内存中的值,不同的话便会返回失败。这是乐观锁的一中实现方式。这种方式就避免了直接使用内核状态的重量级锁。

但是在JDK1.6以后,synchronized进行了优化,引入了偏向锁,轻量级锁,其中也采用了CAS这种思想,效率有了很大的提升。

四.Atomic使用

Atomic包里一共有12个类,主要提供四种原子更新方式:

1、原子方式更新基本类型

以下三个类是以原子方式更新基本类型

(1)AtomicBoolean:原子更新布尔类型。

(2)AtomicInteger:原子更新整型。

(3)AtomicLong:原子更新长整型。

以AtomicInteger为例:

package concurrency;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

    static AtomicInteger ai = new AtomicInteger(1);

    public static void main(String[] args) {
        //相当于i++,返回的是旧值,看方法名就知道,先获取再自增
        System.out.println(ai.getAndIncrement());
        System.out.println(ai.get());
        //先自增,再获取
        System.out.println(ai.incrementAndGet());
        System.out.println(ai.get());
        //增加一个指定值,先add,再get
        System.out.println(ai.addAndGet(5));
        System.out.println(ai.get());
        //增加一个指定值,先get,再set
        System.out.println(ai.getAndSet(5));
        System.out.println(ai.get());
    }

}

注意:

Atomic包提供了三种基本类型的原子更新,剩余的Java的基本类型还有char,float和double等,其更新方式可以参考AtomicBoolean的思路来现,

AtomicBoolean是把boolean转成整型再调用compareAndSwapInt进行CAS来实现的,类似的short和byte也可以转成整形,float和double可以利用Float.floatToIntBits,

Double.doubleToLongBits转成整形和长整形进行相应处理;

2、原子方式更新数组

以下三个类是以原子方式更新数组,

(1)AtomicIntegerArray:原子更新整型数组里的元素。

(2)AtomicLongArray:原子更新长整型数组里的元素。

(3)AtomicReferenceArray:原子更新引用类型数组里的元素。

以AtomicIntegerArray为例,其方法与AtomicInteger很像,多了个数组下标索引;

package concurrency;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayTest {

    static int[] valueArr = new int[] { 1, 2 };

    //AtomicIntegerArray内部会拷贝一份数组
    static AtomicIntegerArray ai = new AtomicIntegerArray(valueArr);

    public static void main(String[] args) {
        ai.getAndSet(0, 3);
        //不会修改原始数组value
        System.out.println(ai.get(0));
        System.out.println(valueArr[0]);
    }

}

 

3、原子方式更新引用

以下三个类是以原子方式更新引用,与其它不同的是,更新引用可以更新多个变量,而不是一个变量;

(1)AtomicReference:原子更新引用类型。

(2)AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

(3)AtomicMarkableReference:原子更新带有标记位的引用类型。

以AtomicReference为例:

package concurrency;

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {

    public static AtomicReference<User> atomicUserRef = new AtomicReference<User>();

    public static void main(String[] args) {
        User user = new User("conan", 15);
        atomicUserRef.set(user);
        User updateUser = new User("Shinichi", 17);
        atomicUserRef.compareAndSet(user, updateUser);
        System.out.println(atomicUserRef.get().getName());
        System.out.println(atomicUserRef.get().getOld());
    }

    static class User {
        private String name;
        private int old;

        public User(String name, int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

        public int getOld() {
            return old;
        }
    }
}

 

4、原子方式更新字段

以下三个类是以原子方式更新字段:

(1)AtomicIntegerFieldUpdater:原子更新整型字段的更新器。

(2)AtomicLongFieldUpdater:原子更新长整型字段的更新器。

(3)AtomicStampedReference:原子更新带有版本号的引用类型,用于解决使用CAS进行原子更新时,可能出现的ABA问题。

以AtomicIntegerFieldUpdater为例:

package concurrency;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterTest {

    private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater
            .newUpdater(User.class, "old");

    public static void main(String[] args) {
        User conan = new User("conan", 10);
        System.out.println(a.getAndIncrement(conan));
        System.out.println(a.get(conan));
    }

    public static class User {
        private String name;
        //注意需要用volatile修饰
        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;
        }
    }
}

5、Atomic类的缺点

(1)ABA问题:

对于一个旧的变量值A,线程2将A的值改成B又改成可A,此时线程1通过CAS看到A并没有变化,但实际A已经发生了变化,这就是ABA问题。

解决这个问题的方法很简单,记录一下变量的版本就可以了,在变量的值发生变化时对应的版本也做出相应的变化,然后CAS操作时比较一下版本就知道变量有没有发生变化。

atomic包下AtomicStampedReference类实现了这种思路。Mysql中Innodb的多版本并发锁也是这个原理。

(2)自旋问题:

atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,

但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。

因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。

当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。

6、总结

对于jdk1.8的并发包来说,底层基本上就是通过Usafe和CAS机制来实现的。有好处也肯定有一个坏处。

从好的方面来讲,就是上面AtomicInteger类可以保持其原子性。

但是从坏的方面来看,Usafe因为直接操作的底层地址,肯定不是那么安全,而且CAS机制也伴随着大量的问题,比如说有名的ABA问题等等。

JDK8又引入了下面几个类:DoubleAdderLongAdderDoubleAccumulatorLongAccumulator,这些类是对AtomicLong这些类的改进与增强,这些类都继承自Striped64这个类。

原文地址:https://www.cnblogs.com/ZJOE80/p/12911502.html