[Código fuente de JUC] Ejemplo de uso y análisis de código fuente de CountDownLatch

CountDownLatch se llama contador en chino y también se traduce como bloqueo de conteo. Su función principal no es bloquear, sino lograr la función de esperar a través del conteo. Hay dos formas principales de esperar:

  1. Deje que un grupo de subprocesos se ejecuten juntos después de que se complete todo el inicio (el primer subproceso iniciado debe bloquear el subproceso que se inicia después de esperar hasta que se inicien todos los subprocesos y luego ejecutarse juntos);
  2. El hilo principal espera a que se complete la ejecución de otro grupo de hilos antes de continuar.

1. Estructura

Las variables de miembros principales y los constructores principales de CountDownLatch son los siguientes:

public class CountDownLatch {
    
    
	// 从 Sync 的继承关系就可以看出,CountDownLatch也是基于AQS框架实现的
    private static final class Sync extends AbstractQueuedSynchronizer {
    
    
        private static final long serialVersionUID = 4982264981922014374L;
		
		// 构造函数,直接设置state=count
        Sync(int count) {
    
    
            setState(count);
        }
		// 调用AQS方法获取state
        int getCount() {
    
    
            return getState();
        }
		// 能否获取到共享锁。如果当前同步器的状态是 0 的话,表示可获得锁
        protected int tryAcquireShared(int acquires) {
    
    
            return (getState() == 0) ? 1 : -1; // state!=0,就拿锁失败
        }
		// 对 state 进行递减,直到 state 变成 0;state 递减为 0 时,返回 true,其余返回 false
        protected boolean tryReleaseShared(int releases) {
    
    
            // 自旋保证 CAS 一定可以成功
            for (;;) {
    
    
                int c = getState();
                // state 已经是 0 了,直接返回 false
                if (c == 0)
                    return false;
                // state--
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
    
    private final Sync sync;
    
    //-----------------------------构造函数------------------------------------
    // 无空参构造,必须传入count,count相当于要等待的线程数
    public CountDownLatch(int count) {
    
    
        if (count < 0) throw new IllegalArgumentException("count < 0");
        // 将count传给sync
        this.sync = new Sync(count);
    }
}

Antes de mirar el código fuente del método específico, pongamos un ejemplo simple de uso: se simula una carrera de 100 metros, 10 corredores están listos, esperando que el árbitro dé una orden. Cuando todos llegan a la meta, el juego termina. Es fácil pensar que el hilo principal simula al árbitro y abre diez subprocesos para simular a los deportistas, pero hay dos problemas:

  1. 10 subprocesos deben esperar hasta que el hilo principal emita un comando (la consola imprime Game Start) antes de ejecutarse
  2. El hilo principal debe esperar a que se completen los 10 hilos secundarios antes de salir

Veamos cómo resolver estos dos problemas mediante dos CountDownLatch.

public class CountDownLatchTest {
    
    

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

        // 开始的倒数锁。count设置为1 是因为主线程只用-1
        final CountDownLatch begin = new CountDownLatch(1);  

        // 结束的倒数锁,count设置为10 是因为有10个子线程都要-1
        final CountDownLatch end = new CountDownLatch(10);  

        // 通过线程池创造出十名选手 (十个线程)
        final ExecutorService exec = Executors.newFixedThreadPool(10);  
		
		// 让这十个线程运行起来
        for (int index = 0; index < 10; index++) {
    
    
            final int NO = index + 1;  // 编号【1,10】
            Runnable run = new Runnable() {
    
    
                public void run() {
    
      
                    try {
    
      
                        // 如果当前计数为零(主线程已就绪),则此方法立即返回。
                        // 如果当前计数不为0(主线程还未调用countDown),等待。
                        begin.await();  
                        Thread.sleep((long) (Math.random() * 10000));  
                        System.out.println("No." + NO + " arrived");  
                    } catch (InterruptedException e) {
    
      
                    } finally {
    
      
                        // 每个选手到达终点(线程执行完毕)时,end就减一
                        end.countDown();
                    }  
                }  
            };  
            exec.submit(run);
        }  
        System.out.println("Game Start");  
        // begin减一,开始游戏
        begin.countDown();  
        // 主线程会阻塞在这里,等待end变为0,即所有选手到达终点
        end.await();  
        System.out.println("Game Over");  
        exec.shutdown();  
    }
}

La salida de la consola es la siguiente:

Game Start
No.9 arrived
No.6 arrived
No.8 arrived
No.7 arrived
No.10 arrived
No.1 arrived
No.5 arrived
No.4 arrived
No.2 arrived
No.3 arrived
Game Over

2. Análisis de métodos y API

CountDownLatch esencialmente usa el modo de bloqueo compartido AQS

  1. Pase el recuento al construir y asigne el recuento al estado AQS
  2. await: agrega el hilo actual a la cola de sincronización y duerme.
  3. countDown: state--cuando el estado = 0, despierta todos los subprocesos bloqueados en espera para reanudar la operación

Nota: ¿Por qué no utilizar candados exclusivos aquí? Debido a que puede haber varias posiciones de espera, debe despertar todo después del estado = 0

2.1 esperar

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

// 带有超时时间的,最终都会转化成毫秒
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    
    
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

En AQS, el método adquiridoSharedInterruptbly llamará finalmente al método
Inserte la descripción de la imagen aquí
doAcquireInterruptbly doAcquireInterruptbly

  1. Encapsule el hilo actual en un nodo y luego únase a la cola de sincronización.
  2. Si el nodo del hilo actual no es el equipo dos y el estado no es igual a 0, el hilo entra en el estado de bloqueo.
  3. A través del giro, se garantiza que todos los subprocesos despertados puedan reanudar su ejecución.
private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
    
    
    	// 将当前线程封装为node,并加到同步队列队尾
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
    
    
        	// 自旋,保证所有被唤醒的线程都能依次恢复运行
            for (;;) {
    
    
                final Node p = node.predecessor();
                // 当前node前进到队二 && tryAcquire成功(state减到0),就可以执行了
                if (p == head && tryAcquire(arg)) {
    
    
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
    
    
            if (failed)
                cancelAcquire(node);
        }
}

2.2 cuenta atrás

Después de que el hilo llame al método countDown, hará AQS state-1, si state = 0, despertará todos los hilos bloqueados en espera.

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

releaseShared

public final boolean releaseShared(int arg) {
    
    
    // 将state-1,若state=0了,表示当前线程释放锁成功
    if (tryReleaseShared(arg)) {
    
    
        // 唤醒后续节点
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared

protected boolean tryReleaseShared(int releases) {
    
    
    // 自旋保证 CAS 一定可以成功
    for (;;) {
    
    
        int c = getState();
        // state 已经是 0 了,直接返回 false
        if (c == 0)
            return false;
        // state--
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

doReleaseShared

private void doReleaseShared() {
    
    
    // 自旋,保证所有线程正常的线程都能被唤醒
    for (;;) {
    
    
        Node h = head;
        // 还没有到队尾,此时队列中至少有两个节点
        if (h != null && h != tail) {
    
    
            int ws = h.waitStatus;
            // 如果头结点状态是 SIGNAL ,说明后续节点都需要唤醒
            if (ws == Node.SIGNAL) {
    
    
                // CAS 保证只有一个节点可以运行唤醒的操作
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 进行唤醒操作
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 退出自旋条件 h==head,一般出现于以下两种情况
        // 第一种情况,头节点没有发生移动,结束。
        // 第二种情况,因为此方法可以被两处调用,一次是获得锁的地方,一处是释放锁的地方,
        // 加上共享锁的特性就是可以多个线程获得锁,也可以释放锁,这就导致头节点可能会发生变化,
        // 如果头节点发生了变化,就继续循环,一直循环到头节点不变化时,结束循环。
        if (h == head)                   // loop if head changed
            break;
    }
}

3. Ejemplo de reembolso simulado

  • Xiao Ming compró un producto en Taobao y sintió que no era bueno, por lo que devolvió el producto (el producto aún no se ha enviado, solo se reembolsará el dinero). Lo llamamos reembolso de producto único. Cuando se ejecuta un reembolso de producto único en el sistema de fondo, el consumo total El tiempo es de 30 milisegundos.
  • En Double 11, Xiao Ming compró 40 artículos en Taobao y generó el mismo pedido (de hecho, se pueden generar múltiples pedidos, pero por el bien de la descripción, diremos uno). Al día siguiente, Xiao Ming descubrió que 30 de los artículos se consumieron impulsivamente. Sí, debe devolver 30 artículos juntos.
// 单商品退款,耗时 30 毫秒,退款成功返回 true,失败返回 false
@Slf4j
public class RefundDemo {
    
    

  /**
   * 根据商品 ID 进行退款
   * @param itemId
   * @return
   */
  public boolean refundByItem(Long itemId) {
    
    
    try {
    
    
      // 线程沉睡 30 毫秒,模拟单个商品退款过程
      Thread.sleep(30);
      log.info("refund success,itemId is {}", itemId);
      return true;
    } catch (Exception e) {
    
    
      log.error("refundByItemError,itemId is {}", itemId);
      return false;
    }
  }
}
@Slf4j
public class BatchRefundDemo {
    
    
  //定义线程池
  public static final ExecutorService EXECUTOR_SERVICE =
      new ThreadPoolExecutor(10, 10, 0L,
                                TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<>(20));
  @Test
  public void batchRefund() throws InterruptedException {
    
    
    // state 初始化为 30 
    CountDownLatch countDownLatch = new CountDownLatch(30);
    RefundDemo refundDemo = new RefundDemo();

    // 准备 30 个商品
    List<Long> items = Lists.newArrayListWithCapacity(30);
    for (int i = 0; i < 30; i++) {
    
    
      items.add(Long.valueOf(i+""));
    }

    // 准备开始批量退款
    List<Future> futures = Lists.newArrayListWithCapacity(30);
    for (Long item : items) {
    
    
      // 使用 Callable,因为我们需要等到返回值
      Future<Boolean> future = EXECUTOR_SERVICE.submit(new Callable<Boolean>() {
    
    
        @Override
        public Boolean call() throws Exception {
    
    
          boolean result = refundDemo.refundByItem(item);
          // 每个子线程都会执行 countDown,使 state -1 ,但只有最后一个才能真的唤醒主线程
          countDownLatch.countDown();
          return result;
        }
      });
      // 收集批量退款的结果
      futures.add(future);
    }

    log.info("30 个商品已经在退款中");
    // 使主线程阻塞,一直等待 30 个商品都退款完成,才能继续执行
    countDownLatch.await();
    log.info("30 个商品已经退款完成");
    // 拿到所有结果进行分析
    List<Boolean> result = futures.stream().map(fu-> {
    
    
      try {
    
    
        // get 的超时时间设置的是 1 毫秒,是为了说明此时所有的子线程都已经执行完成了
        return (Boolean) fu.get(1,TimeUnit.MILLISECONDS);
      } catch (InterruptedException e) {
    
    
        e.printStackTrace();
      } catch (ExecutionException e) {
    
    
        e.printStackTrace();
      } catch (TimeoutException e) {
    
    
        e.printStackTrace();
      }
      return false;
    }).collect(Collectors.toList());
    
     // 打印结果统计
    long success = result.stream().filter(r->r.equals(true)).count();
    log.info("执行结果成功{},失败{}",success,result.size()-success);
  }
}

A través del código anterior, después de que se completa el reembolso de 30 productos, el tiempo total es de aproximadamente 200 milisegundos. Sin embargo, se tarda aproximadamente 1 segundo en reembolsar un solo producto a través del bucle for y la diferencia de rendimiento es aproximadamente 5 veces. El código para el reembolso del bucle for es el siguiente:

long begin1 = System.currentTimeMillis();
for (Long item : items) {
    
    
  refundDemo.refundByItem(item);
}
log.info("for 循环单个退款耗时{}",System.currentTimeMillis()-begin1);

Al final del artículo, se plantea una pregunta como revisión y resumen del texto completo. Si un hilo necesita esperar a que se ejecute un grupo de hilos antes de continuar, ¿hay alguna buena manera? ¿Cómo se logra?

Respuesta: CountDownLatch proporciona dicho mecanismo. Por ejemplo, si hay 5 subprocesos en un grupo, solo necesita asignar 5 al estado del sincronizador al inicializar CountDownLatch, el subproceso principal ejecuta CountDownLatch.await y los subprocesos secundarios ejecutan CountDownLatch.countDown. .

Supongo que te gusta

Origin blog.csdn.net/weixin_43935927/article/details/108718739
Recomendado
Clasificación