深入了解AtomicXXX

2017-07-01  本文已影响0人  EnjoyTheLife

接触Java有段时间了,基本的原理和使用大概清楚了,想通过阅读源码来进一步提升Java能力,听说Doug Lea的java.util.concurrent包很值得一读,所以就产生了这篇文章。

Atomic包是java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类。这个包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。

下面这张图是java.util.concurrent.atomic包下的类结构,总共12个类,可以按照一定的类别分成4组:

在具体了解每个类的实现前,我们先了解下这些类共同依赖的基础类Unsafe。这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果。

下面以AtomicInteger为例将其源码走一遍,详细介绍每段代码的实现逻辑和功能。

screenshot

第一行代码是获取Unsafe类的实例的,Unsafe是原子操作的基础类,也就是所有的原子操作都是基于unsafe来实现的。而valueOffset表示AtomicInteger实例中的value属性在内存中的地址。

screenshot

上面这几行代码是用来获取AtomicInteger实例中的value属性在内存中的位置。这里使用了Unsafe的objectFieldOffset方法。这个方法是一个本地方法, 该方法用来获取一个给定的静态属性的位置。

screenshot

这个非常简单,每个AtomicInteger实例都会存放一个值,这个值就用变量value来表示。细心的你一定注意到了volatile这个关键字,惭愧啊,写了一年Java也没场景使用过这个东东。根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。所以,同一变量的值在工作内存和主存中可能不一致。volatile其实是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我。

screenshot

两个构造函数,如果带参数,就将参数赋值给AtomicInteger实例的value属性。

screenshot

value属性的get和set方法,由于value属性上添加了volatile关键字,所以value的读写操作是无须加锁的。

screenshot

方法getAndSet内部调用了compareAndSet,所以我们先了解下compareAndSet的原理,其实Atomic的基础是CAS,那么什么是CAS,系下面是来自维基百科的解释。

In computer science, the compare-and-swap CPU instruction ("CAS") (or the Compare & Exchange - CMPXCHG instruction in the x86 and Itanium architectures) is a special instruction that atomically (regarding intel x86, lock prefix should be there to make it really atomic) compares the contents of a memory location to a given value and, only if they are the same, modifies the contents of that memory location to a given new value. This guarantees that the new value is calculated based on up-to-date information; if the value had been updated by another thread in the meantime, the write would fail. The result of the operation must indicate whether it performed the substitution; this can be done either with a simple Boolean response (this variant is often called compare-and-set), or by returning the value read from the memory location (not the value written to it). Compare-and-Swap (and Compare-and-Swap-Double) has been an integral part of the IBM 370(and all successor) architectures since 1970. The operating systems which run on these architectures make extensive use of Compare-and-Swap (and Compare-and-Swap-Double) to facilitate process (i.e., system and user tasks) and processor (i.e., central processors) parallelism while eliminating, to the greatest degree possible, the "disabled spin locks" which were employed in earlier IBM operating systems. In these operating systems, new units of work may be instantiated "globally", into the Global Service Priority List, or "locally", into the Local Service Priority List, by the execution of a single Compare-and-Swap instruction. This dramatically improved the responsiveness of these operating systems.

CAS是硬件CPU提供的元语,它的原理是:我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。而上图中的compareAndSet就是调用CAS元语完成的。

screenshot

这两个方法是value属性的自增自减操作,由于volatile,value的get和set不需要加锁的,那为什么自增自减操作需要通过CAS完成呢?仔细观察incrementAndGet()方法,发现自增操作其实拆成了两步完成的


screenshot

由于valatile只能保证读取或写入的是最新值,那么可能出现以下情况:

  1. A线程执行get()操作,获取current值(假设为1)
  2. B线程执行get()操作,获取current值(为1)
  3. B线程执行next = current + 1操作,next = 2
  4. A线程执行next = current + 1操作,next = 2

这样的结果明显不是我们想要的,所以,自增操作必须采用CAS来完成。

在阅读源码的过程中,还发现了一些不太容易理解的方法,比如下面这个


screenshot

既然已经有set,为什么还有个lazySet,困惑不懂,马上google,非常幸运,找到了原作者的解释

"As probably the last little JSR166 follow-up for Mustang, we added a "lazySet" method to the Atomic classes (AtomicInteger, AtomicReference, etc). This is a niche method that is sometimes useful when fine-tuning code using non-blocking data structures. The semantics are that the write is guaranteed not to be re-ordered with any previous write, but may be reordered with subsequent operations (or equivalently, might not be visible to other threads) until some other volatile write or synchronizing action occurs).
The main use case is for nulling out fields of nodes in non-blocking data structures solely for the sake of avoiding long-term garbage retention; it applies when it is harmless if other threads see non-null values for a while, but you'd like to ensure that structures are eventually GCable. In such cases, you can get better performance by avoiding the costs of the null volatile-write. There are a few other use cases along these lines for non-reference-based atomics as well, so the method is supported across all of the AtomicX classes.
For people who like to think of these operations in terms of machine-level barriers on common multiprocessors, lazySet provides a preceeding store-store barrier (which is either a no-op or very cheap on current platforms), but no store-load barrier (which is usually the expensive part of a volatile-write)."

screenshot

weakCompareAndSet( )方法和compareAndSet( )类似,都是conditional modifier方法。这2个方法接受2个参数,一个是期望数据(expected),一个是新数据(new);如果atomic里面的数据和期望数据一 致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。JSR规范中说:以原子方式读取和有条件地写入变量但不 创建任何 happen-before 排序,因此不提供与除 weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。大意就是说调用weakCompareAndSet时并不能保证不存在happen- before的发生(也就是可能存在指令重排序导致此操作失败)。但是从Java源码来看,其实此方法并没有实现JSR规范的要求,最后效果和 compareAndSet是等效的,都调用了unsafe.compareAndSwapInt()完成操作。

至于其他类,大致原理都差不多,下面列举下阅读源码中发现的一些差异:

数组类型 AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

更新器类型 AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

复合变量类型 AtomicMarkableReference,AtomicStampedReference

上一篇下一篇

猜你喜欢

热点阅读