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

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

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


对规则的分析


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

public boolean equals(Object obj) {
    return (this == obj);
}

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

  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:

AtomicInteger aInt1 = new AtomicInteger(0);
AtomicInteger aInt2 = new AtomicInteger(0);
aInt1.equals(aInt2);   // 返回false

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

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

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

image.png


原子类的特性


原子类属于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>为例,该方法的定义为:

 public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }

其操作过程为:

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

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

 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记录当前链表的元素个数。

相关文章
|
7月前
|
Java
Java 变量命名规则
4月更文挑战第6天
|
7月前
|
存储 Java 开发者
Java变量命名规则
Java变量命名规则
129 0
|
7月前
|
存储 缓存 算法
Java并发基础:原子类之AtomicMarkableReference全面解析
AtomicMarkableReference类能够确保引用和布尔标记的原子性更新,有效避免了多线程环境下的竞态条件,其提供的方法可以轻松地实现基于条件的原子性操作,提高了程序的并发安全性和可靠性。
Java并发基础:原子类之AtomicMarkableReference全面解析
|
3月前
|
Java 程序员
java基础(5)标识符命名规则和命名规范
Java标识符命名规则包括只能使用数字、字母、下划线\_、$,且数字不能开头,不能使用关键字命名,且严格区分大小写。命名规范建议类名、接口名首字母大写,变量名、方法名首字母小写,常量名全大写。
75 2
|
3月前
|
Java
Java源文件声明规则详解
Java源文件的声明规则是编写清晰、可读且符合语法规范的Java程序的基础。这些规则包括文件名必须与公共类名相同、包声明位于文件顶部、导入声明紧跟其后、类声明需明确访问级别,并允许使用注释增强代码可读性。一个源文件可包含多个类,但只能有一个公共类。遵循这些规则有助于提升代码质量和维护性。
|
5月前
|
Java 开发者
Java实现基于清除后分配规则的垃圾回收器及其实现原理
通过上述简化模型的实现,我们可以理解基于清除后分配规则的垃圾回收器的基本工作原理。实际上,现代JVM中的垃圾回收器比这个例子复杂得多,它们可能包括更多阶段、优化策略,以及不同类型的垃圾回收器协同工作。然而,理解这一基本概念对于深入理解垃圾回收机制和内存管理非常有帮助。
24 3
|
7月前
|
存储 安全 Java
掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
|
5月前
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
51 0
|
5月前
|
监控 Java 开发者
Java面试题:解释Java内存模型中的内存顺序规则,Java中的线程组(ThreadGroup)的工作原理,Java中的FutureTask的工作原理
Java面试题:解释Java内存模型中的内存顺序规则,Java中的线程组(ThreadGroup)的工作原理,Java中的FutureTask的工作原理
28 0
|
6月前
|
Java
(JAVA) 电话、邮箱脱敏,带脱敏规则
(JAVA) 电话、邮箱脱敏,带脱敏规则
277 2