Java compara e intercambia semántica y rendimiento

¿Cuál es la semántica de comparar e intercambiar en Java? A saber, ¿el método de comparar e intercambiar de unAtomicInteger solo garantiza el acceso ordenado entre diferentes subprocesos a la ubicación de memoria particular de la instancia de entero atómico, o garantiza el acceso ordenado a todas las ubicaciones en la memoria, es decir, actúa como si fuera un volátil (una valla de memoria).

Desde eldocs:

weakCompareAndSet lee atómicamente y escribe condicionalmente una variable, pero no crea ningún orden de suceder antes, por lo que no ofrece garantías con respecto a lecturas y escrituras anteriores o posteriores de cualquier variable que no sea el objetivo delweakCompareAndSet.compareAndSet y todas las demás operaciones de lectura y actualización, comogetAndIncrement tener los efectos de memoria de leer y escribir variables volátiles.

De la documentación de la API se desprende quecompareAndSet actúa como si fuera una variable volátil. Sin embargo,weakCompareAndSet se supone que solo cambia su ubicación de memoria específica. Por lo tanto, si esa ubicación de memoria es exclusiva de la memoria caché de un único procesador,weakCompareAndSet se supone que es mucho más rápido que el normalcompareAndSet.

Lo pregunto porque he comparado los siguientes métodos ejecutandothreadnum diferentes hilos, variandothreadnum de 1 a 8, y teniendototalwork=1e9 (el código está escrito en Scala, un lenguaje JVM compilado estáticamente, pero tanto su significado como la traducción de bytecode son isomorfos al de Java en este caso; estos breves fragmentos deben ser claros):

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
}

en un AMD con 4 núcleos duales de 2,8 GHz y un procesador i7 de 4 núcleos a 2,67 GHz. La JVM es Sun Server Hotspot JVM 1.6. Los resultados no muestran diferencias de rendimiento.

Especificaciones: AMD 8220 4x dual-core @ 2.8 GHzNombre de prueba: loop_atomic_tlocal_casHilo num .: 1

Tiempos de ejecución: (mostrando los últimos 3) 7504.562 7502.817 7504.626 (promedio = 7415.637 min = 7147.628 max = 7504.886)

Hilo num .: 2

Tiempos de ejecución: (mostrando los últimos 3) 3751.553 3752.589 3751.519 (promedio = 3713.5513 min = 3574.708 max = 3752.949)

Hilo num .: 4

Tiempos de ejecución: (mostrando los últimos 3) 1890.055 1889.813 1890.047 (promedio = 2065.7207 min = 1804.652 max = 3755.852)

Hilo num .: 8

Tiempos de ejecución: (mostrando los últimos 3) 960.12 989.453 970.842 (promedio = 1058.8776 min = 940.492 max = 1893.127)

Nombre de prueba: loop_atomic_weakcasHilo num .: 1

Tiempos de ejecución: (mostrando los últimos 3) 7325.425 7057.03 7325.407 (promedio = 7231.8682 min = 7057.03 max = 7325.45)

Hilo num .: 2

Tiempos de ejecución: (mostrando los últimos 3) 3663.21 3665.838 3533.406 (promedio = 3607.2149 min = 3529.177 max = 3665.838)

Hilo num .: 4

Tiempos de ejecución: (mostrando los últimos 3) 3664.163 1831.979 1835.07 (promedio = 2014.2086 min = 1797.997 max = 3664.163)

Hilo num .: 8

Tiempos de ejecución: (mostrando los últimos 3) 940.504 928.467 921.376 (promedio = 943.665 min = 919.985 max = 997.681)

Nombre de prueba: loop_atomic_tlocal_weakcasHilo num .: 1

Tiempos de ejecución: (mostrando los últimos 3) 7502.876 7502.857 7502.933 (promedio = 7414.8132 min = 7145.869 max = 7502.933)

Hilo num .: 2

Tiempos de ejecución: (mostrando los últimos 3) 3752.623 3751.53 3752.434 (promedio = 3710.1782 min = 3574.398 max = 3752.623)

Hilo num .: 4

Tiempos de ejecución: (mostrando los últimos 3) 1876.723 1881.069 1876.538 (promedio = 4110.4221 min = 1804.62 max = 12467.351)

Hilo num .: 8

Tiempos de ejecución: (mostrando los últimos 3) 959.329 1010.53 969.767 (promedio = 1072.8444 min = 959.329 max = 1880.049)

Especificaciones: Intel i7 quad-core @ 2.67 GHzNombre de prueba: loop_atomic_tlocal_casHilo num .: 1

Tiempos de ejecución: (mostrando los últimos 3) 8138.3175 8130.0044 8130.1535 (promedio = 8119.2888 min = 8049.6497 max = 8150.1950)

Hilo num .: 2

Tiempos de ejecución: (mostrando los últimos 3) 4067.7399 4067.5403 4068.3747 (promedio = 4059.6344 min = 4026.2739 max = 4068.5455)

Hilo num .: 4

Tiempos de ejecución: (mostrando los últimos 3) 2033.4389 2033.2695 2033.2918 (promedio = 2030.5825 min = 2017.6880 max = 2035.0352)

Nombre de prueba: loop_atomic_weakcasHilo num .: 1

Tiempos de ejecución: (mostrando los últimos 3) 8130.5620 8129.9963 8132.3382 (promedio = 8114.0052 min = 8042.0742 max = 8132.8542)

Hilo num .: 2

Tiempos de ejecución: (mostrando los últimos 3) 4066.9559 4067.0414 4067.2080 (promedio = 4086.0608 min = 4023.6822 max = 4335.1791)

Hilo num .: 4

Tiempos de ejecución: (mostrando los últimos 3) 2034.6084 2169.8127 2034.5625 (promedio = 2047.7025 min = 2032.8131 max = 2169.8127)

Nombre de prueba: loop_atomic_tlocal_weakcasHilo num .: 1

Tiempos de ejecución: (mostrando los últimos 3) 8132.5267 8132.0299 8132.2415 (promedio = 8114.9328 min = 8043.3674 max = 8134.0418)

Hilo num .: 2

Tiempos de ejecución: (mostrando los últimos 3) 4066.5924 4066.5797 4066.6519 (promedio = 4059.1911 min = 4025.0703 max = 4066.8547)

Hilo num .: 4

Tiempos de ejecución: (mostrando los últimos 3) 2033.2614 2035.5754 2036.9110 (promedio = 2033.2958 min = 2023.5082 max = 2038.8750)

Si bien es posible que los subprocesos locales en el ejemplo anterior terminen en las mismas líneas de caché, me parece que no hay una diferencia de rendimiento observable entre CAS normal y su versión débil.

Esto podría significar que, de hecho, una comparación e intercambio débiles actúa como una valla de memoria completa, es decir, actúa como si fuera una variable volátil.

Pregunta: ¿Es correcta esta observación? Además, ¿hay una arquitectura conocida o distribución de Java para la cual una comparación y un conjunto débiles sean realmente más rápidos? Si no, ¿cuál es la ventaja de usar un CAS débil en primer lugar?

Respuestas a la pregunta(3)

Su respuesta a la pregunta