资讯专栏INFORMATION COLUMN

一文读懂 Java 中的原子类

jas0n / 1060人阅读

摘要:一无锁方案并发包中的原子类都是基于无锁方案实现的,相较于传统的互斥锁,无锁并没有加锁解锁线程切换的消耗,因此无锁解决方案的性能更好,同时无锁还能够保证线程安全。线程首先读取的值并加,如果此时有另一个线程更新了,则期望值和不相等,更新失败。

一、无锁方案

Java 并发包中的原子类都是基于无锁方案实现的,相较于传统的互斥锁,无锁并没有加锁、解锁、线程切换的消耗,因此无锁解决方案的性能更好,同时无锁还能够保证线程安全。

1. 无锁方案的实现原理

无锁主要依赖 CAS(Compare And Swap) ,即比较并交换,CAS 是一条 CPU 指令,其本身是能够保证原子性的。CAS 中有三个参数:

共享变量的内存地址 A

用于比较的值 B

共享变量的新值 C

public class SimpleCAS {

    private int value;

    public synchronized int cas(int expectVal, int newVal){
        int curVal = value;
        if (expectVal == curVal){
            value = newVal;
        }
        return curVal;
    }
}

上面的代码展示了 CAS 的简单实现,从内存中读出当前 value 的值,并且需要判断,期望值 expectVal == curVal 的时候,才会将 value 更新为新值。

仍然以上面的代码,来实现一个简单的,基于 CAS 的线程安全的 value+1 方法。这里的 cas 方法仅用于帮助理解,所以执行结果可能有出入。

public class SimpleCAS {

    private volatile int value;

    public void addValue(){
        int newVal = value + 1;
        while (value != cas(value, newVal)){
            newVal = value + 1;
        }
    }
    
    private synchronized int cas(int expectVal, int newVal){
        int curVal = value;
        if (expectVal == curVal){
            value = newVal;
        }
        return curVal;
    }
}

线程首先读取 value 的值并加 1,如果此时有另一个线程更新了 value,则期望值和 value 不相等,更新失败。更新失败后,循环尝试,重新读取 value 的值,直到更新成功退出循环。

2. ABA 问题

无锁的实现方案中需要注意的一个问题便是 ABA 问题。

例如上面的代码,value 的初始值为 0,线程 t1 取到了 value 的值,并将其更新为 1,然后线程又将 value 更新为 0。

假如这个过程中有另外一个线程 t2,和 t1 同时取初始值为 0 的 value,t2 在 t1 执行完后更新 value,这个时候 value 虽然还是为 0,但已经被 t1 修改过了。

大多数情况下,我们并不需要关心 ABA 问题,例如数值型数据的加减,但是对象类型的数据遇到了 ABA 问题的话,可能前后的属性已经发生了变化,所以需要解决。

解决的办法也很简单,给对象类型的数据加上一个版本号即可,每更新一次,版本号加 1,这样即使对象数据从 A 变成 B 后 又变成 A,但是版本号是递增的,就可以分辨出对象还是被修改过的。

二、原子类 1. 原子化基本数据类型

有三个实现类:AtomicBoolean、AtomicInteger、AtomicLong

常用的方法如下,以 AtomicInteger 为例,其他的类似:

AtomicInteger i = new AtomicInteger(0);

i.getAndSet(int newValue);//获取当前值并设置新值

i.getAndIncrement();//相当于 i ++
i.incrementAndGet();//相当于 ++ i

i.getAndDecrement();//相当于 i --
i.decrementAndGet();//相当于 -- i

i.addAndGet(int delta);//相当于 i + delta,并返回添加后的值
i.getAndAdd(int delta);//相当于 i + delta,并返回添加前的值

i.compareAndSet(int expect, int update);//CAS 操作,返回 boolean值,表示是否更新成功

i.getAndUpdate(update -> 10);//通过函数更新值
i.updateAndGet(update -> 10);//类似上面
2. 原子化对象引用类型

实现类分别是:AtomicReference、AtomicStampedReference、AtomicMarkableReference,其中后两个可以实现了解决 ABA 问题的方案。

AtomicReference 常用的方法如下:

//假设有一个叫做 Order 的类
AtomicReference orderReference = new AtomicReference<>();

orderReference.getAndSet(Order newValue);//获取并设置

orderReference.set(Order order);//设置值
Order order1 = orderReference.get();//获取对象

orderReference.compareAndSet(Order expect, Order update);//比较交换

orderReference.getAndUpdate();//通过函数更新值
orderReference.updateAndGet();

AtomicStampedReference 需要传入初始值和初始 stamp,其中 stamp 相当于对象的版本号(用来解决 ABA 问题),使用示例如下:

AtomicStampedReference reference = new AtomicStampedReference<>("roseduan", 0);

//尝试修改stamp的值
boolean b = reference.attemptStamp(reference.getReference(), 10);

//获取值
String str = reference.getReference();

//获取stamp
int stamp = reference.getStamp();

//重新设置值和stamp
reference.set("I am not roseduan", 20);

//比较交换
boolean b1 = reference.compareAndSet("roseduan", "jack", 20, 0);

AtomicMarkableReference 使用一个 mark 标记(boolean 类型) 代替了 AtomicStampedReference 中的 stamp,用这种更简单的方式来解决 ABA 问题。使用的方式和上面的类似,只是将方法中的 stamp 变为 boolean 类型的值即可。

3. 原子化数组类型

实现类有三个:

AtomicIntegerArray:原子化的整型数组

AtomicLongArray:原子化长整型数组

AtomicReferenceArray:原子化对象引用数组

使用和原子化基本类型都是差不多的,只是需要在方法中加上数组下标即可。

4. 原子化对象属性更新器

也有三个实现类:

AtomicIntegerFieldUpdater:更新对象的整型属性

AtomicLongFieldUpdater:更新对象的长整型属性

AtomicReferenceFieldUpdater:更新对象的引用型属性

这三个类都是利用 Java 的反射机制实现的,并且为了保证原子性,要求被更新的对象的属性必须是 volatile 类型的。使用示例如下:

@Data
@Builder
public class User {

    private volatile int age;

    private volatile long number;

    private volatile String name;

    public static void main(String[] args) {
        User user = User.builder().age(22).number(15553663L).name("roseduan").build();

        //更新age属性的值
        AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
        integerFieldUpdater.set(user, 25);

        //更新number属性的值
        AtomicLongFieldUpdater longFieldUpdater = AtomicLongFieldUpdater.newUpdater(User.class, "number");
        longFieldUpdater.set(user, 1000101L);

        //更新对象类型的属性的值
        AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
        referenceFieldUpdater.set(user, "I am not roseduan");

        System.out.println(user.toString());
    }
}

程序中创建了一个 User 类,有三个属性 age、number、name 分别对应整型、长整型、引用类型。然后使用对象属性更新器进行属性值的更新,更新器的其他方法的使用和前面说到的几种原子化类型类似。

5. 原子化累加器

实现类有四个:

DoubleAdder

DoubleAccumulator

LongAdder

LongAccumulator

这几个类的功能有限,仅用来执行累加操作,但是速度非常快。下面介绍 DoubleAdder 和 DoubleAccumulator 的用法,其余两个类似。

//DoubleAccumulator使用示例
DoubleAccumulator a = new DoubleAccumulator(Double::sum, 0);//设初始值为0
//累加
a.accumulate(1);
a.accumulate(2);
a.accumulate(3);
a.accumulate(4);

System.out.println(a.get());//输出10

//DoubleAdder使用示例
DoubleAdder adder = new DoubleAdder();
adder.add(1);
adder.add(2);
adder.add(3);
adder.add(4);
adder.add(5);

System.out.println(adder.intValue());//输出15

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74612.html

相关文章

  • 从源码入手,一文带你读懂Spring AOP面向切面编程

    摘要:,,面向切面编程。,切点,切面匹配连接点的点,一般与切点表达式相关,就是切面如何切点。例子中,注解就是切点表达式,匹配对应的连接点,通知,指在切面的某个特定的连接点上执行的动作。,织入,将作用在的过程。因为源码都是英文写的。 之前《零基础带你看Spring源码——IOC控制反转》详细讲了Spring容器的初始化和加载的原理,后面《你真的完全了解Java动态代理吗?看这篇就够了》介绍了下...

    wawor4827 评论0 收藏0
  • 一文读懂微服务架构的重构策略

    摘要:相反,它由单体中的适配器和使用一个或多个进程间通信机制的服务组成。因为微服务架构的本质是一组围绕业务功能组织的松耦合服务。如果你尝试将此类功能实现为服务,则通常会发现,由于过多的进程间通信而导致性能下降。这是快速展示微服务架构价值的好方法。你很有可能正在处理大型复杂的单体应用程序,每天开发和部署应用程序的经历都很缓慢而且很痛苦。微服务看起来非常适合你的应用程序,但它也更像是一项遥不可及的必杀...

    jaysun 评论0 收藏0
  • IaaS的演进!一文读懂裸金属和容器即服务

    摘要:英特尔机架规模设计则能实现以机架为单位的软硬件解耦,为裸金属即服务提供容量更大的资源池,并可通过开放的和协议如和,高效发掘管理和调配这些资源。 江湖上,一直流传着得IaaS(基础设施即服务),得公有云天下的说法。想握紧IaaS这柄云端杀手锏, 大热的裸金属即服务和容器即服务,还不了解一下? 它们为什么如此受人...

    MadPecker 评论0 收藏0
  • 一文读懂Java线程状态转换

    摘要:前言本文描述线程线程状态及状态转换,不会涉及过多理论,主要以代码示例说明线程状态如何转换。被终止线程执行完毕正常结束或执行过程中因未捕获异常意外终止都会是线程进入被终止状态。线程执行完毕打印状态。 前言 本文描述Java线程线程状态及状态转换,不会涉及过多理论,主要以代码示例说明线程状态如何转换。 基础知识 1. 线程状态 线程可以有6种状态: New(新建) Runnable(可运...

    summerpxy 评论0 收藏0
  • 一文读懂链路追踪

    摘要:链路追踪链路追踪一词是在年提出的,当时谷歌发布了一篇论文,介绍了谷歌自研的分布式链路追踪的实现原理,还介绍了他们是怎么低成本实现对应用透明的。感兴趣的同学可以去深入了解一下链路追踪,希望本文对你有所帮助。 showImg(https://upload-images.jianshu.io/upload_images/13711841-f54b415cc8d07fdc?imageMogr2...

    JerryWangSAP 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<