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


目录
相关文章
|
存储 C语言
【c语言指针详解】指针的基本概念和用法
【c语言指针详解】指针的基本概念和用法
499 0
|
存储 弹性计算 NoSQL
libcuckoo论文概述
本文简要阐述libcuckoo项目的两篇论文基础。如有错漏之处,欢迎指出一起讨论交流。 ## 论文1 《MemC3: Compact and Concurrent MemCache with Dumber Caching and Smarter Hashing》 这篇论文主要讲了在多线程模式下如何提升cuckoo hash table的吞吐。 ### 问题 传统hash表在并发效率上并不
2022 0
libcuckoo论文概述
|
6月前
|
前端开发 JavaScript Java
Java中将图片转换为base64格式的技巧
这样,你就可以在Java中将图片转换为Base64格式了。这个方法的实现非常简单,只需要使用Java的内置库,无需任何额外的库。希望这个方法对你有所帮助。
374 22
|
12月前
|
JavaScript 容器
乾坤qiankun框架搭建 主应用为vue3的项目。
乾坤qiankun框架搭建 主应用为vue3的项目。
624 2
|
算法 Java Go
Go vs Java:内存管理与垃圾回收机制对比
对比了Go和Java的内存管理与垃圾回收机制。Java依赖JVM自动管理内存,使用堆栈内存并采用多种垃圾回收算法,如标记-清除和分代收集。Go则提供更多的手动控制,内存分配与释放由分配器和垃圾回收器协同完成,使用三色标记算法并发回收。示例展示了Java中对象自动创建和销毁,而Go中开发者需注意内存泄漏。选择语言应根据项目需求和技术栈来决定。
|
JSON 前端开发 JavaScript
Long类型字段在前后端传值问题
Long类型字段在前后端传值问题
534 0
nrm 安装后报错 Error [ERR_REQUIRE_ESM]: require() of ES Module
nrm 安装后报错 Error [ERR_REQUIRE_ESM]: require() of ES Module
1072 0
|
Java DataX 开发工具
datax-elasticsearch 同步踩坑记录
为了用datax同步es数据到其他地方,踩了不少坑.记录一下.
748 0
|
索引
Elasticsearch update_by_query 语句使用记录
Elasticsearch update_by_query 语句使用记录
547 0