34

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

文档

  • weakCompareAndSet原子地读取和有条件地写入变量,但不会创建任何发生前的顺序,因此不保证对除weakCompareAndSet.
  • compareAndSet以及所有其他读取和更新操作,例如getAndIncrement具有读取和写入 volatile 变量的记忆效应。

从 API 文档中可以明显看出,compareAndSet它就像一个 volatile 变量。但是,weakCompareAndSet应该只是更改其特定的内存位置。因此,如果该内存位置专用于单个处理器的缓存,weakCompareAndSet则应该比常规的compareAndSet.

我之所以问这个问题是因为我通过运行threadnum不同的线程(threadnum从 1 到 8 不等)并拥有totalwork=1e9(代码是用静态编译的 JVM 语言 Scala 编写的,但它的含义和字节码翻译都是同构的)对以下方法进行了基准测试在这种情况下是 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
}

在具有 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(平均 = 2014.2086 最小值 = 1797.997 最大值 = 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(平均 = 8114.0052 最小值 = 8042.0742 最大值 = 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(平均值 = 8114.9328 最小值 = 8043.3674 最大值 = 8134.0418)

  • 线程数:2

运行时间:(显示最后 3 个)4066.5924 4066.5797 4066.6519(平均 = 4059.1911 最小值 = 4025.0703 最大值 = 4066.8547)

  • 线程数:4

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


虽然上面示例中的线程局部变量可能最终位于相同的缓存行中,但在我看来,常规 CAS 与其弱版本之间没有明显的性能差异。

这可能意味着,事实上,弱比较和交换就像完全成熟的内存栅栏一样,即就像它是一个易失性变量一样。

问:这个观察正确吗?此外,是否存在已知的架构或 Java 发行版,其弱比较和设置实际上更快?如果不是,那么首先使用弱 CAS 有什么好处?

4

3 回答 3

33

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

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

一般来说,尽量避免通过实验对与并发相关的行为做出任何结论。要考虑的变量太多了,如果您不遵循 JLS 保证正确的内容,那么您的程序可能随时中断(也许在不同的架构上,也许在更积极的优化下代码布局的变化,可能是在未来不存在的 JVM 构建下,等等)。从来没有理由假设您可以摆脱那些声明不能保证的事情,因为实验表明“它有效”。

于 2010-11-15T10:05:26.893 回答
33

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

没有指令可以在不创建内存栅栏的情况下完成这项工作,因此很可能两者都compareAndSet映射weakCompareAndSetLOCK CMPXCHG并执行完整的内存栅栏。

但这是针对 x86 的,其他架构(包括 x86 的未来变体)可能会做不同的事情。

于 2010-11-15T10:06:52.017 回答
7

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

即:它们都被实现为单线

return unsafe.compareAndSwapObject(this, valueOffset, expect, update);

它们具有完全相同的性能,因为它们具有完全相同的实现!(至少在 OpenJDK 中)。其他人评论说,无论如何,您在 x86 上确实不能做得更好,因为硬件已经“免费”为您提供了一堆保证。只有在像 ARM 这样更简单的架构上,您才需要担心它。

于 2014-03-14T00:28:39.490 回答