无锁
CAS
1、Compare and Swap / Compare And Set
2、底层:lock cmpxchg 指令(X86 架构)
(1)在单核 CPU、多核 CPU 下,都能够保证 CAS 的原子性
(2)在多核状态下,某个核执行到带 lock 的指令时,CPU 会锁住总线,当这个核把此指令执行完毕,再开启总线,过程中不会被线程的调度机制所打断,保证多线程对内存操作的准确性、原子性
3、volatile
(1)获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰
(2)修饰成员变量、静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存,即一个线程对 volatile 变量的修改,对另一个线程可见
(3)只保证共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
(4)CAS 必须借助 volatile 才能读取到共享变量的最新值,实现 CAS 效果
4、无锁效率高
(1)无锁情况下,即使重试失败,线程始终在高速运行,没有停歇
(2)而 synchronized 会让线程在没有获得锁时,发生上下文切换,进入阻塞
5、结合 CAS、volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下
(1)无锁情况下,因为线程要保持运行,需要额外 CPU 的支持
(2)CAS 是基于乐观锁(实际无锁)的思想
(3)synchronized 是基于悲观锁的思想
6、CAS 体现无锁并发、无阻塞并发
(1)因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
(2)但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
原子整数
1、AtomicBoolean
2、AtomicInteger
3、AtomicLong
原子引用
1、AtomicReference
2、ABA 问题
(1)在 CAS 操作时,其他线程将变量值 A 改为 B,但又改回 A
(2)等到本线程使用期望值 A,与当前变量进行比较时,发现变量 A 没有变,于是 CAS 就将 A 值进行交换操作
(3)实际上该值已经被其他线程修改过,这与乐观锁的设计思想不符合
2、AtomicMarkableReference
(1)可以给原子引用加上版本号,追踪原子引用整个的变化过程
(2)可以知道引用变量中途被更改几次
(3)静态内部类
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);
}
}
3、AtomicStampedReference
(1)不关心引用变量更改了几次,只是单纯的关心是否更改过
(2)静态内部类
private static class Pair<T> {
//引用对象
final T reference;
//修改标志
final boolean mark;
private Pair(T reference, boolean mark) {
this.reference = reference;
this.mark = mark;
}
static <T> Pair<T> of(T reference, boolean mark) {
return new Pair<T>(reference, mark);
}
}
原子数组
1、AtomicIntegerArray
2、AtomicLongArray
3、AtomicReferenceArray
字段更新器
1、AtomicReferenceFieldUpdater
2、AtomicIntegerFieldUpdater
3、AtomicLongFieldUpdater
4、利用字段更新器,可以针对对象的某个域(Field)进行原子操作
5、只能配合 volatile 修饰的字段使用,否则会出现异常
原子累加器
1、DoubleAccumulator
2、DoubleAdder
3、LongAccumulator
4、LongAdder
5、性能:AtomicLong < LongAdder
(1)AtomicLong:高并发情况下,CAS 冲突概率大,导致经常自旋,影响整体效率
(2)LongAdder:在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],Thread-1 累加 Cell[1]……,最后将结果汇总,在累加时操作不同的 Cell 变量,因此减少 CAS 重试失败
缓存、内存
1、结构
2、速度比较
起点 -> 终点 | 大约所需时间周期(cycle) |
CPU -> 寄存器 | 1 |
CPU -> L1 缓存 | 3 ~ 4 |
CPU -> L2 缓存 | 10 ~ 20 |
CPU -> L3 缓存 | 40 ~ 45 |
CPU -> 内存 | 120 ~ 240 |
(1)因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率
(2)缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte
(3)加入缓存,会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
(4)CPU 要保证数据的一致性,如果某个 CPU 核心更改数据,其它 CPU 核心对应的整个缓存行必须失效
LongAdder
1、继承父类 Striped64
(1)关键 field
//累加单元数组, 懒加载
transient volatile Cell[] cells;
//基础值,如果没有竞争, 则用 CAS 累加这个域
transient volatile long base;
//在 cells 创建或扩容时, 设置为1, 表示加锁
transient volatile int cellsBusy;
(2)静态内部类
//防止缓存行伪共享
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) {
value = x;
}
//最重要的方法,使用CAS方式进行累加,prev表示旧值,next表示新值
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
//省略其余代码
}
2、@sun.misc.Contended 防止缓存伪共享原理
(1)因为 Cell 为数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头、8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象
(2)问题:Core-0 要修改 Cell[0],Core-1 要修改 Cell[1],读取时都会将 Cell[0]、Cell[1] 加载到各自的缓存中,无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加
(3)解决:在使用 @sun.misc.Contended 的对象或字段的前后,各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时,占用不同的缓存行,这样不会造成对方缓存行的失效
3、累加所调用的方法
(1)add 源码
public void add(long x) {
//x:累加值
//as:累加单元数组
//b:基础值
//v:当前Cell对象的值
//m:累加单元数组的最后一个元素索引
//a:当前线程的Cell对象
Cell[] as; long b, v; int m; Cell a;
//进入if的两个条件
//1、as 有值, 表示已经发生过竞争
//2、cas给base累加时失败, 表示base发生竞争
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 = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
(2)add 流程图
(3)父类 Striped64 的 longAccumulate
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
//当前线程还没有对应Cell对象, 需要随机生成一个h值,用来将当前线程绑定到Cell对象
if ((h = getProbe()) == 0) {
//初始化probe
ThreadLocalRandom.current(); // force initialization
//h对应新的probe值,用来对应Cell对象
h = getProbe();
wasUncontended = true;
}
//collide为true时,表示需要扩容
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
//cells已创建,当前线程的Cell对象未创建
//cells已创建,则进入
if ((as = cells) != null && (n = as.length) > 0) {
//cells中没有当前线程的Cell对象,则进入
if ((a = as[(n - 1) & h]) == null) {
//cellBusy未被加锁,则进入
if (cellsBusy == 0) { // Try to attach new Cell
//直接创建Cell对象,初始累加值为x
Cell r = new Cell(x); // Optimistically create
//再次检查cellBusy未被加锁,若ture,则为cellsBusy加锁
if (cellsBusy == 0 && casCellsBusy()) {
//创建标志,表示Cell对象是否创建成功
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if (
//再次检查cells已创建,且长度大于0
(rs = cells) != null &&
(m = rs.length) > 0 &&
//再次检查当前线程Cell对象为null
rs[j = (m - 1) & h] == null) {
rs[j] = r;
//Cell对象创建成功
created = true;
}
} finally {
//释放cellsBusy锁
cellsBusy = 0;
}
if (created)
//若Cell对象创建成功,则break
break;
//若Cell对象创建失败,则进入下一轮循环
continue; // Slot is now non-empty
}
}
collide = false;
}
//存在竞争
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//cells已创建,当前线程的Cell对象已创建
//cas尝试累加,fn配合LongAccumulator不为null,配合LongAdder为null
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
//累加成功,则break
break;
//累加失败
//如果cells长度已经超过最大长度(CPU最大线程数),或者已经扩容,则进入
else if (n >= NCPU || cells != as)
//不需要扩容,确保collide为false,下一次循环不会进行扩容
collide = false; // At max size or stale
else if (!collide)
collide = true;
//检测cellBusy未加锁,则尝试加锁
else if (cellsBusy == 0 && casCellsBusy()) {
try {
//再次检查cells未扩容
if (cells == as) { // Expand table unless stale
//创建新cells,新容量 = 原容量 * 2
Cell[] rs = new Cell[n << 1];
//复制原cells所有元素,到新cells
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
//释放cellsBusy锁
cellsBusy = 0;
}
//扩容成功,collide复位
collide = false;
//继续下一轮循环
continue; // Retry with expanded table
}
//改变当前线程对应Cell对象
h = advanceProbe(h);
}
//cells 未创建
//没有cells, 尝试给 cellsBusy 加锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
//初始化标志,表示是否成功初始化cells
boolean init = false;
try { // Initialize table
if (cells == as) {
//初始化cells,初始长度为2
Cell[] rs = new Cell[2];
//填充一个Cell对象,Cell对象的value初始值为累加值
//对象要么在索引0,要么在索引1,另一个空位体现Cell对象的懒加载
rs[h & 1] = new Cell(x);
cells = rs;
//初始化cells成功
init = true;
}
} finally {
//释放cellsBusy锁
cellsBusy = 0;
}
//成功创建cells,则break;
if (init)
break;
}
//创建cells失败(加锁失败),尝试给 base 累加
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
(4)cells 未创建
(5)cells 已创建,当前线程的 Cell 对象未创建
(6)cells 已创建,当前线程的 Cell 对象已创建
(7)通过 sum(),获取最终结果
public long sum() {
//as:cells的临时变量
//a:cells中的Cell对象的临时变量
Cell[] as = cells; Cell a;
//sum:base的临时变量,作为累加的最终结果
long sum = base;
//检测cells不为null
if (as != null) {
//遍历所有Cell对象
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
//若Cell不为null,则加总
sum += a.value;
}
}
return sum;
}
Unsafe 类
1、Unsafe 对象
(1)提供非常底层的,操作内存、线程的方法
(2)不能直接调用,只能通过反射获得
2、底层类,分配、释放直接内存,不建议使用,由 JDK 开发人员使用
3、使用 Unsafe 类,完成直接内存的分配、回收,回收不是由 JVM GC 释放,而是需要主动调用 freeMemory 方法
4、分配内存
//返回值:直接内存的地址
public native long allocateMemory(long var1);
public void setMemory(long var1, long var3, byte var5) {
this.setMemory((Object)null, var1, var3, var5);
}
public native void setMemory(Object var1, long var2, long var4, byte var6);
5、释放内存
public native void freeMemory(long var1);
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战