De acuerdo con Java concurrencia en la práctica , es peligroso para iniciar un hilo dentro de un constructor de la clase. La razón es que esto expone el this
puntero a otro hilo antes de que el objeto está totalmente construido.
A pesar de este tema se discute en muchas preguntas StackOverflow anteriores, todavía estoy teniendo dificultades para entender por qué esto es como una preocupación. En particular, espero que buscar claridad sobre si iniciar un subproceso dentro de un constructor puede conducir a problemas de consistencia de memoria desde el punto de vista del modelo de memoria de Java.
Déjeme darle un ejemplo concreto de la clase de cosas que quiero hacer. (La salida deseado para este trozo de código es para el número 20 a imprimir a la consola.)
private static class ValueHolder {
private int value;
private Thread thread;
ValueHolder() {
this.value = 10;
thread = new Thread(new DoublingTask(this)); // exposing "this" pointer!!!
thread.start(); // starting thread inside constructor!!!
}
int getValue() {
return value;
}
void awaitTermination() {
try {
thread.join();
} catch (InterruptedException ex) {}
}
}
private static class DoublingTask implements Runnable {
private ValueHolder valueHolder;
DoublingTask(ValueHolder valueHolder) {
this.valueHolder = valueHolder;
}
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println(valueHolder.getValue() * 2); // I expect to print out 20...
}
}
public static void main(String[] args) {
ValueHolder myValueHolder = new ValueHolder();
myValueHolder.awaitTermination();
}
Sí, ya sé que el hilo se inicia antes de regresar desde el constructor. Sí, ya sé que el this
puntero se expone al hilo. Y, sin embargo, estoy convencido de que el código es correcto. Creo que siempre va a imprimir el número 20 a la consola.
- La asignación
this.value = 10
sucede-antesthread.start()
. (Esto se debe a quethis.value = 10
precedethread.start()
en el orden del programa.) - El llamado de
thread.start()
en el hilo principal que ocurre antes del inicio del.run()
método en el hilo de nueva creación. (Debido a quethread.start()
es una acción de sincronización.) - El inicio del
.run()
método ocurre antes de laSystem.out.println(valueHolder.getValue() * 2);
sentencia de impresión. (De nuevo, por orden del programa.)
Por lo tanto, por el modelo de memoria de Java, la declaración de impresión debe leer el valor correctamente inicializado-de valueHolder.value
(a saber, 10). Así que a pesar de haber ignorado el consejo de Java concurrencia en la práctica , todavía me parece que he escrito una pieza correcta de código.
¿He cometido un error? ¿Qué me estoy perdiendo?
ACTUALIZACIÓN : En base a las respuestas y comentarios, ahora entiendo que mi ejemplo de código es funcionalmente correcto por las razones que he proporcionado. Sin embargo, es mala práctica de escribir código de esta manera porque existe la posibilidad de que otros desarrolladores en el futuro agregar instrucciones de inicialización adicionales después de la partida de la rosca. Una situación en la que tales errores son probablemente es en la aplicación de subclases de esta clase.
Supongamos que subclase de la clase. Puede que no haya inicializado sus campos en el momento en que se necesiten.
class BetterValueHolder extends ValueHolder
{
private int betterValue;
BetterValueHolder(final int betterValue)
{
// not actually required, it's added implicitly anyway.
// just to demonstrate where your constructor is called from
super();
try
{
Thread.sleep(1000); // just to demonstrate the race condition more clearly
}
catch (InterruptedException e) {}
this.betterValue = betterValue;
}
@Override
public int getValue()
{
return betterValue;
}
}
Esto imprimirá cero, independientemente de qué valor se le da al constructor BetterValueHolder
.