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

简介: 对于 Java 部分的面试来说,突然想到并发这一块的内容是不太完整的,这篇文章会通篇把多线程和并发都大致阐述一遍,至少能够达到了解原理和使用的目的,内容会比较多,从最基本的线程到我们常用的类会统一说一遍,慢慢看。

原子类

多线程环境下操作变量,除了可以用我们上面一直说的加锁的方式,还有其他更简单快捷的办法吗?

JDK1.5之后引入的原子操作包下面的一些类提供给了我们一种无锁操作变量的方式,这种通过CAS操作的方式更高效并且线程安全。

c644a8c4e7b4b93f8fb3d62a30faa67d.jpg原子类

基本数据类型


我们先说针对基本数据类型提供的AtomicIntegerAtomicLongAtomicBoolean,看名字都知道是干嘛的,由于基本上没什么区别,以AtomicInteger的方法举例来说明。

public final int getAndIncrement(); //旧值+1,返回旧值
public final int getAndDecrement(); //旧值-1,返回旧值
public final int getAndAdd(int delta); //旧值+delta,返回旧值
public final int getAndSet(int newValue); //旧值设置为newValue,返回旧值
public final int getAndAccumulate(int x,IntBinaryOperator accumulatorFunction); //旧值根据传入方法进行计算,返回旧值
public final int getAndUpdate(IntUnaryOperator updateFunction)//旧值根据传入进行计算,返回旧值

与之相对应的还有一套方法比如incrementAndGet()等等,规则完全一样,只是返回的是新值。

我们看看下面的例子,针对自定义规则传参,比如我们可以把计算规则改成乘法。

public class AtomicIntegerTest {
    public static void main(String[] args) {
        AtomicInteger atomic = new AtomicInteger(10);
        System.out.println(atomic.getAndIncrement()); //10
        System.out.println(atomic.getAndDecrement()); //11
        System.out.println(atomic.getAndAdd(2));//10
        System.out.println(atomic.getAndSet(10)); //12
        System.out.println(atomic.get());             //10
        System.out.println("=====================");
        System.out.println(atomic.getAndAccumulate(3, (left, right) -> left * right)); // 10
        System.out.println(atomic.get()); //30
        System.out.println(atomic.getAndSet(10)); //30
        System.out.println("=====================");
        System.out.println(atomic.getAndUpdate(operand -> operand * 20)); // 10
        System.out.println(atomic.get()); //200
    }
}

另外提到一嘴,基本数据类型只给了Integer、Long、Boolean,那其他的基本数据类型呢?其实看下AtomicBoolean的源码我们发现其实他本质上是转成了Integer处理的,那么针对其他的类型也可以参考这个思路来实现。

数组


针对数组类型的原子操作提供了3个,可以方便的更新数组中的某个元素。

AtomicIntegerArray:针对Integer数组的原子操作。

AtomicLongArray:针对Long数组的原子操作。

AtomicReferenceArray:针对引用类型数组的原子操作。

和上面说的Atomic其实也没有太大的区别,还是以AtomicIntegerArray举例说明,主要方法也基本一样。

public final int getAndIncrement(int i);
public final int getAndDecrement(int i);
public final int getAndAdd(int i, int delta);
public final int getAndSet(int i, int newValue);
public final int getAndAccumulate(int i, int x,IntBinaryOperator accumulatorFunction);
public final int getAndUpdate(int i, IntUnaryOperator updateFunction);

操作一模一样,只是多了一个参数表示当前索引的位置,同样有incrementAndGet等一套方法,返回最新值,没有区别,对于引用类型AtomicReferenceArray来说只是没有了increment和decrement这些方法,其他的也都大同小异,不再赘述。

说实话,这个都没有举栗子的必要。

public class AtomicIntegerArrayTest {
    public static void main(String[] args) {
        int[] array = {10};
        AtomicIntegerArray atomic = new AtomicIntegerArray(array);
        System.out.println(atomic.getAndIncrement(0)); //10
        System.out.println(atomic.get(0));//11
        System.out.println(atomic.getAndDecrement(0)); //11
        System.out.println(atomic.getAndAdd(0, 2));//10
        System.out.println(atomic.getAndSet(0, 10)); //12
        System.out.println(atomic.get(0));             //10
        System.out.println("=====================");
        System.out.println(atomic.getAndAccumulate(0, 3, (left, right) -> left * right)); // 10
        System.out.println(atomic.get(0)); //30
        System.out.println(atomic.getAndSet(0, 10)); //30
        System.out.println("=====================");
        System.out.println(atomic.getAndUpdate(0, operand -> operand * 20)); // 10
        System.out.println(atomic.get(0)); //200
    }
}

引用类型


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


相关文章
|
JavaScript 前端开发
nodejs实现解析chm文件列表,无需转换为PDF文件格式,在线预览chm文件以及目录,不依赖任何网页端插件
nodejs实现解析chm文件列表,无需转换为PDF文件格式,在线预览chm文件以及目录,不依赖任何网页端插件
|
JSON 数据格式
【异常】com.alibaba.fastjson.JSONException: unclosed string : U
【异常】com.alibaba.fastjson.JSONException: unclosed string : U
2777 0
|
8月前
|
存储 分布式计算 负载均衡
数据分布式存储:在海量数据面前,我们如何站稳脚跟?
数据分布式存储:在海量数据面前,我们如何站稳脚跟?
1248 1
|
8月前
|
人工智能 自然语言处理 程序员
AI 程序员上线,一个全新的时代就要来了
随着生成式AI技术的发展,编程领域迎来了新的变革。英伟达CEO黄仁勋曾表示未来编程可交由AI完成,引发热议。然而,AI目前更多是作为程序员的助手存在。阿里云的“通义灵码”已在国内某互联网大厂上岗,担任代码助理角色,好评率超80%。它能7x24小时辅助编写、调试、优化代码,大幅提高开发效率。宏哥在视频中展示了其根据上下文续写代码及智能排查异常报错的能力,感兴趣的可以自行体验。
417 6
|
消息中间件 存储 负载均衡
两个实验让我彻底弄懂了「订阅关系一致」
这篇文章,笔者想聊聊 RocketMQ 最佳实践之一:**保证订阅关系一致**。 订阅关系一致指的是同一个消费者 Group ID 下所有 Consumer 实例所订阅的 Topic 、Tag 必须完全一致。 如果订阅关系不一致,消息消费的逻辑就会混乱,甚至导致消息丢失。
两个实验让我彻底弄懂了「订阅关系一致」
|
11月前
|
SQL 人工智能 分布式计算
MaxFrame 产品深度评测
本文全面评测了 MaxFrame,这款新兴的 Python 分布式计算框架,涵盖其在分布式 Pandas 处理、大语言模型数据处理等方面的优势。通过实际案例和用户体验,展示了 MaxFrame 在企业业务和个人学习中的重要作用,并与其他工具进行了对比,指出了其优点和改进空间。
|
Ubuntu 网络安全 Apache
Ubuntu下安装Apache2.4.7遇到的问题及解决办法
Ubuntu下安装Apache2.4.7遇到的问题及解决办法
227 2
|
数据采集 存储 人工智能
cdga|数据治理:应对核心业务数据质量参差不齐的挑战与策略
数据治理是指通过制定并实施一系列政策、流程和技术手段,确保数据的可用性、完整性、准确性和安全性,以支持企业的决策和业务运营。对于核心业务数据质量参差不齐的问题,数据治理的重要性不言而喻
|
计算机视觉
【YOLOv8改进】Shape-IoU:考虑边框形状与尺度的指标(论文笔记+引入代码)
YOLO目标检测专栏探讨了边框回归损失的创新方法,强调了目标形状和尺度对结果的影响。提出的新方法Shape-IoU关注边框自身属性,通过聚焦形状和尺度提高回归精度。实验显示,该方法提升了检测效果,超越现有技术,在多个任务中达到SOTA。论文和代码已公开。
|
Java 测试技术 API
技术笔记:UML的9种图例解析(转)
技术笔记:UML的9种图例解析(转)