Preguntas típicas sobre synchronized en Java

¿Cuándo elegir un método synchronized o un bloque synchronized? ¿Es verdad que synchronized hace que nuestro programa sea lento? ¿Los monitores van asociados a las instancias o a las clases? Voy a intentar responder a estas preguntas.

¿Cuándo debo utilizar un método synchronized y cuándo debo usar un bloque synchronized?

Una de las preguntas más típicas es cuándo es preferible ponerle synchronized a un método, o utilizar synchronized como bloque dentro del cuerpo de otro método. Aquí no hay una respuesta definitiva, sino que esto es algo que tendremos que decidir en base a cómo vamos a usar esa región de código que deba ejecutarse de forma sincronizada.

Si todo el método debe ejecutarse de forma sincronizada, y además vamos a utilizar this como monitor, entonces podríamos tener interés en utilizar directamente synchronized a nivel de método. Es una palabra más y afecta a todo el método, sin cambiar mucho el flujo de ejecución del programa.

Si no todo el método debe ejecutarse de forma sincronizada, sino solamente algunas partes del mismo, entonces tal vez sea de nuestra preferencia utilizar un bloque synchronized para poder especificar las líneas de código de la función que deban ejecutarse de forma sincronizada. La alternativa

Además, si vamos a utilizar un objeto diferente como monitor, entonces también debería ser preferible utilizar bloques antes que usar un método, ya que los métodos synchronized siempre usan this como monitor, y esto es algo que no se puede cambiar.

Los monitores y synchronized sólo son útiles cuando hay múltiples hilos

Si tu programa no tiene múltiples hilos, llamar a un método marcado como synchronized no tendrá mucho efecto. No está prohibido y se ejecutará con normalidad, pero dado que sólo hay un hilo y por lo tanto nadie con quien competir, el efecto no se notará. El programa no correrá ni más lento ni más rápido.

No obstante, puede ser útil marcar un método como synchronized incluso en entornos monohilo si posteriormente queremos implementar multihilo, porque en ese caso ya tenemos parte del diseño de la clase hecho.

Los monitores van asociados con la instancia de una clase, no con una clase

Este es importante. El monitor que se usa para bloquear un hilo va asociado con las instancias, no con las clases como tal. Eso quiere decir que si en los ejemplos anteriores tuviese dos instancias de Timbre y cada hilo usase una diferente, no se bloquearían entre sí las llamadas.

Esto es algo que puede tener sentido si tenemos N hilos, M recursos compartidos diferentes, y queremos repartir los hilos en grupo para que a cada subconjunto de los hilos totales le toque un recurso compartido. Imaginalo como que tienes 20 hilos, 4 timbres, y quieres que unos hilos toquen un timbre y otros hilos toquen otro. En ese caso, puede ser útil que tengamos 4 monitores diferentes, para que los bloqueos y las sincronizaciones afecten únicamente al grupo de hilos que también esté asociado con el mismo recurso compartido.

Sin embargo, tienes que tener en cuenta esto porque es importante. Si te encuentras con que los métodos synchronized no están haciendo su trabajo como deben, plantéate antes si realmente el recurso está siendo compartido. Todos los hilos tienen que estar usando la misma instancia del mismo objeto, para que el monitor les afecte por igual.

¿Puede hacer synchronized que mi código vaya lento?

Si lo diseñamos mal, claro que sí. La explicación es muy simple.

Implementamos concurrencia y paralelismo porque queremos aprovechar múltiples CPUs en nuestro sistema para hacer que las tareas se ejecuten en simultáneo. Sin embargo, la ejecución de código sincronizada precisamente busca impedir que esto ocurra, definiendo regiones de código donde la ejecución tiene que ser secuencial, y donde los hilos tienen que esperarse entre sí para ejecutar una región de código una tras otra.

Ambos conceptos son contrarios. Los bloques synchronized de forma efectiva crean un cuello de botella en la ejecución concurrente, que puede pausar todos los hilos simultáneos de una máquina hasta que llegue su turno de ejecutar una región de código.

La cuestión es que esto no es una característica positiva ni negativa de synchronized, sino una mecánica que debemos aceptar cuando lo usamos. Y para evitar que esa mecánica se vuelva en nuestra contra, es importante que los métodos y las regiones de código que tienen ejecución sincronizada sean cortos, para mantener los hilos pausados la menor cantidad de tiempo posible.

Esencialmente: cuanto más larga sea una región de código sincronizada, más probable es que su cuello de botella afecte al rendimiento del programa. Sin embargo, regiones sincronizadas acotadas en los puntos que realmente deberían ejecutarse de forma ordenada no deberían causar un impacto negativo muy grande en el rendimiento del programa en comparación con el impacto positivo que podríamos obtener al ejecutarlo multihilo.

Lista de reproducción
  1. 1
    ¿Qué es la concurrencia?
    8 minutos
  2. 2
    ¿Qué es un hilo?
    5 minutos
  3. 3
    Crear un hilo en Java
    10 minutos
  4. 4
    Interrupción de hilos
    10 minutos
  5. 5
    Cómo usar Thread.join
    14 minutos
  6. 6
    Corrupción de memoria
    8 minutos
  7. 7
    Monitores y synchronized
    10 minutos
  8. 8
    Bloque synchronized
    12 minutos
  9. 9
    Preguntas típicas sobre synchronized en Java
    8 minutos
  10. 10
    Interbloqueos, synchronized y el problema de la cena de los filósofos
    13 minutos
  11. 11
    Introducción a Executor en Java
    11 minutos
  12. 12
    Introducción al uso de ExecutorService en Java
    14 minutos
  13. 13
    Introducción a Thread Pools en Java
    10 minutos
  14. 14
    Cómo crear Thread Pools en Java
    11 minutos
  15. 15
    ¿Cómo funcionan wait() y notify()?
    10 minutos
  16. 16
    Ejemplo de wait() y notify() en Java
    9 minutos
  17. 17
    La palabra clave volatile
    8 minutos
  18. 18
    Cómo usar Future
    8 minutos
  19. 19
    Variables atómicas
    7 minutos