Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Threads em Java 2: programação concorrente avançada

Threads em Java 2: programação concorrente avançada

O projeto Servidor de tarefas - Introdução

Oi, alunos. Bem-vindos à Alura, bem-vindo nesse segundo curso sobre threads, eu sou o instrutor Nico Steppat e vou acompanhar vocês nessas próximas aulas sobre essa tecnologia de threads. No primeiro módulo, só lembrando, vimos como funciona, como o Java nasceu, como funcionam os threads na plataforma, os principais recursos dos threads.

Nós falamos sobre a classe thread, os métodos principais, como start, sleep, vimos como passar uma tarefa para a thread através dessa interface runnabe, vimos também como você pode fazer duas threads se sincronizarem e comunicam entre si através desses métodos de wait e notify e por fim essa palavra-chave synchronized para fechar a porta de um objeto e apenas um thread ao mesmo tempo pode acessar aquele objeto.

Esses são recursos que vocês deveriam estar familiarizados. Vou assumir que vocês conhecem esses principais recursos. Claro, durante o treinamento eu sempre vou dar uma revisada, explicando as coisas, mas isso vocês deveriam estar conscientes e seguros.

O que vamos fazer então nesse segundo módulo? Duas coisas. Primeiro vamos criar um projeto mais real, vamos criar um servidor, nosso próprio servidor, onde os clientes podem se conectar via rede. Isso é um prato cheio para a programação concorrente, algumas coisas legais que podemos aplicar diante desse projeto.

E além disso, lógico que vamos continuar trabalhando com os recursos principais dos threads, mas nós vamos focar e ver várias classes desse pacote Java.util.concurrent que foi introduzido na versão 5 do Java e melhorou e facilitou várias coisas no mundo de threads.

Como vai funcionar o nosso projeto? Nós vamos chamar esse projeto de “servidor tarefas”. A ideia é que um cliente pode se conectar com nosso próprio servidor, enviar um comando, e baseado nesse comando o servidor precisa executar agora uma tarefa.

Vem agora a questão, como posso fazer isso? Hoje em dia, se você pensa na comunicação remota, logo você usa o protocolo HTTP. Nós vamos utilizar aqui um protocolo que na verdade roda por baixo do HTTP, o protocolo TCP. O TCP é o protocolo que vamos utilizar para enviar dados e receber dados.

Mas o TCP não define um vocabulário. O TCP apenas garante que os dados serão transferidos na rede de maneira confiável. Qual o significado desses dados? O TCP não define, o TCP é apenas um protocolo de transporte, diferente do protocolo HTTP, que é um protocolo de aplicação, que tem esse vocabulário.

Nós vamos de certa forma criar nosso próprio, não vai ser bem um protocolo, mas vamos definir nosso próprio vocabulário. Aí vem agora a pergunta, vamos usar esse protocolo de mais baixo nível TCP para enviar e receber dados, criar uma conexão, mas como faço isso no mundo Java?

Nós vamos utilizar as classes do mundo socket, que usam por baixo dos panos o protocolo TCP e abstraem a complexidade. Então do lado do servidor nós vamos criar um server socket falando, o nosso server socket vai rodar na máquina local, naquela porta, a porta é uma entrada de rede que o computador, que a máquina vai oferecer.

Nosso cliente vai utilizar um socket para se conectar com nosso servidor. Um socket é um ponto final de uma comunicação. Então nós vamos ter um do lado do cliente e um socket do lado do servidor também. Os dois vão se comunicar usando o TCP.

Nesse primeiro vídeo agora vamos implementar esse servidor. No segundo vídeo nós já vamos começar a mexer nos threads, mas nesse primeiro vídeo agora vamos focar como implementar esse projeto, como usar os sockets no mundo Java. Então mãos à obra, vamos lá, chega de papo, vamos no Eclipse. Então não vamos perder tempo e começar a implementar já nosso projeto.

Ainda nesse vídeo agora sem threads, mas vem com certeza. Vamos criar um novo projeto, agora o projeto que vou criar é um Java project normal, nada de web project, não vamos trabalhar com protocolo HTTP, só TCP por baixo dos panos, e vou chamar esse projeto servidor-tarefas. Tudo aqui na máquina, tudo padrão, só ok, confirmando.

Vou mudar a perspectiva para Java. Essas janelas eu sempre fecho. Dentro da pasta “src” já vamos criar nossa primeira classe. Vamos criar um novo pacote br.com.alura.servidor, e a classe também vou chamar servidorTarefas. E já vamos criar aqui o método main para poder subir nosso servidor.

Dentro desse método main vamos usar aqueleServerSocket que eu já falei, só importando do Java.net. Vou chamar ele de servidor e criando, e na hora de criar deveríamos passar aqui a porta, que deveria ser não mais de dois bits, ou seja, de zero até 65 mil alguma coisa. O importante é sempre usar um número mais alto, porque tem algumas portas que o sistema operacional já utiliza por padrão, por exemplo, 80 para HTTP.

Para não criar conflito eu vou colocar um número de cinco dígitos, “12345”, muito criativo. Essas classes são cheias de exceções, por isso já faço um throws, onde vou jogar uma exception. O tratamento de erro agora neste momento não é nosso foco, vamos falar ainda sobre isso.

Meu ServerSocket já está no AA, e a partir desse ServerSocket agora eu posso aceitar conexões, isso você faz através do método accept. Esse accept agora me devolve o ponto concreto de um ponto final da comunicação, o que falamos, que o socket é um ponto final de comunicação, então essa ServerSocket me devolve agora um socket.

Vou colocar aqui socket por enquanto, isso seria agora quando o cliente se conecta o resultado, a partir desse cara agora eu posso me comunicar. Isso já é uma coisa. Vamos só colocar um System.out.println para sabermos o que está acontecendo, iniciando servidor, para acompanhar mais fácil. E agora já podemos pensar no lado do cliente. Vamos mexer muito aqui, não vamos ficar assim.

Vou criar aqui uma nova classe, vou colocar outro pacote cliente, para separar isso, vou chamar ClienteTarefas. Lá também vou criar o método main. Então a ideia agora com o servidor no ar, vamos rodar essa classe, se conectar agora no lado do cliente. Como faço isso?

Também através de um socket. Não há muito mistério, só que agora não é um ServerSocket, apenas um socket, Java.net, e aqui agora vou falar onde se encontra o meu servidor. Passando dois parâmetros, o ip da máquina, o host e a porta. Como aqui na máquina local, vou colocar localhost, e a nossa porta que nós definimos que é a porta inicial, comunicação.

Aqui também é cheio de exceções, eu vou fazer também já throws exception para me livrar um pouco disso. Agora com isso já temos uma comunicação estabelecida, temos então socket de um lado do servidor e temos um socket do lado do cliente, são dois pontos de comunicação, e por baixo dos panos eles se comunicam com TCP que vamos fazer ainda.

Posso também dar uma mensagem aqui na conexão estabelecida, alguma coisa assim. E depois, como nós somos bonzinhos, vamos dar um close no socket. Já temos duas classes para testar. Ainda muito simples, mas não se preocupem, vai ficar mais legal.

Rodando então o servidor Java application, reparem agora que minha console está rodando, a máquina virtual está rodando, porque esse método é bloqueante. Ele travou o thread main aqui para aceitar uma conexão. Criar esse socket e estabelecer essa conexão TCP por baixo dos panos. O servidor está rodando, agora vou rodar o cliente que agora vai justamente se conectar. Vamos lá.

Rodando ele também. Repare, lembra que conseguiu estabelecer a conexão e depois nós fechamos o cliente, a máquina voltou, terminou. Aqui está certo. Vamos ver a outra. Reparem, o meu servidor agora, esse método que era bloqueante recebia um socket e depois parou também, foi esperado. Claro que assim nosso servidor não pode funcionar, mas pelo menos conseguimos estabelecer uma conexão.

Agora temos que aceitar vários clientes, certo? Então vamos “mandar bala”. Como eu faço? Não há muito mistério. Se você quer aceitar vários clientes e não posso chamar esse método aceita, que uma vez preciso continuar chamando, sempre esperando o próximo cliente. Então o que vamos fazer? Vamos fazer um laço. Enquanto está true, ou seja, para sempre, aceita novos clientes.

Para sabermos que o cliente foi aceito, vamos fazer também um System.out.println(“Aceitando novo cliente”), agora vou conectar aqui uma informação do nosso socket. Há várias informações, e vou pedir agora do meu socket, que é o ponto final da comunicação entre cliente e servidor, vou pedir para ele me passar a porta dele. Vou colocar aceitando novo cliente na porta “tal”.

Vamos ver o que ele imprime para nós. Vou fechar os consoles, rodando o servidor de novo. Aparentemente a máquina está rodando, vamos testar o cliente, estabeleceu e fechou porque nós fechamos o socket. Vou rodar o cliente mais uma vez. De novo apertando, rodamos, conectamos dois clientes agora.

Vou voltar no console do servidor, reparem aqui, aceitando novo cliente, o meu servidor continua agora rodando. O interessante agora aqui é um detalhe, na verdade, do TCP por baixo dos panos. Repare que nossa porta inicial é “12345”, então quando usamos o cliente nos conectamos com esse ServerSocket na porta 12345, mas quando o ServerSocket aceitou, eles fizeram uma negociação e para cada cliente ele usa uma nova porta, isso faz parte do TCP, no HTTP também usa TCP por baixa dos panos, não é diferente, você estabelece uma conexão com porta 80 HTTP e depois o navegador com o servidor usam uma porta específica.

Então para cada cliente nosso servidor abriu uma porta específica para saber que é para esse cliente, porque nossos dois clientes que eu rodei agora sempre vêm da máquina local, localhost, tudo dentro dessa máquina, então como o servidor pode saber, vai ser esse cliente ou esse? Através da porta, abriu duas portas separadas para o nosso cliente.

Nosso primeiro objetivo aqui, fazer uma comunicação via socket já funcionou, claro que é enviar, receber, vamos fazer, isso já fizemos, no segundo vídeo realmente vamos começar a mexer com threads e tratar esses vários clientes que temos no lado do servidor, atender eles através de várias threads. Até lá.

O projeto Servidor de tarefas - Primeira tarefa

Já conseguimos estabelecer uma conexão entre cliente e servidor, uma conexão TCP usando os sockets no Java. Agora qual o meu problema aqui? Seguinte, nosso servidor já está funcional, mas ele não consegue atender dois clientes ao mesmo tempo. Vou provar isso para vocês.

Dentro do while, onde ele já aceita novo cliente, vou fazer um thread.sleep, vou deixar ele dormir uns vinte segundos. Ou seja, ele aceita o cliente, imprime a informação e fica esperando vinte segundos. E agora no cliente também temos que mudar uma coisa, porque meu cliente se conecta e vai embora, assim nosso programa não vai funcionar, nós vamos estabelecer uma conexão com o cliente, vai deixar esse cliente online, vamos simular isso online agora.

O que eu vou fazer, definir aqui uma entrada do meu teclado através da classe Java.util.scanner, que recebe no construtor input stream, esse input stream deveria ser o system.in, que é o teclado. Aí posso simular aqui teclado, uma leitura, e chamando next line. Esse código também bloqueia nessa linha, e espera que eu digite uma linha, dou um enter no console.

Se faço nada no console a máquina fica travada aqui. Então vamos testar isso. Rodando o servidor, inicializando, já está no while, já está esperando aqui. Eu rodando o cliente. Reparem agora que meu cliente já mudou e ele está continuando rodando. No meu servidor ele acertou a conexão, está fazendo o sleep ainda.

Já acabou porque passaram vinte segundos. Reparem agora que se eu volto no meu cliente, só para mostrar, aperto no console “Enter”, ele termina. Então agora o cliente realmente fechou o socket, ele foi embora. Mas isso não foi a prova que eu queria mostrar.

Mas para vermos isso realmente funcionando vou pedir para você o seguinte. Eu quero que você no Eclipse, na aba console, no lado direito, abra uma nova console. Quero que vocês arrastem esse console do lado do outro. Vou colocar do lado esquerdo embaixo para podermos simular o console do servidor e o cliente ao mesmo tempo.

Repare que aqui tem o servidor ainda rodando. Eu vou pendurar, até tem um pin console, essa console, aí essa console não muda mais, o Eclipse não fica mais alterando o console entre máquinas virtuais diferentes. Essa console sempre será associada com o servidor, ótimo, isso que eu queria.

Agora nessa console vamos rodar nosso cliente. Vamos lá. Uma vez. Estabeleceu a conexão, apareceu aqui. E agora está no sleep, nos vinte segundos. Dentro desses vinte segundos agora vou rodar o cliente de novo. Reparem agora que não apareceu a saída.

Agora foi. Reparem que apareceu a saída do cliente, porque o cliente antigo saía dessa thread sleep, aí ele conseguiu acertar o novo cliente, vamos testar isso de novo, só mudando para os dois clientes que estão rodando ainda. Eu vou parar esses clientes, sempre alterando no console, vou parar, vou fechar todos os consoles que são inativos. Agora temos os dois, o servidor, vou de novo rodar o cliente.

Mudando agora para o cliente. Apareceu essa saída. Agora vou rodar de novo o cliente. Reparem, não aparece a saída, porque o outro cliente, o cliente anterior ainda trava o thread main. Então estamos tentando aceitar os clientes no mesmo thread main. Então quando um cliente agora trava essa thread, porque faz um processamento pesado, os outros ficam esperando.

Nós não queremos isso, certo? Nós já sabemos como resolver isso. Estou fechando os consoles, fechando tudo, os clientes. Vamos então mexer no nosso servidor para resolver esse problema, não pode ser que um servidor só pode atender um cliente ao mesmo tempo, então qual a ideia? Criar um thread para cada cliente. Ou seja, quero fazer uma coisa aqui assim, Thread thread(Cliente - new Thread e agora passando aqui para ele o que tem que ser feito.

Você sabe que aqui você precisa passar um runable. Como vou chamar esse runable? A ideia é que cada cliente pode enviar comandos e baseado nesses comandos o servidor agora executa tarefas, então vou chamar essa tarefa de distribuirTarefas. Eu vou criar esse cara.

Eu também já quero que ele leve o socket do cliente para ele poder avisar o cliente que está processando, essas coisas, eu vou passar o socket no construtor. Esse distribuirTarefas vou passar para minha thread, e depois só chamando threadClient.start.

Temos que criar essa classe, Eclipse ajuda, "Ctrl + 1", vamos criar essa classe, e já vou colocar a interface runable que você já conhece. Está aqui, voltando. Já vamos criar esse construtor pelo Eclipse também, só confirmando, sempre usando o Eclipse, aqui também "Ctrl + 1", quero criar um novo atributo. E agora temos que fazer a implementação.

Ou seja, vou fazer a mesma simulação, essa thread sleep agora não vai ficar aqui, no meu thread main, vai ficar nesse outro lá na distribuir tarefas, vou fazer um thread sleep. Só que aqui tem que fazer um try catch porque não podemos jogar uma exceção, não tem jeito. Eu só vou dar um System.out.println colocando uma informação, distribuindo tarefas para, vou concatenar o socket para imprimir as informações desse cliente.

Ele vai travar aqui também vinte segundos, mas nós abrimos um thread dedicado para essa socket, para esse cliente. Ou seja, nosso comportamento agora deveria ser bem diferente. Então vamos testar isso de novo. Rodando o servidor. Servidor está rodando, fazendo um pin nesse console.

Agora no outro console vou subir o primeiro cliente. Rolando, mudando o console, o cliente, estabeleceu a conexão, e apareceu distribuindo tarefa. Agora vou rodar de novo porque estamos dentro desses vinte segundos ainda. Reparem, eu já consegui, apareceu de novo, aceitou de novo o cliente quantos eu quiser. Então rodando aqui perfeito. Ou seja, se eu agora exagerar um pouco, todos os clientes agora online estão rodando e meu servidor aceita quantos clientes ele quiser.

Então a primeira tarefa já foi feita através do nosso servidor, já estamos atendendo os clientes baseado nos threads, criando nosso thread, passando a tarefa para ele. Agora já temos uma base para praticar um pouco nos exercícios e depois vamos pensar direito no próximo capítulo quantos threads realmente deveríamos criar dentro do nosso servidor. Mãos à obra, cai nos exercícios, e até o próximo capítulo.

Reuso de threads - inputStream e outputStream

Bem-vindos de volta ao nosso treinamento sobre threads, no segundo treinamento sobre threads. No primeiro capítulo foi de certa forma uma revisão, trabalhamos com a classe thread na mão para distribuir as nossas tarefas dentro da nossa aplicação cliente servidor.

Também vimos como estabelecer uma conexão com sockets, mas ainda não enviamos dados. Neste capítulo agora vamos realmente enviar um comando através do cliente e vamos pensar como controlar, reaproveitar nossas threads no lado do servidor melhor.

A primeira coisa que vou fazer no meu cliente é realmente enviar alguma informação, travou nosso cliente através desse teclado, mas antes de tratar eu quero enviar um comando. Como eu faço? A partir do meu socket agora eu preciso mandar algo para meu servidor, e para isso vou pegar um OutputStream, vou escrever um comando nessa OutputStream.

Vou pegar essa variável, OutputStream, do cliente. E como é meio duro trabalhar com OutputStream na raça, vou embrulhar esse OutputStream dentro de um PrintStream do pacote Java.io, vou chamar aqui saída, criando esse PrintStream e passando o OutputStream do cliente.

Na verdade, já vou passar diretamente o OutputStream, essa linha no construtor. Posso apagar essa linha, deixando um pouco mais enxuto. A partir dessa saída agora que escrevo para aquele OutputStream posso agora imprimir dados. Vamos enviar agora um comando.

Poderíamos agora criar um protocolo sofisticado, mas nossa ideia agora em um treinamento de criação de protocolo nosso foco são os threads. Vou enviar uma coisa bem simples. Vamos pensar que o cliente pode mandar um comando e esse comando se chama c1.

Bem sofisticado, mas já vai rolar. Vou ser bonzinho e fechar também o teclado, que não tinha feito no exercício anterior, e vamos também fechar a saída. Então meu cliente já manda alguma informação para o comando c1, para o meu servidor. E agora quero receber esse comando no lado do servidor, porque baseado nesse comando ele precisa fazer algo.

No meu servidor já fizemos a distribuição da tarefa e esse trabalho de analisar, agora, o que o cliente mandou, vai ficar nessa classe, no nosso método run. Estou com o socket cliente em mãos, vou tentar capturar os dados. Aqui também vou utilizar o socket, mas não o OutputStream, quero agora ler os dados que o cliente mandou. Vou pegar o InputStream.

Esse InputStream agora eu jogo no scanner, é parecido com o que fizemos do lado do cliente, só que meu InputStream agora no teclado, InputStream são os dados que o cliente vai enviar. Só falta importar e vamos criar uma variável entradaCliente, por exemplo, alguma coisa assim.

Exige aqui infelizmente um tratamento de erro, então eu faço o seguinte, vou fazer um try catch global, todo o código dentro do try catch, e vou fazer um catch de exception jogando uma nova exceção. Se acontecer algum erro, eu crio uma RuntimeException, uma exceção unchecked, e jogo essa exceção passando a exceção original, assim conseguimos resolver isso.

Para ler agora vou ficar lendo, vou fazer um laço, ou seja, enquanto a entrada do cliente tem uma próxima linha, eu vou ficar lendo. Aí vou recapturar o que ele me mandou, essa linha, que na verdade é um comando que ele envia. Como faço isso? Através do meu scanner, me dá a próxima linha. Aí vou imprimir o comando. Vou fazer aqui um comando, depois melhoramos isso.

Meu servidor recebe a entrada através do InputStream e fica lendo enquanto há novas linhas imprimindo o comando. Esse thread sleep podemos apagar e como somos bonzinhos fechamos no final, dou um close nele, no scanner, se não há mais nada para ler, nós fechamos.

Já podemos testar isso. Vamos lá, rodando o servidor, lembrando desse negócio de duas consoles, o servidor no lado esquerdo, e agora rodando o cliente que vai enviar o comando. Vou mudar o console para o cliente, estabelecido, ou seja, o cliente agora já envia os dados, no lado do servidor aceitou o cliente, distribuiu, ou seja, abriu um novo thread para nós e recebeu o comando c1. Conseguimos já enviar algumas informações, um comando para o nosso servidor.

Vamos testar isso de novo, lembrando que com enter você consegue fechar o cliente e rodar de novo o cliente, fica de olho no console. Reparem, aceitou, nova porta, distribuindo o novo thread e mandou o comando c1. Perfeito. Foi bem rápido, não há muito mistério, trabalhando com input e output stream. Agora vamos pensar quantos clientes deveríamos ou conseguimos atender no servidor, mas isso fica para o próximo vídeo.

Sobre o curso Threads em Java 2: programação concorrente avançada

O curso Threads em Java 2: programação concorrente avançada possui 212 minutos de vídeos, em um total de 87 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Plus

De
R$ 1.800
12X
R$109
à vista R$1.308
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

Matricule-se

Pro

De
R$ 2.400
12X
R$149
à vista R$1.788
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Luri, a inteligência artificial da Alura

    Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com Luri até 100 mensagens por semana.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas