java - Loop no ve el valor cambiado sin una declaración de impresión
multithreading synchronization (1)
La JVM puede suponer que otros subprocesos no cambian la variable pizzaArrived
durante el ciclo. En otras palabras, puede izar la prueba de pizzaArrived == false
fuera del ciclo, optimizando esto:
while (pizzaArrived == false) {}
dentro de esto:
if (pizzaArrived == false) while (true) {}
que es un bucle infinito.
Para garantizar 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 hacerlo es hacer que la variable compartida sea volatile
:
volatile boolean pizzaArrived = false;
Hacer que una variable sea volatile
garantiza que los diferentes hilos verán los efectos de los cambios de cada uno. Esto evita que la JVM pizzaArrived
caché el valor de la pizzaArrived
o pizzaArrived
la prueba fuera del ciclo. En cambio, debe leer el valor de la variable real todo el tiempo.
(Más formalmente, volatile
crea una relación de pasar antes a los accesos a la variable. Esto significa que todos los demás trabajos que hizo un hilo antes de entregar la pizza también son visibles para el hilo que recibe la pizza, incluso si esos otros cambios no son variables volatile
.)
Los métodos sincronizados se usan principalmente para implementar la exclusión mutua (evitando que ocurran dos cosas al mismo tiempo), pero también tienen los mismos efectos secundarios que los volatile
. 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 objeto PrintStream
. 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 pizzaArrived
caché durante el ciclo. Estrictamente hablando, ambos hilos deben sincronizarse en el mismo objeto para garantizar que los cambios a la variable sean visibles. (Por ejemplo, llamar a println
después de configurar pizzaArrived
y pizzaArrived
llamarlo antes de leer pizzaArrived
sería correcto.) Si solo un subproceso se sincroniza en un objeto particular, la JVM puede ignorarlo. En la práctica, la JVM no es lo suficientemente inteligente como para demostrar que otros hilos no invocarán a println
después de configurar pizzaArrived
, por lo que se supone que podrían hacerlo. Por lo tanto, no puede almacenar en caché la variable durante el ciclo si llama a System.out.println
. Es por eso que los bucles como este funcionan cuando tienen una declaración de impresión, aunque no es la solución correcta.
Usar System.out
no es la única manera de causar este efecto, pero es el que las personas descubren más a menudo, cuando intentan depurar por qué su bucle no funciona.
El problema más grande
while (pizzaArrived == false) {}
es un bucle de espera ocupada. ¡Eso es malo! Mientras espera, encierra 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, por lo que no encierra la CPU.
Aquí hay algunas maneras de hacerlo:
Usando esperar / notificar
Una solución de bajo nivel es usar los métodos wait / notify de Object
:
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 subproceso del bucle llama a wait()
, lo que pone el hilo en suspensión. No usará ningún ciclo de CPU mientras duerme. Después de que el segundo hilo establece la variable, llama a notifyAll()
para notifyAll()
cualquiera / todos los hilos que estaban esperando en ese objeto. Esto es como hacer que el pizzero toque el timbre de la puerta, para que pueda sentarse y descansar mientras espera, en lugar de quedarse torpemente en la puerta.
Al llamar a wait / notify 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í lo usé (la instancia de MyHouse
). Por lo general, dos subprocesos no podrán ingresar bloques sincronizados del mismo objeto simultáneamente (lo cual 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 método wait()
.
BlockingQueue
Un BlockingQueue
se usa para implementar colas productor-consumidor. Los "consumidores" toman los artículos del frente de la cola y los "productores" empujan los artículos en la parte posterior. Un ejemplo:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
// take next item from the queue (sleeps while waiting)
Object food = queue.take();
// and do something with it
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
// in producer threads, we push items on to the queue.
// if there is space in the queue we can return immediately;
// the consumer thread(s) will get to it later
queue.put("A delicious pizza");
}
}
Nota: Los métodos de put
y take
de BlockingQueue
pueden lanzar InterruptedException
, que son excepciones marcadas que deben ser manejadas. En el código anterior, por simplicidad, las excepciones se vuelven a lanzar. Es posible que prefiera detectar las excepciones en los métodos y vuelva a intentar la llamada de envío o recepción para asegurarse de que tenga éxito. Además de ese punto de fealdad, BlockingQueue
es muy fácil de usar.
Aquí no se necesita ninguna otra sincronización porque un BlockingQueue
se asegura de que todo lo que hace el hilo antes de poner elementos en la cola es visible para los hilos que sacan esos elementos.
Ejecutores
Executor
son como las BlockingQueue
hechas que ejecutan tareas. Ejemplo:
// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish
Para más detalles, consulte el documento para Executor
, ExecutorService
y Executors
.
Manejo de eventos
Hacer bucles mientras espera que el usuario haga clic en algo en una IU es incorrecto. En su lugar, use las características de manejo de eventos del conjunto de herramientas UI. En Swing , por ejemplo:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
// This event listener is run when the button is clicked.
// We don''t need to loop while waiting.
label.setText("Button was clicked");
});
Debido a que el controlador de eventos se ejecuta en el hilo de envío del evento, hacer un trabajo prolongado en el controlador de eventos bloquea otra interacción con la interfaz de usuario hasta que el trabajo finalice. Las operaciones lentas pueden iniciarse en un nuevo hilo, o enviarse a un hilo en espera usando una de las técnicas anteriores (wait / notify, BlockingQueue
o Executor
). También puede usar un SwingWorker
, que está diseñado exactamente para esto, y suministra automáticamente un hilo de trabajador de fondo:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {
// Defines MyWorker as a SwingWorker whose result type is String:
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
// This method is called on a background thread.
// You can do long work here without blocking the UI.
// This is just an example:
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
// This method is called on the Swing thread once the work is done
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result); // will display "Answer is 42"
}
}
// Start the worker
new MyWorker().execute();
});
Temporizadores
Para realizar acciones periódicas, puede usar un java.util.Timer
. Es más fácil de usar que escribir tu propio ciclo de temporización, y es 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 java.util.Timer
tiene su propio hilo de fondo que se utiliza para ejecutar sus TimerTask
programados. Naturalmente, el subproceso duerme entre tareas, por lo que no encierra la CPU.
En el código Swing, también hay un javax.swing.Timer
, que es similar, pero ejecuta el oyente en el subproceso Swing, por lo que puede interactuar de forma segura con los componentes Swing sin necesidad de cambiar los hilos manualmente:
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 lo que está disponible:
Y también vea la sección Concurrencia de los tutoriales de Java. ¡El subprocesamiento múltiple es complicado, pero hay mucha ayuda disponible!
En mi código, tengo un bucle que espera que se cambie un estado de un hilo diferente. El otro subproceso funciona, pero mi ciclo nunca ve el valor cambiado. Espera para siempre Sin embargo, cuando pongo una instrucción System.out.println
en el bucle, ¡de repente funciona! ¿Por qué?
El siguiente es un ejemplo de mi código:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
while (pizzaArrived == false) {
//System.out.println("waiting");
}
System.out.println("That was delicious!");
}
void deliverPizza() {
pizzaArrived = true;
}
}
Mientras el ciclo while se está ejecutando, deliverPizza()
desde un subproceso diferente para establecer la variable pizzaArrived
. Pero el ciclo solo funciona cuando elimino el comentario de System.out.println("waiting");
declaración. ¿Que esta pasando?