resumen
Esta serie se basa en el curso de los números de refinación en el oro, y para aprender mejor, se hicieron una serie de registros. Este artículo presenta principalmente: 1. Ideas y métodos de optimización de bloqueos 2. Optimización de bloqueo en la máquina virtual 3. Un caso de uso incorrecto de los bloqueos 4. Threadlocal y su análisis del código fuente
1. Ideas y métodos de optimización de bloqueos
El nivel de concurrencia se menciona en la introducción a [Alta concurrencia Java 1].
Una vez que se usa un bloqueo, significa que esto es un bloqueo, por lo que la concurrencia generalmente es un poco más baja que la situación sin bloqueo.
La optimización de bloqueo mencionada aquí se refiere a cómo evitar que el rendimiento se vuelva demasiado pobre en el caso del bloqueo. Pero no importa cómo lo optimice, el rendimiento generalmente será un poco peor que la situación sin bloqueo.
Cabe señalar aquí que el TreyLock en Reentrantlock mencionado en [Alta concurrencia Java V] JDK Concurrence Package 1 tiende a ser un método sin bloqueo porque no se cuelga cuando el Trylock juzga.
Para resumir las ideas y métodos de optimización de bloqueos, existen los siguientes tipos.
1.1 Reducir el tiempo de retención de bloqueo
public sincronizado void syncMethod () {otherCode1 (); mutextmethod (); OtherCode2 (); } Al igual que el código anterior, debe obtener el bloqueo antes de ingresar el método, y otros hilos tienen que esperar afuera.
El punto de optimización aquí es reducir el tiempo de espera de otros hilos, por lo que solo se usa para agregar bloqueos en los programas con requisitos de seguridad de los subprocesos.
public void syncMethod () {OtherCode1 (); sincronizado (this) {mutextmethod (); } OTROCODE2 (); }1.2 Reducir el tamaño de la partícula de bloqueo
Dividen los objetos grandes (se puede acceder a este objeto mediante muchos hilos) en objetos pequeños, aumentando en gran medida el paralelismo y reduciendo la competencia de bloqueo. Solo al reducir la competencia por los bloqueos y el sesgo hacia las cerraduras, mejorará la tasa de éxito de los bloqueos livianos.
El caso más típico de reducir la granularidad del bloqueo es concurrenthashmap. Esto se menciona en [Alta concurrencia Java V] JDK Concurrencia Paquete 1.
1.3 Separación de bloqueo
La separación de bloqueo más común es el bloqueo de lectura de escritura ReadWriteReLock, que se separa en bloqueos de lectura-escritura y bloqueos de escritura de acuerdo con la función. De esta manera, la lectura y la lectura no son mutuamente excluyentes, la lectura y la escritura son mutuamente excluyentes, lo que garantiza la seguridad de los hilos y mejora el rendimiento. Para obtener más detalles, verifique [Java V] Java V] JDK Paquete de concurrencia 1.
Se puede extender la idea de separación de la lectura y la escritura, y mientras las operaciones no se afecten entre sí, el bloqueo se puede separar.
Por ejemplo, LinkedBlokingqueue
Sácalo de la cabeza y ponga los datos de la cola. Por supuesto, también es similar al robo de trabajo en Forkjoinpool mencionado en [Alta concurrencia Java VI] JDK Concurrencia Paquete 2.
1.4 Roughing de bloqueo
En términos generales, para garantizar una concurrencia efectiva entre múltiples hilos, se requerirá que cada hilo mantenga el bloqueo lo más corto posible, es decir, el bloqueo debe liberarse inmediatamente después de usar recursos públicos. Solo de esta manera pueden otros hilos que esperan en este bloqueo obtener recursos para ejecutar tareas lo antes posible. Sin embargo, todo tiene un título. Si el mismo bloqueo se solicita constantemente, se sincroniza y se libera, consumirá recursos valiosos del sistema, que no conducen a la optimización del rendimiento.
Por ejemplo:
public void demomETHOD () {Synchronized (Lock) {// do sth. } // hacer otro trabajo de sincronización no deseado, pero se puede ejecutar rápidamente sincronizado (bloqueo) {// do sth. }} En este caso, de acuerdo con la idea de la rugosidad del bloqueo, debe fusionarse
public void demomETHOD () {// Integre en una solicitud de bloqueo sincronizado (bloqueo) {// do sth. // hacer otro trabajo de sincronización no deseado, pero se puede ejecutar rápidamente}} Por supuesto, hay un requisito previo, el trabajo en el medio que no requiere sincronización se ejecutará rápidamente.
Déjame darte otro ejemplo extremo:
para (int i = 0; i <círculo; i ++) {sincronizado (bloqueo) {}} Los bloqueos deben obtenerse en un bucle. Aunque JDK optimizará este código internamente, es mejor escribirlo directamente
sincronizado (bloqueo) {for (int i = 0; i <círculo; i ++) {}} Por supuesto, si es necesario decir que tal bucle es demasiado largo y que debe dar otros hilos para no esperar demasiado, entonces solo puede escribirlo como lo anterior. Si no hay requisitos similares, es mejor escribirlo directamente en el siguiente.
1.5 Eliminación de bloqueo
La eliminación de bloqueo es una cosa de nivel de compilador.
En el compilador instantáneo, si se encuentran objetos que no son posibles para compartir, se puede eliminar la operación de bloqueo de estos objetos.
Tal vez le resulte extraño que, dado que se puede acceder a algunos objetos mediante múltiples hilos, ¿por qué debería agregar bloqueos? ¿No sería mejor simplemente no agregar cerraduras al escribir código?
Pero a veces, estas cerraduras no son escritas por programadores. Algunos de ellos tienen bloqueos en implementaciones JDK, como clases como Vector y StringBuffer. Muchos de sus métodos tienen cerraduras. Cuando usamos métodos de estas clases sin seguridad de subprocesos, cuando se cumplen ciertas condiciones, el compilador eliminará el bloqueo para mejorar el rendimiento.
Por ejemplo:
public static void main (string args []) lanza interruptedException {long start = system.currentTimemillis (); para (int i = 0; i <2000000; i ++) {createStringBuffer ("jvm", "diagnóstico"); } long bufferCost = System.CurrentTimemillis () - inicio; 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 (); } El StringBuffer.append en el código anterior es una operación sincrónica, pero StringBuffer es una variable local, y el método no devuelve el StringBuffer, por lo que es imposible que múltiples subproceses accedan a ella.
Luego, la operación de sincronización en StringBuffer no tiene sentido en este momento.
La cancelación de bloqueo se establece en los parámetros JVM, por supuesto, debe estar en modo servidor:
-server -xx:+doescapeanalysis -xx:+Eliminatelocks
Y encender el análisis de escape. La función del análisis de escape es ver si es probable que la variable escape del alcance.
Por ejemplo, en el StringBuffer anterior, la devolución de Craetestringbuffer en el código anterior es una cadena, por lo que esta variable local StringBuffer no se usará en otro lugar. Si cambias a Craetestringbuffer a
public static stringbuffer craetestringbuffer (String S1, String S2) {StringBuffer sb = new StringBuffer (); sb.append (S1); sb.append (S2); regresar SB; } Luego, después de que se devuelve este StringBuffer, se puede usar en cualquier otro lugar (por ejemplo, la función principal devolverá el resultado y lo pondrá en el mapa, etc.). Luego, se puede analizar el análisis de escape JVM que esta variable local StringBuffer escapa de su alcance.
Por lo tanto, en base al análisis de escape, el JVM puede juzgar que si la variable local StringBuffer no escapa de su alcance, se puede determinar que el StringBuffer no se accederá mediante múltiples hilos, y luego se pueden eliminar estos bloqueos adicionales para mejorar el rendimiento.
Cuando los parámetros JVM son:
-server -xx:+doescapeanalysis -xx:+Eliminatelocks
Producción:
craetestringbuffer: 302 ms
Los parámetros JVM son:
-server -xx:+doescapeanalysis -xx: -eliminatelocks
Producción:
Craetestringbuffer: 660 ms
Obviamente, el efecto de eliminación de bloqueo sigue siendo muy obvio.
2. Optimización de bloqueo en la máquina virtual
Primero, necesitamos introducir el encabezado del objeto. En el JVM, cada objeto tiene un encabezado de objeto.
Marque palabra, marcador para encabezado de objeto, 32 bits (sistema de 32 bits).
Describa el hash, la información de bloqueo, las etiquetas de recolección de basura, la edad
También guardará un puntero en el registro de bloqueo, un puntero al monitor, una ID de rosca de bloqueo sesgada, etc.
En pocas palabras, el encabezado del objeto es guardar información sistemática.
2.1 Bloqueo positivo
El llamado sesgo es la excentricidad, es decir, el bloqueo tenderá hacia el hilo que actualmente posee el bloqueo.
En la mayoría de los casos, no hay competencia (en la mayoría de los casos, un bloque de sincronización no tiene múltiples hilos al mismo tiempo de bloqueo de competencia), por lo que el rendimiento puede mejorarse mediante el sesgo. Es decir, cuando no hay competencia, cuando el hilo que obtuvo previamente el bloqueo obtiene nuevamente el bloqueo, determinará si el bloqueo me apunta, por lo que el hilo no necesitará obtener el bloqueo nuevamente y puede ingresar directamente al bloque de sincronización.
La implementación del bloqueo de sesgo es establecer la marca de la marca del encabezado del objeto como sesgada y escribir la ID de subproceso en la marca del encabezado del objeto.
Cuando otros hilos solicitan el mismo bloqueo, el modo de sesgo termina
JVM habilita el bloqueo de polarización por defecto -xx:+UseBiasedLocking
En una competencia feroz, el bloqueo sesgado aumentará la carga del sistema (el juicio de si está sesgado se agrega cada vez)
Ejemplo de bloqueo sesgado:
Prueba de paquete; import java.util.list; import java.util.vector; prueba de clase pública {public static list <integer> numberList = new Vector <Integer> (); public static void main (string [] args) lanza interruptedException {long begin = system.currentTimemillis (); int count = 0; int startnum = 0; while (Count <10000000) {numberList.Add (startnum); startnum += 2; contar ++; } Long End = System.CurrentTimemillis (); System.out.println (final - begin); }} Vector es una clase segura de hilo que utiliza el mecanismo de bloqueo internamente. Cada vez que se agrega, se realizará una solicitud de bloqueo. El código anterior solo tiene un hilo principal y luego agrega repetidamente la solicitud de bloqueo.
Use los siguientes parámetros JVM para establecer el bloqueo de sesgo:
-Xx:+UseBiasedLocking -xx: BesedLockingStartUpDelay = 0
BesedLockingStartUpDelay significa que el bloqueo de sesgo está habilitado después de que el sistema comienza durante unos segundos. El valor predeterminado es de 4 segundos, porque cuando se inicia el sistema, la competencia de datos general es relativamente feroz. Los bloqueos de sesgo habilitados en este momento reducirán el rendimiento.
Desde aquí, para probar el rendimiento del bloqueo de sesgo, el tiempo de bloqueo de sesgo de retraso se establece en 0.
En este momento la salida es 9209
Apague el bloqueo de sesgo a continuación:
-Xx: -sebiasedlocking
La salida es 9627
En general, cuando no hay competencia, el rendimiento de los bloqueos de sesgo habilitadores mejorará en aproximadamente un 5%.
2.2 Bloqueo liviano
La seguridad de múltiples subprocesos de Java se implementa en función del mecanismo de bloqueo, y el rendimiento del bloqueo a menudo no es satisfactorio.
La razón es que Monitorenter y Monitorexit, dos primitivas de código de byto que controlan la sincronización múltiple, se implementan por JVM dependiendo de Mutex para el sistema operativo.
Mutex es una operación relativamente que consume recursos que hace que el hilo cuelgue y necesita ser reprogramado al hilo original en un corto período de tiempo.
Para optimizar el mecanismo de bloqueo de Java, el concepto de bloqueo liviano se ha introducido desde Java6.
El bloqueo liviano está destinado a reducir la posibilidad de ingresar a múltiples mutex, no reemplazar a Mutex.
Utiliza la CPU Primitive Compare and-Swap (CAS, Instrucción de ensamblaje CMPXCHG) e intenta remediar antes de ingresar al mutex.
Si el bloqueo de sesgo falla, el sistema realizará una operación de bloqueo liviano. El propósito de su existencia es evitar utilizar Mutex en el nivel del sistema operativo tanto como sea posible, porque ese rendimiento será relativamente pobre. Debido a que JVM en sí es una aplicación, espero resolver el problema de sincronización de subprocesos en el nivel de aplicación.
Para resumir, Lightwight Blok es un método de bloqueo rápido. Antes de ingresar a Mutex, use las operaciones CAS para intentar agregar cerraduras. Trate de no usar Mutex a nivel del sistema operativo para mejorar el rendimiento.
Luego, cuando el bloqueo de sesgo falla, los pasos del bloqueo liviano:
1. Guarde el puntero de marca del encabezado del objeto en el objeto bloqueado (el objeto aquí se refiere al objeto bloqueado, como sincronizado (esto) {}, este es el objeto aquí).
LOCK-> SET_DISPLACED_HEADER (Mark);
2. Establezca el encabezado del objeto como un puntero al bloqueo (en el espacio de pila de subprocesos).
if (mark == (markoop) atomic :: cmpxchg_ptr (bloqueo, obj ()-> mark_addr (), mark)) {TeVent (slow_enter: liberar stacklock); devolver ; } La cerradura se encuentra en la pila de hilo. Por lo tanto, para determinar si un hilo contiene este bloqueo, solo determine si el espacio señalado por el encabezado del objeto está en el espacio de direcciones de la pila de subprocesos.
Si el bloqueo liviano falla, significa que hay competencia y actualización a un bloqueo de peso pesado (bloqueo normal), que es el método de sincronización a nivel del sistema operativo. En ausencia de la competencia de bloqueo, las cerraduras livianas reducen la pérdida de rendimiento causada por las cerraduras tradicionales que utilizan Mutexes OS. Cuando la competencia es muy feroz (las cerraduras livianas siempre fallan), las cerraduras livianas realizan muchas operaciones adicionales, lo que resulta en la degradación del rendimiento.
2.3 Bloqueo de giro
Cuando existe la competencia, debido a que el intento de bloqueo liviano falla, puede actualizarse directamente a un bloqueo de peso pesado para usar la exclusión mutua de nivel de sistema operativo. También es posible probar el bloqueo de giro nuevamente.
Si el hilo puede obtener el bloqueo rápidamente, entonces no puede colgar el hilo en la capa del sistema operativo, deje que el hilo realice varias operaciones vacías (giro) e intente constantemente obtener el bloqueo (similar a Trylock). Por supuesto, el número de bucles es limitado. Cuando alcanza el número de bucles, aún se actualizará a un bloqueo de peso pesado. Por lo tanto, cuando cada hilo tiene poco tiempo para sostener el bloqueo, el bloqueo de giro puede intentar evitar que los hilos se suspendan en la capa del sistema operativo.
JDK1.6 -xx:+Usespinning está habilitado
En JDK1.7, elimine este parámetro y cámbielo a una implementación incorporada.
Si el bloque de sincronización es muy largo y el giro falla, el rendimiento del sistema se degradará. Si el bloque de sincronización es muy corto y el giro es exitoso, ahorra el tiempo de conmutación de suspensión de subproces y mejora el rendimiento del sistema.
2.4 Bloqueo positivo, bloqueo liviano, resumen de bloqueo de spin
El bloqueo anterior no es un método de optimización de bloqueo a nivel de idioma Java, sino que está integrado en el JVM.
En primer lugar, las cerraduras de sesgo es evitar el consumo de rendimiento de un hilo cuando adquiere/libera repetidamente el mismo bloqueo. Si el mismo hilo aún adquiere este bloqueo, ingresará directamente al bloque de sincronización al intentar sesgar los bloqueos, y no hay necesidad de obtener el bloqueo nuevamente.
Las cerraduras livianas y los bloqueos de giro están destinados a evitar llamadas directas a las operaciones de Mutex a nivel del sistema operativo, porque suspender los subprocesos es una operación muy consumidor de recursos.
Para evitar el uso de cerraduras de peso pesado (mutex a nivel del sistema operativo), primero probaremos un bloqueo liviano. El bloqueo liviano intentará usar la operación CAS para obtener el bloqueo. Si el bloqueo liviano no puede obtener, significa que hay competencia. Pero tal vez obtenga la cerradura pronto, e intentará que los bloqueos giratorios hagan algunos bucles vacíos en el hilo e intenten obtener las cerraduras cada vez que bucee. Si el bloqueo de giro también falla, solo se puede actualizar a un bloqueo de peso pesado.
Se puede ver que las cerraduras sesgadas, las cerraduras livianas y las cerraduras de giro son cerraduras optimistas.
3. Un caso de uso de bloqueos incorrectamente
Public Class IntegerLock {Integer Static i = 0; public static class addthread extiende hilo {public void run () {for (int k = 0; k <100000; k ++) {sincronizado (i) {i ++; }}}} public static void main (string [] args) lanza interruptedException {addThread t1 = new AddThread (); AddThread t2 = new AddThread (); t1.start (); t2.start (); t1.Join (); t2.join (); System.out.println (i); }} Un error muy básico es que en el patrón de diseño de concurrencia [Alta concurrencia Java VII], el Interger no cambia, y después de cada ++, se generará un nuevo Interger y se asignará a I, por lo que los bloqueos compitieron entre los dos hilos son diferentes. Entonces no es seguro de hilo.
4. ThreadLocal y su análisis de código fuente
Puede ser un poco inapropiado mencionar ThreadLocal aquí, pero ThreadLocal es una forma de reemplazar las cerraduras. Por lo tanto, todavía es necesario mencionarlo.
La idea básica es que en un subproceso múltiple, los datos conflictivos deben bloquearse. Si se usa ThreadLocal, se proporciona una instancia de objeto para cada hilo. Diferentes hilos solo acceden a sus propios objetos, no a otros objetos. De esta manera no hay necesidad de que exista el bloqueo.
prueba de paquete; import java.text.parseException; import java.text.simpledateFormat; import java.util.date; import java.util.concurrent.executorservice; import java.util.concurrent.executors; test de clase pública {private estatic estatic final simpledate-format sdf = new-neowformat ("" " HH: MM: SS "); Public static class parsedate implementos runnables {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 = Ejecutors.NewFixedThreadPool (10); para (int i = 0; i <1000; i ++) {es.execute (nuevo parsedate (i)); }}} Dado que SimpleDateFormat no es seguro de hilo, el código anterior se usa incorrectamente. La forma más fácil es definir una clase tú mismo y envolverla con sincronizado (similar a la colección. SynchronizedMap). Esto causará problemas al hacerlo con alta concurrencia. La contención de sincronizado da como resultado un hilo que ingresa a la vez, y el volumen de concurrencia es muy bajo.
Este problema se resuelve utilizando ThreadLocal para encapsular SimpleDateFormat.
prueba de paquete; import java.text.parseException; import java.text.simpledateFormat; import java.util.date; import java.util.concurrent.executorservice; import java.util.concurrent.executors; public class test {staticLoclocal <simpledateFormat> tl = newsteTaTaTaTaTaTaTaTaTaTaTaTaMat> (SimpleStatAtlOcal> (SimpleStatAtlOtAtlOcal> (SimpleStatAtlOcal> (SimpleStatAtlOtAtlOcal> (SimpleStatAtlOtAtlOn Public static class parsedate implementos runnables {int i = 0; public parsedate (int i) {this.i = i; } public void run () {try {if (tl.get () == null) {tl.set (new SimpleDateFormat ("yyyyy-mm-dd hh: mm: ss")); } Fecha 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 = Ejecutors.NewFixedThreadPool (10); para (int i = 0; i <1000; i ++) {es.execute (nuevo parsedate (i)); }}}Cuando cada hilo se ejecute, determinará si el hilo actual tiene un objeto SimpleDateFormat.
if (tl.get () == nulo)
Si no, New SimpleDateFormat estará vinculado al hilo actual
tl.set (new SimpleDateFormat ("aaa yyy-mm-dd hh: mm: ss"));
Luego use el SimpledAteFormat del hilo actual para analizar
tl.get (). Parse ("2016-02-16 17:00:" + I % 60);
En el código inicial, solo había un SimpleDateFormat, que usaba ThreadLocal, y un SimpleDateFormat era nuevo para cada hilo.
Cabe señalar que no debe establecer un Public SimpleDateFormat en cada ThreadLocal aquí, ya que esto es inútil. Cada uno debe ser nuevo en un SimpleDateFormat.
En Hibernate, hay aplicaciones típicas para ThreadLocal.
Echemos un vistazo a la implementación del código fuente de ThreadLocal
En primer lugar, hay una variable miembro en la clase de hilo:
ThreadLocal.ThreadLocalMap ThreadLocals = null;
Y este mapa es la clave para la implementación de ThreadLocal
Public void set (t value) {Thread t = Thread.CurrentThread (); ThreadLocalMap map = getMap (t); if (map! = null) map.set (este, valor); else createMap (t, valor); } Según ThreadLocal, puede configurar y obtener el valor correspondiente.
La implementación de ThreadLocalMap aquí es similar a HASHMAP, pero existen diferencias en el manejo de conflictos hash.
Cuando se produce un conflicto hash en ThreadLocalMap, no es como HASHMAP usar listas vinculadas para resolver el conflicto, sino poner el índice ++ en el siguiente índice para resolver el conflicto.