¿Evitar sincronizado (esto) en Java?


381

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

Algunas de las razones dadas son:

Otras personas, incluyéndome a mí, argumentan que synchronized(this)es un idioma que se usa mucho (también en las bibliotecas de Java), es seguro y se entiende bien. No debe evitarse porque tiene un error y no tiene ni idea de lo que está sucediendo en su programa multiproceso. En otras palabras: si es aplicable, utilícelo.

Estoy interesado en ver algunos ejemplos del mundo real (sin cosas de foobar) en los que evitar un bloqueo thises preferible cuando synchronized(this)también haría el trabajo.

Por lo tanto: ¿siempre debe evitarlo synchronized(this)y reemplazarlo con un candado en una referencia privada?


Alguna información adicional (actualizada a medida que se dan las respuestas):

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

44
También me gustaría señalar que el contexto es importante: el bit "Típicamente es el bloqueo en la instancia misma" está dentro de una sección sobre la documentación de una clase condicionalmente segura para subprocesos, cuando se hace público el bloqueo. En otras palabras, esa oración se aplica cuando ya ha tomado esta decisión.
Jon Skeet el

En ausencia de sincronización interna, y cuando se necesita sincronización externa, el bloqueo suele ser la instancia en sí misma, dice Bloch básicamente. Entonces, ¿por qué este no sería el caso de la sincronización interna con bloqueo en 'esto' también? (La importancia de la documentación es otra cuestión)
Eljenso

Existe una compensación entre granularidad extendida y sobrecarga adicional de CPU y sobrecarga de solicitudes de bus, ya que el bloqueo en un Objeto externo probablemente requerirá que se modifique e intercambie una línea de caché separada entre cachés de CPU (cf. MESIF y MOESI).
ArtemGr

1
Creo que, en el mundo de la programación defensiva, se evitan errores no por idioma sino por código. Cuando alguien me hace una pregunta, "¿Qué tan optimizada es su sincronización?", Quiero decir "Muy" en lugar de "Muy, a menos que alguien más no siga el idioma".
Sgene9

Respuestas:


132

Cubriré cada punto por separado.

  1. Algún código maligno puede robar tu cerradura (muy popular este, también tiene una variante "accidentalmente")

    Estoy más preocupado por accidentalmente . Lo que significa es que este uso thises 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 como Collections.synchronizedMap(ver el javadoc).

  2. Todos los métodos sincronizados dentro de la misma clase usan 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 para el rendimiento requerirá más reflexión.

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

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


1. "sincronizado" no es parte de la interfaz expuesta de su clase. 2. de acuerdo 3. ver 1.
eljenso

66
Esencialmente sincronizado (esto) está expuesto porque significa que el código externo puede afectar el funcionamiento de su clase. Por lo tanto, afirmo que debe documentarlo como interfaz, incluso si el idioma no lo hace.
Darron

15
Similar. Consulte el Javadoc para Collections.synchronizedMap (): el objeto devuelto usa sincronizado (esto) internamente y esperan que el consumidor aproveche eso para usar el mismo bloqueo para operaciones atómicas a gran escala como la iteración.
Darron

3
De hecho, Collections.synchronizedMap () NO usa sincronizado (esto) internamente, usa un objeto de bloqueo final privado.
Bas Leijdekkers

1
@Bas Leijdekkers: la documentación especifica claramente que la sincronización ocurre en la instancia de mapa devuelta. Lo interesante es que las vistas devueltas keySet()y values()no se bloquean en (sus) this, sino en la instancia del mapa, que es importante para obtener un comportamiento coherente para todas las operaciones del mapa. La razón, el objeto de bloqueo se factoriza en una variable, es que la subclase lo SynchronizedSortedMapnecesita para implementar submapas que se bloqueen en la instancia de mapa original.
Holger

86

Bueno, en primer lugar hay que señalar que:

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

es semánticamente equivalente a:

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

cuál es una razón para no usar synchronized(this). Podría argumentar que puede hacer cosas alrededor del synchronized(this)bloque. La razón habitual es tratar de evitar tener que hacer la verificación sincronizada, lo que conduce a todo tipo de problemas de concurrencia, específicamente el problema del bloqueo de doble verificación , que solo muestra lo difícil que puede ser hacer una verificación relativamente simple a salvo de amenazas.

Una cerradura privada es un mecanismo defensivo, que nunca es una mala idea.

Además, como aludiste, los bloqueos 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.


44
"sincronizado (esto) simplemente no te da nada". Ok, lo reemplazo con una sincronización (myPrivateFinalLock). ¿Qué me da eso? Hablas de que es un mecanismo defensivo. ¿De qué estoy protegido?
eljenso

14
Estás protegido contra el bloqueo accidental (o malicioso) de 'esto' por objetos externos.
cletus

14
No estoy de acuerdo en absoluto con esta respuesta: un bloqueo siempre debe mantenerse durante el menor tiempo posible, y esa es precisamente la razón por la que desea "hacer cosas" alrededor de un bloque sincronizado en lugar de sincronizar todo el método .
Olivier el

55
Hacer cosas fuera del bloque sincronizado siempre tiene buenas intenciones. El punto es que las personas se equivocan muchas veces y ni siquiera se dan cuenta, al igual que en el problema de bloqueo doblemente verificado. El camino al infierno está pavimentado con buenas intenciones.
cletus

16
No estoy de acuerdo en general con "X es un mecanismo defensivo, que nunca es una mala idea". Hay una gran cantidad de código innecesariamente hinchado por esta actitud.
finnw

54

Mientras usa sincronizado (esto), usa la instancia de clase como un bloqueo en sí mismo. 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 ++;      
    }
    // ................
}

El método 1 que modifica la variable a y el método 2 que modifican la variable b , la modificación concurrente de la misma variable por dos hilos debe evitarse 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 bloqueo; Esto significa que los subprocesos, incluso si no están en una condición de carrera, deberían esperar y, obviamente, el código sacrifica la concurrencia del programa.

La solución es usar 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 usa más bloqueos de grano fino (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 ...


Esto es muy peligroso Ahora ha introducido un requisito de orden de bloqueo del lado del cliente (usuario de esta clase). Si dos hilos llaman a method1 () y method2 () en un orden diferente, es probable que se bloqueen, pero el usuario de esta clase no tiene idea de que este es el caso.
daveb

77
La granularidad no proporcionada por "sincronizado (esto)" está fuera del alcance de mi pregunta. ¿Y no deberían ser definitivos sus campos de bloqueo?
eljenso

10
para tener un punto muerto debemos realizar una llamada desde el bloque sincronizado por A al bloque sincronizado por B. daveb, te equivocas ...
Andreas Bakurov

3
No hay punto muerto en este ejemplo por lo que puedo ver. Acepto que es solo pseudocódigo, pero usaría una de las implementaciones de java.util.concurrent.locks.Lock como java.util.concurrent.locks.ReentrantLock
Shawn Vader el

15

Si bien estoy de acuerdo en no adherirme ciegamente a las reglas dogmáticas, ¿te parece tan excéntrico el escenario de "robo de cerraduras"? Un hilo podría adquirir el bloqueo de su objeto "externamente" ( synchronized(theObject) {...}), bloqueando otros hilos que esperan métodos de instancia sincronizados.

Si no cree en el 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, "haga algo a prueba de idiotas y alguien inventará un mejor idiota".

Así que estoy de acuerdo con la escuela de pensamiento que depende de lo que la clase haga.


Editar siguiendo los primeros 3 comentarios de eljenso:

Nunca he experimentado el problema de robo de cerradura, pero aquí hay un escenario imaginario:

Digamos que su sistema es un contenedor de servlets, y el objeto que estamos considerando es la ServletContextimplementación. Su getAttributemétodo debe ser seguro para subprocesos, ya que los atributos de contexto son datos compartidos; así que 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, despliega su servlet malicioso en su sitio. Contiene el siguiente código en elinit método:

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

Suponiendo que compartamos el mismo contexto de servlet (permitido por la especificación siempre que los dos servlets estén en el mismo host virtual), mi llamada getAttributeestá bloqueada para siempre. El hacker ha logrado un DoS en mi servlet.

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

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, lo demuestra.

Por lo tanto, elegiría el diseño en función de la consideración 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 qué hace la clase: si se trata de un objeto "importante", ¿se bloquea la referencia privada? ¿El bloqueo de otra instancia será suficiente?
eljenso

66
Sí, el escenario de robo de cerradura me parece descabellado. Todos lo mencionan, pero ¿quién lo ha hecho o experimentado? Si "accidentalmente" bloquea un objeto que no debería, entonces hay un nombre para este tipo de situación: es un error. Arreglalo.
eljenso

44
Además, bloquear las referencias internas no está libre del "ataque de sincronización externa": si sabe que cierta parte sincronizada del código espera a que ocurra un evento externo (por ejemplo, escritura de archivo, valor en DB, evento de temporizador) probablemente pueda arregle para que también se bloquee.
eljenso

Déjame confesar que soy uno de esos idiotas, aunque lo hice cuando era joven. Pensé que el código era más limpio al no crear un objeto de bloqueo explícito y, en cambio, usé otro objeto final privado que necesitaba participar en el monitor. No sabía que el objeto en sí mismo se sincronizaba. Puedes imaginar el consiguiente hijinx ...
Alan Cabrera

12

Depende de la situación.
Si solo hay una entidad que comparte o más de una.

Vea el ejemplo de trabajo completo aquí

Una pequeña introducción

Subprocesos y entidades que
se pueden compartir Es posible que varios subprocesos accedan a la misma entidad, por ejemplo, múltiples hilos de conexión que comparten un único mensajeQueue. Dado que los subprocesos se ejecutan simultáneamente, puede existir la posibilidad de anular los datos de uno por otro, lo que puede ser una situación desordenada.
Por lo tanto, necesitamos alguna forma de asegurarnos de que solo se acceda a una entidad que se puede compartir a la vez. (CONCURRENCIA).

Bloque
sincronizado El bloque sincronizado () es una forma de garantizar el acceso concurrente de la entidad que se puede compartir.
Primero, una pequeña analogía.
Suponga que hay P1, P2 (hilos) para dos personas, 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.
Un enfoque consiste en cerrar la puerta con P1 cuando la puerta está cerrada. P2 espera hasta que p1 complete su trabajo.
P1 abre la puerta,
entonces solo p1 puede usar el lavabo.

sintaxis.

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

"this" proporcionó el bloqueo intrínseco asociado con 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 subprocesos (1: N). N entidades compartibles-Hilos M 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 un desperdicio de recursos ya que nadie está utilizando B2 (lavabo). Un enfoque 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 manera, P1 puede acceder a B1 y P2 puede acceder a B2 y viceversa.
ingrese la descripción de la imagen aquí

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

ingrese la descripción de la imagen aquí
ingrese la descripción de la imagen aquí

Ver más en hilos ----> aquí


11

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

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

Considerando que la mayoría del código C # opta por el posiblemente 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
}

El lenguaje C # es ciertamente más seguro. Como se mencionó anteriormente, no se puede hacer un acceso malicioso / accidental a la cerradura 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 hacia la versión un poco menos segura, pero un poco más concisa.

Eso no pretende ser una excavación contra Java, solo un reflejo de mi experiencia trabajando en ambos idiomas.


3
Quizás, dado que C # es un lenguaje más joven, ¿aprendieron de los malos patrones que se descubrieron en el campamento de Java y codificaron cosas como esta mejor? ¿Hay también menos singletons? :)
Bill K

3
El he. ¡Posiblemente cierto, pero no voy a morder el anzuelo! Creo que puedo decir con certeza que hay más letras mayúsculas en el código C #;)
serg10

1
Simplemente no es cierto (para decirlo bien)
tcurdt

7

El java.util.concurrentpaquete ha reducido enormemente la complejidad de mi código seguro para subprocesos. Solo tengo pruebas anecdóticas para continuar, pero la mayoría del trabajo que he visto synchronized(x)parece estar implementando de nuevo un Lock, Semaphore o Latch, pero usando los monitores de nivel inferior.

Con esto en mente, la sincronización usando cualquiera de estos mecanismos es análoga a la sincronización en un objeto interno, en lugar de perder un bloqueo. Esto es beneficioso porque tiene la certeza absoluta de que controla la entrada en el monitor mediante dos o más subprocesos.


6
  1. Haga que sus datos sean inmutables si es posible ( finalvariables)
  2. Si no puede evitar la mutación de datos compartidos en varios subprocesos, utilice construcciones de programación de alto nivel [por ejemplo, LockAPI granular ]

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

Código de muestra para usar ReentrantLockque implementa la Lockinterfaz

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

Ventajas del bloqueo sobre sincronizado (esto)

  1. El uso de métodos o declaraciones sincronizadas obliga a que todas las adquisiciones y liberaciones de bloqueos ocurran de forma estructurada en bloques.

  2. Las implementaciones de bloqueo proporcionan funcionalidad adicional sobre el uso de métodos y declaraciones sincronizadas al proporcionar

    1. Un intento sin bloqueo para adquirir un bloqueo ( tryLock())
    2. Un intento de adquirir el bloqueo que se puede interrumpir ( lockInterruptibly())
    3. Un intento de adquirir el bloqueo que puede agotar el tiempo de espera ( tryLock(long, TimeUnit)).
  3. Una clase de bloqueo también puede proporcionar un comportamiento y una semántica bastante diferente de la del bloqueo de monitor implícito, como

    1. pedido garantizado
    2. uso no entrante
    3. Detección de punto muerto

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

Sincronización vs bloqueo

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

Los objetos de bloqueo admiten modismos de bloqueo que simplifican muchas aplicaciones concurrentes.

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

Las Colecciones concurrentes facilitan la gestión de grandes colecciones de datos y pueden reducir en gran medida la necesidad de sincronización.

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

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

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


5

Si has decidido eso:

  • lo que debe 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 syncizezd (esto).

Algunas personas usan deliberadamente sincronizado (esto) (en lugar de marcar el método sincronizado) dentro de todo el contenido de un método porque piensan que es "más claro para el lector" en qué objeto se está sincronizando realmente. Mientras las personas hagan una elección informada (por ejemplo, comprendan que al hacerlo, en realidad están insertando bytecodes adicionales en el método y esto podría tener un efecto secundario en las optimizaciones potenciales), no veo un problema en particular con esto . Siempre debe documentar el comportamiento concurrente de su programa, por lo que no veo que el argumento "'sincronizado' publique el comportamiento" sea tan convincente.

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


1
"si esto fuera esperado por la lógica ..." es un punto que estoy tratando de transmitir también. No veo el punto de usar siempre bloqueos privados, aunque el consenso general parece ser que es mejor, ya que no duele y es más defensivo.
eljenso

4

Creo que hay una buena explicación de por qué cada una de estas son técnicas vitales en tu haber en un libro llamado Java Concurrency In Practice de Brian Goetz. Él deja un punto muy claro: 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. Por ejemplo, Vector sincroniza todos sus métodos. Si tiene un identificador para un objeto vectorial y va a hacer "poner si está ausente", simplemente Vector sincronizando sus propios métodos individuales no lo protegerá de la corrupción del estado. Necesita sincronizar usando sincronizado (vectorHandle). Esto dará como resultado que el mismo bloqueo sea adquirido por cada subproceso que tenga un identificador para el vector y protegerá el estado general del vector. Esto se llama bloqueo del lado del cliente. De hecho, sabemos que el vector se 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 eres seguro para subprocesos solo porque estás usando una colección segura para subprocesos. 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 bloqueo del lado del cliente.
  2. Si tiene un objeto de bloqueo privado, hace que el bloqueo del lado del cliente sea imposible. Esto está bien si sabe que su clase no tiene el tipo de funcionalidad "poner si está ausente".
  3. Si está diseñando una biblioteca, entonces sincronizar esto o sincronizar el método suele ser más sabio. Porque rara vez estás en condiciones de decidir cómo se utilizará tu clase.
  4. Si Vector hubiera utilizado un objeto de bloqueo privado, hubiera sido imposible "ponerlo ausente" correctamente. El código del cliente nunca obtendrá un control para el bloqueo privado, rompiendo así la regla fundamental de usar el BLOQUEO MISMO EXACTO para proteger su estado.
  5. Sincronizar con esto o los métodos sincronizados tienen un problema como otros han señalado: alguien podría obtener un bloqueo y nunca liberarlo. Todos los otros subprocesos seguirían esperando que se libere el bloqueo.
  6. Entonces, sepa lo que está haciendo y adopte el correcto.
  7. Alguien argumentó que tener un objeto de bloqueo privado le brinda una mejor granularidad, por ejemplo, si dos operaciones no están relacionadas, podrían estar protegidas por diferentes bloqueos que darían como resultado un mejor rendimiento. Pero creo que esto es olor de diseño y no olor de código: si dos operaciones no tienen relación alguna, ¿por qué forman parte de la MISMA clase? ¿Por qué un club de clase no tiene funcionalidades relacionadas? Puede ser una clase de utilidad? Hmmmm: ¿alguna utilidad que proporcione manipulación de cadenas y formato de fecha de calendario a través de la misma instancia? ... no tiene ningún sentido para mí al menos !!

3

No, no deberías siempre . Sin embargo, tiendo a evitarlo cuando hay múltiples preocupaciones sobre un objeto en particular que solo necesitan ser seguras con respecto a ellos mismos. Por ejemplo, puede tener un objeto de datos mutables que tenga los campos "etiqueta" y "padre"; estos deben ser seguros para subprocesos, pero cambiar uno no tiene por qué impedir que el otro se escriba / lea. (En la práctica, evitaría esto declarando los campos volátiles y / o usando los envoltorios AtomicFoo de java.util.concurrent).

La sincronización en general es un poco torpe, ya que bloquea un gran bloqueo en lugar de pensar exactamente cómo se podría permitir que los hilos trabajen uno alrededor del otro. El uso synchronized(this)es aún más torpe y antisocial, ya que dice "nadie puede cambiar nada en esta clase mientras sostengo el bloqueo". ¿Con qué frecuencia necesitas hacer eso?

Preferiría tener cerraduras más granulares; incluso si desea evitar que todo cambie (tal vez está serializando el objeto), puede adquirir todos los bloqueos para lograr lo mismo, además es más explícito de esa manera. Cuando lo usa 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 a qué se limitan los efectos de su sección crítica.


3

Respuesta corta : debe comprender la diferencia y elegir según el código.

Respuesta larga : en general, preferiría tratar de evitar la sincronización (esto) para reducir la contención, pero los bloqueos privados agregan complejidad que debe tener en cuenta. Así que use la sincronización correcta para el trabajo correcto. Si no tiene tanta experiencia con la programación de subprocesos múltiples, prefiero seguir bloqueando instancias y leer sobre este tema. (Dicho esto: solo usar sincronizar (esto) no hace que su clase sea completamente segura para subprocesos). Este no es un tema fácil, pero una vez que se acostumbra, la respuesta sobre si usar sincronizar (esto) o no es algo natural .


¿Te entiendo correctamente cuando dices que depende de tu experiencia?
eljenso

En primer lugar, depende del código que desea escribir. Solo digo que es posible que necesite un poco más de experiencia cuando desvía para no usar sincronizar (esto).
tcurdt

2

Un bloqueo se usa para la visibilidad o para proteger algunos datos de modificaciones concurrentes que pueden conducir a la carrera.

Cuando solo necesita hacer operaciones de tipo primitivas para que sean atómicas, hay opciones disponibles como AtomicIntegery me gusta.

Pero supongamos que tiene dos números enteros que están relacionados entre sí, como xy ycoordina, que están relacionados entre sí y deben ser cambiadas de forma atómica. Entonces los protegerías usando una misma cerradura.

Una cerradura solo debe proteger el estado 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 se enfrentarán incluso si se actualiza 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, solo tengo un método que muta tanto xy yno dos métodos diferentes como xy yestán relacionados y si hubiera dado dos métodos diferentes para mutar xy por yseparado, entonces no habría sido seguro para subprocesos.

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

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

El proceso de usar diferentes bloqueos para proteger estados no relacionados se llama Bloqueo de franjas o Bloqueo de división


1

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

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

  1. ¿Qué tipo de acceso está protegido por esto ?
  2. es una cerradura realmente suficiente, ¿alguien no 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 bloqueos; cuando es hora de invocar l.onMyEvent(...), tenemos un punto muerto porque ninguno de los hilos puede adquirir el bloqueo del otro objeto.

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


2
La única forma de obtener un punto muerto en este ejemplo es cuando BadObjectA invoca longOperationa B, pasando A myListenery viceversa. No imposible, pero bastante complicado, apoyando mis puntos anteriores.
eljenso

1

Como ya se dijo aquí, el bloque sincronizado puede usar una variable definida por el usuario como objeto de bloqueo, cuando la función sincronizada usa solo "esto". Y, por supuesto, puede manipular las á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 bloque que cubre la función completa usando "esto" como objeto de bloqueo. Eso no es cierto, la diferencia está en el código de bytes que se generará en ambas situaciones. En caso de uso de bloque sincronizado, debe asignarse una variable local que contenga referencia a "esto". Y como resultado tendremos un tamaño de función un poco más grande (no relevante si solo tiene un número reducido de 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 bloque 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 hilos que esperan ese bloqueo deben desbloquearse, pero solo uno de esos hilos puede tomar el bloqueo; todos los demás ven que se ha bloqueado y vuelven al estado bloqueado. Eso no es solo una gran cantidad de ciclos de procesamiento desperdiciados: a menudo, el cambio de contexto para desbloquear un subproceso también implica paginar la memoria del disco, y eso es muy, muy costoso.

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


1

Esto es realmente complementario a las otras respuestas, pero si su principal objeción al uso de objetos privados para el bloqueo es que satura su clase con campos que no están relacionados con la lógica de negocios, entonces el Proyecto Lombok tiene @Synchronizedque generar la plantilla en tiempo de compilación:

@Synchronized
public int foo() {
    return 0;
}

compila a

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}

0

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...
}

Como puede ver aquí, utilizamos sincronizar en esto para cooperar fácilmente de forma prolongada (posiblemente un bucle infinito del método de ejecución) con algunos métodos sincronizados allí.

Por supuesto, puede reescribirse muy fácilmente con el uso sincronizado en campo privado. Pero a veces, cuando ya tenemos un diseño con métodos sincronizados (es decir, clase heredada, derivamos de, sincronizado (esto) puede ser la única solución).


Cualquier objeto podría usarse como cerradura aquí. No necesita serlo this. Podría ser un campo privado.
finnw

Correcto, pero el propósito de este ejemplo era mostrar cómo hacer una sincronización adecuada, si decidimos usar la sincronización de métodos.
Bart Prokop

0

Depende de la tarea que quieras hacer, pero no la usaría. Además, verifique si el proceso de guardado de subprocesos que desea lograr no se pudo hacer sincronizando (esto) en primer lugar. También hay algunos bloqueos agradables en la API que podrían ayudarte :)


0

Solo quiero mencionar una posible solución para referencias privadas únicas en partes atómicas de código sin dependencias. Puede usar un Hashmap estático con bloqueos y un método estático simple llamado atomic () que crea las referencias requeridas automáticamente usando la información de la pila (nombre completo de la clase y número de línea). Luego puede usar este método para sincronizar sentencias sin escribir un nuevo objeto de bloqueo.

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}

0

Evite usarlo synchronized(this)como mecanismo de bloqueo: esto bloquea toda la instancia de clase y puede causar puntos muertos. En tales casos, refactorice el código para bloquear solo un método o variable específicos, de esa manera no se bloqueará toda la clase. Synchronisedse puede usar dentro del nivel del método.
En lugar de usar synchronized(this), el siguiente código muestra cómo puede bloquear un método.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }

0

Mis dos centavos en 2019 a pesar de que esta pregunta ya podría haberse resuelto.

Bloquear 'esto' no es malo si sabes lo que estás haciendo, pero detrás de la escena bloqueando 'esto' es (que desafortunadamente lo que permite la palabra clave sincronizada en la definición del método).

Si realmente desea que los usuarios de su clase puedan 'robar' su bloqueo (es decir, evitar que otros hilos lo traten), realmente desea que todos los métodos sincronizados esperen mientras se está ejecutando otro método de sincronización, etc. Debe ser intencional y bien pensado (y, por lo tanto, documentado para ayudar a sus usuarios a comprenderlo).

Para más detalles, en el reverso debe saber qué está 'ganando' (o 'perdiendo') si bloquea una cerradura no accesible (nadie puede 'robar' su cerradura, usted tiene el control total, etc.) ..).

El problema para mí es que la palabra clave sincronizada en la firma de definición de método hace que sea demasiado fácil para los programadores no pensar en qué bloquear, lo cual es una cosa muy importante en la que pensar si no desea tener problemas en un sistema múltiple. programa roscado.

No se puede argumentar que "típicamente" no desea que los usuarios de su clase puedan hacer estas cosas o que "típicamente" desee ... Depende de qué funcionalidad esté codificando. No puede hacer una regla general ya que no puede predecir todos los casos de uso.

Considere, por ejemplo, la impresora de imprenta que usa un bloqueo interno, pero luego las personas luchan por usarla desde múltiples hilos si no quieren que su salida se intercale.

Si su cerradura es accesible fuera de la clase o no, es su decisión como programador en función de la funcionalidad que tenga la clase. Es parte de la api. No puede alejarse, por ejemplo, de sincronizado (esto) a sincronizado (provateObjet) sin arriesgarse a romper los cambios en el código que lo usa.

Nota 1: Sé que puede lograr lo que sincroniza (esto) 'logra' usando un objeto de bloqueo explícito y exponiéndolo, pero creo que es innecesario si su comportamiento está bien documentado y realmente sabe lo que significa bloquear 'esto'.

Nota 2: No estoy de acuerdo con el argumento de que si algún código está robando accidentalmente su bloqueo, es un error y tiene que resolverlo. De alguna manera, esto es el mismo argumento que decir que puedo hacer públicos todos mis métodos, incluso si no están destinados a ser públicos. Si alguien llama "accidentalmente" a mi método destinado a ser privado, es un error. ¿Por qué permitir este accidente en primer lugar! Si la capacidad de robar su cerradura es un problema para su clase, no lo permita. Tan simple como eso.


-3

Creo que los puntos uno (otra persona que usa su candado) y dos (todos los métodos que usan el mismo candado innecesariamente) pueden suceder en cualquier aplicación bastante grande. Especialmente cuando no hay una buena comunicación entre los desarrolladores.

No está escrito en piedra, es principalmente una cuestión de buenas prácticas y de prevención de errores.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.