En esta entrada dejaré el resultado de un experimento que surgió por una difícil pregunta que me generó varias dudas...
El experimento es revisar el comportamiento de un programa usando un Map en forma concurrente:
Comenzamos creando la clase con el map, luego agregarle algunos datos y el main:el main lanzará 2 hilos (t1 y t2), cada uno tendrá acceso al mismo objeto (c) intentando modificar su estado en distintos métodos (m1 y m2) de la misma referencia.
public class Concurrido {
private final Map<Integer, String> estado = new HashMap<y>();
{
IntStream.rangeClosed(1, 1_000_000)
.forEach(i -> estado.put(i, String.valueOf(1)));
}
public static void main(String[] args) throws InterruptedException {
final var c = new Concurrido();
final var t1 = new Thread(() -> {
c.m1();
System.out.println("m1 terminado");
});
final var t2 = new Thread(() -> {
c.m2();
System.out.println("m2 terminado");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("fin");
}
Prueba 1
la primera prueba es crear m1 y m2 sin ninguna modificación especial, uno quita elementos y otro los muestra:al lanzarlo el resultado es el siguiente:
private void m1() {
System.out.println("trabajando en m1");
for (var j = 0; j < 100_000_000_0L; j++) {
estado.remove(j);
}
}
private void m2() {
System.out.println("trabajando en m2");
estado.forEach((a, b) -> {});
}
No se puede ocupar en forma concurrente como m1 y m2 están definidos
trabajando en m1
trabajando en m2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.base/java.util.HashMap.forEach(HashMap.java:1428)
at com.sebastian.concurrente.Concurrido.m2(Concurrido.java:47)
at com.sebastian.concurrente.Concurrido.lambda$main$2(Concurrido.java:26)
at java.base/java.lang.Thread.run(Thread.java:832)
m1 terminado
fin
Prueba 2
Lo siguiente será usar synchronized solo en un bloque dentro de m1:Algo que no será muy útil pues se está sincronizando solo un bloque dentro del método m1:
private void m1() {
synchronized(estado) {
System.out.println("trabajando en m1");
for (var j = 0; j < 100_000_000_0L; j++) {
estado.remove(j);
}
}
}
private void m2() {
System.out.println("trabajando en m2");
estado.forEach((a, b) -> {});
}
trabajando en m1
trabajando en m2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.base/java.util.HashMap.forEach(HashMap.java:1428)
at com.sebastian.concurrente.Concurrido.m2(Concurrido.java:49)
at com.sebastian.concurrente.Concurrido.lambda$main$2(Concurrido.java:26)
at java.base/java.lang.Thread.run(Thread.java:832)
Prueba 3
Ahora será sincronizar ambos métodos sobre la misma variable de instancia:Genera lo siguiente:
private void m1() {
synchronized(estado) {
System.out.println("trabajando en m1");
for (var j = 0; j < 100_000_000_0L; j++) {
estado.remove(j);
}
}
}
private void m2() {
synchronized(estado) {
System.out.println("trabajando en m2");
estado.forEach((a, b) -> {});
}
}
En este caso como el lock se obtiene sobre el estado en m1, m2 no podrá obtenerlo hasta que m1 termine. funciona sin generar error.
trabajando en m1
m1 terminado
trabajando en m2
m2 terminado
fin
Prueba 4
lo siguiente será usar synchronized en m1:y genera lo siguiente:
private synchronized void m1() {
System.out.println("trabajando en m1");
for (var j = 0; j < 100_000_000_0L; j++) {
estado.remove(j);
}
}
private void m2() {
System.out.println("trabajando en m2");
estado.forEach((a, b) -> {});
}
en este caso synchronized en el método obtiene el lock sobre la instancia, pero m2 no ocupa lock asi que explota igual
trabajando en m1
trabajando en m2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.base/java.util.HashMap.forEach(HashMap.java:1428)
at com.sebastian.concurrente.Concurrido.m2(Concurrido.java:48)
at com.sebastian.concurrente.Concurrido.lambda$main$2(Concurrido.java:27)
at java.base/java.lang.Thread.run(Thread.java:832)
m1 terminado
fin
Prueba 5
Ahora dejaré ambos métodos con synchronized, así ambos tienen que competir por el lock sobre la instancia:y el resultado es el siguiente:
private synchronized void m1() {
System.out.println("trabajando en m1");
for (var j = 0; j < 100_000_000_0L; j++) {
estado.remove(j);
}
}
private synchronized void m2() {
System.out.println("trabajando en m2");
estado.forEach((a, b) -> {});
}
funciona, como el lock es sobre la instancia es equivalente a:
trabajando en m1
m1 terminado
trabajando en m2
m2 terminado
fin
y NO equivale a:
private synchronized void m1() {
System.out.println("trabajando en m1");
for (var j = 0; j < 100_000_000_0L; j++) {
estado.remove(j);
}
}
private void m2() {
synchronized(this) {
System.out.println("trabajando en m2");
estado.forEach((a, b) -> {});
}
}
m1 está obteniendo el lock sobre la instancia y m2 sobre una variable de instancia, el resultado es:
private synchronized void m1() {
System.out.println("trabajando en m1");
for (var j = 0; j < 100_000_000_0L; j++) {
estado.remove(j);
}
}
private void m2() {
synchronized(estado) {
System.out.println("trabajando en m2");
estado.forEach((a, b) -> {});
}
}
trabajando en m1
trabajando en m2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.base/java.util.HashMap.forEach(HashMap.java:1428)
at com.sebastian.concurrente.Concurrido.m2(Concurrido.java:49)
at com.sebastian.concurrente.Concurrido.lambda$main$2(Concurrido.java:27)
at java.base/java.lang.Thread.run(Thread.java:832)
m1 terminado
fin
Prueba 6
En esta prueba el map será un ConcurrentHashMap:y los métodos serán sin modificaciones:
private final Map<Integer, String> estado = new ConcurrentHashMap<>();
El resultado es:
private void m1() {
System.out.println("trabajando en m1");
for (var j = 0; j < 100_000_000_0L; j++) {
estado.remove(j);
}
}
private void m2() {
System.out.println("trabajando en m2");
estado.forEach((a, b) -> {});
}
En este caso ni t1 ni t2 esperan, ambos hilos pasan a m1 y m2 respectivamente y funciona.
trabajando en m2
trabajando en m1
m2 terminado
m1 terminado
fin
Así finalizan las pruebas 😀
Oye, pero qué interesante... :b
ResponderBorrarMe sirvió para aclarar varias cosas