Este artigo visa apresentar algumas das novas APIs do Java 8 que não estão sendo tão comentadas, mas que tornaram o Java melhor de várias maneiras.
1. StampedLock
O código multithreaded tem sido um desafio para os desenvolvedores. Com o tempo, algumas construções complexas foram adicionadas às blibliotecas Java visando auxiliar o desenvolvedor com códigos multithreaded, e seu acesso a recursos compartilhados. Um exemplo é o ReadWriteLock, que permite ao desenvolvedor dividir o código em seções que precisam ser mutuamente exclusivas ("writers") e seções que não precisam ("readers").
Na teoria parece ótimo. Porém um problema com ReadWriteLock é que a aplicação pode ficar muito lenta, às vezes até dez vezes mais lenta. O Java 8 introduziu um novo bloqueio ReadWrite, chamado StampedLock. A boa notícia é que o StampedLock é muito rápido. A má notícia é que é mais complicado de usar e envolve mais estados. O StampedLock não é "reentrante", o que significa que uma thread pode causar um deadlock envolvendo ela mesma.
O StampedLock possui o modo "otimista", que emite uma marcação (stamp) retornada por cada operação de bloqueio (lock). Essa marcação é utilizada como uma espécie de ingresso. Cada operação de desbloquei (unlock) precisa gerar sua marcação correspondente. Qualquer thread que adquirir o bloqueio de escrita, enquanto outra thread está com o bloqueio de escrita do modo otimista, fará com que o unlock seja invalidado (o stamp não será mais válido). Nesse ponto, a aplicação pode começar tudo novamente, possivelmente com um lock no modo "pessimista", que também é implementado pelo StampedLock. O gerenciamento de tudo isso fica por conta do desenvolvedor. E como um stamp não pode ser utilizado para desbloquear outro, deve-se tomar cuidado.
Veja a seguir um exemplo deste tipo de lock em ação:
long stamp = lock.tryOptimisticRead(); //sem caminho bloqueante - muito rápido. work(); // é esperado que não ocorra nenhuma escrita por hora. if (lock.validate(stamp)){ //sucesso! Sem contenção com a thread de escrita. } else { //outra thread precisa obter um bloqueio de escrita entretanto mudando o stamp. //voltando a ter um bloqueio de leitura mais pesado. stamp = lock.readLock(); //Este é uma operação de leitura que causa o lock. try { //Sem escrita acontecendo agora. work(); } finally { lock.unlock(stamp); // Liberação do stamp utilizado. } }
2. Adicionadores concorrentes
Outra novidade do Java 8 lida especificamente com código escalável: os "Adicionadores Concorrentes (Concurrent Adders)". Um dos padrões mais básicos de concorrência é a leitura e escrita de contadores numéricos. Há diversas maneiras de fazer isso hoje em dia, mas nenhuma tão eficiente ou elegante como a existente no Java 8.
Antes do Java 8, isso era feito utilizando "Atomics", que utilizavam instruções diretas da CPU de comparação e swap (CAS), através da classe sun.misc.Unsafe, para realizar a tentativa e marcar o valor de um contador. O problema era que quando um CAS falhava devido a uma contenção, o AtomicInteger ficava em loop, tentando continuamente o CAS em um loop infinito, até o seu sucesso. Em altos níveis de contenção isso poderia se mostrar muito lento.
A partir do Java 8 foi adicionado o LongAdders. Esse conjunto de classes fornece uma maneira conveniente de leitura e escrita de valores numéricos em cenários de grande escalabilidade. A utilização é simples. Basta iniciar um novo LongAdder e utilizar os seus métodos add() e intValue() para aumentar e retornar o contador.
A diferença entre esta versão do Atomics e a antiga é que, quando o CAS falha devido a alguma contenção, em vez de fazer o giro na CPU, o Adder armazenará o delta em uma célula interna alocada para aquela thread. Então adicionará o valor junto de qualquer outro valor de células pendentes ao resultado do método intValue(). Isso reduz a necessidade de voltar e realizar o CAS novamente, ou bloquear outras threads.
A utilização do modelo de Adicionadores concorrentes deve ser sempre preferido frente ao modelo Atômico para o gerenciamento de contadores.
3. Ordenação Paralela
Assim como os Adicionadores aceleram os contadores, o Java 8 traz uma maneira concisa de acelerar a ordenação. E de uma maneira muito simples, em vez de realizar a ordenação desta maneira:
Array.sort(myArray);
Agora pode utilizar o método pronto para a paralelização:
Arrays.parallelSort(myArray);
Isso automaticamente dividirá a sua coleção em partes, as quais serão ordenadas através de um número de "cores" e então agrupadas novamente. Há uma ressalva, quando o método de ordenação paralela é chamado em ambientes com multi-thread elevado, tal como um web container sobrecarregado, os benefícios iniciais são reduzidos, até mais de 90%, devido ao aumento de trocas de contexto da CPU.
4. Migrando para nova API de data
O Java 8 introduz uma nova API de datas. Uma nova maneira de lidar com horas e a maioria dos métodos da versão atual estão marcados como obsoletos. A nova API traz a facilidade de uso e a precisão fornecidas pela popular API Joda Time para o núcleo da biblioteca Java.
Assim como em qualquer nova API, as boas novas são o estilo mais elegante e funcional. Infelizmente, muito já foi feito com a antiga API e a transição deverá levar tempo.
Para ajudar na transição, a classe Date possui um novo método chamado "toInstant()", que converte uma data para a nova representação. Isso é especialmente útil ao fazer uso de APIs que recebe o formato clássico de data, pois basta converter a data para o novo formato e utilizar todos os benefícios da nova API de datas.
5. Controle de processos do sistema operacional
Lançar um processo do sistema operacional diretamente do código Java já é possível através das chamadas Java Native Interface (JNI), mas com isso é bem provável se deparar com resultados inesperados ou exceções sérias mais para a frente. Ainda assim, é um mal necessário.
Além disso, os processos têm mais um lado desagradável - o fato de tenderem a ficar suspensos. O problema ao iniciar um processo a partir do código Java até a versão 7 tem sido difícil o controle sobre um processo uma vez iniciado.
Para ajudar nessa tarefa o Java 8 traz três novos métodos na classe "Process":
- destroyForcibly() termina um processo com um grau de sucesso bem maior do que antes;
- isAlive() informa se um processo iniciado ainda está vivo;
- Uma nova sobrecarga de waitFor() especifica a quantidade de tempo para que o processo termine. Issa retorna ou o sucesso da execução ou o time-out, neste último caso é possível terminar o processo.
Duas boas utilizações para estes métodos são:
- Caso um processo não termine no tempo determinado, forçar seu término e seguir em diante:
if (process.wait(MY_TIMEOUT, TimeUnit.MILLISECONDS)){ //success! } else { process.destroyForcibly(); }
- Garantir que, antes que um código termine, não seja deixado nenhum processo para trás. Processos suspensos certamente esgotarão seu sistema operacional, mesmo que lentamente.
for (Process p : processes) { if (p.isAlive()) { p.destroyForcibly(); } }
6. Operações Numéricas Precisas
Um overflow numérico pode causar alguns dos mais estranhos problemas em software, dada sua característica implícita. Isso é especialmente verdade em sistemas nos quais tem contadores inteiros incrementados de acordo com o tempo. Nesses casos, recursos que funcionam corretamente em pré-produção, e até mesmo por longos períodos em produção, podem começar a se comportarem de maneira completamente estranha de repente, quando as operações sofrem o overflow e produzem valores inesperados.
Para auxiliar nesse problema, o Java 8 adicionou diversos métodos "exact" à classe Math, protegendo um código suscetível a overflows, através do lançamento de "uncheckedArithmeticException" quando o valor de uma operação causa um overflow.
int safeC = Math.multiplyExact(bigA, bigB); // Será lançada uma ArithmeticException caso o resultado exceda +-2^31
O único aspecto negativo disso é que a responsabilidade em identificar como o código pode ter problemas com overflow fica por conta de quem está desenvolvendo. Não há solução mágica, porém isso já é melhor que nada.
7. Geração segura de números aleatórios
O Java tem sido criticado por anos pelos problemas de segurança. Justo ou não, um grande esforço tem sido feito para blindar a JVM e os frameworks de possíveis ataques. Os números aleatórios com baixo nível de entropia tornam sistemas que os utilizem na criação de chaves para criptografia ou códigos hash mais suscetíveis a ataques.
Até o momento, a escolha do algoritmo de geração de números aleatórios é de responsabilidade do desenvolvedor. O problema é que, nas implementações que dependem de um hardware, sistema operacional ou JVM específicos, o algoritmo desejado pode não estar disponível. Em tais casos, as aplicações têm tendência a migrar para versões mais fracas de geradores, o que aumenta o risco de ataques.
O Java 8 adicionou o novo método SecureRandom.getInstancStrong() com o objetivo de permitir que a JVM escolha uma versão segura. Se escrever código sem o completo controle sobre o hardware/SO/JVM em que será executado, deve-se considerar o uso deste método. (É uma situação comum quando se constroem aplicações a serem executadas na nuvem ou em ambientes de PaaS.)
8. Referências opcionais
As exceções do tipo NullPointerException são bem antigas. Porém ainda hoje estão presentes no dia-a-dia de uma equipe de desenvolvimento, não importa quão experiente a equipe seja. Para ajudar com este problema o Java 8 introduz um novo template chamado Optional<T>.
Baseado em linguagens como Scala e Haskell, esse template indica explicitamente quando uma referência passada ou retornada por uma função pode ser nula. Isso ajuda a reduzir as dúvidas se uma referência pode ou não ser nula, devido à confiança em documentações que podem estar desatualizadas, ou à códigos que podem mudam com o tempo.
Optional<User> tryFindUser(int userID) {
ou
void processUser(User user, Optional<Cart> shoppingCart) {
O template Optional possui funções que tornam o trabalho mais conveniente, como "isPresent()" que verifica se um valor não-nulo está disponível, ou "ifPresent()", que recebe uma função Lambda que será executada caso "isPresent()" seja verdadeiro. O lado negativo é, assim como ocorre com a nova API de Data e Hora, levará tempo até que está novidade esteja presente nas diversas APIs usadas diariamente.
Aqui está a nova sintaxe Lambda para escrever um valor opcional:
value.ifPresent(System.out::print);
Sobre o autor
Tal Weiss é o CEO da Takipi. Weiss vem desenhando aplicações escaláveis, de tempo real em Java e C++ nos últimos 15 anos. Ele ainda gosta de analisar um bom bug através da instrumentação de código Java. Em seu tempo livre Weiss toca Jazz na bateria.