【问题标题】:Java compare and swap semantics and performanceJava 比较和交换语义和性能
【发布时间】:2011-05-10 03:38:58
【问题描述】:

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

来自docs

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

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

我之所以问这个问题是因为我通过运行 threadnum 不同的线程、从 1 到 8 变化 threadnum 并使用 totalwork=1e9 对以下方法进行了基准测试(代码是用静态编译的 JVM Scala 编写的语言,但在这种情况下,它的含义和字节码翻译都与 Java 同构——这个简短的 sn-ps 应该很清楚):

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 有什么好处?

【问题讨论】:

  • x86 不支持非强保证 CAS,因为它具有 LOCK 前缀。所以 Weak 和 Standard CAS 是一样的操作。

标签: java performance concurrency jvm compare-and-swap


【解决方案1】:

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

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

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

【讨论】:

  • 你能提供 JLS 的引文吗?我能找到的关于weakCompareAndSet 语义的所有信息都是该方法上的javadocs。
  • future version will refine this 它已经在 jdk-9 中这样做了,weak 很好...... weak
【解决方案2】:

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

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

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

【讨论】:

  • +1 用于提供支持观察的技术背景。
  • 只是指令集参考的链接:intel.com/products/processor/manuals
  • x86 是严格排序的,但是 IBM Power 上的 LL/CS [加载链接/存储条件] 类型的指令可以使用弱 CAS,即使前提条件很好,弱 CAS 也可能实际上失败。跨度>
【解决方案3】:

weakCompareAndSwap 不能保证更快;它只是允许更快。你可以看看 OpenJDK 的开源代码,看看一些聪明人决定用这个权限做什么:

即:它们都被实现为单线

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

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

【讨论】:

  • 这些方法可能是内在的,所以我不确定你看到的代码是运行的代码......
猜你喜欢
  • 2020-11-10
  • 2011-07-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-12
  • 2012-03-10
  • 2011-05-11
  • 2013-10-27
相关资源
最近更新 更多