BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos 8 funcionalidades pouco conhecidas do Java 8

8 funcionalidades pouco conhecidas do Java 8

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":

  1. destroyForcibly() termina um processo com um grau de sucesso bem maior do que antes;
  2. isAlive() informa se um processo iniciado ainda está vivo;
  3. 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.

Conteúdo educacional

BT