O foco deste artigo está nas questões de desempenho de aplicativos multithread. Primeiro definiremos o desempenho e a escalabilidade e depois estudaremos cuidadosamente a regra de Amdahl. No conteúdo a seguir, examinaremos como usar diferentes métodos técnicos para reduzir a concorrência de bloqueio e como implementá -lo com o código.
1. Desempenho
Todos sabemos que o multithreading pode ser usado para melhorar o desempenho do programa, e a razão por trás disso é que temos CPUs com vários núcleos ou várias CPUs. Cada núcleo da CPU pode concluir as tarefas por si só, portanto, dividir uma grande tarefa em uma série de pequenas tarefas que podem ser executadas independentemente uma da outra pode melhorar o desempenho geral do programa. Você pode dar um exemplo. Por exemplo, existe um programa que altera o tamanho de todas as imagens em uma pasta no disco rígido, e a aplicação da tecnologia de threading múltipla pode melhorar seu desempenho. O uso de uma única abordagem rosqueada pode atravessar apenas todos os arquivos de imagem em sequência e executar modificações. Se nossa CPU tiver vários núcleos, não há dúvida de que só pode usar um deles. Usando o multi-threading, podemos ter um thread de produtor digitalizar o sistema de arquivos para adicionar cada imagem a uma fila e, em seguida, usar vários threads de trabalhadores para executar essas tarefas. Se o número de threads do trabalhador for o mesmo que o número total de núcleos da CPU, podemos garantir que cada núcleo da CPU tenha trabalho a fazer até que todas as tarefas sejam executadas.
Para outro programa que requer mais espera de IO, o desempenho geral também pode ser aprimorado usando a tecnologia de múltiplas threading. Suponha que queremos escrever um programa que precisamos rastejar todos os arquivos HTML de um determinado site e armazená -los no disco local. O programa pode iniciar a partir de uma determinada página da web, analisar todos os links para este site nesta página da web e, em seguida, rasteja esses links, por sua vez, para que se rependa. Como leva um tempo para esperar desde o momento em que iniciamos uma solicitação ao site remoto até o tempo que recebemos todos os dados da página da web, podemos entregar essa tarefa a vários threads para execução. Deixe um ou um pouco mais de thread analisar a página HTML recebida e coloque o link encontrado na fila, deixando todos os outros threads responsáveis por solicitar a página. Ao contrário do exemplo anterior, neste exemplo, você ainda pode obter melhorias de desempenho, mesmo que use mais threads do que o número de núcleos da CPU.
Os dois exemplos acima nos dizem que o alto desempenho é fazer o maior número possível de coisas em um curto período de tempo. É claro que essa é a explicação mais clássica do termo desempenho. Mas, ao mesmo tempo, o uso de threads também pode melhorar bem a velocidade de resposta de nossos programas. Imagine que temos um aplicativo de interface gráfico, com uma caixa de entrada acima e um botão chamado "Processo" abaixo da caixa de entrada. Quando o usuário pressiona esse botão, o aplicativo precisa renderizar o status do botão (o botão parece ser pressionado e retorna ao seu estado original quando o botão do mouse esquerdo é liberado) e comece a processar a entrada do usuário. Se essa tarefa for demorada para processar a entrada do usuário, um programa de thread único não poderá continuar respondendo a outras ações de entrada do usuário, como o usuário clicando no evento do mouse ou o ponteiro do mouse que move o evento transmitido do sistema operacional, etc. As respostas a esses eventos precisam ser um thread independente para responder.
A escalabilidade significa que os programas têm a capacidade de obter maior desempenho adicionando recursos de computação. Imagine que precisamos ajustar o tamanho de muitas imagens, porque o número de núcleos de CPU da nossa máquina é limitado, aumentar o número de encadeamentos nem sempre melhora o desempenho de acordo. Pelo contrário, como o agendador precisa ser responsável pela criação e desligamento de mais threads, também ocupará recursos da CPU, o que pode reduzir o desempenho.
1.1 Regra de Amdahl
O parágrafo anterior mencionou que, em alguns casos, adicionar recursos adicionais de computação pode melhorar o desempenho geral do programa. Para calcular quanta melhoria de desempenho podemos obter quando adicionamos recursos adicionais, é necessário verificar quais partes do programa são executadas em série (ou de síncrona) e quais peças são executadas em paralelo. Se quantificarmos a proporção de código que precisarmos ser executados de maneira síncrona em B (por exemplo, o número de linhas de código que precisam ser executadas de maneira síncrona) e registrará o número total de núcleos da CPU como n, então, de acordo com a lei de Amdahl, o limite superior de melhorias de desempenho que podemos obter é:
Se n tende ao infinito, (1-b)/n converge para 0. Portanto, podemos ignorar o valor dessa expressão; portanto, a contagem de bits de melhoria de desempenho converge para 1/b, onde B representa a proporção de código que deve ser executada de maneira síncrona. Se B for igual a 0,5, significa que metade do código do programa não pode ser executado em paralelo e o recíproco de 0,5 é 2, portanto, mesmo se adicionarmos inúmeros núcleos de CPU, obtemos um máximo de melhoria de desempenho 2x. Suponha que modificamos o programa agora e, após a modificação, apenas o código 0,25 deve ser executado de maneira síncrona. Agora 1/0,25 = 4 significa que, se nosso programa executar em hardware com um grande número de CPUs, será cerca de 4 vezes mais rápido do que em hardware único.
Por outro lado, através da Lei da Amdahl, também podemos calcular a proporção do código de sincronização que o programa deve se basear no alvo de aceleração que queremos obter. Se quisermos obter 100 vezes a aceleração e 1/100 = 0,01 significa que o número máximo de código que nosso programa executa de forma síncrona não pode exceder 1%.
Para resumir a lei da Amdahl, podemos ver que a melhoria máxima de desempenho que obtemos ao adicionar uma CPU extra depende de quão pequena a proporção do programa executa parte do código de maneira síncrona. Embora, na realidade, nem sempre seja fácil calcular essa proporção, sem falar em enfrentar algumas grandes aplicações de sistemas comerciais, a lei da Amdahl nos dá uma inspiração importante, ou seja, devemos considerar o código que deve ser executado de forma síncrona e tentar reduzir essa parte do código.
1.2 Efeito no desempenho
Como o artigo escreve aqui, afirmamos que adicionar mais threads pode melhorar o desempenho e a capacidade de resposta do programa. Mas, por outro lado, não é fácil alcançar esses benefícios e também requer algum preço. O uso de threads também afetará a melhoria do desempenho.
Primeiro, o primeiro impacto vem da época da criação de threads. Durante a criação de threads, a JVM precisa solicitar recursos correspondentes do sistema operacional subjacente e inicializar a estrutura de dados no agendador para determinar a ordem dos threads de execução.
Se o seu número de threads for o mesmo que o número de núcleos da CPU, cada encadeamento será executado em um núcleo para que eles não sejam interrompidos com frequência. Mas, de fato, quando o seu programa estiver em execução, o sistema operacional também terá algumas de suas próprias operações que precisam ser processadas pela CPU. Portanto, mesmo nesse caso, seu thread será interrompido e aguarde o sistema operacional retomar sua operação. Quando a contagem de threads excede o número de núcleos da CPU, a situação pode piorar. Nesse caso, o agendador de processos da JVM interromperá determinados threads para permitir que outros threads sejam executados. Quando os threads são alterados, o estado atual do encadeamento em execução precisa ser salvo para que o estado de dados possa ser restaurado na próxima vez que for executado. Além disso, o agendador também atualizará sua própria estrutura de dados interna, que também requer ciclos de CPU. Tudo isso significa que a troca de contexto entre os threads consome recursos de computação da CPU, trazendo assim o desempenho de desempenho em comparação com o de um único caso de rosca.
Outra despesas gerais trazidas por programas multithread vem da proteção de acesso síncrono de dados compartilhados. Podemos usar a palavra -chave sincronizada para proteção de sincronização, ou podemos usar a palavra -chave volátil para compartilhar dados entre vários threads. Se mais de um thread quiser acessar uma estrutura de dados compartilhada, ocorrerá uma contenção. No momento, a JVM precisa decidir qual processo é o primeiro e qual processo está atrasado. Se o thread a ser executado não for o thread atualmente em execução, ocorre uma comutação de thread. O thread atual precisa esperar até adquirir com sucesso o objeto de bloqueio. A JVM pode decidir como executar isso "Wait". Se a JVM espera ser mais curta para adquirir com êxito o objeto bloqueado, a JVM poderá usar métodos de espera agressivos, como tentar constantemente adquirir o objeto bloqueado até que seja bem -sucedido. Nesse caso, esse método pode ser mais eficiente, porque ainda é mais rápido comparar a troca de contexto do processo. Mover um tópico de espera de volta à fila de execução também trará uma sobrecarga adicional.
Portanto, devemos tentar o nosso melhor para evitar a troca de contexto causada pela concorrência de bloqueio. A seção a seguir explicará duas maneiras de reduzir a ocorrência de tal concorrência.
1.3 Competição de bloqueio
Conforme mencionado na seção anterior, o acesso concorrente ao bloqueio por dois ou mais threads trará uma sobrecarga computacional adicional porque a concorrência ocorre para forçar o agendador a entrar em um estado de espera agressivo ou a deixá -lo executar um estado de espera, causando dois interruptores de contexto. Existem alguns casos em que as consequências da concorrência de bloqueio podem ser atenuadas por:
1. Reduza o escopo das fechaduras;
2. Reduza a frequência de bloqueios que precisam ser adquiridos;
3. Tente usar operações otimistas de bloqueio suportadas pelo hardware em vez de sincronizadas;
4. Tente usar sincronizado o mínimo possível;
5. Reduza o uso de cache de objeto
1.3.1 Reduzindo o domínio de sincronização
Se o código mantiver o bloqueio por mais do que o necessário, esse primeiro método poderá ser aplicado. Geralmente, podemos mover uma ou mais linhas de código para fora da área de sincronização para reduzir o tempo em que o encadeamento atual mantém a trava. Quanto menos o código é executado na área de sincronização, mais cedo o thread atual liberará o bloqueio, permitindo que outros threads adquiram o bloqueio anteriormente. Isso é consistente com a lei da Amdahl, porque isso reduz isso a quantidade de código que precisa ser executada de maneira síncrona.
Para uma melhor compreensão, consulte o seguinte código -fonte:
classe pública ReducelockDuration implementa Runnable {private estático final int number_of_threads = 5; mapa final estático privado <string, número inteiro> map = new hashmap <string, inteiro> (); public void run () {for (int i = 0; i <10000; i ++) {sincronizado (map) {uuid randomuuid = uuid.randomuuid (); Valor inteiro = Integer.ValueOf (42); String key = randomuuid.toString (); map.put (chave, valor); } Thread.yield (); }} public static void main (string [] args) lança interruptedException {thread [] threads = new Thread [number_of_threads]; for (int i = 0; i <número_of_threads; i ++) {threads [i] = new Thread (new ReducelockDuration ()); } long startMillis = system.currenttimemillis (); for (int i = 0; i <número_of_threads; i ++) {threads [i] .start (); } para (int i = 0; i <número_of_threads; i ++) {threads [i] .Join (); } System.out.println ((System.currenttimemillis ()-startMillis)+"ms"); }}No exemplo acima, deixamos cinco threads competirem para acessar a instância do mapa compartilhado. Para apenas um thread pode acessar a instância do mapa ao mesmo tempo, colocamos a operação de adicionar chave/valor ao mapa no bloco de código protegido sincronizado. Quando analisamos cuidadosamente esse código, podemos ver que as poucas frases de código que calculam a chave e o valor não precisam ser executadas de maneira síncrona. A chave e o valor pertencem apenas ao thread que atualmente executa este código. É significativo apenas para o encadeamento atual e não será modificado por outros threads. Portanto, podemos tirar essas frases da proteção de sincronização. do seguinte modo:
public void run () {for (int i = 0; i <10000; i ++) {uuid randomUuid = uuid.randomuuid (); Valor inteiro = Integer.ValueOf (42); String key = randomuuid.toString (); sincronizado (map) {map.put (chave, valor); } Thread.yield (); }}O efeito de reduzir o código de sincronização é mensurável. Na minha máquina, o tempo de execução de todo o programa foi reduzido de 420ms para 370ms. Dê uma olhada, apenas movendo três linhas de código para fora do bloco de proteção de sincronização pode reduzir o tempo de execução do programa em 11%. O código Thread.yield () é induzir a comutação de contexto do encadeamento, porque esse código informará à JVM que o encadeamento atual deseja entregar os recursos de computação atualmente usados para que outros threads que aguardam para executar possam ser executados. Isso também levará a mais concorrência de bloqueio, porque se esse não for o caso, um thread ocupará um certo núcleo por mais tempo, reduzindo assim a troca de contexto do encadeamento.
1.3.2 Lock Split
Outra maneira de reduzir a concorrência de bloqueio é espalhar um bloco de código protegido por bloqueio em vários blocos de proteção menores. Este método funcionará se você usar um bloqueio no seu programa para proteger vários objetos diferentes. Suponha que queremos contar alguns dados por meio de um programa e implementar uma classe de contagem simples para manter vários indicadores estatísticos diferentes e representá -los com uma variável de contagem básica (tipo longo). Como nosso programa é multi-thread, precisamos proteger síncrono as operações que acessam essas variáveis, porque essas ações vêm de diferentes threads. A maneira mais fácil de conseguir isso é adicionar a palavra -chave sincronizada a cada função que acessa essas variáveis.
Classe estática public estática contra -implementos de implementos {private Long CustomerCount = 0; Shipping de longa longa privado = 0; public sincronizado void incrementCustomer () {CustomerCount ++; } public sincronizado void incrementShipping () {fretCount ++; } public sincronizada longa getCustomerCount () {return CustomerCount; } public sincronizado longo getShippingCount () {return ShippingCount; }}Isso significa que cada modificação dessas variáveis causará o bloqueio de outras instâncias contra. Se outros threads quiserem chamar o método de incremento em outra variável diferente, eles só poderão aguardar que o thread anterior libere o controle de bloqueio antes de terem a chance de concluí -lo. Nesse caso, o uso de uma proteção sincronizada separada para cada variável diferente melhorará a eficiência da execução.
Classe estática pública contraparatelock implementa o contador {private static final objeto clientelock = new Object (); private estático final objeto fretelock = new objeto (); Private Long CustomerCount = 0; Shipping de longa longa privado = 0; public void incrementCustomer () {sincronizado (CustomerLock) {CustomerCount ++; }} public void incrementShipping () {sincronizado (fretetLock) {ShippingCount ++; }} public Long GetCustomerCount () {Synchronized (CustomerLock) {return CustomerCount; }} public Long GetShippingCount () {Synchronized (ShippingLock) {return ShippingCount; }}}Esta implementação apresenta um objeto sincronizado separado para cada métrica de contagem. Portanto, quando um thread deseja aumentar a contagem de clientes, ele deve aguardar outro thread que está aumentando a contagem de clientes para concluir, em vez de esperar por outro thread que esteja aumentando a contagem de remessa para concluir.
Usando as classes a seguir, podemos calcular facilmente as melhorias de desempenho trazidas por bloqueios divididos.
classe pública LocksLitting implementa runnable {private estático final int number_of_threads = 5; contador privado; contador de interface pública {void incrementCustomer (); vazio incrementShipping (); long getCustomerCount (); long getShippingCount (); } classe estática public estática contraonellock implementa o contador {...} public static class Static Counterseparatelock implementa o contador {...} public Locksplitting (contador contador) {this.counter = contador; } public void run () {for (int i = 0; i <100000; i ++) {if (threadlocalrandom.current (). nextBoolean ()) {contat.incrementCustomer (); } else {contat.incrementShipping (); }}} public static void main (string [] args) lança interruptedException {thread [] threads = new Thread [number_of_threads]; Contador de contador = new contraonelock (); for (int i = 0; i <número_of_threads; i ++) {threads [i] = new Thread (new Locksplitting (contador)); } long startMillis = system.currenttimemillis (); for (int i = 0; i <número_of_threads; i ++) {threads [i] .start (); } para (int i = 0; i <número_of_threads; i ++) {threads [i] .Join (); } System.out.println ((System.currenttimemillis () - startMillis) + "ms"); }}Na minha máquina, o método de implementação de um único bloqueio leva uma média de 56ms e a implementação de dois bloqueios separados é de 38ms. O tempo demorado é reduzido em cerca de 32%.
Outra maneira de melhorar é que podemos ir além para proteger a leitura e a gravação com diferentes bloqueios. A classe de contador original fornece métodos para leitura e escrita de indicadores de contagem, respectivamente. No entanto, de fato, as operações de leitura não requerem proteção de sincronização. Podemos ter certeza de que vários threads podem ler o valor do indicador atual em paralelo. Ao mesmo tempo, as operações de gravação devem ser protegidas de maneira síncrona. O pacote java.util.Concurrent fornece uma implementação da interface ReadWritelock, que pode facilmente obter essa distinção.
A implementação do ReentrantreadWritelock mantém dois bloqueios diferentes, um protege a operação de leitura e o outro protege a operação de gravação. Ambos os bloqueios têm operações para adquirir e liberar bloqueios. Um bloqueio de gravação só pode ser obtido com sucesso quando ninguém adquire um bloqueio de leitura. Por outro lado, desde que o bloqueio de gravação não seja adquirido, o bloqueio de leitura pode ser adquirido por vários threads ao mesmo tempo. Para demonstrar essa abordagem, a seguinte classe de contador usa o ReadWritelock, como segue:
Classe estática pública CounterreadWritelock implementa contador {private final reentrantreadWritelock CustomerLock = new ReentrantreadWritelock (); Clientewritelock de bloqueio final privado = CustomerLock.Writelock (); private final Lock CustomerReadLock = CustomerLock.readlock (); PRIVADO FINAL REENTRANTREADWRITELOCK REVENDLOCK = NOVA REENTRANTREADWRITELOCK (); Final de trava final privado ShipperWritelock = ShippingLock.Writelock (); Final de bloqueio privado ShippingLock = ShippingLock.readlock (); Private Long CustomerCount = 0; Shipping de longa longa privado = 0; public void incrementCustomer () {CustomerWritelock.lock (); CustomerCount ++; CustomerWritelock.unlock (); } public void incrementShipping () {ShipperWritelock.lock (); ShippingCount ++; ShipperWritelock.unlock (); } public Long GetCustomerCount () {CustomerReadlock.lock (); long count = customerCount; clientereadlock.unlock (); contagem de retorno; } public Long GetShippingCount () {ShippingReadlock.lock (); long count = ShippingCount; ShippingReadlock.unlock (); contagem de retorno; }}Todas as operações de leitura são protegidas por bloqueios de leitura e todas as operações de gravação são protegidas por bloqueios de gravação. Se as operações de leitura realizadas no programa forem muito maiores que as operações de gravação, essa implementação poderá trazer maiores melhorias de desempenho do que a seção anterior, porque as operações de leitura podem ser executadas simultaneamente.
1.3.3 Bloqueio de separação
O exemplo acima mostra como separar um único bloqueio em vários bloqueios separados, para que cada encadeamento possa apenas obter o bloqueio do objeto que eles estão prestes a modificar. Mas, por outro lado, esse método também aumenta a complexidade do programa e pode causar impasse se implementado de forma inadequada.
Uma trava de destacamento é um método semelhante a uma trava de destacamento, mas uma trava de destacamento é adicionar uma trava para proteger diferentes trechos ou objetos de código, enquanto uma trava de destacamento deve usar uma trava diferente para proteger diferentes faixas de valores. Concurrenthashmap no pacote java.util.util.Concurrent usa essa ideia para melhorar o desempenho dos programas que dependem fortemente no hashmap. Em termos de implementação, o concorrente usa 16 bloqueios diferentes internamente, em vez de encapsular um hashmap de forma síncrona protegida. Cada um dos 16 bloqueios é responsável por proteger o acesso síncrono a um décimo dos bits do balde (baldes). Dessa forma, quando threads diferentes desejam inserir teclas em diferentes segmentos, as operações correspondentes serão protegidas por bloqueios diferentes. Mas também trará alguns problemas ruins, como a conclusão de certas operações agora requer vários bloqueios em vez de uma trava. Se você deseja copiar o mapa inteiro, todos os 16 bloqueios precisam ser obtidos para concluir.
1.3.4 Operação atômica
Outra maneira de reduzir a concorrência de bloqueio é usar operações atômicas, que elaborarão os princípios em outros artigos. O pacote java.util.Concurrent fornece classes atomicamente encapsuladas para alguns tipos de dados básicos comumente usados. A implementação da classe de operação atômica é baseada na função "Permutação de comparação" (CAS) fornecida pelo processador. A operação CAS executará apenas uma operação de atualização quando o valor do registro atual for o mesmo que o valor antigo fornecido pela operação.
Esse princípio pode ser usado para aumentar o valor de uma variável de maneira otimista. Se nosso thread souber o valor atual, ele tentará usar a operação CAS para executar a operação de incremento. Se outros encadeamentos modificaram o valor da variável durante esse período, o chamado valor de corrente fornecido pelo encadeamento será diferente do valor real. Neste momento, a JVM tenta recuperar o valor atual e tentar novamente, repetindo -o novamente até que seja bem -sucedido. Embora as operações de loops desperdiçam alguns ciclos da CPU, o benefício de fazer isso é que não precisamos de nenhuma forma de controle de sincronização.
A implementação da classe do contador abaixo usa operações atômicas. Como você pode ver, não há código sincronizado usado.
Classe estática pública Implementos contraatômicos Contador {private atomiclong CustomerCount = new Atomiclong (); Shippount privado atômico = new Atomiclong (); public void incrementCustomer () {CustomerCount.Incremendget (); } public void incrementShipping () {ShippoundCount.Incremendget (); } public Long getCustomerCount () {return personalizerCount.get (); } public Long GetShippingCount () {return ShippingCount.get (); }}Comparado com a classe Counterseparatelock, o tempo médio de execução foi reduzido de 39ms para 16ms, o que é cerca de 58%.
1.3.5 Evite segmentos de código de hotspot
Uma implementação de lista típica registra o número de elementos contidos na própria lista, mantendo uma variável no conteúdo. Toda vez que um elemento é excluído ou adicionado da lista, o valor dessa variável muda. Se a lista for usada em um aplicativo de thread único, esse método será compreensível. Cada vez que você chama tamanho (), você pode retornar o valor após o último cálculo. Se essa variável de contagem não for mantida internamente por lista, cada chamada para tamanho () fará com que a lista seja transferida e calcule o número de elementos.
Esse método de otimização usado por muitas estruturas de dados se tornará um problema quando estiver em um ambiente multithread. Suponha que compartilhemos uma lista entre vários threads e vários threads adicionem ou excluam elementos na lista e consulte o grande comprimento. No momento, a variável de contagem lista interna se torna um recurso compartilhado; portanto, todo o acesso a ela deve ser processado de maneira síncrona. Portanto, as variáveis de contagem se tornam um ponto quente em toda a implementação da lista.
O snippet de código a seguir mostra esse problema:
Classe estática pública CarrepositorywithCounter implementa Carrepository {Map privado <String, Cars> cars = new Hashmap <String, Car> (); mapa privado <string, carro> caminhões = novo hashmap <string, car> (); Objeto privado carcountSync = new Object (); private int carcount = 0; public void addcar (carro) {if (car.getlicenceplate (). startSwith ("c")) {sincronizado (cars) {car foundcar = car.get (car.getlicenceplate ()); if (foundcar == null) {cars.put (car.getlicenceplate (), carro); sincronizado (carcountSync) {carcount ++; }}}} else {sincronizado (caminhões) {car foundcar = caminhão.get (car.getLicenceplate ()); if (foundcar == null) {caminhão.put (car.getLicenceplate (), carro); sincronizado (carcountSync) {carcount ++; }}}}}} public int getCarcount () {sincronizado (carcountSync) {return carcount; }}}A implementação acima do Carrepositório possui duas variáveis de lista dentro, uma é usada para colocar o elemento de lavagem do carro e o outro é usado para colocar o elemento do caminhão. Ao mesmo tempo, fornece um método para consultar o tamanho total dessas duas listas. O método de otimização utilizado é que sempre que um elemento de carro é adicionado, o valor da variável de contagem interna será aumentado. Ao mesmo tempo, a operação incrementada é protegida por sincronizada e o mesmo se aplica ao retorno do valor da contagem.
Para evitar essa sobrecarga adicional de sincronização de código, consulte outra implementação do Carrepositório abaixo: ele não usa mais uma variável de contagem interna, mas conta esse valor em tempo real no método de retornar o número total de carros. do seguinte modo:
Classe estática pública CarrepositorywithoutCounter implementa Carrepository {Map privado <String, Cars> cars = new Hashmap <String, Car> (); mapa privado <string, carro> caminhões = novo hashmap <string, car> (); public void addcar (carro) {if (car.getlicenceplate (). startSwith ("c")) {sincronizado (cars) {car foundcar = car.get (car.getlicenceplate ()); if (foundcar == null) {cars.put (car.getlicenceplate (), carro); }}} else {synchronized (caminhões) {car foundcar = caminhão.get (car.getlicenceplate ()); if (foundcar == null) {caminhão.put (car.getLicenceplate (), carro); }}}}} public int getCarcount () {Synchronized (cars) {sincronizado (caminhões) {return cars.size () + caminhão.size (); }}}}Agora, apenas no método getCarcount (), o acesso das duas listas precisa de proteção de sincronização. Como a implementação anterior, a sobrecarga de sincronização toda vez que um novo elemento não existe mais.
1.3.6 Evite reutilização de cache de objetos
Na primeira versão da Java VM, a sobrecarga de usar a nova palavra -chave para criar novos objetos é relativamente alta, muitos desenvolvedores estão acostumados a usar o modo de reutilização de objetos. Para evitar a criação repetida de objetos repetidamente, os desenvolvedores mantêm um pool de buffer. Após cada criação de instâncias de objetos, eles podem ser salvos no pool de buffer. Da próxima vez que outros threads precisarem usá -los, eles podem ser recuperados diretamente do pool de buffer.
À primeira vista, esse método é muito razoável, mas esse padrão pode causar problemas em aplicativos multithread. Como o pool de objetos de buffer é compartilhado entre vários threads, todas as operações de todos os threads ao acessar objetos neles precisam de proteção síncrona. A sobrecarga dessa sincronização é maior que a criação do próprio objeto. Obviamente, a criação de muitos objetos aumentará o ônus da coleta de lixo, mas mesmo levando isso em consideração, ainda é melhor evitar as melhorias de desempenho trazidas ao sincronizar o código do que usar o pool de cache de objetos.
Os esquemas de otimização descritos neste artigo mostram mais uma vez que cada método de otimização possível deve ser cuidadosamente avaliado quando realmente é aplicado. Uma solução de otimização imatura parece fazer sentido na superfície, mas, na verdade, é provável que se torne um gargalo de desempenho.