Sincronización vs bloqueo


183

java.util.concurrentAPI proporciona una clase llamada as Lock, que básicamente serializaría el control para acceder al recurso crítico. Da método como park()y unpark().

Podemos hacer cosas similares si podemos usar synchronizedpalabras clave y usar wait()y notify() notifyAll()métodos.

Me pregunto cuál de estos es mejor en la práctica y por qué.


Respuestas:


178

Si simplemente está bloqueando un objeto, preferiría usar synchronized

Ejemplo:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

Tienes que hacerlo explícitamente en try{} finally{}todas partes.

Mientras que con sincronizado, es súper claro e imposible equivocarse:

synchronized(myObject) {
    doSomethingNifty();
}

Dicho esto, Locks puede ser más útil para cosas más complicadas en las que no puede adquirir y liberar de una manera tan limpia. Sinceramente, preferiría evitar usar bare Locks en primer lugar, y simplemente ir con un control de concurrencia más sofisticado, como a CyclicBarriero a LinkedBlockingQueue, si satisfacen sus necesidades.

Nunca he tenido una razón para usar wait()o notify()pero puede haber algunas buenas.


1
¿Cuál es la diferencia entre esperar / notificar vs estacionar / dejar de estacionar LockSupport? docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
Pacerier

66
Al principio, el ejemplo tenía sentido con las cerraduras, pero luego me di cuenta de que si usa un intento, finalmente bloquee ese problema que se evitaría si no se liberan las cerraduras
William Reed

Ahh ... Uno de esos momentos para apreciar el modelo RAII en C ++. std::lock_guard
WhiZTiM

67

Me pregunto cuál de estos es mejor en la práctica y por qué.

He descubierto que Locky Condition(y otras concurrentclases nuevas ) son solo más herramientas para la caja de herramientas. Podía hacer casi todo lo que necesitaba con mi viejo martillo (la synchronizedpalabra clave), pero era difícil de usar en algunas situaciones. Varias de esas situaciones incómodas se volvieron mucho más simples una vez que agregué más herramientas a mi caja de herramientas: un mazo de goma, un martillo de bola, una palanca y algunos punzones. Sin embargo , mi viejo martillo todavía ve su parte de uso.

No creo que uno sea realmente "mejor" que el otro, sino que cada uno es mejor para diferentes problemas. En pocas palabras, el modelo simple y la naturaleza orientada al alcance de synchronizedme ayudan a protegerme de los errores en mi código, pero esas mismas ventajas son a veces obstáculos en escenarios más complejos. Es estos escenarios más complejos que el paquete concurrente se creó para ayudar a abordar. Pero el uso de estas construcciones de nivel superior requiere una administración más explícita y cuidadosa en el código.

===

Creo que JavaDoc hace un buen trabajo al describir la distinción entre Locky synchronized(el énfasis es mío):

Las implementaciones de bloqueo proporcionan operaciones de bloqueo más extensas que las que se pueden obtener utilizando métodos y declaraciones sincronizados. Permiten una estructuración más flexible , pueden tener propiedades bastante diferentes y pueden admitir múltiples objetos de condición asociados .

...

El uso de métodos o sentencias sincronizadas proporciona acceso al bloqueo de monitor implícito asociado con cada objeto, pero obliga a que todas las adquisiciones y liberaciones de bloqueos ocurran de forma estructurada en bloques : cuando se adquieren múltiples bloqueos , deben liberarse en el orden opuesto , y todos los bloqueos deben liberarse en el mismo ámbito léxico en el que fueron adquiridos .

Si bien el mecanismo de definición del alcance para métodos y declaraciones sincronizados hace que sea mucho más fácil programar con bloqueos de monitor y ayuda a evitar muchos errores de programación comunes que involucran bloqueos, hay ocasiones en las que necesita trabajar con bloqueos de una manera más flexible. Por ejemplo, * * algunos algoritmos * para atravesar estructuras de datos a las que se accede simultáneamente requieren el uso de "mano sobre mano" o "bloqueo de cadena" : adquiere el bloqueo del nodo A, luego el nodo B, luego suelta A y adquiere C, luego suelte B y adquiera D y así sucesivamente. Las implementaciones de la interfaz de bloqueo permiten el uso de tales técnicas al permitir que un bloqueo sea adquirido y liberado en diferentes ámbitos , ypermitiendo la adquisición y liberación de múltiples bloqueos en cualquier orden .

Con esta mayor flexibilidad viene una responsabilidad adicional . La ausencia de bloqueo estructurado por bloques elimina la liberación automática de bloqueos que ocurre con los métodos y las declaraciones sincronizadas. En la mayoría de los casos, se debe usar el siguiente idioma:

...

Cuando el bloqueo y el desbloqueo se producen en diferentes ámbitos , se debe tener cuidado para garantizar que todo el código que se ejecuta mientras se mantiene el bloqueo esté protegido por try-finally o try-catch para garantizar que el bloqueo se libere cuando sea necesario.

Las implementaciones de bloqueo proporcionan una funcionalidad adicional sobre el uso de métodos y sentencias sincronizadas al proporcionar un intento sin bloqueo para adquirir un bloqueo (tryLock ()), un intento de adquirir el bloqueo que se puede interrumpir (lockInterruptibly (), y un intento de adquirir el bloqueo que puede expirar (tryLock (long, TimeUnit)).

...


23

Usted puede lograr todo lo que las empresas de servicios públicos en java.util.concurrent hacen con las primitivas de bajo nivel como synchronized, volatileo espera / notifique

Sin embargo, la concurrencia es complicada y la mayoría de las personas se equivocan al menos en algunas partes, lo que hace que su código sea incorrecto o ineficiente (o ambos).

La API concurrente proporciona un enfoque de nivel superior, que es más fácil (y, como tal, más seguro) de usar. En pocas palabras, ya no debería necesitar usarlo synchronized, volatile, wait, notifydirectamente.

El bloqueo de la clase en sí está en el lado de bajo nivel de esta caja de herramientas, es posible que ni siquiera necesidad de usar que directa o bien (se puede utilizar Queuesy semáforo y esas cosas, etc, la mayoría de las veces).


2
¿La espera / notificación simple se considera una primitiva de nivel inferior que java.util.concurrent.locks.LockSupport's park / unpark, o es al revés?
Pacerier

@Pacerier: considero que ambos son de bajo nivel (es decir, algo que un programador de aplicaciones desearía evitar usar directamente), pero ciertamente las partes de nivel inferior de java.util.concurrency (como el paquete de bloqueos) están construidas en la parte superior de las primitivas JVM nativas esperar / notificar (que son incluso de nivel inferior).
Thilo

2
No, quiero decir fuera de los 3: Thread.sleep / interrupt, Object.wait / notify, LockSupport.park / unpark, ¿cuál es el más primitivo?
Pacerier

2
@Thilo No estoy seguro de cómo respalda su declaración que java.util.concurrentes más fácil [en general] que las características del lenguaje ( synchronized, etc.). Cuando lo usa java.util.concurrent, tiene que acostumbrarse a completar lock.lock(); try { ... } finally { lock.unlock() }antes de escribir el código, mientras que con un synchronizedestá básicamente bien desde el principio. Solo sobre esta base, diría que synchronizedes más fácil (dado que desea su comportamiento) que java.util.concurrent.locks.Lock. par 4
Iwan Aucamp

1
No piense que puede replicar exactamente el comportamiento de las clases AtomicXXX solo con las primitivas de concurrencia, ya que dependen de la invocación nativa de CAS no disponible antes de java.util.concurrent.
Duncan Armstrong

15

Hay 4 factores principales sobre por qué querrías usar synchronizedo java.util.concurrent.Lock.

Nota: el bloqueo sincronizado es lo que quiero decir cuando digo bloqueo intrínseco.

  1. Cuando Java 5 salió con ReentrantLocks, demostraron tener una diferencia de rendimiento bastante notable y luego un bloqueo intrínseco. Si está buscando un mecanismo de bloqueo más rápido y está ejecutando 1.5, considere jucReentrantLock. El bloqueo intrínseco de Java 6 ahora es comparable.

  2. jucLock tiene diferentes mecanismos para bloquear. Bloqueo interrumpible: intente bloquear hasta que se interrumpa el hilo de bloqueo; bloqueo temporizado: intente bloquear durante un cierto período de tiempo y renunciar si no tiene éxito; tryLock: intenta bloquear, si algún otro hilo está reteniendo el bloqueo. Todo esto está incluido aparte de la cerradura simple. El bloqueo intrínseco solo ofrece un bloqueo simple

  3. Estilo. Si tanto 1 como 2 no caen en categorías de lo que le preocupa a la mayoría de las personas, incluido yo mismo, las semenáticas de bloqueo intrínsecas serían más fáciles de leer y menos detalladas que el bloqueo jucLock.
  4. Múltiples condiciones. Un objeto que bloquee solo puede ser notificado y esperar un solo caso. El nuevo método de Condición de Lock permite que un solo Lock tenga múltiples razones para esperar o señalar. Todavía tengo que necesitar esta funcionalidad en la práctica, pero es una buena característica para aquellos que la necesitan.

Me gustaron los detalles de tu comentario. Agregaría un punto más: el ReadWriteLock proporciona un comportamiento útil si se trata de varios hilos, de los cuales solo algunos necesitan escribir en el objeto. Varios subprocesos pueden leer el objeto simultáneamente y solo se bloquean si otro subproceso ya está escribiendo en él.
Sam Goldberg

5

Me gustaría agregar algunas cosas más sobre la respuesta de Bert F.

Locksadmite varios métodos para un control de bloqueo de grano más fino, que son más expresivos que los monitores implícitos ( synchronizedbloqueos)

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. Sin embargo, algunos bloqueos pueden permitir el acceso concurrente a un recurso compartido, como el bloqueo de lectura de un ReadWriteLock.

Ventajas del bloqueo sobre la sincronización de la página de documentación

  1. El uso de métodos o sentencias sincronizadas proporciona acceso al bloqueo de monitor implícito asociado con cada objeto, pero obliga a que todas las adquisiciones y liberaciones de bloqueos ocurran de forma estructurada en bloques.

  2. Las implementaciones de bloqueo proporcionan una funcionalidad adicional sobre el uso de métodos y declaraciones sincronizadas al proporcionar un intento sin bloqueo para adquirir un lock (tryLock()), un intento de adquirir el bloqueo que se puede interrumpir ( lockInterruptibly()y un intento de adquirir el bloqueo que puede) timeout (tryLock(long, TimeUnit)).

  3. Una clase de bloqueo también puede proporcionar un comportamiento y una semántica que es bastante diferente del bloqueo de monitor implícito, como el pedido garantizado, el uso no reentrante o la detección de punto muerto

ReentrantLock : en términos simples según mi entendimiento, ReentrantLockpermite que un objeto vuelva a entrar de una sección crítica a otra sección crítica. Como ya tiene bloqueo para ingresar a una sección crítica, puede usar otra sección crítica en el mismo objeto utilizando el bloqueo actual.

ReentrantLockcaracterísticas clave según este artículo

  1. Capacidad para bloquear de forma interrumpida.
  2. Capacidad de tiempo de espera mientras se espera el bloqueo.
  3. Poder para crear un bloqueo justo.
  4. API para obtener la lista de subprocesos en espera de bloqueo.
  5. Flexibilidad para intentar bloquear sin bloquear.

Puedes usar ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock para adquirir más control sobre el bloqueo granular en operaciones de lectura y escritura.

Además de estos tres ReentrantLocks, Java 8 proporciona un bloqueo más

Bloqueo sellado:

Java 8 se entrega con un nuevo tipo de bloqueo llamado StampedLock que también admite bloqueos de lectura y escritura como en el ejemplo anterior. A diferencia de ReadWriteLock, los métodos de bloqueo de StampedLock devuelven un sello representado por un valor largo.

Puede usar estos sellos para liberar un bloqueo o para verificar si el bloqueo sigue siendo válido. Además, los bloqueos estampados admiten otro modo de bloqueo denominado bloqueo optimista.

Eche un vistazo a este artículo sobre el uso de diferentes tipos ReentrantLocky StampedLockbloqueos.


4

La principal diferencia es la equidad, en otras palabras, ¿las solicitudes se manejan FIFO o puede haber una intrusión? La sincronización a nivel de método garantiza una asignación justa o FIFO de la cerradura. Utilizando

synchronized(foo) {
}

o

lock.acquire(); .....lock.release();

No garantiza la equidad.

Si tiene mucha contención por el bloqueo, puede encontrar fácilmente la intrusión donde las solicitudes más nuevas se bloquean y las solicitudes más antiguas se atascan. He visto casos en los que 200 hilos llegan en breve para un bloqueo y el segundo en llegar se procesó en último lugar. Esto está bien para algunas aplicaciones, pero para otras es mortal.

Consulte el libro "Concurrencia de Java en la práctica" de Brian Goetz, sección 13.3 para una discusión completa de este tema.


55
"La sincronización a nivel de método garantiza una asignación justa o FIFO de la cerradura". => ¿En serio? ¿Está diciendo que un método sincronizado se comporta de manera diferente y justa que envolver el contenido de los métodos en un bloque {} sincronizado? No lo creo, ¿o entendí mal esa frase ...?
weiresr

Sí, aunque sorprendente y contra intuitivo, eso es correcto. El libro de Goetz es la mejor explicación.
Brian Tarbox

Si observa el código provisto por @BrianTarbox, el bloque sincronizado está usando algún objeto que no sea "esto" para bloquear. En teoría, no hay diferencia entre un método sincronizado y poner todo el cuerpo de dicho método dentro de un bloque sincronizado, siempre y cuando el bloque use "esto" como bloqueo.
xburgos

La respuesta debe editarse para incluir una cita, y dejar en claro que "garantía" aquí es "garantía estadística", no determinista.
Nathan Hughes

Lo siento, acabo de descubrir que, por error, rechacé esta respuesta hace unos días (clic torpe). Desafortunadamente, SO actualmente no me deja revertirlo. Espero poder arreglarlo más tarde.
Mikko Östlund

3

El libro de Brian Goetz "Java Concurrency In Practice", sección 13.3: "... Al igual que el ReentrantLock predeterminado, el bloqueo intrínseco no ofrece garantías de equidad deterministas, pero las garantías de equidad estadística de la mayoría de las implementaciones de bloqueo son lo suficientemente buenas para casi todas las situaciones ..."


2

Lock facilita la vida de los programadores. Aquí hay algunas situaciones que se pueden lograr fácilmente con el bloqueo.

  1. Bloquee en un método y libere el bloqueo en otro método.
  2. Sin embargo, si tiene dos subprocesos trabajando en dos partes diferentes de código, en el primer subproceso tiene un requisito previo sobre una determinada parte del código en el segundo subproceso (mientras que otros subprocesos también funcionan en la misma parte del código en el segundo hilo simultáneamente). Un bloqueo compartido puede resolver este problema con bastante facilidad.
  3. Implementación de monitores. Por ejemplo, una cola simple donde los métodos put y get se ejecutan desde muchos otros hilos. Sin embargo, no desea que varios métodos put (u get) se ejecuten simultáneamente, ni el método put y get se ejecuten simultáneamente. Una cerradura privada hace que su vida sea mucho más fácil de lograr.

Mientras, el bloqueo y las condiciones se basan en el mecanismo sincronizado. Por lo tanto, ciertamente puede lograr la misma funcionalidad que puede lograr con el bloqueo. Sin embargo, resolver escenarios complejos con sincronizado puede dificultar su vida y puede desviarse de resolver el problema real.


1

Gran diferencia entre bloqueo y sincronización:

  • con bloqueos, puede liberar y adquirir los bloqueos en cualquier orden.
  • con sincronizado, puede liberar los bloqueos solo en el orden en que se adquirió.

0

Bloquear y sincronizar bloque sirve para el mismo propósito, pero depende del uso. Considera la parte de abajo

void randomFunction(){
.
.
.
synchronize(this){
//do some functionality
}

.
.
.
synchronize(this)
{
// do some functionality
}


} // end of randomFunction

En el caso anterior, si un hilo ingresa al bloque de sincronización, el otro bloque también está bloqueado. Si hay varios bloques de sincronización en el mismo objeto, todos los bloques están bloqueados. En tales situaciones, java.util.concurrent.Lock se puede usar para evitar el bloqueo no deseado de bloques

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.