Java8中CAS的增强

几天前,我偶然地将之前写的用来测试AtomicInteger和synchronized的自增性能的代码跑了一下,意外地发现AtomicInteger的性能比synchronized更好了,经过一番原因查找,有了如下发现:

在jdk1.7中,AtomicInteger的getAndIncrement是这样的:

[code lang=”java”]
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
[/code]

而在jdk1.8中,是这样的:

[code lang=”java”]
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
[/code]

可以看出,在jdk1.8中,直接使用了Unsafe的getAndAddInt方法,而在jdk1.7的Unsafe中,没有此方法。(PS:为了找出原因,我反编译了Unsafe,发现CAS的失败重试就是在getAndAddInt方法里完成的,我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码,但测试结果却跟jdk1.7的getAndIncrement一样慢,不知道Unsafe里面究竟玩了什么黑魔法,还请高人不吝指点)(补充:文章末尾已有推论)

通过查看AtomicInteger的源码可以发现,受影响的还有getAndAdd、addAndGet等大部分方法。

有了这次对CAS的增强,我们又多了一个使用非阻塞算法的理由。

最后给出测试代码,需要注意的是,此测试方法简单粗暴,compareAndSet的性能不如synchronized,并不能简单地说synchronized就更好,两者的使用方式是存在差异的,而且在实际使用中,还有业务处理,不可能有如此高的竞争强度,此对比仅作为一个参考,该测试能够证明的是,AtomicInteger.getAndIncrement的性能有了大幅提升。

[code lang=”java”]
package performance;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class AtomicTest {
//测试规模,调用一次getAndIncreaseX视作提供一次业务服务,记录提供TEST_SIZE次服务的耗时
private static final int TEST_SIZE = 100000000;
//客户线程数
private static final int THREAD_COUNT = 10;
//使用CountDownLatch让各线程同时开始
private CountDownLatch cdl = new CountDownLatch(THREAD_COUNT + 1);

private int n = 0;
private AtomicInteger ai = new AtomicInteger(0);
private long startTime;

public void init() {
startTime = System.nanoTime();
}

/**
* 使用AtomicInteger.getAndIncrement,测试结果为1.8比1.7有明显性能提升
* @return
*/
private final int getAndIncreaseA() {
int result = ai.getAndIncrement();
if (result == TEST_SIZE) {
System.out.println(System.nanoTime() – startTime);
System.exit(0);
}
return result;
}

/**
* 使用synchronized来完成同步,测试结果为1.7和1.8几乎无性能差别
* @return
*/
private final int getAndIncreaseB() {
int result;
synchronized (this) {
result = n++;
}
if (result == TEST_SIZE) {
System.out.println(System.nanoTime() – startTime);
System.exit(0);
}
return result;
}

/**
* 使用AtomicInteger.compareAndSet在java代码层面做失败重试(与1.7的AtomicInteger.getAndIncrement的实现类似),
* 测试结果为1.7和1.8几乎无性能差别
* @return
*/
private final int getAndIncreaseC() {
int result;
do {
result = ai.get();
} while (!ai.compareAndSet(result, result + 1));
if (result == TEST_SIZE) {
System.out.println(System.nanoTime() – startTime);
System.exit(0);
}
return result;
}

public class MyTask implements Runnable {
@Override
public void run() {
cdl.countDown();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true)
getAndIncreaseA();// getAndIncreaseB();
}
}

public static void main(String[] args) throws InterruptedException {
AtomicTest at = new AtomicTest();
for (int n = 0; n < THREAD_COUNT; n++)
new Thread(at.new MyTask()).start();
System.out.println("start");
at.init();
at.cdl.countDown();
}
}

[/code]

以下是在Intel(R) Core(TM) i7-4710HQ CPU @2.50GHz(四核八线程)下的测试结果(波动较小,所以每项只测试了四五次,取其中一个较中间的值):
jdk1.7
AtomicInteger.getAndIncrement 12,653,757,034
synchronized 4,146,813,462
AtomicInteger.compareAndSet 12,952,821,234

jdk1.8
AtomicInteger.getAndIncrement 2,159,486,620
synchronized 4,067,309,911
AtomicInteger.compareAndSet 12,893,188,541


补充:应网友要求,在此提供Unsafe.getAndAddInt的相关源码以及我的测试代码。
用jad反编译jdk1.8中Unsafe得到的源码:

[code lang=”java”]
public final int getAndAddInt(Object obj, long l, int i)
{
int j;
do
j = getIntVolatile(obj, l);
while(!compareAndSwapInt(obj, l, j, j + i));
return j;
}
public native int getIntVolatile(Object obj, long l);
public final native boolean compareAndSwapInt(Object obj, long l, int i, int j);
[/code]

openjdk8的Unsafe源码:

[code lang=”java”]
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
public native int getIntVolatile(Object o, long offset);
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
[/code]

我的测试代码(提示:如果eclipse等ide报错,那是因为使用了受限的Unsafe,可以将警告级别从error降为warning,具体百度即可):

[code lang=”java”]

import sun.misc.Unsafe;
public class AtomicTest {
….
private Unsafe unsafe;
private long valueOffset;
public AtomicTest(){
Field f;
try {
f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe)f.get(null);
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
}catch(NoSuchFieldException e){

}
}
private final int getAndIncreaseD(){
int result;
do{
result = unsafe.getIntVolatile(ai, valueOffset);
}while(!unsafe.compareAndSwapInt(ai, valueOffset, result, result+1));
if(result == MAX){
System.out.println(System.nanoTime()-startTime);
System.exit(0);
}
return result;
}

}
[/code]


补充2:对于性能提升的原因,有以下推论,虽不敢说百分之百正确(因为没有用jvm的源码作为论据),但还是有很大把握的,感谢网友@周 可人和@liuxinglanyue!

Unsafe是经过特殊处理的,不能理解成常规的java代码,区别在于:
在调用getAndAddInt的时候,如果系统底层支持fetch-and-add,那么它执行的就是native方法,使用的是fetch-and-add;
如果不支持,就按照上面的所看到的getAndAddInt方法体那样,以java代码的方式去执行,使用的是compare-and-swap;
这也正好跟openjdk8中Unsafe::getAndAddInt上方的注释相吻合:
[code lang=”java”]
// The following contain CAS-based Java implementations used on
// platforms not supporting native instructions
[/code]
Unsafe的特殊处理也就是我上文所说的“黑魔法”。

相关链接:
http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.html
http://hg.openjdk.java.net/jdk8u/hs-dev/jdk/file/a006fa0a9e8f/src/share/classes/sun/misc/Unsafe.java

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Java8中CAS的增强

  • Trackback 关闭
  • 评论 (29)
    • mysh
    • 2015/05/09 3:37下午

    8当中get和cas都是本地方法, 自然更快

    • liuxinglanyue
    • 2015/05/10 2:41下午

    关于这一点“我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码,但测试结果却跟jdk1.7的getAndIncrement一样慢”

    我猜测了一种可能性,Unsafe.class 里的方法默认情况下是直接生成机器码的,自己写的代码,方法调用次数必须达到多少才会生成机器码(可通过-XX:CompileThreshold来设置)。所以博主可以先预热下,再测试。

    • liuxinglanyue
    • 2015/05/10 3:35下午

    liuxinglanyue :
    关于这一点“我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码,但测试结果却跟jdk1.7的getAndIncrement一样慢”
    我猜测了一种可能性,Unsafe.class 里的方法默认情况下是直接生成机器码的,自己写的代码,方法调用次数必须达到多少才会生成机器码(可通过-XX:CompileThreshold来设置)。所以博主可以先预热下,再测试。

    猜想失败,预热后性能没啥变化

    • liuxinglanyue
    • 2015/05/10 3:37下午

    猜想失败,预热后性能没啥变化

    • 这不是显然的吗?实现的底层不一样,导致变快。两个方法对应的指令本来就不一样……

      http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.html

        • liuxinglanyue
        • 2015/05/11 2:51下午

        先吐槽下,这个引用为啥还要审核啊,等了这么久还没审核,干脆直接回复了。

        @周 可人 你理解错这句话的意思了 “我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码,但测试结果却跟jdk1.7的getAndIncrement一样慢”

        这不是在纠结jdk7和jdk8的性能差距。 实际是,将jdk8的getAndAddInt方法 挪出来,自己重写一次。然后和直接调用jdk8的getAndIncrement进行对比。 我机器上测试,直接调用快3倍左右。

      • http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/classfile/vmSymbols.hpp
        unsafe.getAndAddInt在上面的宏定义中
        do_intrinsic(_getAndAddInt, sun_misc_Unsafe, getAndAddInt_name, getAndAddInt_signature, F_R)
        JIT会根据instrinsics id生成特定的针对平台优化指令集, 此时unsafe.getAndAddInt的具体实现已经不是你看到的那些代码了(已被替换), 而你仿照unsafe.getAndAddInt重写的方法不会有这样的优化, 类似的例子我也遇到过 (lazySet的具体实现) http://ifeve.com/juc-atomic-class-lazyset-que/

    • 文章末尾已更新Unsafe相关代码和测试代码,供参考

  1. 一个是fectch-and-add,一个是compare-and-swap。就是这里不同,将来肯定会支持更多原子指令,譬如说什么64位的cas之类的。

    其实阻止我们使用并发方法的另一个瓶颈在内存上,还好在java里这个问题比较简单。

    “我反编译了Unsafe,发现CAS的失败重试就是在getAndAddInt方法里完成的,我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码”,我不太懂,能把代码贴一下吗?谢谢!

    • 文章末尾已更新Unsafe相关代码和测试代码,供参考

    • liuxinglanyue
    • 2015/05/11 2:25下午

    周 可人 :
    这不是显然的吗?实现的底层不一样,导致变快。两个方法对应的指令本来就不一样……
    http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.html

    @周 可人 你理解错这句话的意思了 “我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码,但测试结果却跟jdk1.7的getAndIncrement一样慢”

    这不是在纠结jdk7和jdk8的性能差距。 实际是,将jdk8的getAndAddInt方法 挪出来,自己重写一次。然后和直接调用jdk8的getAndIncrement进行对比。 我机器上测试,直接调用快3倍左右。

    • liuxinglanyue
    • 2015/05/11 2:56下午

    @博主

    针对这句话 “我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码,但测试结果却跟jdk1.7的getAndIncrement一样慢”

    看着这篇文章后 http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.html 知道了为啥性能有差距了。

    unsafe的方法getAndAddInt,在执行时有专门的指令lock xadd。而反编译unsafe获得了getAndAddInt实现,然后自己重写一次,指令就变多了,没有直接调用快了。 so。。。。大概有3倍的性能差距。

  2. liuxinglanyue :

    @博主

    针对这句话 “我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码,但测试结果却跟jdk1.7的getAndIncrement一样慢”

    看着这篇文章后 http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.html 知道了为啥性能有差距了。

    unsafe的方法getAndAddInt,在执行时有专门的指令lock xadd。而反编译unsafe获得了getAndAddInt实现,然后自己重写一次,指令就变多了,没有直接调用快了。 so。。。。大概有3倍的性能差距。

    嗯,我大概是这个意思,你应该已经明白了吧。
    lz他自己提出来的代码是自己重写的,本质上对应not native的实现。
    // The following contain CAS-based Java implementations used on
    // platforms not supporting native instructions

    /**
    * Atomically adds the given value to the current value of a field
    * or array element within the given object o
    * at the given offset.
    *
    * @param o object/array to update the field/element in
    * @param offset field/element offset
    * @param delta the value to add
    * @return the previous value
    * @since 1.8
    */
    public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
    v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
    }
    但是在jdk1.8中会用直接用native方法,其实1楼就说明了。

      • liuxinglanyue
      • 2015/05/11 3:14下午

      之前理解错1楼的意思了

      • 我觉得在对事情的原因缺乏认识的时候,很难从一楼这样的评论中获取到有用的信息(而且是自己缺乏了解的人)

        • liuxinglanyue
        • 2015/05/11 7:50下午

        一楼的一句话,点醒不了梦中人

      • liuxinglanyue
      • 2015/05/11 3:19下午

      @周 可人

      知道这个是通过什么工具看的吗?谢谢。

      0x0000000002cf49c7: mov %rbp,0x10(%rsp)
      0x0000000002cf49cc: mov $0x1,%eax
      0x0000000002cf49d1: lock xadd %eax,0xc(%rdx) ;*invokevirtual getAndAddInt
      ; – java.util.concurrent.atomic.AtomicInteger::incrementAndGet@8 (line 186)

      • liuxinglanyue
      • 2015/05/11 3:32下午

      google到了工具

    • 您好,我对我的理解在楼下的评论中作了下总结,麻烦看下吧,谢谢

      • 恩恩,不错。另外,@liuxinglanyue 同学,能不能介绍下是用什么工具看的呢?我觉得这个可以补充一下。

        • liuxinglanyue
        • 2015/05/11 11:00下午

        hsdis

  3. 多谢各位的指点与讨论,我说下我对评论的理解吧:
    Unsafe是经过特殊处理的,不能理解成常规的java代码,区别在于,在调用getAndAddInt的时候,如果系统底层支持fetch-and-add,那么它执行的就是native方法,使用的是fetch-and-add;如果不支持,就按照上面的所看到的getAndAddInt方法体那样,以java代码的方式去执行,使用的是compare-and-swap,这也正好跟openjdk8中Unsafe::getAndAddInt上方的注释相吻合:
    // The following contain CAS-based Java implementations used on
    // platforms not supporting native instructions
    Unsafe的特殊处理也就是我文中所说的“黑魔法”。

    如有理解偏差,还请指正

  4. http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/classfile/vmSymbols.hpp
    unsafe.getAndAddInt在上面的宏定义中
    do_intrinsic(_getAndAddInt, sun_misc_Unsafe, getAndAddInt_name, getAndAddInt_signature, F_R)
    JIT会根据instrinsics id生成特定的针对平台优化指令集, 此时unsafe.getAndAddInt的具体实现已经不是你看到的那些代码了(已被替换), 而你仿照unsafe.getAndAddInt重写的方法不会有这样的优化, 类似的例子我也遇到过 (lazySet的具体实现) http://ifeve.com/juc-atomic-class-lazyset-que/

    • 接楼上回复:

      http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/opto/library_call.cpp
      参考library_call.cpp, 下面是一段注释

      // LS_xadd:
      // public int getAndAddInt( Object o, long offset, int delta)
      // public long getAndAddLong(Object o, long offset, long delta)
      //
      // LS_xchg:
      // int getAndSet(Object o, long offset, int newValue)
      // long getAndSet(Object o, long offset, long newValue)
      // Object getAndSet(Object o, long offset, Object newValue)
      //
      bool LibraryCallKit::inline_unsafe_load_store(BasicType type, LoadStoreKind kind) {

    • 嗯,你这篇文章前些天看过了,也刚好佐证了我上面的猜测,不过我对C不太熟,没能像你这般深入的分析了。看到你这篇文章之前,我也注意到了lazySet这个问题,当时我以为他们弄出两个实现“一模一样”的方法,是为了以后的性能优化占坑,原来是早就使用了这种容易让人误会的手法。

  5. //—————————-inline_unsafe_load_store—————————-
    // This method serves a couple of different customers (depending on LoadStoreKind):
    //
    // LS_cmpxchg:
    // public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
    // public final native boolean compareAndSwapInt( Object o, long offset, int expected, int x);
    // public final native boolean compareAndSwapLong( Object o, long offset, long expected, long x);
    //
    http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/opto/library_call.cpp
    参考library_call.cpp, 下面是一段注释

    // LS_xadd:
    // public int getAndAddInt( Object o, long offset, int delta)
    // public long getAndAddLong(Object o, long offset, long delta)
    //
    // LS_xchg:
    // int getAndSet(Object o, long offset, int newValue)
    // long getAndSet(Object o, long offset, long newValue)
    // Object getAndSet(Object o, long offset, Object newValue)
    //
    bool LibraryCallKit::inline_unsafe_load_store(BasicType type, LoadStoreKind kind) {

  6. 接楼上回复:

    http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/opto/library_call.cpp
    参考library_call.cpp, 下面是一段注释

    // LS_xadd:
    // public int getAndAddInt( Object o, long offset, int delta)
    // public long getAndAddLong(Object o, long offset, long delta)
    //
    // LS_xchg:
    // int getAndSet(Object o, long offset, int newValue)
    // long getAndSet(Object o, long offset, long newValue)
    // Object getAndSet(Object o, long offset, Object newValue)
    //
    bool LibraryCallKit::inline_unsafe_load_store(BasicType type, LoadStoreKind kind) {

    • wfh6732
    • 2016/09/28 6:02下午

    请恕小弟愚钝, jdk不是自带源代码的么? 为什么要反编译呢?

    • Unsafe是sun.misc包下的类(非java.xxx),并未直接提供源码

return top