Algunos bloqueos de uso común en Java

Reimpreso de: https://www.cnblogs.com/jyroy/p/11365935.html

Uno, cerradura pesimista y cerradura optimista

Bloqueo pesimista: para operaciones concurrentes sobre los mismos datos, el bloqueo pesimista cree que debe haber otros subprocesos para modificar los datos cuando se utilizan los datos, por lo que primero se bloqueará al adquirir los datos para garantizar que los datos no sean modificados por otros subprocesos. . En Java, la palabra clave sincronizada y la clase de implementación de Lock son bloqueos pesimistas

Bloqueo optimista: El bloqueo optimista piensa que no hará que otros subprocesos modifiquen los datos al usar los datos, por lo que no agregará bloqueos. Solo juzga si otros subprocesos han actualizado los datos antes de actualizar los datos. Si estos datos no se actualizan, el hilo actual escribe correctamente los datos modificados. Si los datos han sido actualizados por otros subprocesos, se realizan diferentes operaciones (como informes de errores o reintentos automáticos) de acuerdo con diferentes implementaciones.

Según la descripción del concepto anterior, podemos encontrar:

  • El bloqueo pesimista es adecuado para escenarios donde hay muchas operaciones de escritura. El bloqueo primero puede garantizar que los datos sean correctos durante las operaciones de escritura.

  • El bloqueo optimista es adecuado para escenarios con muchas operaciones de lectura, y la función de no bloqueo puede mejorar en gran medida el rendimiento de las operaciones de lectura.

Escriba un ejemplo a continuación para verificar:

public class Test {
    public int number = 1;
    /**  ------------------------ 悲观锁实现 start-------------------**/
    // 方式一:使用synchronized关键字
    public synchronized void testMethod(){
        number ++;// 操作同步资源
    }
    // 方式二:使用lock锁,需要保证多个线程使用同一个锁
    private ReentrantLock lock = new ReentrantLock();
    public void modify(){
        lock.lock();
        number ++;// 操作同步资源
        lock.unlock();
    }
    /**  ------------------------ 悲观锁实现 end-------------------**/

    /**  ------------------------ 乐观锁实现 start-------------------**/
    // java.util.concurrent包中的原子类
    private AtomicInteger atomicInteger = new AtomicInteger();
    public int increment(){
        return atomicInteger.incrementAndGet();// 执行自增1
    }
    /**  ------------------------ 乐观锁实现 end-------------------**/
}

Simular solicitudes simultáneas

package com.xiateng;

public class MyThread extends Thread{
    private Test test;
    public MyThread(Test test){
        this.test = test;
    }
    @Override
    public void run(){
        try {
            Thread.sleep(1000);// 模拟逻辑处理时间
            int increment = test.increment();
            System.out.println("number = "+ increment);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 Iniciar 10 subprocesos para procesar la prueba de recursos lo averiguará;

class test1{
    public static void main(String[] args) {
        Test test = new Test();
        for (int i = 1; i < 10; i++){
            MyThread thread = new MyThread(test);
            new Thread(thread).start();
        }
    }
}

Si no usa bloqueos: los subprocesos no se pueden ejecutar secuencialmente, lo que genera datos sucios

Use bloqueos pesimistas: opere los recursos de sincronización después del bloqueo explícito

Utilice el bloqueo optimista: opere directamente los recursos de sincronización

¿Por qué los bloqueos optimistas pueden manipular directamente los recursos de sincronización y lograr la sincronización de subprocesos?

El bloqueo optimista se realiza a través de CAS. El nombre completo de CAS es Compare And Swap (Compare And Swap), que es un algoritmo sin bloqueo. Realice una sincronización variable entre varios subprocesos sin utilizar bloqueos (no se bloquean los subprocesos).

El algoritmo CAS involucra tres operandos:

  • El valor de memoria V que debe leerse y escribirse.

  • El valor A que se va a comparar.

  • El nuevo valor B que se escribirá.

Si y solo si el valor de V es igual a A, CAS usa el nuevo valor B para actualizar el valor de V atómicamente (la comparación + actualización es una operación atómica en su conjunto ), de lo contrario no realizará ninguna operación. En general, "actualizar" es una operación que se reintenta constantemente.

El código fuente es el siguiente:

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = this.getIntVolatile(o, offset);
        } while(!this.compareAndSwapInt(0, offset, v, v + delta));

        return var5;
    }

De acuerdo con el código fuente de OpenJDK 8, podemos ver que el bucle getAndAddInt () obtiene el valor v en el desplazamiento en el objeto dado o, y luego juzga si el valor de la memoria es igual av. Si son iguales, el valor de memoria se establece en v + delta; de lo contrario, devuelve falso y el bucle continúa reintentando, hasta que la configuración sea exitosa, se puede salir del bucle y se devuelve el valor anterior. Toda la operación "comparar + actualizar" está encapsulada en compareAndSwapInt (), que se completa con una instrucción de CPU en JNI. Es una operación atómica y puede garantizar que varios subprocesos puedan ver el valor modificado de la misma variable.

El JDK subsiguiente usa la instrucción cmpxchg de la CPU para comparar la A en el registro con el valor V en la memoria. Si son iguales, almacene el nuevo valor B para escribirlo en la memoria. Si no son iguales, el valor de memoria V se asigna al valor A en el registro. Luego, vuelva a llamar a la instrucción cmpxchg a través del bucle while en el código Java para volver a intentarlo hasta que la configuración sea correcta.

Aunque CAS es muy eficiente, existen tres problemas:

1. Problema ABA, CAS necesita verificar si el valor de la memoria ha cambiado al operar el valor, y actualizará el valor de la memoria si no hay ningún cambio, pero si el principio del valor de la memoria es A, entonces se convierte en B, y luego se convierte en A, entonces la verificación CAS pensará que el valor de la memoria no ha cambiado, y la solución puede ser agregar el número de versión delante de la variable.

2. Cuando el tiempo del ciclo es largo, la sobrecarga es relativamente grande. Si la operación CAS no tiene éxito durante mucho tiempo, hará que gire todo el tiempo, lo que genera una gran sobrecarga para la CPU.

3. Solo se puede garantizar el funcionamiento atómico de una variable compartida. Si hay múltiples variables compartidas, CAS no puede garantizar la atomicidad de la operación.

2. Bloqueo de giro y bloqueo de giro adaptativo

Entonces, ¿qué es un bloqueo giratorio?

El bloqueo o la activación de un subproceso de Java requiere que el sistema operativo cambie el estado de la CPU para completar. El cambio de estado requiere tiempo de procesador. Si el procesamiento lógico del bloque de código sincronizado es relativamente simple, el cambio de estado puede llevar más tiempo que el tiempo de ejecución del código. La ganancia no vale la pena perder.

Para dejar que el hilo actual "espere un rato", necesitamos girar el hilo actual. Si el bloqueo anterior se ha liberado después de que se completa el giro, entonces el hilo actual puede adquirir el bloqueo directamente sin bloquear, evitando así el cambio hilos La sobrecarga, este es el bloqueo de giro.

Ventajas y desventajas: aunque la espera de giro evita la sobrecarga del cambio de hilo, consume tiempo del procesador. Si la cerradura está ocupada por poco tiempo, el efecto de giro será muy bueno. Por el contrario, si la cerradura está ocupada por mucho tiempo tiempo, entonces los subprocesos auto-Spinning solo desperdiciarán recursos del procesador adiós, por lo que el tiempo de espera del giro debe tener un cierto límite. Si el giro excede el número limitado de veces y el bloqueo no se obtiene con éxito, el hilo actual se suspenderá.

El bloqueo de giro se introdujo en JDK1.4.2. Use -XX: + UseSpinning para activarlo. En JDK 6, está activado de forma predeterminada y se introduce un bloqueo de giro adaptativo (bloqueo de giro adaptativo).

Adaptable significa que el tiempo (número de veces) del giro ya no es fijo, sino que está determinado por el tiempo de giro anterior en la misma cerradura y el estado del propietario de la cerradura. Si en el mismo objeto de bloqueo, la espera de giro acaba de adquirir el bloqueo con éxito y el subproceso que mantiene el bloqueo se está ejecutando, entonces la máquina virtual pensará que es probable que este giro también tenga éxito nuevamente y permitirá el giro La espera dura durante un tiempo relativamente más largo. Si el giro rara vez se obtiene con éxito para un determinado bloqueo, es posible omitir el proceso de giro al intentar adquirir el bloqueo en el futuro y bloquear directamente el hilo para evitar desperdiciar recursos del procesador.

Tres, las cuatro etapas de evolución de sincronizado: sin bloqueo -> bloqueo sesgado -> bloqueo ligero -> bloqueo pesado

1 、 sincronizado

Introducción

Sincronizado es una palabra clave a nivel de jvm, que puede sincronizar una determinada pieza de código. A diferencia de ReentrantLock, no se puede interrumpir.

Monitor

Monitor puede entenderse como una herramienta de sincronización o un mecanismo de sincronización, generalmente descrito como un objeto. Cada objeto Java tiene un bloqueo invisible llamado bloqueo interno o bloqueo de monitor.

Monitor es una estructura de datos privada de subprocesos.Cada subproceso tiene una lista de registros de monitor disponibles y una lista global de registros de monitor disponibles. Cada objeto bloqueado está asociado con un monitor, y hay un campo Propietario en el monitor para almacenar el identificador único del subproceso que posee el bloqueo, lo que indica que el bloqueo está ocupado por este subproceso.

La sincronización de subprocesos sincronizada se logra a través del Monitor, que se basa en el bloqueo de mutex (bloqueo de exclusión mutua) del sistema operativo subyacente para lograr la sincronización de subprocesos.

Encabezado de objeto Java

El encabezado del objeto incluye principalmente dos partes de datos: marca de palabra (campo marcado), puntero de Klass (puntero de tipo)

Marcar palabra: El código Hash, la edad de generación y la información de la bandera de bloqueo del objeto se almacenan de forma predeterminada. (La relación entre el contenido almacenado en Mark Word y la bandera de bloqueo es la siguiente)

Punto de Klass: el puntero del objeto a sus metadatos de clase. La máquina virtual utiliza este puntero para determinar qué instancia de clase es este objeto.
Inserte la descripción de la imagen aquí
Sincronizado antes de JDK1.6: cuando se utilizan bloqueos sin giro (mutex), "el bloqueo o la activación de un subproceso de Java requiere que el sistema operativo cambie el estado de la CPU para completarse, y esta transición de estado requiere tiempo de procesador. Si sincroniza el bloque de código El contenido de es demasiado simple y el tiempo consumido por la transición de estado puede ser más largo que el tiempo de ejecución del código de usuario ". Este método es la forma en que se sincronizó la sincronización realizada originalmente, que es la razón de la baja eficiencia de la sincronización antes de JDK 6. Este tipo de bloqueo que depende del sistema operativo Mutex Lock se denomina "bloqueo pesado". Con el fin de reducir el costo de rendimiento de adquirir y liberar bloqueos, JDK 6 introdujo "bloqueos sesgados" y "bloqueos ligeros".

2. Sin candado

Ningún bloqueo no bloquea el recurso, todos los subprocesos pueden acceder y modificar el mismo recurso, pero solo un subproceso puede modificarlo correctamente al mismo tiempo.

La característica de lock-free es que la operación de modificación se realiza en un bucle y el hilo intentará modificar constantemente el recurso compartido. Si no hay conflicto, la modificación se realiza correctamente y se cierra; de lo contrario, el bucle seguirá intentándolo. Si varios subprocesos modifican el mismo valor, uno de los subprocesos debe poder modificarlo correctamente, y otros subprocesos que no se modifiquen continuarán intentándolo hasta que la modificación sea exitosa. El principio y la aplicación de CAS que presentamos anteriormente es la realización de lock-free. Sin candado no se puede reemplazar completamente con candado, pero el rendimiento de sin candado en algunas situaciones es muy alto.

3. Bloqueo de sesgo

Un bloqueo sesgado significa que un hilo ha accedido a un fragmento de código sincronizado, luego el hilo adquirirá automáticamente el bloqueo, reduciendo el costo de adquirir el bloqueo.

En la mayoría de los casos, el bloqueo siempre lo adquiere el mismo subproceso varias veces y no hay competencia de subprocesos múltiples, por lo que hay un bloqueo sesgado. El objetivo es mejorar el rendimiento cuando solo un subproceso ejecuta bloques de código sincronizados.

Cuando un subproceso accede al bloque de código sincronizado y adquiere el bloqueo, el ID de subproceso del sesgo del bloqueo se almacena en la palabra de marca. Cuando el hilo entra y sale del bloque sincronizado, la operación CAS ya no se utiliza para bloquear y desbloquear, sino para comprobar si la palabra de marca almacena un bloqueo sesgado que apunta al hilo actual. La introducción de bloqueos sesgados es para minimizar las rutas de ejecución de bloqueos ligeros innecesarios sin competencia de subprocesos múltiples, porque la adquisición y liberación de bloqueos ligeros se basan en múltiples instrucciones atómicas CAS, y los bloqueos sesgados solo necesitan reemplazar el ThreadID Simplemente confíe en la instrucción atómica CAS una vez.

El bloqueo sesgado solo liberará el bloqueo cuando otros hilos intenten competir por el bloqueo sesgado, y el hilo no liberará el bloqueo sesgado de forma activa. Para revocar un bloqueo sesgado, debe esperar un punto de seguridad global (en este punto donde no se ejecuta ningún código de bytes), primero suspenderá el hilo que posee el bloqueo sesgado para determinar si el objeto de bloqueo está bloqueado. Después de revocar el bloqueo de polarización, vuelva al estado sin bloqueo (el bit de bandera es "01") o al bloqueo ligero (el bit de bandera es "00").

El bloqueo de polarización está habilitado de forma predeterminada en la JVM de JDK 6 y posteriores. Puede desactivar el bloqueo sesgado mediante el parámetro JVM: -XX: -UseBiasedLocking = false. Después de cerrar, el programa entrará en el estado de bloqueo ligero de forma predeterminada.

4. Cerradura ligera

Significa que cuando el candado es un candado sesgado y se accede por otro hilo, el candado sesgado se actualizará a un candado ligero, y otros hilos intentarán adquirir el candado en forma de giro sin bloquear, mejorando así el rendimiento.

Cuando el código ingresa al bloque de sincronización, si el estado de bloqueo del objeto de sincronización está desbloqueado (el indicador de bloqueo está en el estado "01", si es un bloqueo sesgado es "0"), la máquina virtual primero creará uno en el marco de pila del subproceso actual El espacio denominado Registro de bloqueo se utiliza para almacenar una copia de la palabra de marca actual del objeto de bloqueo y luego copiar la palabra de marca en el encabezado del objeto al registro de bloqueo.

Después de que la copia sea exitosa, la máquina virtual utilizará la operación CAS para intentar actualizar la palabra de marca del objeto a un puntero al registro de bloqueo, y apuntará el puntero del propietario en el registro de bloqueo a la palabra de marca del objeto.

Si la acción de actualización es exitosa, entonces el subproceso tiene el bloqueo del objeto y el indicador de bloqueo del objeto Mark Word se establece en "00", lo que indica que el objeto está en un estado de bloqueo ligero.

Si la operación de actualización del bloqueo ligero falla, la máquina virtual primero verificará si la marca de palabra del objeto apunta al marco de pila del hilo actual. Si es así, significa que el hilo actual ya posee el bloqueo de este objeto. , por lo que puede ingresar directamente al bloque de sincronización para continuar. Ejecutar, de lo contrario significa que varios subprocesos compiten por bloqueos.

Si solo hay un hilo en espera, el hilo espera girando. Pero cuando el giro excede un cierto número de veces, o un hilo mantiene un candado, otro gira y hay una tercera visita, el candado ligero se actualiza a un candado pesado.

5. Bloqueo de peso pesado

Al actualizar a un candado pesado, el valor de estado de la bandera del candado se convierte en "10". En este momento, el puntero al candado pesado se almacena en la palabra de marca. En este momento, los subprocesos que esperan el candado entrarán en el bloqueo Expresar.

Resumen: El bloqueo parcial resuelve el problema de bloqueo al comparar Mark Word y evita la ejecución de operaciones CAS. El candado ligero resuelve el problema de bloqueo mediante el uso de operaciones CAS y giros para evitar el bloqueo de subprocesos y la activación que afectan el rendimiento. Los candados pesados ​​bloquean todos los hilos excepto el hilo que posee el candado.

Cuatro, cerradura justa y cerradura injusta

Cerradura justa

Un bloqueo justo significa que varios subprocesos adquieren el bloqueo en el orden en que solicitan el bloqueo. El subproceso entra directamente en la cola y el primer subproceso de la cola puede adquirir el bloqueo. La ventaja de un candado justo es que los hilos que esperan el candado no morirán de hambre. La desventaja es que la eficiencia de rendimiento general es menor que la de los bloqueos injustos. Todos los subprocesos en la cola de espera, excepto el primer subproceso, se bloquearán y la sobrecarga de despertar los subprocesos bloqueados por la CPU es mayor que la de los bloqueos injustos.

Bloqueo injusto

Los bloqueos injustos son cuando varios subprocesos intentan adquirir el bloqueo directamente cuando están bloqueados, y esperarán hasta el final de la cola de espera hasta que no se puedan adquirir. Pero si el bloqueo solo está disponible en este momento, entonces este subproceso puede adquirir el bloqueo directamente sin bloquear, por lo que puede ocurrir un bloqueo injusto cuando el subproceso que solicita el bloqueo adquiere el bloqueo por primera vez. La ventaja de los bloqueos injustos es que pueden reducir la sobrecarga de invocar subprocesos, y la eficiencia de rendimiento general es alta, porque los subprocesos tienen la posibilidad de obtener bloqueos directamente sin bloqueos y la CPU no tiene que activar todos los subprocesos. La desventaja es que los hilos en la cola de espera pueden morir de hambre o esperar mucho tiempo antes de adquirir el candado.

A continuación, explicaremos los bloqueos justos y los bloqueos injustos a través del código fuente de ReentrantLock.

De acuerdo con el código, hay una clase interna Sync en ReentrantLock. Sync hereda AQS (AbstractQueuedSynchronizer). La mayoría de las operaciones de agregar bloqueos y liberar bloqueos se implementan realmente en Sync. Tiene dos subcategorías: FairSync y NonfairSync. ReentrantLock usa bloqueos injustos de forma predeterminada, y también puede especificar explícitamente el uso de bloqueos justos a través del constructor.

 Ingrese el método hasQueuedPredecessors ()

Encontraremos que este método es principalmente para determinar si el hilo actual es el primero en la cola de sincronización, si lo es, devuelve verdadero, de lo contrario devuelve falso.

para resumir

El bloqueo justo es darse cuenta de que varios subprocesos adquieren bloqueos en el orden en el que se aplican a los bloqueos sincronizando la cola, y de ese modo se dan cuenta de las características de equidad. Cuando se bloquea el bloqueo injusto, no se considera la cola de espera, y se intenta directamente el bloqueo para obtener el bloqueo, por lo que se da el caso de que el bloqueo se obtiene después de que se aplica la aplicación.

Cinco, cerraduras reentrantes y cerraduras no reentrantes

El bloqueo reentrante, también conocido como bloqueo recursivo, significa que cuando el mismo subproceso adquiere el bloqueo en el método externo, el método interno del subproceso adquirirá automáticamente el bloqueo (siempre que el objeto de bloqueo debe ser el mismo objeto o clase). porque se ha adquirido antes pero no se ha lanzado. Tanto ReentrantLock como sincronizados en Java son bloqueos reentrantes Una ventaja de los bloqueos reentrantes es que los interbloqueos se pueden evitar hasta cierto punto.

El siguiente código de muestra se utiliza para el análisis:

 En el código anterior, los dos métodos de la clase son modificados por el bloqueo integrado sincronizado, y el método doOthers () se llama en el método doSomething (). Debido a que el bloqueo incorporado es reentrante, el mismo hilo puede obtener directamente el bloqueo del objeto actual al llamar a doOthers () e ingresar doOthers () para la operación.

Si es un bloqueo no reentrante, el subproceso actual debe liberar el bloqueo del objeto actual adquirido durante doSomething () antes de llamar a doOthers (). De hecho, el bloqueo del objeto ha sido retenido por el subproceso actual y no se puede liberar. Entonces habrá un punto muerto en este momento.

¿Por qué los bloqueos reentrantes pueden llamar repetidamente a los recursos sincronizados?

Tome ReentrantLock como ejemplo. AQS, la clase principal de ReentrantLock, mantiene un estado de estado de sincronización internamente para contar el número de reentradas. El valor inicial del estado es 0.

Cuando un subproceso intenta adquirir un bloqueo, el bloqueo reentrante primero intenta adquirir y actualizar el valor de estado. Si el estado == 0 significa que ningún otro subproceso está ejecutando el código de sincronización, el estado se establece en 1 y el subproceso actual comienza a ejecutarse . Si status! = 0, juzgue si el subproceso actual es el subproceso que ha adquirido el bloqueo, si lo es, ejecute el estado + 1, y el subproceso actual puede adquirir el bloqueo nuevamente. El bloqueo no reentrante es para adquirir directamente e intentar actualizar el valor del estado actual. Si status! = 0, no podrá adquirir el bloqueo y se bloqueará el hilo actual.

Cuando se libera el bloqueo, el bloqueo reentrante también obtiene primero el valor del estado actual, siempre que el hilo actual sea el hilo que sostiene el bloqueo. Si status-1 == 0, significa que todas las operaciones repetidas de adquisición de bloqueo del hilo actual se han ejecutado, y entonces el hilo realmente liberará el bloqueo. El bloqueo no reentrante es establecer directamente el estado en 0 después de determinar que el hilo actual es el hilo que sostiene el bloqueo y liberar el bloqueo.

Seis, candado exclusivo y candado compartido

Las cerraduras exclusivas y las cerraduras compartidas también son un concepto. Primero presentamos los conceptos específicos y luego presentamos bloqueos exclusivos y bloqueos compartidos a través del código fuente de ReentrantLock y ReentrantReadWriteLock.

Un candado exclusivo también se llama candado exclusivo, lo que significa que el candado solo puede ser retenido por un hilo a la vez. Si el subproceso T agrega un bloqueo exclusivo a los datos A, otros subprocesos ya no pueden agregar ningún tipo de bloqueo a A. El hilo que obtiene el bloqueo exclusivo puede leer los datos y modificarlos. Las clases de implementación de sincronizado en JDK y Lock in JUC son bloqueos de exclusión mutua.

Bloqueo compartido significa que el bloqueo puede ser retenido por varios subprocesos. Si el subproceso T agrega un bloqueo compartido a los datos A, otros subprocesos solo pueden agregar un bloqueo compartido a A y no pueden agregar un bloqueo exclusivo. El hilo que obtiene el bloqueo compartido solo puede leer los datos y no puede modificarlos.

Las cerraduras exclusivas y las cerraduras compartidas también se realizan a través de AQS, implementando diferentes métodos se pueden realizar cerraduras exclusivas o compartidas.

La siguiente figura muestra parte del código fuente de ReentrantReadWriteLock:

 

Vemos que ReentrantReadWriteLock tiene dos bloqueos: ReadLock y WriteLock, que se entienden por palabras, un bloqueo de lectura y un bloqueo de escritura, denominados colectivamente "bloqueo de lectura-escritura". Se pueden encontrar más observaciones de que ReadLock y WriteLock son bloqueos implementados por la clase interna Sync. Sync es una subclase de AQS, y esta estructura también existe en CountDownLatch, ReentrantLock y Semaphore.

En ReentrantReadWriteLock, el cuerpo principal del bloqueo de lectura y escritura es Sync, pero el método de bloqueo de bloqueo de lectura y bloqueo de escritura es diferente. Los bloqueos de lectura son bloqueos compartidos y los bloqueos de escritura son bloqueos exclusivos. El bloqueo compartido del bloqueo de lectura puede garantizar que la lectura simultánea sea muy eficiente y que los procesos de lectura, escritura, lectura y escritura sean mutuamente excluyentes, porque el bloqueo de lectura y el bloqueo de escritura están separados. Por lo tanto, la simultaneidad de ReentrantReadWriteLock se ha mejorado mucho en comparación con los bloqueos mutex generales.

¿Cuál es la diferencia entre el método de bloqueo específico de bloqueo de lectura y bloqueo de escritura? Antes de comprender el código fuente, necesitamos revisar otros conocimientos.

Cuando mencionamos AQS por primera vez, también mencionamos el campo de estado (tipo int, 32 bits), que se usa para describir cuántos subprocesos mantienen bloqueos.

En un bloqueo exclusivo, este valor suele ser 0 o 1 (si es un bloqueo reentrante, el valor de estado es el número de reentrantes), y en un bloqueo compartido, el estado es el número de bloqueos retenidos. Pero hay dos bloqueos para leer y escribir en ReentrantReadWriteLock, por lo que debe describir el número de bloqueos de lectura y bloqueos de escritura (o estado) en un estado de variable entera. Entonces, la variable de estado se "corta bit a bit" en dos partes, los 16 bits superiores representan el estado de bloqueo de lectura (número de bloqueos de lectura) y los 16 bits inferiores representan el estado de bloqueo de escritura (número de bloqueos de escritura). Como se muestra abajo:

Después de entender el concepto, veamos el código, primero miremos el código fuente de bloqueo del bloqueo de escritura:

  • Este código primero obtiene el número de bloqueos actuales cy luego usa c para obtener el número de bloqueos de escritura w. Debido a que el bloqueo de escritura son los 16 bits inferiores, tome el valor máximo de los 16 bits inferiores y realice la operación AND con la corriente c (int w = exclusiveCount (c);), los 16 bits superiores y 0 son Y después de la operación es 0 y el resto es el bit inferior. El valor de la operación también es el número de subprocesos que mantienen el bloqueo de escritura.

  • Después de obtener el número de subprocesos de bloqueo de escritura, primero determine si alguno de los subprocesos ya mantiene el bloqueo. Si ya hay un subproceso que mantiene el bloqueo (c! = 0), verifique el número actual de subprocesos de bloqueo de escritura. Si el número de subprocesos de escritura es 0 (es decir, hay un bloqueo de lectura en este momento) o el subproceso mantener el bloqueo no es el hilo actual, devolverá la falla (que implica la realización de bloqueos justos y bloqueos injustos).

  • Si el número de bloqueos de escritura es mayor que el número máximo (65535, 2 elevado a la 16ª potencia -1), se produce un error.

  • Si el número de subprocesos de escritura es 0 (entonces el subproceso de lectura también debe ser 0, porque la situación c! = 0 se ha manejado anteriormente), y el subproceso actual debe bloquearse, devuelva el error; si aumenta el número de escrituras subprocesos a través de CAS falla, también devuelve falla.

  • Si c = 0, w = 0 o c> 0, w> 0 (reentrante), establezca el propietario del hilo actual o bloquee y devuelva el éxito.

Además de la condición de reentrada (el hilo actual es el hilo que ha adquirido el bloqueo de escritura), tryAcquire () agrega un juicio para la existencia de un bloqueo de lectura. Si hay un bloqueo de lectura, no se puede adquirir el bloqueo de escritura, porque se debe garantizar que la operación del bloqueo de escritura sea visible para el bloqueo de lectura. Si se permite que el bloqueo de lectura adquiera el bloqueo de escritura cuando se ha adquirido, entonces otros subprocesos del lector se están ejecutando Es imposible percibir el funcionamiento del subproceso del escritor actual.

Por lo tanto, el bloqueo de escritura puede ser adquirido por el subproceso actual solo después de esperar a que otros subprocesos del lector liberen el bloqueo de lectura, y una vez que se adquiere el bloqueo de escritura, se bloquean los accesos posteriores de otros subprocesos de lectura y escritura. El proceso de liberación del bloqueo de escritura es básicamente similar al proceso de liberación de ReentrantLock. Cada liberación reduce el estado de escritura. Cuando el estado de escritura es 0, significa que se ha liberado el bloqueo de escritura, y luego los subprocesos de lectura y escritura en espera pueden continuar para acceder al bloqueo de lectura y escritura La modificación del hilo de escritura es visible para los hilos de lectura y escritura posteriores.

El siguiente es el código para leer el candado:

Se puede ver que en el método tryAcquireShared (int unused), si otros subprocesos ya han adquirido el bloqueo de escritura, el subproceso actual no logra adquirir el bloqueo de lectura y entra en el estado de espera. Si el subproceso actual adquiere el bloqueo de escritura o no se adquiere el bloqueo de escritura, el subproceso actual (seguridad del subproceso, confiando en la garantía CAS) aumenta el estado de lectura y adquiere con éxito el bloqueo de lectura. Cada liberación del bloqueo de lectura (seguro para subprocesos, varios subprocesos del lector pueden liberar el bloqueo de lectura al mismo tiempo) reduce el estado de lectura y el valor reducido es "1 << 16". Por lo tanto, el bloqueo de lectura-escritura puede compartir el proceso de lectura y lectura, y los procesos de lectura, escritura, lectura y escritura son mutuamente excluyentes.

En este punto, echemos un vistazo al código fuente del bloqueo justo y el bloqueo injusto en el mutex ReentrantLock:

 

Descubrimos que aunque hay candados justos y candados injustos en ReentrantLock, todos agregan candados exclusivos. De acuerdo con el código fuente, cuando un subproceso llama al método de bloqueo para adquirir un bloqueo, si el recurso de sincronización no está bloqueado por otros subprocesos, el subproceso actual se apropiará exitosamente del recurso después de actualizar exitosamente el estado con CAS. Si el recurso público está ocupado y no está ocupado por el subproceso actual, el bloqueo fallará. Por lo tanto, se puede determinar que los bloqueos agregados de ReentrantLock son bloqueos exclusivos independientemente de la operación de lectura o escritura.

Siete, bloqueo explícito y bloqueo implícito

Bloqueo explícito: Bloqueo Bloqueo implícito: Sincronizar

¿Cuál es la diferencia entre bloqueo explícito y bloqueo implícito?

1. Diferentes niveles

Sincronizar: la JVM mantiene las palabras clave en Java. Es un bloqueo a nivel de JVM.

Lock: Es una clase específica que solo apareció después de JDK5. Usar bloqueo es llamar a la API correspondiente. Es un bloqueo a nivel de API

2. Diferentes formas de uso

Sincronizar: no hay necesidad de bloquear y desbloquear manualmente, el sistema liberará automáticamente el bloqueo, que es mantenido por el sistema, y ​​generalmente no se producirá un punto muerto a menos que haya un problema lógico

Bloqueo: es necesario bloquear y desbloquear manualmente, no el desbloqueo manual provocará un interbloqueo. Método de bloqueo manual: lock.lock (). Suelte el bloqueo: método de desbloqueo. Necesita cooperar con el bloque de declaraciones tyr / finaly para completar.

3. Si se puede interrumpir la espera

Sincronizar: no se puede interrumpir a menos que se produzca una excepción o se complete la ejecución.

Bloqueo: se puede interrumpir. Llame al método set timeout tryLock (timeout largo, unidad timeUnit), luego llame a lockInterruptiblemente () en el bloque de código y luego llame al método interrupt () para interrumpir

4. ¿Es un candado justo?

Sincronizar: bloqueo injusto

Lock: Ambos están bien. El valor predeterminado es bloqueo injusto. Puede pasar valores booleanos en su método de construcción.

5. El candado vincula múltiples condiciones a la condición.

Sincronizar: active aleatoriamente un hilo o active todos los hilos

Bloqueo: se usa para darse cuenta del hilo que debe ser despertado por el despertador grupal, y puede despertarse con precisión

6 、

 

 

Supongo que te gusta

Origin blog.csdn.net/qq_43037478/article/details/111625768
Recomendado
Clasificación