xiaobaoqiu Blog

Think More, Code Less

Java Atomic

在多线程环境下,Java里面的++i或者–i操作不是线程安全的,因为它其实包含了三个操作:获取当前值,++或者–,写回. 在没有额外资源可以利用的情况下,只能使用加锁才能保证三个操作时“原子性”的.

在Java中,协调对线程间共享字段的访问的传统方法是使用同步,确保完成对共享字段的所有访问,同时具有适当的锁定。带来的问题主要是锁定竞争太厉害(线程常常在其他线程具有锁定时要求获得该锁定那么该线程将被阻塞,直到该锁定可用),会损害吞吐量,因为竞争的同步非常昂贵,另外还存在死锁等问题。对于现代 JVM 而言,无竞争的同步现在非常便宜。

java.util.concurrent.atomic包就是提供原子操作的类的小工具包,支持在单个变量上不用锁定(Lock-Free)的线程安全编程。

1.Atomic整体介绍

1.1 Boolean,Integer,Long和Reference的Atomic版本:

AtomicBoolean
AtomicInteger
AtomicLong
AtomicReference

1.2 Integer,Long,Reference的数组版本:

AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray

1.3 基于反射的原子更新字段操作:

AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater

约束:

1. 字段必须是volatile类型的;
2. 字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段;
3. 只能是实例变量,不能是类变量(static变量);
4. 只能是可修改变量,不能使final变量,因为final的语义就是不可修改,实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在;
5. 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long),如果要修改包装类型就需要使用AtomicReferenceFieldUpdater;

1.4 AtomicMarkableReference与AtomicStampedReference

AtomicMarkableReference
AtomicStampedReference

AtomicMarkableReference类描述的一个<Object,Boolean>的对,可以原子的修改Object或者Boolean的值,这种数据结构在一些缓存或者状态描述中比较有用。这种结构在单个或者同时修改Object/Boolean的时候能够有效的提高吞吐量。

AtomicStampedReference类维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。对比AtomicMarkableReference 类的<Object,Boolean>,AtomicStampedReference 维护的是一种类似<Object,int>的数据结构,其实就是对对象(引用)的一个并发计数。但是与AtomicInteger 不同的是,此数据结构可以携带一个对象引用(Object),并且能够对此对象和计数同时进行原子操作。

在后面的章节中会提到“ABA问题”,而AtomicMarkableReference/AtomicStampedReference 在解决“ABA问题”上很有用。

2.AtomicInteger实现

其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法,从而避免了synchronized的高开销,执行效率大为提升。

native的意思是使用Unsafe类,Unsafe类的功能函数是native的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AtomicInteger extends Number implements java.io.Serializable {

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
      try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
      } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
}

内部使用unsafe实现,value存真正的int值,valueOffset存value在AtomicInteger对象内的内存偏移地址.

2.1 volatile语义

volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制.它确保对volatile字段的更新以可预见的方式告知其他的线程.

volatile包含以下语义:

(1).Java存储模型不会对valatile指令的操作进行重排,这个保证对volatile变量的操作时按照指令的出现顺序执行的.
(2).volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果.也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果.

尽管volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果).

2.2 常用函数

getAndIncrement()相当于i++

incrementAndGet()相当于++i

getAndDecrement()相当于i–

decrementAndGet()相当于–i

2.3 特殊函数

2.3.1 set() VS lazySet

set设置为给定值,直接修改原始值;

lazySet延时设置变量值,这个等价于set()方法,但是由于value是volatile类型的,因此此字段的修改会比普通字段(非volatile字段)有稍微的性能延时(尽管可以忽略),所以如果不是想立即读取设置的新值,允许在“后台”修改值,那么此方法就很有用.

2.3.2 compareAndSet() VS weakCompareAndSet

接受2个参数,一个是期望数据(expected),一个是新数据(new);如果atomic里面的数据和期望数据一 致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。

按照JSR规范,调用weakCompareAndSet时并不能保证不存在happen- before的发生(也就是可能存在指令重排序导致此操作失败).

实际实现中,compareAndSet()和weakCompareAndSet没有区别:

1
2
3
4
5
6
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final boolean weakCompareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

3.CAS操作

在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁.

锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

3.1 悲观锁和乐观锁

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁;

而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

3.2 比较并交换

上面的乐观锁用到的机制就是比较并交换(CAS,Compare and Swap).

CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法可以对该操作重新计算。

3.3 硬件同步原语

大多数现代处理器都包含对多处理的支持。当然这种支持包括多处理器可以共享外部设备和主内存,同时它通常还包括对指令系统的增加来支持多处理的特殊要求。特别是,几乎每个现代处理器都有通过可以检测或阻止其他处理器的并发访问的方式来更新共享变量的指令.

拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。

首先毫无以为,在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。这样才获取变量的值的时候才能直接读取。

1
2
3
4
5
private volatile int value;
...
public final int get() {
        return value;
}

然后来看看++i是怎么做到的。

1
2
3
4
5
6
7
8
public final int incrementAndGet() {
    for (;;) {  //重试
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。

而compareAndSet利用JNI来完成CPU指令的操作。

1
2
3
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。

4.指令重排

Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致,这个过程通过叫做指令的重排序。

指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。

5.Happen-Before

Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。

举例来说,假设存在如下三个线程,分别执行对应的操作:

线程A中执行如下操作:i=1
线程B中执行如下操作:j=i
线程C中执行如下操作:i=2

假设线程A中的操作”i=1“ happen—before线程B中的操作“j=i”,那么就可以保证在线程B的操作执行后,变量j的值一定为1,即线程B观察到了线程A中操作“i=1”所产生的影响;现在,我们依然保持线程A和线程B之间的happen—before关系,同时线程C出现在了线程A和线程B的操作之间,但是C与B并没有happen—before关系,那么j的值就不确定了,线程C对变量i的影响可能会被线程B观察到,也可能不会,这时线程B就存在读取到不是最新数据的风险,不具备线程安全性。

6.ABA 问题

CAS操作通常存在ABA问题.

因为在更改 V 之前,CAS 主要询问“V 的值是否仍为 A”,所以在第一次读取 V 以及对 V 执行 CAS 操作之前,如果将值从 A 改为 B,然后再改回 A,会使基于 CAS 的算法混乱。在这种情况下,CAS 操作会成功,但是在一些情况下,结果可能不是您所预期的。这类问题称为 ABA 问题,通常通过将标记或版本编号与要进行 CAS 操作的每个值相关联,并原子地更新值和标记,来处理这类问题。AtomicStampedReference 类支持这种方法.

7.参考

https://www.ibm.com/developerworks/cn/java/j-jtp11234/