java并发编程十 原子累加器和Unsafe

news2024/11/20 7:41:29

文章目录

    • 原子累加器
      • cas 锁
      • 原理之伪共享
    • Unsafe
      • Unsafe CAS 操作

原子累加器

累加器性能比较

private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
 T adder = adderSupplier.get();
 long start = System.nanoTime();
 List<Thread> ts = new ArrayList<>();
 // 4 个线程,每人累加 50 万
for (int i = 0; i < 40; i++) {
 ts.add(new Thread(() -> {
 for (int j = 0; j < 500000; j++) {
 action.accept(adder);
            }
        }));
    }
 ts.forEach(t -> t.start());
 ts.forEach(t -> {
 try {
 t.join();
        } 
catch (InterruptedException e) {
 e.printStackTrace();
        }
    });
 long end = System.nanoTime();
 System.out.println(adder + " cost:" + (end - start)/1000_000);
 }

比较 AtomicLong 与 LongAdder

 for (int i = 0; i < 5; i++) {
 demo(() -> new LongAdder(), adder -> adder.increment());
 }
 for (int i = 0; i < 5; i++) {
 demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
 }

输出

1000000 cost:43 
1000000 cost:9 
1000000 cost:7 
1000000 cost:7 
1000000 cost:7 
1000000 cost:31 
1000000 cost:27 
1000000 cost:28 
1000000 cost:24 
1000000 cost:22 

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性
能。

  • 源码之 LongAdder
    LongAdder 是并发大师 @author Doug Lea 的作品,设计的非常精巧
    LongAdder 类有几个关键域
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
 // 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
 // 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;

cas 锁

// 不要用于实践!!!
public class LockCas {
 private AtomicInteger state = new AtomicInteger(0);
 public void lock() {
 while (true) {
 if (state.compareAndSet(0, 1)) {
 break;
            }
        }
    }
     public void unlock() {
 log.debug("unlock...");
 state.set(0);
    }
}

测试

LockCas lock = new LockCas();
 new Thread(() -> {
 log.debug("begin...");
 lock.lock();
 try {
 log.debug("lock...");
 sleep(1);
    } 
finally {
 lock.unlock();
    }
 }).start();
 new Thread(() -> {
 log.debug("begin...");
 lock.lock();
 try {
 log.debug("lock...");
    } 
finally {
 lock.unlock();
    }
 }).start();

输出

18:27:07.198 c.Test42 [Thread-0] - begin... 
18:27:07.202 c.Test42 [Thread-0] - lock... 
18:27:07.198 c.Test42 [Thread-1] - begin... 
18:27:08.204 c.Test42 [Thread-0] - unlock... 
18:27:08.204 c.Test42 [Thread-1] - lock... 
18:27:08.204 c.Test42 [Thread-1] - unlock... 

原理之伪共享

其中 Cell 即为累加单元

// 防止缓存行伪共享
@sun.misc.Contended 
static final class Cell {
 volatile long value;
 Cell(long x) { value = x; }
 // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
final boolean cas(long prev, long next) {
 return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
    }
 // 省略不重要代码
}

得从缓存说起
缓存与内存的速度比较
在这里插入图片描述

从 cpu 到大约需要的时钟周期
寄存器1 cycle (4GHz 的 CPU 约为0.25ns)
L13~4 cycle
L210~20 cycle
L340~45 cycle
内存120~240 cycle

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
在这里插入图片描述

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]
    论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效
    @sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
    在这里插入图片描述

累加主要调用下面的方法

public void add(long x) {
 // as 为累加单元数组
// b 为基础值
// x 为累加值
Cell[] as; long b, v; int m; Cell a;
 // 进入 if 的两个条件
// 1. as 有值, 表示已经发生过竞争, 进入 if
// 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
 if ((as = cells) != null || !casBase(b = base, b + x)) {
 // uncontended 表示 cell 没有竞争
boolean uncontended = true;
 if (
 // as 还没有创建
as == null || (m = as.length - 1) < 0 ||
 // 当前线程对应的 cell 还没有
            (a 
= as[getProbe() & m]) == null ||
 // cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
 !(uncontended = a.cas(v = a.value, v + x))
        ) {
         // 进入 cell 数组创建、cell 创建的流程
         longAccumulate(x, null, uncontended);
        }
    }
 }

add 流程图
在这里插入图片描述

final void longAccumulate(long x, LongBinaryOperator fn,
 boolean wasUncontended) {
 int h;
 // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
 if ((h = getProbe()) == 0) {
 // 初始化 probe
 ThreadLocalRandom.current();
 // h 对应新的 probe 值, 用来对应 cell
 h = getProbe();
 wasUncontended = true;
    }
 // collide 为 true 表示需要扩容
boolean collide = false;                
for (;;) {
 Cell[] as; Cell a; int n; long v;
 // 已经有了 cells
 if ((as = cells) != null && (n = as.length) > 0) {
 // 还没有 cell
 if ((a = as[(n - 1) & h]) == null) {
 // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
 // 成功则 break, 否则继续 continue 循环
            }
 // 有竞争, 改变线程对应的 cell 来重试 cas
 else if (!wasUncontended)
 wasUncontended = true;
 // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
 else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
 break;
 // 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
 else if (n >= NCPU || cells != as)
 collide = false;
 // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
else if (!collide)
 collide = true;
 // 加锁
else if (cellsBusy == 0 && casCellsBusy()) {
 // 加锁成功, 扩容
continue;
            }
 // 改变线程对应的 cell
 h = advanceProbe(h);
        }
 // 还没有 cells, 尝试给 cellsBusy 加锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
 // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
 // 成功则 break;
        }
         // 上两种情况失败, 尝试给 base 累加
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
 break;
   }
 }

longAccumulate 流程图
在这里插入图片描述

在这里插入图片描述

每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)
在这里插入图片描述

获取最终结果通过 sum 方法

public long sum() {
 Cell[] as = cells; Cell a;
 long sum = base;
 if (as != null) {
 for (int i = 0; i < as.length; ++i) {
 if ((a = as[i]) != null)
 sum += a.value;
        }
    }
 return sum;
 }

Unsafe

概述
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

public class UnsafeAccessor {
 static Unsafe unsafe;
 static {
 try {            
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
 theUnsafe.setAccessible(true);
 unsafe = (Unsafe) theUnsafe.get(null);
        } 
catch (NoSuchFieldException | IllegalAccessException e) {
 throw new Error(e);
        }
    }
    static Unsafe getUnsafe() {
 return unsafe;
    }
 }

Unsafe CAS 操作

@Data
 class Student {
 volatile int id;
 volatile String name;
 }
 Unsafe unsafe = UnsafeAccessor.getUnsafe();
 Field id = Student.class.getDeclaredField("id");
 Field name = Student.class.getDeclaredField("name");
 // 获得成员变量的偏移量
long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
 long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
  Student student = new Student();
 // 使用 cas 方法替换成员变量的值
UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20);  // 返回 true
 UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
 System.out.println(student);

输出

Student(id=20, name=张三) 

使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现

class AtomicData {
 private volatile int data;
 static final Unsafe unsafe;
 static final long DATA_OFFSET;
 static {
 unsafe = UnsafeAccessor.getUnsafe();
 try {
 // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
        } 
catch (NoSuchFieldException e) {
 throw new Error(e);
        }
    }
 public AtomicData(int data) {
 this.data = data;
    }
    public void decrease(int amount) {
 int oldValue;
 while(true) {
 // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
oldValue = data;
 // cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false
 if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
 return;
            }
        }
    }
 public int getData() {
 return data;
    }
 }

Account 实现

Account.demo(new Account() {
 AtomicData atomicData = new AtomicData(10000);
 @Override
 public Integer getBalance() {
 return atomicData.getData();
    }
 @Override
 public void withdraw(Integer amount) {
 atomicData.decrease(amount);
    }
 });

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1332004.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ubuntu20.04安装timeshift最新方法

总结&#xff1a; 现在可以使用如下代码安装 sudo apt-get update sudo apt-get install timeshift原因&#xff1a; 在尝试Timeshift系统备份与还原中的方法时&#xff0c; sudo apt-add-repository -y ppa:teejee2008/ppa运行失败。 更改为以下代码&#xff1a; sudo a…

【PHY6222】绑定详解

1.函数详解 bStatus_t GAPBondMgr_SetParameter( uint16 param, uint8 len, void* pValue ) 设置绑定参数。 bStatus_t GAPBondMgr_GetParameter( uint16 param, void* pValue ) 获取绑定参数。 param&#xff1a; GAPBOND_PAIRING_MODE&#xff0c;配对模式&#xff0c;…

第4章 | 安徽某高校《统计建模与R软件》期末复习

第4章 参数估计 参数估计是统计建模的关键步骤之一&#xff0c;它涉及根据样本数据推断总体参数的过程。在统计学中&#xff0c;参数通常用于描述总体的特征&#xff0c;如均值、方差等。通过参数估计&#xff0c;我们可以利用样本信息对这些未知参数进行推断&#xff0c;从而…

LDO频率补偿

频率补偿 为了维持系统稳定的条件&#xff0c;一般的做法是建立一个低频几点&#xff0c;并把第二个极点放在单位增益频率 f0db 附近。在线性稳压器中&#xff0c;这两个极点是输出极点Po和误差放大器极点Pe。在确定了哪一个极点应该是主极点后&#xff0c;补偿的目的就是理解系…

【FPGA】Verilog 实践:优先级编码器 | Priority encoder

0x00 优先级编码器&#xff08;Priority encoder&#xff09; "能将多个二进制输入压缩成更少数目输出的电路或算法的编码器" 优先级编码器是一种编码器&#xff0c;它考虑了两个或更多输入位同时变为 1 但没有收到输入的情况。当输入进来时&#xff0c;优先级编码…

Flink实时电商数仓(五)

FlinkSQL的join Regular join普通join&#xff0c;两条流的数据都时存放在内存的状态中&#xff0c;如果两条流数据都很大&#xff0c;对内存压力很大。Interval Join: 适合两条流到达时间有先后关系的&#xff1b;一条流的存活时间短&#xff0c;一条流的存活时间长。Lookup …

使用office打开word文档时候提示错误:0x426-0x0的解决方案

在使用office打开word文档时候提示错误&#xff1a;0x426-0x0。如下图&#xff1a; 昨天还用的好好的&#xff0c;怎么今天就不行了&#xff1f;为什么呢&#xff1f; 更多工作中遇到问题见&#xff1a;凯哥BK 这个错误导致office无法启动通常是由于office软件所依赖的服务无…

[工具]java_sublime的快速使用

目录 使用 : 怎么运行: 调整字体: 使用 : 新建--->写好代码后-->另存为尾缀是.java的文件 怎么运行: 在你另存为的目录下cmd调用控制台输入dos指令--->执行javac 文件名.java(有.java尾缀)(编译为.class文件)--->java 文件名(没有.class尾缀设计者认为执行的是…

【Spring实战】04 Lombok集成及常用注解

文章目录 0. 集成1. Data2. Getter 和 Setter3. NoArgsConstructor&#xff0c;AllArgsConstructor和RequiredArgsConstructor4. ToString5. EqualsAndHashCode6. NonNull7. Builder总结 Lombok 是一款 Java 开发的工具&#xff0c;它通过注解的方式简化了 Java 代码的编写&…

INFINI Gateway 如何防止大跨度查询

背景 业务每天生成一个日期后缀的索引&#xff0c;写入当日数据。 业务查询有时会查询好多天的数据&#xff0c;导致负载告警。 现在想对查询进行限制–只允许查询一天的数据&#xff08;不限定是哪天&#xff09;&#xff0c;如果想查询多天的数据就走申请。 技术分析 在每…

面试题:JVM 对锁都进行了哪些优化?

文章目录 锁优化自旋锁和自适应自旋锁消除锁粗化逃逸分析方法逃逸线程逃逸通过逃逸分析&#xff0c;编译器对代码的优化 锁优化 jvm 在加锁的过程中&#xff0c;会采用自旋、自适应、锁消除、锁粗化等优化手段来提升代码执行效率。 自旋锁和自适应自旋 现在大多的处理器都是…

OpenSource - SCM服务管理平台

文章目录 官方网址文档下载版本功能解决了哪些问题使用对象优势Linxu版本scm-dev deb服务列表 Windows版本scm-dev 服务列表scm-all 服务列表scm-jdk 服务列表scm-springboot 精简版本服务列表scm-springboot 服务列表scm-tomcat 服务列表 SCM 截图 官方网址 https://scm.chus…

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《计及风电不确定性的多场景多时段安全约束机组组合解耦求解方法》

这个标题涉及到一种解决在能源系统中考虑风电不确定性的方法。让我们逐步分解这个标题&#xff0c;以便更好地理解其含义&#xff1a; 计及风电不确定性&#xff1a; 这指的是在能源系统中&#xff0c;风力发电的产出具有不确定性。因为风速是难以预测的&#xff0c;风力发电的…

SpringBoot 3 集成Hive 3

前提条件: 运行环境&#xff1a;Hadoop 3.* Hive 3.* MySQL 8 &#xff0c;如果还未安装相关环境&#xff0c;请参考&#xff1a;Hive 一文读懂 Centos7 安装Hadoop3 单机版本&#xff08;伪分布式版本&#xff09; SpringBoot 2 集成Hive 3 pom.xml <?xml ver…

Yestar成都艺星舒适热玛吉星品沙龙会在蓉成功发布

12月21日&#xff0c;由Yestar成都艺星联合索塔医疗联合举办的“舒适无痛热玛吉星品沙龙会”在院内圆满举行&#xff0c;索塔医疗西区大客户经理肖峰、中国临床事业部刘颖&#xff0c;成都艺星运营部长程燕佳&#xff0c;皮肤科院长朱紫婷、技术院长杨海皎、主任王小琴&#xf…

小程序radio单选框回显

话不多说&#xff0c;效果图如下&#xff1a; 具体代码如下&#xff1a; <radio-group name"radio" bindchange"getSex"><label><radio value"1" checked"{{xingbie1}}" />男</label><label><radio…

3D数字化系统建设

以3D可视化、数字化技术为基础&#xff0c;其实&#xff0c;很多传统的系统软件都可以重新做一下。 比如&#xff1a;以下这个使用场景&#xff1a;零售门店陈列&#xff1b; 还有&#xff0c;数字化仓储系统&#xff0c;3D数字化供应链系统&#xff0c;3D数字化的生产系统&a…

网络首发:MTF-CNN-Attention故障识别: 基于马尔可夫场(MTF)和卷积网络(CNN)融合注意力机制的故障识别程序

适用平台&#xff1a;Matlab2023版本及以上 本程序参考中文EI期刊《电网技术》网络首发文献&#xff1a;《基于马尔可夫转换场与多头注意力机制的电能质量扰动分类方法》&#xff0c;程序注释清晰&#xff0c;干货满满&#xff0c;下面对文章和程序做简要介绍&#xff01; 文献…

实验三:路由器的管理与配置

实验目的&#xff1a; 了解路由器的作用熟悉路由器的基本配置方法熟悉Packet Tracer 路由模拟软件的使用 实验环境&#xff1a; Windows 10 下的 Cisco Packet Tracer 实验内容&#xff1a; 熟悉Packet Tracer组件搭建实验拓扑 实验步骤&#xff1a; 1.连接设备 2.配置IP地…

柯桥外语学习-俄语零基础入门教学之与衣服有关的词汇

本期为大家带来的是与衣物有关的相关词汇&#xff01; 最近全国大范围降温&#xff0c;大家一定要关注天气预告及时增减衣物&#xff0c;小心不要感冒啦~ 一、服装组成部分 领子 воротник 方领 квадрадный воротник 圆领 закругленн…