tabla quién que programacion maquina interprete extension estructura ejemplos codigo java x86 bytecode processor atomicity

java - quién - Relación entre las instrucciones de bytecode y las operaciones del procesador



tabla de codigo bytecode (3)

La especificación de Java garantiza que las asignaciones de variables primitivas sean siempre atómicas (espere para types long y dobles.

Por el contrario, la operación Fetch-and-Add correspondiente a la famosa operación de incremento de i++ no sería atómica porque llevaría a una operación de lectura-modificación-escritura.

Suponiendo este código:

public void assign(int b) { int a = b; }

El bytecode generado es:

public void assign(int); Code: 0: iload_1 1: istore_2 2: return

Por lo tanto, vemos que la tarea se compone de dos pasos (carga y almacenamiento).

Suponiendo este código:

public void assign(int b) { int i = b++; }

Bytecode:

public void assign(int); Code: 0: iload_1 1: iinc 1, 1 //extra step here regarding the previous sample 4: istore_2 5: return

Sabiendo que el procesador X86 puede (al menos los modernos), opera la operación de incremento atómicamente, como se dijo:

En informática, la instrucción de CPU fetch-and-add es una instrucción especial que modifica atómicamente los contenidos de una ubicación de memoria. Se utiliza para implementar la exclusión mutua y algoritmos concurrentes en sistemas multiprocesador, una generalización de semáforos.

Por lo tanto, primera pregunta: A pesar de que bytecode requiere ambos pasos (carga y almacenamiento), ¿se basa Java en que la operación de asignación es una operación que se realiza de forma atómica cualquiera que sea la arquitectura del procesador y puede asegurar una atomicidad permanente (para asignaciones primitivas? ) en su especificación?

Segunda pregunta: ¿Es incorrecto confirmar que con un procesador X86 muy moderno y sin compartir el código compilado en diferentes arquitecturas, no hay necesidad de sincronizar la operación i++ (o AtomicInteger )? Teniendo en cuenta que ya es atómico.


Con respecto a su primera pregunta: la lectura y la escritura son atómicas, pero la operación de lectura / escritura no lo es. No pude encontrar una referencia específica sobre primitivos, pero el JLS # 17.7 dice algo similar con respecto a las referencias:

Las escrituras y lecturas de referencias son siempre atómicas, independientemente de si están implementadas como valores de 32 bits o de 64 bits.

Entonces, en su caso, tanto el iload como el istore son atómicos, pero la operación completa (iload, istore) no lo es.

¿Es incorrecto [considerar que] no hay necesidad de sincronizar la operación i ++?

En cuanto a su segunda pregunta, el código siguiente imprime 982 en mi máquina x86 (y no en 1,000) que muestra que algunos ++ se perdieron en la traducción ==> necesita sincronizar correctamente una operación ++ incluso en una arquitectura de procesador que admita una búsqueda y agregar instrucción.

public class Test1 { private static int i = 0; public static void main(String args[]) throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(10); final CountDownLatch start = new CountDownLatch(1); final Set<Integer> set = new ConcurrentSkipListSet<>(); Runnable r = new Runnable() { @Override public void run() { try { start.await(); } catch (InterruptedException ignore) {} for (int j = 0; j < 100; j++) { set.add(i++); } } }; for (int j = 0; j < 10; j++) { executor.submit(r); } start.countDown(); executor.shutdown(); executor.awaitTermination(1, TimeUnit.SECONDS); System.out.println(set.size()); } }


Considerando la segunda pregunta .

Usted implica que i++ se traducirá en la instrucción X86 Fetch-And-Add que no es verdadera. Si el código es compilado y optimizado por la JVM, puede ser cierto (tendría que verificar el código fuente de JVM para confirmarlo), pero ese código también puede ejecutarse en modo interpretado, donde la búsqueda y el agregado están separados y no sincronizados.

Por curiosidad, verifiqué qué código de ensamblado se genera para este código de Java:

public class Main { volatile int a; static public final void main (String[] args) throws Exception { new Main ().run (); } private void run () { for (int i = 0; i < 1000000; i++) { increase (); } } private void increase () { a++; } }

Utilicé Java HotSpot(TM) Server VM (17.0-b12-fastdebug) for windows-x86 JRE (1.6.0_20-ea-fastdebug-b02), built on Apr 1 2010 03:25:33 versión de JVM (este tenía un lugar en mi disco).

Este es el resultado crucial de ejecutarlo ( java -server -XX:+PrintAssembly -cp . Main ):

Al principio se compila en esto:

00c PUSHL EBP SUB ESP,8 # Create frame 013 MOV EBX,[ECX + #8] # int ! Field VolatileMain.a 016 MEMBAR-acquire ! (empty encoding) 016 MEMBAR-release ! (empty encoding) 016 INC EBX 017 MOV [ECX + #8],EBX ! Field VolatileMain.a 01a MEMBAR-volatile (unnecessary so empty encoding) 01a LOCK ADDL [ESP + #0], 0 ! membar_volatile 01f ADD ESP,8 # Destroy frame POPL EBP TEST PollPage,EAX ! Poll Safepoint 029 RET

Luego está en línea y compilado en esto:

0a8 B11: # B11 B12 &lt;- B10 B11 Loop: B11-B11 inner stride: not constant post of N161 Freq: 0.999997 0a8 MOV EBX,[ESI] # int ! Field VolatileMain.a 0aa MEMBAR-acquire ! (empty encoding) 0aa MEMBAR-release ! (empty encoding) 0aa INC EDI 0ab INC EBX 0ac MOV [ESI],EBX ! Field VolatileMain.a 0ae MEMBAR-volatile (unnecessary so empty encoding) 0ae LOCK ADDL [ESP + #0], 0 ! membar_volatile 0b3 CMP EDI,#1000000 0b9 Jl,s B11 # Loop end P=0.500000 C=126282.000000

Como puede ver, no usa las instrucciones Fetch-And-Add para a++ .


Incluso si el i ++ se traduce en una instrucción X86 Fetch-And-Add no cambiaría nada porque la memoria mencionada en la instrucción Fetch-And-Add se refiere a los registros de memoria local de la CPU y no a la memoria general del dispositivo / aplicación . En una CPU moderna, esta propiedad se extenderá a los cachés de memoria local de la CPU e incluso puede extenderse a las diversas cachés utilizadas por los diferentes núcleos para una CPU multinúcleo, pero en el caso de una aplicación multiproceso; no hay absolutamente ninguna garantía de que esta distribución se extenderá a la copia de la memoria utilizada por los propios hilos.

En claro, en una aplicación multiproceso, si una variable puede ser modificada por diferentes hilos que se ejecutan al mismo tiempo, entonces debe usar algún mecanismo de sincronización proporcionado por el sistema y no puede confiar en el hecho de que la instrucción i ++ ocupa una sola línea de java código para ser atómico.