domingo, 20 de abril de 2008

¿Cómo funciona synchronized en Java?

Mucha gente que usa Java no tiene claro cómo funciona realmente la palabra clave synchronized. Voy a intentar explicarlo acá porque es realmente muy fácil si se tienen en cuenta algunas cosas.

En primer lugar, usar synchronized en un método de instancia es lo mismo que poner un bloque de synchronized(this){} que contenga todo el código del método. Es decir,
public synchronized void metodo() {
   // codigo del metodo aca
}
es lo mismo que
public void metodo() {
   synchronized(this) {
      // codigo del metodo aca
   }
}
Si el método es de clase entonces es lo mismo pero el bloque de synchronized se aplica a la clase. Ejemplo, si el método está en la clase MiClase entonces esto
public static synchronized void metodo() {
   // codigo del metodo aca
}
es lo mismo que
public static void metodo() {
   synchronized(MiClase.class) {
      // codigo del metodo aca
   }
}


Habiendo dicho esto, el problema se reduce a entender cómo funciona el synchronized de un bloque porque, como vimos, el synchronized en métodos de instancia o de clase son simplemente casos especiales de esto.

El bloque synchronized lleva entre paréntesis la referencia a un objeto. Cada vez que un thread intenta acceder a un bloque sincronizado le pregunta a ese objeto si no hay algún otro thread que ya tenga el lock para ese objecto. En otras palabras, le pregunta si no hay otro thread ejecutando algun bloque sincronizado con ese objeto (y recalco que es ese objeto porque en eso radica la clave para entender el funcionamiento)
Si el lock está tomado por otro thread, entonces el thread actual es suspendido y puesto en espera hasta que el lock se libere. Si el lock está libre, entonces el thread actual toma el lock del objeto y entra a ejecutar el bloque. Al tomar el lock, cuando venga el proximo thread a intentar ejecutar un bloque sincronizado con ese objeto, será puesto en espera.
¿Cuándo se libera el lock? Se libera cuando el thread que lo tiene tomado sale del bloque por cualquier razón: termina la ejecución del bloque normalmente, ejecuta un return o lanza una excepción.

Es importante notar una vez más que el lock es sobre un objeto en particular. Si hay dos bloques synchronized que hacen referencia a distintos objetos (por más que ambos utilicen el mismo nombre de variable), la ejecución de estos bloques no será mutuamente excluyente.

Conclusiones:

De todo esto se puede concluir que un método synchronized de instancia puede ser ejecutado al mismo tiempo que uno de clase.
Además, dos threads pueden ejecutar el mismo método de instancia al mismo tiempo aunque este sea synchronized si la invocación se hace a dos objetos diferentes.
Por último, todos los métodos synchronized de instancia son mutuamente excluyentes entre sí. Es decir que, dado un objeto compartido por más de un thread, sólo uno de ellos puede acceder en un determinado momento a uno de esos métodos de instancia. Los otros threads deberán esperar aunque hayan querido acceder a otro de los métodos (porque todos están synchronizados con el mismo this).

Puede encontrarse mucho más info del tema en este tutorial de sun (en inglés).

36 comentarios:

Jorge Rodriguez dijo...

buena la explicacion , son unos detalles importantes que a uno se le escapa cuando uno aprende de estos temas

fekke dijo...

Excelenete!! Nunca lo ví explicado tan claro y en tan pocas palabras!

Te digo que me has ahorrado muchos dolores de cabeza.

Gracias

Luis Miguel Ballestas dijo...

Gracias... tenia la idea pero ahora todo parece mas claro de repente

LILIANITA dijo...

es sorprendente como me aclaro las cosas, Gracias

jgui dijo...

muchas gracias, como ya ha dicho fekke, claro y conciso :)

L3N@ dijo...

genial!! me sacaste las dudas con las q cargaba desde q empezamos con esto en clase.
gracias..

Su dijo...

y entonces para qué sirven wait() y notify()??

Agustin dijo...

hola quisiera hacerte una correccion de sintaxis.

public void synchronized metodo() {
// codigo del metodo aca
}

La palabra synchronized debe estar antes del void. Lo dejo corregido.

public synchronized void metodo() {
// codigo del metodo aca
}

Edu dijo...

Gracias, Agustín!

Ahí corregí el post.

Un cambio por Diseño dijo...
Este blog ha sido eliminado por un administrador de blog.
j@g dijo...

Que pasa con algo asi?

public class MiClase{
private String variable;

public void test(){
sychronized(this.variable){
//codigo
}
}
}

Edu dijo...

j@g: No se bien a qué te referís...

Así como está, si se ejecutara el método test daría NullPointerException porque no se puede hacer synchronized(null).

Si hicieras que variable apuntara a un objeto String, entonces sería ese String el que se usaría como monitor.

j@g dijo...

hmm lo escribi mal, seria mas como esto, pero obtengo un warning que no me deja correr la aplicacion: synchronized on non static field

public class MiClase{
private String variable="hmm";

public void test(){
sychronized(this.variable){
//codigo
}
}
}

Edu dijo...

j@g, así cómo está, el único error es que synchronized está mal escrito.

Lo único que se me ocurre es que tu método test en realidad sea static. Si querés pegá tu código real a ver qué problema tiene, pero si tanto variable como el método test son estáticos, entonces lo que sobra es el this.

j@g dijo...

Edu muchas gracias por tu tiempo, aquí te mando las dos clases que estoy usando, en clase dijeron que puedo usar el this.nombreHilo en el synchronized pero no puedo correrlo en Netbeans 6.8. Gracias de nuevo

public class PrintMe implements Runnable {
private String nombreHilo;
private static int numTrue, numFalse;

public void run() {
try {
for (int i = 0; i < 10; i++) {
synchronized(this.nombreHilo){
this.nombreHilo = Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName().equals(this.nombreHilo));

if(Thread.currentThread().getName().equals(this.nombreHilo)){
numTrue++;
}else
numFalse++;
}
Thread.sleep(100);
}//fin for

System.out.println("True " + numTrue + "\nFalse " + numFalse);
} catch (InterruptedException i) {
}
}//fin run

public static void display(){

System.out.println("True " + numTrue + "\nFalse " + numFalse);
}//hacer que se ejecute al final de todos los hilos
}//fin PrintMe

y la clase Test

public class TestThreads {
public static void main(String[] args) {

Runnable prog = new PrintMe();

for (int i = 0; i < 100; i++) {
Thread T = new Thread(prog," " + i);
T.start();
}

}
}

Edu dijo...

j@g, no te anda porque al momento de hacer el synchronized la variable está en null.
Probá mover esta línea al comienzo del método run():
this.nombreHilo = Thread.currentThread().getName();

j@g dijo...

Muy bien!! Gracias por tu pronta respuesta. Lo que hice fue poner private String nombreHilo=" "; para que la asignación del nombre del hilo quede dentro de synchronized.

Una ultima pregunta, al entrar al synchronized uno de los thread pregunta por el lock de nombreHilo y si lo obtiene ningún otro thread puede acceder al código mientras el otro esté caminando por ahí, pero obtengo por lo menos 2 o mas false, diciendome que algún thread se metió por ahí y su nombre no coincide con el thread que se usó para asignar el nombre a nombreHilo, cómo se explica esto?

Espero que se claro el punto que estoy haciendo. Gracias, has sido muy amable.

Edu dijo...

Si lo que vos querés es que solamente un thread de todos los que hiciste entre a ese synchronized a la vez, entonces el objeto que se pasa al synchronized tiene que ser compartido.
Si pasás el nombre del current thread no vas a lograr tu objetivo porque cada thread tiene su propio nombre.
Te recomiendo, en este caso, usar synchronized(this).

Otra cosa. Vos decís "algún thread se metió por ahí y su nombre no coincide con el thread que se usó para asignar el nombre a nombreHilo". Eso es lo esperado por lo que te dije más arriba.

Txumari dijo...

Public class MiClase{

public static void metodo() {
synchronized(MiClase.class) {
// codigo del metodo aca
}
}

}

Segun he entendido no puedo acceder a MiClase.metodo() desde dos threads distintos?

Edu dijo...

Tuxmari: Correcto. Todos los threads pueden acceder, pero de a uno por vez.

Txumari dijo...

Muchar gracias por contestar tan rápido. Me sirve de gran ayuda, ya que revisando mis implementaciones Singleton, he observado que esta sentencia me da mucha mayor estabilidad y seguridad.

Jeison Nisperuza dijo...
Este comentario ha sido eliminado por el autor.
corey dijo...

buena explicacion ! gracias

Txumari dijo...

Si yo tengo esto:

public static synchronized void metodo() {
// codigo del metodo aca
}

perteneciente a la clase Miclase,¿A que objeto le pregunta para saber que esta bloqueado por un thread?

Edu dijo...

Txumari, se lo pregunta al objeto Miclase.class.

Es lo mismo que
public static void metodo() {
synchronized(Miclase.class) {
// codigo del metodo aca
}
}

sanchez gabriel dijo...

hola amigo una consulta que pasa si existen ademas metodos no synchronized
y el lock esta tomado por un tread,otro tread de ese objeto puede ejecutar ese metodo ??.

Edu dijo...

Hola, Gabriel,
Si un método NO es synchronized puede ser ejecutado por cualquier thread sin importar quién tenga los locks porque, al no ser synchronized, los locks no se tienen en cuenta para ese método.
Edu

Rocha dijo...

hay un error cuando te refieres a metodo de clase

no es MiClase.class, si no Miclase.this

Ya que el primero arroja un error.
" Exception in thread "Consumer" java.lang.IllegalMonitorStateException
at java.lang.Object.notifyAll(Native Method)"

Edu dijo...

Hola, Rocha,

No se bien en qué consiste tu código, pero si MiClase.class y MiClase.this son dos cosas bien diferentes.

El primero se refiere a la instancia de la clase en sí misma y es lo que es bloqueado cuando el método es estático.

Lo segundo suele usarse en clases inner cuando quieren acceder a miembros de la clase que los contiene. Cuando hacés synchronized(MiClase.this) estás sincronizando sobre la instancia de la clase contenedora que contiene a la instancia de la clase inner que ejecuta el synchronized(MiClase.this).

Si no estás usando clases inner, entonces me parece que MiClase.this es exactamente lo mismo que this.

Si querés pasá tu código y lo vemos. Por la excepción que enviás, pareciera que estás intentando usar alguno de los métodos wait(), notify() y notifyAll() de la clase Object (que son métodos de instancia y por eso es que tenés que synchronizar sobre el objeto y no sobre la clase).

Rocha dijo...

public class implementaBuffer implements interfazBuffer{

int bufer= -1;
int cuentaBuferOcupado=0;


public int obtener() {

// funciona con synchronized(this){
// funciona con synchronized(implementaBuffer.this){
synchronized(implementaBuffer.class){// asi sale un error
while(cuentaBuferOcupado==0){
try{
System.err.println(Thread.currentThread().getName()+"intenta Leer");
System.err.println(Thread.currentThread().getName()+"Espera");
wait();
}catch(InterruptedException e){
e.printStackTrace();

}

}// fin del while

--cuentaBuferOcupado;
notifyAll();

System.err.println(Thread.currentThread().getName()+"Lee\t"+bufer);

return bufer;
}
}

public synchronized void establecer( int valor) {



while(cuentaBuferOcupado==1){

try{
System.err.println(Thread.currentThread().getName()+"intenta Escribir");
System.err.println(Thread.currentThread().getName()+"Espera");
wait();

}catch(InterruptedException e){
e.printStackTrace();
}// fin del try catch

}// fin del while

System.err.println(Thread.currentThread().getName()+"Escribe\t"+valor);
bufer=valor;
++cuentaBuferOcupado;
notifyAll();
}


}

Edu este es el codigo, tines razon estoy utilizando metodos wait(), notifyAll(), ya que estoy trabajando con hilos, intente con todas las formas que describes pero la ultima me arrojo el error que mencione anteriormente. de antemano mil gracias por tu atenta ayuda.

Rocha dijo...

Hola Edu imagino que andas bien ocupado, en cuento puedas y los que puedan al leer quisiera me aclaran algo:

Para que son las Clases internas anónimas, que ventajas tienen ante las clases normales?

Ej clase anonima:
new ActionListener(){
public void actionPerformed(ActionEcvent e){

... codigo...
}// cierre metodo interno
}// cierre clase anonima
Ej: clase normal

public class ManipulaEvents implements ActionListener{

public void ActionPerformed(ActionEvent e){

}// fin metodo de clase

}// fin clase normal

Mi pregunta por lo siguiente, la clase anónima solo se puede utilizar una sola vez, para este evento que manipula eventos, pero las clases normales se pueden instanciar y pueden ser mas útiles, pueden manejar miles de eventos, en cambio una clase interna solo se escribe para algo especifico, que ventaja tiene?

javin paul dijo...

your post seems good though I can not understand but for those who understand English I would like to share my experience with synchronization keyword by my blogpsot How Synchronization works in Java

Laura dijo...

Hola, estoy empezando a estudiar java y tengo una duda si esto suele aplicarse en una tienda virtual en la que se lista articulos de una determinada categoria mostrando si el producto esta agotado o disponible.

Edu dijo...

Laura,
La verdad es que la sincronización de threads está a mucho más bajo nivel que lo que puede ser una tienda virtual.

Lo más probable es que uses algún framework que te abstraiga del manejo de threads, pero seguramente ese framework use los conceptos que describo en este post.

Si tenés algún ejemplo concreto quizás te pueda ayudar un poco más.

Saludos!

Daniel dijo...

Excelente explicación. Dejaste muy en claro un tema muuuuy complejo y delicado. Gracias por compartir tu conocimiento.

María dijo...

Hola Edu,

En primer lugar muchas gracias, me ha sido muy útil.

Me preguntaba si sabrías sobre Android. En concreto, cómo se guarda una imagen de una vista Surfaceview.

Gracias,

Mary