O objetivo da programação simultânea é tornar o programa mais rápido, mas o uso da simultaneidade pode não necessariamente fazer o programa funcionar mais rapidamente. As vantagens da programação simultânea só podem ser refletidas quando o número de programas simultâneos atingir uma certa ordem de magnitude. Portanto, é apenas significativo falar sobre programação simultânea quando há uma alta simultaneidade. Embora não tenham sido desenvolvidos programas com alto volume de concorrência, o aprendizado de concorrência é entender melhor algumas arquiteturas distribuídas. Então, quando o volume de simultaneidade do programa não é alto, como um programa de thread único, a eficiência de execução de um thread único é maior que o de um programa com vários threads. Por que isso? Aqueles que estão familiarizados com o sistema operacional devem saber que a CPU implementa multi-threading alocando fatias de tempo para cada thread. Dessa forma, quando a CPU mudar de uma tarefa para outra, o estado da tarefa anterior será salvo. Quando a tarefa for executada, a CPU continuará executando o estado da tarefa anterior. Esse processo é chamado de comutação de contexto.
No Java Multithreading, a palavra -chave sincronizada da palavra -chave volátil desempenha uma função importante. Todos eles podem implementar a sincronização de threads, mas como é implementada na parte inferior?
volátil
A volátil só pode garantir a visibilidade das variáveis para cada encadeamento, mas não pode garantir a atomicidade. Não vou dizer muito sobre como usar o idioma java volátil. Minha sugestão é usá -lo em qualquer outra situação, exceto a biblioteca de classes no pacote java.util.concurrent.atomic. Veja este artigo para obter mais explicações.
Introdução
Veja o seguinte código
pacote org.go; classe pública go {volatile int i = 0; private void inc () {i ++; } public static void main (string [] args) {go go = new go (); for (int i = 0; i <10; i ++) {new Thread (() -> {for (int j = 0; j <1000; j ++) go.inc ();}). start (); } while (thread.activeCount ()> 1) {thread.yield (); } System.out.println (go.i); }} O resultado de cada execução do código acima é diferente e o número de saída é sempre menor que 10000. Isso ocorre porque, ao executar o Inc (), o I ++ não é uma operação atômica. Talvez algumas pessoas sugerissem o uso de sincronizado para sincronizar Inc () ou usar o bloqueio no pacote java.util.concurrent.locks para controlar a sincronização do encadeamento. Mas eles não são tão bons quanto as seguintes soluções:
pacote org.go; importar java.util.concurrent.atomic.atomicinteger; classe pública go {atomicinteger i = new atomicinteger (0); private void Inc () {i.getAndIncrement (); } public static void main (string [] args) {go go = new go (); for (int i = 0; i <10; i ++) {new Thread (() -> {for (int j = 0; j <1000; j ++) go.inc ();}). start (); } while (thread.activeCount ()> 1) {thread.yield (); } System.out.println (go.i); }} No momento, se você não entender a implementação do Atomic, definitivamente suspeitará que o atômico subjacente possa ser implementado usando bloqueios, portanto, pode não ser eficiente. Então, o que exatamente é, vamos dar uma olhada.
Implementação interna de classes atômicas
Seja atomicinteger ou concorrente da classe de nó de nó concurrentLinkedQueue.node, eles têm uma variável estática
Sun estático privado Sun.misc.unsafe inseguro;, esta classe é um encapsulamento de java do sol :: misc :: inseguro que implementa a semântica atômica. Eu quero ver a implementação subjacente. Por acaso, tenho o código -fonte do GCC4.8 em mãos. Comparado com o caminho local, é muito conveniente encontrar o caminho para o GitHub. Olhe aqui.
Veja o exemplo de implementação da interface getAndIncrement ()
Atomicinteger.java
Final estático privado inseguro inseguro = inseguro.getunsfe (); public final int getAndIncrement () {for (;;) {int current = get (); int próximo = corrente + 1; if (comparaandndSet (atual, próximo)) retornar corrente; }} public final boolean ComparaNDSet (int espera, int update) {return unsafe.compareandswapint (este, valueoffset, espera, atualização); } Preste atenção a isso pelo loop, ele só retornará se o ComparaDSet for bem -sucedido. Caso contrário, sempre se comparará.
A implementação do ComparaDSet é chamada. Aqui, notei que a implementação do Oracle JDK é um pouco diferente. Se você observar o SRC no JDK, poderá ver que o Oracle JDK chama de getAndIncrement () inseguro (), mas acredito que quando o Oracle JDK implementa inseguro.java, ele deve chamar apenas o ComparaNDSet, porque um comparaandsset pode implementar operações atômicas de aumentar, diminuir e definir valores.
Inseguro.java
public nativo boolean comparaandswapint (objeto obj, deslocamento longo, int espera, int atualização);
Implementação de C ++ chamado por JNI.
Natunsafe.cc
JBooleansun :: Misc :: Unsefe :: Comparanswapint (Jobject Obj, Jlong Offset, Jint Espere, Jint Atualize) {Jint *addr = (Jint *) ((char *) obj + deslocamento); Retornar comparandswap (addr, espera, atualização);} estático em linha boolcompareandswap (JINT volátil *addr, jint antigo, jint new_val) {JBoolean resultado = false; trava de spinlock; if ((resultado = ( *addr == antigo))) *addr = new_val; resultado de retorno;} Inseguro :: Comparanswapint chama a função estática comparandswap. Comparandswap usa spinlock como bloqueio. O spinlock aqui tem o significado de Lockguard, que é bloqueado durante a construção e liberado durante a destruição.
Precisamos nos concentrar no spinlock. Aqui está para garantir que o spinlock seja uma verdadeira implementação de operações atômicas antes de ser lançada.
O que é spinlock
Spinlock, uma espécie de ocupado esperando para adquirir a fechadura do recurso. Ao contrário do bloqueio da Mutex e da liberação de recursos da CPU para aguardar os recursos necessários, o spinlock não entrará no processo de suspensão, aguardando as condições a serem atendidas e comparará a CPU. Isso significa que o spinlock é melhor que o MUTEX apenas se o custo da espera pelo bloqueio for menor que o custo da chave de contexto de execução do encadeamento.
Natunsafe.cc
classe spinlock {estático obj_addr_t bloqueio; public: spinlock () {while (! compare_and_swap (& bloqueio, 0, 1)) _jv_threadyield (); } ~ spinlock () {release_set (& bloqueio, 0); }}; Use uma variável estática estática volátil obj_addr_t bloqueio; Como um bit de bandeira, um guarda é implementado através do C ++ RAII, portanto o chamado bloqueio é na verdade a variável estática variável obj_addr_t bloqueio. O volátil em C ++ não pode garantir a sincronização. O que é garantido é o compare_and_swap chamado no construtor e uma bloqueio de variável estática. Quando essa variável de bloqueio é 1, você precisa esperar; Quando é 0, você o define para 1 através da operação atômica, indicando que obteve o bloqueio.
É realmente um acidente usar uma variável estática aqui, o que significa que todas as estruturas sem bloqueio compartilham a mesma variável (na verdade, tamanho_t) para distinguir se deve adicionar um bloqueio. Quando essa variável é definida como 1, outro spinlock precisa ser esperado. Por que não adicionar uma variável privada obj_addr_t bloqueio em sol :: misc :: inseguro e passe para spinlock como um parâmetro construtor? Isso é equivalente a compartilhar um bit de bandeira para cada inseguro. O efeito será melhor?
_Jv_threadyield No arquivo a seguir, o recurso da CPU é desistido através do sistema CHAMADA AGRADED_YIELD (Man 2 Sched_yield). A macro teve_sched_yield é definida na configuração, o que significa que, se a definição for indefinida durante a compilação, o spinlock será chamado de bloqueio de spin verdadeiro.
posix-threads.h
inline void_jv_threadyield (void) {#ifdef Have_sched_yield sched_yield ();#endif / * Have_sched_yield * /} Este bloqueio.h possui implementações diferentes em diferentes plataformas. Nós pegamos a plataforma IA64 (Intel AMD x64) como exemplo. Outras implementações podem ser vistas aqui.
IA64/Locks.h
typedef size_t obj_addr_t; inline estático boolcompare_and_swap (obj_addr_t_t *addr, obj_addr_t antigo, obj_addr_t new_val) {return __sync_bool_compare_and_swap (addr, antigo, new_val); *addr, obj_addr_t new_val) {__asm__ __volatile __ ("" ::: "memória"); *(addr) = new_val;}__SYNC_BOOL_COMPARE_AND_SWAP é uma função GCC integrada e a instrução de montagem "memória" completa a barreira da memória.
Em resumo, o hardware garante a sincronização de CPU com vários núcleos e a implementação de inseguros é o mais eficiente possível. O GCC-Java é bastante eficiente, acredito que o Oracle e OpenJDK não serão piores.
Operações atômicas e operações atômicas internas do GCC
Operação atômica
Expressões Java e Expressões C ++ não são operações atômicas, o que significa que você está no código:
// Suponha que eu seja uma variável i ++ compartilhada entre threads;
Em um ambiente multithread, o acesso não atômico e é realmente dividido nos três operandos a seguir:
O compilador altera o momento da execução, portanto o resultado da execução pode não ser esperado.
Operação atômica incorporada do GCC
O GCC possui operações atômicas integradas, que foram adicionadas a partir de 4.1.2. Antes, eles eram implementados usando a montagem embutida.
TIPO __SYNC_FETCH_AND_ADD (TIPO *PTR, VALOR DO TIPO, ...) TIPO __SYNC_FETCH_AND_SUB (TIPO *PTR, VALOR DO TIPO, ...) TIPO __SYNC_FETCH_AND_OR (TIPO *PTR, TIPO VALOR, ...) TIPO __SYNC_FEND_AND_OR (TIPO *PTR, valor, ...) digite __sync_fetch_and_xor (tipo *ptr, digite valor, ...) TIPO __SYNC_FETCH_AND_NAND (TIPO *PTR, VALOR DO TIPO, ...) TIPO __SYNC_ADD_AND_FETCH (TIPO *PTR, TIPO VALOR ...) TIPO __SYNC_SUD_AND_FETH (TIPO *PTR, TIPO, ...) Tipo __ync_subAnd_Fetch (Tipo *Ptr, Tipo, ...) Tipo __sync_and_and_fetch (Tipo *Ptr, Tipo, ...) (TIPO *PTR, VALOR DO TIPO, ...) TIPO __SYNC_AND_AND_FETCH (TIPO *PTR, VALOR DO TIPO, ...) TIPO __SYNC_AND_AND_FETCH (TIPO *PTR, TIPO VALOR, ...) TIPO __SYNOND_XOR_AND_FETCH (TIPO *PTR, TIPO, TIPO, ...) TIPO __YNOND_NOND_OND_AND_AND_FO __SYNC_BOOL_COMPARE_AND_SWAP (TIPO *PTR, TIPO ADENVEL TIPO NEWVAL, ...) TIPO __SYNC_VAL_COMPARE_AND_SWAP (TIPO *PTR, TIPO TIPO ANTENVEL NEWVAL, ...) __ SYNC_SYNCHRONIZE (...) TIPO __SYNC_LOCK_TEST_AND_ET_STELENS (tipo *ptr, ...)
O que deve ser observado é:
Arquivos relacionados ao OpenJDK
Abaixo estão algumas implementações de operação atômica do OpenJDK9 no GitHub, na esperança de ajudar aqueles que precisam saber. Afinal, o OpenJDK é mais amplamente utilizado que o GCC. - Mas, afinal, não há código -fonte para o Oracle JDK, embora se diga que o código -fonte entre o OpenJDK e o Oracle é muito pequeno.
Atomicinteger.java
Insegu.java::COMPARENDEXCHANGEOBJETO
insegu.cpp :: insefe_compareAndExChangeObject
oop.inline.hpp :: oopdesc :: atomic_compare_exchange_oop
atomic_linux_x86.hpp :: atomic :: cmpxchg
Jlong atomic :: cmpxchg (Jlong Exchange_value, Jlong volátil* dest, Jlong compare_value, cmmpxchg_memory_order order) {bool mp = os :: is_mp (); __ASM__ __VOLATILE__ (LOCK_IF_MP (%4) "CMPXCHGQ%1, (%3)": "= A" (Exchange_value): "R" (Exchange_value), "A" (Compare_value), "R" (dest), "R" (MP): "CC" "; Retornar Exchange_value;} Aqui precisamos dar um aviso aos programadores Java que não estão familiarizados com o C/C ++. O formato das instruções de montagem incorporado é a seguinte
__ASM__ [__Volatile __] (modelo de montagem // Modelo de montagem: [Lista de operando de saída] // Lista de entrada: [Lista de operando de entrada] // Lista de saída: [Lista de Clobber]) // Lista de destruição
%1, %3, %4 no modelo de montagem corresponde à seguinte lista de parâmetros {"r" (Exchange_value), "r" (dest), "r" (mp)}, e a lista de parâmetros é separada por vírgulas e classificada de 0. O parâmetro de saída é colocado à direita do primeiro cólon, e o parâmetro de saída é colocado no ponto de vista, o segundo direito. "R" significa colocado em um registro geral "A" significa registrar eax e "=" significa para saída (gravar de volta). A instrução CMPXCHG implica o uso de registros EAX, ou seja, parâmetro %2.
Outros detalhes não serão listados aqui. A implementação do GCC é transmitir o ponteiro a ser trocado e, após a comparação bem-sucedida, o valor é atribuído diretamente (atribuindo não atômico) e a atomicidade é garantida por spinlock.
A implementação do OpenJDK é transmitir o ponteiro a ser trocado e atribuir valores diretamente através da instrução de montagem CMMPXCHGQ, e a atomicidade é garantida através da instrução de montagem. Obviamente, a camada subjacente do spinlock do GCC também é garantida através do CMMPXCHGQ.
O exposto acima é todo o conteúdo deste artigo. Espero que seja útil para o aprendizado de todos e espero que todos apoiem mais o wulin.com.