JUC Conferencia 11: Explicación detallada del bloqueo JUC LockSupport

JUC Conferencia 11: Explicación detallada del bloqueo JUC LockSupport

Este artículo es la undécima conferencia de JUC: Explicación detallada del bloqueo JUC LockSupport. LockSupport es la base de las cerraduras y una clase de herramienta que proporciona mecanismos de cerradura.

1. Comprenda las preguntas de la entrevista de las principales empresas BAT.

Continúe con estas preguntas, que le ayudarán en gran medida a comprender mejor los puntos de conocimiento relevantes.

  • ¿Por qué LockSupport también es una clase básica central? El marco AQS se basa en dos clases: Unsafe (que proporciona operaciones CAS) y LockSupport (que proporciona operaciones de estacionamiento/desestacionamiento)
  • ¿Escriba cómo lograr la sincronización a través de estacionar/desaparcar de esperar/notificar y LockSupport respectivamente?
  • ¿LockSupport.park() liberará el recurso de bloqueo? ¿Qué pasa con Condition.await()?
  • ¿Cuáles son las diferencias entre Thread.sleep(), Object.wait(), Condition.await() y LockSupport.park()? Puntos clave
  • ¿Qué sucede si se ejecuta notificar() antes de esperar()?
  • ¿Qué sucede si se ejecuta unpark() antes de park()?

2. Introducción a LockSupport

LockSupport es la primitiva básica de bloqueo de subprocesos utilizada para crear bloqueos y otras clases de sincronización . En resumen, al llamar a LockSupport.park, significa que el hilo actual esperará hasta que se obtenga el permiso. Al llamar a LockSupport.unpark, el hilo que espera el permiso debe pasarse como parámetro para que este hilo pueda continuar ejecutándose.

3. Análisis del código fuente de LockSupport

3.1 Atributos de clase

public class LockSupport {
    
    
    // Hotspot implementation via intrinsics API
    private static final sun.misc.Unsafe UNSAFE;
    // 表示内存偏移地址
    private static final long parkBlockerOffset;
    // 表示内存偏移地址
    private static final long SEED;
    // 表示内存偏移地址
    private static final long PROBE;
    // 表示内存偏移地址
    private static final long SECONDARY;
    
    static {
    
    
        try {
    
    
            // 获取Unsafe实例
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            // 线程类类型
            Class<?> tk = Thread.class;
            // 获取Thread的 parkBlocker 字段的内存偏移地址
            parkBlockerOffset = UNSAFE.objectFieldOffset(tk.getDeclaredField("parkBlocker"));
            // 获取Thread的 threadLocalRandomSeed 字段的内存偏移地址
            SEED = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSeed"));
            // 获取Thread的 threadLocalRandomProbe 字段的内存偏移地址
            PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));
            // 获取Thread的 threadLocalRandomSecondarySeed 字段的内存偏移地址
            SECONDARY = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) {
    
     
          	throw new Error(ex); 
        }
    }
}

Descripción: el campo INSEGURO representa sun.misc.Unsafela clase, vea su código fuente, clase mágica Java: análisis de aplicaciones inseguras , las llamadas directas no están permitidas en programas generales y el tipo largo representa la dirección de desplazamiento del campo correspondiente del objeto de instancia en la memoria. y la dirección de compensación se puede utilizar para obtener o establecer el valor de este campo.

3.2 Constructor de clase

// 私有构造函数,无法被实例化
private LockSupport() {
    
    }

Nota: LockSupport tiene solo un constructor privado y no se puede crear una instancia.

3.3 Análisis de funciones centrales

Antes de analizar la función LockSupport, primero introduzca sun.misc.Unsafelas funciones de estacionamiento y desestacionamiento en la clase, porque las funciones principales de LockSupport se basan en las funciones de estacionamiento y desestacionamiento definidas en la clase Unsafe. Las definiciones de las dos funciones se dan a continuación:

public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);

Descripción: La descripción de las dos funciones es la siguiente:

  • La función de estacionamiento bloquea el hilo y el hilo se bloqueará hasta que ocurran las siguientes condiciones :
    • ① Llame a la función de desestacionar para liberar el permiso del hilo;
    • ② El hilo está interrumpido;
    • ③ Es hora de configurar. Y, cuando el tiempo es absoluto, isAbsolute es verdadero; de lo contrario, isAbsolute es falso. Cuando el tiempo es 0, significa esperar indefinidamente hasta que se desestacione.
  • La función unpark libera el permiso del hilo, es decir, activa el hilo que fue bloqueado después de llamar a park . Esta función no es segura. Asegúrese de que el hilo aún esté activo cuando llame a esta función.
1. función de estacionamiento

Hay dos versiones sobrecargadas de la función de estacionamiento. El resumen del método es el siguiente:

public static void park()public static void park(Object blocker)

Nota : La diferencia entre las dos funciones es que la función park() no tiene un bloqueador, es decir, el campo parkBlocker del hilo no está configurado. La función de tipo park(Object) es la siguiente.

public static void park(Object blocker) {
    
    
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 设置Blocker
    setBlocker(t, blocker);
    // 获取许可
    UNSAFE.park(false, 0L);
    // 重新可运行后再此设置Blocker
    setBlocker(t, null);
}

Nota : Al llamar a la función de estacionamiento, primero obtenga el subproceso actual y luego configure el campo parkBlocker del subproceso actual, es decir, llame a la función setBlocker, luego llame a la función de estacionamiento de la clase Unsafe y luego llame a la función setBlocker. Entonces la pregunta es, ¿ por qué necesitamos llamar a la función setBlocker dos veces en esta función de estacionamiento ?

La razón es realmente muy simple: al llamar a la función de estacionamiento, el subproceso actual primero establece el campo parkBlocker y luego llama a la función de estacionamiento de Unsafe. Después de eso, el subproceso actual se ha bloqueado y está esperando que se llame a la función de desbloqueo del subproceso, por lo que la siguiente función setBlocker no se puede ejecutar, se llama a la función unpark. Después de que el hilo obtiene permiso, puede continuar ejecutándose, es decir, se ejecuta el segundo setBlocker y el campo parkBlocker del hilo se establece en nulo, completando así la lógica. de toda la función del parque. Si no hay un segundo setBlocker, entonces no se llama a park (Bloqueador de objetos) más adelante, pero se llama directamente a la función getBlocker y se obtiene el bloqueador establecido por el park (Bloqueador de objetos) anterior, lo cual obviamente es ilógico . En resumen, se debe garantizar que después de ejecutar toda la función park (bloqueador de objetos), el campo parkBlocker del hilo vuelva a ser nulo. Por lo tanto, la función setBlocker debe llamarse dos veces en la función park(Object). El método setBlocker es el siguiente:

private static void setBlocker(Thread t, Object arg) {
    
    
    // 设置线程t的parkBlocker字段的值为arg
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}

Descripción: este método se utiliza para establecer el valor del campo parkBlocker del subproceso t en arg.

Otra versión sobrecargada sin parámetros, la función park() es la siguiente.

public static void park() {
    
    
    // 获取许可,设置时间为无限长,直到可以获取许可
    UNSAFE.park(false, 0L);
}

Nota : Después de llamar a la función de estacionamiento, el hilo actual se deshabilitará a menos que haya permiso disponible. El hilo actual estará en un estado inactivo hasta que ocurra una de las siguientes tres situaciones, es decir, cuando ocurran las siguientes situaciones, el hilo actual obtendrá permiso y podrá continuar ejecutándose.

  • Algún otro hilo llama a desaparcar con el hilo actual como objetivo;
  • Algún otro hilo interrumpe el hilo actual;
  • La llamada regresa de manera ilógica (es decir, sin motivo).
2. función parkNanos

Esta función deshabilita el hilo actual y espera hasta el tiempo de espera especificado antes de que el permiso esté disponible. Las funciones específicas son las siguientes.

public static void parkNanos(Object blocker, long nanos) {
    
    
    if (nanos > 0) {
    
     // 时间大于0
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 设置Blocker
        setBlocker(t, blocker);
        // 获取许可,并设置了时间
        UNSAFE.park(false, nanos);
        // 设置许可
        setBlocker(t, null);
    }
}

Nota: Esta función también llama dos veces a la función setBlocker.El parámetro nanos representa el tiempo relativo y cuánto tiempo esperar.

3. función parkUntil

Esta función significa deshabilitar el hilo actual antes del límite de tiempo especificado a menos que haya permiso disponible. La función específica es la siguiente:

public static void parkUntil(Object blocker, long deadline) {
    
    
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 设置Blocker
    setBlocker(t, blocker);
    UNSAFE.park(true, deadline);
    // 设置Blocker为null
    setBlocker(t, null);
}

Nota: Esta función también llama dos veces a la función setBlocker.El parámetro de fecha límite representa el tiempo absoluto y representa el tiempo especificado.

4. función de desaparcar

Esta función indica que el permiso del hilo dado está disponible si aún no está disponible . Si un hilo está bloqueado en el parque, lo desbloqueará. De lo contrario, se garantiza que la próxima llamada a aparcar no será bloqueada. Si el hilo dado aún no se ha iniciado, no hay garantía de que esta operación tenga algún efecto. Las funciones específicas son las siguientes:

public static void unpark(Thread thread) {
    
    
    if (thread != null) // 线程为不空
        UNSAFE.unpark(thread); // 释放该线程许可
}

Descripción: Libere el permiso y el hilo especificado podrá continuar ejecutándose.

4. Descripción del ejemplo de LockSupport

4.1 Utilice esperar/notificar para lograr la sincronización de subprocesos

class MyThread extends Thread {
    
    
    public void run() {
    
    
        synchronized (this) {
    
    
            System.out.println("before notify");
            notify();
            System.out.println("after notify");
        }
    }
}

public class WaitAndNotifyDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        MyThread myThread = new MyThread();            
        synchronized (myThread) {
    
    
            try {
    
            
                myThread.start();
                // 主线程睡眠3s
                Thread.sleep(3000);
                System.out.println("before wait");
                // 阻塞主线程
                myThread.wait();
                System.out.println("after wait");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }            
        }        
    }
}

resultado de la operación

before wait
before notify
after notify
after wait

Nota: El diagrama de flujo específico es el siguiente

imagen

Cuando use esperar / notificar para lograr la sincronización, primero debe llamar a esperar y luego llamar a notificar. Si llama a notificar primero y luego a esperar, no funcionará . El código específico es el siguiente.

class MyThread extends Thread {
    
    
    public void run() {
    
    
        synchronized (this) {
    
    
            System.out.println("before notify");            
            notify();
            System.out.println("after notify");    
        }
    }
}

public class WaitAndNotifyDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        MyThread myThread = new MyThread();        
        myThread.start();
        // 主线程睡眠3s
        Thread.sleep(3000);
        synchronized (myThread) {
    
    
            try {
    
            
                System.out.println("before wait");
                // 阻塞主线程
                myThread.wait();
                System.out.println("after wait");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }            
        }        
    }
}

resultado de la operación:

before notify
after notify
before wait

Nota: Dado que primero se llama a notificar y luego a esperar, el hilo principal seguirá bloqueado en este momento.

4.2 Utilice aparcar/desaparcar para lograr la sincronización de subprocesos

import java.util.concurrent.locks.LockSupport;

class MyThread extends Thread {
    
    
    private Object object;

    public MyThread(Object object) {
    
    
        this.object = object;
    }

    public void run() {
    
    
        System.out.println("before unpark");
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        // 获取blocker
        System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));
        // 释放许可
        LockSupport.unpark((Thread) object);
        // 休眠500ms,保证先执行park中的setBlocker(t, null);
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        // 再次获取blocker
        System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));

        System.out.println("after unpark");
    }
}

public class test {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread(Thread.currentThread());
        myThread.start();
        System.out.println("before park");
        // 获取许可
        LockSupport.park("ParkAndUnparkDemo");
        System.out.println("after park");
    }
}

resultado de la operación:

before park
before unpark
Blocker info ParkAndUnparkDemo
after park
Blocker info null
after unpark

Nota : Este programa primero ejecuta park, luego ejecuta despark para sincronización y llama a getBlocker antes y después de despark. Puede ver que los resultados de las dos veces son diferentes y el resultado de la segunda llamada es nulo. Esto se debe a que después de llamar unpark, se ejecuta Lock.park(Object blocker)la función setBlocker (t, null) en la función , por lo que la segunda vez que se llama a getBlocker, es nulo.

En el ejemplo anterior, primero se llama a estacionar y luego a deshacer el estacionamiento. Ahora modifique el programa para llamar primero a deshacer el estacionamiento y luego llamar a estacionar para ver si la sincronización puede ser correcta. El código específico es el siguiente.

import java.util.concurrent.locks.LockSupport;

class MyThread extends Thread {
    
    
    private Object object;

    public MyThread(Object object) {
    
    
        this.object = object;
    }

    public void run() {
    
    
        System.out.println("before unpark");        
        // 释放许可
        LockSupport.unpark((Thread) object);
        System.out.println("after unpark");
    }
}

public class ParkAndUnparkDemo {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread(Thread.currentThread());
        myThread.start();
        try {
    
    
            // 主线程睡眠3s
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("before park");
        // 获取许可
        LockSupport.park("ParkAndUnparkDemo");
        System.out.println("after park");
    }
}

resultado de la operación:

before unpark
after unpark
before park
after park

Nota: Se puede ver que cuando se llama primero a desaparcar y luego se llama a estacionar, la sincronización aún se puede lograr correctamente sin causar el bloqueo causado por una secuencia de llamada de espera/notificación incorrecta. Por lo tanto, aparcar/desaparcar es más flexible que esperar/notificar.

4.3 Respuesta de interrupción

Mira el ejemplo de abajo

import java.util.concurrent.locks.LockSupport;

class MyThread extends Thread {
    
    
    private Object object;

    public MyThread(Object object) {
    
    
        this.object = object;
    }

    public void run() {
    
    
        System.out.println("before interrupt");
        try {
    
    
            // 休眠3s
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }    
        Thread thread = (Thread) object;
        // 中断线程 **
        thread.interrupt();
        System.out.println("after interrupt");
    }
}

public class InterruptDemo {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread(Thread.currentThread());
        myThread.start();
        System.out.println("before park");
        // 获取许可
        LockSupport.park("ParkAndUnparkDemo");
        System.out.println("after park");
    }
}

resultado de la operación:

before park
before interrupt
after interrupt
after park

Nota : Se puede ver que después de que el hilo principal llama a estacionar para bloquear, se envía una señal de interrupción en el hilo myThread. En este momento, el hilo principal continuará ejecutándose, lo que significa que la interrupción juega el mismo papel que desestacionar en este momento. tiempo .

5. Comprensión más profunda

5.1 La diferencia entre Thread.sleep() y Object.wait()

En primer lugar, echemos un vistazo a la diferencia entre Thread.sleep() y Object.wait(). Esta es una gran pregunta. Todos deberían poder distinguir dos puntos.

  • Thread.sleep() no liberará el bloqueo retenido, pero Object.wait() liberará el bloqueo retenido;
  • Thread.sleep () debe pasar el tiempo, y Object.wait () puede o no pasarlo. Si no se pasa, significa que continuará bloqueándose;
  • Thread.sleep() se activará automáticamente cuando se acabe el tiempo y luego continuará la ejecución;
  • Object.wait() no tiene tiempo y requiere que otro hilo se active usando Object.notify();
  • Object.wait() tiene tiempo. Si no se notifica, se activará automáticamente cuando se acabe el tiempo. En este momento, hay dos situaciones. Una es que el bloqueo se adquiere inmediatamente y el hilo continuará naturalmente. ejecutar, la otra es que el bloqueo no se adquiere inmediatamente, el hilo ingresa a la cola de sincronización y espera adquirir el bloqueo;

De hecho, la mayor diferencia entre ellos es que Thread.sleep() no liberará recursos de bloqueo, mientras que Object.wait() liberará recursos de bloqueo.

5.2 La diferencia entre Object.wait() y Condition.await()

Los principios de Object.wait() y Condition.await() son básicamente los mismos, la diferencia es que la capa inferior de Condition.await() llama a LockSupport.park() para bloquear el hilo actual .

De hecho, hace dos cosas antes de bloquear el hilo actual: una es agregar el hilo actual a la cola de condiciones y la otra es liberar "completamente" el bloqueo , es decir, cambiar la variable de estado a 0, y luego llame a LockSupport..park() bloquea el hilo actual .

5.3 La diferencia entre Thread.sleep() y LockSupport.park()

LockSupport.park() también tiene varios métodos hermanos: parkNanos(), parkUtil(), etc. El método park() del que estamos hablando aquí se refiere colectivamente a este tipo de método.

  • Funcionalmente hablando, los métodos Thread.sleep() y LockSupport.park() son similares: ambos bloquean la ejecución del hilo actual y no liberarán los recursos de bloqueo ocupados por el hilo actual;
  • Thread.sleep() no se puede despertar desde el exterior, solo puede despertarse por sí solo;
  • El método LockSupport.park() puede ser activado por otro subproceso que llame al método LockSupport.unpark();
  • La declaración del método Thread.sleep() arroja InterruptedException, por lo que la persona que llama debe detectar esta excepción o lanzarla nuevamente;
  • El método LockSupport.park() no necesita detectar excepciones de interrupción;
  • Thread.sleep() en sí mismo es un método nativo;
  • La capa inferior de LockSupport.park() es el método nativo de Unsafe llamado ;

5.4 La diferencia entre Object.wait() y LockSupport.park()

Ambos bloquearán la ejecución del hilo actual. ¿Cuál es la diferencia entre ellos? Después del análisis anterior, creo que debes quedar muy claro, ¿es cierto? ¡Sigue leyendo!

  • El método Object.wait() debe ejecutarse en un bloque sincronizado;
  • LockSupport.park() se puede ejecutar en cualquier lugar;
  • El método Object.wait() declara que se lanza una excepción de interrupción y la persona que llama debe detectarla o lanzarla nuevamente;
  • LockSupport.park() no necesita detectar excepciones de interrupción;
  • Object.wait() no tiene tiempo de espera y requiere que otro subproceso ejecute notify() para activarlo, pero no necesariamente continúa ejecutando contenido posterior;
  • LockSupport.park () no tiene tiempo de espera y requiere que otro subproceso ejecute unpark () para activarlo, y el contenido posterior definitivamente continuará ejecutándose;

El principio subyacente de park()/unpark() es un "semáforo binario". Puede considerarlo como un semáforo con una sola licencia , excepto que este semáforo no agregará más licencias cuando unpark() se ejecute repetidamente. sólo una licencia como máximo.

1. ¿Qué sucede si se ejecuta notificar () antes de esperar ()?

Si el hilo actual no es el propietario del bloqueo de este objeto, pero llama al método notify() o wait() del objeto, se lanza una excepción IllegalMonitorStateException;

Si el hilo actual es el propietario de este bloqueo de objeto, esperar() siempre se bloqueará porque no habrá otra notificación() para activarlo más tarde.

2. ¿Qué sucede si se ejecuta unpark() antes de park()?

El hilo no se bloqueará, omitirá park () directamente y continuará ejecutando el contenido posterior.

5.5 ¿LockSupport.park() liberará recursos de bloqueo?

No, solo es responsable de bloquear el hilo actual, y liberar el recurso de bloqueo en realidad se implementa en el método await () de Condición.

6. Artículos de referencia

Supongo que te gusta

Origin blog.csdn.net/qq_28959087/article/details/129813032
Recomendado
Clasificación