tutorial paneles example create java multithreading performance concurrency synchronization

paneles - Rendimiento de la sección de sincronización en Java



table swing java (6)

Tuve una pequeña disputa sobre el rendimiento del bloque sincronizado en Java. Esta es una pregunta teórica, que no afecta la aplicación de la vida real. Considera la aplicación de un único subproceso, que usa bloqueos y sincroniza secciones. ¿Este código funciona más lento que el mismo código sin sincronizar las secciones? Si es así, ¿por qué? No discutimos la concurrencia, ya que es solo una aplicación de un solo hilo

Upd

Encontré un benchmark interesante para probarlo. Pero es de 2001. Las cosas podrían haber cambiado drásticamente en la última versión de JDK


El código de un solo subproceso seguirá siendo más lento cuando se usan bloques synchronized . Obviamente, no tendrá otros hilos atascados mientras espera que otros hilos terminen, sin embargo, tendrá que lidiar con los otros efectos de sincronización, es decir, la coherencia del caché.

Los bloques sincronizados no solo se usan para concurrencia , sino también para visibilidad . Cada bloque sincronizado es una barrera de memoria: la JVM puede trabajar libremente en las variables de los registros, en lugar de en la memoria principal, bajo la suposición de que múltiples hilos no accederán a esa variable. Sin bloques de sincronización, estos datos podrían almacenarse en la memoria caché de una CPU y diferentes subprocesos en diferentes CPU no verían los mismos datos. Al usar un bloque de sincronización, fuerza a la JVM a escribir estos datos en la memoria principal para visibilidad de otros hilos.

Por lo tanto, a pesar de que no tiene conflictos de bloqueo, la JVM tendrá que realizar tareas de mantenimiento en la descarga de datos a la memoria principal.

Además, esto tiene restricciones de optimización. La JVM es libre de reordenar las instrucciones para proporcionar optimización: considere un ejemplo simple:

foo++; bar++;

versus:

foo++; synchronized(obj) { bar++; }

En el primer ejemplo, el compilador puede cargar foo y bar al mismo tiempo, luego incrementarlos y luego guardarlos. En el segundo ejemplo, el compilador debe realizar la carga / agregar / guardar en foo , luego realizar la carga / agregar / guardar en la bar . Por lo tanto, la sincronización puede afectar la capacidad del JRE para optimizar las instrucciones.

(Un excelente libro sobre el Modelo de Memoria de Java es Java Concurrency In Practice de Brian Goetz).


Este código de muestra (con 100 subprocesos que hacen 1,000,000 iteraciones cada uno) demuestra la diferencia de rendimiento entre evitar y evitar un bloque sincronizado.

Salida:

Total time(Avoid Sync Block): 630ms Total time(NOT Avoid Sync Block): 6360ms Total time(Avoid Sync Block): 427ms Total time(NOT Avoid Sync Block): 6636ms Total time(Avoid Sync Block): 481ms Total time(NOT Avoid Sync Block): 5882ms

Código:

import org.apache.commons.lang.time.StopWatch; public class App { public static int countTheads = 100; public static int loopsPerThead = 1000000; public static int sleepOfFirst = 10; public static int runningCount = 0; public static Boolean flagSync = null; public static void main( String[] args ) { for (int j = 0; j < 3; j++) { App.startAll(new App.AvoidSyncBlockRunner(), "(Avoid Sync Block)"); App.startAll(new App.NotAvoidSyncBlockRunner(), "(NOT Avoid Sync Block)"); } } public static void startAll(Runnable runnable, String description) { App.runningCount = 0; App.flagSync = null; Thread[] threads = new Thread[App.countTheads]; StopWatch sw = new StopWatch(); sw.start(); for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(runnable); } for (int i = 0; i < threads.length; i++) { threads[i].start(); } do { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } while (runningCount != 0); System.out.println("Total time"+description+": " + (sw.getTime() - App.sleepOfFirst) + "ms"); } public static void commonBlock() { String a = "foo"; a += "Baa"; } public static synchronized void incrementCountRunning(int inc) { runningCount = runningCount + inc; } public static class NotAvoidSyncBlockRunner implements Runnable { public void run() { App.incrementCountRunning(1); for (int i = 0; i < App.loopsPerThead; i++) { synchronized (App.class) { if (App.flagSync == null) { try { Thread.sleep(App.sleepOfFirst); } catch (InterruptedException e) { e.printStackTrace(); } App.flagSync = true; } } App.commonBlock(); } App.incrementCountRunning(-1); } } public static class AvoidSyncBlockRunner implements Runnable { public void run() { App.incrementCountRunning(1); for (int i = 0; i < App.loopsPerThead; i++) { // THIS "IF" MAY SEEM POINTLESS, BUT IT AVOIDS THE NEXT //ITERATION OF ENTERING INTO THE SYNCHRONIZED BLOCK if (App.flagSync == null) { synchronized (App.class) { if (App.flagSync == null) { try { Thread.sleep(App.sleepOfFirst); } catch (InterruptedException e) { e.printStackTrace(); } App.flagSync = true; } } } App.commonBlock(); } App.incrementCountRunning(-1); } } }


Hay 3 tipos de bloqueo en HotSpot

  1. Grasa : JVM depende de mutexes del sistema operativo para adquirir el bloqueo.
  2. Delgado : JVM está utilizando el algoritmo CAS.
  3. Parcial : CAS es una operación bastante costosa en parte de la arquitectura. Bloqueo parcial: es un tipo especial de bloqueo optimizado para el escenario cuando solo un hilo está trabajando en el objeto.

Por defecto, JVM usa un bloqueo delgado . Más tarde, si JVM determina que no hay contención, el bloqueo delgado se convierte en bloqueo sesgado . La operación que cambia el tipo de bloqueo es bastante costosa, por lo tanto, JVM no aplica esta optimización de inmediato. Hay una opción de JVM especial: XX: BiasedLockingStartupDelay = Retraso que le dice a JVM cuándo se debe aplicar este tipo de optimización.

Una vez sesgado, ese hilo puede bloquear y desbloquear el objeto sin recurrir a costosas instrucciones atómicas.

Responde a la pregunta: depende. Pero si está sesgado, el código de un solo hilo con bloqueo y sin bloqueo tiene el mismo rendimiento promedio.


Hay algunos gastos indirectos en la adquisición de un bloqueo no controvertido, pero en las JVM modernas es muy pequeño.

Una optimización clave del tiempo de ejecución que es relevante para este caso se denomina "Bloqueo parcial" y se explica en el Documento técnico de rendimiento de Java SE 6 .

Si quería tener algunos números de rendimiento que sean relevantes para su JVM y su hardware, podría construir un micro-benchmark para tratar de medir esta sobrecarga.


Suponiendo que está utilizando la máquina virtual de HotSpot, creo que la JVM puede reconocer que no hay contención para ningún recurso dentro del bloque synchronized y tratarlo como código "normal".


Usar bloqueos cuando no lo necesite ralentizará su aplicación. Podría ser demasiado pequeño para medir o podría ser sorprendentemente alto.

En mi humilde opinión, el mejor enfoque es utilizar el código de bloqueo en un solo programa de subprocesos para dejar en claro que este código no está destinado a ser compartido a través de subprocesos. Esto podría ser más importante para el mantenimiento que cualquier problema de rendimiento.

public static void main(String... args) throws IOException { for (int i = 0; i < 3; i++) { perfTest(new Vector<Integer>()); perfTest(new ArrayList<Integer>()); } } private static void perfTest(List<Integer> objects) { long start = System.nanoTime(); final int runs = 100000000; for (int i = 0; i < runs; i += 20) { // add items. for (int j = 0; j < 20; j+=2) objects.add(i); // remove from the end. while (!objects.isEmpty()) objects.remove(objects.size() - 1); } long time = System.nanoTime() - start; System.out.printf("%s each add/remove took an average of %.1f ns%n", objects.getClass().getSimpleName(), (double) time/runs); }

huellas dactilares

Vector each add/remove took an average of 38.9 ns ArrayList each add/remove took an average of 6.4 ns Vector each add/remove took an average of 10.5 ns ArrayList each add/remove took an average of 6.2 ns Vector each add/remove took an average of 10.4 ns ArrayList each add/remove took an average of 5.7 ns

Desde el punto de vista del rendimiento, si 4 ns es importante para usted, debe usar la versión no sincronizada.

Para el 99% de los casos de uso, la claridad del código es más importante que el rendimiento. El código claro y simple a menudo funciona razonablemente bien también.

Por cierto: estoy usando un i7 2600 a 4.6 GHz con Oracle Java 7u1.

A modo de comparación, si hago lo siguiente, donde perfTest1,2,3 son idénticos.

perfTest1(new ArrayList<Integer>()); perfTest2(new Vector<Integer>()); perfTest3(Collections.synchronizedList(new ArrayList<Integer>()));

yo obtengo

ArrayList each add/remove took an average of 2.6 ns Vector each add/remove took an average of 7.5 ns SynchronizedRandomAccessList each add/remove took an average of 8.9 ns

Si uso un método común de perfTest , no puedo perfTest el código de manera óptima y todos son más lentos

ArrayList each add/remove took an average of 9.3 ns Vector each add/remove took an average of 12.4 ns SynchronizedRandomAccessList each add/remove took an average of 13.9 ns

Intercambiando el orden de las pruebas

ArrayList each add/remove took an average of 3.0 ns Vector each add/remove took an average of 39.7 ns ArrayList each add/remove took an average of 2.0 ns Vector each add/remove took an average of 4.6 ns ArrayList each add/remove took an average of 2.3 ns Vector each add/remove took an average of 4.5 ns ArrayList each add/remove took an average of 2.3 ns Vector each add/remove took an average of 4.4 ns ArrayList each add/remove took an average of 2.4 ns Vector each add/remove took an average of 4.6 ns

uno a la vez

ArrayList each add/remove took an average of 3.0 ns ArrayList each add/remove took an average of 3.0 ns ArrayList each add/remove took an average of 2.3 ns ArrayList each add/remove took an average of 2.2 ns ArrayList each add/remove took an average of 2.4 ns

y

Vector each add/remove took an average of 28.4 ns Vector each add/remove took an average of 37.4 ns Vector each add/remove took an average of 7.6 ns Vector each add/remove took an average of 7.6 ns Vector each add/remove took an average of 7.6 ns