Atomic 类

简介: Atomic 类

                      整个Concurrent 包的层次体系

1、AtomicInteger 和 AtomicLong

1.1 悲观锁和乐观锁

悲观锁:数据发生并发冲突的概率很大,所以读操作之前就加上锁。synchronized关键字和ReentrantLock 都是悲观锁的典型例子。

乐观锁:数据发生并发冲突的概率很小,所以读操作之前不上锁。等到写操作的时候,再判断数据在此期间是否被其他线程修改了。如果被其他线程修改了,就把数据重新读出来,重复该过程;如果没有被修改,就写回去。判断数据是否被修改,同时写回新值,这两个操作要合成一个原子操作,也就是CAS(Compare And Set)。

AtomicInteger 就是典型的乐观锁


1.2 unsafe 的 cas 详解

public final boolean compareAndSet(int expect,int update){
    return unsafe.compareAndSwapInt(this, valueOffset, expect,update);
}

AtomicInteger 封装过的compareAndSet 有两个参数。第一个参数expect是指变量的旧值(是读出来的值,写回去的时候,希望没有被其他线程修改);第二个参数update 是指变量的新值(修改过的,没有写入的值)。当expect变量等于当前变量时,说明在修改的期间,没有其他线程对此变量进行修改,所以可以成功写入,变量被更新为update,返回true;否则返回false;

public final native boolean compareAndSwapInt(Object var1,long var2,int var4,int var5);

  该函数有4个参数,第一个是对象(AtomicInteger 变量);第二个参数是对象的成员变量(AtomicInteger 里面包的int 变量value),它是long型整数,常被称为xxxOffset,意思是某个成员变量在对应类中的内存偏移量,表示该成员变量本身。

var1 atomicInteger 变量

var2
xxxOfffset
某个成员变量在对应类中的内存偏移量, 表示该成员变量本身

   无论Unsafe还是valueOffset,都是静态的,也就是类级别的,所有对象共用的。

   在转化的时候,先通过反射(getDeclaredField)获取value成员变量对应的Field对象,再通过objectFieldOffset函数转换成valueOffset。此处的valueOffset就代表了value变量本身,后面执行CAS操作的时候,不是直接操作value,而是操作valueOffset。


1.3 自旋与阻塞

当一个线程拿不到锁的时候,有两种基本的等待策略;

策略1:放弃CPU,进入阻塞状态,等待后续被唤醒,再重新被操作系统调度

策略2:不放弃CPU,空转,不断重试,也就是所谓的“自旋”。

这两种策略不是互斥的,可以结合使用。


2   AtomicBoolean 和 AtomicReference

2.1 为什么需要AtomicBoolean

if(flag == false){
    flag = true;
    ......
}

实现compare 和 set 两个操作合在一起的原子性,这也是CAS提供的功能。

if(compareAndSet(false,true)){
   ......
}

AtomicReference:

public final boolean compareAndSet(V expect, V update){
    return unsafe.compareAndSwapObject(this,valueOffset,expect,update);
}

2.2  如何支持boolean 类型

     对于用int类型来代替的,在入参的时候,将boolean类型转换为int类型;在返回值的时候,将int类型转换成boolean类型。


3  AtomicStampedReference  和 AtomicMarkanle

3.1  ABA问题与解决方法

   CAS 都是基于“值”来做比较。但如果另外一个线程把变量的值从A改为B,再从B改回到A,尽管修改过两次,可是在当前线程做CAS操作的时候,却会因为值没变而认为数据没有被其他线程修改过,这就是所谓的ABA问题。

解决方法:不仅比较值,还要比较“版本号”


3.2  为什么没有AtomicStampedInteger  AtomicStampedLong

    因为要同时比较"值"和"版本号",而Integer 型或者Long型的CAS没有办法同时比较这两个变量,于是只能把值和版本号封装成一个对象,然后通过对象引用的CAS来实现。


3.3 AtomicMarkableReference

    Pair里面的版本号是boolean 类型的,而不是整型的累加变量。因为是boolean类型,只能有true、false两个版本号,所以并不能完全避免ABA问题,只是降低了ABA发生的概率。


3.4 为什么需要AtomicXXXFieldUpdater (AtomicIntegerUpdater,AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater)

     如果一个类是自己编写的,则可以在编写的时候把成员变量定义为Atomic类型。如果一个已有的类,在不能更改其源码的情况下,要想实现对其成员变量的原子操作,就需要AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater。


3.5 限制条件

    要想使用AtomicIntegerFieldUpdater 修改成员变量,成员变量必须是volatile 的int 类型(不能是Integer 包装类)


3.6 AtomicIntegerArray,AtomicLongArray 和 AtomicReferenceArray

    Concurrent包提供了AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 三个数组元素的原子操作。住:并不是说对整个数组的操作是原子的,而是针对数组中一个元素的原子操作而言。

3.6.1  使用方法

     相比于AtomicInteger的getAndIncrement()函数,这里只是多了一个传入参数:数组的下标 i

public final boolean compareAndSet(int i,int expect,int update){...}
public final int getAndDecrement(int i){...}
public final int getAndSet(int i,int newValue){...}

3.6.2  实现原理

    其底层的CAS函数用的还是 compareAndSwapInt,但是把数组下标 i 转换成对应的内存偏移量,所用的方法和之前的AtomicInteger 不太一样。

public static long byteOffset(int i){
    return ((long) i << shift) + base;
} 

 把下标 i 转换成对应的内存地址, 用到了shift 和 base 两个变量。这两个变量都是AtomicIntegerArray 的静态成员变量,用Unsafe 类的arrayBaseOffset 和 arrayIndexScale 两个函数来获取。

    base 表示数组的首地址的位置,scale表示一个数组元素的大小,i 的偏移量则等于 i * scale + base  但为了优化性能,使用了位移操作,shift 表示scale中 1的位置(scale是2的整数次方)。所以,偏移量的计算变成上面代码中的:i << shift + base,表达的意思就是:

i * scale + base


4  Striped64 与 LongAdder

                         Striped64 相关的类的继承层次

4.1 LongAdder 原理

     AtomicLong 内部是一个volatile long 型变量, 由多个线程对这个变量进行CAS操作。多个线程同时对一个变量进行CAS操作,在高并发的场景下仍不够快,如果再要提高性能,怎么做?

     把一个变量拆分成多份,变为多个变量,有些类似于ConcurrentHashMap 的分段锁的例子。把一个Long型拆分成一个base变量外加多个cell,每个Cell 包装成一个Long型变量。当多个线程并发累加的时候,如果并发度低,就直接加到base变量上;如果并发度高,冲突大,平摊到这些Cell 上。最后取值的时候,再把base和这些Cell 求 sum 运算。

4.2 最终一致性

    在sum 求和函数中, 并没有对 cells[] 数组加锁。也就是说,一边有线程对其执行求和操作,一边还有线程修改数组里的值,也就是最终一致性,而不是强一致性。

4.3 伪共享于缓存行填充

     @sun.misc.Contended

    每个CPU都有自己的缓存。缓存与主内存进行数据交换的基本单位叫Cache Line(缓存行)。要刷新到主内存的时候,最少刷新64字节。

4.4 LongAdder核心实现
    当一个线程调用add(x) 的时候,首先会尝试使用casBase 把x 加到base变量上。如果不成功,则再用a.case(...) 函数尝试把x加到 Cell 数组的某个元素上。如果还不成功,最后再调用longAccumulate(..)函数

    注:Cell[] 数组的大小始终是2的整数次方, 在运行中会不断扩容,每次扩容都长2倍。

4.5 LongAccumulator

    LongAccumulator 与 LongAdder 构造函数对比:

public LongAdder(){...}
public LongAccumulator(LongBinaryOperator accumulatorFunction,long identity){...}
public interface LongBinaryOperator{
   long applyAsLong(long left,long right);
}
  • LongAdder 只能进行累加操作,并且初始值默认为0;LongAccumulator 可以自己定义一个二元操作符,并且传入一个初始值。操作符的左值,就是base变量或者Cells[]中元素的当前值;右值,就是add()函数传入的参数x。
  • LongAccumulator 的 accumulate(x) 函数与 LongAdder的 add(x) 函数类似,最后都是调用Striped64的LongAccumulate(...)函数。唯一的差别是LongAdder 的 add(x) 函数调用的是casBase(b,b+x),LongAccumulator 调用的是casBase(b,r),其中,r = function.applyAsLong(b,base,x)。


目录
相关文章
|
6天前
|
存储 安全 C++
C++ 原子变量atomic variable
原子变量是 C++11 引入的一种同步机制,用于多线程环境中的无锁、线程安全操作。其操作不可分割,避免了数据竞争和不一致问题。原子变量位于 `&lt;atomic&gt;` 头文件中,支持多种类型如 `std::atomic&lt;T&gt;` 和特化类型。基本用法包括定义原子变量、加载、存储、交换及比较交换操作。内存顺序(如 `std::memory_order_seq_cst`)用于控制内存访问顺序和可见性,适用于不同场景。原子变量常用于线程安全的计数器和标志位等。
|
16天前
|
安全 Java API
Java多线程编程:使用Atomic类实现原子操作
在Java多线程环境中,共享资源的并发访问可能导致数据不一致。传统的同步机制如`synchronized`关键字或显式锁虽能保障数据一致性,但在高并发场景下可能导致线程阻塞和性能下降。为此,Java提供了`java.util.concurrent.atomic`包下的原子类,利用底层硬件的原子操作确保变量更新的原子性,实现无锁线程安全。
12 0
|
4月前
|
安全 Java vr&ar
Atomic原子类总结
Atomic原子类总结
|
4月前
|
存储 安全 算法
【C++ 包装器类 std::atomic 】全面入门指南:深入理解并掌握C++ std::atomic 原子操作 的实用技巧与应用
【C++ 包装器类 std::atomic 】全面入门指南:深入理解并掌握C++ std::atomic 原子操作 的实用技巧与应用
263 1
|
4月前
|
存储 C语言 C++
std::atomic 相关接口(来自cppreference.com)
std::atomic 相关接口(来自cppreference.com)
49 0
|
调度 C++
C++11之线程库(Thread、Mutex、atomic、lock_guard、同步)
C++11之线程库(Thread、Mutex、atomic、lock_guard、同步)
176 0
并发编程(八)Unsafe&Atomic
并发编程(八)Unsafe&Atomic
72 0
|
安全 算法 Java
JUC 包中的 Atomic 原子类总结
根据操作的数据类型,可以将JUC包中的原子类分为5类,基本类型 使用原子的方式更新基本类型,数组类型 使用原子的方式更新数组里的某个元素,引用类型,对象的属性修改类型,JDK1.8新增。
JUC 包中的 Atomic 原子类总结