2011-12-15 12 views
29

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 hablamos de concurrencia, ya que es única aplicación de un solo hiloRendimiento de la sección de sincronización en Java

UPD

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

+0

Niza en que esta disposición es, las cosas han evolucionado mucho ** ** ya que fue escrito hace diez años. – NPE

+0

respuesta corta: ¡sí! – bestsss

+0

Respuesta larga: sí. La JVM siempre tendrá que resolver si la clave del objeto está disponible, independientemente de la evolución de Java. –

Respuesta

27

Hay 3 tipos de bloqueo en HotSpot

  1. grasa: JVM se basa en los mutex OS para adquirir bloqueo.
  2. Thin: JVM usa el algoritmo CAS.
  3. Biased: CAS es una operación bastante costosa en algunas arquitecturas. Bloqueo parcial: es un tipo especial de bloqueo optimizado para el escenario cuando solo un hilo está trabajando en el objeto.

Por defecto JVM utiliza delgada bloqueo. Más adelante si JVM determina que no hay contención, el bloqueo delgado se convierte a con polarización bloqueando. 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 JVM especial - XX: BiasedLockingStartupDelay = delay que le dice a JVM cuándo se debe aplicar este tipo de optimización.

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

Respondo 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.

+4

Muy informativo . Sin embargo, ¿podría indicar para qué versión de Java/VM se ha escrito esta respuesta? –

17

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

Una optimización del tiempo de ejecución clave que es relevante para este caso se denomina "Bloqueo parcial" y se explica en el Java SE 6 Performance White Paper.

Si desea tener algunos números de rendimiento que sean relevantes para su JVM y su hardware, podría construir una micro-referencia para intentar medir esta sobrecarga.

+5

He probado esto. Es tan pequeño que no se puede medir el efecto en absoluto. Dicen que el efecto fue mucho más significativo para las versiones anteriores de JVM. – AlexR

+0

@AlexR: Bien, gracias por compartir. No me sorprende que el efecto solía ser más significativo, ya que las optimizaciones de Biased Locking solo se agregaron en Java 6. – NPE

+4

* tan pequeño que no se puede medir el efecto en absoluto * dicha afirmación no puede hacerse a la ligera. cuando se prueba algo en un circuito cerrado, JVM puede hacer grandes magias. pero eso no representa aplicaciones del "mundo real". JVM se vuelve estúpido realmente rápido cuando la ejecución se vuelve compleja. – irreputable

-1

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 la trata como código "normal".

+3

Citación, por favor. No creo que la JVM pueda eliminar por completo las entradas y salidas del monitor. – erickson

+0

También lo leí en alguna parte. Si el compilador de Hotspot está seguro de que el código solo es accesible desde un hilo, debe omitir la sincronización por completo.Sin embargo, no estoy muy seguro de la parte de "estoy seguro ..." y nunca he conseguido que la máquina virtual lo haga. Incluso en una aplicación de subproceso único, la sobrecarga de sincronización no debe subestimarse. – jarnbjo

+0

No estoy seguro de que sea posible que JVM realice esta optimización – Anton

8

El uso de 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, la mejor opción es utilizar el código de bloqueo en un único 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); 
} 

impresiones

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, usted tiene que utilizar 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.

BTW: Estoy usando un i7 2600 a 4.6 GHz con Oracle Java 7u1.


Para la 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>())); 

me sale

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 utilizo un método común perfTest no puede inline el código de la forma más óptima y todos ellos son 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 

Intercambiar 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 
+0

Lo probé en un IBM JDK y, a excepción de la primera ejecución, Vector y ArrayList tienen una diferencia de rendimiento de aproximadamente 10% en mi máquina (54ns frente a 48-50ns). También lo probé con Collections.synchronizedList y me sorprendió su mala actuación. Era aproximadamente dos veces más lento que Vector/ArrayList (110ns). – Stefan

+0

Esta es otra razón para preocuparse por el micro ajuste. Usando un sistema diferente, hardware, JVM puede darle un resultado diferente. –

+0

Por cierto, el código de ese tipo se optimiza primero para Vector, luego se desoptimiza y se optimiza de nuevo, ya que el objetivo de la llamada (Lista ) cambia. Como no se puede estar seguro acerca de la desoptimización adecuada (podría ser solo llamada guardada a vector + trampa), el caso de ArrayList sufriría. ¿Puedes cambiar la prueba, es decir, ArrayList y Vector? Mayormente curioso. OTOH el caso es una prueba de bloqueo diagonal perfecta sangrienta también. Además, el CAS es bastante barato en su CPU, en arquitecturas antiguas CAS es una llamada bastante costosa (si el bloqueo sesgado está deshabilitado) – bestsss

42

código de un solo subproceso todavía se ejecutará más lento cuando se utiliza synchronized bloques. 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é.

bloques sincronizados no sólo se utilizan para concurrencia, sino también la 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, aunque no tenga problemas 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 cambiar el orden de las instrucciones con el fin de proporcionar la optimización: considerar un ejemplo sencillo:

foo++; 
bar++; 

frente:

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

En el primer ejemplo, el compilador es libre para cargar foo y bar en el al mismo tiempo, luego increméntelos y luego guárdelos. En el segundo ejemplo, el compilador debe realizar la carga/agregar/guardar en foo, luego realizar la carga/agregar/guardar en 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 Brian Goetz de Java Concurrency In Practice.)

0

Este código de ejemplo (con 100 hilos haciendo 1.000.000 iteraciones cada uno) demuestra la diferencia de rendimiento entre evitar y no evitar una 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); 
     } 
    } 
}