O uso de Threads em Java e suas peculiaridades

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (6)  (0)

Veja neste artigo como utilizar os métodos específicos da classe Thread em Java e como realizar processamentos paralelos em sua aplicação.

Um assunto muito comum em Java é o uso de Threads, e ao mesmo tempo que este assunto é comum, é também muito confuso. Isso porque abrange diversos conceitos, muitos deles voltados a Sistemas Operacionais.

Queremos neste tutorial, tentar ser o mais didático possível, mostrando de maneira prática como funciona as thread e seus recursos, além disso é importante saber que nosso objetivo não é explorar todo conteúdo e conceito sobre thread, mas tentar ser o mais prático possível afim de levar ao leitor todo entendimento prático do assunto.

Vamos iniciar este artigo com um exemplo bem simples, inicializar 2 thread em paralelo e com um contador para cada uma, assim poderemos ver sua ordem de execução pelo processador.

Listagem 1: Inicializando 2 threads

import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

	/**
	 * @param args
	 * @throws InvocationTargetException 
	 * @throws InterruptedException 
	 */
	public static void main(String[] args) throws InterruptedException, InvocationTargetException {
		Thread t1 = new Thread() {
			@Override
			public void run() {
				for(int i = 0; i < 10000; i++)
				System.out.println(i+": t1");
			}

		};

		Thread t2 = new Thread() {
			@Override
			public void run() {
				for(int i = 0; i < 10000; i++)
					System.out.println(i+": t2");
			}

		};

		t1.start();
		t2.start();
		
	
	}

}

A saída acima será algo parecido com:

0: t1
0: t2
1: t1
1: t2
2: t1
2: t2
3: t1
3: t2
4: t1
4: t2
5: t1
5: t2
6: t2
7: t2
8: t2 …

Quando dizemos “parecido” é porque a saída vai depender de como o escalonador do processador está trabalhando naquele determinado momento, ou seja, não temos como saber ao certo qual será a ordem de execução dessas duas Threads.

Dado o código acima, após a execução da linha “t2.start();” quantas Threads nós temos em execução neste exato momento ? A resposta mais óbvia seria 2 threads, mas na verdade são 3, vamos exemplificar o porque disto.

Há um método na classe Thread chamado “activeCount()”, que retorna o número de Threads ativas no momento. Então vamos utilizá-lo para comprovar o que falamos anteriormente, temos 3 threads ativas e não 2. Vamos começar contando as threads antes de executar a linha “t1.start()”, ou seja, antes de iniciar uma nova thread.

Listagem 2: Contando as threads antes de iniciar t1 e t2

import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

	/**
	 * @param args
	 * @throws InvocationTargetException 
	 * @throws InterruptedException 
	 */
	public static void main(String[] args) throws InterruptedException, InvocationTargetException {
		Thread t1 = new Thread() {
			@Override
			public void run() {
				for(int i = 0; i < 10000; i++)
				System.out.println(i+": t1");
			}

		};

		Thread t2 = new Thread() {
			@Override
			public void run() {
				for(int i = 0; i < 10000; i++)
					System.out.println(i+": t2");
			}

		};

		System.out.println("THREADS ATIVAS = "+Thread.activeCount());
		t1.start();
		t2.start();
	
	}

}

A saída do código acima será: THREADS ATIVAS = 1. A única Thread ativa no momento será a Thread “main”, ou seja, a responsável pela execução do nosso programa principal.

Fique atento a esses conceitos: Thread é um “pedaço” de um processo, ou seja, quando executamos nosso programa acima criamos um processo no Sistema Operacional, e esse nosso processo é composto por 1 Thread (que é a nossa Thread main), mas poderíamos criar quantas Threads forem necessárias em 1 único processo, então imagine um Processo como sendo um pacote de biscoito, e cada biscoito dentro do pacote é uma Thread.

Agora vamos ver quantas Threads teremos após as linhas “t1.start()” e “t2.start()”.

Listagem 3: Quantidade de Threads após inicialização das threads t1 e t2

import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

	/**
	 * @param args
	 * @throws InvocationTargetException 
	 * @throws InterruptedException 
	 */
	public static void main(String[] args) throws InterruptedException, InvocationTargetException {
		Thread t1 = new Thread() {
			@Override
			public void run() {
				for(int i = 0; i < 10000; i++)
				System.out.println(i+": t1");
			}

		};

		Thread t2 = new Thread() {
			@Override
			public void run() {
				for(int i = 0; i < 10000; i++)
					System.out.println(i+": t2");
			}

		};

		
		t1.start();
		t2.start();
		System.out.println("THREADS ATIVAS = "+Thread.activeCount());
	
	}

}

O resultado será: 3, pois teremos agora as threads: main, t1 e t2. Vamos agora ver alguns outros exemplos com um pouco mais de complexidade.

Listagem 4: Testando se thread está “viva”

import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

	/**
	 * @param args
	 * @throws InvocationTargetException
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException,
			InvocationTargetException {
		Thread t1 = new Thread() {
			@Override
			public void run() {
				// for(int i = 0; i < 10000; i++)
				// System.out.println(i+": t1");
			}

		};

		Thread t2 = new Thread() {
			@Override
			public void run() {
				// for(int i = 0; i < 10000; i++)
				// System.out.println(i+": t2");
			}

		};

		

		t1.start();
		t2.start();
		System.out.println("NOME THREAD T1: " + t1.getName() + " | isAlive: "
				+ t1.isAlive());
		System.out.println("NOME THREAD T2: " + t2.getName() + " | isAlive: "
				+ t2.isAlive());

	}

}

Um problema muito comum em Threads é o controle se uma Thread está “morta” ou “viva”, ou seja, se ainda está em execução ou não. Na listagem 4 fizemos comentários dentro dos métodos “run()” da Thread t1 e t2 e vamos explicar o porque.

Quando você executar a linha “t1.start()” será criada uma nova Thread em memória (juntamente com a Thread main que já existia, pois ela fica viva enquanto o programa estiver rodando). Então imagine, a Thread t1 foi criada em memória e está sendo executada, mas a Thread main continua sua execução e cria Thread t2, através do “t2.start()”, agora a Thread main já criou 2 Threads em memória (t1 e t2) e continua sua execução.

Quando a Thread main tentar executar o primeiro “System.out.println” o resultado será “isAlive: false” tanto para a Thread t1 como a Thread t2. Mas porque ? Isso ocorre, pois quando a Thread main executou o “t1.start()” a sua execução foi tão rápida (pois não tem nada no método “run()”) que a Thread t1 acabou morrendo quase que instantaneamente, e o mesmo ocorreu com a Thread t2.

Faça um teste: retire os comentários da listagem 4 e veja qual será a saída. Provavelmente será “ativa” para as thread t1 e t2, a não ser que seu processador seja tão rápido que antes de executar o primeiro “System.out.println” ele já terminou a execução da Thread t1 e t2, mas isso também não é uma questão de rapidez e sim de prioridade do escalonador.

Veja no código abaixo como podemos fazer para mostrar todas as Threads ativas (principalmente a Thread main que tanto falamos).

Listagem 5: Mostrando threads ativas e seus nomes

import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

	/**
	 * @param args
	 * @throws InvocationTargetException
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException,
			InvocationTargetException {
		Thread t1 = new Thread() {
			@Override
			public void run() {
				 for(int i = 0; i < 10000; i++)
				 System.out.println(i+": t1");
			}

		};

		Thread t2 = new Thread() {
			@Override
			public void run() {
				 for(int i = 0; i < 10000; i++)
				 System.out.println(i+": t2");
			}

		};

		

		t1.start();
		t2.start();
		System.out.println("NOME THREAD MAIN: " + Thread.currentThread().getName() + " | isAlive: "
				+ Thread.currentThread().isAlive());
		System.out.println("NOME THREAD T1: " + t1.getName() + " | isAlive: "
				+ t1.isAlive());
		System.out.println("NOME THREAD T2: " + t2.getName() + " | isAlive: "
				+ t2.isAlive());

	}

}

O resultado do código acima será algo parecido com:

0: t1
1: t1
2: t1
3: t1
0: t2
1: t2
NOME THREAD MAIN: main | isAlive: true
NOME THREAD T1: Thread-0 | isAlive: true
NOME THREAD T2: Thread-1 | isAlive: true

Lembre-se que estamos executando os “System.out.println” na Thread main, então ele pode aparecer no inicio, meio ou fim da sua saída. Perceba que o nome das threads seguem um padrão, começando de zero (que nosso caso é a thread t1) e assim por diante.

Imagine agora que você precisa garantir que a Thread t1 seja executada por completo antes que a Thread t2 inicie, por diversas questão, como por exemplo: Garantir a consistência dos dados que serão lidos na t2 que por algum motivo necessitam de um processamento prévio da t1. Garantimos isso através do método “join()” da Classe Thread, onde iremos dizer a Thread main que só irá passar para a próxima linha, após toda execução da Thread t1 terminar. Veja nosso código abaixo.

Listagem 6: Usando o join

import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

	/**
	 * @param args
	 * @throws InvocationTargetException
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException,
			InvocationTargetException {
		Thread t1 = new Thread() {
			@Override
			public void run() {
				 for(int i = 0; i < 10000; i++)
				 System.out.println(i+": t1");
			}

		};

		Thread t2 = new Thread() {
			@Override
			public void run() {
				 for(int i = 0; i < 10000; i++)
				 System.out.println(i+": t2");
			}

		};

		

		t1.start();
		t1.join();
		t2.start();
		System.out.println("NOME THREAD MAIN: " + Thread.currentThread().getName() + " | isAlive: "
				+ Thread.currentThread().isAlive());
		System.out.println("NOME THREAD T1: " + t1.getName() + " | isAlive: "
				+ t1.isAlive());
		System.out.println("NOME THREAD T2: " + t2.getName() + " | isAlive: "
				+ t2.isAlive());

	}

}

Olha que interessante, quando a Thread main executar o “t1.start()” ele vai começar a mostrar os valores de t1 (0,12,3,4,5...), então a Thread main executa o “t1.join()” e para por ali, ela é congelada até que o método run() da Thread t1 finalize. Finalizado o processamento de t1, a Thread main continua sua execução agora fazendo “t2.start()” e logo em seguida executa os “System.out.println”.

No primeiro System.out.println teremos a saída: “NOME THREAD MAIN: main | isAlive: true” pois enquanto nosso programa estiver rodando a Thread main estará “viva”.

No segundo System.out.println teremos a saída: NOME THREAD T1: Thread-0 | isAlive: false”, ou seja, nossa Thread t1 está “morta”, pois como usamos o join, a Thread main foi obrigada a esperar o término de todo processamento de t1 e qualquer código executado depois do “t1.join()” a Thread t1 já está “morta”.

Por fim no último System.out.println a Thread t2 continua viva pois ela está sendo executada em paralelo com a nossa Thread main.

O assunto abordado neste artigo pode parecer um tanto quanto confuso dada muitas circunstâncias e casos específicos, mas lendo e relendo o artigo diversas vezes e principalmente testando os códigos aqui demonstrados, você com certeza entenderá o funcionamento das threads com exatidão.

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?