Aálise de um Problema de Concorrência
Vamos analisar o seguinte problema de programação concorrente:
Implementar um contêiner que forneça dois métodos: adicionar e tamanho. Criar duas threads: a primeira adiciona 10 elementos ao contêiner, e a segunda monitora o número de elementos, terminando quando o contador atingir 5.
O código inicial:
package exemplo.concorrencia;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ContenedorBasico {
private List<object> elementos = new ArrayList<>();
public void adicionar(Object elemento) {
elementos.add(elemento);
}
public int tamanho() {
return elementos.size();
}
public static void main(String[] args) {
ContenedorBasico cont = new ContenedorBasico();
new Thread(() -> {
for(int i=0; i<10; i++) {
cont.adicionar(new Object());
System.out.println("Adicionado " + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread1").start();
new Thread(() -> {
while(true) {
if(cont.tamanho() == 5) {
break;
}
}
System.out.println("Thread2 finalizada");
}, "Thread2").start();
}
}
</object>
Análise do Problema
Esta implementação não funciona corretamente devido a problemas de visibilidade entre threads e falta de sincronização.
Adicionando Volatile
Podemos tentar adicionar o modificador volatile para garantir a visibilidade:
public class ContenedorComVolatile {
private volatile List<object> elementos = new ArrayList<>();
public void adicionar(Object elemento) {
elementos.add(elemento);
}
public int tamanho() {
return elementos.size();
}
// Restante do código...
}
</object>
No entanto, essa abordagem apresenta dois problemas principais:
- Não é precisa, pois a verificação do tamanho pode ocorrer após outra thread ter adicionado mais elementos
- Consome excessivamente CPU devido ao loop contínuo
Além disso, o volatile não funciona como esperado quando modificamos o conteúdo do objeto referenciado, apenas a referência em si.
Usando Coleções Sincronizadas
Para resolver o problema de thread-safety, podemos usar uma coleção sincronizada:
private volatile List<object> elementos = Collections.synchronizedList(new ArrayList<>());
</object>
Limitações do Volatile
Como mencionado, o volatile não resolve completamente o problema, pois não detecta mudanças no estado do objeto referenciado:
package exemplo.concorrencia;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ExemploVolatile {
volatile List<object> elementos = Collections.synchronizedList(new ArrayList<>());
public void adicionar(Object o) {
elementos.add(o);
}
public int tamanho(){
return elementos.size();
}
public static void main(String[] args) {
ExemploVolatile c = new ExemploVolatile();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
c.adicionar(new Object());
System.out.println("Adicionado " + i);
}
}, "Thread1").start();
new Thread(() -> {
while (true){
if(c.tamanho() == 5){
break;
}
}
System.out.println("Thread2 finalizada");
}, "Thread2").start();
}
}
</object>
Usando wait() e notify()
Para evitar o loop contínuo que consome CPU, podemos usar os métodos wait() e notify():
package exemplo.concorrencia;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ContenedorComWaitNotify {
private List<object> elementos = new ArrayList();
private final Object lock = new Object();
public void adicionar(Object elemento) {
elementos.add(elemento);
}
public int tamanho() {
return elementos.size();
}
public static void main(String[] args) {
ContenedorComWaitNotify c = new ContenedorComWaitNotify();
new Thread(() -> {
synchronized(c.lock) {
System.out.println("Thread2 iniciada");
if(c.tamanho() != 5) {
try {
c.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread2 finalizada");
}
}, "Thread2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("Thread1 iniciada");
synchronized(c.lock) {
for(int i=0; i<10; i++) {
c.adicionar(new Object());
System.out.println("Adicionado " + i);
if(c.tamanho() == 5) {
c.lock.notify();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Thread1").start();
}
}
</object>
Explicação de wait() e notify()
wait()enotify()devem ser chamados em um objeto bloqueadowait()suspende a thread e libera o locknotify()acorda uma thread esperando no mesmo objetonotifyAll()acorda todas as threads esperando
Problema com a Impleemntação
Na implementação acima, mesmo quando o tamanho atinge 5, a Thread2 só é finalizada após a Thread1 completar sua execução. Isso ocorre porque notify() não libera o lock imediatamente.
Melhorando a Implementação com wait() e notify()
Para corrigir o problema, precisamos que ambas as threads notifiquem uma após a outra:
package exemplo.concorrencia;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ContenedorMelhorado {
private List<object> elementos = new ArrayList();
private final Object lock = new Object();
public void adicionar(Object elemento) {
elementos.add(elemento);
}
public int tamanho() {
return elementos.size();
}
public static void main(String[] args) {
ContenedorMelhorado c = new ContenedorMelhorado();
new Thread(() -> {
synchronized(c.lock) {
System.out.println("Thread2 iniciada");
if(c.tamanho() != 5) {
try {
c.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread2 finalizada");
c.lock.notify();
}
}, "Thread2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("Thread1 iniciada");
synchronized(c.lock) {
for(int i=0; i<10; i++) {
c.adicionar(new Object());
System.out.println("Adicionado " + i);
if(c.tamanho() == 5) {
c.lock.notify();
try {
c.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Thread1").start();
}
}
</object>
Simplificando com CountDownLatch
A comunicação entre threads pode ser simplificada usando CountDownLatch:
package exemplo.concorrencia;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ContenedorComLatch {
private List<object> elementos = new ArrayList();
public void adicionar(Object elemento) {
elementos.add(elemento);
}
public int tamanho() {
return elementos.size();
}
public static void main(String[] args) {
ContenedorComLatch c = new ContenedorComLatch();
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("Thread2 iniciada");
if (c.tamanho() != 5) {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread2 finalizada");
}, "Thread2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("Thread1 iniciada");
for (int i = 0; i < 10; i++) {
c.adicionar(new Object());
System.out.println("Adicionado " + i);
if (c.tamanho() == 5) {
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread1").start();
}
}
</object>
Melhorando a Implementação com CountDownLatch
Para garantir que a verificação seja precisa quando o tamanho atinge exatamente 5, precisamos de dois latches:
package exemplo.concorrencia;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ContenedorComLatchDuplo {
List<object> elementos = Collections.synchronizedList(new ArrayList<>());
public void adicionar(Object o) {
elementos.add(o);
}
public int tamanho() {
return elementos.size();
}
public static void main(String[] args) {
ContenedorComLatchDuplo c = new ContenedorComLatchDuplo();
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
new Thread(() -> {
System.out.println("Thread2 iniciada");
if (c.tamanho() != 5) {
try {
latch1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread2 finalizada");
latch2.countDown();
}, "Thread2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("Thread1 iniciada");
for (int i = 0; i < 10; i++) {
c.adicionar(new Object());
System.out.println("Adicionado " + i);
if (c.tamanho() == 5) {
latch1.countDown();
try {
latch2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Thread1").start();
}
}
</object>
Usando LockSupport
Outra alternativa simplificada é usar LockSupport:
package exemplo.concorrencia;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class ContenedorComLockSupport {
List<object> elementos = Collections.synchronizedList(new ArrayList<>());
public void adicionar(Object o) {
elementos.add(o);
}
public int tamanho() {
return elementos.size();
}
static Thread thread1 = null, thread2 = null;
public static void main(String[] args) {
ContenedorComLockSupport c = new ContenedorComLockSupport();
thread1 = new Thread(() -> {
System.out.println("Thread1 iniciada");
for (int i = 0; i < 10; i++) {
c.adicionar(new Object());
System.out.println("Adicionado " + i);
if (c.tamanho() == 5) {
LockSupport.unpark(thread2);
LockSupport.park();
}
}
}, "Thread1");
thread2 = new Thread(() -> {
System.out.println("Thread2 iniciada");
LockSupport.park();
System.out.println("Thread2 finalizada");
LockSupport.unpark(thread1);
}, "Thread2");
thread2.start();
thread1.start();
}
}
</object>
Usando Semaphore
Para controle de threads, podemos usar Semaphore:
package exemplo.concorrencia;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class ContenedorComSemaphore {
List<object> elementos = Collections.synchronizedList(new ArrayList<>());
public void adicionar(Object o) {
elementos.add(o);
}
public int tamanho() {
return elementos.size();
}
public static void main(String[] args) {
ContenedorComSemaphore c = new ContenedorComSemaphore();
Semaphore semaforo = new Semaphore(1);
Thread thread2 = new Thread(() -> {
System.out.println("Thread2 iniciada");
try {
semaforo.acquire();
System.out.println("Thread2 finalizada");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaforo.release();
}
}, "Thread2");
new Thread(() -> {
System.out.println("Thread1 iniciada");
try{
semaforo.acquire();
thread2.start();
for (int i = 0; i < 10; i++) {
c.adicionar(new Object());
System.out.println("Adicionado " + i);
if (c.tamanho() == 5) {
semaforo.release();
thread2.join();
semaforo.acquire();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
semaforo.release();
}
}, "Thread1").start();
}
}
</object>
Exemplo de Uso de Semaphore
Um exemplo mais simples de Semaphore:
package exemplo.concorrencia;
import java.util.concurrent.Semaphore;
/**
* Exemplo de uso de Semaphore para controle de acesso a recursos
*/
public class ExemploSemaphore {
public static void main(String[] args) {
// Criar semáforo com 1 permissão (fair = true)
Semaphore semaforo = new Semaphore(1, true);
// Primeira thread
new Thread(() -> {
try {
semaforo.acquire();
System.out.println("Thread1 iniciada...");
Thread.sleep(2000);
System.out.println("Thread1 finalizada...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaforo.release();
}
}).start();
// Segunda thread
new Thread(() -> {
try {
semaforo.acquire();
System.out.println("Thread2 iniciada...");
Thread.sleep(2000);
System.out.println("Thread2 finalizada...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaforo.release();
}
}).start();
}
}
Problema Clássico: Impressão Sequencial A1B2C3...
Um problema clássico de programação concorrente é imprimir caracteres e números de forma alternada usando duas threads:
package exemplo.concorrencia;
import java.util.concurrent.CountDownLatch;
/**
* Exemplo de impressão sequencial A1B2C3... usando duas threads
*/
public class ImpressaoSequencial {
public static void main(String[] args) {
String[] caracteres = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"};
String[] numeros = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
final Object lock = new Object();
StringBuilder resultado = new StringBuilder();
CountDownLatch latch = new CountDownLatch(2);
Thread threadCaracteres = new Thread(() -> {
for (int i = 0; i < caracteres.length; i++) {
synchronized (lock) {
resultado.append(caracteres[i]);
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
}
}
latch.countDown();
});
Thread threadNumeros = new Thread(() -> {
for (int j = 0; j < numeros.length; j++) {
synchronized (lock) {
resultado.append(numeros[j]);
lock.notify();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
latch.countDown();
});
threadCaracteres.start();
threadNumeros.start();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Resultado: " + resultado.toString());
}
}