resumo
Esta série é baseada no curso de refinar os números em ouro e, para aprender melhor, uma série de registros foi feita. Este artigo apresenta principalmente: 1. Ideias e métodos de otimização de bloqueio 2. Otimização de bloqueio na máquina virtual 3. Um caso de uso errado de bloqueios 4. Threadlocal e sua análise de código -fonte
1. Ideias e métodos de otimização de bloqueio
O nível de simultaneidade é mencionado na introdução ao [alta concorrência Java 1].
Depois que uma trava é usada, isso significa que isso está bloqueando, portanto, a simultaneidade é geralmente um pouco menor que a situação sem trava.
A otimização da trava mencionada aqui refere -se a como impedir que o desempenho se torne muito pobre no caso de bloqueio. Mas não importa como você o otimize, o desempenho geralmente será um pouco pior do que a situação sem trava.
Deve-se notar aqui que o Trylock em Reentrantlock mencionado em [Pacote de Concorrência JDK de alta concorrência JDK 1 tende a ser um método sem trava, porque não se pendurará quando os juízes de Trylock.
Para resumir as idéias e métodos de otimização de bloqueio, existem os seguintes tipos.
1.1 Reduza o tempo de retenção de bloqueio
public sincronizado void syncmethod () {othercode1 (); mutextMethod (); outrocode2 (); } Como o código acima, você precisa obter o bloqueio antes de entrar no método, e outros threads precisam esperar do lado de fora.
O ponto de otimização aqui é reduzir o tempo de espera de outros threads, para que seja usado apenas para adicionar bloqueios aos programas com os requisitos de segurança de threads.
public void syncmethod () {Othercode1 (); sincronizado (this) {mutextMethod (); } outrocode2 (); }1.2 Reduza o tamanho das partículas de travamento
Divida objetos grandes (esse objeto pode ser acessado por muitos threads) em objetos pequenos, aumentando bastante o paralelismo e reduzindo a concorrência de bloqueio. Somente reduzindo a concorrência por bloqueios e desvio em direção a bloqueios, a taxa de sucesso de bloqueios leves será melhorada.
O caso mais típico de redução da granularidade de trava é simultaneamente. Isso é mencionado em [High Concurrency Java V] JDK Concorrência Pacote 1.
1.3 Separação de bloqueio
A separação de bloqueio mais comum é o ReadWritelock de trava de leitura-gravação, que é separado em travas de leitura e gravação de acordo com a função. Dessa forma, a leitura e a leitura não são mutuamente exclusivas, a leitura e a escrita são mutuamente exclusivas, o que garante a segurança do thread e melhora o desempenho. Para obter detalhes, verifique [High Concurrency Java V] JDK Concorrência Pacote 1.
A idéia de separação de leitura e escrita pode ser estendida e, desde que as operações não se afetem, a trava poderá ser separada.
Por exemplo, LinkedBlockingQueue
Retire -o da cabeça e coloque os dados da cauda. É claro que também é semelhante ao roubo de trabalho em Forkjoinpool mencionado no pacote de concorrência JDK de alta concorrência JDK.
1.4 Rougelim de trava
De um modo geral, para garantir uma simultaneidade eficaz entre vários threads, cada encadeamento será necessário para manter o bloqueio o mais curto possível, ou seja, o bloqueio deve ser liberado imediatamente após o uso de recursos públicos. Somente dessa maneira outros threads aguardam esse bloqueio obter recursos para executar tarefas o mais rápido possível. No entanto, tudo tem um diploma. Se o mesmo bloqueio for constantemente solicitado, sincronizado e liberado, ele consumirá recursos valiosos do sistema, que não são propícios à otimização do desempenho.
Por exemplo:
public void Demomethod () {sincronizado (Lock) {// do sth. } // Faça outro trabalho de sincronização indesejado, mas pode ser executado rapidamente sincronizado (bloqueio) {// do sth. }} Nesse caso, de acordo com a idéia de bloqueio de bloqueio, deve ser mesclado
public void DemOMETHOD () {// Integrar em uma solicitação de bloqueio sincronizada (Lock) {// do sth. // Faça outro trabalho de sincronização indesejada, mas pode ser executado rapidamente}} Obviamente, há um pré -requisito, o trabalho no meio que não requer sincronização será executado rapidamente.
Deixe -me dar outro exemplo extremo:
for (int i = 0; i <círculo; i ++) {sincronizado (bloqueio) {}} Os bloqueios devem ser obtidos em um loop. Embora o JDK otimize este código internamente, é melhor escrevê -lo diretamente
sincronizado (bloqueio) {for (int i = 0; i <círculo; i ++) {}} Obviamente, se houver necessidade de dizer que esse loop é muito longo e você precisa dar a outros tópicos para não esperar muito tempo, então você só pode escrevê -lo como acima. Se não houver requisitos semelhantes, é melhor escrevê -lo diretamente no seguinte.
1.5 Eliminação de bloqueio
A eliminação de bloqueio é uma coisa do nível do compilador.
No compilador instantâneo, se objetos que não forem compartilhados forem encontrados, a operação de bloqueio desses objetos pode ser eliminada.
Talvez você ache estranho que, como alguns objetos não possam ser acessados por vários threads, por que devo adicionar bloqueios? Não seria melhor não adicionar bloqueios ao escrever código.
Mas, às vezes, esses bloqueios não são escritos por programadores. Alguns deles têm bloqueios nas implementações do JDK, como classes como Vector e StringBuffer. Muitos de seus métodos têm bloqueios. Quando usamos métodos dessas classes sem segurança de encadeamentos, quando certas condições forem atendidas, o compilador removerá o bloqueio para melhorar o desempenho.
por exemplo:
public static void main (string args []) lança interruptedException {long start = system.currenttimemillis (); for (int i = 0; i <2000000; i ++) {createStringBuffer ("JVM", "Diagnóstico"); } long buffercost = System.currenttimemillis () - start; System.out.println ("CraeTestringBuffer:" + buffercost + "ms"); } public static string CreateStringBuffer (String S1, String S2) {StringBuffer sb = new StringBuffer (); sb.append (S1); sb.append (s2); return sb.toString (); } O StringBuffer.Append no código acima é uma operação síncrona, mas o StringBuffer é uma variável local e o método não retorna o StringBuffer, por isso é impossível para vários threads acessá -lo.
Em seguida, a operação de sincronização no StringBuffer não tem sentido neste momento.
O cancelamento de bloqueio está definido nos parâmetros da JVM, é claro, ele precisa estar no modo de servidor:
-Server -xx:+DoCapeanAlysis -xx:+eliminatelocks
E ativar a análise de fuga. A função da análise de fuga é ver se a variável provavelmente escapará do escopo.
Por exemplo, no StringBuffer acima, o retorno do CraeTestringBuffer no código acima é uma string; portanto, essa variável local StringBuffer não será usada em nenhum outro lugar. Se você mudar o CraeTestringBuffer para
public Static StringBuffer CraeTestringBuffer (String S1, String S2) {StringBuffer sb = new StringBuffer (); sb.append (S1); sb.append (s2); retornar sb; } Depois que esse StringBuffer é retornado, ele pode ser usado em qualquer outro lugar (por exemplo, a função principal retornará o resultado e a colocará no mapa, etc.). Em seguida, a análise da JVM Escape pode ser analisada que essa variável local StringBuffer escapa seu escopo.
Portanto, com base na análise de escape, a JVM pode julgar que, se a variável local StringBuffer não escapar de seu escopo, pode -se determinar que o StringBuffer não será acessado por vários threads e, em seguida, esses bloqueios extras podem ser removidos para melhorar o desempenho.
Quando os parâmetros da JVM são:
-Server -xx:+DoCapeanAlysis -xx:+eliminatelocks
Saída:
CraeTestringBuffer: 302 ms
Os parâmetros da JVM são:
-Server -xx:+dodaCapeanAlysis -xx: -eliminatelocks
Saída:
CraeTestringBuffer: 660 ms
Obviamente, o efeito de eliminação da fechadura ainda é muito óbvio.
2. Otimização de bloqueio na máquina virtual
Primeiro, precisamos apresentar o cabeçalho do objeto. Na JVM, cada objeto tem um cabeçalho de objeto.
Mark Word, marcador para cabeçalho de objeto, 32 bits (sistema de 32 bits).
Descreva o hash, informações de bloqueio, tags de coleta de lixo, idade
Ele também salvará um ponteiro para o registro de bloqueio, um ponteiro para o monitor, um ID de encadeamento de bloqueio tendencioso, etc.
Simplificando, o cabeçalho do objeto é salvar algumas informações sistemáticas.
2.1 Bloqueio positivo
O chamado viés é a excentricidade, ou seja, a fechadura tenderá a direção do fio que atualmente possui a trava.
Na maioria dos casos, não há concorrência (na maioria dos casos, um bloco de sincronização não possui vários threads ao mesmo tempo com bloqueio de competição), portanto, o desempenho pode ser melhorado pelo polarização. Ou seja, quando não há concorrência, quando o thread que obteve anteriormente o bloqueio obtém o bloqueio novamente, ele determinará se o bloqueio está apontando para mim, para que o thread não precise obter o bloqueio novamente e possa entrar diretamente no bloco de sincronização.
A implementação do bloqueio de viés é definir a marca da marca do cabeçalho do objeto como tendenciosa e escrever o ID do thread na marca do cabeçalho do objeto.
Quando outros threads solicitam o mesmo bloqueio, o modo de polarização termina
A JVM permite o bloqueio de polarização por padrão -xx:+usebiasedLocking
Na concorrência feroz, o bloqueio tendencioso aumentará a carga do sistema (o julgamento de se é tendencioso é adicionado toda vez)
Exemplo de trava tendenciosa:
teste do pacote; importar java.util.list; importar java.util.vector; public class Test {public static list <Teger> numberList = new Vector <Teger> (); public static void main (string [] args) lança interruptedException {long begin = system.currenttimemillis (); int conting = 0; int startnum = 0; while (contagem <10000000) {numberList.add (startnum); startnum += 2; contagem ++; } long end = System.currenttimemillis (); System.out.println (end - BEGIN); }} O Vector é uma classe segura para threads que usa mecanismo de travamento internamente. Cada vez que adicionar, uma solicitação de bloqueio será feita. O código acima possui apenas um thread principal e, em seguida, adiciona repetidamente a solicitação de bloqueio.
Use os seguintes parâmetros da JVM para definir o bloqueio de viés:
-Xx:+usebiasedlocking -xx: BInesedLockingStartUpDelay = 0
BiasedLockingStartupDelay significa que a trava do viés é ativada após o início do sistema por alguns segundos. O padrão é de 4 segundos, porque quando o sistema é iniciado, a concorrência geral de dados é relativamente feroz. Os bloqueios de viés ativados neste momento reduzirão o desempenho.
Desde aqui, para testar o desempenho da trava do viés, o tempo de bloqueio de atraso é definido como 0.
Neste momento, a saída é 9209
Desligue a trava do viés abaixo:
-Xx: -UsebiasEdLocking
A saída é 9627
Geralmente, quando não há concorrência, o desempenho de permitir bloqueios de viés será aprimorado em cerca de 5%.
2.2 Bloqueio leve
A segurança de vários threades de Java é implementada com base no mecanismo de bloqueio, e o desempenho do bloqueio geralmente não é satisfatório.
O motivo é que o MonitorEnter e o Monitorexit, duas primitivos de bytecode que controlam a sincronização multithread, são implementados pela JVM dependem do MUTEX para o sistema operacional.
O Mutex é uma operação relativamente que consome recursos que faz com que o thread pendure e precisa ser remarcado de volta ao thread original em um curto período de tempo.
Para otimizar o mecanismo de bloqueio de Java, o conceito de bloqueio leve foi introduzido desde o Java6.
O bloqueio leve tem como objetivo reduzir a chance de fazer threading multi-tiro mutex, não substituir o mutex.
Ele usa a CPU Primitive Compare-and-Swap (CAS, Instruções de montagem CMPXCHG) e tenta remediar antes de entrar no Mutex.
Se o bloqueio do viés falhar, o sistema executará uma operação de bloqueio leve. O objetivo de sua existência é evitar a utilização do MUTEX no nível do sistema operacional o máximo possível, porque esse desempenho será relativamente ruim. Como a própria JVM é um aplicativo, espero resolver o problema de sincronização de threads no nível do aplicativo.
Em resumo, o bloqueio leve é um método de bloqueio rápido. Antes de inserir o MUTEX, use operações do CAS para tentar adicionar bloqueios. Tente não usar o MUTEX no nível do sistema operacional para melhorar o desempenho.
Então, quando a trava do viés falha, as etapas da trava leve:
1. Salve o ponteiro Mark do cabeçalho do objeto no objeto bloqueado (o objeto aqui se refere ao objeto bloqueado, como sincronizado (this) {}, este é o objeto aqui).
bloqueio-> set_displaced_header (mark);
2. Defina o cabeçalho do objeto como um ponteiro para a trava (no espaço da pilha de roscas).
if (mark == (markoop) atomic :: cmpxchg_ptr (bloqueio, obj ()-> mark_addr (), mark)) {tevent (slow_enter: libere Stacklock); retornar ; } A fechadura está localizada na pilha de threads. Portanto, para determinar se um encadeamento segura esse bloqueio, basta determinar se o espaço apontado pelo cabeçalho do objeto está no espaço de endereço da pilha de threads.
Se o bloqueio leve falhar, significa que há concorrência e atualização para uma trava pesada (bloqueio regular), que é o método de sincronização no nível do sistema operacional. Na ausência de concorrência de bloqueio, os bloqueios leves reduzem a perda de desempenho causada por bloqueios tradicionais usando mutexes do sistema operacional. Quando a concorrência é muito feroz (os bloqueios leves sempre falham), os bloqueios leves fazem muitas operações extras, resultando em degradação do desempenho.
2.3 trava de rotação
Quando a concorrência existe, como a tentativa de bloqueio leve falha, ela pode ser atualizada diretamente para um bloqueio pesado para usar a exclusão mútua no nível do sistema operacional. Também é possível experimentar o bloqueio de rotação novamente.
Se o thread puder obter a trava rapidamente, você não poderá pendurar o thread na camada do sistema operacional, deixe o thread fazer várias operações vazias (spin) e tentar constantemente obter a trava (semelhante ao Trylock). Obviamente, o número de loops é limitado. Quando o número de loops atingir, ele ainda será atualizado para uma trava pesada. Portanto, quando cada encadeamento tem pouco tempo para manter a trava, a trava de spin pode tentar evitar que as roscas sejam suspensas na camada do sistema operacional.
Jdk1.6 -xx:+usespinning está ativado
No JDK1.7, remova este parâmetro e altere-o para uma implementação interna.
Se o bloco de sincronização for muito longo e o spin falhar, o desempenho do sistema será degradado. Se o bloco de sincronização for muito curto e o spin for bem -sucedido, ele economiza o tempo de comutação da suspensão do encadeamento e melhora o desempenho do sistema.
2.4 Bloqueio positivo, trava leve, resumo da trava de rotação
O bloqueio acima não é um método de otimização de bloqueio em nível de idioma Java, mas é incorporado na JVM.
Primeiro de tudo, bloqueios de polarização é evitar o consumo de desempenho de um thread quando adquire/libera repetidamente a mesma trava. Se o mesmo thread ainda adquirir esse bloqueio, ele entrará diretamente no bloco de sincronização ao tentar influenciar os bloqueios e não há necessidade de obter o bloqueio novamente.
Bloqueios leves e bloqueios de rotação têm como objetivo evitar chamadas diretas para operações mutex no nível do sistema operacional, porque a suspender de threads é uma operação de consumo de recursos.
Para evitar o uso de bloqueios pesados (Mutex no nível do sistema operacional), primeiro tentaremos uma trava leve. O bloqueio leve tentará usar a operação CAS para obter o bloqueio. Se o bloqueio leve não conseguir, significa que há concorrência. Mas talvez você obtenha a trava em breve e experimente as fechaduras, fará alguns loops vazios no segmento e tentará obter as fechaduras toda vez que você fizer um loop. Se o bloqueio de spin também falhar, ele só poderá ser atualizado para uma trava pesada.
Pode -se observar que fechaduras tendenciosas, bloqueios leves e travas de rotação são bloqueios otimistas.
3. Um caso de uso de bloqueios incorretamente
classe pública Integerlock {Inteiro estático i = 0; classe estática public static addThread estende thread {public void run () {for (int k = 0; k <100000; k ++) {sincronizado (i) {i ++; }}}} public static void main (string [] args) lança interruptedException {addThread t1 = new addThread (); AddThread t2 = new addThread (); t1.start (); t2.start (); t1.Join (); t2.Join (); System.out.println (i); }} Um erro muito básico é que, no padrão de design de simultaneidade [de alta concorrência Java VII], o Interger não é alterado e, após cada ++, um novo interger será gerado e atribuído a I, para que os bloqueios competidos entre os dois threads sejam diferentes. Portanto, não é seguro para fios.
4. Threadlocal e sua análise de código -fonte
Pode ser um pouco inapropriado mencionar o ThreadLocal aqui, mas o Threadlocal é uma maneira de substituir os bloqueios. Portanto, ainda é necessário mencioná -lo.
A idéia básica é que, em um multi-thread, os dados conflitantes precisam ser bloqueados. Se threadlocal for usado, uma instância de objeto será fornecida para cada thread. Diferentes threads acessam apenas seus próprios objetos, não outros objetos. Dessa forma, não há necessidade de existir o bloqueio.
teste de pacote; importar java.text.parseException; importar java.text.simpledEformat; importar java.util.date; importar java.util.concurrent.executorService; importação java.util.concurrent.xecutores; Teste público {private final simplentateFormatato simplado. Hh: mm: ss "); Classe estática pública Parsedate implementa runnable {int i = 0; public parsedate (int i) {this.i = i; } public void run () {try {date t = sdf.parse ("2016-02-16 17:00:" + i % 60); System.out.println (i + ":" + t); } catch (parseException e) {e.printStackTrace (); }}} public static void main (string [] args) {executorService ES = executores.newfixedthreadpool (10); for (int i = 0; i <1000; i ++) {es.execute (novo parsedato (i)); }}} Como o SimpleDateFormat não é seguro, o código acima é usado incorretamente. A maneira mais fácil é definir uma classe você mesmo e envolvê -la com sincronizado (semelhante às coleções.SynchronizedMap). Isso causará problemas ao fazê -lo em alta simultaneidade. A contenção nos resultados sincronizados em apenas uma rosca entrando por vez e o volume de simultaneidade é muito baixo.
Esse problema é resolvido usando o ThreadLocal para encapsular o SimpleDateFormat.
teste de pacote; importar java.text.parseException; importar java.text.simpledEformat; importar java.util.date; importar java.util.Concurrent.ExecutorService; importação java.util.concurrent.executores; Classe estática pública Parsedate implementa runnable {int i = 0; public parsedate (int i) {this.i = i; } public void run () {try {if (tl.get () == null) {tl.set (new SimpleDateFormat ("yyyy-mm-dd hh: mm: ss")); } Data t = tl.get (). Parse ("2016-02-16 17:00:" + i % 60); System.out.println (i + ":" + t); } catch (parseException e) {e.printStackTrace (); }}} public static void main (string [] args) {executorService ES = executores.newfixedthreadpool (10); for (int i = 0; i <1000; i ++) {es.execute (novo parsedato (i)); }}}Quando cada encadeamento estiver em execução, ele determinará se o thread atual possui um objeto SimpleDateFormat.
if (tl.get () == null)
Caso contrário, o novo SimpleDateFormat estará vinculado ao tópico atual
tl.set (novo SimpleDateFormat ("AAA YYYY-MM-DD HH: MM: SS"));
Em seguida, use o SimpleDateFormat do thread atual para analisar
tl.get (). Parse ("2016-02-16 17:00:" + i % 60);
No código inicial, havia apenas um SimpleDateFormat, que usava Threadlocal, e um SimpleDateFormat era novo para cada thread.
Deve -se notar que você não deve definir um público simplificado para cada threadlocal aqui, pois isso é inútil. Cada um precisa ser concedido a um SimpleDateFormat.
No hibernato, existem aplicações típicas para threadlocal.
Vamos dar uma olhada na implementação do código -fonte do Threadlocal
Primeiro de tudo, há uma variável de membro na classe de threads:
Threadlocal.threadlocalmap threadlocals = null;
E este mapa é a chave para a implementação do Threadlocal
public void set (valor t) {thread t = thread.currentThread (); Threadlocalmap map = getMap (t); if (map! = null) map.set (this, valor); else createMap (t, valor); } De acordo com o ThreadLocal, você pode definir e obter o valor correspondente.
A implementação do Threadlocalmap aqui é semelhante ao HashMap, mas existem diferenças no manuseio de conflitos de hash.
Quando ocorre um conflito de hash no Threadlocalmap, não é como o hashmap usar listas vinculadas para resolver o conflito, mas para colocar o índice ++ no próximo índice para resolver o conflito.