Aprenda los bloqueos de Java de una manera sencilla (1)

Aprenda los bloqueos de Java de una manera sencilla (1)

Bajo la marea de Internet, las excelentes características del lenguaje de Java han atraído el entusiasmo de varios fabricantes importantes. Esto inevitablemente requiere que los estudiantes que planean ingresar a Dachang tengan una base informática sólida. A continuación, el tema se centra en los puntos de conocimiento básico de varios bloqueos y la implementación y uso de bloqueos de Java para ayudar a los estudiantes a lidiar mejor con varias preguntas complicadas de entrevistas de las grandes fábricas.

El significado de la cerradura

En una computadora con una arquitectura de CPU múltiple, puede prevenir de manera efectiva que varios subprocesos operen simultáneamente el mismo recurso de la computadora y causen inconsistencia de datos o lectura sucia. El bloqueo es una buena solución en escenarios de subprocesos múltiples, pero el uso inadecuado puede causar serios problemas de rendimiento, como agotamiento de la CPU, interbloqueo y bajo rendimiento del servicio .

Características comunes de los bloqueos de Java

Un punto, tenga en cuenta: una cerradura puede tener múltiples características , como cerraduras optimistas y cerraduras reentrantes. A continuación, expanda según las características.

Cerradura pesimista y cerradura optimista

conceptos básicos

El bloqueo pesimista se refiere a la creencia pesimista de que otros subprocesos modificarán el recurso de sincronización cuando el subproceso actual esté en funcionamiento, por lo que el subproceso actual debe agregar un bloqueo para garantizar que el recurso de sincronización no sea operado por otros subprocesos. Las implementaciones de bloqueo pesimistas comunes en Java son las clases de implementación de sincronizado y bloqueo.
El bloqueo optimista se refiere a la visión optimista de que ningún otro subproceso modificará el subproceso actual al operar los recursos de sincronización, por lo que el subproceso actual no se bloqueará. Es solo que cuando se actualiza el recurso de sincronización, comprobará activamente si el recurso de sincronización ha sido modificado por otros subprocesos; si se modifica, se puede resolver de diferentes formas (como reintentar o lanzar una excepción). Si no hay modificación, simplemente actualícela directamente.

escenas que se utilizarán

Los dos tipos de bloqueos tienen diferentes escenarios de uso y es imposible generalizar quién es bueno y quién es malo. El bloqueo pesimista se ocupa principalmente de escenarios en los que leer cada vez más escribe; el bloqueo optimista se ocupa principalmente de escenarios en los que leer más y escribir menos.

Código de muestra

  • Bloqueo pesimista

El propósito de esta demostración permite a los estudiantes ver claramente que cada hilo se ejecuta exclusivamente , uno por uno, pero no asegura que los hilos se ejecuten en el orden de 1-10 .

/**
 * @author : 乌鸦
 * @since : 2020/9/18
 */
public class Demo{
     //同步资源
    private String name;
    private ReentrantLock lock = new ReentrantLock();

    //悲观锁实现一
    public synchronized void updateName(String name, Integer threadNum) throws InterruptedException {
        //do something about name
        System.out.println(threadNum + "is doing something at " + Calendar.getInstance().getTimeInMillis());
        this.name = name;
        //为了演示效果故休眠1s
        Thread.sleep(1000);
    }

    //悲观锁实现二
    public void setName(String name, Integer threadNum) throws InterruptedException {
        //阻塞直到获取到锁
        lock.lock();
        try{
            //do something about name
            System.out.println(threadNum + " is doing something at " + Calendar.getInstance().getTimeInMillis());
            this.name = name;
            //为了演示效果故休眠1s
            Thread.sleep(1000);
        }finally{
            lock.unlock();
        }
    }

    public void setNameWithNoLock(String name, Integer threadNum)throws InterruptedException {
        //do something about name
        System.out.println(threadNum + " is doing something at " + Calendar.getInstance().getTimeInMillis());
        this.name = name;
        //为了演示效果故休眠1s
        Thread.sleep(1000);
    }

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();

        //悲观锁
        final CountDownLatch latch = new CountDownLatch(10);
        System.out.println("悲观锁输出结果,请注意看输出的时间戳:");
        for(int i=0;i<10;i++){
            final int num = i;
            new Thread(() -> {
                try {
                    demo.setName("乌鸦"+num,num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                latch.countDown();
            }).start();
        }
        //主要是为了hold主线程,让10个线程跑完
        latch.await();

        //无锁
        final CountDownLatch latch2 = new CountDownLatch(10);
        System.out.println("乐观锁输出结果,请注意看输出的时间戳:");
        for(int i=0;i<10;i++){
            final int num = i;
            new Thread(() -> {
                try {
                    demo.setNameWithNoLock("乌鸦"+num,num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                latch2.countDown();
            }).start();
        }
        //主要是为了hold主线程,让10个线程跑完
        latch2.await();
    }
}

=================输出结果===================
悲观锁输出结果,请注意看输出的时间戳:
0 is doing something at 1600418985619
1 is doing something at 1600418986632
2 is doing something at 1600418987637
3 is doing something at 1600418988642
4 is doing something at 1600418989646
5 is doing something at 1600418990647
6 is doing something at 1600418991651
7 is doing something at 1600418992656
8 is doing something at 1600418993660
9 is doing something at 1600418994666
乐观锁输出结果,请注意看输出的时间戳:
0 is doing something at 1600418995672
1 is doing something at 1600418995672
2 is doing something at 1600418995672
3 is doing something at 1600418995672
4 is doing something at 1600418995672
5 is doing something at 1600418995673
6 is doing something at 1600418995673
7 is doing something at 1600418995673
8 is doing something at 1600418995673
9 is doing something at 1600418995673
  • Cerradura optimista
    
    package com.example.demo;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/ **

  • @autor: cuervo
  • @since: 2020/9/18
    * /
    public class OptimisticLockDemo {
    public AtomicInteger atomicCount = new AtomicInteger ();
    public Integer commonCount = 0;

    public void atomicInc () {
    try {
    Thread.sleep (1); // Retraso 1 milisegundo
    ) catch (InterruptedException e) {
    e.printStackTrace ();
    }
    atomicCount.getAndIncrement ();
    }
    public void commonInc () {
    try {
    Thread .sleep (1); // Retraso de 1 milisegundo, para ver claramente los problemas de concurrencia
    } catch (InterruptedException e) {
    e.printStackTrace ();
    }
    commonCount = commonCount + 1;
    }

    public static void main (String [] args) throws InterruptedException {

    OptimisticLockDemo demo = new OptimisticLockDemo();
    
    //有锁case
    final CountDownLatch latch1 = new CountDownLatch(100);
    for(int i=0;i<100;i++){
        new Thread(() -> {
            demo.atomicInc();
            latch1.countDown();
        }).start();
    }
    latch1.await();
    System.out.println("atomicCount运行结果(确定为100):"+demo.atomicCount);
    
    //无锁case
    final CountDownLatch latch2 = new CountDownLatch(100);
    for(int i=0;i<100;i++){
        new Thread(() -> {
            demo.commonInc();
            latch2.countDown();
        }).start();
    }
    latch2.await();
    System.out.println("commonCount运行结果(不确定):"+demo.commonCount);

    }
    }

===== Resultado de salida ====== resultado de la
operación atomicCount (determinado como 100): 100
resultado de la operación commonCount (no estoy seguro): 91

## 自旋锁与非自旋锁
### 基本概念
在展开此概念之前,先介绍下线程的几个状态,详见如下图(图摘自:[线程的六种状态及转化](https://baijiahao.baidu.com/s?id=1658121385190352035&wfr=spider&for=pc))。同学们可以看到运行中的线程到就绪再到阻塞,会进行线程上下文切换,比较耗CPU资源。在JVM系统中,往往频繁地线程上下文切换会导致CPU使用率偏高,故我们需要避免,不断优化。
![image.png](https://cdn.nlark.com/yuque/0/2020/png/262173/1600421083542-8e913b64-d0fb-4c42-aa41-dde223dfb07d.png#align=left&display=inline&height=403&margin=%5Bobject%20Object%5D&name=image.png&originHeight=806&originWidth=1228&size=409389&status=done&style=none&width=614)
自旋锁是让当前线程"稍微等一下",但是_**CPU资源仍旧没有放弃**_,避免了线上下文的切换。_**同学们请注意,自旋不代表阻塞**_。自旋短时间等待,效果非常好。反之,如果锁被占用的时间很长,那就白白浪费了CPU资源。所以,建议_**自旋等待的时间必须要有一定的限度**_。
### 使用场景
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间。
### 代码示例
在AtomicInteger中的addAndGet方法实现,内部依赖的就是Unsafe中的getAndAddInt,其内部实现就是通过一个do-while来实现自旋。同时JVM也提供相关参数来设置自旋次数,具体可以参考[JVM参数](https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html)——PreBlockSpin。
```java
=======AtomicInteger部分源码=====
public class AtomicInteger extends Number implements java.io.Serializable {
    ......

    public final int addAndGet(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    ......
}

public final class Unsafe {
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
}

Cerraduras reentrantes y no reentrantes

conceptos básicos

El bloqueo reentrante significa que el mismo hilo puede adquirir el mismo bloqueo varias veces sin interbloqueo, como los bloqueos reentrantes sincronizados y ReentrantLock (habrá un tema especial para analizar por qué son reentrantes) . Cerraduras no reentrantes, a diferencia de las cerraduras reentrantes, la misma cerradura no se puede adquirir varias veces, de lo contrario provocará un interbloqueo.

escenas que se utilizarán

Cuando el mismo subproceso necesita ingresar repetidamente al recurso de la región crítica varias veces, se requiere un bloqueo reentrante para evitar de manera efectiva el punto muerto.

Código de muestra

Estudiantes, veamos cómo el bloqueo ReentrantLock en el JDK implementa bloqueos reentrantes. Cuenta principalmente el número de reentrantes a través del estado, evitando operaciones frecuentes de liberación de retención, lo que mejora la eficiencia y evita los puntos muertos. .

public class ReentrantLock implements Lock, java.io.Serializable {

    /**
     * The synchronization state.
     */
    private volatile int state;
    /**
     * Returns the current value of synchronization state.
     * This operation has memory semantics of a {@code volatile} read.
     * @return current state value
     */
    protected final int getState() {
        return state;
    }

    ......

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    ......
}

Cerradura compartida y cerradura exclusiva

conceptos básicos

Bloqueo exclusivo: Solo un hilo puede ocuparlo, para sincronizado, obviamente es un bloqueo exclusivo.
Bloqueo compartido: se pueden contener varios subprocesos al mismo tiempo. El representante típico en el JDK es el bloqueo de lectura de ReentrantReadWriteLock, que puede garantizar una lectura de datos eficiente.

escenas que se utilizarán

Para tener un mayor rendimiento de lectura, algunos recursos de la sección crítica permiten que varios subprocesos adquieran bloqueos de lectura compartidos al mismo tiempo sin actualizar. Como su nombre lo indica, los bloqueos exclusivos se utilizan principalmente para proteger los recursos de la sección crítica para que no se ensucien.

Código de muestra

Para la implementación de bloqueos compartidos, veamos la implementación de bloqueo de lectura de ReentrantReadWriteLock en el JDK. Puede verse que en el método tryAcquireShared, 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 aumenta el estado de lectura y adquiere con éxito el bloqueo de lectura. La implementación específica aún se encuentra en un estado similar, que se debe principalmente a la asombrosa abstracción de AQS en el JDK (habrá una columna de análisis de código fuente específico más adelante).

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

Cerradura justa y cerradura injusta

conceptos básicos

El bloqueo justo significa principalmente que varios subprocesos ingresan a la cola en el orden en que se solicita el bloqueo, de acuerdo con el principio FIFO (primero en entrar, primero en salir). Sus ventajas son obvias, no permitirá que algunos subprocesos esperen mucho tiempo y evitará de manera efectiva que aparezca la escena "hambrienta", pero las desventajas también son obvias. La CPU consume una gran cantidad de cambio de contexto de subproceso, lo que resulta en un rendimiento menor que los bloqueos injustos.
Bloqueo injusto significa principalmente que algunos subprocesos pueden saltar a la cola para obtener el bloqueo, y la cola se procesará cuando la cola no tenga éxito. La ventaja es que puede 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 bloquearse, y la CPU no necesita despertar todos los subprocesos; la desventaja es que los subprocesos en la cola de espera pueden morir de hambre o esperar mucho tiempo. Consigue la cerradura.

escenas que se utilizarán

Si el tiempo de procesamiento del negocio de subprocesos es mucho más largo que el tiempo de espera, entonces la eficiencia de los bloqueos injustos puede no ser muy alta, pero los bloqueos justos agregan mucha controlabilidad al negocio.

Código de muestra

ReenTrantLock tiene dos métodos de implementación justos e injustos incorporados. El código de ejemplo específico es el siguiente. El bloqueo justo pasa por la cola. Para obtener más información, consulte el método hasQueuedPredecessors

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

/**
 * Fair version of tryAcquire.  Don't grant access unless
 * recursive call or no waiters or is first.
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

estado de bloqueo sincronizado

Las cerraduras sincronizadas no tenían cerraduras o cerraduras de peso pesado antes de la versión 1.6 Las deficiencias son muy obvias: el rendimiento es muy bajo con las cerraduras de peso pesado. Para mejorar el rendimiento, la optimización se llevó a cabo después de 1.6. Se introducen dos estados adicionales (bloqueo sesgado y bloqueo ligero) para hacer frente a diferentes escenarios. El orden de progresión es: sin bloqueo -> bloqueo sesgado -> bloqueo ligero -> bloqueo pesado.

sin cerradura

No hay bloqueo de recursos. Por ejemplo, los principios y aplicaciones de CAS que presentaremos en detalle más adelante están libres de bloqueos.

Bloqueo de sesgo

Si un hilo accede al código del bloque de sincronización varias veces, el hilo adquiere automáticamente el bloqueo sesgado, lo que reduce el costo de adquirir el bloqueo y mejora el rendimiento.

Cerradura ligera

Si hay otro hilo que accede al bloqueo de polarización, el bloqueo de polarización se actualiza a un bloqueo ligero y otros hilos intentan adquirir el bloqueo a través de un bucle giratorio sin bloquear, mejorando así el rendimiento.

Bloqueo de peso pesado

Si otros subprocesos no pueden adquirir el bloqueo mediante operaciones como el giro, ingresan al bloqueo pesado, bloquean el subproceso y devuelven el derecho a usar la CPU.

para resumir

Este artículo proporciona una introducción básica a los conceptos de bloqueo comunes en Java y lo explica desde la perspectiva del código fuente y las aplicaciones prácticas. De hecho, el propio Java ha encapsulado bien el bloqueo, lo que reduce la dificultad de uso por parte de los estudiantes de I + D en su trabajo diario. Sin embargo, en la etapa de entrevista, los estudiantes deben estar familiarizados con los principios subyacentes del candado para poder responder libremente.
A continuación, continuaremos explicando en detalle la implementación y el uso de bloqueos en Java. Tenga paciencia.

Preste atención a la cuenta pública: Tpark técnico artesano. ¡El contenido original se publicará todas las semanas! ! ! Además, puede empujar a Ali adentro, venir y enviar su currículum: [email protected]
Aprenda los bloqueos de Java de una manera sencilla (1)

Supongo que te gusta

Origin blog.51cto.com/14942543/2541939
Recomendado
Clasificación