Java线程安全中的原子性操作

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割,而只执行其中的一部分(不可中断性)。将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。

说到原子性,在Java中一共有两个方面需要学习和掌握

  1. 一个是JDK中已经提供好的Atomic包,他们均使用了CAS完成线程的原子性操作。
  2. 另一个是使用锁的机制来处理线程之间的原子性。锁包括synchronized、Lock。

什么是CAS?

CAS(Compare and swap)比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。CAS操作需要输入两个数值,一个旧值(期望操作前的值)A和一个新值B,在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换,交换失败后进行自旋,直到成功或者线程失败结束。Java中的sun.misc.Unsafe类,提供了compareAndSwapint()和compareAndSwapLong()等几个方法实现CAS。

操作系统修改内存中的值通常是经过找到变量在内存中的内存地址,然后修改变量的值,但是作为开发人员,我们是在JVM下编写代码,Java的API是不允许我们像操作系统一样去通过内存地址去找到变量,修改变量的值。通常我们是使用上面所提到的sun.misc.Unsafe类,Unsafe知道到每个对象在内存中的内存区域是怎样的,并且可以得到对应字段的offset,也就是我们所说的偏移量,偏移量的类型是Long类型。下面是通过Unsafe,简单的实现CAS操作

public class CounterUnsafe {
 
    volatile int i = 0;
 
    private static Unsafe unsafe = null;
    //偏移量
    private static Long valueOffset;
 
    static {
        try {
            //通过反射得到Unsafe类
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            //获取i字段的偏移量
            Field fieldi = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(fieldi);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
 
    public void add() {
 
        for (;;){
            //通过偏移量取到变量值
            int current = unsafe.getIntVolatile(this,valueOffset);
            //若CAS操作成功,则停止自旋
            if(unsafe.compareAndSwapInt(this, valueOffset, current, current+1)) {
                break;
            }
        }
 
    }
}

Atomic包

从java1.5开始,jdk提供了java.util.concurrent.atomic包,这个包中的原子操作类(均使用CAS机制完成原子性操作),提供了一种用法简单,性能高效,线程安全的更新一个变量的方式。

atomic包里面一共提供了13个类,分为4种类型,分别是:原子更新基本类型,原子更新数组,原子更新引用,原子更新属性,这13个类都是使用Unsafe实现的包装类。

在这里插入图片描述

一、原子更新基本类型

Atomic包提供了3个类用于原子更新基本类型:分别是AtomicInteger原子更新整型,AtomicLong原子更新长整型,AtomicBoolean原子更新bool值。由于这三个类提供的方法几乎是一样的,以AtomicInteger为例进行说明。

AtomicInteger

public class CountExample {

    //请求总数
    public static int clientTotal  = 5000;
    //同时并发执行的线程数
    public static int threadTotal = 200;
    //变量声明:计数
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();//创建线程池
        final Semaphore semaphore = new Semaphore(threadTotal);//定义信号量,给出允许并发的数目
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);//定义计数器闭锁
        for (int i = 0;i<clientTotal;i++){
            executorService.execute(()->{
                try {
                    semaphore.acquire();//判断进程是否允许被执行
                    add();
                    semaphore.release();//释放进程
                } catch (InterruptedException e) {
                    log.error("excption",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();//保证信号量减为0
        executorService.shutdown();//关闭线程池
        log.info("count:{}",count.get());//变量取值
    }

    private static void add(){
        count.incrementAndGet();//变量操作
    }
}

对count变量的+1操作,采用的是incrementAndGet方法,此方法的源码中调用了一个名为unsafe.getAndAddInt的方法:

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

而getAndAddInt方法的具体实现为:

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;
}

代码中的count可以理解为JMM中的工作内存,而这里的底层数值即为主内存

getAndAddInt调用了Unsafe的native方法:getIntVolatile和compareAndSwapInt,在do-while循环中先取得当前值,然后通过CAS判断当前值是否和current一致,如果一致意味着值没被其他线程修改过,把当前值设置为当前值+var4,如果不相等程序进入新的CAS循环。

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);

通过代码,我们发现Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现。

AtomicLong 与 LongAdder对比

LongAdder是java8为我们提供的新的类,跟AtomicLong有相同的效果。首先看一下代码实现:

//AtomicLong:
//变量声明
public static AtomicLong count = new AtomicLong(0);
//变量操作
count.incrementAndGet();
//变量取值
count.get();

//LongAdder:
//变量声明
public static LongAdder count = new LongAdder();
//变量操作
count.increment();
//变量取值
count

那么问题来了,为什么有了AtomicLong还要新增一个LongAdder呢?
原因是:CAS底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈的时候,修改成功率很高,否则失败率很高。在失败的时候,这些重复的原子性操作会耗费性能。

对于普通类型的long、double变量,JVM允许将64位的读操作或写操作拆成两个32位的操作。

LongAdder类的实现核心是将热点数据分离,比如说它可以将AtomicLong内部的内部核心数据value分离成一个数组,每个线程访问时,通过hash等算法映射到其中一个数字进行计数,而最终的计数结果则为这个数组的求和累加,其中热点数据value会被分离成多个单元的cell,每个cell独自维护内部的值。当前对象的实际值由所有的cell累计合成,这样热点就进行了有效地分离,并提高了并行度。这相当于将AtomicLong的单点的更新压力分担到各个节点上。在低并发的时候通过对base的直接更新,可以保障和AtomicLong的性能基本一致。而在高并发的时候通过分散提高了性能。

public void increment() {
    add(1L);
}
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

如果在统计的时候,如果有并发更新,可能会有统计数据有误差。实际使用中在处理高并发计数的时候优先使用LongAdder。在线程竞争不是很激烈的情况下,使用AtomicLong会简单效率更高一些。比如序列号生成(准确性)

二、原子更新数组

Atomic包里提供了三个类用于原子更新数组里面的元素,分别是:AtomicIntegerArray:原子更新整型数组里的元素;AtomicLongArray:原子更新长整型数组里的元素;AtomicReferenceArray:原子更新引用数组里的元素;因为每个类里面提供的方法都一致,因此以AtomicIntegerArray为例来说明。

AtomicIntegerArray

public class AtomicIntegerArrayTest {

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

    public static void main(String[] args){
        atomicInteger.getAndSet(0,12);
        System.out.println(atomicInteger.get(0));
        System.out.println(value[0]);
    }

}

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

三、原子更新引用

原子更新基本类型的AtomicInteger只能更新一个变量,如果要原子更新多个变量,就需要使用原子更新引用类型提供的类了。原子引用类型atomic包主要提供了以下几个类:AtomicReference:原子更新引用类型;AtomicReferenceFieldUpdater:原子更新引用类型里的值;AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。以上类中提供的方法基本一致,我们以AtomicReference为例说明:

AtomicReference

public class AtomicReferenceTest {

    private static AtomicReference<User> reference = new AtomicReference<User>();

    public static void main(String[] args){
        User user = new User("tom",23);
        reference.set(user);
        User updateUser = new User("ketty",34);
        reference.compareAndSet(user,updateUser);
        System.out.println(reference.get().getName());
        System.out.println(reference.get().getAge());
    }


    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 int getAge() {
            return age;
        }
    }
}

上述代码中首先创建一个user对象,然后把user对象设置进AtomicReference中,最后通过compareAndSet做原子更新操作,运行结果如下:

ketty
34

四、原子更新属性

如果需要原子更新某个对象的某个字段,就需要使用原子更新属性的相关类,Atomic中提供了一下几个类用于原子更新属性:AtomicIntegerFieldUpdater:原子更新整型属性的更新器;AtomicLongFieldUpdater:原子更新长整型的更新器;AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。

想要原子的更新字段,需要两个步骤:

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

AtomicIntegerFieldUpdater与AtomicLongFieldUpdater方法基本一致,下面用AtomicIntegerFieldUpdater为例说明。

AtomicIntegerFieldUpdater

public class AtomicExample5 {

    //原子性更新某一个类的一个实例
    private static AtomicIntegerFieldUpdater<AtomicExample5> updater
            = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");

    @Getter
    public volatile int count = 100;//必须要volatile标记,且不能是static

    public static void main(String[] args) {
        AtomicExample5 example5 = new AtomicExample5();

        if(updater.compareAndSet(example5,100,120)){
            log.info("update success 1,{}",example5.getCount());
        }

        if(updater.compareAndSet(example5,100,120)){
            log.info("update success 2,{}",example5.getCount());
        }else{
            log.info("update failed,{}",example5.getCount());
        }
    }
}

此方法输出的结果为:
[main] INFO com.superboys.concurrency.example.Atomic.AtomicExample5 - update success 1,120
[main] INFO com.superboys.concurrency.example.Atomic.AtomicExample5 - update failed,120

由此可见,count的值只修改了一次。

AtomicStampReference

什么是ABA问题?

CAS操作的时候,其他线程将变量的值A改成了B,但是随后又改成了A,本线程在CAS方法中使用期望值A与当前变量进行比较的时候,发现变量的值未发生改变,于是CAS就将变量的值进行了交换操作。但是实际上变量的值已经被其他的变量改变过,这与设计思想是不符合的。

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

private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

private volatile Pair<V> pair;

private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&   //排除新的引用和新的版本号与底层的值相同的情况
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
}

解释一下上边的源码:
类中维护了一个volatile修饰的Pair类型变量current,Pair是一个私有的静态类,current可以理解为底层数值。
compareAndSet方法的参数部分分别为期望的引用、新的引用、期望的版本号、新的版本号。
return的逻辑为判断了期望的引用和版本号是否与底层的引用和版本号相符,并且排除了新的引用和新的版本号与底层的值相同的情况(即不需要修改)的情况(return代码部分3、4行)。条件成立,执行casPair方法,调用CAS操作

AtomicStampReference的处理思想是,每次变量更新的时候,将变量的版本号+1,之前的ABA问题中,变量经过两次操作以后,变量的版本号就会由1变成3,也就是说只要线程对变量进行过操作,变量的版本号就会发生更改。从而解决了ABA问题。

synchronized

在synchronized锁中,Java默默在临界区前后加上了lock和unlock方法,好处是,加锁和解锁一定是成对出现了,毕竟如果忘记unlock解锁,意味着其他线程只能死等下去了。

依赖于JVM去实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程对其进行操作的。
synchronized是java中的一个关键字,是一种同步锁。它可以修饰的对象主要有四种:

一、synchronized 修饰一个代码块

被修饰的代码称为同步语句块,作用的范围是大括号括起来的部分。作用的对象是调用这段代码的对象

public class SynchronizedExample {
    public void test(int j){
        synchronized (this){
            for (int i = 0; i < 10; i++) {
                log.info("test - {} - {}",j,i);
            }
        }
    }
    //使用线程池方法进行测试:
    public static void main(String[] args) {
        SynchronizedExample example1 = new SynchronizedExample();
        SynchronizedExample example2 = new SynchronizedExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()-> example1.test(1));
        executorService.execute(()-> example2.test(2));
    }
}

不同对象之间的操作互不影响

二、synchronized 修饰一个方法

被修饰的方法称为同步方法,作用的范围是大括号括起来的部分,作用的对象是调用这段代码的对象

public class SynchronizedExample 
    public synchronized void test(int j){
        for (int i = 0; i < 10; i++) {
            log.info("test - {} - {}",j,i);
        }
    }
    //验证方法与上面相同
    ...
}

结果:不同对象之间的操作互不影响

如果当前类是一个父类,子类调用父类的被synchronized修饰的方法,不会携带synchronized属性,因为synchronized不属于方法声明的一部分

三、synchronized 修饰一个静态方法

作用的范围是synchronized 大括号括起来的部分,作用的对象是这个类的所有对象

public class SynchronizedExample{
    public static synchronized void test(int j){
        for (int i = 0; i < 10; i++) {
            log.info("test - {} - {}",j,i);
        }
    }
    //验证方法与上面相同
    ...
}

结果:同一时间只有一个线程可以执行

四、synchronized 修饰一个类

public class SynchronizedExample{
    public static void test(int j){
        synchronized (SynchronizedExample.class){
            for (int i = 0; i < 10; i++) {
                log.info("test - {}-{}",j,i);
            }
        }
    }
    //验证方法与上面相同
    ...
}

结果:同一时间只有一个线程可以执行

原子性操作方法对比

  • Atomic:竞争激烈时能维持常态,比Lock性能好,每次只能同步一个值
  • synchronized:不可中断锁,适合竞争不激烈,可读性好
  • Lock:可中断锁,多样化同步,竞争激烈时能维持常态

参考链接

https://www.cnblogs.com/senlinyang/p/7856339.html

https://blog.csdn.net/jesonjoke/article/details/79837508#原子性

https://blog.csdn.net/weixin_39267363/article/details/97569304

https://www.jianshu.com/p/7188fe52e9b9

欢迎公众号:Data Porter 免费获取数据结构、Java、Scala、Python、大数据、区块链、机器学习等学习资料。好受不敌双拳,双拳不如四手!希望认识更多的朋友一起成长、共同进步!

发布了86 篇原创文章 · 获赞 69 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/lp284558195/article/details/105282041