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