Introdução: por que são necessárias filas de tarefas em Java?
\n\nNo desenvolvimento moderno em Java, multithreading não é um luxo, mas uma necessidade. As aplicações processam milhares de requisições, executam tarefas em segundo plano e interagem com serviços externos. Sem a organização adequada de filas de tarefas, o código se transforma em caos: threads se bloqueiam mutuamente, a memória transborda e o desempenho cai.
\n\nFilas de Tarefas (Task Queues) são um mecanismo que permite distribuir o trabalho de forma eficiente entre as threads. Em Java, existe um conjunto poderoso de ferramentas no pacote java.util.concurrent. Neste artigo, vamos explorar como usar filas de tarefas na prática, com exemplos reais de código.
Você aprenderá:
\n- \n
- Como funcionam
BlockingQueuee suas implementações \n - Como configurar
ThreadPoolExecutorpara tarefas específicas \n - Como evitar erros típicos ao trabalhar com filas \n
- Como testar e depurar código multithread \n
Fundamentos de filas de tarefas: BlockingQueue
\n\nBlockingQueue é uma interface que está na base da maioria das filas de tarefas em Java. Ela suporta operações de espera: se a fila estiver vazia, a thread consumidora espera até que um elemento apareça; se a fila estiver cheia, a thread produtora espera até que haja espaço disponível.
Principais implementações de BlockingQueue
\n\nEm Java, existem várias implementações de BlockingQueue, cada uma com suas características:
- \n
- ArrayBlockingQueue — tamanho fixo, baseada em array. Adequada para cenários com limite conhecido de tarefas. \n
- LinkedBlockingQueue — opcionalmente limitada, baseada em lista encadeada. Boa para cargas dinâmicas. \n
- PriorityBlockingQueue — fila ilimitada com prioridades. Usada quando as tarefas têm diferentes importâncias. \n
- SynchronousQueue — fila sem capacidade: cada insert deve esperar pelo remove correspondente. Ideal para transferência direta de tarefas. \n
Exemplo: fila de tarefas simples com ArrayBlockingQueue
\n\nVamos considerar um exemplo básico, onde uma thread gera tarefas e outra as processa:
\n\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.BlockingQueue;\n\npublic class SimpleTaskQueue {\n public static void main(String[] args) {\n BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);\n\n // Thread produtora\n Thread producer = new Thread(() -> {\n try {\n for (int i = 1; i <= 20; i++) {\n String task = "Tarefa nº" + i;\n queue.put(task); // Bloqueia se a fila estiver cheia\n System.out.println("Adicionada: " + task);\n Thread.sleep(100); // Simulação de geração\n }\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n }\n });\n\n // Thread consumidora\n Thread consumer = new Thread(() -> {\n try {\n while (true) {\n String task = queue.take(); // Bloqueia se a fila estiver vazia\n System.out.println("Processada: " + task);\n Thread.sleep(300); // Simulação de processamento\n }\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n }\n });\n\n producer.start();\n consumer.start();\n }\n}\n\n\nEste código demonstra o padrão chave: o produtor adiciona tarefas via put(), o consumidor as retira via take(). Ambos os métodos bloqueiam quando necessário, garantindo sincronização natural.
ThreadPoolExecutor: fila de tarefas profissional
\n\nNa prática, raramente se usa