Java 19 – Virtual Threads (JEP 425 preview – Como usar?!)

Finalmente estamos colhendo os primeiros resultados do Project Loom, trazendo Virtual Threads para a JVM. Virtual threads são threas controlados pelo Java Runtime, em oposição aos Platform Threads que dependem de threads do sistema operacional.

Threads de runtime Java geralmente são correspondentes à threads do kernel do sistema (one-to-one), e o thread scheduler do kernel do sistema é responsável pelo agendamento das threads Java.

O modelo acima era até então o modelo multithreading que utilizamos para resolver problemas de programação concorrente. Mas temos dois problemas centrais com as threads da máquina: são caras e têm um número limitado.
Sendo threads do sistema operacional, elas são caras para criar, tanto em tempo quanto em consumo de recursos. É por isso que você só pode ter tantos deles antes de ficar sem recursos. E quando uma plataform thread é bloqueada, a thread do sistema operacional também é bloqueada, sendo assim nenhum outro código pode ser executado na thread do sistema operacional durante o período de bloqueio.

Virtual threads chegaram para resolver esse problema. A thread que usamos até agora é chamada de platform thread que ainda corresponde à thread do kernel do sistema (aquela one-to-one). Um grande número (M) de virtual threads é executado em um número menor (N) de platform threads (programação M:N).

Multiplos virtual threads serão scheduled pela JVM para execução em um determinado encadeamento de plataforma e um platform thread executará apenas uma virtual thread ao mesmo tempo.

Ou seja, o Java 19 apresenta uma prévia de uma alternativa que deve melhorar drasticamente o manuseio de operações simultâneas por servidores.

Criando Virtual Threads! – Exemplos de uso:

Novas APIs para criar virtual e platform threads:
Thread.ofVirtual() e Thread.ofPlatform()

Thread.startVirtualThread(Runnable) Cria e faz start da thread:

Thread.isVirtual() determina se é uma virtual thread:

Thread.join e Thread.sleep esperam pelo fim da virtual thread e a coloca para dormir:

Executors.newVirtualThreadPerTaskExecutor() cria um ExecutorService que criará uma nova thread virtual para cada task:

Web servers

Como você pode ver, apenas usamos um método diferente para indicar que tipo de thread gostaríamos de criar. A partir daí, não há diferença em como você as usa. Servidores da Web como Tomcat ou Jetty não serão diferentes para você ao usá-los.

A Oracle está desenvolvendo também uma nova versão de seu próprio framework de microsserviços Helidon que utiliza virtual threads.

Pra finalizar…

Embora as Virtual Threads tragam enormes melhorias, atualmente não são destinadas a todos os casos de uso. Especialmente as tarefas com uso intensivo de CPU não se beneficiarão tanto da escalabilidade aprimorada, pois tendem a bloquear menos. Também vale a pena mencionar que o código que usa muitos blocos sincronizados para limitar o acesso simultâneo ao código deve ser reescrito em ReentrantLocks.

Espero que tenham gostado, obrigado por ler.
🙂

Vou continuar com mais novidades do Java no próximo post.


referencias:
https://medium.com/javarevisited/how-to-use-java-19-virtual-threads-c16a32bad5f7

Como funciona Java Thread Pool?

Pergunta de entrevista***

Vamos lá, diretamente ao ponto:

Internamente, as tasks são inseridas em uma Blocking Queue da qual as threads do pool estão sendo desenfileirados. Quando uma nova task é inserida na fila, uma das threads ociosas a desenfileirará com sucesso e a executará. O restante das Threads ociosas no pool serão bloqueadas, aguardando para desenfileirar tasks.

Thread Pool é usado principalmente para reduzir o número de threads do aplicativo e fornecer gerenciamento das threads de trabalho. Os aplicativos podem enfileirar itens de trabalho(work items), associar esses workers à handles, enfileirar automaticamente com base em um keepAliveTime e vincular com I/O.

Resposta curta:

Java Thread pool representa um grupo de threads de trabalho que estão aguardando a tarefa e são reutilizadas muitas vezes.

Agora um pouco mais devagar 🙂

Vamos dar uma olhada em alguns recursos chave da classe ThreadPoolExecutor:

corePoolSize: o número mínimo de workers para manter vivos.
maximumPoolSize: a capacidade do pool.
keepAliveTime: tempo limite em nanossegundos para threads que estejam ociosas.
workQueue: a fila usada para manter as tasks e entregar para worker threads.
allowCoreThreadTimeOut: usado para aplicar a política de tempo limite à threads principais, desde que o valor keepAliveTime seja diferente de zero.

Como esses atributos estão vinculados?

Uma analogia:

Imagine o pool de threads como uma fábrica;
Core Thread são funcionários em tempo integral da fábrica;
Non-core Threads são os contratados terceirizados;
Blocking Queue (fila de bloqueio) é o pool de tarefas(task pool).

Quando uma task é entregue à fábrica, os funcionários em tempo integral são solicitados primeiro a lidar com as tasks.

Se todos os funcionários estiverem ocupados, a task será colocada no task pool.

Se o task pool estiver cheio, a fábrica solicita aos contratados terceirizados que concluam as tasks.

Se todos os funcionários e contratados estiverem ocupados, a fábrica rejeitará as novas tasks recebidas.

Se os funcionários terceirizados terminarem suas tasks, eles aguardarão um pouco (por exemplo, keepAliveTime) e deixarão a fábrica se nenhuma nova task for dada.

No entanto, parece que os funcionários em tempo integral estão esperando e trabalhando para sempre.

A fábrica decide liberar os funcionários em tempo integral depois de algum tempo, como contratados terceirizados, ativando o switch allowCoreThreadTimeOut.

Vantagem:

Performance, pois não há necessidade de criar novas threads.


Espero ter ajudado e ter proporcionado uma boa leitura.

Obrigado.

Big O notation e um feliz 2022!

Quando o tempo de execução te assusta!

Gráfico de complexidade da notação

A notação Big O é um conceito que todos os estudantes de ciência da computação encontrarão em seus estudos em algum lugar ao longo do caminho. Mas, qual a importância que o Big O tem?

Big O?


A letra “O” vem de ordem, outro termo para descrever o crescimento de uma função. A letra “N” sempre utilizada na matemática, representa o número de entradas em questão.
Utilizando a notação Big O, em termos básicos, você irá saber com que rapidez uma função crescerá ou diminuirá. O tempo de execução de uma função é expresso em quão rápido ela cresce em relação à entrada conforme a entrada aumenta.
Para um algoritmo com muito poucas entradas, isso não é um grande negócio. Mas, à medida que a quantidade de insumos aumenta para um número muito grande, torna-se cada vez mais importante. Afinal, ninguém quer esperar o dia todo apenas para carregar uma página em um site ou a visualização de um aplicativo.

O tempo de execução não é o único fator ao considerar a eficiência da função. A eficiência de um algoritmo também considera o uso da rede, de disco e de memória.
A complexidade da CPU (tempo) é o principal fator. É considerado principalmente como o desempenho, quanto espaço em disco ou memória / tempo é usado quando um programa está sendo executado junto com a complexidade.

Por exemplo, como os requisitos de recursos aumentam à medida que as entradas aumentam.

Complexidade da notação

Seguindo a ordem dos menos eficientes aos mais eficientes:

  1. O(n!) (fatorial) o número de instruções executadas cresce muito rapidamente para um pequeno crescimento do número de itens processados. Dentre os ilustrados é o pior comportamento para um algoritmo, pois rapidamente o processamento se torna inviável. É o caso da implementação inocente do Problema do Caixeiro Viajante ou de um algoritmo que gere todas as possíveis permutações de uma lista, por exemplo (fonte desse exemplo).
  2. O(2n) (exponencial) também é bem ruim, pois o número de instruções também cresce muito rápidamente (exponencialmente), ainda que numa taxa menor do que o anterior. É o caso de algoritmos que fazem busca em árvores binárias não ordenadas, por exemplo.
  3. O(n2) (quadrático) é factível, mas tende a se tornar muito ruim quando a quantidade de dados é suficientemente grande. É o caso de algorítmos que têm dois laços (for) encadeados, como, por exemplo, o processamento de itens em uma matriz bidimensional.
  4. O(n log n) (sub-quadrático ou super-linear) é melhor do que o quadrático, sendo geralmente até onde se consegue otimizar algoritmos que são quadráticos em sua implementação mais direta e inocente (naïve). É o caso do algoritmo de ordenação QuickSort, por exemplo (que tem essa complexidade no caso médio, mas que ainda assim é quadrático no pior caso).
  5. O(n) (linear) é aquele cujo crescimento no número de operações é diretamente proporcional ao crescimento do número de itens. É o caso de algoritmos de busca em uma matriz unidimensional não ordenada, por exemplo.
  6. O(log n) (logaritmo) é aquele cujo crescimento do número de operações é menor do que o do número de itens. É o caso de algoritmos de busca em árvores binárias ordenadas (Binary Search Trees), por exemplo (no caso médio, no pior caso continua sendo linear).
  7. O(1) (constante) é aquele em que não há crescimento do número de operações, pois não depende do volume de dados de entrada (n). É o caso do acesso direto a um elemento de uma matriz, por exemplo.

Conclusão

Espero ter ajudado e ter proporcionado uma boa leitura, em breve devo escrever exemplos para cada tipo de Big O Notation.

Precisava escrever algo para o último dia do ano, e porque não sobre um assunto que dominou meu ultimo mês de trabalho de 2021?

Obrigado.

Feliz 2022! Com muito amor e saúde!

Fonte: https://www.bigocheatsheet.com/

Java – instanceof Pattern Matching

O Pattern Matching para o operador instanceof foi introduzido como um recurso de visualização com JDK 14 e foi finalizada com JDK 16, desde então, não é surpreendente ver mudanças sendo feitas no JDK para tirar vantagem dessa nova funcionalidade.

Nesta postagem, vamos focar no uso de Pattern Matching do instanceof na implementação anterior (antes da JDK 14) e atual.

Anterior:

if (animal instanceof Duck) {
    Duck duck = (Duck) animal;
    duck.quack();
} else if (animal instanceof Cat) {
    Cat cat = (Cat) animal;
    cat.meow();
}

Atual:

if (animal instanceof Duck duck) {
    duck.quack();
} else if (animal instanceof Cat cat) {
    cat.meow();
}

O que está a acontecer? Simplesmente o resultado do instanceof está sendo atribuído via pattern matching às variáveis cat e duck.

Mais um exemplo bem didático:

if (object instanceof String str && str.length() > 1) {
    //do something...
} else if (object instanceof List list) {
    list.forEach(o -> {
        if (o instanceof String str && str.length() > 1) {
            //do something...
        }
    });
}

Muito legal, não é? Com essa abordagem a nova feature faz com que tenhamos algumas boas vantagens, pois já não será mais preciso escrever tanto código, que dependendo do negócio se tornava muito estressante e tedioso.

Algumas vantagens:

  • Não é mais preciso escrever este tipo de código em que precisamos testar o tipo e fazer uma conversão para cada bloco condicional;
  • Não é mais preciso repetir o nome do tipo três vezes para cada bloco condicional;
  • A legibilidade é muito limpa, pois já não sujamos a conversão e extração do valor para a variável;
  • Não existe mais problemas com legibilidade, e previne possíveis erros de excecução ao adicionar várias variáveis e casts, ou adicionando um novo tipo à da super classe. (ex: animal)

Essa feature veio para tornar nosso código conciso, mais simples de escrever e fácil de ler, todos os dias.

Essas mudanças no JDK para alavancar o pattern matching de instância podem fornecem muitas idéias e exemplos para os desenvolvedores saberem onde começar a aplicar isso no próprio código.

Brian Goetz usa um bom exemplo de como muitas vezes implementamos equals (Object), e vamos exemplificá-lo aqui com o objeto chamado “Marreta“:

É uma implementação bastante comum e conhecida, não é?

@Override
public boolean equals(Object obj){
    if (obj instanceof Marreta) {
       Marreta other = (Marreta) obj;
        if (model.equals(other.model) && strength == other.strength) {
            return true;
        }
    }
    return false;
}

Agora utilizando o Pattern Matching do instanceof:

@Override
public boolean equals(Object obj){
    return (obj instanceof Marreta other && 
            model.equals(other.model) && 
            strength == other.strength);
}

Bom, como desenvolvedores, costumamos usar o operador instanceof em nosso código, e minha intenção foi apresentar e exemplificar como utiliza-lo atualmente(desde a JDK 14), de forma a melhorar sua produtividade e legibilidade.

Obrigado.

🙂

Como utilizar Optional.stream()

Olá! Tudo bem com vocês? Depois de algum tempo ausente, finalmente estou disponível para continuar escrevendo em meu blog.

Hoje vou tentar demonstrar como deixar seu código mais limpo, ajudando assim na leitura do mesmo e também a vida das pessoas que fazem review do seu código java.

Optional.stream() é uma feature do Java 9, porém mesmo inserido na JDK há tanto tempo, muitos ainda não utilizam-o, ou não em sua complexidade.

Vamos iniciar com a sequência de como seria para computar o valor total de um pedido:

public BigDecimal getOrderPrice(Long orderId) {
    List<OrderItem> items = orderRepository.findByOrderId(orderId);
    BigDecimal price = BigDecimal.ZERO;       
    for (OrderItem orderItem : items) {
        price = price.add(orderItem.getPrice());   
    }
    return price;
}
  1. Primeiro vamos ter uma variável para acumular o preço.
  2. A cada iteração adicionamos valor à nossa variável.

Hoje em dia, é provavelmente mais adequado usar streams em vez de iterações. Esse snippet é equivalente ao anterior:

public BigDecimal getOrderPrice(Long orderId) {
    List<OrderItem> items = orderRepository.findByOrderId(orderId);
    return items.stream()
                .map(OrderItem::getPrice)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
}

Temos um problema, pois a variável orderId pode ser nula.

Bem, a maneira imperativa de lidar com valores nulos é verificá-los no início do método e, se for o caso, lançar uma exceção.

Vamos atualizar nosso código:

public BigDecimal getOrderPrice(Long orderId) {
    if (orderId == null) {
        throw new IllegalArgumentException("Order ID cannot be null.");
    }
    List<OrderItem> items = orderRepository.findByOrderId(orderId);
    return items.stream()
                .map(OrderItem::getPrice)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
}

Agora vamos tentar repeti-lo em uma maneira funcional. Vamos envolver o orderId em um optional.

Esta é a aparência do código utilizando Optional:

public BigDecimal getOrderPrice(Long orderId) {
    return Optional.ofNullable(orderId)                            
            .map(orderRepository::findByOrderId)                   
            .flatMap(items -> {                                    
                BigDecimal sum = items.stream()
                        .map(OrderItem::getPrice)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                return Optional.of(sum);                           
            }).orElse(BigDecimal.ZERO);                            
}
  • Envolva o orderId em um Optional;
  • Encontre os items do pedido;
  • Use flatMap() para obter um Optional<BigDecimal>, nesse caso pode surgir o questionamento, de porque não utilizar um map(), porém o map() obteria um Optional<Optional<BigDecimal>>, e não é isso que queremos.
  • Precisamos colocar o resultado em um Optional para estar em conformidade com a assinatura do método;
  • Se o Optional não contém um valor, a soma é 0.

Dessa forma acredito que o Opcional torna o código até menos legível, que antes…

Mas sempre acreditei no princípio de que a legibilidade deve superar o estilo do código.

E aí que felizmente, Optional oferece um método stream() (desde Java 9) que permite simplificar o pipeline funcional dessa forma:

public BigDecimal getOrderPrice(Long orderId) {
    return Optional.ofNullable(orderId)
            .stream()
            .map(orderRepository::findByOrderId)
            .flatMap(Collection::stream)
            .map(OrderItem::getPrice)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
}

Agora vou explicar linha a linha do pipeline:

Optional.ofNullable(orderId)Optional<Long>
stream()Stream<Long>
map(orderRepository::findByOrderId)Stream<List<OrderLine>>
flatMap(Collection::stream)Stream<OrderLine>
map(OrderItem::getPrice)Stream<BigDecimal>
reduce(BigDecimal.ZERO, BigDecimal::add)BigDecimal

Código funcional não significa necessariamente código legível. Porém com essas mudanças, acredito que sejam as duas coisas.

Espero que passem a utilizar Optional.stream(), ou que melhorem a dinâmica do seu uso.

Obrigado.

🙂

Minha participação no @2devs podcast | Bem vindo 2021

Antes de tudo gostaria de dar as boas vindas ao ano de 2021, desejar a todos que tenham seus sonhos realizados e saúde.

Bom o ano começou muito legal, já teve o primeiro meetup este ano da comunidade SIMBORADEVS, fica aqui o convite para participar do nosso slack e ficar por dentro das ações.

E esse post é para comunicar que já está no ar a primeira parte da minha participação num dos melhores podcasts de desenvolvimento de software do Brasil, o @2devs, foi um bate papo muito interessante e divertido, tentei passar um pouco do meu conhecimento e atualizações do java.

Fica aqui meu agradecimento aos amigos Thiago Ramos e Rashid Calazans, que são os hosts desse podcast, muito grato em participar pela segunda vez. Recomendo que escutem todos os episódios pois vale muito a pena.

Fica aqui o link direto para o episódio e deixem seus comentários.

Forte abraço e espero que gostem!

E em breve mais novidades!

Java 15 – Sealed Classes and Interfaces

Photo by John-Mark Smith on Pexels.com

A novidade que tenho pra vocês me deixou realmente animado, adorei essa nova feature do Java 15, a linguagem está a ficar cada vez mais moderna e divertida!

“Sealed Classes” ou “classes seladas” é o nome da nova feature que veio através da JEP 360, e tem como fundamento definir um design limpo onde você pode restringir a estensão ou implementação de uma classe base.

Como utilizar?

Usando duas novas keywords: sealed e permits.

Exemplos? Claro!

Vamos dizer que esteja criando uma classe base e deseja que ela seja estendida por outros desenvolvedores para reutilizar o código.

Digamos que você desenvolveu uma classe base chamada Animal e deseja permitir que outro desenvolvedor crie uma classe Cat que pode estender essa classe base.

Então, você tornou a classe base Animal pública e qualquer outra classe poderia estendê-la.

E aí veio um desenvolvedor que criou uma classe Eagle e estendeu a sua classe base Animal só porque ele queria reutilizar um dos métodos da classe Animal, digamos um método chamado walk().

Mas você não quer permitir isto! O que você quer é que apenas certas subclasses estendam sua classe base Animal.

Você não pode marcar sua classe como final porque isso restringirá qualquer classe a estendê-la.

Você não pode colocar em um package privado, porque todas as classes dentro do pacote serão capazes de criar uma subclasse de Animal.

Java não permitiu tal recurso por muito tempo e esse recurso surgiu agora no Java 15!

Vamos codar!

package dev.ivanmarreta;
 
public sealed class Animal permits Cat, Dog {

   void walk();
}
package dev.ivanmarreta;
 
public final class Cat extends Animal {
 
}
package dev.ivanmarreta;
 
public final class Dog extends Animal {
 
}

Se a classe Animal permitir que apenas Cat e Dog a estendam, um erro do compilador será lançado quando uma classe Eagle tentar estendê-la.

package dev.ivanmarreta;
 
public class Eagle extends Animal {
 
}
Eagle.java:3:8 error: class is not allowed to extend sealed class: Animal

Modificadores de classe – final vs sealed

final: não pode ser estendido mais.

sealed: só pode ser estendido por suas subclasses permitidas. Dessa forma, podemos restringir ainda mais a subclasse.

non-sealed: pode ser estendido por subclasses desconhecidas.

Quando utilizar?

Não permitirá que nenhuma outra classe o estenda?

Então deve ser final.

Permitirá que apenas certas subclasses o estendam?

Então deve ser sealed.

Permitirá que qualquer número de classes o estenda?

Então deve ser non-sealed.

Mudanças também em java.lang.Class

A Reflections API também foi alterada para oferecer suporte às sealed classes.

Foram adicionados os métodos isSealed() que retorna um valor boolean e permittedSubclasses() que retorna um array do tipo ClassDesc das subclasses que são permitidas a extensão ou implementação.

Exemplo:

package dev.ivanmarreta;

public class AnimalSealedClassExample {
    public static void main(String[] args) {
        System.out.println("Animal is sealed: " + Animal.class.isSealed());
        System.out.println("Animal permittedSubclasses:");
        for (ClassDesc clazzDesc : Animal.class.permittedSubclasses()) {
            System.out.println(clazzDesc.toString());
        }
    }
}

Output:

Animal is sealed: true
Animal permittedSubclasses:
ClassDesc[Cat]
ClassDesc[Dog]

Conclusão

Java está a ficar cada vez mais dinâmico e moderno, e isso também se deve a vários recursos de linguagens JVM mais modernas, como Scala ou Kotlin.

Utilizo diariamente a JDK 11, mas estou animado pra poder usar todas as novas features das JDKs mais novas o mais breve possível. Enquanto isso a JDK 16 já anda em desenvolvimento. õ/

Espero ter animado vocês também!

Obrigado e até mais! 🙂

SynchronizedMap x ConcurrentHashMap – micro benchmark

Photo by Lukas on Pexels.com

Conforme dito previamente, agora vamos criar uma classe para fazer um micro benchmark para exemplificar o que foi explicado sobre SynchronizedMap e ConcurrentHashMap nos posts anteriores.

Quais foram os passos que segui?

1. Criei a classe TestBenchmark;

2. Passei uma implementação diferente como argumento para o método de teste Collections.synchronizedMap(new HashMap()) e ConcurrentHashMap();

3. Criei uma lógica para adicionar(PUT) e recuperar(GET) 600 mil entradas do Map;

4. Calculei a média de tempo em milissegundos para os processamentos;

5. Utilizei um ExecutorService simples para executar 5 threads em paralelo e para cada uma delas repetiremos por 5 vezes as iterações para capturar uma média de tempo.

6. Aproveitei e adicionei o Hashtable, que é a estrutura de dados base, ao teste.

Teste

package dev.ivanmarreta;

import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TestBenchmark {

    private static final int SIX_HUNDRED_THOUSAND = 600_000;
    private static final int THREAD_POOL_SIZE = 5;
    private static final int TIMES_TO_TEST = 5;
    
    public static void main(String[] args) throws InterruptedException {
 
        // Hashtable
        microBenchmarkTest(new Hashtable<String, Integer>());
 
        // Collections.synchronizedMap
        microBenchmarkTest(Collections.synchronizedMap(new HashMap<String, Integer>()));
 
        // ConcurrentHashMap
        microBenchmarkTest(new ConcurrentHashMap<String, Integer>());
 
    }
 
    public static void microBenchmarkTest(final Map<String, Integer> map) throws InterruptedException {
        
        System.out.println("Iniciando o teste de performance para: " + map.getClass());
        
        long averageTime = 0;
        for (int i = 0; i < TIMES_TO_TEST; i++) {
 
            long startTime = System.nanoTime();
            ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
 
            for (int j = 0; j < THREAD_POOL_SIZE; j++) {
                executor.execute(() -> {
                    for (int key = 0; key < SIX_HUNDRED_THOUSAND; key++) {
                        Integer randomValue = (int) Math.ceil(Math.random() * SIX_HUNDRED_THOUSAND);

                        // GET
                        Integer value = map.get(String.valueOf(randomValue));

                        // PUT 
                        map.put(String.valueOf(randomValue), randomValue);
                    }
                });
            }

            executor.shutdown();
            executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.MINUTES);
 
            long totalTime = (System.nanoTime() - startTime) / 1000000L;
            averageTime += totalTime;
            System.out.println("600 mil registros foram adicionados(PUT)/recuperados(GET) em " + totalTime + " ms");
        }
        System.out.println("Média de tempo de execução para a implementação " + map.getClass() + ": " + averageTime / TIMES_TO_TEST + " ms \n");
    }
    
}

Nas linhas 56 e 57, chamo shutdown() para o executor service não esperar mais tasks e awaitTermination() para bloquear as threads até que as tasks finalizem.

Resultado do teste:

Máquina de teste: Macbook Pro 2.6GHz quad-core Intel Core i7 16GB

Conclusão:

Conforme conversamos sobre SynchronizedMap e ConcurrentHashMap, os resultados obtidos refletem a forma de acesso aos objetos simultaneamente.

Portanto a estrátegia de lock do objeto ou de segmentos do objeto é bastante relevante, e parece-me ficar bem exemplificada.

Obrigado e até breve. 🙂

HashMap pode ser sincronizado em Java?

HashMap é uma estrutura de dados muito poderosa em Java e nós a usamos todos os dias e em quase todos os aplicativos.

Como devem saber HashMap é uma classe de coleção não sincronizada, caso não saibam, a novidade é saber que SIM, HashMap pode ser sincronizado.

Neste tutorial, vamos tentar entender “Por Quê” e “Como” podemos sincronizar o Hashmap!

Por quê?

O Map é uma estrutura de dados que armazena elementos, formado por uma combinação de uma chave de identificação exclusiva e um valor atribuído mapeado. Se você tiver uma aplicação altamente concorrente no qual deseja modificar ou ler o valor da chave em diferentes threads, digo-te logo que o ideal usar a implementação ConcurrentHashMap. O melhor exemplo é o Producer/Consumer, que faz com leitura/gravação concorrente.

Então, o que significa o Map ser thread-safe?

Significa que se vários encadeamentos acessam um HashMap em concorrência e pelo menos um dos encadeamentos modifica o Map estruturalmente, ele deve ser sincronizado externamente para evitar uma visualização inconsistente do conteúdo. Tão isso quanto isso.

Como?

Vamos lá! Existem duas maneiras de sincronizar o HashMap:
  1. Usar ConcurrentHashMap (Que já aprendemos a utilizar no post anterior)
  2. Java Collections – método synchronizedMap()
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());

SynchronizedHashMap

  • Sincronização a nível do objeto.
  • Cada operação de leitura/gravação precisa adquirir lock.
  • Fazer lock a coleção inteira é uma sobrecarga de desempenho.
  • Essencialmente dá acesso a apenas uma thread para todo o Map e bloqueia todos as outras threads.
  • Pode causar contenção.
  • SynchronizedHashMap retorna um Iterator, que falha rapidamente se utilizado em concorrêcia.

No próximo post pretendo então exemplificar, explicar e fazer um benchmark entre SynchronizedHashMap e ConcurrentHashMap.

Obrigado e até lá. 🙂

ConcurrentHashMap

ConcurrentHashMap é uma melhoria do HashMap, pois sabemos que, ao lidar com Threads, o HashMap não é uma boa escolha porque o em termos de desempenho deixa muito a desejar.

A classe ConcurrentHashMap é thread-safe, ou seja, várias threads podem operar em um único objeto sem complicações. Ao mesmo tempo, qualquer número de encadeamentos é aplicável para uma operação de leitura sem bloquear o objeto ConcurrentHashMap que não existe no HashMap.

A estrutura de dados que o ConcurrentHashMap utiliza é a HashTable.

No ConcurrentHashMap, o objeto é dividido em vários segmentos de acordo com o concurrency-level, e como default o concurrency-level do ConcurrentHashMap é 16.


No ConcurrentHashMap, a qualquer momento mais que uma thread pode executar operação de recuperação de valor, só que para fazer update no objeto, a thread deve bloquear esse segmento específico que a thread deseja operar. Este tipo de mecanismo de bloqueio é conhecido como Segment locking or bucket locking. Consequentemente, 16 operações de update podem ser realizadas por threads.

A inserção de objetos nulos não é possível em ConcurrentHashMap como chave ou valor. Diferentemente da implementação mais utilizada, a HashMap.

public class ConcurrentHashMap<K,​V> extends AbstractMap<K,​V> implements ConcurrentMap<K,​V>, Serializable

Onde K é a chave do objeto e V é o valor do objeto.

Inicializando um ConcurrentHashMap

Podemos utilizar um dos 5 contrutores para isso:

//Cria um novo map vazio com valores default: initial capacity (16), load factor (0.75) e concurrencyLevel (16).
new ConcurrentHashMap<>();

//Cria um novo map vazio com: initial capacity que foi definida, e com default load factor (0.75) e concurrencyLevel (16).
new ConcurrentHashMap<>(int initialCapacity);

//Cria um novo map vazio com: initial capacity e load factor que foram definidos, e com concurrencyLevel default (16).
new ConcurrentHashMap<>(int initialCapacity, float loadFactor);

//Cria um novo map vazio com: initial capacity, load factor e concurrencyLevel que foram definidos.
new ConcurrentHashMap<>(int initialCapacity, float loadFactor, int concurrencyLevel);

//Cria um novo map com os mesmos atributos do map passado como parâmetro.
new ConcurrentHashMap<>(Map m);

Por quê?

Quando precisar de alta simultaneidade em seu projeto.

É thread-safe sem sincronizar todo o mapa. (apenas os segmentos, lembram?)

As leituras podem acontecer muito rapidamente enquanto a gravação é feita com um bloqueio.

Não há bloqueio no nível do objeto.

O bloqueio tem uma granularidade muito mais fina no nível do depósito de hashmap.

Não lança uma ConcurrentModificationException se uma thread tenta modificá-lo enquanto outra está iterando sobre ele.

ConcurrentHashMap usa uma infinidade de bloqueios.


🙂