代码之家  ›  专栏  ›  技术社区  ›  axel22

Java比较交换语义与性能

  •  33
  • axel22  · 技术社区  · 14 年前

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

    docs :

    • weakCompareAndSet 原子地读取和有条件地写入变量,但不会在排序之前创建任何happens,因此对于除 黄鼠狼 .
    • compareAndSet 以及所有其他读取和更新操作,如 getAndIncrement 具有读写易失性变量的记忆效果。

    从API文档中可以明显看出 比较集 就像是一个不稳定的变量。然而, 黄鼠狼 应该只是改变它的特定内存位置。因此,如果该内存位置对单个处理器的缓存是独占的, 弱compareandset 应该比平时快得多 比较集 .

    我这么问是因为我已经通过运行 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
    }
    

    在AMD上有4个2.8GHz双核和2.67GHz 4核i7处理器。JVM是Sun服务器热点JVM 1.6。结果显示没有性能差异。

    规格:AMD82204X双核@2.8GHz

    测试名称: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.504928.467921.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)

    规格:英特尔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有什么好处?

    3 回复  |  直到 12 年前
        1
  •  29
  •   Andrzej Doyle    14 年前

    弱小的比较和交换 能够 充当一个完全可变的变量,当然,这取决于JVM的实现。事实上,如果在某些架构上不可能以比普通CAS更显著的性能实现弱CAS,我也不会感到惊讶。在这些体系结构中,很可能是这样的情况,即弱案例的实现与完整的CAS完全相同。或者它可能只是您的JVM在使弱情况变得特别快方面没有太多的优化,所以 现在的 实现只是调用一个完整的CAS,因为它实现起来很快,未来的版本将对此进行改进。

    JLS只是说一个弱的CAS不能建立一个 以前发生过 关系,所以根本就没有 保证 它导致的修改在其他线程中可见。在这种情况下,您所得到的只是保证compare and set操作是原子的,但不能保证(潜在的)新值的可见性。这不等于保证 不会 你的测试结果与此一致。

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

        2
  •  29
  •   Daniel    14 年前

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

    没有指令可以在不创建内存界限的情况下完成此任务,因此很可能 compareAndSet weakCompareAndSet 映射到 锁定CMPXCHG 执行完整的内存限制。

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

        3
  •  6
  •   Quuxplusone    10 年前

    weakCompareAndSwap 不是 放心 为了更快;只是 被允许 为了更快。您可以查看OpenJDK的开源代码,了解一些聪明人决定使用此权限执行的操作:

    即:它们都被实现为一行程序

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

    他们的表演完全一样,因为 它们的实现方式完全相同! (至少在OpenJDK中)。其他人说,无论如何,你在x86上做不到更好的地方,因为硬件已经给了你“免费”的保证。只有在更简单的架构上,比如ARM,你才需要担心它。

    推荐文章