Java 2 - Concorrência em Threads Prof. Ricardo Linden Cocorrência em Threads 1 Condição de Corrida (race conditions) • Em um mundo concorrente, sempre assuma que alguém está acessando seus objetos sem você saber. • Todas as threads do mundo são suas adversárias. • Exemplo. Considere o que pode acontecer quando lendo e escrevendo simultaneamente. Thread 1 Thread 2 f1 = a.field1 a.field1 = 1 f2 = a.field2 a.field2 = 2 Cocorrência em Threads 2 Races • Suponha que e a Thread 1 começa primeiro • Thread 1 lê valores originais Thread 1 Thread 2 f1 = a.field1 a.field1 = 1 f2 = a.field2 a.field2 = 2 Cocorrência em Threads 3 Races • Suponha que a Thread 2 vai primeiro • A Thread 1 lê os novos valores Thread 1 Thread 2 f1 = a.field1 a.field1 = 1 f2 = a.field2 a.field2 = 2 Cocorrência em Threads 4 Races • Thread 2 vai primeiro, mas é interrompida no meio • A Thread 1 vê um valor novo, um antigo. Thread 1 Thread 2 f1 = a.field1 a.field1 = 1 f2 = a.field2 a.field2 = 2 Cocorrência em Threads 5 Operações Atômicas • São operações que não podem ser quebradas em operações menores. • Para todos os efeitos, são tratadas como se fossem uma única operação. • Logo, uma thread não pode ser interrompida no meio de sua execução, já que não existe como quebrá-las. Cocorrência em Threads 6 Operações não atômicas • Leituras e escritas de 32-bit são garatidamente atômicas • Operações de 64-bit podem não sê-lo. • Logo, int i; double d; Thread 1 Thread 2 i = 10; i = 20; d = 10.0; d = 20.0; i conterá 10 ou 20 d pode conter 10, 20 ou lixo. Cocorrência em Threads 7 Acesso Sincronizado a Dados Compartilhados • Considere uma classe Banco que proveja um método de transferência que mude o saldo de duas contas. • Se múltiplas threads Transaction querem ter acesso ao mesmo objeto de classe Banco problemas podem surgir. Cocorrência em Threads 8 Código Fonte class Bank { public static final int NTEST = 10000; private final int[] accounts; private long ntransacts = 0; public Bank(int n, int initialBalance) { accounts = new int[n]; for (int i=0;i<accounts.length;i++) accounts[i] = initialBalance; ntransacts = 0; } public void transfer(int from, int to, int amount) throws InterruptedException { if (accounts[from] < amount) return; accounts[from] -= amount; accounts[to] += amount; ntransacts++; if (ntransacts % NTEST == 0) test(); } Cocorrência em Threads } 9 Operações Atômicas • Lembrando a definição : São operações que para todos os efeitos, são tratadas como se fossem uma única operação. • Logo, uma thread não pode ser interrompida no meio de sua execução, já que não existe como quebrá-las. • No método transfer, as operações marcadas em vermelho, apesar de serem uma única linha de código, não são operações atômicas quando compiladas para byte-code. Cocorrência em Threads 10 Operações não atômicas em Threads separadas • • • • Assuma que existem duas threads diferentes rodando o método transfer(). Ambos farão o seguinte: – Lerão o saldo da conta original e copiarão para um registrador. – Diminuirão o valor a transferir da conta original – Copiarão o valor do registrador para a conta original. – Os mesmos passos se darão com a conta de destino do dinheiro. Duas threads ocorrendo ao mesmo tempo podem se interromper em qualquer ponto. Se uma interromper a outra no meio de um crédito ou de um débito, o saldo final pode ficar corrompido. Cocorrência em Threads 11 Fazendo com que os métodos sejam atômicos • Usamos a palavra chave synchronized para declarar que um método acessa informação crítica e como tal não deve ser interrompido. public synchronized void transfer(int from, int to, int amount) throws InterruptedException if (accounts[from] < amount) return; accounts[from] -= amount; accounts[to] += amount; ntransacts++; if (ntransacts % NTEST == 0) test(); } Cocorrência em Threads { 12 Locks de objetos • Cada objeto Java tem um lock (tranca) que pode ser obtido por uma thread. • Uma thread tem que esperar se tentar obter um lock em um objeto já trancado. • É mais ou menos como uma fila para usar um banheiro que tenha apenas um reservado. • Entretanto, se há mais de um reservado, então podemos ter duas pessoas usando-os simultaneamente. • Logo, se há dois objetos de lock, cada um pode ser usado sem preocupação com o outro. Cocorrência em Threads 13 Fazendo com que os métodos sejam atômicos • Quando uma thread manda uma mensagem sincronizada para um objeto e nenhuma outra thread está executando um método sincronizado naquele objeto, o que acontece? • O Java “tranca” aquele objeto e outros métodos sincronizados não podem acessar aquele objeto até que o primeiro termine. • É como se uma pessoa quisesse ir ao banheiro e não tivesse ninguém no reservado ela entra e tranca a porta. • Agora ninguém mais pode entrar no reservado até que a pessoa destranque a porta. Cocorrência em Threads 14 E se a pessoa passa mal dentro do reservado? • Um problema possível com uma situação destas é que uma pessoa pode ter um ataque cardíaco e morrer dentro do banheiro. • Como é que fazemos para que outras possam também usar o banheiro? • O mesmo acontece com threads: se uma thread que tem um lock de um objeto entrar em loop eterno, temos problemas sérios... Cocorrência em Threads 15 Outro problema : deadlock synchronized(Foo) { synchronized(Bar) { /* Deadlocked */ } } Está com o lock de Foo e espera que outro libere o lock de Bar. synchronized(Bar) { synchronized(Foo) { /* Deadlocked */ } } Está com o lock de Bar e espera que outro libere o lock de Foo. Regra : sempre adquira os locks na mesma ordem... Cocorrência em Threads 16 Blocos sincronizados • A palavra chave synchronized não se restringe a métodos. • Podemos sincronizar blocos de código sobre objetos que são compartilhados. • Assim, só o trecho dentro do bloco é devidamente sincronizado. O resto do método não. • O código ficaria assim: public void run() { : synchronized (objeto) { //Ações a serem tomadas com o objeto } : } Cocorrência em Threads 17 O que queremos dizer é... Object mutexLock = new Object(); : public void someMethod() { SeçãoNãoCrítica; synchronized(mutexLock) { SeçãoCrítica; } SeçãoNãoCrítica2; } Para que eu preciso sincronizar o método inteiro se só aquela seção crítica pode dar problemas? Cocorrência em Threads 18 Prioridades • Toda thread tem uma prioridade variando de 1 a 10. • Existem constantes dentro da classe Thread para coordenar isto – MAX_PRIORITY – MIN_PRIORITY – NORM_PRIORITY • Para definir a prioridade de uma thread, use o método setPriority. • É trabalho do Scheduler manter as threads de prioridade mais altas rodando. Cocorrência em Threads 19 Probleminha... • Nem todo sistema operacional tem 10 níveis distintos de prioridade. • O Windows, por exemplo, tem apenas 7. • Assim, uma thread de prioridade n pode acabar tendo os mesmos privilégios (ou até menos) que outra de prioridade n-1, se ambas as prioridades forem mapeadas para um mesmo valor pela JVM. Cocorrência em Threads 20 Especificação • Extraído de The Java Language Specification Every thread has a priority. When there is competition for processing resources, threads with higher priority are generally executed in preference to threads with lower priority. Such preference is not, however, a guarantee that the highest priority thread will always be running, and thread priorities cannot be used to reliably implement mutual exclusion. • Moral da história : o escalonamento de threads por prioridades é altamente dependente da implementação da JVM e não devem os confiar muito nele... Cocorrência em Threads 21 Nota da tradução... • Repare que a especificação da linguagem não menciona nada quanto a threads de mesma prioridade rodando ao mesmo tempo... • Isto é, o implementador da JVM tem grande liberdade neste tópico. • O Windows implementa timeslices. • Já o Solaris deixa as threads de rodando até que elas morram ou entrem em estado idle (ao pedir um recurso travado ou executando algum comando que as interrompa como wait(), yield(), etc.) Cocorrência em Threads 22 Starvation • Ocorre quando não temos um escalonador justo. • As threads de mais altas prioridades consomem todos os recursos e impedem que threas de menos prioridade executem. • Isto é chamado starvation • Pode acontecer, dependendo do SO e da implementação do Java Cocorrência em Threads 23 Threads Egoístas • Uma thread egoísta é aquela que quando toma conta da máquina, não quer sair nunca mais. • Neste caso, só o SO pode retirá-la da máquina. • Não é boa prática de programação fazer threads egoístas. • Solução: usar os comandos sleep() e yield(). Cocorrência em Threads 24 O método sleep() • O método sleep coloca o processo para dormir por um número de milissegundos definido pelo seu parâmetro. public void run() { for(;;) { try { sleep(1000); // Pause for 1 second } catch (InterruptedException e) { return; // caused by thread.interrupt() } System.out.println(“Tick”); } } Cocorrência em Threads 25 O método sleep() public void run() { for(;;) { try { sleep(1000); } catch (InterruptedException e) { return; } System.out.println(“Tick”); } } Esta thread não necessariamente imprime a palavra “Tick” uma vez por segundo. Afinal, ela entra em estado idle e não necessariamente é escalonada imediatamente após “acordar do seu sono”. O tempo do sleep é um limite inferior para o tempo entre impressões. Cocorrência em Threads 26 Interrompendo uma thread • Quando o método run() termina, a thread se encerra (entra em estado dead e espera que o Garbage Collector a elimine da memória). • Neste caso, está tudo bem. • Entretanto, outros podem pedir que a thread termine. • Para fazê-lo, devemos usar o método interrupt() da thread. Cocorrência em Threads 27 Interrompendo uma thread • Logo, o código da nosso método run() deveria ter o seguinte formato: public void run() { : while((!interrupted())&&(outras condições) { //faça o trabalho } : } • O método interrupted() é estático enquanto que o isInterrupted() pertence ao objeto e não tem efeitos colaterais. • O problema é que esta thread é uma thread egoísta!!!! Cocorrência em Threads 28 Interrompendo o sono de uma Thread • Quando uma thread está em modo sleep e sofre uma interrupção, uma exceção da classe InterruptedException acontece. • Logo, devemos tratá-la!!! • Podemos ignorar o pedido de interrupção, se a thread for importante o suficiente ou podemos preparar-nos para sair. • Nota: a interrupção só ocorre se estávamos dormindo. Caso contrário, temos que checar os flags de interrupção. Cocorrência em Threads 29 Interrompendo o sono de uma Thread • O código para fazer isto é: public void run() { : try { while(condições) { //faça o trabalho : sleep(n) : //mais trabalho } } catch (InterruptedException e) { //A thread foi interrompida durante o sono } finally { //Ações de limpeza e término do loop } : } Cocorrência em Threads 30 Sendo bonzinho... • Uma maneira de uma thread ser gentil, é usar o método yield(). • Usando este método, ela sai do controle da máquina e diz para a JVM escalonar outra thread em seu lugar. • O problema é que se a thread boazinha é a da mais alta prioridade, certas implementações da JVM pode escaloná-la novamente. • É melhor chamar o método sleep(). Assim garantimos que a thread atual não será escalonada imediatamente... Cocorrência em Threads 31 Esperando uma condição • Imagine que você deseja esperar por uma condição antes de continuar. • Se você fizer um loop infinito, você pode fazer o sistema entrar em deadlock. while (!condition) {} • Usar o comando yield evita o deadlock, mas é muito ineficiente. while (!condition) {yield()} Cocorrência em Threads 32 Esperando uma condição : wait e notify • wait() é parecido com yield(), mas necessita que alguém o acorde. : while (!condition) wait(); //Agora a cond. foi satisfeita : • A thread que pode afetar a condição de espera chama o método notify() para acordar uma das threads que foi dormir. • Não necessariamente a thread que acorda é aquela que queremos, • O programador precisa se assegurar que para cada chamada de wait() existe uma chamada correspondente de notify() Cocorrência em Threads 33 Outra solução • O método notifyAll() habilita todas as threads que estão em estado de wait. • Uma delas será escalonada e aproveitará da condição satisfeita. • As outras, quando forem escalonadas, não terão a condição satisfeita e voltarão a dormir... Cocorrência em Threads 34 Produtor Consumidor • O problema do produtor-consumidor é um caso clássico de uma thread que deve esperar por uma condição a ser satisfeita por outras threads. • No nosso caso abaixo, o Homer só pode comer biscoitos depois que a Marge os fizer!!! Cocorrência em Threads 35 Marge & Homer • A Marge só assa biscoitos quando precisa, pois a jarra é de tamanho limitado. • O Homer só os come quando eles existem. • Solução: – Marge faz biscoitos, avisa para o Homer que estão prontos e espera. – Homer come os biscoitos, avisa para a Marge que eles acabaram e espera. Cocorrência em Threads 36 Produtor e Consumidor Consumidor synchronized (lock) { while (!resourceAvailable()) { lock.wait(); } consumeResource(); } Produtor produceResource(); synchronized (lock) { lock.notifyAll(); } Quando o produtor executa, ele tira do estado de wait todos os objetos consumidores que estão esperando sobre o objeto lock Cocorrência em Threads 37 Seqüência de execução Wait/Notify 1. synchronized(lock){ 2. lock.wait(); 9. consumeResource(); 10. } Thread Consumidor Objeto Lock 7. Readquire lock 8. Retorna do wait() Cocorrência em Threads 3. produceResource() 4. synchronized(lock) { 5. lock.notify(); 6.} Thread Produtor 38 A classe JarraBiscoitos public class CookyJar { private int contents; private boolean available = false; public synchronized void getCooky(String who) { while (!available) { try {wait();} catch (InterruptedException e) { } } available = false; notifyAll(); System.out.println( who + " ate cooky " + contents); } public synchronized void putCooky(String who, int value) { while (available) { try {wait();} catch (InterruptedException e) { } } contents = value; available = true; System.out.println(who + " put cooky " + contents + " in the jar"); notifyAll(); } } Cocorrência em Threads 39 A classe Homer public class Homer implements Runnable { CookyJar jar; public Homer(CookyJar jar) { this.jar = jar; } public void eat() { jar.getCooky("Homer"); try { Thread.sleep((int)Math.random() * 1000); } catch (InterruptedException ie) {} } public void run() { for (int i = 1 ; i <= 10 ; i++) eat(); } } Cocorrência em Threads 40 A classe Marge public class Marge implements Runnable { CookyJar jar; public Marge(CookyJar jar) { this.jar = jar; } public void bake(int cookyNumber) { jar.putCooky("Marge", cookyNumber); try { Thread.sleep((int)Math.random() * 500); } catch (InterruptedException ie) {} } public void run() { for (int i = 0 ; i < 10 ; i++) bake(i); } } Cocorrência em Threads 41 O testador das classes public class SimpsonsTest { public static void main(String[] args) { CookyJar jar = new CookyJar(); Homer homer = new Homer(jar); Marge marge = new Marge(jar); new Thread(homer).start(); new Thread(marge).start(); } } Cocorrência em Threads 42 Semáforos • Os sistemas operacionais normalmente oferecem semáforos como oeprações primitivas. • Java não oferece mecanismos de semáforos, mas nós podemos criálos facilmente usando o mecanismo de sincronização. Cocorrência em Threads 43 Semáforos em Java public class Semaphore { private int value; public Semaphore() {value = 0;} public Semaphore(int value) {this.value = value;} public synchronized void acquire() { while (value == 0) try {wait();} catch (InterruptedException ie) { } value--; } public synchronized void release() { ++value; notify(); } } Cocorrência em Threads 44 Moral da história : estados possíveis de uma Thread born start() blocked I/O completed ready yield() waiting wait() running sleep() sleeping terminate dead Cocorrência em Threads 45