Threads em Java
Lucas Catani1, Cedemir1
1
Curso de Ciência da Computação – Universidade de Passo Fundo (UPF)
Caixa Postal 611 – 99.001-970 – Passo Fundo – RS – Brazil
lucascatani@[Link], cedemir@[Link]
Resumo. Este artigo tem por finalidade demonstrar o uso de threads em Java.
Para entender o funcionamento de threads no Java precisamos nos
aprofundar em alguns conceitos como pool de threads, sincronização de
métodos e classes, compartilhamento de objetos e classes, bem como métodos
de implementar.
1. Introdução
Neste artigo será abordado o uso de threads em Java, que permitem aos
computadores com múltiplos processadores a execução paralela de atividades. Com
isso, o desempenho e o desperdício de hardware é minimizado.
O Java disponibiliza a concorrência por meio da linguagem ou das APIs. O
programados especifica que um aplicativo contém threads ou linhas de execução
separadas, nas quais cada uma tem sua própria pilha de chamadas de métodos e seu
próprio contador de programa, permitindo a execução simultânea entre várias threads e
o compartilhamento de recursos no nível do aplicativo com a memória.
2. Java
A linguagem de programação Java tem sido calorosamente recebida pela
comunidade Mundial de desenvolvedores de software e provedores de conteúdo para a
Internet (Arnold, Gosling, Holmes1). O Java permite que o usuário desenvolva o código
apenas uma vez, sem a necessidade de “portar” suas aplicações para cada plataforma de
hardware ou software.
3. Threads
Segundo Daniel da Silva, Carla E. de Castro Franco e Diogo Florenzano
Avelino, quando um programa está em execução, o sistema operacional cria um
processo juntamente com alguns dados adicionais para controlá-lo. No momento da
criação deste processo existe uma única linha de execução (trecho de código) sendo
executado por ele. A thread é esta linha de execução dentro de um processo.
A estrutura das threads permite o controle do fluxo de execução dentro de um
programa e são oferecidas pelo próprio sistema operacional ou podem ser
implementadas utilizando-se bibliotecas de uma determinada linguagem. No caso deste
artigo, será utilizado o conceito de threads em Java.
1
Autores do livro A Linguagem de programação Java
4. Theads em Java
Neste capítulo serão abordados métodos de utilização de threads em Java, bem como
seus conceitos e aplicação prática.
4.1. Formas de Implementar
A maneira mais simples de criar uma thread em Java é criando uma subclasse de
Thread e implementando o que a thread vai fazer sobrecarregando o método run.
Para criar uma thread de controle, devemos iniciar criando um objeto Thread:
Thread worker = new Thread();
Após criar o objeto acima, podemos configurá-lo e depois executá-lo. Configurar uma
thread envolve determinar sua prioridade inicial, nome e assim por diante. Quando uma
thread estiver pronta para ser executada devemos invocar seu método start. Esse método
gera uma nova thread de controle com base nos dados do objeto Thread, e então retorna.
A máquina virtual (JVM2) então invoca o método run da nova thread, tornando-a ativa
(a thread). Você pode invocar start somente uma vez para cada uma das threads, se
invocar duas vezes resultará em IllegalThreadStateException.
Exemplo de criação de threads estendendo a classe Thread:
/** Criação de NovaThread herdando da classe Thread */
public class NovaThread extends Thread {
/** Construtor de NovaThread*/
public NovaThread (String threadName) {
/* construtor de Thread passando o nome da Thread como parâmetro */
super(threadName);
}
/** método run o qual representa o comportamento de NovaThread */
public void run () {
/* Implementação do comportamento da thread */
}
}
Figura 1. Exemplo de criação de thread, estendendo a classe Thread
A outra forma de criarmos threads em Java é implementando a interface
Runnable e o método run definido nessa interface. A classe que implementa a thread
deve declarar um objeto do tipo Thread e depois, para instanciá-lo, deve chamar o
construtor da classe Thread passando como parâmetro a instância da própria classe que
implementa Runnable, mais o nome da thread. Como o objeto do tipo Thread é local a
classe que vai implementar thread, é geralmente privado, há a necessidade de criação de
um método start para permitir a execução do objeto thread local. Isso deve ser feito para
qualquer outro método da classe Thread que dever ser visualizado ou usado fora da
classe que implementa Runnable.
2
Máquina Virtual Java.
Exemplo de thread implementando a interface Runnable
/** Criação de NovaThread através da implementação da Interface Runnable */
public class NovaThread implements Runnable {
/* Objeto local do tipo Thread*/
private Thread thread = null;
/* Construtor da classe */
public NovaThread (String threadName) {
/* Caso o objeto thread local não tiver sido criado ainda */
if (thread == null) {
/* A mesma deve ser instanciada com o nome recebido no construtor
* Detalhe importante é que uma instância da própria classe (this)
* também é passada ao construtor da classe Thread permitindo assim
* a associação do método run com a thread
*/
thread = new Thread(this, threadName);
}
}
/* Método para iniciar o objeto thread local */
public void start() {
[Link]();
}
public void run () {
/* Comportamento da thread */
}
}
Figura 2. Exemplo de criação de thread, usando a interface Runnable
Na hora de escolher qual método usar, o principal fator a ser levado em
consideração é que a linguagem Java não permite herança múltipla, ou seja, quando uma
classe que já for sub-classe de outra precisar implementar thread é obrigatório que isso
seja feito através da implementação da interface Runnable, caso contrário pode-se
utilizar sub-classes da classe Thread.
4.2 Pool de Threads
4.2.1 O que é um Pool de Threads?
Um pool de threads é uma coleção de threads disponíveis para realizar tarefas.
4.2.2 Por que usar Pool de Threads?
Pools de Threads geralmente fornecem uma melhor performance quando se executa um
grande número de tarefas devido a um overhead3 reduzido de chamadas por tarefa.
Outro benefício do uso de pool de threads é a capacidade de limitar recursos
3
Overhead é geralmente considerado qualquer processamento ou armazenamento em excesso, seja de
tempo de computação, de memória, de largura de banda ou qualquer outro recurso que seja requerido
para ser utilizado ou gasto para executar determinada tarefa.
consumidos ao executar uma coleção de tarefas (um programa com milhares de threads
pode sofrer em desempenho).
Com um pool de threads, não é necessário preocupar-se com o ciclo de vida dos threads
(criação, destruição).
4.2.2 Como usar Pool de Threads?
Para usar pools de threads, instancie uma implementação da interface ExecutorService e
entregue tarefas para execução. Da interface ExecutorService, utilizaremos duas
implementações: ThreadPoolExecutor e ScheduledThreadPoolExecutor. Essas duas
implementações permitem que sejam estabelecidos:
O número básico e máximo de pool (número de threads)
O tipo de estrutura de dados para armazenar as tarefas
Como tratar tarefas rejeitadas
Como criar e terminar threads
No exemplo abaixo temos uma tarefa WorkerThread que será executada por uma thread:
import [Link];
import [Link];
import [Link];
import [Link];
public class ThreadPool {
/** Flag de shutdown. */
boolean _shutdown = false;
/** Fila de jobs. */
LinkedList _jobs = new LinkedList();
/** Lista de threads no pool. */
private List _threads = new ArrayList();
/** Lock artificial. Evita que outras threads externas fiquem em wait no
lock. Eu poderia ter
* usado _jobs para isso, mas acho que assim fica mais organizado. */
final Object LOCK = new Object();
public ThreadPool(int size) {
for (int i = 0; i < size; i++) {
this._threads.add(new Worker());
}
}
/**
* Diz pro pool para parar de aceitar jobs e encerrar todas as suas
threads.
*/
public void shutdown() {
//Seta a flag de shutdown e avisa todas as threads dormentes para
acordarem.
//Assim, elas verão a flag true e sairão do loop
synchronized (LOCK) {
_shutdown = true;
_jobs.clear(); //descarta jobs não executados
[Link](); //notifica TODAS as thread em wait
}
//Espera por cada thread morrer
Iterator it = _threads.iterator();
while ([Link]()) {
Thread t = (Thread) [Link]();
try {
[Link]();
} catch (InterruptedException e) {
[Link]();
}
}
}
/**
* Adiciona o job à fila e avisa uma thread (em wait) de que há jobs na
fila. Se não houver
* nenhuma thread em wait, a próxima que terminar o serviço pegará
automaticamente o próximo
* job.
* @param job
*/
public void execute(Runnable job) {
if (_shutdown) {
throw new IllegalStateException("Pool marcado para shutdown");
}
synchronized (LOCK) {
_jobs.addLast(job);
[Link](); //notifica UMA thread em wait
}
}
/**
* Classe worker, a thread que efetivamente executa os jobs no pool.
* @author Ronald Tetsuo Miura - Mirante Informática
*/
class Worker extends Thread {
public Worker() {
setDaemon(true);
start();
}
public void run() {
//repete até que o pool seja marcado para shutdown
while (!_shutdown) {
Runnable job;
synchronized (LOCK) {
//espera até algum job estar disponível na fila
while (_jobs.isEmpty()) {
try {
//se não tem, libera o lock e espera
[Link]();
} catch (InterruptedException e) {
[Link]();
}
}
//pega o primeiro job da fila (note que ainda estou dentro
do bloco synchronized)
job = (Runnable) _jobs.removeFirst();
}
//fora do bloco synchronized, senão trava todas as threads até
o fim do job
//executa job
[Link]();
}
}
}
public static void main(String[] args) throws Exception {
ThreadPool pool = new ThreadPool(5);
for (int i = 0; i < 200; i++) {
[Link](new Runnable() {
public void run() {
[Link](this + " início");
try {
[Link](1000);
} catch (InterruptedException e) {
[Link]();
}
[Link](this + " fim");
}
});
}
//não vai executar os 200 jobs, porque depois de 5 segundos marca o
pool para shutdown.
//os jobs não executados após o shutdown são descartados.
[Link](5000);
//o shutdown() só retorna após todas as threads pararem.
[Link]();
[Link]("fim");
}
}
Figura 3. Exemplo do uso de Pool de Threads
4.3 Sincronização de Métodos e Classes
Quando múltiplas threads compartilham um objeto e ele é modificado por uma ou
várias delas, podem ocorrer resultados indeterminados, a menos que o acesso ao objeto
compartilhado seja gerenciado adequadamente.
4.3.1 O problema a ser resolvido
O problema (de acesso a determinados objetos e classes simultaneamente, causando
inconsistências) pode ser resolvido fornecendo a somente uma thread, por vez, o código
de acesso exclusivo que manipula o objeto compartilhado. Dessa forma, outras threads
que tentarem manipular o objeto nesse mesmo tempo, são mantidas na espera. Quando a
thread (que tem o acesso exclusivo) terminar de manipular o objeto, umas das threads da
lista de espera tem a permissão de prosseguir. Dessa maneira ocorre a sincronização de
threads, através da exclusão mútua.
4.3.2 Implementação da Exclusão Mútua
Uma maneira de realizar a sincronização de threads é utilizar os monitores predefinidos
no Java. Todo o objeto tem um monitor e um bloqueio de monitor. Os bloqueios de
monitor podem ser usados para forçar a exclusão mútua, pois o monitor assegura que o
bloqueio de monitor do seu objeto é mantido por no máximo uma única thread em
qualquer dado momento.
No arquivo em anexo chamado Sincronismo, implementamos a sincronização de
threads.
4.3.3 Compartilhamento de objetos sem sincronização
Compartilhamento de objetos sem sincronização é perigoso e não é recomendado para
ser usado quando existir mais de uma thread.
4.4 Compartilhamento de variáveis e objetos entre Threads
As threads podem compartilhar acesso a objetos e variáveis, esse acesso compartilhado
é ao mesmo tempo uma das características mais úteis de multithreading4 e uma de suas
maiores armadilhas. Esta espécie de sequência (obtém-modifica-escreve) tem aquilo que
é conhecido como race condition, que ocorre quando duas threads podem
potencialmente modificar a mesma entrada de dados, ou a mesma sequência de dados de
uma maneira intercalada, o que pode corromper os dados, ou até mesmo mudar o
sentido do programa. Para contornar esse problema, cada thread deve bloquear o objeto
que estiver usando, para outras threads saberem que não podem usá-lo em determinado
momento.
4.4.1 Compartilhamento Objeto / Thread
4.4.2 Compartilhamento Thread / Objeto
4
Mutithreading é uma forma de um processo dividir a si mesmo em duas ou mais tarefas que podem ser
executadas concorrentemente. O suporte à thread é fornecido pelo próprio sistema operativo (SO), no
caso da linha de execução ao nível do núcleo (em inglês: Kernel-Level Thread (KLT)), ou implementada
através de uma biblioteca de uma determinada linguagem, no caso de uma User-Level Thread (ULT).
Referências
Daniel da Silva, Carla E. de Castro Franco e Diogo Florenzano Avelino (2012) “
Implementação de Sockets e THR envolvimento de sistemas cliente um estudo em
[Link]”
[Link] no dia 04/10/12.
Ken Arnold, James Gosling, David Holmes (2007) “A Linguagem de Programação
Java, 4ª Edição”
José Maria Rodrigues Santos Júnior (2010) “Threads em Java”
Monografia, Universidade Federal da Paraíba, Aracaju
Jacques Phillipe Sauvé (2011) “Threads”
[Link]
Deitel Paul, Deitel Harvey (2010) “Java, como programar 8ª Edição”