Entrevistador de Jingdong: ¡Cuénteme sobre la diferencia entre sincronizado y ReentrantLock!

Prefacio

Anteriormente, presentamos una gran cantidad de contenido sobre subprocesos múltiples. En el subproceso múltiple, hay un tema muy importante que debemos superar, es decir, la seguridad de los subprocesos. Los problemas de seguridad de subprocesos se refieren a la contaminación de los datos u otros resultados inesperados de ejecución de programas causados ​​por operaciones simultáneas entre subprocesos en varios subprocesos.

A salvo de amenazas

1) Casos no seguros para subprocesos

Por ejemplo, si A y B transfieren dinero a C al mismo tiempo, suponga que el saldo original de C es de 100 yuanes, y A transfiere 100 yuanes a C, y está en proceso de transferencia. En este momento, B también transfiere 100 yuanes a C. En este momento, A se transfiere a C con éxito. El saldo se convierte en 200 yuanes, pero B pregunta que el saldo de C es de 100 yuanes por adelantado, y también es de 200 yuanes después de que la transferencia sea exitosa. Cuando tanto A como B han completado la transferencia a C, el saldo sigue siendo de 200 yuanes en lugar de los 300 yuanes esperados, que es un problema típico de seguridad de hilos.

2) Ejemplo de código no seguro para subprocesos

No importa si no comprende el contenido anterior, veamos el código específico que no es seguro para subprocesos:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -› addNumber());
        Thread thread2 = new Thread(() -› addNumber());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Los resultados de ejecución del programa anterior son los siguientes:

número: 12085

El resultado de cada ejecución puede variar ligeramente, pero casi nunca es igual a la suma acumulada (correcta) de 20000.

3) Solución segura para subprocesos

La solución segura para subprocesos tiene las siguientes dimensiones:

  • Los datos no se comparten, un solo subproceso es visible, como ThreadLocal es un solo subproceso visible;
  • Utilice clases seguras para subprocesos, como StringBuffer y clases de seguridad en JUC (java.util.concurrent) (se presentará específicamente en el siguiente artículo);
  • Utilice códigos de sincronización o bloqueos.

Sincronización y bloqueo de subprocesos

1) sincronizado

① Introducción a sincronizados

Sincronizado es un mecanismo de sincronización proporcionado por Java. Cuando un subproceso está operando un bloque de código sincronizado (código modificado sincronizado), otros subprocesos solo pueden bloquear y esperar a que el subproceso original se ejecute antes de ejecutarse.

② uso sincronizado

sincronizado puede modificar el bloque de código o el método, el código de muestra es el siguiente:

// 修饰代码块
synchronized (this) {
    // do something
}
// 修饰方法
synchronized void method() {
    // do something
}

Use sincronizado para completar el código no seguro para subprocesos al principio de este artículo.

Método 1: Use sincronizado para decorar el bloque de código, el código es el siguiente:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread sThread = new Thread(() -› {
            // 同步代码
            synchronized (ThreadSafeTest.class) {
                addNumber();
            }
        });
        Thread sThread2 = new Thread(() -› {
            // 同步代码
            synchronized (ThreadSafeTest.class) {
                addNumber();
            }
        });
        sThread.start();
        sThread2.start();
        sThread.join();
        sThread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Los resultados de ejecución del programa anterior son los siguientes:

número: 20000

Método 2: use el método de modificación sincronizada, el código es el siguiente:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread sThread = new Thread(() -› addNumber());
        Thread sThread2 = new Thread(() -› addNumber());
        sThread.start();
        sThread2.start();
        sThread.join();
        sThread2.join();
        System.out.println("number:" + number);
    }
    public synchronized static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Los resultados de ejecución del programa anterior son los siguientes:

número: 20000

③ Principio de realización sincronizada

La esencia de la sincronización es lograr la seguridad de los hilos entrando y saliendo del objeto Monitor. Tome el siguiente código como ejemplo:

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("Java");
        }
    }
}

Cuando usamos javap para compilar, el código de bytes generado es el siguiente:

Compiled from "SynchronizedTest.java"
public class com.interview.other.SynchronizedTest {
  public com.interview.other.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."‹init›":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/interview/other/SynchronizedTest
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Java
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      14: monitorexit
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

Se puede ver que la JVM (Java Virtual Machine) utiliza las instrucciones monitorenter y monitorexit para lograr la sincronización, la instrucción monitorenter es equivalente a bloquear y monitorexit equivale a liberar el bloqueo. El monitorenter y monitorexit se implementan en base a Monitor.

2) ReentrantLock

① Introducción a ReentrantLock

ReentrantLock (bloqueo de reentrada) es una implementación de bloqueo proporcionada por Java 5, y su función es básicamente la misma que la sincronizada. El bloqueo de reentrada adquiere el bloqueo llamando al método lock () y libera el bloqueo llamando a unlock ().

② Uso de ReentrantLock

Uso básico de ReentrantLock, el código es el siguiente:

Lock lock = new ReentrantLock();
lock.lock();    // 加锁
// 业务代码...
lock.unlock();    // 解锁

Use ReentrantLock para mejorar el código no seguro para subprocesos al principio de este artículo, consulte el siguiente código:

public class LockTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        // ReentrantLock 使用
        Lock lock = new ReentrantLock();
        Thread thread1 = new Thread(() -› {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });
        Thread thread2 = new Thread(() -› {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Intenta adquirir el candado

ReentrantLock puede intentar acceder al bloqueo sin bloquear, utilizando el método tryLock (), que se utiliza específicamente de la siguiente manera:

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -› {
    try {
        reentrantLock.lock();
        Thread.sleep(2 * 1000);
 
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}).start();
// 线程二
new Thread(() -› {
    try {
        Thread.sleep(1 * 1000);
        System.out.println(reentrantLock.tryLock());
        Thread.sleep(2 * 1000);
        System.out.println(reentrantLock.tryLock());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

El resultado de la ejecución del código anterior es el siguiente:

falso
verdadero

Intente adquirir el candado por un período de tiempo

tryLock () tiene un método de extensión tryLock (tiempo de espera largo, unidad TimeUnit) para intentar adquirir el bloqueo durante un período de tiempo. El código de implementación específico es el siguiente:

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -› {
    try {
        reentrantLock.lock();
        System.out.println(LocalDateTime.now());
        Thread.sleep(2 * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}).start();
// 线程二
new Thread(() -› {
    try {
        Thread.sleep(1 * 1000);
        System.out.println(reentrantLock.tryLock(3, TimeUnit.SECONDS));
        System.out.println(LocalDateTime.now());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

El resultado de la ejecución del código anterior es el siguiente:

2019-07-05 19:53:51
cierto
2019-07-05 19:53:53

Se puede ver que el bloqueo es adquirido directamente por el subproceso dos después de dormir durante 2 segundos, por lo que el parámetro de tiempo de espera en el método tryLock (tiempo de espera largo, unidad TimeUnit) se refiere al tiempo máximo de espera para adquirir el bloqueo.

③ Precauciones con ReentrantLock

Cuando use ReentrantLock, debe recordar liberar el candado, de lo contrario el candado estará ocupado permanentemente.

Preguntas relacionadas con la entrevista

1. ¿Cuáles son los métodos de ReentrantLock que se utilizan comúnmente?

Respuesta: Los métodos comunes de ReentrantLock son los siguientes:

  • lock (): utilizado para obtener el bloqueo
  • desbloquear (): se usa para liberar el bloqueo
  • tryLock (): intenta adquirir el bloqueo
  • getHoldCount (): consulta el número de veces que el hilo actual ejecuta el método lock ()
  • getQueueLength (): devuelve el número de subprocesos que están en cola para adquirir este bloqueo
  • isFair (): si el bloqueo es un bloqueo justo

2. ¿Cuáles son las ventajas de ReentrantLock?

Respuesta: ReentrantLock tiene la característica de adquirir el bloqueo de forma no bloqueante, utilizando el método tryLock (). ReentrantLock puede interrumpir el bloqueo adquirido. Después de adquirir el bloqueo utilizando el método lockInterruptbly (), si se interrumpe el hilo, se lanzará una excepción y se liberará el bloqueo adquirido actualmente. ReentrantLock puede adquirir el bloqueo dentro del rango de tiempo especificado, utilizando el método tryLock (tiempo de espera largo, unidad TimeUnit).

3. ¿Cómo crea ReentrantLock un candado justo?

Respuesta: el nuevo ReentrantLock () crea un bloqueo injusto de forma predeterminada. Si desea crear un bloqueo justo, puede usar el nuevo ReentrantLock (verdadero).

4. ¿Cuál es la diferencia entre bloqueo justo y bloqueo injusto?

Respuesta: El bloqueo regular se refiere al orden en el que los subprocesos adquieren bloqueos de acuerdo con el orden de bloqueo, mientras que el bloqueo no equitativo se refiere al mecanismo de agarre de bloqueo. El hilo que bloquea () primero no necesariamente adquiere el bloqueo primero.

5. ¿Cuál es la diferencia entre lock () y lockInterruptiblemente () en ReentrantLock?

Respuesta: La diferencia entre lock () y lockInterruptbly () es que si el hilo se interrumpe mientras se adquiere el hilo, lock () ignorará la excepción y continuará esperando el hilo de adquisición, mientras que lockInterruptiblemente () lanzará una InterruptedException. Análisis de problemas: ejecute el siguiente código y use lock () y lockInterruptbly () en el hilo para ver los resultados en ejecución. El código es el siguiente:

 Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            interruptLock.lock();
            //interruptLock.lockInterruptibly();  // java.lang.InterruptedException
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);

Si ejecuta el siguiente código, encontrará que el programa no informará un error al usar lock () y saldrá directamente después de que se complete la operación; mientras usa lockInterruptbly () arrojará una excepción java.lang.InterruptedException, lo que significa : si el hilo se interrumpe en el camino para obtener el hilo, Lock () ignorará la excepción y continuará esperando para adquirir el hilo, mientras que lockInterruptiblemente () lanzará una InterruptedException.

6. ¿Cuál es la diferencia entre sincronizado y ReentrantLock?

Respuesta: tanto sincronizados como ReentrantLock garantizan la seguridad de los subprocesos, y sus diferencias son las siguientes:

  • ReentrantLock es más flexible de usar, pero debe haber una acción cooperativa para liberar el bloqueo;
  • ReentrantLock debe adquirir y liberar manualmente el candado, mientras que sincronizado no necesita liberar y abrir manualmente el candado;
  • ReentrantLock solo se aplica a bloqueos de bloque de código, mientras que sincronizado se puede utilizar para modificar métodos, bloques de código, etc .;
  • El rendimiento de ReentrantLock es ligeramente superior al de sincronizado.

7. TryLock de ReentrantLock (3, TimeUnit.SECONDS) significa esperar 3 segundos antes de adquirir el bloqueo. ¿Es correcta esta afirmación? ¿Por qué?

Respuesta: No, tryLock (3, TimeUnit.SECONDS) significa que el tiempo máximo de espera para adquirir el candado es de 3 segundos, tiempo durante el cual siempre intentará adquirir en lugar de esperar 3 segundos antes de adquirir el candado.

8. ¿Cómo se realiza la actualización sincronizada de la cerradura?

Respuesta: Hay un campo threadid en el encabezado del objeto del objeto de bloqueo. El threadid está vacío cuando se accede a él por primera vez. La JVM (Java Virtual Machine) le permite mantener un bloqueo sesgado y establece el threadid en su hilo id. En este momento, primero juzgará si el threadid es consistente, especialmente el ID del hilo. Si es consistente, se puede usar directamente. Si no es consistente, el bloqueo de polarización de actualización es un bloqueo ligero. El bloqueo es adquirido a través de un cierto número de ciclos de giro sin bloqueo. Después de un cierto número de ejecuciones, se actualizará a un candado pesado, ingrese al bloque, todo el proceso es el proceso de actualización del candado.

para resumir

Este artículo presenta dos formas de sincronización de subprocesos, sincronizada y ReentrantLock. ReentrantLock es más flexible y eficiente. Sin embargo, ReentrantLock solo puede modificar el bloque de código. El uso de ReentrantLock requiere que el desarrollador libere manualmente el bloqueo. Si olvida liberar el bloqueo, el La cerradura siempre estará ocupada. Synchronized utiliza una gama más amplia de escenarios, que pueden modificar métodos comunes, métodos estáticos y bloques de código. El editor también resume un mapa mental de múltiples subprocesos aquí. Para facilitar a los amigos a organizar mejor los puntos técnicos, compártelo con todos !

[Error en la transferencia de la imagen del enlace externo. El sitio de origen puede tener un mecanismo de enlace anti-sanguijuela. Se recomienda guardar la imagen y subirla directamente (img-0WEYlMrX-1614240957624) (https: //ask8088-private-1251520898.cn- south.myqcloud.com/developer -images / article / 7948575 / n2ecyj3vob.png? q-sign-algorítm = sha1 & q-ak = AKID2uZ1FGBdx1pNgjE3KK4YliPpzyjLZvug & q-sign-time = 1614240359; q-key-1614247559 1614240359-q-keylist = & q-signature = d02e79d208db8b3706d7c9fd331fd49efb9bdaf9)]

Supongo que te gusta

Origin blog.csdn.net/QLCZ0809/article/details/114089183
Recomendado
Clasificación