前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java规则:原子类的相等性判断

Java规则:原子类的相等性判断

作者头像
张逸
发布2023-03-23 18:13:40
1.1K0
发布2023-03-23 18:13:40
举报
文章被收录于专栏:斑斓斑斓

Java的规则S2204规定,对于Java并发库定义的诸如AtomicIntegerAtomicLong等原子类,不能使用equals()方法测试其值是否相等。

对规则的分析

倘若程序员只是一知半解地了解相等性的判断,反而不会违背这一规则。引用类型都有一个共同的父类Object,它的equals()仅仅比较了对象是否属于同一个实例,以此确定是否相等。该实现如代码所示:

代码语言:javascript
复制
public boolean equals(Object obj) {
    return (this == obj);
}

然而,对于像Integer、Long这样的包装类而言,深谙Java基础知识的程序员都知道它们作为Number的子类,重写了equals()hashcode()方法,使得对它们的相等性判断变得更简单。以Integer为例,在对其进行判等时,实际比较的是它包装的int值:

代码语言:javascript
复制
    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

由于原子类同样是Number的子类,也可以认为是对int、long等内建类型的包装,只不过具有并发访问的原子特性罢了,这就可能让Java程序员滋生一种误解,以为它们提供了和Integer、Long一样的判等行为。

可惜,这种推论是错误的,它们并没有重写Object的equals()方法。因此,在定义如下的两个原子对象时,它们的值虽然相等,equals()方法却会返回false:

代码语言:javascript
复制
AtomicInteger aInt1 = new AtomicInteger(0);
AtomicInteger aInt2 = new AtomicInteger(0);

aInt1.equals(aInt2);   // 返回false

正确做法是通过get()方法获得它包装的值,然后再进行相等性比较:

代码语言:javascript
复制
AtomicInteger aInt1 = new AtomicInteger(0);
AtomicInteger aInt2 = new AtomicInteger(0);

aInt1.get() == aInt2.get();  // 返回true

除了相等性不同之外,还要注意区分其他特性的不同。所有基本类型的包装类都是final类,也就是说这些类型都是不可修改的,但原子类不同,它的类定义没有声明final。这说明你可以通过定义这些原子类的子类来改变某些行为,例如重写eqauls()hashcode()方法,使其能够像基本类型的包装类那样进行判等操作。不过,为了避免破坏原子类的原子性,这些原子类的主要方法都是final方法。原子类的派生子类只能重写如下图所示的操作(以AtomicInteger的子类为例):

原子类的特性

原子类属于Java 5引入的并发包中的内容。Bruce Eckel认为:“这些类提供了原子性的更新能力,充分利用了现代处理器的硬件级原子性,实现了快速、无锁的操作。”保证操作的原子性是确保线程安全的有效手段。《Java并发编程实战》对原子操作进行了阐释:

假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。

以整数的累加操作++count来说,在Java语言中,它看起来只有一条语句,但实际上却是三个独立的操作:

  • 读取count的值
  • 将值加1
  • 将计算结果写入count

这样的操作称之为“组合操作”。如果无法保证组合操作的原子性,当AB两个线程同时访问++count语句时,就会出现A线程将count加1的同时,B线程也在执行加1的操作,读到的值却是A执行加1前的值,导致累加的值不准确。

原子类可以让这些组合操作以原子方式执行,例如AtomicInteger原子类提供的incrementAndGet()方法就是原子操作。

当然,我们也可以通过为组合操作加锁的方式来保证原子性,但锁是一种阻塞算法,对内部操作采用了独占方式,就使得操作不够高效。准确地说,是在竞争适中或偏低的情况下(相对于高度竞争而言,这才是真实的竞争情况),原子变量的性能超过锁的性能。Java并发库定义的原子类采用了支持CAS(Compare and Set,即比较并交换)的非阻塞机制,将发生竞争的范围缩小到单个变量上,因而,它比锁的粒度更细,量级更轻,有利于实现更加高效的并发代码。

Java并发库一共定义了12个原子类,其中,AtomicIntegerAtomicLongAtomicBoolean以及AtomicReference是最常用的原子类,它们都支持CAS。

AtomicInteger为例,它定义的诸如incrementAndGet()getAndIncrement()等方法,相当于是对整数i执行i++++i操作,但它们是原子操作,具有线程安全性。

对CAS的支持则体现为compareAndSet()方法,它相当于一种乐观锁的实现。以AtomicReference<T>为例,该方法的定义为:

代码语言:javascript
复制
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }

其操作过程为:

  • 如果给定的值(expect,即旧的预期值)等于内存值,则将内存值设置为更新值(update)
  • 更新成功返回true,若返回false,则说明内存值并不等于旧的预期值(可能其他线程已经更新了内存值)

可以通过循环判断该方法返回的值,如果为false,就继续取内存值和旧的预期值进行比较,直到返回true,则意味着更新成功。AtomicReference自己定义的getAndSet()方法就调用了它:

代码语言:javascript
复制
    public final V getAndUpdate(UnaryOperator<V> updateFunction) {
        V prev, next;
        do {
            prev = get();
            next = updateFunction.apply(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }

原子类在JDK 5一经推出,就得到并发编程者的青睐,并发库中的许多并发容器也大量使用了原子类,如ConcurrentHashMap<K, V>LinkedBlockingQueue<E>等。ConcurrentHashMap<K, V>使用了AtomicReference对Map中的值进行线程安全的更新操作,LinkedBlockingQueue<E>则使用了AtomicInteger记录当前链表的元素个数。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 逸言 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 对规则的分析
  • 原子类的特性
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档