Java 比较和交换语义和性能规格数据: AMD 8220 4x 双核 @ 2.8 GHz规格数据: 英特尔 i7 四核 @ 2.67 GHz

Java中比较和交换的语义是什么?也就是说,just的比较和交换方法是否保证不同线程之间对原子整数实例的特定内存位置的有序访问,或者它是否保证对内存中所有位置的有序访问,即它的行为就好像它是易失性的(内存栅栏)。AtomicInteger

文档中

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

从API文档中可以明显看出,它的行为就好像它是一个易失性变量一样。但是,应该只是更改其特定的内存位置。因此,如果该内存位置是单个处理器的缓存所独有的,则应该比常规的快得多。compareAndSetweakCompareAndSetweakCompareAndSetcompareAndSet

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

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
}

在 AMD 上,具有 4 个双 2.8 GHz 内核和一个 2.67 GHz 4 核 i7 处理器。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 min = 7147.628 最大值 = 7504.886 )

  • 线程数: 2

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

  • 线程数: 4

运行时间: (显示最后 3 个) 1890.055 1889.813 1890.047 (平均值 = 2065.7207 min = 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 min = 7057.03 最大值 = 7325.45 )

  • 线程数: 2

运行时间: (显示最后 3) 3663.21 3665.838 3533.406 (平均值 = 3607.2149 min = 3529.177 max = 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 min = 919.985 最大值 = 997.681 )


测试名称:loop_atomic_tlocal_weakcas

  • 线程数: 1

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

  • 线程数: 2

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

  • 线程数: 4

运行次数: (显示最后 3 个) 1876.723 1881.069 1876.538 (平均值 = 4110.4221 min = 1804.62 max = 12467.351 )

  • 线程数: 8

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

规格数据: 英特尔 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 (平均值 = 8114.0052 min = 8042.0742 max = 8132.8542 )

  • 线程数: 2

运行时间: (显示最后 3 个) 4066.9559 4067.0414 4067.2080 (平均值 = 4086.0608 min = 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 (平均值 = 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 的优势是什么?


答案 1

当然,弱比较和交换可以充当完全易失性变量,具体取决于JVM的实现。事实上,如果在某些架构上不可能以比普通CAS性能更高的方式实现弱CAS,我不会感到惊讶。在这些体系结构上,弱 CAS 的实现很可能与完整 CAS 完全相同。或者可能只是因为您的JVM没有对弱CAS进行太多优化,因此当前的实现只是调用一个完整的CAS,因为它可以快速实现,并且未来的版本将对此进行优化。

JLS只是说弱CAS不会建立之前发生的关系,所以只是不能保证它引起的修改在其他线程中是可见的。在这种情况下,您所得到的只是保证比较和集合操作是原子的,但不保证(潜在)新值的可见性。这与保证它不会被看到并不相同,因此您的测试与此一致。

通常,尽量避免通过试验对并发相关行为得出任何结论。有太多的变量需要考虑,如果你不遵循JLS保证正确的内容,那么你的程序可能随时中断(也许在不同的架构上,也许是在更积极的优化下,这是由代码布局的轻微变化引起的,也许是在JVM的未来版本中还不存在, 等)。永远没有理由假设你可以逃脱一些被声明不能保证的东西,因为实验表明“它有效”。


答案 2

“原子比较和交换”的 x86 指令是 。此指令创建一个完整的内存栅栏。LOCK CMPXCHG

没有指令可以在不创建内存栅栏的情况下完成此工作,因此很可能同时映射和执行完整的内存栅栏。compareAndSetweakCompareAndSetLOCK CMPXCHG

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


推荐