4-Atomic原子类专题

JDK 底层Unsafe工具类

概念

JDK提供的一个工具类unsafe里面的方法大多是native方法,可以理解为unsafe类是JDK给你提供的一个直接调用操作系统底层功能的一个工具类,unsafe提供了非常多操作系统级别的方法。

  1. 通过unsafe让操作系统直接分配、释放内存
  2. 突破java 语法本身的限制,直接从内存级别去操作堆里面的某个对象的数据。直接通过内存地址address找到这块内存然后直接操作这个内存块的数据
  3. 调用操作系统的CAS指令,实现CAS功能
  4. 操作系统级别挂起和恢复线程
  5. 提供操作系统级别的内存屏障,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()源码

  1. 首先(o + valueOffset)得到value变量在内存中的地址,然后根据地址直接取出value在主内存值,这个值记录为expected
  2. 根据 (o + offsetSet)地址偏移量,expected期待的值跟当前内存的值进行对比,如果相等则CAS操作成功内存的值修改为 expected + x
  3. 如果值不相等,则进入下一次循环,直到CAS操作成功为止
  4. 由于使用了volatile 修饰符修饰了value,所以一旦修改了别的线程能立马可见、同时volatile还是用内存屏障确保有序性。
  5. 所以上面的CAS操作确保了原子性,通过volatile确保可见性、有序性;线程安全的三个特性都满足了,上面的操作就是线程安全的。

AtomicBoolean的源码和AtomicInteger实现一致,只是value限制在01

AtomicReference、AtomicStampedReference底层原理,多个变量更新原子操作

概念

AtomicReference。它可以将多个变量封装为对象多个属性,然后一次性的更新整个对象,就能cas的更新多个变量,确保原子性。

AtomicReference原理

  1. 内部属性

  1. compareAndSet内部源码

Atomicinteger和AtomicBoolean原理是一样的,只不过AtomicInteger、AtomicBoolean底层调用的是unsafe.compareAndSwapInt方法CAS操作int的值,而这里是compareAndSwapObject是CAS操作一个内存对象而已,没啥大区别

AtomicStampedReference底层原理

实例对象中的ABA问题

PS:你的女朋友分手后再和你复合,你的女朋友还是你的女朋友吗?

原理

AtomicStampedReference,是AtomicReference的升级版本看名字你就知道多了一个叫做Stamped的东西,这东西就是版本号,也叫作邮戳。在执行CAS操作之前对比reference的值的同时也对比版本号,如果reference一样但是stamped不一样,说明期间有人修改过但是又把值改回来了,就不允许执行CAS操作了,这样就能解决ABA的问题了

源码

  1. 内部结构,将对象和版本号绑定到了Pair

  1. 执行核心修改的cas方法,同时比较对象和版本号

  1. 执行图

LongAdder的底层分段锁

设计原因

使用AtomicInteger、AtomicLong底层的CAS操作竞争非常激烈,会导致大量线程CAS失败而不断自旋,耗费CPU的性能。LongAdder 使用使用分段锁机制解决该问题

分段锁思想

银行办事大厅多窗口机制为例。

  1. 常规人少无并发,只开放常规窗口,要知道所有服务过的客户,汇总各个窗口即可。

  1. 访客比较多,竞争大,开放备用窗口。假如说有4个备用窗口,分别为A、B、C、D窗口,每个用户自己有一个id,比如用户2(id=4)来了之后看到常规窗口有人了,就会根据 id % 备用窗口大小 =》 4 % 4 = 0,就会派到第一个备用窗口,也就是窗口A
  2. 同时并发非常激烈的时候同一个备用窗口也是存在竞争的,比如用户3(id=5)、用户6(id=9)根据 id % 备用窗口大小 = 2,所以都竞争备用窗口B;用户4(id=6)和用户7(id=10)同样会竞争备用窗口C
  3. 使用了备用窗口列表,相当于将不同的用户派遣到不同的窗口减少了竞争,这样并发性能自然就提升上去了

源码分析

从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
    }
}

三大分支处理:

  1. 备用窗口开放进入其中处理
  2. 备用窗口没有开放同时可以初始化,进入初始化
  3. 备用窗口没有开放又无法初始化,只能竞争常规窗口

初始化备用窗口
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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值