Entrevista de la serie JUC de Java: Enciclopedia Java Lock

Bloqueo justo y bloqueo injusto del bloqueo de Java

concepto

Cerradura justa

Significa que varios subprocesos adquieren bloqueos en el orden en que solicitan bloqueos, que es similar a hacer cola para comprar comida, por orden de llegada, por orden de llegada, es justo, es decir, cola

Bloqueo injusto

Se refiere al orden en el que varios subprocesos adquieren bloqueos, no en el orden de aplicación de bloqueos. Es posible que el subproceso aplicado obtenga el bloqueo primero que el primer subproceso aplicado. En un entorno de alta concurrencia, la prioridad puede invertirse o subprocesos hambrientos puede ser causado (es decir, un hilo no ha podido obtener el bloqueo)

Como crear

La creación de ReentrantLock en el paquete concurrente puede especificar el tipo booleano del destructor para obtener un bloqueo justo o un bloqueo injusto. El valor predeterminado es un bloqueo injusto.

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);

La diferencia entre los dos

Bloqueo justo : es muy justo. En un entorno concurrente, cada hilo verificará primero la cola de espera mantenida por este bloqueo al adquirir un bloqueo. Si está vacío o el hilo actual es el primero en la cola de espera, el bloqueo será No se agregará a la cola de espera, y las reglas de instalación de FIFO se tomarán de la cola más tarde.

Cerraduras injustas: las cerraduras injustas son más groseras e intentan sujetar la cerradura directamente. Si el intento falla, utilice un método similar a las cerraduras normales.

Digresión

Java ReenttrantLock especifica si el bloqueo es justo a través del constructor. El valor predeterminado es un bloqueo injusto, porque la ventaja de un bloqueo injusto es que el rendimiento es mayor que el de un bloqueo justo.对于synchronized而言,也是一种非公平锁

Por qué Synchronized no puede prohibir el reordenamiento de instrucciones, pero puede garantizar el orden

Prefacio

En primer lugar, tenemos que analizar esta pregunta. Esta simple pregunta en realidad contiene mucha información. Para responder bien a esta pregunta, el entrevistador debe al menos conocer el concepto:

  • Modelo de memoria Java
  • Problema de orden de programación concurrente
  • Reordenamiento de pedidos
  • sincronizado 锁
  • Bloqueo reentrante
  • Cerradura exclusiva
  • semántica como-si-serial
  • Hilo único y hilo múltiple

Respuesta estándar

Para mejorar aún más todos los aspectos de las capacidades de la computadora, se han realizado muchas optimizaciones a nivel de hardware, como la optimización del procesador y la reorganización de las instrucciones, pero la introducción de estas tecnologías causará problemas de orden.

Primero explique cuál es el problema del pedido y también sepa qué causa el problema del pedido.

También sabemos que la mejor manera de resolver el problema del orden es prohibir la optimización del procesador y el reordenamiento de instrucciones, al igual que el uso de barreras de memoria en volátiles.

Demuestre que sabe qué es la reordenación de instrucciones y cómo se implementa

Sin embargo, aunque muchos hardware realizarán algunas reorganizaciones para la optimización, en Java, no importa cómo sea el orden, no puede afectar el resultado de la ejecución de un programa de un solo subproceso. Esta es la semántica como-si-serie El requisito previo para todas las optimizaciones de hardware es cumplir con la semántica como-si-serie.

La semántica como en serie protege los programas de un solo subproceso . Los compiladores que cumplen con la semántica como en serie, el tiempo de ejecución y los procesadores crean en conjunto una ilusión para los programadores que escriben programas de un solo subproceso: los programas de un solo subproceso se ejecutan mediante programación. ejecutado secuencialmente. La semántica como si fuera en serie libera a los programadores de un solo subproceso de preocuparse por reordenarlos, perturbarlos o preocuparse por problemas de visibilidad de la memoria.

¡Atención! Explique cuál es la semántica de como-si-serial, porque esta es la primera palabra clave de esta pregunta, la respuesta es mitad correcta

Hablemos de sincronizado. Es un bloqueo proporcionado por Java. Puede bloquear objetos en Java a través de él, y es un bloqueo exclusivo y reentrante.

Por lo tanto, cuando un hilo ejecuta un fragmento de código modificado por sincronizado, lo bloqueará primero y luego lo desbloqueará después de la ejecución. Una vez que el bloqueo está bloqueado y antes de que se desbloquee, otros hilos no pueden obtener el bloqueo nuevamente, y solo este hilo de bloqueo puede obtener el bloqueo repetidamente.

Introduzca el principio de sincronizado, este es el segundo punto clave de esta pregunta, básicamente puede obtener la máxima puntuación aquí.

La sincronización asegura que el código modificado por sincronizado se ejecute en un solo hilo al mismo tiempo mediante bloqueo exclusivo. Entonces, esto satisface una premisa clave de la semántica como-si-serial, es decir, de un solo subproceso Debido a la garantía semántica como-si-serial, el orden de un solo subproceso existe naturalmente.

Girar bloqueo de bloqueo de Java

Spinlock: Spinlock significa que el subproceso que intenta adquirir el bloqueo no se bloqueará inmediatamente, sino que intentará adquirir el bloqueo de manera circular. La ventaja de esto es reducir el consumo de cambio de contexto de subproceso, pero la desventaja es que el bucle consume CPU

En la comparación e intercambio original mencionado, la capa inferior usa spin. Spin es múltiples intentos y múltiples accesos. El estado que no bloquea es spin.

imagen-20200315154143781

Pros y contras

Ventajas: adquisición de comparación de bucles hasta el éxito, no hay bloqueo similar a esperar

Desventajas: cuando más y más subprocesos giran continuamente, los recursos de la CPU se consumirán continuamente debido a la ejecución del ciclo while.

Bloqueo de giro escrito a mano

El bloqueo de giro se completa a través de la operación CAS. El subproceso A primero llama al método myLock para mantener el bloqueo durante 5 segundos. Luego, B entra y encuentra que actualmente hay un subproceso que retiene el bloqueo, que no es nulo, por lo que puede espere girando hasta que A libere el candado. Luego agarró

/**
 * 手写一个自旋锁
 *
 * 循环比较获取直到成功为止,没有类似于wait的阻塞
 *
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到

 */
public class SpinLockDemo {
    
    

    // 现在的泛型装的是Thread,原子引用线程
    AtomicReference<Thread>  atomicReference = new AtomicReference<>();

    public void myLock() {
    
    
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");

        // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {
    
    

        }
    }

    /**
     * 解锁
     */
    public void myUnLock() {
    
    

        // 获取当前进来的线程
        Thread thread = Thread.currentThread();

        // 自己用完了后,把atomicReference变成null
        atomicReference.compareAndSet(thread, null);

        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
    }

    public static void main(String[] args) {
    
    

        SpinLockDemo spinLockDemo = new SpinLockDemo();

        // 启动t1线程,开始操作
        new Thread(() -> {
    
    

            // 开始占有锁
            spinLockDemo.myLock();


            try {
    
    
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t1").start();


        // 让main线程暂停1秒,使得t1线程,先执行
        try {
    
    
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        // 1秒后,启动t2线程,开始占用这个锁
        new Thread(() -> {
    
    

            // 开始占有锁
            spinLockDemo.myLock();
            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t2").start();

    }
}

La salida final

t1	 come in 
.....五秒后.....
t1	 invoked myUnlock()
t2	 come in 
t2	 invoked myUnlock()

La primera salida es t1 entra

Luego, 1 segundo después, el subproceso t2 comienza y encuentra que el bloqueo está ocupado por t1. Todos los métodos compareAndSet se ejecutan continuamente para comparar hasta que t1 libera el bloqueo, es decir, 5 segundos después, t2 adquiere exitosamente el bloqueo y luego lo libera. .

Cerraduras reentrantes y cerraduras recursivas ReentrantLock

concepto

Las cerraduras reentrantes son cerraduras recursivas

Significa que después de que la función externa del mismo hilo adquiere el bloqueo, la función recursiva interna aún puede adquirir el código del bloqueo. Cuando el mismo hilo adquiere el bloqueo en el método externo, adquirirá automáticamente el bloqueo cuando ingrese al método interno.

En otras palabras:线程可以进入任何一个它已经拥有的锁所同步的代码块

ReentrantLock / Synchronized es un bloqueo reentrante típico

Código

El bloqueo de reentrante es agregar un bloqueo a un método del método 1, el método 2 también está bloqueado, luego tienen el mismo bloqueo

public synchronized void method1() {
    
    
	method2();
}

public synchronized void method2() {
    
    

}

En otras palabras, solo necesitamos ingresar el método1, luego también puede ingresar directamente el método2, porque tienen el mismo bloqueo.

efecto

El papel más importante de las cerraduras reentrantes es evitar interbloqueos

Verificación de bloqueo reentrante

Prueba sincronizada

/**
 * 可重入锁(也叫递归锁)
 * 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
 *
 * 也就是说:`线程可以进入任何一个它已经拥有的锁所同步的代码块`

 */

/**
 * 资源类
 */
class Phone {
    
    

    /**
     * 发送短信
     * @throws Exception
     */
    public synchronized void sendSMS() throws Exception{
    
    
        System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");

        // 在同步方法中,调用另外一个同步方法
        sendEmail();
    }

    /**
     * 发邮件
     * @throws Exception
     */
    public synchronized void sendEmail() throws Exception{
    
    
        System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()");
    }
}
public class ReenterLockDemo {
    
    


    public static void main(String[] args) {
    
    
        Phone phone = new Phone();

        // 两个线程操作资源列
        new Thread(() -> {
    
    
            try {
    
    
                phone.sendSMS();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }, "t1").start();

        new Thread(() -> {
    
    
            try {
    
    
                phone.sendSMS();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

Aquí, hemos escrito un teléfono de clase de recursos, que tiene dos métodos de sincronización con sincronizado, a saber, sendSMS y sendEmail. En el método sendSMS, llamamos sendEmail. Finalmente, se abrieron dos subprocesos para probarlos en el subproceso principal al mismo tiempo, y el resultado final fue:

t1	 invoked sendSMS()
t1	 invoked sendEmail()
t2	 invoked sendSMS()
t2	 invoked sendEmail()

Esto significa que cuando el subproceso t1 ingresa a sendSMS, tiene un bloqueo y el subproceso t2 no puede ingresar al mismo tiempo. Hasta que el subproceso t1 mantenga el bloqueo y ejecute el método sendEmail, el bloqueo se libera, de modo que t2 puede ingresar

t1	 invoked sendSMS()      t1线程在外层方法获取锁的时候
t1	 invoked sendEmail()    t1在进入内层方法会自动获取锁

t2	 invoked sendSMS()      t2线程在外层方法获取锁的时候
t2	 invoked sendEmail()    t2在进入内层方法会自动获取锁

Prueba de ReentrantLock

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 资源类
 */
class Phone implements Runnable{
    
    

    Lock lock = new ReentrantLock();

    /**
     * set进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法
     */
    public void getLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void setLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
    
    
            lock.unlock();
        }
    }

    @Override
    public void run() {
    
    
        getLock();
    }
}
public class ReenterLockDemo {
    
    


    public static void main(String[] args) {
    
    
        Phone phone = new Phone();

        /**
         * 因为Phone实现了Runnable接口
         */
        Thread t3 = new Thread(phone, "t3");
        Thread t4 = new Thread(phone, "t4");
        t3.start();
        t4.start();
    }
}

Ahora usamos ReentrantLock para verificar. Primero, la clase de recurso implementa la interfaz Runnable, reescribe el método Run y ​​llama al método get en su interior. Cuando el método get ingresa, se agrega el bloqueo.

    public void getLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
    
    
            lock.unlock();
        }
    }

Luego, en el método, llame a otro método setLock con bloqueo

    public void setLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
    
    
            lock.unlock();
        }
    }

En la salida final, podemos encontrar que el resultado es el mismo que el del método sincronizado, una vez que el método externo adquiere el bloqueo, el hilo puede entrar directamente en la capa interna.

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock

¿Qué sucede cuando agregamos dos bloqueos al método getLock? (Entrevista de Ali)

    public void getLock() {
    
    
        lock.lock();
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
    
    
            lock.unlock();
            lock.unlock();
        }
    }

El resultado final es el mismo, porque no importa cuántas cerraduras haya en ella, las demás son la misma cerradura, lo que significa que se pueden abrir con la misma llave.

¿Qué sucede cuando agregamos dos bloqueos al método getLock, pero solo desbloqueamos un bloqueo?

public void getLock() {
    lock.lock();
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + "\t get Lock");
        setLock();
    } finally {
        lock.unlock();
        lock.unlock();
    }
}

tengo la respuesta

t3	 get Lock
t3	 set Lock

Es decir, el programa se atasca directamente y el hilo no puede salir, lo que significa que solicitamos algunos bloqueos y finalmente necesitamos liberar algunos bloqueos.

¿Qué sucede cuando solo agregamos un candado, pero usamos dos candados para desbloquearlo?

    public void getLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
    
    
            lock.unlock();
            lock.unlock();
        }
    }

En este momento, el programa en ejecución informará un error directamente

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock
Exception in thread "t3" Exception in thread "t4" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
	at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
	at java.lang.Thread.run(Thread.java:745)
java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
	at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
	at java.lang.Thread.run(Thread.java:745)

Supongo que te gusta

Origin blog.csdn.net/weixin_43314519/article/details/110195932
Recomendado
Clasificación