LongAddr 源码分析

首先介绍下 LongAddr 这个类的作用,这个类,是JDK8 以后提供的新的,用于并发计数的工具。以前我们并发的计数的话,使用的都是 AtomicLong 但是这个原子类,性能是有不足的,因为当存在大量并发的时候,可能很多的 CAS 操作都是失败的,所以线程会不断的重试CAS,但是每一批中能成功的 CAS 操作只会有一个,当并发太高时候,会出现性能问题!

LongAddr 采用的方法是,共享热点数据分离的计数,将一个数字的值拆分为一个数组!然后这个数组,后面修改这个值的时候,就修改这个数组的一部分!如果要得到这个数字的话,就要把这个值加起来!这样的技术,能让并发量大大提高!

热点数据分离 也是分段锁思想的一种延伸!将原本的对一个值的乐观锁,分离成多个值的乐观锁。

LongAddr 的优点:有很高性能的并发写的能力!
缺点:读取的性能不是很高效!而且可能不是很准确,因为是要取数组所有的值相加

---源码解析---

LongAddr 继承自 Striped64 这个类

Striped64 这个类,有一个 Cell 的内部类,这个类有个 Contended 注解,这个注解,用于将数据放置在不同的缓冲行中,这样可以避免伪共享问题,提供并发量(伪共享问题,后面单独说明)伪共享

Striped64 还有 cells 这个 Cell 数组,这个用于保存我们所说的,分离的数据

base 字段,在并发量不大,没有导致CAS失败的情况下,会直接修改这个值!

cellsBusy,这个值为 1 代表我们正在修改cells的大小!

LongAddr 最核心的方法就是 add方法了:

public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        // 如果cells数组不为空,或者,cas的修改base的值失败了
        // cells数组不为空,那么代表曾经发生过并发修改失败了
        // casBase修改失败,代表本次并发修改失败了
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            // 如果 cells数组是空的,直接进入longAccumulate
            // 如果 cells数组小于1 直接进入
            // 如果 cells 当前 slot的值为null 直接进入
            // 如果 cas 当前 slot的值失败了直接进入
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

add 方法总结:

我们总是 如果 cells 不为 NULL ,那么尝试修改 cells 对应的 slot 的值
如果 cells 是空的,那么我们就尝试修改 base , base 修改失败了,代表当前有并发,也会进入 longAccumulate

上面的add操作的核心方法是:longAccumulate 这个方法是Striped64提供的:

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        // getProbe 方法是当前线程的随机数!使用这个就能随机的修改cells的值了
        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;
            // 如果 cells 是空的,代表第一次发生并发!
            if ((as = cells) != null && (n = as.length) > 0) {
            // 进入此处的,都是cells不为空的
            // 随机选择一个 slot ,如果是空的,则创建一个新的Cell添加进去!
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        // 设置 Busy
                        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;
                }
                // 这里是如果发生了锁竞争,并且当前的slot已经被占用了
                // 进入这里 ,代表选择下一个 随机值,然后再来一次
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                // 如果两次随机值 都被占用了,那么就尝试设置下 新增 当前Slot的值
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                // 如果还是失败了,那么再判断下,cells 的个数能否添加了,
                // cells 个数最大是 NCPU 的最近的2的倍数!
                // 如果 cells 数目已经大于等于这个值了,那么只会 一直的改变slot
                // 如果slot都被占用的话,就一直尝试去加在这个slot的值上
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                // 如果cells的值 不太够!小于 NCPU的话!会进入这里扩容cells
                // 扩容的话,首先也是 设置 busy 
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                    // 直接复制数据!
                        if (cells == as) {      // Expand table unless stale
                            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
                }
                // 上面的每次循环结束的话,都会改变下Probe 
                // 这样就实现了随机的修改cells的某个元素!
                h = advanceProbe(h);
            }
            // 这里是初始化 Cells的逻辑,先检查 cellsBusy 
            // 如果有线程进入了Busy的话,就执行下一步,也就是casbase,再不行就轮训操作!
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        //默认的Cells大小为2
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                // 设置完毕,就可以退出了
                if (init)
                    break;
            }
            // 如果 队列为空,并且有其他线程,正在实例化,cells的话
            // 就去修改base的值!
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

总结下 add 方法:

假设我们现在对一个 LongAddr 对象,进行写操作

  1. 首先,是对 base 进行 cas 的写,如果cas的写失败了,那么代表有线程在进行竞争!
  2. 此时 cells 是空的,方法执行进入 longAccumulate 方法,这个方法,会实例化一个 新的cells数组
  3. 之后如果有线程,执行add方法的时候,首先去修改 cells 数组里面的对象的值。具体修改哪个值是按照线程的随机数,随机选出来的,但是 cells 数组的大小是 CPU 数量的最近的2倍值!后面的并发修改,大部分都是 并行的修改 cells 里面元素的Value 的值!这样相当于,将原本的一个锁,分化成了 多个锁,提高了并发度!

LongAddr 还有其他几个方法,基本上都是 间接的调用这个 add 方法:

incrementadd(1L)

decrementadd(-1L)

为了获取,LongAddr 的结果,我们需要调用 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;
    }

上代码可以看出:结果就是 sum + cells 的结果!
但是这个结果是不一定准确的!因为可能读取的时候,可能有的线程正在写数据!

reset : 方法就是把 base 设置为 0 ,并且每个 cells 的元素的值都设置为0

除了LongAddr之外,,Java还提供了 DoubleAdder 的实现,这个类的实现 调用的是 doubleAccumulate 方法,这个方法的实现和longAccumulate的实现 非常类似!就是通过 doubleToRawLongBits 方法将都double的值转换成Long

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,527评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,687评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,640评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,957评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,682评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,011评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,009评论 3 449
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,183评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,714评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,435评论 3 359
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,665评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,148评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,838评论 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,251评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,588评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,379评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,627评论 2 380

推荐阅读更多精彩内容