El propósito de la programación concurrente es hacer que el programa se ejecute más rápido, pero el uso de concurrencia puede no necesariamente hacer que el programa se ejecute más rápido. Las ventajas de la programación concurrente solo se pueden reflejar cuando el número de programas concurrentes alcanza un cierto orden de magnitud. Por lo tanto, solo es significativo hablar sobre la programación concurrente cuando hay una alta concurrencia. Aunque todavía no se han desarrollado programas con alto volumen de concurrencia, el aprendizaje de la concurrencia es comprender mejor algunas arquitecturas distribuidas. Luego, cuando el volumen de concurrencia del programa no es alto, como un programa de un solo subproceso, la eficiencia de ejecución de un solo hilo es mayor que la de un programa multiproceso. ¿Por qué es esto? Aquellos que están familiarizados con el sistema operativo deben saber que la CPU implementa múltiples subprocesos al asignar cortes de tiempo a cada hilo. De esta manera, cuando la CPU cambia de una tarea a otra, se guardará el estado de la tarea anterior. Cuando se ejecuta la tarea, la CPU continuará ejecutando el estado de la tarea anterior. Este proceso se llama conmutación de contexto.
En Java multithreading, la palabra clave sincronizada de la palabra clave volátil juega un papel importante. Todos pueden implementar la sincronización de hilos, pero ¿cómo se implementa en la parte inferior?
volátil
Volátil solo puede garantizar la visibilidad de las variables a cada hilo, pero no puede garantizar la atomicidad. No diré mucho sobre cómo usar el lenguaje Java volátil. Mi sugerencia es usarlo en cualquier otra situación, excepto la biblioteca de clases en el paquete java.util.concurrent.atomic. Vea este artículo para obtener más explicaciones.
Introducción
Ver el siguiente código
paquete org.go; public class 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); }} El resultado de cada ejecución del código anterior es diferente, y el número de salida siempre es inferior a 10000. Esto se debe a que al realizar Inc (), i ++ no es una operación atómica. Quizás algunas personas sugerirían usar sincronizado para sincronizar inc (), o usar el bloqueo en el paquete java.util.concurrent.locks para controlar la sincronización de hilos. Pero no son tan buenos como las siguientes soluciones:
paquete org.go; import java.util.concurrent.atomic.atomicinteger; public class Go {AtomicInteger i = new AtomItInseger (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); }} En este momento, si no comprende la implementación de Atomic, definitivamente sospechará que el AtomicInteger subyacente puede implementarse utilizando cerraduras, por lo que puede no ser eficiente. Entonces, ¿qué es exactamente? Echemos un vistazo.
Implementación interna de clases atómicas
Ya sea que se trate de AtomicInteger o concurrentLinkedqueue Node Clase concurrentLinkedqueue. Nodo, tienen una variable estática
Sun.misc.Misc.Misc.unsafe inseguro; esta clase es una encapsulación de Java de Sun :: Misc :: Inseguro que implementa la semántica atómica. Quiero ver la implementación subyacente. Tengo el código fuente de GCC4.8 a mano. En comparación con la ruta local, es muy conveniente encontrar el camino hacia GitHub. Mira aquí.
Tome el ejemplo de implementación de la interfaz getAndincrement ()
AtomicInteger.java
Private Static Final Insafe Unsafe = unsafe.getUnsafe (); public final int getAndincrement () {for (;;) {int current = get (); int next = corriente + 1; if (compareandSet (actual, siguiente)) return corriente; }} Public Final Boolean CompareandSet (int espere, int actualización) {return unsafe.compareandswapint (this, valueOffset, espere, actualización); } Preste atención a esto para el bucle, solo volverá si el conjunto de comparación tiene éxito. De lo contrario, siempre se comparará.
Se llama a la implementación de la comparación. Aquí, noté que la implementación de Oracle JDK es ligeramente diferente. Si observa el SRC bajo JDK, puede ver que Oracle JDK llama inseguro getandincrement (), pero creo que cuando Oracle JDK implementa inseguros.java, solo debe llamar a la comparación, porque un conjunto de comparación puede implementar operaciones atómicas de valores aumentados, disminuidos y de ajuste.
Inseguro.java
Public nativo boolean compareanddswapint (obj obj, offset largo, int espere, int actualización);
Implementación de C ++ llamado a través de JNI.
natunsafe.cc
JBOOLEANSUN :: MISC :: UNSAFE :: COMPLEGANDSWAPINT (Jobject obj, Jlong offset, jint Weping, Jint Update) {jint *addr = (jint *) ((char *) obj + offset); return compareandswap (addr, esperanza, actualización);} estática en línea boolcompareandswap (volatile jint *addr, jint old, jint new_val) {jboolean resultado = false; bloqueo de spinlock; if ((resultado = ( *addr == Old))) *addr = new_val; Resultado de retorno;} Inseguro :: compareanddswapint llama a la función estática CompareAndsWap. CompareAndsWap usa spinlock como bloqueo. El spinlock aquí tiene el significado de Lockguard, que está bloqueado durante la construcción y se libera durante la destrucción.
Necesitamos centrarnos en Spinlock. Aquí está para garantizar que SpinLock sea una verdadera implementación de las operaciones atómicas antes de su lanzamiento.
Que es spinlock
Spinlock, una especie de ocupado esperando para adquirir el bloqueo del recurso. A diferencia del bloqueo de Mutex, el hilo actual y la liberación de los recursos de la CPU para esperar los recursos requeridos, SpinLock no ingresará al proceso de suspensión, esperando que se cumplan las condiciones y vuelva a competir la CPU. Esto significa que SpinLock es mejor que Mutex solo si el costo de esperar el bloqueo es menor que el costo del interruptor de contexto de ejecución de subprocesos.
natunsafe.cc
class spinLock {static volátil obj_addr_t bloqueo; public: spinLock () {while (! compare_and_swap (& bloqueo, 0, 1)) _jv_threadyield (); } ~ spinLock () {roteo_set (& bloqueo, 0); }}; Use una variable estática volátil estática obj_addr_t bloqueo; Como un bit de bandera, se implementa un guardia a través de C ++ RAII, por lo que el llamado bloqueo es en realidad la variable de miembro estático OBJ_ADDR_T LOCK. El volátil en C ++ no puede garantizar la sincronización. Lo que se garantiza es el comparte_and_swap llamado en el constructor y un bloqueo de variable estática. Cuando esta variable de bloqueo es 1, debe esperar; Cuando es 0, lo establece en 1 a través de la operación atómica, lo que indica que ha obtenido el bloqueo.
Realmente es un accidente usar una variable estática aquí, lo que significa que todas las estructuras sin bloqueo comparten la misma variable (en realidad size_t) para distinguir si agregar un bloqueo. Cuando esta variable se establece en 1, se debe esperar otro spinlock. ¿Por qué no agregar una variable privada volátil obj_addr_t bloquear el sol :: misc :: inseguro y pasarlo a spinlock como parámetro de constructor? Esto es equivalente a compartir un bit de bandera para cada inseguro. ¿Será mejor el efecto?
_Jv_threadyield En el siguiente archivo, el recurso CPU se renuncia a través de la llamada del sistema SCHED_YIED (Man 2 SCHED_YIELD). La macro have_sched_yield se define en configure, lo que significa que si la definición no está definida durante la compilación, el spinlock se llama bloqueo de spin verdadero.
POSIX-THEADS.H
inline void_jv_threadyield (void) {#ifdef have_sched_yield sched_yield ();#endif / * have_sched_yield * /} Este bloqueo. H tiene diferentes implementaciones en diferentes plataformas. Tomamos la plataforma IA64 (Intel AMD X64) como ejemplo. Se pueden ver otras implementaciones aquí.
IA64/Locks.h
typedef size_t obj_addr_t; inline static boolcompare_and_swap (volatile obj_addr_t *addr, obj_addr_t Old, obj_addr_t new_val) {return __sync_bool_compare_and_swap (addr, vieja, new_val);} inline static voidRelease (voleed obj_addr_t *addr, obj_addr_t new_val) {__asm__ __volatile __ ("" ::: "Memoria"); *(addr) = new_val;}__sync_bool_compare_and_swap es una función GCC incorporada, y la instrucción de ensamblaje "memoria" completa la barrera de memoria.
En resumen, el hardware garantiza la sincronización de CPU de múltiples núcleos, y la implementación de inseguros es lo más eficiente posible. GCC-Java es bastante eficiente, creo que Oracle y OpenJDK no serán peores.
Operaciones atómicas y operaciones atómicas integradas por GCC
Operación atómica
Las expresiones de Java y las expresiones C ++ no son operaciones atómicas, lo que significa que está en el código:
// Suponga que I es una variable i ++ compartida entre hilos;
En un entorno multiproceso, el acceso no es atómico y en realidad se divide en los siguientes tres operandos:
El compilador cambia el momento de la ejecución, por lo que no se puede esperar el resultado de la ejecución.
Operación atómica incorporada de GCC
GCC tiene operaciones atómicas incorporadas, que se agregaron a partir de 4.1.2. Antes, se implementaron utilizando ensamblaje en línea.
type __sync_fetch_and_add (type *ptr, type value, ...) type __sync_fetch_and_sub (type *ptr, type value, ...) type __sync_fetch_and_or (type *ptr, type value, ...) type __sync_fetch_and_or (type *ptr, type value, ...) type __sync_fettetch valor, ...) tipo __sync_fetch_and_xor (type *ptr, type value, ...) type __sync_fetch_and_nand (type *ptr, type value, ...) type __sync_add_and_fetch (type *ptr, type value, ...) type __sync_sub_and_fetch (type *ptr, type value, ...) type, type value, ...) type __sync_sub_and_fetch (type *ptr, type value, ...) type, type valor, ...) type __sync_sub_and_fetch (type *ptr, type value, ...) type __ __andy (type *ptr, type value, ...) type __sync_and_and_fetch (type *ptr, type value, ...) type __sync_and_and_fetch (type *ptr, type valor, ...) type __sync_xor_and_fetch (type *ptr, type value, ...) type __sync_nand_and_and_fetch (type *ptr, type value, ...) __sync_bool_compare_and_swap (type *ptr, type Oldval Type NewVal, ...) Tipo __sync_val_Compare_and_Swap (type *ptr, type OldVal type newVal, ...) __ sync_synchronize (...) type __sync_test_and_and_set (type *ptr, type valor, ...) void (...) tipo __sync_lock_release (tipo *PTR, ...)
Lo que debe tenerse en cuenta es:
Archivos relacionados con OpenJDK
A continuación se presentan algunas implementaciones de operación atómica de OpenJDK9 en GitHub, con la esperanza de ayudar a aquellos que necesitan saber. Después de todo, OpenJDK se usa más ampliamente que GCC. "" Pero después de todo, no hay código fuente para Oracle JDK, aunque se dice que el código fuente entre OpenJDK y Oracle es muy pequeño.
AtomicInteger.java
Unsafe.java::compareanDexchangeObject
unsafe.cpp :: unsafe_compareanDexchangeObject
oop.inline.hpp :: oopdesc :: atomic_compare_exchange_oop
atomic_linux_x86.hpp :: atomic :: cmpxchg
en línea Jlong Atomic :: CMPXCHG (JLong Exchange_Value, Volatile Jlong* 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", "Memoria"); return Exchange_Value;} Aquí debemos dar un aviso a los programadores de Java que no están familiarizados con C/C ++. El formato de instrucciones de ensamblaje integrado es el siguiente
__asm__ [__Volatile __] (Plantilla de ensamblaje // Plantilla de ensamblaje: [Lista de operando de salida] // Lista de entrada: [Lista de operando de entrada] // Lista de salida: [Lista de clobber]) // Destruir Lista
%1, %3, %4 En la plantilla de ensamblaje corresponde a la siguiente lista de parámetros {"r" (Exchange_Value), "R" (Dest), "R" (MP)}, y la lista de parámetros está separada por comas y se clasifica de 0. El parámetro de salida se coloca a la derecha del primer colon, y el parámetro de salida se coloca a la derecha del segundo colon. "R" significa poner en un registro general, "A" significa registro EAX, y "=" significa para la salida (escribir). La instrucción CMPXCHG implica el uso de registros EAX, es decir, el parámetro %2.
Otros detalles no se enumerarán aquí. La implementación de GCC es transmitir el puntero para intercambiar, y después de una comparación exitosa, el valor se asigna directamente (asignando no atómico), y la atomicidad está garantizada por spinlock.
La implementación de OpenJDK es transmitir el puntero para intercambiar y asignar directamente valores a través de la instrucción de ensamblaje CMMPXCHGQ, y la atomicidad se garantiza a través de la instrucción de ensamblaje. Por supuesto, la capa subyacente del spinlock de GCC también está garantizada a través de CMMPXCHGQ.
Lo anterior es todo el contenido de este artículo. Espero que sea útil para el aprendizaje de todos y espero que todos apoyen más a Wulin.com.