termine - ¿Por qué los métodos de espera/notificación/notificación no están sincronizados en java?
thread java (7)
en Java, siempre que tengamos que llamar a esperar / notificar / notificar todo, necesitamos tener acceso al monitor de objetos (ya sea a través del método sincronizado o mediante el bloque sincronizado). Así que mi pregunta es por qué Java no optó por los métodos de espera / notificación sincronizados, lo que elimina la restricción de llamar a estos métodos desde un bloque o métodos sincronizados.
En caso de que estos se declaren como sincronizados, habría tomado automáticamente el acceso del monitor.
Alguien con más experiencia en multihilo debería sentirse libre de intervenir, pero creo que esto eliminaría la versatilidad de los bloques sincronizados. El punto de usarlos es sincronizar en un objeto particular que funciona como el recurso / semáforo monitoreado. Los métodos de espera / notificación se utilizan para controlar el flujo de ejecución dentro del bloque sincronizado.
Tenga en cuenta que los métodos sincronizados son abreviados para sincronizar this
durante la duración del método (o la clase para métodos estáticos). La sincronización de los propios métodos de espera / notificación eliminaría el punto de su uso como señales de detención / avance entre las hebras.
Buena pregunta. Los comentarios en la implementación del objeto JDK7 arrojaron algo de luz sobre esto, creo (énfasis mío):
Este método hace que el subproceso actual (llámelo
T
) se coloque en el conjunto de espera para este objeto y luego renuncie a todas y cada una de las reclamaciones de sincronización de este objeto ....
El hilo
T
se elimina del conjunto de espera para esta manera habitual con otros hilos para que el derecho se sincronice en el objeto; Una vez que ha ganado el control del objeto, todas sus reclamaciones de sincronización sobre el objeto se restauran al status quo ante, es decir, a la situación desde el momento en que se invocó el método dewait
. El hiloT
luego regresa de la invocación del método dewait
. Por lo tanto, al regresar del método dewait
, el estado de sincronización del objeto y del hiloT
es exactamente como estaba cuando se invocó el método dewait
.
Así que creo que el primer punto a tener en cuenta es que wait()
no regresa hasta que la persona que llama haya terminado de esperar (obviamente). Lo que significa que si la propia wait()
estuviera sincronizada, el interlocutor continuaría manteniendo el bloqueo en el objeto, y nadie más podría wait()
o notify()
.
Ahora, obviamente, wait()
está haciendo algo difícil detrás de escena para forzar a la persona que llama a perder el control del objeto, pero tal vez este truco no funcione (o sería mucho más difícil de hacer funcionar) si wait()
Se sincronizaron.
El segundo punto es que si hay varios subprocesos esperando en un objeto, cuando notify()
se utiliza para despertar exactamente uno de ellos, se utiliza el método de contención estándar para permitir que solo un subproceso se sincronice en el objeto, y se supone que wait()
para restaurar los reclamos de sincronización de la persona que llama al estado exacto en que se encontraban antes de la llamada a wait()
. Me parece posible que requerir que la persona que llama mantenga el bloqueo antes de wait()
a la llamada wait()
simplifica esto, ya que elimina la necesidad de verificar si la persona que llama debe o no continuar manteniendo el bloqueo después de wait()
retorna la wait()
. El contrato estipula que la persona que llama debe continuar sosteniendo el bloqueo y, por lo tanto, parte de la implementación se simplifica.
O tal vez simplemente se hizo para evitar la aparición de la paradoja lógica de "si wait()
y notify()
están sincronizados, y wait()
no vuelve hasta que se llame a notify()
, ¿cómo se puede usar con éxito? ? ".
Esos son mis pensamientos, de todos modos.
Creo que wait
sin synchronized
puede funcionar bien en algún escenario. Pero no se puede usar para situaciones complicadas sin condición de carrera , pueden producirse "activaciones espurias".
El código funciona para la cola.
// producer
give(element){
list.add(element)
lock.notify()
}
// consumer
take(){
obj = null;
while(obj == null)
lock.wait()
obj = list.remove(0) // ignore error indexoutofrange
return obj
}
Este código no ha explicado el estado de los datos compartidos, ignorará el último elemento y puede que no funcione en la condición de hilo mutil. Sin condición de carrera, esta lista en 1
, 2
puede tener estados totalmente diferentes.
// consumer
take(){
while(list.isEmpty()) // 1
lock.wait()
return list.remove(0) // 2
}
Y ahora, lo hace un poco más complicado y obvio.
ejecución de instrucción
-
give(element) lock.notify()->take() lock.wait() resurrected->take() list.remove(0)->rollback(element)
-
give(element) lock.notify()->take() lock.wait() resurrected->rollback(element)->take() list.remove(0)
se producen "activaciones espurias", también hace que el código sea impredecible.
// producer
give(element){
list.add(element)
lock.notify()
}
rollback(element){
list.remove(element)
}
// business code
produce(element){
try{
give(element)
}catch(Exception e){
rollback(element) // or happen in another thread
}
}
// consumer
take(){
obj = null;
while(obj == null)
lock.wait()
obj = list.remove(0) // ignore error indexoutofrange
return obj
}
El modelo de sincronización de espera de notificación requiere que adquiera los monitores en un objeto primero, antes de proceder a realizar cualquier trabajo. Es diferente del modelo de exclusión mutua que utiliza un bloque sincronizado.
El modelo de espera-notificación o cooperación mutua se usa normalmente en un escenario de productor-consumidor donde un hilo produce eventos que son consumidos por otro hilo. Las implementaciones bien escritas se esforzarán por evitar los escenarios en los que un consumidor está hambriento o el productor supera al consumidor con demasiados eventos. Para evitar esto, usaría el protocolo de notificación de espera donde
- el consumidor
wait
que el productor produzca un evento. - el productor produce el evento y lo
notifies
al consumidor, y luego generalmente se pone en reposo, hasta que el consumidor lonotified
. - cuando el consumidor es notificado de un evento, se despierta, procesa el evento y
notifies
al productor que ha terminado de procesar el evento.
Podrías tener muchos productores y consumidores en este escenario. La adquisición del monitor a través del modelo de exclusión mutua, en wait
, notify
o notify
, está obligada a destruir este modelo, ya que el productor y el consumidor no realizan la espera explícitamente. Los subprocesos subyacentes estarán presentes en el conjunto de espera (utilizado por el modelo de notificación de espera) o en el conjunto de entrada (utilizado por el modelo de exclusión mutua) de un monitor. La invocación de notify
o notify
notifyAll
las hebras notifyAll
se notifyAll
del conjunto de espera al conjunto de entrada del monitor (donde puede haber conflicto para el monitor, entre varias hebras, y no solo la notificada recientemente).
Ahora, cuando desea adquirir automáticamente el monitor en wait
, notify
y notify
notifyAll
utilizando el modelo de exclusión mutua, generalmente es una indicación de que no necesita usar el modelo de notificación de espera. Esto es por inferencia: por lo general, señalaría otros subprocesos, solo después de realizar algún trabajo en un subproceso, es decir, en un cambio de estado. Si adquiere automáticamente el monitor e invoca notify
o notify
notifyAll
, simplemente está moviendo los hilos del conjunto de espera al conjunto de entrada, sin ningún estado intermedio en su programa, lo que implica que la transición no es necesaria. Obviamente, los autores de la JVM estaban al tanto de esto y no habían declarado los métodos como sincronizados.
Puede leer más sobre los conjuntos de espera y los conjuntos de entrada de monitores en el libro de Bill Venner: Dentro de la máquina virtual de Java .
Entre todos los códigos no defectuosos que he leído y escrito, todos ellos usan wait/notify
en un bloque de sincronización más grande que involucra lectura / escritura de otras condiciones
synchronized(lock)
update condition
lock.notify()
synchronized(lock)
while( condition not met)
lock.wait()
Si la wait/notify
están synchronized
, no se hace ningún daño a todos los códigos correctos (puede ser una pequeña penalización de rendimiento); Tampoco servirá de nada a todos los códigos correctos.
Sin embargo, habría permitido y alentado códigos mucho más incorrectos.
Para notificar y notificar a Todos, el problema con su idea es que cuando usted le notifica también tiene otras cosas que normalmente realiza en el mismo bloque sincronizado. Así que sincronizar el método de notificación no le compraría nada, aún necesitaría el bloqueo. Del mismo modo, la espera debe estar en un bloque o método sincronizado para que sea útil, como estar dentro de un spinlock donde la prueba debe estar sincronizada de todos modos. Por lo tanto, la granularidad del bloqueo es errónea para lo que sugieres.
Aquí hay un ejemplo, se trata de la implementación de cola más simple que puede tener en Java:
public class MyQueue<T> {
private List<T> list = new ArrayList<T>();
public T take() throws InterruptedException {
synchronized(list) {
while (list.size() == 0) {
list.wait();
}
return list.remove(0);
}
}
public void put(T object) {
synchronized(list) {
list.add(object);
list.notify();
}
}
}
Por lo tanto, puede tener subprocesos de productores que agregan elementos a la cola y subprocesos de consumidores que eliminan elementos Cuando un subproceso va a sacar algo de la cola, debe verificar dentro del bloque sincronizado que hay algo en la lista, y una vez que se le haya notificado, debe volver a adquirir el bloqueo y asegurarse de que todavía haya algo en la lista (porque algunos otro subproceso del consumidor podría haber intervenido y capturado). También existe el fenómeno del "despertar falso": no puedes confiar en que te despiertan como evidencia suficiente de que algo sucedió, debes verificar la condición que estás esperando. for es realmente cierto, y eso debe hacerse dentro del bloque sincronizado.
En ambos casos, las comprobaciones que rodean la espera deben realizarse con el bloqueo retenido, de modo que cuando el código toma medidas basadas en esas comprobaciones, sepa que esos resultados son válidos actualmente.
Supongo que la razón por la que se requiere el bloqueo synchronized
es que el uso de wait()
o notify()
como la única acción en un bloque synchronized
es casi siempre un error.
Findbugs incluso tiene una advertencia para esto, que llama " notificación desnuda ".