Análisis del principio del sincronizador de subprocesos de Java

Análisis del principio CountDownLatch

En el desarrollo diario, a menudo nos encontramos con un escenario en el que un subproceso necesita esperar a que algunos subprocesos terminen antes de que pueda continuar ejecutándose hacia abajo. Antes de que apareciera CountDownLatch, el método de unión se usaba generalmente para implementarlo, pero el método de unión no era lo suficientemente flexible. , por lo que CountDownLatch fue desarrollado.

Ejemplo

public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(2);

    ExecutorService executorService = Executors.newFixedThreadPool(2);
    // 添加任务
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                // 模拟运行时间
                Thread.sleep(1000);
                System.out.println("thread one over...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 递减计数器
                countDownLatch.countDown();
            }
        }
    });

    // 同上
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println("thread two over...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                countDownLatch.countDown();
            }
        }
    });

    System.out.println("wait all child thread over!");
    // 阻塞直到被interrupt或计数器递减至0
    countDownLatch.await();
    System.out.println("all child thread over!");
    executorService.shutdown();
}

La salida es:

wait all child thread over!
thread one over...
thread two over...
all child thread over!

Las ventajas de CountDownLatch sobre el método de unión se encuentran aproximadamente en dos puntos:

  • Después de llamar al método de unión de un subproceso secundario, el subproceso se bloqueará hasta que el subproceso secundario termine de ejecutarse, y CountDownLatch permite que el subproceso secundario termine de ejecutarse o disminuya el contador durante el proceso en ejecución, lo que significa que el método de espera no tiene que esperar hasta que el hilo secundario termine de regresar.

  • El uso del grupo de subprocesos para administrar subprocesos es generalmente para agregar Runnable directamente al grupo de subprocesos. En este momento, no hay forma de llamar al método de unión del subproceso, pero el contador aún se puede disminuir en el subproceso secundario, lo que significa que CountDownLatch puede ser más flexible que el método de unión Controlar la sincronización de subprocesos.

Estructura del diagrama de clases

Como puede verse en la figura, CountDownLatch se implementa en base a AQS.

Como se puede ver en el siguiente código, el valor del contador de CountDownLatch es el valor de estado de AQS .

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
Sync(int count) {
    setState(count);
}

Análisis de código fuente

vacío espera ()

Cuando el hilo llama al método de espera de CountDownLatch, el hilo actual se bloqueará hasta que el valor del contador de CountDownLatch disminuya a 0 u otros hilos llamen al método de interrupción del hilo actual.

public void await() throws InterruptedException {
    // 允许中断(中断时抛出异常)
    sync.acquireSharedInterruptibly(1);
}

// AQS的方法
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // state=0时tryAcquireShared方法返回1,直接返回
    // 否则执行doAcquireSharedInterruptibly方法
    if (tryAcquireShared(arg) < 0)
        // state不为0,调用该方法使await方法阻塞
        doAcquireSharedInterruptibly(arg);
}

// Sync的方法(重写了AQS中的该方法)
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

// AQS的方法
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 获取state值,state=0时r=1,直接返回,不再阻塞
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 若state不为0则阻塞调用await方法的线程
            // 等到其他线程执行countDown方法使计数器递减至0
            // (state变为0)或该线程被interrupt时
            // 该线程才能继续向下运行
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

espera booleana (tiempo de espera largo, unidad TimeUnit)

En comparación con el método de espera anterior, el hilo de llamada se bloquea hasta el tiempo de espera (unidad especificada por unidad) después de llamar a este método. Incluso si el contador no se reduce a 0 o el hilo de llamada no se interrumpe, el hilo de llamada continuará para correr hacia abajo.

public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

vacío countDown ()

Disminuir el contador Cuando el valor del contador es 0 (es decir, estado = 0), todos los subprocesos bloqueados al llamar al método await se despertarán.

public void countDown() {
    // 将计数器减1
    sync.releaseShared(1);
}

// AQS的方法
public final boolean releaseShared(int arg) {
    // 当state被递减至0时tryReleaseShared返回true
    // 会执行doReleaseShared方法唤醒因调用await方法阻塞的线程
    // 否则如果state不是0的话什么也不做
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

// Sync重写的AQS中的方法
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        // 如果state已经为0,没有递减必要,直接返回
        // 否则会使state变成负数
        if (c == 0)
            return false;
        int nextc = c-1;
        // 通过CAS递减state的值
        if (compareAndSetState(c, nextc))
            // 如果state被递减至0,返回true以进行后续唤醒工作
            return nextc == 0;
    }
}

 

 

El principio de CyclicBarrier

El contador de CountDownLatch es único, lo que significa que cuando el contador llega a 0, la llamada a los métodos await y countDown volverá directamente. CyclicBarrier resuelve este problema. CyclicBarrier significa una barrera de loopback. Puede hacer que un grupo de subprocesos alcancen un estado y luego ejecutarlos todos al mismo tiempo, y luego restablecer su propio estado y usarlo para la siguiente sincronización de estado.

Ejemplo

Suponga que una tarea se compone de la fase 1, la fase 2 y la fase 3. Cada subproceso debe ejecutar la fase 1, la fase 2 y la fase 3 en serie. Cuando varios subprocesos ejecutan la tarea, es necesario asegurarse de que todos los subprocesos ejecuten la fase 1 Fase 2 solo se puede ingresar después de la finalización, y la Fase 3 solo se puede ingresar cuando se hayan ejecutado todos los subprocesos de la Fase 2. El siguiente código se puede usar para lograr:

public static void main(String[] args) {
    // 等待两个线程同步
    CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

    ExecutorService executorService = Executors.newFixedThreadPool(2);
    // 运行两个子线程,当两个子线程的step1都执行完毕后才会执行step2
    // 当两个子线程的step2都执行完毕后才会执行step3
    for(int i = 0; i < 2; i++) {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread() + " step1");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread() + " step2");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread() + " step3");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
    }
    executorService.shutdown();
}

El resultado es el siguiente:

Thread[pool-1-thread-1,5,main] step1
Thread[pool-1-thread-2,5,main] step1
Thread[pool-1-thread-1,5,main] step2
Thread[pool-1-thread-2,5,main] step2
Thread[pool-1-thread-2,5,main] step3
Thread[pool-1-thread-1,5,main] step3

Estructura del diagrama de clases

CyclicBarrier se basa en ReentrantLock y se basa esencialmente en AQS. Las partes se utilizan para registrar el número de subprocesos, lo que indica cuántos subprocesos llaman al método de espera antes de que todos los subprocesos atraviesen la barrera y se agoten. Al principio, el recuento es igual a las partes. Cuando el hilo llama al método await, se reducirá en 1. Cuando el recuento se vuelve 0, se alcanza el punto de barrera. Todos los hilos que llaman a await se ejecutarán juntos. tiempo, CyclicBarrier debe reiniciarse, y contar = partes nuevamente.

El bloqueo se utiliza para garantizar la atomicidad del recuento del contador de actualizaciones. La condición de disparo variable de bloqueo se usa para apoyar la comunicación entre subprocesos usando await y signalAll.

El siguiente es el constructor de CyclicBarrier:

public CyclicBarrier(int parties) {
    this(parties, null);
}

// barrierAction为达到屏障点(parties个线程调用了await方法)时执行的任务
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

La definición de Generación es la siguiente:

private static class Generation {
    // 记录当前屏障是否可以被打破
    boolean broken = false;
}

Análisis de código fuente

int espera ()

El hilo actual se bloqueará al llamar a este método y no regresará hasta que se cumpla una de las siguientes condiciones:

  • Las partes han llamado al método de espera, lo que significa que han alcanzado el punto de barrera.

  • Otros hilos llaman al método de interrupción del hilo actual

  • La bandera rota del objeto Generation se establece en true y se lanza BrokenBarrierExecption

  • public int await() throws InterruptedException, BrokenBarrierException {
        try {
            // false表示不设置超时时间,此时后面参数无意义
            // dowait稍后具体分析
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

    boolean await (tiempo de espera largo, unidad TimeUnit) En comparación con await (), el tiempo de espera devolverá falso.  

public int await(long timeout, TimeUnit unit)
    throws InterruptedException,
            BrokenBarrierException,
            TimeoutException {
           // 设置了超时时间
           // dowait稍后分析     
    return dowait(true, unit.toNanos(timeout));
}

int dowait (tiempo booleano, nanos largos)

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
            TimeoutException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        final Generation g = generation;
        // 屏障已被打破则抛出异常
        if (g.broken)
            throw new BrokenBarrierException();

        // 线程中断则抛出异常
        if (Thread.interrupted()) {
            // 打破屏障
            // 会做三件事
            // 1\. 设置generation的broken为true
            // 2\. 重置count为parites
            // 3\. 调用signalAll激活所有等待线程
            breakBarrier();
            throw new InterruptedException();
        }

        int index = --count;
        // 到达了屏障点
        if (index == 0) {
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    // 执行每一次到达屏障点所需要执行的任务
                    command.run();
                ranAction = true;
                // 重置状态,进入下一次屏障
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // 如果index不为0
        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                // 执行此处时,有可能其他线程已经调用了nextGeneration方法
                // 此时应该使当前线程正常执行下去
                // 否则打破屏障
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();
            // 如果此次屏障已经结束,则正常返回
            if (g != generation)
                return index;
            // 如果是因为超时,则打破屏障并抛出异常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

// 打破屏障
private void breakBarrier() {
    // 设置打破标志
    generation.broken = true;
    // 重置count
    count = parties;
    // 唤醒所有等待的线程
    trip.signalAll();
}

private void nextGeneration() {
    // 唤醒当前屏障下所有被阻塞的线程
    trip.signalAll();
    // 重置状态,进入下一次屏障
    count = parties;
    generation = new Generation();
}

 

Investigación del principio del semáforo

El semáforo semáforo también es un sincronizador. A diferencia de CountDownLatch y CyclicBarrier, su contador interno se incrementa y el valor inicial del contador (normalmente 0) se puede especificar durante la inicialización, pero no es necesario saber el número de subprocesos que deben ser En su lugar, especifique el número de subprocesos que deben sincronizarse al llamar al método de adquisición donde se necesita sincronización.

Ejemplo

public static void main(String[] args) throws InterruptedException {
    final int THREAD_COUNT = 2;
    // 初始信号量为0
    Semaphore semaphore = new Semaphore(0);
    ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

    for (int i = 0; i < THREAD_COUNT; i++){
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + " over");
                // 信号量+1
                semaphore.release();
            }
        });
    }

    // 当信号量达到2时才停止阻塞
    semaphore.acquire(2);
    System.out.println("all child thread over!");

    executorService.shutdown();
}

Estructura del diagrama de clases

Se puede ver en la figura que Semaphore todavía se implementa usando AQS, y se puede seleccionar una estrategia de equidad (el valor predeterminado es injusto).

Análisis de código fuente

adquirir nulo ()

Indica que el hilo actual quiere adquirir un recurso de semáforo. Si el semáforo actual es mayor que 0, el recuento de semáforo actual se reduce en 1 y el método regresa directamente. De lo contrario, si el semáforo actual es igual a 0, se bloquea.

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 可以被中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 调用Sync子类方法尝试获取,这里根据构造函数决定公平策略
    if (tryAcquireShared(arg) < 0)
        // 将当前线程放入阻塞队列,然后再次尝试
        // 如果失败则挂起当前线程
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared es implementado por una subclase de Sync para tomar las acciones correspondientes basadas en la equidad.

La siguiente es la implementación de la estrategia no justa NofairSync:

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        // 如果剩余信号量小于0直接返回
        // 否则如果更新信号量成功则返回
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

Supongamos que el hilo A llama al método de adquisición para intentar adquirir el semáforo, pero está bloqueado debido a un semáforo insuficiente. En este momento, el hilo B aumenta el semáforo a través de la liberación. En este momento, el hilo C puede llamar al método de adquisición para adquirir con éxito el semáforo ( si el semáforo es suficiente)), esta es una manifestación de injusticia.

Lo siguiente es la realización de la equidad:

protected int tryAcquireShared(int acquires) {
    for (;;) {
        // 关键在于先判断AQS队列中是否已经有元素要获取信号量
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

El método hasQueuedPredecessors (consulte el análisis del principio de bloqueo en el paquete concurrente de Java en el Capítulo 6) se utiliza para determinar si el nodo predecesor del subproceso actual también está esperando para adquirir el recurso; de ser así, renunciará al permiso adquirido, y luego el hilo actual se colocará en AQS, de lo contrario, intente obtenerlo.

adquirir nulo (permisos int)

Se pueden obtener múltiples semáforos.

public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}

anular adquirir ininterrumpidamente ()

No responde a las interrupciones.

public void acquireUninterruptibly() {
    sync.acquireShared(1);
}

anular adquirir ininterrumpidamente (permisos int)

No responde a las interrupciones y puede adquirir múltiples semáforos.

public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}

lanzamiento vacío ()

Agregue 1 al semáforo Si un hilo está actualmente bloqueado al llamar al método de adquisición y se coloca en el AQS, se seleccionará un hilo cuyo número de semáforos se pueda satisfacer para su activación de acuerdo con la estrategia de equidad.

public void release() {
    sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
    // 尝试释放资源(增加信号量)
    if (tryReleaseShared(arg)) {
        // 释放资源成功则根据公平性策略唤醒AQS中阻塞的线程
        doReleaseShared();
        return true;
    }
    return false;
}

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

liberación nula (permisos int)

Se pueden agregar varios semáforos.

public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}

 

Supongo que te gusta

Origin blog.csdn.net/u011694328/article/details/113103368
Recomendado
Clasificación