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

相关文章
|
1月前
|
Java
Java 变量命名规则
4月更文挑战第6天
|
2月前
|
存储 Java 开发者
Java变量命名规则
Java变量命名规则
27 0
|
2月前
|
存储 缓存 算法
Java并发基础:原子类之AtomicMarkableReference全面解析
AtomicMarkableReference类能够确保引用和布尔标记的原子性更新,有效避免了多线程环境下的竞态条件,其提供的方法可以轻松地实现基于条件的原子性操作,提高了程序的并发安全性和可靠性。
Java并发基础:原子类之AtomicMarkableReference全面解析
|
4天前
|
存储 Java API
掌握8条方法设计规则,设计优雅健壮的Java方法
掌握8条方法设计规则,设计优雅健壮的Java方法
|
4天前
|
存储 安全 Java
掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
|
14天前
|
Java 开发者
Java变量命名规则
Java变量命名规则
19 0
|
20天前
|
Java
Java基础知识整理,驼峰规则、流程控制、自增自减
在这一篇文章中我们总结了包括注释、关键字、运算符的Java基础知识点,今天继续来聊一聊命名规则(驼峰)、流程控制、自增自减。
37 3
|
28天前
|
存储 Java C++
leetcode 2525 根据规则将箱子分类 c++ pyhton java c题解
leetcode 2525 根据规则将箱子分类 c++ pyhton java c题解
14 1
|
5月前
|
Java
Java | 类、实例初始化 、方法重写规则
Java | 类、实例初始化 、方法重写规则
25 0
|
2月前
|
安全 Java
Java中的基本类型原子类介绍
Java中的基本类型原子类介绍
16 1