Desmistificando a Certificação SCJP6 - Parte V - Parte 2/3

Nesta parte do artigo trataremos de sincronização de threads e prioridades.

Prioridades

Os threads serão executados com algum nível de prioridade, normalmente representado por números entre um e dez, embora em alguns casos o intervalo seja menor que dez.

O agendador de grande parte das JVMs usa o agendamento preemptivo, com base em prioridades, o que implica em alguma partição do tempo, conhecido em inglês como time slicing. Isso não quer dizer que todas as JVMs utilizem divisão de tempo.

A especificação da JVM não obriga a VM a implementar um agendador com divisão de tempo em que seja alocado para cada thread um período ideal e, em seguida, seja retornado ao estado executável, dando oportunidade para outro thread ser executado.

Não podemos confiar plenamente em prioridades quando projetarmos um sistema com muitos threads. Pois o comportamento das prioridades para diversos threads não é garantido, utilize-as como uma maneira de aprimorar a eficiência de seu programa, mas é preciso certificar-se de que ele não depende desse comportamento para funcionar adequadamente.

Vejamos a seguir um exemplo:

package devmedia; public class X implements Runnable{ public void run(){ for(int i = 0; i < 50; i++) System.out.println(Thread.currentThread().getName()); } public static void main(String args[ ]){ X x = new X(); Thread th1 = new Thread(x); Thread th2 = new Thread(x); Thread th3 = new Thread(x); th1.start(); th2.start(); th3.start(); th1.setPriority(10); th2.setPriority(Thread.MIN_PRIORITY); th3.setPriority(Thread.NORM_PRIORITY); } }

Blocos synchronized

Um dos exemplos mais comuns de multithreading, é o modelo produtor-consumidor. Utilizamos uma estrutura de dados, diga-se de passagem, de modo bem rudimentar, chamada Pilha. O produtor coloca elementos na pilha, ao passo que o Consumidor retira elementos dessa Pilha.

package devmedia; class Pilha { int x; synchronized int get() { System.out.println(Thread.currentThread().getName()); System.out.println(“Retornou: ” + x); return n; } synchronized void put(int x) { System.out.println(Thread.currentThread().getName()); this.x = x; System.out.println(“Colocou: ” + x); } } class Produtor implements Runnable { Pilha Pilha; Produtor(Pilha Pilha) { this.Pilha = Pilha; new Thread(this, “Producer”).start(); } public void run() { int n = 0; while(true) { Pilha.put(n++); } } } class Consumidor implements Runnable { Pilha Pilha; Consumidor(Pilha Pilha) { this.Pilha = Pilha; new Thread(this, “Consumer”).start(); } public void run() { while(true) { Pilha.get(); } } } public class PilhaTeste { public static void main(String args[ ]) { Pilha Pilha = new Pilha(); new Produtor(Pilha); new Consumidor(Pilha); }

Todo objeto em Java tem um bloqueio interno que só surge quando um objeto tem um código de método sincronizado. Quando entramos em um método sincronizado não-estático, de forma automática adquirimos o bloqueio associado à instância atual da classe, cujo código está sendo executando, que é a instância “this”. Essa operação de adquirir um bloqueio para um objeto é conhecida também como obtenção de bloqueio, bloquear no objeto, bloquear o objeto, ou sincronizar no objeto.

Já que só existe um bloqueio por objeto, se um thread utilizar o bloqueio nenhum outro objeto poderá ter acesso ao código sincronizado até que o bloqueio seja liberado.

É preciso sempre lembrar dos seguintes pontos-chave sobre sincronização e bloqueio: só os métodos podem ser synchronized, atributos não podem. Cada objeto tem apenas um bloqueio. Uma classe pode possuir tanto métodos não sincronizados como sincronizados. Se 2 threads estiverem na iminência de executar um método synchronized, e ambos estiverem utilizando a mesma instância para chamar o método, apenas um thread poderá executar o método por vez. A sincronização tem um impacto no desempenho e pode até causar um impasse, conhecido como dead lock. Se um thread entrar em suspensão, ele mantém os seus bloqueios.

É possível sincronizar um bloco de código, ao invés de um método inteiro. Avalie mais uma vez a Pilha: em vez de sincronizarmos os métodos inteiros, sincronizamos os blocos que possuem código que precisa ser protegido.

class Pilha { int x; int get() { synchronized(this) { System.out.println(Thread.currentThread().getName()); System.out.println(“Retornou: ” + x); return n; } } void put(int x) { synchronized(this) { System.out.println(Thread.currentThread().getName()); this.x = x; System.out.println(“Colocou: ” + x); } } }

Sincronização de métodos static

Métodos static também podem ser synchronized. Nessa situação, só existirá uma cópia dos dados static que você tenta proteger, sendo assim será necessário somente um bloqueio por classe para a sincronização de métodos static. Existem 3 formas de sincronização de métodos static.

public synchronized static void fazerAlgumaCoisa1() { } public static void fazerAlgumaCoisa2() { synchronized(Pilha.class) { } } public static void fazerAlgumaCoisa3() { Class reference = null; try { reference = Class.forName(“Pilha”); } catch(ClassNotFoundException e) { e.printStackTrace(); } synchronized(reference) { } }

Pilha.class é denominado literal de classe. Trata-se de um recurso especial da linguagem Java que informa ao compilador: encontre para mim a instância de Class que represente a classe chamada Pilha.

Threads que invocam métodos de instância synchronized da mesma classe só bloquearão uns aos outros se forem chamados utilizando-se a mesma instância. Isso acontece em virtude de cada um deles bloquear na instância “this” e, se forem invocados usando-se duas instâncias diferentes, receberão 2 bloqueios, que não interferem um no outro.

Threads que invocam métodos static synchronized da mesma classe bloquearão sempre uns aos outros. Todos eles bloqueiam a mesma instância de Class. Um método static e outro de instância, sendo ambos synchronized, jamais bloquearão um ao outro.

Na última parte desse artigo trataremos de impasse e aplicaremos um miniteste sobre threads.


Leia todos artigos da série

Artigos relacionados