¿Evitar sincronizado (esto) en Java?
Siempre que surge una pregunta en SO sobre la sincronización de Java, algunas personas están muy ansiosas por señalar quesynchronized(this)
se debe evitar. En lugar de ello, afirman, es preferible bloquear una referencia privada.
Algunas de las razones dadas son:
- algún código maligno puede robar tu cerradura (este es muy popular, también tiene una variante "accidental")
- todos los métodos sincronizados dentro de la misma clase usan exactamente el mismo bloqueo, lo que reduce el rendimiento
- estás exponiendo (innecesariamente) demasiada información
Otras personas, incluyéndome a mí, argumentan quesynchronized(this)
es un modismo que se usa mucho (también en bibliotecas Java), que es seguro y se entiende bien. No debe evitarse porque tenga un error y no tenga idea de lo que está sucediendo en su programa multiproceso. En otras palabras: si es aplicable, úselo.
this
Estoy interesado en ver algunos ejemplos del mundo real (sin cosas de foobar) donde es preferible evitar un bloqueo cuandosynchronized(this)
cuando también funcionaría.
Por lo tanto: ¿ debería siempre evitarlo synchronized(this)
y reemplazarlo con un candado en una referencia privada?
Alguna información adicional (actualizada a medida que se dan respuestas):
- Estamos hablando de sincronización de instancias.
- forma implícita (
synchronized
métodos) y explícita desynchronized(this)
Se consideran - Si cita a Bloch u otras autoridades sobre el tema, no omita las partes que no le gustan (por ejemplo, Java efectivo, elemento sobre seguridad de subprocesos: normalmente es el bloqueo de la instancia en sí, pero hay excepciones).
- Si necesita granularidad en su bloqueo distinta a
synchronized(this)
la proporcionada, entoncessynchronized(this)
no es aplicable, por lo que ese no es el problema.
Cubriré cada punto por separado.
Algún código maligno puede robar tu cerradura (este es muy popular, también tiene una variante "accidental")
Lo que más me preocupa es que sea accidental . Lo que significa es que este uso de
this
es parte de la interfaz expuesta de su clase y debe documentarse. A veces se desea la capacidad de otro código para usar su bloqueo. Esto es cierto para cosas comoCollections.synchronizedMap
(ver el javadoc).Todos los métodos sincronizados dentro de la misma clase utilizan exactamente el mismo bloqueo, lo que reduce el rendimiento.
Este es un pensamiento demasiado simplista; simplemente deshacerse de él
synchronized(this)
no resolverá el problema. La sincronización adecuada del rendimiento requerirá más reflexión.Estás exponiendo (innecesariamente) demasiada información.
Esta es una variante del n.° 1. El uso de
synchronized(this)
es parte de su interfaz. Si no quieres o necesitas que esto se exponga, no lo hagas.
Bueno, en primer lugar cabe señalar que:
public void blah() {
synchronized (this) {
// do stuff
}
}
es semánticamente equivalente a:
public synchronized void blah() {
// do stuff
}
lo cual es una razón para no usarlo synchronized(this)
. Se podría argumentar que puedes hacer cosas alrededor de la synchronized(this)
cuadra. La razón habitual es intentar evitar tener que realizar la verificación sincronizada, lo que conduce a todo tipo de problemas de concurrencia, específicamente el problema de bloqueo de doble verificación , que demuestra lo difícil que puede ser realizar una verificación relativamente simple. a salvo de amenazas.
Una cerradura privada es un mecanismo defensivo, lo que nunca es mala idea.
Además, como mencionaste, las cerraduras privadas pueden controlar la granularidad. Un conjunto de operaciones sobre un objeto puede no tener ninguna relación con otro, pero synchronized(this)
excluirá mutuamente el acceso a todos ellos.
synchronized(this)
Realmente no te aporta nada.
Mientras usa sincronizado (esto), está usando la instancia de clase como un bloqueo en sí. Esto significa que mientras el hilo 1 adquiere el bloqueo , el hilo 2 debe esperar.
Supongamos el siguiente código:
public void method1() {
// do something ...
synchronized(this) {
a ++;
}
// ................
}
public void method2() {
// do something ...
synchronized(this) {
b ++;
}
// ................
}
Método 1 modificando la variable a y método 2 modificando la variable b , se debe evitar la modificación concurrente de la misma variable por dos hilos y así es. PERO mientras thread1 modifica a y thread2 modifica b, se puede realizar sin ninguna condición de carrera.
Desafortunadamente, el código anterior no permitirá esto ya que estamos usando la misma referencia para un candado; Esto significa que los subprocesos, incluso si no están en condición de carrera, deben esperar y, obviamente, el código sacrifica la concurrencia del programa.
La solución es utilizar 2 bloqueos diferentes para dos variables diferentes:
public class Test {
private Object lockA = new Object();
private Object lockB = new Object();
public void method1() {
// do something ...
synchronized(lockA) {
a ++;
}
// ................
}
public void method2() {
// do something ...
synchronized(lockB) {
b ++;
}
// ................
}
}
El ejemplo anterior utiliza bloqueos más detallados (2 bloqueos en lugar de uno ( lockA y lockB para las variables a y b respectivamente) y, como resultado, permite una mejor concurrencia; por otro lado, se volvió más complejo que el primer ejemplo...
Si bien estoy de acuerdo en no adherirse ciegamente a reglas dogmáticas, ¿te parece tan excéntrico el escenario del "robo de cerraduras"? De hecho, un subproceso podría adquirir el bloqueo de su objeto "externamente" ( synchronized(theObject) {...}
), bloqueando otros subprocesos que esperan métodos de instancia sincronizados.
Si no cree en los códigos maliciosos, considere que este código podría provenir de terceros (por ejemplo, si desarrolla algún tipo de servidor de aplicaciones).
La versión "accidental" parece menos probable, pero como dicen, "haz algo a prueba de idiotas y alguien inventará un idiota mejor".
Así que estoy de acuerdo con la escuela de pensamiento "depende de lo que hace la clase".
Edite los siguientes 3 primeros comentarios de eljenso:
Nunca he experimentado el problema del robo de cerraduras, pero aquí hay un escenario imaginario:
Digamos que su sistema es un contenedor de servlets y el objeto que estamos considerando es la ServletContext
implementación. Su getAttribute
método debe ser seguro para subprocesos, ya que los atributos de contexto son datos compartidos; entonces lo declaras como synchronized
. Imaginemos también que proporciona un servicio de alojamiento público basado en la implementación de su contenedor.
Soy su cliente e implemento mi servlet "bueno" en su sitio. Sucede que mi código contiene una llamada a getAttribute
.
Un hacker, disfrazado de otro cliente, implementa su servlet malicioso en su sitio. Contiene el siguiente código en el init
método:
sincronizado (this.getServletConfig().getServletContext()) { mientras (verdadero) {} }
Suponiendo que compartimos el mismo contexto de servlet (lo permitido por la especificación siempre que los dos servlets estén en el mismo host virtual), mi llamada getAttribute
está bloqueada para siempre. El hacker ha conseguido un DoS en mi servlet.
Este ataque no es posible si getAttribute
está sincronizado en un candado privado, porque el código de terceros no puede adquirir este candado.
Admito que el ejemplo es artificial y una visión demasiado simplista de cómo funciona un contenedor de servlets, pero en mi humilde opinión demuestra el punto.
Entonces, elegiría mi diseño basándose en consideraciones de seguridad: ¿tendré control total sobre el código que tiene acceso a las instancias? ¿Cuál sería la consecuencia de que un hilo mantenga un bloqueo en una instancia indefinidamente?
Depende de la situación.
Si solo hay una entidad compartida o más de una.
Vea el ejemplo de trabajo completo aquí
Una pequeña introducción.
Subprocesos y entidades compartibles
Es posible que varios subprocesos accedan a la misma entidad, por ejemplo, múltiples subprocesos de conexión que comparten una única cola de mensajes. Dado que los subprocesos se ejecutan simultáneamente, puede existir la posibilidad de que otro anule los datos de uno, lo que puede ser una situación complicada.
Por lo tanto, necesitamos alguna forma de garantizar que solo un subproceso a la vez acceda a la entidad que se puede compartir. (CONCURRENCIA).
Bloque sincronizado
El bloque sincronizado() es una forma de garantizar el acceso simultáneo de entidades que se pueden compartir.
Primero, una pequeña analogía.
Supongamos que hay dos personas P1, P2 (hilos), un lavabo (entidad compartible) dentro de un baño y hay una puerta (cerradura).
Ahora queremos que una persona use el lavabo a la vez.
Un enfoque es cerrar la puerta con P1 cuando la puerta está cerrada. P2 espera hasta que p1 complete su trabajo.
P1 desbloquea la puerta
y solo p1 puede usar el lavabo.
sintaxis.
synchronized(this)
{
SHARED_ENTITY.....
}
"esto" proporcionó el bloqueo intrínseco asociado con la clase (el desarrollador de Java diseñó la clase Objeto de tal manera que cada objeto pueda funcionar como monitor). El enfoque anterior funciona bien cuando solo hay una entidad compartida y varios subprocesos (1: N). N entidades compartibles-M subprocesos
Ahora piense en una situación en la que hay dos lavabos dentro de un baño y solo una puerta. Si usamos el enfoque anterior, solo p1 puede usar un lavabo a la vez mientras p2 esperará afuera. Es un desperdicio de recursos ya que nadie usa B2 (lavabo).
Una solución más inteligente sería crear una habitación más pequeña dentro del baño y proporcionarles una puerta por lavabo. De esta forma, P1 puede acceder a B1 y P2 puede acceder a B2 y viceversa.
washbasin1;
washbasin2;
Object lock1=new Object();
Object lock2=new Object();
synchronized(lock1)
{
washbasin1;
}
synchronized(lock2)
{
washbasin2;
}
Ver más sobre Temas ----> aquí