Sincronización vs locking

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

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

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

Si simplemente estás 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 try{} finally{} explícitamente try{} finally{} todas partes.

Mientras que con sincronización, es súper claro e imposible equivocarse:

 synchronized(myObject) { doSomethingNifty(); } 

Dicho esto, Lock s puede ser más útil para cosas más complicadas donde no se puede adquirir y liberar de una manera tan limpia. Honestamente, preferiría evitar el uso de Lock s desnudo en primer lugar, y solo ir con un control de concurrencia más sofisticado, como un CyclicBarrier o un LinkedBlockingQueue , si satisfacen sus necesidades.

Nunca tuve una razón para usar wait() o notify() pero puede haber algunos buenos.

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

Descubrí que Lock and Condition (y otras clases concurrent nuevas) son solo más herramientas para la caja de herramientas. Podía hacer casi todo lo que necesitaba con mi viejo martillo de garra (la palabra clave synchronized ), pero era incómodo 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 bolas, una palanca y algunos golpes de uñas. Sin embargo , mi viejo martillo de garra todavía ve su uso compartido.

No creo que uno sea realmente “mejor” que el otro, sino que cada uno se adapta mejor a diferentes problemas. En pocas palabras, el modelo simple y la naturaleza orientada al scope de synchronized me ayudan a protegerme de errores en mi código, pero esas mismas ventajas a veces son obstáculos en escenarios más complejos. Es estos escenarios más complejos que el paquete simultáneo se creó para ayudar a abordar. Pero el uso de estos constructos de mayor nivel requiere una administración más explícita y cuidadosa en el código.

===

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

Las implementaciones de locking proporcionan operaciones de locking 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 Condition asociados .

El uso de métodos o declaraciones sincronizadas proporciona acceso al locking de monitor implícito asociado con cada objeto, pero obliga a que todas las lockings de adquisición y liberación se produzcan de forma bloqueada : cuando se adquieren múltiples lockings , deben liberarse en el orden opuesto , y todos los lockings deben ser liberados en el mismo scope léxico en el que fueron adquiridos .

Si bien el mecanismo de scope para métodos y declaraciones sincronizados hace que sea mucho más fácil progtwigr con lockings de monitor y ayuda a evitar muchos errores de progtwigción comunes relacionados con lockings, hay ocasiones en las que necesita trabajar con lockings de una manera más flexible. Por ejemplo, * * algunos algoritmos * para atravesar estructuras de datos accedidas simultáneamente requieren el uso de “mano sobre mano” o “locking de cadena” : usted adquiere el locking del nodo A, luego el nodo B, luego libera A y adquiere C, luego suelta B y adquieres D, y así sucesivamente. Las implementaciones de la interfaz de locking permiten el uso de tales técnicas al permitir que un locking sea adquirido y liberado en diferentes ámbitos , y permitiendo que se adquieran y liberen múltiples lockings en cualquier orden .

Con esta mayor flexibilidad viene una responsabilidad adicional . La ausencia de locking estructurado elimina la liberación automática de lockings que se produce con métodos y declaraciones sincronizados. En la mayoría de los casos, se debe usar el siguiente modismo:

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

Las implementaciones de locking proporcionan una funcionalidad adicional sobre el uso de métodos y declaraciones sincronizados al proporcionar un bash de no locking para adquirir un locking (tryLock ()), un bash de adquirir el locking que se puede interrumpir (lockInterruptibly (), y un bash de adquirir el locking que puede agotar el tiempo de espera (tryLock (long, TimeUnit)).

Puede lograr todo lo que las utilidades en java.util.concurrent hacen con las primitivas de bajo nivel como synchronized , volatile o esperar / notificar

Sin embargo, la concurrencia es complicada, y la mayoría de las personas obtienen al menos algunas partes incorrectas, haciendo 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 de usar (y, por lo tanto, más seguro). En pocas palabras, no debería necesitar usar synchronized, volatile, wait, notify directamente nunca más.

La clase de locking en sí está en el lado inferior de esta caja de herramientas, es posible que ni siquiera necesite usarla directamente (puede usar Queues y Semáforo y demás, etc., la mayoría de las veces).

Hay 4 factores principales en por qué desearía usar synchronized o java.util.concurrent.Lock .

Nota: El locking sincronizado es lo que quiero decir cuando digo locking intrínseco.

  1. Cuando Java 5 salió con ReentrantLocks, probaron tener una diferencia de rendimiento bastante notable y luego un locking intrínseco. Si estás buscando un mecanismo de locking más rápido y estás ejecutando 1.5, considera jucReentrantLock. El locking 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 locking; locking temporizado: intente bloquear durante un cierto período de tiempo y renuncie si no tiene éxito; tryLock – bash de locking, si algún otro hilo mantiene el locking abandonado. Todo esto se incluye aparte del locking simple. El locking intrínseco solo ofrece un locking simple

  3. Estilo. Si tanto 1 como 2 no entran dentro de las categorías de lo que le preocupa, la mayoría de las personas, incluyéndome a mí, encontrarán que los semenáticos de locking intrínsecos son más fáciles de leer y menos detallados que el locking de juclock.
  4. Múltiples condiciones. Un objeto que encierre solo se puede notificar y esperar por un solo caso. El nuevo método de Condición de Lock permite que un único locking tenga múltiples motivos para esperar o señalizar. Todavía tengo que necesitar esta funcionalidad en la práctica, pero es una buena característica para quienes la necesitan.

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

 synchronized(foo) { } 

o

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

no asegura la equidad

Si tiene muchas disputas por el locking, puede encontrar fácilmente un estallido en el que las solicitudes más recientes obtienen el locking y las solicitudes anteriores se atascan. He visto casos en los que llegan 200 hilos en poco tiempo para un candado y el segundo en llegar fue procesado el último. Esto está bien para algunas aplicaciones pero para otras es mortal.

Consulte la sección 13.3 del libro “Concurrencia en práctica” de Brian Goetz para una discusión completa de este tema.

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

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

Locks admiten varios métodos para un control de locking más fino, que son más expresivos que los monitores implícitos (lockings synchronized )

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. Sin embargo, algunos lockings pueden permitir el acceso simultáneo a un recurso compartido, como el locking de lectura de un ReadWriteLock.

Ventajas de Lock over Synchronization desde la página de documentación

  1. El uso de métodos o declaraciones sincronizadas proporciona acceso al locking de monitor implícito asociado a cada objeto, pero obliga a que todas las capturas y liberaciones de lockings se realicen de forma bloqueada

  2. Las implementaciones de locking proporcionan una funcionalidad adicional sobre el uso de métodos y declaraciones sincronizados al proporcionar un bash de no locking para adquirir un lock (tryLock()) , un bash de adquirir el locking que se puede interrumpir ( lockInterruptibly() , y un bash de adquirir el locking que puede timeout (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 el orden garantizado, el uso sin reentrada o la detección de punto muerto.

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

ReentrantLock características clave según este artículo

  1. Posibilidad de bloquear de forma interrumpible.
  2. Capacidad de tiempo de espera mientras espera el locking.
  3. Poder para crear locking justo.
  4. API para obtener la lista de hilo en espera para el locking.
  5. Flexibilidad para intentar bloquear sin bloquear.

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

Además de estos tres ReentrantLocks, java 8 proporciona un locking más

StampedLock:

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

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

Eche un vistazo a este artículo sobre el uso de diferentes tipos de ReentrantLock y StampedLock .

Lock hace la vida de los progtwigdores más fácil. Aquí hay algunas situaciones que se pueden lograr más fácilmente con el locking.

  1. Bloquee en un método y libere el locking en otro método.
  2. Tiene dos hilos trabajando en dos partes de código diferentes, sin embargo, en el primer hilo hay dependencia en el segundo hilo para completar un fragmento de código antes de continuar (mientras que otros hilos también funcionan simultáneamente). Un locking compartido puede resolver este problema con bastante facilidad.
  3. Implementando monitores. Por ejemplo, una cola simple donde los métodos de poner y obtener se ejecutan desde muchos hilos diferentes. Sin embargo, ¿desea sobrepasar los mismos métodos uno sobre otro, ni los métodos de poner y obtener pueden superponerse? En tal caso, un candado privado hace la vida mucho más fácil.

Mientras tanto, el locking y las condiciones se basan en el sincronizado. Por lo tanto, puede lograr el mismo objective con eso. Sin embargo, eso puede dificultarle la vida y puede desviarlo de la solución del problema real.

La principal diferencia entre locking y sincronización es que, con lockings, puede liberar y adquirir los lockings en cualquier orden. – con sincronización, puede liberar los lockings solo en el orden en que fue adquirido.

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 entra en el bloque de sincronización, el otro bloque también está bloqueado. Si hay varios bloques de sincronización de este tipo en el mismo objeto, todos los bloques están bloqueados. En tales situaciones, java.util.concurrent.Lock se puede usar para evitar el locking no deseado de bloques