2010-01-22 19 views
44

La sabiduría convencional nos dice que las aplicaciones Java empresariales de gran volumen deberían usar agrupamiento de subprocesos antes que engendrar nuevos subprocesos de trabajo. El uso de java.util.concurrent lo hace sencillo.Sobrecarga de creación de subprocesos Java

Existen situaciones, sin embargo, donde la agrupación de subprocesos no es una buena opción. El ejemplo específico con el que estoy luchando actualmente es el uso de InheritableThreadLocal, que permite que las variables ThreadLocal se "transmitan" a cualquier subproceso generado. Este mecanismo se rompe cuando se utilizan grupos de subprocesos, ya que los subprocesos de trabajo generalmente no se generan a partir del subproceso de solicitud, sino que son preexistentes.

Ahora hay formas de evitar esto (los locales del hilo se pueden pasar explícitamente), pero esto no siempre es apropiado o práctico. La solución más simple es generar nuevos hilos de trabajo bajo demanda y dejar que InheritableThreadLocal haga su trabajo.

Esto nos lleva de nuevo a la pregunta: si tengo un sitio de gran volumen, donde los hilos de solicitud de los usuarios están generando media docena de hilos de trabajo cada uno (es decir, no usan un grupo de subprocesos), ¿le dará una JVM? ¿problema? Estamos hablando potencialmente de un par de cientos de nuevos hilos que se crean cada segundo, cada uno dura menos de un segundo. ¿Las JVM modernas optimizan esto bien? Recuerdo los días en que la agrupación de objetos era deseable en Java, porque la creación de objetos era costosa. Esto se ha vuelto innecesario. Me pregunto si lo mismo se aplica a la agrupación de subprocesos.

Lo compararía, si supiera qué medir, pero mi temor es que los problemas sean más sutiles de lo que se puede medir con un generador de perfiles.

Nota: la sabiduría de utilizar locals hilo no es el problema aquí, así que no sugiero que no los use.

+0

Iba a sugerir que envolver su ThreadLocal en un método de acceso probablemente resolvería sus problemas con InheritableThreadLocal, pero parece que no quiere escuchar eso. Además, parece que estás usando InheritableThreadLocal como un marco de llamada fuera de banda, que, para ser honesto, parece un olor a código. – kdgregory

+0

En lo que respecta a las agrupaciones de subprocesos, el principal beneficio es el control: usted sabe que no tratará repentinamente de generar 10.000 subprocesos en un segundo. – kdgregory

+2

@kdgregory: para su primer punto, las ThreadLocals en cuestión son utilizadas por Spring's bean scoping. Así es como funciona Spring, y no es algo sobre lo que yo tenga control. Para su segundo punto, los hilos de solicitud de entrada están limitados por el grupo de subprocesos de tomcat, por lo que la limitación es inherente a eso. – skaffman

Respuesta

36

Aquí es un ejemplo de microanálisis:

public class ThreadSpawningPerformanceTest { 
static long test(final int threadCount, final int workAmountPerThread) throws InterruptedException { 
    Thread[] tt = new Thread[threadCount]; 
    final int[] aa = new int[tt.length]; 
    System.out.print("Creating "+tt.length+" Thread objects... "); 
    long t0 = System.nanoTime(), t00 = t0; 
    for (int i = 0; i < tt.length; i++) { 
     final int j = i; 
     tt[i] = new Thread() { 
      public void run() { 
       int k = j; 
       for (int l = 0; l < workAmountPerThread; l++) { 
        k += k*k+l; 
       } 
       aa[j] = k; 
      } 
     }; 
    } 
    System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms."); 
    System.out.print("Starting "+tt.length+" threads with "+workAmountPerThread+" steps of work per thread... "); 
    t0 = System.nanoTime(); 
    for (int i = 0; i < tt.length; i++) { 
     tt[i].start(); 
    } 
    System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms."); 
    System.out.print("Joining "+tt.length+" threads... "); 
    t0 = System.nanoTime(); 
    for (int i = 0; i < tt.length; i++) { 
     tt[i].join(); 
    } 
    System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms."); 
    long totalTime = System.nanoTime()-t00; 
    int checkSum = 0; //display checksum in order to give the JVM no chance to optimize out the contents of the run() method and possibly even thread creation 
    for (int a : aa) { 
     checkSum += a; 
    } 
    System.out.println("Checksum: "+checkSum); 
    System.out.println("Total time: "+totalTime*1E-6+" ms"); 
    System.out.println(); 
    return totalTime; 
} 

public static void main(String[] kr) throws InterruptedException { 
    int workAmount = 100000000; 
    int[] threadCount = new int[]{1, 2, 10, 100, 1000, 10000, 100000}; 
    int trialCount = 2; 
    long[][] time = new long[threadCount.length][trialCount]; 
    for (int j = 0; j < trialCount; j++) { 
     for (int i = 0; i < threadCount.length; i++) { 
      time[i][j] = test(threadCount[i], workAmount/threadCount[i]); 
     } 
    } 
    System.out.print("Number of threads "); 
    for (long t : threadCount) { 
     System.out.print("\t"+t); 
    } 
    System.out.println(); 
    for (int j = 0; j < trialCount; j++) { 
     System.out.print((j+1)+". trial time (ms)"); 
     for (int i = 0; i < threadCount.length; i++) { 
      System.out.print("\t"+Math.round(time[i][j]*1E-6)); 
     } 
     System.out.println(); 
    } 
} 
} 

Los resultados en 64 bits de Windows 7 de 32 bits de Sun Java 1.6.0_21 VM cliente en el procesador Intel Core 2 Duo E6400 @ 2,13 GHz son los siguientes:

Number of threads 1 2 10 100 1000 10000 100000 
1. trial time (ms) 346 181 179 191 286 1229 11308 
2. trial time (ms) 346 181 187 189 281 1224 10651 

Conclusiones: Dos hilos hacen el trabajo casi dos veces más rápido que uno, como se esperaba ya que mi computadora tiene dos núcleos. Mi computadora puede engendrar casi 10000 hilos por segundo, i. mi. la sobrecarga de creación de subprocesos es 0.1 milisegundos. Por lo tanto, en una máquina de este tipo, un par de cientos de nuevos subprocesos por segundo suponen una sobrecarga insignificante (como también se puede ver al comparar los números en las columnas para 2 y 100 subprocesos).

9

En primer lugar, esto dependerá, por supuesto, de la JVM que utilice. El sistema operativo también jugará un papel importante. Suponiendo que la JVM de Sun (Hm, ¿todavía llama así?):

Un factor importante es la memoria de pila asignado a cada hilo, que se puede ajustar usando el parámetro -Xssn JVM - usted querrá utilizar los más bajos valor con el que puede salirse con la suya.

Y esto es sólo una suposición, pero creo que "un par de cientos de nuevos hilos por segundo" es definitivamente más allá de lo que la JVM está diseñada para manejar cómodamente. Sospecho que un simple punto de referencia revelará rápidamente problemas bastante poco sutiles.

+2

Encuentro la noción de qué 'nuevo Thread()' significa ser interesante . En una JVM moderna, 'new Object()' no siempre asigna nueva memoria, sino que reutiliza los objetos recolectados anteriormente. Me pregunto si hay alguna razón por la cual la JVM no podría tener un grupo interno oculto de hilos reutilizables, de modo que 'new Thread()' no necesariamente cree una nueva cadena de kernel. Obtendrá una agrupación de hilos eficaz, sin necesidad de una API para ello. – skaffman

+2

Si esto es así, debería encontrarse en algunos JSR. Podría ser 133 http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf – Bozho

+1

@skaffman Su hipótesis parece consistente con lo que he estado observando al menos osx/jdk1.6. Varias veces en los últimos meses he competido con el pool de subprocesos + "nuevo ejecutable" contra un semáforo de tamaño similar + "nuevo subproceso" y nunca parece haber ninguna diferencia medible. El enfoque de semáforo parece a veces superar el enfoque de agrupación, pero la diferencia es tan pequeña y tan rara que realmente solo hace hincapié en qué tan difícil es trabajar para obtener alguna diferencia entre ellos. –

1
  • para su referencia puede utilizar un generador de perfiles JMeter +, que debe darle visión general directamente en el comportamiento en un entorno tan pesada carga. Simplemente déjalo funcionar durante una hora y monitoree la memoria, la CPU, etc. Si nada se rompe y la CPU no se sobrecalienta, está bien :)

  • quizás pueda obtener un grupo de subprocesos o personalizarlo (extienda) el que está utilizando al agregar un código para tener el InheritableThreadLocal s adecuado configurado cada vez que se adquiere Thread del grupo de subprocesos. Cada Thread tiene estas propiedades del paquete y el privado:

    /* ThreadLocal values pertaining to this thread. This map is maintained 
    * by the ThreadLocal class. */ 
    ThreadLocal.ThreadLocalMap threadLocals = null; 
    
    /* 
    * InheritableThreadLocal values pertaining to this thread. This map is 
    * maintained by the InheritableThreadLocal class. 
    */ 
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 
    

    Puede utilizar estos (así, con la reflexión) en combinación con el Thread.currentThread() tener el comportamiento deseado. Sin embargo, esto es un poco difícil, y además, no puedo decir si (con el reflejo) no introducirá una sobrecarga mayor que simplemente crear los hilos.

+0

La transcripción de threadlocals es algo que sí consideré. En mi caso particular, sin embargo, estoy usando '@ Async' en Spring 3, que desacopla la mecánica del' Callable' de la lógica comercial. Es muy bueno, pero significa que no tienes acceso al ejecutor en sí o las tareas que se crean. – skaffman

+1

¿Comprobaste si la primavera no tiene algún mecanismo enchufable para reemplazar la impelementación del ejecutor? De lo contrario, para continuar pirateando, podrías intentar crear una clase con el mismo nombre calificado que el que eventualmente colocará tu código personalizado, y dejar que se cargue en lugar del original. Pero ese es un último recurso. – Bozho

+0

Hmmm, sí, Spring te permite especificar el ejecutor utilizado para @Async, así que sí, hay una manera de pasar a través de los hilos de discusión allí, aunque como dijiste, todavía se pondrá bastante feo. – skaffman

0

Me pregunto si es necesario generar nuevos temas en cada petición del usuario si su ciclo de vida típico es tan corto como un segundo. ¿Podrías usar algún tipo de cola de Notificación/Espera donde generas un número dado de hilos (daemon), y todos esperan hasta que haya una tarea por resolver? Si la cola de tareas se alarga, genera hilos adicionales, pero no en una proporción de 1-1. Lo más probable es que funcione mejor que generando cientos de nuevos hilos cuyos ciclos de vida son muy cortos.

+1

Lo que está describiendo es un grupo de subprocesos, que ya describí en la pregunta. – skaffman

+0

Si cada subproceso de Solicitud actúa como ThreadPool, supongo que no veo por qué no podría tener un 'ThreadLocal privado local;' que usted instanciará cada vez que el subproceso Request se active y al procesar cada subproceso de trabajo, usted usa 'local.set()'/'local.get()', pero es probable que malinterprete su problema. – Terje

Cuestiones relacionadas