Java比较和交换语义和性能

axe*_*l22 32 java concurrency performance jvm compare-and-swap

Java中比较和交换的语义是什么?也就是说,AtomicIntegerjust 的compare和swap方法是否保证在不同线程之间对原子整数实例的特定内存位置进行有序访问,或者它是否保证对内存中所有位置的有序访问,即它就像是一个volatile一样(记忆围栏).

来自文档:

  • weakCompareAndSet原子地读取并有条件地写入变量但不创建任何发生前的排序,因此不提供关于除了目标之外的任何变量的先前或后续读取和写入的保证weakCompareAndSet.
  • compareAndSet以及所有其他读取和更新操作,例如getAndIncrement读取和写入volatile变量的内存效应.

从API文档中compareAndSet可以看出,它就好像是一个易变的变量.但是,weakCompareAndSet应该只是改变其特定的内存位置.因此,如果该存储器位置是单个处理器的高速缓存所独有的,weakCompareAndSet则应该比常规处理器快得多compareAndSet.

我问这个是因为我通过运行threadnum不同的线程来对以下方法进行基准测试,threadnum从1到8 不等,并且totalwork=1e9(代码是用Scala编写的,这是一种静态编译的JVM语言,但它的含义和字节码转换都是同构的在这种情况下Java的代码 - 这个简短的代码片段应该是清楚的):

val atomic_cnt = new AtomicInteger(0)
val atomic_tlocal_cnt = new java.lang.ThreadLocal[AtomicInteger] {
  override def initialValue = new AtomicInteger(0)
}

def loop_atomic_tlocal_cas = {
  var i = 0
  val until = totalwork / threadnum
  val acnt = atomic_tlocal_cnt.get
  while (i < until) {
    i += 1
    acnt.compareAndSet(i - 1, i)
  }
  acnt.get + i
}

def loop_atomic_weakcas = {
  var i = 0
  val until = totalwork / threadnum
  val acnt = atomic_cnt
  while (i < until) {
    i += 1
    acnt.weakCompareAndSet(i - 1, i)
  }
  acnt.get + i
}

def loop_atomic_tlocal_weakcas = {
  var i = 0
  val until = totalwork / threadnum
  val acnt = atomic_tlocal_cnt.get
  while (i < until) {
    i += 1
    acnt.weakCompareAndSet(i - 1, i)
  }
  acnt.get + i
}
Run Code Online (Sandbox Code Playgroud)

在具有4个双2.8 GHz内核和2.67 GHz 4核i7处理器的AMD上.JVM是Sun Server Hotspot JVM 1.6.结果显示没有性能差异.

规格:AMD 8220 4x双核@ 2.8 GHz

测试名称:loop_atomic_tlocal_cas

  • 线程数:1

运行时间:(显示最后3个)7504.562 7502.817 7504.626(平均值= 7415.637分钟= 7147.628最大值= 7504.886)

  • 线程数:2

运行时间:(显示最后3个)3751.553 3752.589 3751.519(平均值= 3713.5513最小值= 3574.708最大值= 3752.949)

  • 线程数:4

运行时间:(显示最后3)1890.055 1889.813 1890.047(平均值= 2065.7207最小值= 1804.652最大值= 3755.852)

  • 线程数:8

运行时间:(显示最后3个)960.12 989.453 970.842(平均值= 1058.8776最小值= 940.492最大值= 1893.127)


测试名称:loop_atomic_weakcas

  • 线程数:1

运行时间:(显示最后3个)7325.425 7057.03 7325.407(平均值= 7231.8682最小值= 7057.03最大值= 7325.45)

  • 线程数:2

运行时间:(显示最后3个)3663.21 3665.838 3533.406(平均值= 3607.2149最小值= 3529.177最大值= 3665.838)

  • 线程数:4

运行时间:(显示最后3个)3664.163 1831.979 1835.07(avg = 2014.2086 min = 1797.997 max = 3664.163)

  • 线程数:8

运行时间:(显示最后3个)940.504 928.467 921.376(平均值= 943.665分钟= 919.985最大值= 997.681)


测试名称:loop_atomic_tlocal_weakcas

  • 线程数:1

运行时间:(显示最后3个)7502.876 7502.857 7502.933(平均值= 7414.8132最小值= 7145.869最大值= 7502.933)

  • 线程数:2

运行时间:(显示最后3个)3752.623 3751.53 3752.434(平均值= 3710.1782最小值= 3574.398最大值= 3752.623)

  • 线程数:4

运行时间:(表示最后3)1876.723 1881.069 1876.538(平均值= 4110.4221分钟= 1804.62最大= 12467.351)

  • 线程数:8

运行时间:(显示最后3个)959.329 1010.53 969.767(平均值= 1072.8444最小值= 959.329最大值= 1880.049)

规格:Intel i7四核@ 2.67 GHz

测试名称:loop_atomic_tlocal_cas

  • 线程数:1

运行时间:(表示最后3)8138.3175 8130.0044 8130.1535(平均值= 8119.2888分钟= 8049.6497最大= 8150.1950)

  • 线程数:2

运行时间:(表示最后3)4067.7399 4067.5403 4068.3747(平均值= 4059.6344分钟= 4026.2739最大= 4068.5455)

  • 线程数:4

运行时间:(表示最后3)2033.4389 2033.2695 2033.2918(平均值= 2030.5825分钟= 2017.6880最大= 2035.0352)


测试名称:loop_atomic_weakcas

  • 线程数:1

运行时间:(显示最后3个)8130.5620 8129.9963 8132.3382(avg = 8114.0052 min = 8042.0742 max = 8132.8542)

  • 线程数:2

运行时间:(显示最后3个)4066.9559 4067.0414 4067.2080(平均值= 4086.0608最小值= 4023.6822最大值= 4335.1791)

  • 线程数:4

运行时间:(显示最后3个)2034.6084 2169.8127 2034.5625(平均值= 2047.7025最小值= 2032.8131最大值= 2169.8127)


测试名称:loop_atomic_tlocal_weakcas

  • 线程数:1

运行时间:(显示最后3个)8132.5267 8132.0299 8132.2415(avg = 8114.9328 min = 8043.3674 max = 8134.0418)

  • 线程数:2

运行时间:(显示最后3个)4066.5924 4066.5797 4066.6519(avg = 4059.1911 min = 4025.0703 max = 4066.8547)

  • 线程数:4

运行时间:(显示最后3个)2033.2614 2035.5754 2036.9110(平均值= 2033.2958最小值= 2023.5082最大值= 2038.8750)


虽然上面示例中的线程本地可能最终位于相同的缓存行中,但在我看来,常规CAS与其弱版本之间没有可观察到的性能差异.

这可能意味着,实际上,弱比较和交换充当完全成熟的内存栅栏,即表现为它是一个易变的变量.

问题:这个观察是否正确?此外,是否存在已知的体系结构或Java分发,弱比较和集合实际上更快?如果没有,首先使用弱CAS有什么好处?

And*_*yle 29

弱比较和交换可以充当完整的volatile变量,具体取决于JVM的实现.事实上,如果在某些架构上不可能以比普通CAS更高效的方式实现弱CAS,我不会感到惊讶.在这些体系结构中,很可能是弱CAS实现与完整CAS完全相同.或者可能只是因为你的JVM没有太多的优化使得弱CAS变得特别快,所以当前的实现只调用一个完整的CAS,因为它可以快速实现,未来的版本将改进它.

JLS只是说弱的CAS不会建立一个先发生过的关系,所以它只是不能保证它引起的修改在其他线程中是可见的.在这种情况下你得到的只是保证比较和设置操作是原子的,但不能保证(可能)新值的可见性.这与保证不会被看到不一样,因此您的测试与此一致.

一般来说,尽量避免通过实验对与并发相关的行为做出任何结论.有许多变量需要考虑,如果你不遵循JLS保证的正确性,那么你的程序可能随时都会中断(可能是在不同的架构上,也许是在由于轻微提示的更积极的优化代码布局的变化,可能是在JVM的未来版本中尚未存在,等等.有从来没有一个理由认为你可以逃脱东西是说没有得到保证,因为实验表明,"它的工作原理".

  • 你能提供JLS的引用吗?我所能找到的关于weakCompareAndSet的语义的是该方法的javadocs. (3认同)

Dan*_*iel 29

用于"原子比较和交换"的x86指令是LOCK CMPXCHG.该指令创建一个完整的内存栅栏.

没有任何指令,可以做这个工作,而无需创建一个存储栅栏,因此它很可能既compareAndSetweakCompareAndSet映射到LOCK CMPXCHG并执行完全的存储栅栏.

但是对于x86而言,其他架构(包括x86的未来变体)可能会以不同的方式做事.

  • x86是严格排序的,但IBM Power上的LL/CS [load linked/store conidional]类型的指令可以使用弱CAS.即使前提条件正常,弱CAS也可能实际上失败. (2认同)

Quu*_*one 6

weakCompareAndSwap不能保证更快; 它只是允许更快.您可以查看OpenJDK的开源代码,了解一些聪明人决定使用此权限做什么:

即:它们都被实现为单行

return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
Run Code Online (Sandbox Code Playgroud)

它们具有完全相同的性能,因为它们具有完全相同的实现!(至少在OpenJDK中).其他人已经注意到这样一个事实,你无论如何都无法在x86上做得更好,因为硬件已经为你提供了一堆"免费"的保证.只有像ARM这样的简单架构才能让你担心它.