学了这么久的高并发编程,连Java中的并发原子类都不知道?

Posted 华为云开发者社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学了这么久的高并发编程,连Java中的并发原子类都不知道?相关的知识,希望对你有一定的参考价值。

摘要:保证线程安全是 Java 并发编程必须要解决的重要问题,本文和大家聊聊Java中的并发原子类,看它如何确保多线程的数据一致性。

本文分享自华为云社区学了这么久的高并发编程,连Java中的并发原子类都不知道?这也太Low了吧》,作者:冰 河。

今天我们一起来聊聊Java中的并发原子类。在 java.util.concurrent.atomic包下有很多支持并发的原子类,某种程度上,我们可以将其分成:基本数据类型的原子类、对象引用类型的原子类、数组类型的原子类、对象属性类型的原子类和累加器类型的原子类 五大类。

接下来,我们就一起来看看这些并发原子类吧。

基本数据类型的原子类

基本数据类型的原子类包含:AtomicBoolean、AtomicInteger和AtomicLong。

打开这些原子类的源码,我们可以发现,这些原子类在使用上还是非常简单的,主要提供了如下这些比较常用的方法。

  • 原子化加1或减1操作
//原子化的i++
getAndIncrement() 
//原子化的i--
getAndDecrement() 
//原子化的++i
incrementAndGet() 
//原子化的--i
decrementAndGet() 
  • 原子化增加指定的值
//当前值+=delta,返回+=前的值
getAndAdd(delta) 
//当前值+=delta,返回+=后的值
addAndGet(delta)
  • CAS操作
//CAS操作,返回原子化操作的结果是否成功
compareAndSet(expect, update)
  • 接收函数计算结果
//结果数据可通过传入func函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)

对象引用类型的原子类

对象引用类型的原子类包含:AtomicReference、AtomicStampedReference和AtomicMarkableReference。

利用这些对象引用类型的原子类,可以实现对象引用更新的原子化。AtomicReference提供的原子化更新操作与基本数据类型的原子类提供的更新操作差不多,只不过AtomicReference提供的原子化操作常用于更新对象信息。这里不再赘述。

需要特别注意的是:使用对象引用类型的原子类,要重点关注ABA问题。

关于ABA问题,文章的最后部分会说明。

好在AtomicStampedReference和AtomicMarkableReference这两个原子类解决了ABA问题。

AtomicStampedReference类中的compareAndSet的方法签名如下所示。

boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) 

可以看到,AtomicStampedReference类解决ABA问题的方案与乐观锁的机制比较相似,实现的CAS方法增加了版本号。只有expectedReference的值与内存中的引用值相等,并且expectedStamp版本号与内存中的版本号相同时,才会将内存中的引用值更新为newReference,同时将内存中的版本号更新为newStamp。

AtomicMarkableReference类中的compareAndSet的方法签名如下所示。

boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark) 

可以看到,AtomicMarkableReference解决ABA问题的方案就更简单了,在compareAndSet方法中,新增了boolean类型的校验值。这些理解起来也比较简单,这里,我也不再赘述了。

对象属性类型的原子类

对象属性类型的原子类包含:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater。

利用对象属性类型的原子类可以原子化的更新对象的属性。值得一提的是,这三个类的对象都是通过反射的方式生成的,如下是三个类的newUpdater()方法。

//AtomicIntegerFieldUpdater的newUpdater方法
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName)
//AtomicLongFieldUpdater的newUpdater方法
public static <U> AtomicLongFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName)
//AtomicReferenceFieldUpdater的newUpdater方法    
public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,
                                                                Class<W> vclass,
                                                                String fieldName)

这里,我们不难看出,在AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater三个类的newUpdater()方法中,只有传递的Class信息,并没有传递对象的引用信息。如果要更新对象的属性,则一定要使用对象的引用,那对象的引用是在哪里传递的呢?

其实,对象的引用是在真正调用原子操作的方法时传入的。这里,我们就以compareAndSet()方法为例,如下所示。

//AtomicIntegerFieldUpdater的compareAndSet()方法
compareAndSet(T obj, int expect, int update) 
//AtomicLongFieldUpdater的compareAndSet()方法
compareAndSet(T obj, long expect, long update) 
//AtomicReferenceFieldUpdater的compareAndSet()方法    
compareAndSet(T obj, V expect, V update) 

可以看到,原子化的操作方法仅仅是多了一个对象的引用,使用起来也非常简单,这里,我就不再赘述了。

另外,需要注意的是:使用AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater更新对象的属性时,对象属性必须是volatile类型的,只有这样才能保证可见性;如果对象属性不是volatile类型的,newUpdater()方法会抛出IllegalArgumentException这个运行时异常。

数组类型的原子类

数组类型的原子类包含:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray。

利用数组类型的原子类可以原子化的更新数组里面的每一个元素,使用起来也非常简单,数组类型的原子类提供的原子化方法仅仅是在基本数据类型的原子类和对象引用类型的原子类提供的原子化方法的基础上增加了一个数组的索引参数。

例如,我们以compareAndSet()方法为例,如下所示。

//AtomicIntegerArray的compareAndSet()方法
compareAndSet(int i, int expect, int update) 
//AtomicLongArray的compareAndSet()方法
compareAndSet(int i, long expect, long update)     
//AtomicReferenceArray的compareAndSet()方法   
compareAndSet(int i, E expect, E update) 

可以看到,原子化的操作方法仅仅是对多了一个数组的下标,使用起来也非常简单,这里,我就不再赘述了。

累加器类型的原子类

累加器类型的原子类包含:DoubleAccumulator、DoubleAdder、LongAccumulator和LongAdder。

累加器类型的原子类就比较简单了:仅仅支持值的累加操作,不支持compareAndSet()方法。对于值的累加操作,比基本数据类型的原子类速度更快,性能更好。

使用原子类实现count+1

在并发编程领域,一个经典的问题就是count+1问题。也就是在高并发环境下,如何保证count+1的正确性。一种方案就是在临界区加锁来保护共享变量count,但是这种方式太消耗性能了。

如果使用Java提供的原子类来解决高并发环境下count+的问题,则性能会大幅度提升。

简单的示例代码如下所示。

public class IncrementCountTest
    private  AtomicLong count = new AtomicLong(0);
    public void incrementCountByNumber(int number)
        for(int i = 0; i < number; i++)
            count.getAndIncrement();
        
    

可以看到,原子类实现count+1问题,既没有使用synchronized锁,也没有使用Lock锁。

从本质上讲,它使用的是无锁或者是乐观锁方案解决的count+问题,说的具体一点就是CAS操作。

CAS原理

CAS操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。

如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。

无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)

简单点理解就是:位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只返回位置V现在的值。这其实和乐观锁的冲突检测+数据更新的原理是一样的。

ABA问题

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供的AtomicStampedReference类和AtomicMarkableReference类能够解决CAS的ABA问题。

关于AtomicStampedReference类和AtomicMarkableReference类前文有描述,这里不再赘述。

 

点击关注,第一时间了解华为云新鲜技术~

Java Review - 并发编程_原子操作类原理剖析

文章目录


概述

JUC包提供了一系列的原子性操作类,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作这在性能上有很大提高。

由于原子性操作类的原理都大致相同,我们以AtomicLong类的实现原理为例,并探讨JDK8新增的 LongAdder和LongAccumulator类的原理

原子变量操作类

JUC并发包中包含有AtomicInteger、AtomicLong和AtomicBoolean等原子性操作类

AtomicLong是原子性递增或者递减类,其内部使用Unsafe来实现,我们看下面的代码


package java.util.concurrent.atomic;
import java.util.function.LongUnaryOperator;
import java.util.function.LongBinaryOperator;
import sun.misc.Unsafe;

/**
 * @since 1.5
 * @author Doug Lea
 */
public class AtomicLong extends Number implements java.io.Serializable 
    private static final long serialVersionUID = 1927816293512124184L;

    // 1 获取Unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
	
	// 2 存放变量的偏移量
    private static final long valueOffset;

    /**
     * Records whether the underlying JVM supports lockless
     * compareAndSwap for longs. While the Unsafe.compareAndSwapLong
     * method works in either case, some constructions should be
     * handled at Java level to avoid locking user-visible locks.
     */
	// 3 判断JVM是否支持Long类型的CAS
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();

    /**
     * Returns whether underlying JVM supports lockless CompareAndSet
     * for longs. Called only once and cached in VM_SUPPORTS_LONG_CAS.
     */
    private static native boolean VMSupportsCS8();

    static 
        try 
			// 4 获取value在AtomicLong中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
         catch (Exception ex)  throw new Error(ex); 
    
	
	// 5  实际变量值
    private volatile long value;

    /**
     * Creates a new AtomicLong with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicLong(long initialValue) 
        value = initialValue;
    
	
	.......
	.......
	.......
	.......
 

  • 代码(1)通过Unsafe.getUnsafe()方法获取到Unsafe类的实例

    为何能通过Unsafe.getUnsafe()方法获取到Unsafe类的实例?其实这是因为AtomicLong类也是在rt.jar包下面的,AtomicLong类就是通过BootStarp类加载器进行加载的。

  • 代码(5)中的value被声明为volatile的,这是为了在多线程下保证内存可见性,value是具体存放计数的变量。

  • 代码(2)(4)获取value变量在AtomicLong类中的偏移量。


主要方法

incrementAndGet 、decrementAndGet 、getAndIncrement、getAndDecrement

【JDK8+】

//(6)调用 Insafe方法,原子性设置 value值为原始值+1,返回值为递增后的值
	public final long incrementAndGet() 
		return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
	


//(7)调用 unsafe方法,原子性设置va1ue值为原始值-1,返回值为递减之后的值
 public final long decrementAndGet() 
        return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
    
//(8)调用 unsafe方法,原子性设置va1ue值为原始值+1,返回值为原始值
public final long getAndIncrement() 
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    
//(9)调用 unsafe方法,原子性设置va1ue值为原始值-1,返回值为原始值
  public final long getAndDecrement() 
        return unsafe.getAndAddLong(this, valueOffset, -1L);
    

我们可以发现这几个方法内部都是通过调用Unsafe的getAndAddLong方法来实现操作,这个函数是个原子性操作。

第一个参数是AtomicLong实例的引用, 第二个参数是value变量在AtomicLong中的偏移值, 第三个参数是要设置的第二个变量的值 。

getAndIncrement方法在JDK 7中的实现逻辑为

public final long getAndIncrement()
	while(true)
		long current=get();
		long next= current + 1;
		if (compareAndSet(current, next))
			return current
	

在如上代码中,每个线程是先拿到变量的当前值(由于value是volatile变量,所以这里拿到的是最新的值),然后在工作内存中对其进行增1操作,而后使用CAS修改变量的值。如果设置失败,则循环继续尝试,直到设置成功。

而JDK 8中的逻辑为

unsafe.getAndAddLong(this, valueOffset, -1L);
public final long getAndAddLong(Object var1, long var2, long var4) 
        long var6;
        do 
            var6 = this.getLongVolatile(var1, var2);
         while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    

可以看到,JDK 7的AtomicLong中的循环逻辑已经被JDK 8中的原子操作类UNsafe内置了,之所以内置应该是考虑到这个函数在其他地方也会用到,而内置可以提高复用性。


boolean compareAndSet(long expect, long update)

    /**
     * Atomically sets the value to the given updated value
     * if the current value @code == the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return @code true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(long expect, long update) 
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    

我们可以看到内部还是调用了unsafe.compareAndSwapLong方法。如果原子变量中的value值等于expect,则使用update值更新该值并返回true,否则返回false。


小Demo

线程使用AtomicLong统计0的个数的例子


import java.util.concurrent.atomic.AtomicLong;

/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/11/30 22:52
 * @mark: show me the code , change the world
 */
public class AtomicLongTest 

    //(10)创建Long型原子计数器
    private static AtomicLong atomicLong = new AtomicLong();

    //(11)创建数据源
    private static Integer[] arrayOne = new Integer[]0, 1, 2, 3, 0, 5, 6, 0, 56, 0;

    private static Integer[] arrayTwo = new Integer[]10, 1, 2, 3, 0, 5, 6, 0, 56, 0;

    public static void main(String[] args) throws InterruptedException 
        //(12)线程one统计数组arrayOne中0的个数
        Thread threadOne = new Thread(() -> 
            int size = arrayOne.length;
            for (int i = 0; i < size; ++i) 
                if (arrayOne[i].intValue() == 0) 
                    atomicLong.incrementAndGet();
                
            

        );
        //(13)线程two统计数组arrayTwo中0的个数
        Thread threadTwo = new Thread(() -> 
            int size = arrayTwo.length;
            for (int i = 0; i < size; ++i) 
                if (arrayTwo[i].intValue() == 0) 
                    atomicLong.incrementAndGet();
                
            
        );
        //(14)启动子线程
        threadOne.start();
        threadTwo.start();
        //(15)等待线程执行完毕
        threadOne.join();
        threadTwo.join();
        System.out.println("count 0:" + atomicLong.get());
    


    


两个线程各自统计自己所持数据中0的个数,每当找到一个0就会调用AtomicLong的原子性递增方法


小结

在没有原子类的情况下,实现计数器需要使用一定的同步措施,比如使用synchronized关键字等,但是这些都是阻塞算法,对性能有一定损耗,而这里我们介绍的这些原子操作类都使用CAS非阻塞算法,性能更好。

但是在高并发情况下AtomicLong还会存在性能问题。JDK 8提供了一个在高并发下性能更好的LongAdder类,且听下篇分解

以上是关于学了这么久的高并发编程,连Java中的并发原子类都不知道?的主要内容,如果未能解决你的问题,请参考以下文章

Java Review - 并发编程_原子操作类原理剖析

Java Review - 并发编程_原子操作类原理剖析

并发编程Java中的原子操作

JAVA的高并发编程

JAVA的高并发编程

Java并发编程之验证volatile不能保证原子性