JDK 底层Unsafe工具类
概念
JDK提供的一个工具类,unsafe里面的方法大多是native方法,可以理解为unsafe类是JDK给你提供的一个直接调用操作系统底层功能的一个工具类,unsafe提供了非常多操作系统级别的方法。
- 通过unsafe让操作系统直接分配、释放内存
- 突破java 语法本身的限制,直接从内存级别去操作堆里面的某个对象的数据。直接通过内存地址address,找到这块内存然后直接操作这个内存块的数据
- 调用操作系统的CAS指令,实现CAS功能
- 操作系统级别挂起和恢复线程
- 提供操作系统级别的内存屏障,Load屏障和Store屏障,强制读主内存和刷新主内存
例如著名的并发基础工具类AQS底层就是通过unsafe提供的CAS操作来进行加锁的,加锁失败的线程又是通过unsafe提供的park、unpark操作将线程挂起和唤醒的
cas是如何保证原子性的?
CAS操作是直接通过( 对象地址 + 对象内部属性偏移量offset )直接定位到要修改的变量在内存的位置,然后在内存级别的比较数据和修改数据。 CAS底层硬件使用锁保证原子性。
CPU是不会直接写主内存的,上面的操作存在并发问题,需要CAS底层硬件使用锁保证原子性!
原子类体系讲解
JUC包下提供的原子类底层的实现原理基本都是差不多的,都是基于volatile和CAS操作来保证线程安全的,我们只需要掌握常用的一些原子类就可以了。
AtomicInterger、AtomicBoolean底层原理
一句话说完:基本都是调用unsafe的CAS操作确保原子性,然后使用volatile修饰变量,确保可见性和有序性。
unsafe和volatitle
getAndIncrement()
源码
- 首先(o + valueOffset)得到value变量在内存中的地址,然后根据地址直接取出value在主内存值,这个值记录为expected中
- 根据 (o + offsetSet)地址偏移量,expected期待的值跟当前内存的值进行对比,如果相等则CAS操作成功,内存的值修改为 expected + x
- 如果值不相等,则进入下一次循环,直到CAS操作成功为止
- 由于使用了volatile 修饰符修饰了value,所以一旦修改了别的线程能立马可见、同时volatile还是用内存屏障确保有序性。
- 所以上面的CAS操作确保了原子性,通过volatile确保可见性、有序性;线程安全的三个特性都满足了,上面的操作就是线程安全的。
AtomicBoolean的源码和AtomicInteger实现一致,只是value限制在01
AtomicReference、AtomicStampedReference底层原理,多个变量更新原子操作
概念
AtomicReference。它可以将多个变量封装为对象的多个属性,然后一次性的更新整个对象,就能cas的更新多个变量,确保原子性。
AtomicReference原理
- 内部属性
compareAndSet
内部源码
跟Atomicinteger和AtomicBoolean原理是一样的,只不过AtomicInteger、AtomicBoolean底层调用的是unsafe.compareAndSwapInt方法CAS操作int的值,而这里是compareAndSwapObject是CAS操作一个内存对象而已,没啥大区别
AtomicStampedReference底层原理
实例对象中的ABA问题
PS:你的女朋友分手后再和你复合,你的女朋友还是你的女朋友吗?
原理
AtomicStampedReference,是AtomicReference的升级版本,看名字你就知道多了一个叫做Stamped的东西,这东西就是版本号,也叫作邮戳。在执行CAS操作之前对比reference的值的同时也对比版本号,如果reference一样但是stamped不一样,说明期间有人修改过但是又把值改回来了,就不允许执行CAS操作了,这样就能解决ABA的问题了
源码
- 内部结构,将对象和版本号绑定到了Pair
- 执行核心修改的cas方法,同时比较对象和版本号
- 执行图
LongAdder的底层分段锁
设计原因
使用AtomicInteger、AtomicLong底层的CAS操作竞争非常激烈,会导致大量线程CAS失败而不断自旋,耗费CPU的性能。LongAdder 使用使用分段锁机制解决该问题。
分段锁思想
银行办事大厅多窗口机制为例。
- 常规人少无并发,只开放常规窗口,要知道所有服务过的客户,汇总各个窗口即可。
- 访客比较多,竞争大,开放备用窗口。假如说有4个备用窗口,分别为A、B、C、D窗口,每个用户自己有一个id,比如用户2(id=4)来了之后看到常规窗口有人了,就会根据 id % 备用窗口大小 =》 4 % 4 = 0,就会派到第一个备用窗口,也就是窗口A
- 同时并发非常激烈的时候,同一个备用窗口也是存在竞争的,比如用户3(id=5)、用户6(id=9)根据 id % 备用窗口大小 = 2,所以都竞争备用窗口B;用户4(id=6)和用户7(id=10)同样会竞争备用窗口C
- 使用了备用窗口列表,相当于将不同的用户派遣到不同的窗口,减少了竞争,这样并发性能自然就提升上去了
源码分析
从Strimped64继承的Cell窗口
Cell {
// 计数器,当前访问人数多少
volatile long value;
// 构造函数
Cell(long x) { value = x; }
// CAS竞争同一个窗口,可能多个用户CAS竞争这个窗口
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
}
基础窗口
// 基础窗口
transient volatile long base;
// 争抢基础窗口方法
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
用户id生成方法
// 通过线程id,经过系列运算得到用户id
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
add()方法分析
public void add(long x) {
// 备用窗口列表
Cell[] as;
// b是成功窗口的value值,v是你被分派到的备用窗口的value值
long b, v;
// m是不用窗口的长度-1,分派窗口的算法是用户 id % 窗口总数
int m;
// a是否被分派到的窗口
Cell a;
// cells == null 说明备用窗口不开放
// casBase()是争抢常规窗口,然后执行业务后退出
// 如果casBase失败进去if重试
// 1、没有开放备用窗口,都直接在常规窗口上争抢
if ((as = cells) != null || !casBase(b = base, b + x)) {
// 2、进入到此处,条件1是开放了备用窗口,直接进;条件2是 cells == null 没有开放但同时竞争常规窗口失败了;
// 没有竞争标识
boolean uncontended = true;
// 1 as == null || (m = as.length - 1) < 0 说明没有开放备用窗口,直接 兜底方法等待
if (as == null || (m = as.length - 1) < 0 ||
// 2、分配到的备用窗口没有人,也就是没有初始化完毕,直接 兜底方法等待
(a = as[getProbe() & m]) == null ||
// 3、 分配到了窗口a,竞争cas失败了,也只能兜底
!(uncontended = a.cas(v = a.value, v + x)))
// 兜底方法等待
longAccumulate(x, null, uncontended);
}
}
Strimped64分段锁底层实现源码剖析
longAccumulate()分析
// 处理涉及初始化、调整大小、创建新cell和or争用的更新情况。见上文解释。该方法存在乐观重试代码常见的非模块化问题,
// 依赖于重检查的读取集。
// x - value值
// fn - update函数,或null表示add(此约定避免了LongAdder中需要额外字段或函数)
// wasUncontended - 如果CAS在调用前失败,则为false
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
// 1、如果 getProbe() 获取用户id为0 则强制初始化获取的逻辑
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
// 碰撞标识
boolean collide = false; // True if last slot nonempty
// 循环自旋重试
for (;;) {
Cell[] as; Cell a; int n; long v;
// 1、备用窗口开放,此分支去备用窗口处理
if ((as = cells) != null && (n = as.length) > 0) {
// 如果自己被派到的那个备用窗口为null,说明窗口还么人工作,则叫一个工作人员过来负责
if ((a = as[(n - 1) & h]) == null) {
// 如果没人在初始化,下面则创建一个窗口(叫一个工作人员过来处理)
if (cellsBusy == 0) { // Try to attach new Cell
// 创建一个新的窗口,同时提交自己的请求x
Cell r = new Cell(x); // Optimistically create
// 如果没人在初始化
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
// 再次检查备用窗口列表初始化好了
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// 如果到这里已经知道cas操作失败了,则重新设置一下失败标识,进入下一循环重试
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 根据 用户id % 备用窗口总数 算法,自己应该争抢哪个备用窗口
// 如果争抢成功,则操作结束了,如果失败进入下一循环重试
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
// 走到这里,说明上面的操作都失败了,备用窗口竞争还是很激烈
// 所以需要扩容了,多增加一些备用窗口,缓解竞争激烈的情况
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 这里就是具体的扩容操作了
if (cells == as) { // Expand table unless stale
// 新的备用窗口是原来的2倍
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
// 原来已经存在的备用窗口重新赋值一下
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 在这里重新计算一下自己的用户id,
// 使得用户id尽量的分散到不同的窗口,减少竞争
h = advanceProbe(h);
}
// 2、备用窗口没有开放,ellsBusy == 0 说明没有人在初始化备用窗口,自旋锁一下标识,此分支去初始化备用窗口
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// init是否初始化完成,最开始设置为false
boolean init = false;
try { // Initialize table
if (cells == as) {
// 然后这里就没什么复杂的,就是创建一个数组,初始长度为2
// 但是这个时候数组里面没有元素,每个元素都是空的
Cell[] rs = new Cell[2];
// 创建一个新的窗口,把自己的操作数x交给新创建的窗口
rs[h & 1] = new Cell(x);
cells = rs;
// 然后设置初始化完成表示为true
init = true;
}
} finally {
// 重新设置cellsBusy标识,操作完成,没人在初始化或者扩容了
cellsBusy = 0;
}
// 如果初始化完成,退出循环
if (init)
break;
}
// 3、备用窗口没有开放,同时有人在初始化它,该分支只能竞争常规窗口
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
三大分支处理:
- 备用窗口开放进入其中处理
- 备用窗口没有开放同时可以初始化,进入初始化
- 备用窗口没有开放又无法初始化,只能竞争常规窗口
初始化备用窗口
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// init是否初始化完成,最开始设置为false
boolean init = false;
try { // Initialize table
if (cells == as) {
// 然后这里就没什么复杂的,就是创建一个数组,初始长度为2
// 但是这个时候数组里面没有元素,每个元素都是空的
Cell[] rs = new Cell[2];
// 创建一个新的窗口,把自己的操作数x交给新创建的窗口
rs[h & 1] = new Cell(x);
cells = rs;
// 然后设置初始化完成表示为true
init = true;
}
} finally {
// 重新设置cellsBusy标识,操作完成,没人在初始化或者扩容了
cellsBusy = 0;
}
// 如果初始化完成,退出循环
if (init)
break;
}
备用窗口没有开放又无法初始化,只能竞争常规窗口
// 3、备用窗口没有开放,同时有人在初始化它,该分支只能竞争常规窗口
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
备用窗口开放进入其中处理
Cell[] as; Cell a; int n; long v;
// 1、备用窗口开放,此分支去备用窗口处理
if ((as = cells) != null && (n = as.length) > 0) {
// 如果自己被派到的那个备用窗口为null,说明窗口还么人工作,则叫一个工作人员过来负责
if ((a = as[(n - 1) & h]) == null) {
// 如果没人在初始化,下面则创建一个窗口(叫一个工作人员过来处理)
if (cellsBusy == 0) { // Try to attach new Cell
// 创建一个新的窗口,同时提交自己的请求x
Cell r = new Cell(x); // Optimistically create
// 如果没人在初始化
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
// 再次检查备用窗口列表初始化好了
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// 如果到这里已经知道cas操作失败了,则重新设置一下失败标识,进入下一循环重试
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 根据 用户id % 备用窗口总数 算法,自己应该争抢哪个备用窗口
// 如果争抢成功,则操作结束了,如果失败进入下一循环重试
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
// 走到这里,说明上面的操作都失败了,备用窗口竞争还是很激烈
// 所以需要扩容了,多增加一些备用窗口,缓解竞争激烈的情况
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 这里就是具体的扩容操作了
if (cells == as) { // Expand table unless stale
// 新的备用窗口是原来的2倍
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
// 原来已经存在的备用窗口重新赋值一下
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 在这里重新计算一下自己的用户id,
// 使得用户id尽量的分散到不同的窗口,减少竞争
h = advanceProbe(h);
}