Evitar sincronizado (esto) en Java?

Cada vez que aparece una pregunta sobre SO sobre la sincronización de Java, algunas personas están muy ansiosas por señalar que se debe evitar synchronized(this) . En cambio, afirman que es preferible un locking en una referencia privada.

Algunas de las razones dadas son:

  • Algún código malvado puede robar tu locking (muy popular en este caso, también tiene una variante “accidental”)
  • todos los métodos sincronizados dentro de la misma clase usan exactamente el mismo locking, lo que reduce el rendimiento
  • estás (innecesariamente) exponiendo demasiada información

Otras personas, incluyéndome a mí, argumentan que synchronized(this) es un modismo que se usa mucho (también en las bibliotecas de Java), es seguro y bien entendido. No debe evitarse porque tiene un error y no tiene una idea de lo que está sucediendo en su progtwig multiproceso. En otras palabras: si es aplicable, entonces úselo.

Estoy interesado en ver algunos ejemplos del mundo real (sin material foobar) donde es preferible evitar un locking en this cuando synchronized(this) también haría el trabajo.

Por lo tanto , ¿debería evitar siempre la synchronized(this) y reemplazarla con un locking en una referencia privada?


Alguna información adicional (actualizada como se dan las respuestas):

  • estamos hablando de sincronización de instancias
  • ambos métodos implícitos (métodos synchronized ) y explícitos de synchronized(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 locking de la instancia en sí, pero hay excepciones).
  • si necesita granularidad en su locking que no sea synchronized(this) , entonces synchronized(this) no es aplicable, así que ese no es el problema

Cubriré cada punto por separado.

  1. Algún código malvado puede robar tu locking (muy popular en este caso, también tiene una variante “accidental”)

    Estoy más preocupado por accidente . 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 locking. Esto es cierto para cosas como Collections.synchronizedMap (ver el javadoc).

  2. Todos los métodos sincronizados dentro de la misma clase usan el mismo locking exacto, lo que reduce el rendimiento

    Este es un pensamiento excesivamente simplista; simplemente deshacerse de synchronized(this) no resolverá el problema. La sincronización adecuada para el rendimiento requerirá más reflexión.

  3. Estás (innecesariamente) exponiendo demasiada información

    Esta es una variante de # 1. El uso de synchronized(this) es parte de su interfaz. Si no quiere / necesita esto expuesto, no lo haga.

Bueno, en primer lugar, debe señalarse que:

 public void blah() { synchronized (this) { // do stuff } } 

es semánticamente equivalente a:

 public synchronized void blah() { // do stuff } 

que es una razón para no usar synchronized(this) . Podría argumentar que puede hacer cosas en torno al bloque synchronized(this) . La razón habitual es intentar y evitar tener que hacer la comprobación sincronizada en absoluto, lo que conduce a todo tipo de problemas de concurrencia, específicamente el problema de doble locking comprobado , que simplemente muestra lo difícil que puede ser hacer un control relativamente simple. a salvo de amenazas.

Un locking privado es un mecanismo defensivo, que nunca es una mala idea.

Además, como lo aludió, los lockings privados pueden controlar la granularidad. Un conjunto de operaciones en un objeto puede no estar relacionado con otro, pero synchronized(this) excluirá mutuamente el acceso a todos ellos.

synchronized(this) simplemente no te da nada.

Mientras usa sincronizado (esto) está usando la instancia de clase como un locking en sí mismo. Esto significa que mientras el locking es adquirido por el hilo 1, el hilo 2 debe esperar

Supongamos que 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 el método 2 modificando la variable b , la modificación concurrente de la misma variable por dos hilos debe evitarse y lo es. PERO mientras que la modificación de thread1 y la modificación de thread2 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 locking; Esto significa que los hilos, incluso si no están en condición de carrera, deben esperar y, obviamente, el código sacrifica la concurrencia del progtwig.

La solución es usar 2 lockings diferentes para dos variables diferentes.

  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 usa más lockings de grano fino (2 lockings 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 adherirme ciegamente a las reglas dogmáticas, ¿el escenario de “locking de robo” te parece tan excéntrico? De hecho, un hilo podría adquirir el locking de su objeto “externamente” ( synchronized(theObject) {...} ), bloqueando otros hilos esperando en los métodos de instancia sincronizados.

Si no cree en código malicioso, 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á a un mejor idiota”.

Así que estoy de acuerdo con la escuela de pensamiento it-depends-on-what-the-class-do.


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 implementación de ServletContext . Su método getAttribute 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 y despliegue mi servlet “bueno” en su sitio. Sucede que mi código contiene una llamada a getAttribute .

Un hacker, disfrazado como otro cliente, despliega su servlet malicioso en su sitio. Contiene el siguiente código en el método init :

 sincronizado (this.getServletConfig (). getServletContext ()) {
    while (true) {}
 }

Suponiendo que compartimos el mismo contexto de servlet (permitido por la especificación siempre y cuando los dos servlets estén en el mismo host virtual), mi llamada a getAttribute se bloquea para siempre. El hacker ha logrado un DoS en mi servlet.

Este ataque no es posible si getAttribute está sincronizado en un locking privado, porque el código de terceros no puede adquirir este locking.

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.

Por lo tanto, tomaría la decisión de diseño en función de consideraciones de seguridad: ¿tendré control total sobre el código que tiene acceso a las instancias? ¿Cuál sería la consecuencia de un hilo que mantiene un locking en una instancia indefinidamente?

Parece que hay un consenso diferente en C # y en los campamentos de Java sobre esto. La mayoría del código de Java que he visto usa:

 // apply mutex to this instance synchronized(this) { // do work here } 

mientras que la mayoría del código C # opta por lo que podría decirse que es más seguro:

 // instance level lock object private readonly object _syncObj = new object(); ... // apply mutex to private instance level field (a System.Object usually) lock(_syncObj) { // do work here } 

La expresión de C # es ciertamente más segura. Como se mencionó anteriormente, no se puede realizar acceso malicioso / accidental al locking desde fuera de la instancia. El código Java también tiene este riesgo, pero parece que la comunidad Java ha gravitado con el tiempo a la versión ligeramente menos segura, pero un poco más concisa.

Eso no es una excavación contra Java, solo un reflection de mi experiencia trabajando en ambos idiomas.

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.

Hilos y entidades compartibles
Es posible que varios subprocesos accedan a la misma entidad, por ejemplo, conexiones múltiples que comparten un mensaje único. Como los hilos se ejecutan simultáneamente, puede haber una posibilidad de anular los datos de uno por otro, lo que puede ser una situación desordenada.
Entonces, necesitamos alguna manera de asegurarnos de que se acceda a la entidad compartible solo por un hilo a la vez. (CONCURRENCIA).

Bloque sincronizado
El bloque sincronizado () es una forma de garantizar el acceso concurrente de la entidad compartible.
Primero, una pequeña analogía
Supongamos que hay dos personas P1, P2 (hilos), un lavabo (entidad que se puede compartir) dentro de un baño y hay una puerta (cerradura).
Ahora queremos que una persona use el lavabo a la vez.
El enfoque es bloquear la puerta con P1, cuando la puerta está bloqueada P2 espera a que p1 complete su trabajo
P1 desbloquea la puerta
entonces solo p1 puede usar el lavabo.

syntax.

 synchronized(this) { SHARED_ENTITY..... } 

“this” proporcionó el locking intrínseco asociado a la clase (la clase Object diseñada por el desarrollador Java de tal manera que cada objeto puede funcionar como monitor). El enfoque anterior funciona bien cuando solo hay una entidad compartida y varios hilos (1: N).
enter image description here N entidades compartibles-M hilos
Ahora piense en una situación en la que hay dos lavabos dentro de un baño y solo una puerta. Si estamos utilizando el enfoque anterior, solo p1 puede usar un lavabo a la vez mientras que p2 esperará afuera. Es desperdicio de recursos ya que nadie está usando B2 (lavabo).
Un enfoque más inteligente sería crear salas más pequeñas 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; } 

enter image description here
enter image description here

Ver más en Hilos —-> aquí

El paquete java.util.concurrent ha reducido enormemente la complejidad de mi código de seguridad de subprocesos. Solo tengo evidencia anecdótica para continuar, pero la mayoría del trabajo que he visto con synchronized(x) parece estar reimplementando un Lock, Semaphore o Latch, pero usando los monitores de nivel inferior.

Con esto en mente, sincronizar usando cualquiera de estos mecanismos es análogo a sincronizar en un objeto interno, en lugar de fugar un locking. Esto es beneficioso porque tiene la certeza absoluta de que controla la entrada en el monitor por dos o más hilos.

Si has decidido eso:

  • lo que tienes que hacer es bloquear el objeto actual; y
  • desea bloquearlo con una granularidad más pequeña que un método completo;

entonces no veo el tabú sobre synchronizezd (esto).

Algunas personas usan deliberadamente sincronización (esto) (en lugar de marcar el método sincronizado) dentro de todo el contenido de un método porque creen que es “más claro para el lector” qué objeto se está sincronizando realmente. Siempre que las personas tomen una decisión informada (por ejemplo, entiendan que al hacerlo están insertando códigos de bytes adicionales en el método y esto podría tener un efecto en cadena sobre las posibles optimizaciones), no veo un problema en particular con esto. . Siempre debe documentar el comportamiento simultáneo de su progtwig, por lo que no veo que el argumento “sincronizado” publique el comportamiento sea tan convincente.

En cuanto a la pregunta de qué locking de objeto debe usar, creo que no hay nada de malo en sincronizar en el objeto actual si esto es lo que espera la lógica de lo que está haciendo y cómo se usaría típicamente su clase . Por ejemplo, con una colección, el objeto que lógicamente esperaría bloquear es generalmente la colección en sí misma.

  1. Haga que sus datos sean inmutables si es posible (variables final )
  2. Si no puede evitar la mutación de datos compartidos en varios hilos, use construcciones de progtwigción de alto nivel [por ejemplo, API de Lock granular]

Un locking proporciona acceso exclusivo a un recurso compartido: solo un hilo a la vez puede adquirir el locking y todo acceso al recurso compartido requiere que el locking se adquiera primero.

Código de muestra para usar ReentrantLock que implementa Lock interfaz de Lock

  class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } } 

Ventajas de Lock Over Synchronized (esto)

  1. El uso de métodos o declaraciones sincronizados obliga a que todas las adquisiciones y liberaciones de lockings ocurran de una manera estructurada por bloques.

  2. Las implementaciones de locking proporcionan una funcionalidad adicional sobre el uso de métodos y declaraciones sincronizados al proporcionar

    1. Un bash de no locking para adquirir un locking ( tryLock() )
    2. Un bash de adquirir el locking que puede ser interrumpido ( lockInterruptibly() )
    3. Un bash de adquirir el locking que puede tryLock(long, TimeUnit) tiempo de espera ( tryLock(long, TimeUnit) ).
  3. Una clase de locking también puede proporcionar un comportamiento y semántica que es bastante diferente de la del locking de monitor implícito, como

    1. orden garantizada
    2. uso no reincorporado
    3. Detección de punto muerto

Eche un vistazo a esta pregunta SE con respecto a varios tipos de Locks :

Sincronización vs locking

Puede lograr seguridad de subprocesos utilizando API de concurrencia avanzada en lugar de bloques sincronizados. Esta página de documentación proporciona buenas construcciones de progtwigción para lograr la seguridad del hilo.

Los objetos de locking admiten expresiones de locking que simplifican muchas aplicaciones concurrentes.

Los ejecutores definen una API de alto nivel para iniciar y administrar subprocesos. Las implementaciones de Executor proporcionadas por java.util.concurrent proporcionan administración de grupo de subprocesos adecuada para aplicaciones a gran escala.

Las Colecciones concurrentes facilitan la administración de grandes colecciones de datos y pueden reducir enormemente la necesidad de sincronización.

Las variables atómicas tienen características que minimizan la sincronización y ayudan a evitar errores de coherencia de memoria.

ThreadLocalRandom (en JDK 7) proporciona una generación eficiente de números pseudoaleatorios de múltiples hilos.

Consulte también los paquetes java.util.concurrent y java.util.concurrent.atomic para otras construcciones de progtwigción.

Creo que hay una buena explicación sobre por qué cada una de estas son técnicas vitales en un libro titulado Java Concurrency In Practice por Brian Goetz. Él deja muy claro un punto: debe usar el mismo candado “EN TODAS PARTES” para proteger el estado de su objeto. El método sincronizado y la sincronización en un objeto a menudo van de la mano. Eg Vector sincroniza todos sus métodos. Si tiene un control para un objeto vectorial y va a hacer “poner si está ausente”, entonces simplemente Vector sincronizar sus propios métodos individuales no lo protegerá de la corrupción del estado. Necesita sincronizar usando synchronized (vectorHandle). Esto dará como resultado que el MISMO locking sea adquirido por cada hilo que tenga un mango para el vector y protegerá el estado general del vector. Esto se llama locking del lado del cliente. Sabemos, de hecho, que vector sincroniza (esto) / sincroniza todos sus métodos y, por lo tanto, la sincronización en el objeto vectorHandle dará como resultado una sincronización adecuada del estado de los objetos vectoriales. Es una tontería creer que estás protegido por hilos solo porque estás usando una colección segura para hilos. Esta es precisamente la razón por la que ConcurrentHashMap introdujo explícitamente el método putIfAbsent – para hacer que tales operaciones sean atómicas.

En resumen

  1. La sincronización a nivel de método permite el locking del lado del cliente.
  2. Si tiene un objeto de locking privado, imposibilita el locking del lado del cliente. Esto está bien si sabes que tu clase no tiene el tipo de funcionalidad “poner si falta”.
  3. Si está diseñando una biblioteca, sincronizar con esto o sincronizar el método suele ser más inteligente. Porque rara vez estás en posición de decidir cómo se usará tu clase.
  4. Si Vector hubiera usado un objeto de locking privado, hubiera sido imposible “poner si estuviera ausente” a la derecha. El código del cliente nunca obtendrá un control del locking privado, rompiendo así la regla fundamental de usar EXACT SAME LOCK para proteger su estado.
  5. La sincronización en este o métodos sincronizados tiene un problema como otros señalaron: alguien podría obtener un locking y nunca liberarlo. Todos los otros hilos seguirían esperando que se libere el locking.
  6. Entonces, sepa lo que está haciendo y adopte el correcto.
  7. Alguien argumentó que tener un objeto de locking privado le da una mejor granularidad, por ejemplo, si dos operaciones no están relacionadas, podrían estar protegidas por diferentes lockings, lo que daría como resultado un mejor rendimiento. Pero esto creo que es olor de diseño y no olor a código: si dos operaciones no están relacionadas, ¿por qué son parte de la MISMA clase? ¿Por qué debería un club de clase funcionalidades no relacionadas en absoluto? Puede ser una clase de utilidad? Hmmmm – ¿alguna utilidad que proporciona manipulación de cadenas y formato de fecha del calendario a través de la misma instancia? … ¡no tiene ningún sentido para mí al menos!

No, no deberías siempre . Sin embargo, tiendo a evitarlo cuando hay muchas preocupaciones sobre un objeto en particular que solo tienen que ser seguras para ellos. Por ejemplo, puede tener un objeto de datos mutable que tenga los campos “etiqueta” y “principal”; estos deben ser seguros, pero cambiar uno no necesita impedir que el otro sea escrito / leído. (En la práctica, evitaría esto al declarar los campos volátiles y / o usar los wrappers AtomicFoo de java.util.concurrent).

La sincronización en general es un poco torpe, ya que bloquea un gran locking en lugar de pensar exactamente cómo se puede permitir que los hilos funcionen entre sí. Usar synchronized(this) es incluso más torpe y antisocial, ya que dice “nadie puede cambiar nada en esta clase mientras mantengo el locking”. ¿Con qué frecuencia necesitas hacer eso?

Preferiría tener más lockings granulares; incluso si desea evitar que todo cambie (tal vez está serializando el objeto), puede adquirir todos los lockings para lograr lo mismo, además es más explícito de esa manera. Cuando utiliza synchronized(this) , no está claro exactamente por qué está sincronizando, o cuáles podrían ser los efectos secundarios. Si usa synchronized(labelMonitor) , o incluso mejor, labelLock.getWriteLock().lock() , está claro lo que está haciendo y los efectos de su sección crítica.

Respuesta corta : debes entender la diferencia y elegir según el código.

Respuesta larga : en general, prefiero evitar la sincronización (esto) para reducir la contención, pero los lockings privados agregan complejidad que debe tener en cuenta. Entonces use la sincronización correcta para el trabajo correcto. Si no tiene tanta experiencia con la progtwigción de subprocesos múltiples, preferiría limitarme al locking de instancias y leer sobre este tema. (Dicho esto: solo usar synchronize (esto) no hace que tu clase sea totalmente segura para subprocesos). Este no es un tema fácil, pero una vez que te acostumbras, la respuesta a usar synchronize (this) o no es algo natural .

Se usa un candado para visibilidad o para proteger algunos datos de modificaciones concurrentes que pueden llevar a una carrera.

Cuando necesitas hacer que las operaciones de tipo primitivo sean atómicas, hay opciones disponibles como AtomicInteger y los “me gusta”.

Pero supongamos que tiene dos enteros que están relacionados entre sí, como las coordenadas y , que están relacionados entre sí y deben modificarse de forma atómica. Entonces los protegerías usando un mismo candado.

Un locking solo debería proteger el estado que está relacionado entre sí. No menos y no más. Si usa synchronized(this) en cada método, incluso si el estado de la clase no está relacionado, todos los subprocesos enfrentarán contención incluso si actualizan el estado no relacionado.

 class Point{ private int x; private int y; public Point(int x, int y){ this.x = x; this.y = y; } //mutating methods should be guarded by same lock public synchronized void changeCoordinates(int x, int y){ this.x = x; this.y = y; } } 

En el ejemplo anterior, tengo un solo método que muta tanto x como y no dos métodos diferentes, ya que y están relacionados y si hubiera dado dos métodos diferentes para mutar y separado, entonces no habría sido seguro para subprocesos.

Este ejemplo es solo para demostrar y no necesariamente la forma en que debería implementarse. La mejor manera de hacerlo sería hacerlo INMUTABLE .

Ahora, en oposición al ejemplo de Point , hay un ejemplo de TwoCounters ya proporcionado por @Andreas donde el estado que está siendo protegido por dos lockings diferentes ya que el estado no está relacionado entre sí.

El proceso de utilizar diferentes lockings para proteger estados no relacionados se denomina Bloqueo de ttwig o locking de división

La razón para no sincronizar con esto es que a veces necesita más de un locking (el segundo locking a menudo se elimina después de un pensamiento adicional, pero aún lo necesita en el estado intermedio). Si bloquea esto , siempre debe recordar cuál de los dos lockings es este ; si bloquea en un objeto privado, el nombre de la variable le dice eso.

Desde el punto de vista del lector, si ve el locking en esto , siempre tiene que responder las dos preguntas:

  1. ¿Qué tipo de acceso está protegido por esto ?
  2. una cerradura es realmente suficiente, ¿alguien introdujo un error?

Un ejemplo:

 class BadObject { private Something mStuff; synchronized setStuff(Something stuff) { mStuff = stuff; } synchronized getStuff(Something stuff) { return mStuff; } private MyListener myListener = new MyListener() { public void onMyEvent(...) { setStuff(...); } } synchronized void longOperation(MyListener l) { ... l.onMyEvent(...); ... } } 

Si dos hilos comienzan longOperation() en dos instancias diferentes de BadObject , adquieren sus lockings; cuando es hora de invocar l.onMyEvent(...) , tenemos un interlocking porque ninguno de los subprocesos puede adquirir el locking del otro objeto.

En este ejemplo, podemos eliminar el punto muerto utilizando dos lockings, uno para operaciones cortas y uno para operaciones largas.

Como ya se ha dicho, el bloque sincronizado puede usar variables definidas por el usuario como objeto de locking, cuando la función sincronizada solo usa “esto”. Y, por supuesto, puede manipular áreas de su función que deberían estar sincronizadas, etc.

Pero todos dicen que no hay diferencia entre la función sincronizada y el locking que cubre toda la función usando “esto” como objeto de locking. Eso no es cierto, la diferencia está en el código de bytes que se generará en ambas situaciones. En caso de uso sincronizado de bloques, se debe asignar una variable local que contenga referencia a “esto”. Y como resultado tendremos un tamaño de función un poco mayor (no relevante si tiene pocas funciones).

Explicación más detallada de la diferencia que puede encontrar aquí: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Además, el uso del locking sincronizado no es bueno debido al siguiente punto de vista:

La palabra clave sincronizada es muy limitada en un área: al salir de un bloque sincronizado, todos los subprocesos que esperan ese locking deben desbloquearse, pero solo uno de esos subprocesos toma el locking; todos los demás ven que se toma la cerradura y vuelven al estado bloqueado. No se trata solo de un montón de ciclos de procesamiento desperdiciados: a menudo el cambio de contexto para desbloquear un hilo también implica que la memoria de paginación esté fuera del disco, y eso es muy, muy, costoso.

Para obtener más detalles en esta área, le recomiendo que lea este artículo: http://java.dzone.com/articles/synchronized-considered

Un buen ejemplo de uso sincronizado (esto).

 // add listener public final synchronized void addListener(IListener l) {listeners.add(l);} // remove listener public final synchronized void removeListener(IListener l) {listeners.remove(l);} // routine that raise events public void run() { // some code here... Set ls; synchronized(this) { ls = listeners.clone(); } for (IListener l : ls) { l.processEvent(event); } // some code here... } 

As you can see here, we use synchronize on this to easy cooperate of lengthly (possibly infinite loop of run method) with some synchronized methods there.

Of course it can be very easily rewritten with using synchronized on private field. But sometimes, when we already have some design with synchronized methods (ie legacy class, we derive from, synchronized(this) can be the only solution).

It depends on the task you want to do, but I wouldn’t use it. Also, check if the thread-save-ness you want to accompish couldn’t be done by synchronize(this) in the first place? There are also some nice locks in the API that might help you 🙂

I think points one (somebody else using your lock) and two (all methods using the same lock needlessly) can happen in any fairly large application. Especially when there’s no good communication between developers.

It’s not cast in stone, it’s mostly an issue of good practice and preventing errors.