浅讲JUC下的原子类

原子整数 原子引用

原子整数,如AtomicInteger、AtomicLong等,它们的方法都是采用cas+volatile保证了原子性。

原子引用,如AtomicReference、AtomicStampReference、AtomicMarkableReference,用于引用类型,使用方法跟原子整数差不多,都是new出来,然后对象调用方法。

它们实际维护的值被放在属性value中。value被volatile修饰,确保读取值时到主存读取,拿到的值是最新值。

cas一般传入两个参数,这里拿AotmicInteger举例,expect是传入你获得到的value值,update传入要修改成的值(要更新,拿到value值,计算得到新值)。cas会拿expect去跟现在最新的value值对比,如果一致,证明value没有被其他线程修改过,那么修改成功;如果不一致,证明value已经被其他线程修改了,那么这次cas修改失败。

ABA

由于cas是对比原先值和最新值,只要一致,就可以修改。于是,会出现ABA问题。

ABA问题就是,比如value初始值是A,线程1拿到value为A,准备把value修改为C。在拿到A后,修改为C前,有线程2将value从A改为B,线程3又将value从B改为A,然后,线程1才接着执行,它要把value改为C,一看,它拿到的value是A,现在最新的value也是A,一致,于是cas修改成功。在这种情况下,线程A无法感知到其他线程对共享变量value从A改到B,又从B改回A的操作。

如果希望,线程修改value时,只要value被其他线程修改过,就cas失败,那么,可以加上一个版本号,只要value被修改一次,版本号就加1。线程修改时,不仅对比value值,还对比版本号,这样就可以避免ABA问题。

JUC也提供了带版本号的原子类AtomicStampedReference

它维护的值放在属性reference中,版本号是stamp。

它也提供了compareAndSet(cas)方法,比之前的类就是多了版本号。

但有时候,我们并不关心变量被修改了多少次,只关心是否被修改过,于是AtomicMarkableReference就登场了,它不像AtomicStampedReference维护整数版本号,而是维护一个boolean值。

原子数组

因为原子引用保护的是引用本身的线程安全,它修改的是引用本身地址,但它无法保护引用内部元素的变化,如数组。当线程不是想修改引用本身地址,而是想修改内部元素时,就要用到原子数组类了。如AtomicIntegerArray,该类还重写了toString()方法,可以直接打印。

一个使用原子数组和不安全数组进行自增的对比测试,使用了函数式接口编程,博主觉得是个非常好的例子,分享给大家。

public class Test39 {
    public static void main(String[] args) {
        //线程不安全数组
        demo(
                () -> new int[10],
                (array) -> array.length,
                (array,index) -> array[index]++,
                (array) -> System.out.println(Arrays.toString(array))
        );
        
        //线程安全数组
        demo(
                () -> new AtomicIntegerArray(10),  //原子数组
                (array) -> array.length(),
                (array,index) -> array.getAndIncrement(index),
                (array) -> System.out.println(array)
        );
    }

    /**
     * 
     * @param arraySupplier 提供数组,可以是线程不安全数组或线程安全数组
     * @param lengthFun 获取数组长度的方法
     * @param putConsumer   自增方法,回传array、index
     * @param printConsumer 打印数组的方法
     */
    //参数是函数式接口给你的,结果是调用者要给回来的
    //supplier  提供者   无中生有(无参,要结果)    () -> 结果
    //function  函数    一个参数一个结果      (参数)->结果            BiFunction  (参数1,参数2)->结果
    //consumer  消费者   一个参数没结果   (参数) -> void            BiConsumer  (参数1,参数2) -> void
    private static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer ){
        List<Thread> ts = new ArrayList<>();    //线程集合,将线程加到集合里方便统一操作
        //拿到数组
        T array = arraySupplier.get();
        int length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {  //length个线程,即数组有多长,就拿多少个线程
            //每个线程对数组作10000次操作
            ts.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);  //j % length是拿下标,将操作均摊到数组的每个元素上
                }
            }));
        }
        
        //启动所有线程
        ts.forEach(t -> t.start());
        //等待所有线程结束
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        printConsumer.accept(array);
    }
}

输出结果:

[7730, 7745, 7732, 7696, 7704, 7725, 7764, 7730, 7721, 7756]
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

字段更新器

字段更新器保护的是对象中的属性,它能保证多个线程访问同一个对象的成员变量时,成员变量的线程安全性。(即原子修改对象的成员变量)如AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater。

要注意,被保护的属性不能被private修饰,因为字段更新器要访问它。同时,被保护的属性必须被volatile修饰,因为要用cas,如果不被volatile修饰,会报错。

@Slf4j(topic = "c.Test40")
public class Test40 {
    public static void main(String[] args) {
        Student s = new Student();
        //参数1:哪个类
        //参数2:哪个属性
        //参数3:属性名
        AtomicReferenceFieldUpdater updater = 
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        System.out.println(updater.compareAndSet(s, null, "张三"));
        System.out.println(s);
    }
}

class Student {
    volatile String name;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}
true
Student{name='张三'}

原子累加器

虽然原子整数可以调用如getAndIncrement()等方法进行累加,但JUC提供LongAdder原子累加器,它利用了cells累加单元,提高了累加效率,比调用原子整数方法累加效率快了许多。

具体原因是原子整数类累加都是在一个共享变量上累加,当竞争比较激烈时,就会频繁出现cas失败重试的现象,影响效率。而LongAdder在有竞争时会创建累加单元,每个线程有自己的累加单元,线程都在自己的累加单元上累加,最后将结果汇总,这样就减少了cas重试次数,提高了效率。

increment()

LongAdder的自增方法:increment()

它是调用了add(1L)

add流程如下:

①初始时,累加单元数组cells为空,即(as = cells) == null。caseBase在base属性上累加(没有竞争,就在base属性上累加,等同于原子整数类在一个变量上累加),如果cas成功,累加完成,add方法结束;如果cas失败,证明有竞争了,进入第一个if块。

因为【as == null】成立(累加单元数组cells为空),进入第二个if块,执行longAccumulate(),里面会去创建累加单元数组。

②再次调用add(1L),累加单元数组不为空了(那证明已经有过竞争了,不再在base属性上自增,要直接去累加单元上自增了),进入第1个if块,【a = as[getProbe() & m]) == null】判断当前线程是否有累加单元,如果没有,也就是条件成立,进入第二个if块。进入longAccumulate() (它去为当前线程创建累加单元)。如果当前线程有累加单元,【!(uncontended = a.cas(v = a.value, v + x))】让当前线程在自己的累加单元上进行cas操作,如果成功,add方法结束;如果失败,进入longAccumulate() (证明累加单元不够,扩容)

总结流程:

先看有没有累加单元数组,判断是否发生过竞争,没有就在base属性上cas,成功了直接返回,失败了就去调用longAccumulate()。有累加单元数组了,存在过竞争看当前线程是否有,累加单元,如果有,在累加单元上累加,成功返回,失败证明累加单元不够,去扩容,走longAccumulate();如果当前线程还没有累加单元,执行longAccumulate()

流程图:

longAccumulate()

先贴个整体

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        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;
            if ((as = cells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        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;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                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
                            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
                }
                h = advanceProbe(h);
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

主要逻辑在for死循环中,用if/else分为三大块:

对应add()调用longAccumulate()的三种情况,分别来看:

①没有累加单元数组进来:

cells就是累加单元数组,当没有累加单元时,第一个if ((as = cells) != null && (n = as.length) > 0)判断失败,进入第二个if。cellsBusy == 0判断还没有线程对累加单元数组加锁,cells==as判断累加单元数组还是原来的那个,即没有被别的线程修改过(多线程情况下,可能当前线程判断了累加单元数组为空后,准备第二个if判断,另一个线程刚好改完释放了锁,当前线程就拿到了锁,所以还要判断一下cells是不是原来拿到的cells),casCellsBusy()是用cas对cells加锁,即将cellsBusy从0改为1。如果cas失败,会进入第三个if块,会去修改base的值,修改成功方法结束,修改失败继续循环。

如果三个条件都成立,进入第二个if块,准备创建累加单元数组

    else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    //再次判断cells还是原来的cells
                    if (cells == as) {
                        //刚开始创建累加单元数组,大小只有2,后面才会慢慢扩容。
                        Cell[] rs = new Cell[2];    //大小为2的数组
                        rs[h & 1] = new Cell(x);    //但只创建了一个累加单元
                        cells = rs;    //将数组赋值给cells
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;    //最后解锁
                }
                if (init)
                    break;
            }

②有累加单元数组,但当前线程还没有累加单元:

明显会进入死循环中第一个if块,然后会进入这个if块,因为a = as[(n - 1) & h]) == null表示当前线程没有自己的累加单元。

            if ((a = as[(n - 1) & h]) == null) {
                    //cells还没被加锁
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        //创建一个累加单元
                        Cell r = new Cell(x);   // Optimistically create
                        //再次判断没有被加锁,cas去对cells加锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                //再次检查cells不为空,并且当前线程累加单元为空
                                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;
                }

③有累加单元数组,当前线程也有累加单元,但对累加单元累加失败:

这个块是对当前线程的累加单元再次尝试累加,如果成功,方法结束;如果失败,不会进这个else if,接着往下走。

boolean collide表示是否要扩容,如果累加单元数组长度大于CPU核心数,那么扩容后,即再增加累加单元,是没有意义的。如果没有超过CPU核心数,到else if(!collide),证明有累加单元数组并且在当前线程的累加单元上累加失败了,collide为false,于是进入这个else if块,将collide改为true,表示要扩容。

然后会继续循环,下一次循环会走扩容逻辑。

扩容逻辑进入下面这个块,如果cas能够成功加锁,并且cells还是原来的cells,创建一个长度为原来2倍的累加单元数组,将原数组元素拷贝到新数组,再将新数组赋值给cells,最后解锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值