Java的规则S2204规定,对于Java并发库定义的诸如AtomicInteger
、AtomicLong
等原子类,不能使用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的子类为例):
原子类的特性
原子类属于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个原子类,其中,AtomicInteger
、AtomicLong
、AtomicBoolean
以及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
记录当前链表的元素个数。