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:
- algún código maligno puede robar tu cerradura (muy popular este, también tiene una variante "accidentalmente")
- Todos los métodos sincronizados dentro de la misma clase utilizan exactamente el mismo bloqueo, lo que reduce el rendimiento
- estás (innecesariamente) exponiendo demasiada información
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 this
es 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 (
synchronized
métodos) y forma 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: 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, entoncessynchronized(this)
no es aplicable, por lo que ese no es el problema
Respuestas:
Cubriré cada punto por separado.
Estoy más preocupado por accidentalmente . Lo que significa es que este uso
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).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.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.fuente
keySet()
yvalues()
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 loSynchronizedSortedMap
necesita para implementar submapas que se bloqueen en la instancia de mapa original.Bueno, en primer lugar hay que señalar que:
es semánticamente equivalente a:
cuál es una razón para no usar
synchronized(this)
. Podría argumentar que puede hacer cosas alrededor delsynchronized(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.fuente
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:
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:
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 ...
fuente
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
ServletContext
implementación. SugetAttribute
método debe ser seguro para subprocesos, ya que los atributos de contexto son datos compartidos; así que lo declaras comosynchronized
. 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 el
init
método: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
getAttribute
está bloqueada para siempre. El hacker ha logrado un DoS en mi servlet.Este ataque no es posible si
getAttribute
está 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?
fuente
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.
"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.
Ver más en hilos ----> aquí
fuente
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:
Considerando que la mayoría del código C # opta por el posiblemente más seguro:
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.
fuente
El
java.util.concurrent
paquete 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 vistosynchronized(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.
fuente
final
variables)Lock
API granular ]Código de muestra para usar
ReentrantLock
que implementa laLock
interfazVentajas del bloqueo sobre sincronizado (esto)
El uso de métodos o declaraciones sincronizadas obliga a que todas las adquisiciones y liberaciones de bloqueos ocurran de forma estructurada en bloques.
Las implementaciones de bloqueo proporcionan funcionalidad adicional sobre el uso de métodos y declaraciones sincronizadas al proporcionar
tryLock()
)lockInterruptibly()
)tryLock(long, TimeUnit)
).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
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.
fuente
Si has decidido eso:
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.
fuente
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
fuente
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 usasynchronized(labelMonitor)
, o incluso mejorlabelLock.getWriteLock().lock()
, está claro lo que está haciendo y a qué se limitan los efectos de su sección crítica.fuente
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 .
fuente
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
AtomicInteger
y me gusta.Pero supongamos que tiene dos números enteros que están relacionados entre sí, como
x
yy
coordina, 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.En el ejemplo anterior, solo tengo un método que muta tanto
x
yy
no dos métodos diferentes comox
yy
están relacionados y si hubiera dado dos métodos diferentes para mutarx
y pory
separado, 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
Point
ejemplo, hay un ejemplo deTwoCounters
ya 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
fuente
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:
Un ejemplo:
Si dos hilos comienzan
longOperation()
en dos instancias diferentes deBadObject
, adquieren sus bloqueos; cuando es hora de invocarl.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.
fuente
BadObject
A invocalongOperation
a B, pasando AmyListener
y viceversa. No imposible, pero bastante complicado, apoyando mis puntos anteriores.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:
Para obtener más detalles en esta área, le recomendaría que lea este artículo: http://java.dzone.com/articles/synchronized-considered
fuente
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
@Synchronized
que generar la plantilla en tiempo de compilación:compila a
fuente
Un buen ejemplo de uso sincronizado (esto).
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).
fuente
this
. Podría ser un campo privado.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 :)
fuente
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.
fuente
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.Synchronised
se 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.fuente
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.
fuente
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.
fuente