La JVM puede asumir que otros subprocesos no cambian la pizzaArrived
variable durante el ciclo. En otras palabras, puede elevar la pizzaArrived == false
prueba fuera del bucle, optimizando esto:
while (pizzaArrived == false) {}
dentro de esto:
if (pizzaArrived == false) while (true) {}
que es un bucle infinito.
Para asegurarse de que los cambios realizados por un hilo sean visibles para otros hilos, siempre debe agregar alguna sincronización entre los hilos. La forma más sencilla de hacer esto es crear la variable compartida volatile
:
volatile boolean pizzaArrived = false;
Hacer una variable volatile
garantiza que los diferentes hilos verán los efectos de los cambios de los demás. Esto evita que la JVM almacene en caché el valor pizzaArrived
o eleve la prueba fuera del bucle. En cambio, debe leer el valor de la variable real cada vez.
(De manera más formal, volatile
crea una relación de pasa antes de los accesos a la variable. Esto significa que todo el trabajo que hizo un hilo antes de entregar la pizza también es visible para el hilo que recibe la pizza, incluso si esos otros cambios no son para las volatile
variables).
Los métodos sincronizados se utilizan principalmente para implementar la exclusión mutua (evitando que sucedan dos cosas al mismo tiempo), pero también tienen los mismos efectos secundarios que volatile
tiene. Usarlos al leer y escribir una variable es otra forma de hacer que los cambios sean visibles para otros hilos:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
while (getPizzaArrived() == false) {}
System.out.println("That was delicious!");
}
synchronized boolean getPizzaArrived() {
return pizzaArrived;
}
synchronized void deliverPizza() {
pizzaArrived = true;
}
}
El efecto de una declaración impresa
System.out
es un PrintStream
objeto. Los métodos de PrintStream
se sincronizan así:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
La sincronización evita que pizzaArrived
se almacene en caché durante el ciclo. Estrictamente hablando, ambos hilos deben sincronizarse en el mismo objeto para garantizar que los cambios en la variable sean visibles. (Por ejemplo, llamar println
después de configurar pizzaArrived
y volver a llamarlo antes de leer pizzaArrived
sería correcto). Si solo un hilo se sincroniza en un objeto en particular, la JVM puede ignorarlo. En la práctica, la JVM no es lo suficientemente inteligente como para demostrar que otros subprocesos no llamarán println
después de la configuración pizzaArrived
, por lo que se supone que sí. Por lo tanto, no puede almacenar en caché la variable durante el bucle si llama System.out.println
. Es por eso que los bucles como este funcionan cuando tienen una declaración impresa, aunque no es una solución correcta.
Usar System.out
no es la única forma de causar este efecto, pero es la que las personas descubren con más frecuencia cuando intentan depurar por qué su ciclo no funciona.
El mayor problema
while (pizzaArrived == false) {}
es un bucle de espera ocupado. ¡Eso es malo! Mientras espera, acapara la CPU, lo que ralentiza otras aplicaciones y aumenta el uso de energía, la temperatura y la velocidad del ventilador del sistema. Idealmente, nos gustaría que el hilo de bucle duerma mientras espera, para que no acabe con la CPU.
A continuación se muestran algunas formas de hacerlo:
Usar esperar / notificar
Una solución de bajo nivel es utilizar los métodos de espera / notificación deObject
:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
synchronized (this) {
while (!pizzaArrived) {
try {
this.wait();
} catch (InterruptedException e) {}
}
}
System.out.println("That was delicious!");
}
void deliverPizza() {
synchronized (this) {
pizzaArrived = true;
this.notifyAll();
}
}
}
En esta versión del código, el hilo de bucle llama wait()
, lo que pone al hilo en suspensión. No utilizará ningún ciclo de CPU mientras está inactivo. Después de que el segundo subproceso establece la variable, llama notifyAll()
para despertar todos los subprocesos que estaban esperando ese objeto. Esto es como hacer que el pizzero toque el timbre, para que puedas sentarte y descansar mientras esperas, en lugar de pararte torpemente en la puerta.
Al llamar a esperar / notificar en un objeto, debe mantener el bloqueo de sincronización de ese objeto, que es lo que hace el código anterior. Puede usar cualquier objeto que desee siempre que ambos hilos usen el mismo objeto: aquí usé this
(la instancia de MyHouse
). Por lo general, dos subprocesos no podrían ingresar bloques sincronizados del mismo objeto simultáneamente (que es parte del propósito de la sincronización), pero funciona aquí porque un subproceso libera temporalmente el bloqueo de sincronización cuando está dentro del wait()
método.
BlockingQueue
A BlockingQueue
se utiliza para implementar colas de productor-consumidor. Los "consumidores" toman los artículos del frente de la cola y los "productores" empujan los artículos al final. Un ejemplo:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
Object food = queue.take();
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
queue.put("A delicious pizza");
}
}
Nota: Los métodos put
y take
de BlockingQueue
can throw InterruptedException
s, que son excepciones comprobadas que deben manejarse. En el código anterior, por simplicidad, se vuelven a lanzar las excepciones. Es posible que prefiera detectar las excepciones en los métodos y volver a intentar la llamada put o take para asegurarse de que se realiza correctamente. Aparte de ese punto de fealdad, BlockingQueue
es muy fácil de usar.
Aquí no se necesita ninguna otra sincronización porque BlockingQueue
garantiza que todo lo que hicieron los subprocesos antes de colocar elementos en la cola sea visible para los subprocesos que eliminan esos elementos.
Ejecutores
Executor
Los correos electrónicos son como correos electrónicos listos para BlockingQueue
usar que ejecutan tareas. Ejemplo:
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
executor.execute(eatPizza);
executor.execute(cleanUp);
Para más detalles véase el documento de Executor
, ExecutorService
y Executors
.
Manejo de eventos
Hacer un bucle mientras espera que el usuario haga clic en algo en una interfaz de usuario es incorrecto. En su lugar, utilice las funciones de manejo de eventos del kit de herramientas de la interfaz de usuario. En Swing , por ejemplo:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
label.setText("Button was clicked");
});
Dado que el controlador de eventos se ejecuta en el subproceso de despacho de eventos, realizar un trabajo prolongado en el controlador de eventos bloquea otras interacciones con la interfaz de usuario hasta que finaliza el trabajo. Las operaciones lentas pueden iniciarse en un nuevo hilo o enviarse a un hilo en espera utilizando una de las técnicas anteriores (esperar / notificar, a BlockingQueue
, o Executor
). También puede usar a SwingWorker
, que está diseñado exactamente para esto, y proporciona automáticamente un hilo de trabajo en segundo plano:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
button.addActionListener((ActionEvent e) -> {
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result);
}
}
new MyWorker().execute();
});
Temporizadores
Para realizar acciones periódicas, puede utilizar un java.util.Timer
. Es más fácil de usar que escribir su propio ciclo de tiempo, y más fácil de iniciar y detener. Esta demostración imprime la hora actual una vez por segundo:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Cada uno java.util.Timer
tiene su propio hilo de fondo que se utiliza para ejecutar sus mensajes de correo electrónico programados TimerTask
. Naturalmente, el hilo duerme entre tareas, por lo que no acapara la CPU.
En el código Swing, también hay un javax.swing.Timer
, que es similar, pero ejecuta el oyente en el hilo Swing, por lo que puede interactuar de forma segura con los componentes Swing sin necesidad de cambiar manualmente los hilos:
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);
Otras maneras
Si está escribiendo código multiproceso, vale la pena explorar las clases en estos paquetes para ver qué está disponible:
Y también consulte la sección Concurrencia de los tutoriales de Java. El subproceso múltiple es complicado, ¡pero hay mucha ayuda disponible!
wait()
libera el bloqueo de sincronización!).java public class ThreadTest { private static boolean flag = false; private static class Reader extends Thread { @Override public void run() { while(flag == false) {} System.out.println(flag); } } public static void main(String[] args) { new Reader().start(); flag = true; } }
@Boann, este código no eleva lapizzaArrived == false
prueba fuera del bucle, y el bucle puede ver la bandera cambiada por el hilo principal, ¿por qué?