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.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT