并发编程从入门到放弃系列开始和结束(三)

发布于 2022-6-13 17:41
浏览
0收藏

 

引用类型
像AtomicInteger那种,只能原子更新一个变量,如果需要同时更新多个变量,就需要使用我们的引用类型的原子类,针对引用类型的原子操作提供了3个。

AtomicReference:针对引用类型的原子操作。

AtomicMarkableReference:针对带有标记位的引用类型的原子操作。

AtomicStampedReference:针对带有标记位的引用类型的原子操作。

AtomicMarkableReference和AtomicStampedReference非常类似,他们是为了解决CAS中的ABA的问题(别说你不知道啥是ABA问题),只不过这个标记的类型不同,我们看下源码。

AtomicMarkableReference标记类型是布尔类型,所以其实他版本就俩,true和false。

AtomicMarkableReference标记类型是整型,那可不就是正常的版本号嘛。

public class AtomicMarkableReference<V> {

    private static class Pair<T> {
        final T reference;
        final boolean mark; //标记
    }
}

public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference;
        final int stamp; // 标记
    }
}

方法还是那几个,老样子。

public final V getAndSet(V newValue);
public final V getAndUpdate(UnaryOperator<V> updateFunction);
public final V getAndAccumulate(V x, BinaryOperator<V> accumulatorFunction);
public final boolean compareAndSet(V expect, V update);

简单举个栗子:

public class AtomicReferenceTest {
    public static void main(String[] args) {
        User user = new User(1L, "test", "test");
        AtomicReference<User> atomic = new AtomicReference<>(user);

        User pwdUpdateUser = new User(1L,"test","newPwd");
        System.out.println(atomic.getAndSet(pwdUpdateUser));
        System.out.println(atomic.get());
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @ToString
    static class User {
        private Long id;
        private String username;
        private String password;
    }
}
//输出
AtomicReferenceTest.User(id=1, username=test, password=test)
AtomicReferenceTest.User(id=1, username=test, password=newPwd)

对象属性
针对对象属性的原子操作也还是提供了3个。

AtomicIntegerFieldUpdater:针对引用类型里的整型属性的原子操作。

AtomicLongFieldUpdater:针对引用类型里的长整型属性的原子操作。

AtomicReferenceFieldUpdater:针对引用类型里的属性的原子操作。

需要注意的是,需要更新的属性字段不能是private,并且必须用volatile修饰,否则会报错。

举个栗子:

public class AtomicReferenceFieldTest {
    public static void main(String[] args) {
        AtomicReferenceFieldUpdater<User, String> atomic = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "password");
        User user = new User(1L, "test", "test");
        System.out.println(atomic.getAndSet(user, "newPwd"));
        System.out.println(atomic.get(user));
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @ToString
    static class User {
        private Long id;
        private String username;
        volatile String password;
    }
}
//输出
test
newPwd

累加器
累加器有4个,都来自JDK1.8新增的,为啥新增呢?因为Doug大佬觉得AtomicLong还不够快,虽然说通过CAS操作已经很快了,但是众所知周,高并发同时操作一个共享变量只有一个成功,那其他的线程都在无限自旋,大量的浪费了CPU的资源,所以累加器Accumulator的思路就是把一个变量拆成多个变量,这样多线程去操作竞争多个变量资源,性能不就提升了嘛。

也就是说,在高并发的场景下,可以尽量的使用下面这些类来替换基础类型操作的那些AtomicLong之类的,可以提高性能。

LongAdder:Long类型的累加,LongAccumulator的特例。

LongAccumulator:Long类型的累加。

DoubleAdder:Double类型的累加,DoubleAccumulator的特例。

DoubleAccumulator:Double类型的累加。

由于LongAdder和DoubleAdder都是一样的,我们以LongAdder和LongAccumulator举例来说明它的一些简单的原理。

LongAdder

它继承自Striped64,内部维护了一个Cell数组,核心思想就是把单个变量的竞争拆分,多线程下如果一个Cell竞争失败,转而去其他Cell再次CAS重试。

transient volatile Cell[] cells;
transient volatile long base;

在计算当前值的时候,则是累加所有cell的value再加上base。

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

这里还涉及到一个伪共享的概念,至于啥是伪共享,看看之前我写的真实字节二面:什么是伪共享?。

解决伪共享的真正的核心就在Cell数组,可以看到,Cell数组使用了Contented注解。

@sun.misc.Contended static final class Cell {
 volatile long value;
 Cell(long x) { value = x; }
}

在上面我们提到数组的内存地址都是连续的,所以数组内的元素经常会被放入一个缓存行,这样的话就会带来伪共享的问题,影响性能,这里使用Contented进行填充,就避免了伪共享的问题,使得数组中的元素不再共享一个缓存行。

LongAccumulator

上面说到,LongAdder其实就是LongAccumulator的一个特例,相比LongAdder他的功能会更加强大,可以自定义累加的规则,在上面演示AtomicInteger功能的时候其实我们也使用过了。

*** ***,实际上就是实现了一个LongAdder的功能,初始值我们传入0,而LongAdder的初始值就是0并且只能是0。

public class LongAdderTest {
    public static void main(String[] args) {
        LongAdder longAdder = new LongAdder();
        LongAccumulator accumulator = new LongAccumulator((left, right) -> 0, 0);
    }
}

 

工具类&容器类

这里要说到一些我们在平时开发中经常使用到的一些类以及他们的实现原理。

并发编程从入门到放弃系列开始和结束(三)-开源基础软件社区

 工具类&容器类


CountDownLatch
CountDownLatch适用于在多线程的场景需要等待所有子线程全部执行完毕之后再做操作的场景。

假设现在我们有一个业务场景,我们需要调用多个RPC接口去查询数据并且写入excel,最后把所有excel打包压缩发送邮件出去。

public class CountDownLatchTest {
    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CountDownLatch countDownLatch = new CountDownLatch(2);
        executorService.submit(()->{
            try {
                Thread.sleep(1000);
                System.out.println("写excelA完成");
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        executorService.submit(()->{
            try {
                Thread.sleep(3000);
                System.out.println("写excelB完成");
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        System.out.println("等待excel写入完成");
        countDownLatch.await();
        System.out.println("开始打包发送数据..");

        executorService.shutdown();

    }
}
//输出
等待excel写入完成
写excelA完成
写excelB完成
开始打包发送数据..

 

整个过程如下:

初始化一个CountDownLatch实例传参2,因为我们有2个子线程,每次子线程执行完毕之后调用countDown()方法给计数器-1,主线程调用await()方法后会被阻塞,直到最后计数器变为0,await()方法返回,执行完毕。

他和join有个区别,像我们这里用的是ExecutorService创建线程池,是没法使用join的,相比起来,CountDownLatch的使用会显得更加灵活。

CountDownLatch基于AQS实现,用volatile修饰state变量维持倒数状态,多线程共享变量可见。

  1. CountDownLatch通过构造函数初始化传入参数实际为AQS的state变量赋值,维持计数器倒数状态
  2. 当主线程调用await()方法时,当前线程会被阻塞,当state不为0时进入AQS阻塞队列等待。
  3. 其他线程调用countDown()时,通过CAS修改state值-1,当state值为0的时候,唤醒所有调用await()方法阻塞的线程

 

文章转自公众号:艾小仙

分类
标签
已于2022-6-13 17:41:51修改
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐