[Código-fonte JUC] Análise de código-fonte CountDownLatch e exemplo de uso

CountDownLatch é chamado de contador em chinês e também é traduzido como bloqueio de contagem. Sua principal função não é bloquear, mas realizar a função de espera durante a contagem. Existem duas formas principais de espera:

  1. Deixe um grupo de threads executar junto depois que toda a inicialização for concluída (o primeiro thread iniciado precisa bloquear o thread que é iniciado depois de esperar até que todos os threads sejam iniciados e, em seguida, execute juntos);
  2. O thread principal aguarda a execução de outro grupo de threads para ser concluído antes de continuar.

1. Estrutura

As variáveis ​​de membro principais e os construtores principais de CountDownLatch são os seguintes:

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 olharmos o código-fonte do método específico, vamos colocar um exemplo simples de uso: uma corrida de 100 metros é simulada, 10 corredores estão prontos, apenas esperando o árbitro dar uma ordem. Quando todos alcançam a linha de chegada, o jogo termina. É fácil pensar que o thread principal simula o árbitro e abre dez sub-threads para simular atletas, mas existem dois problemas:

  1. 10 sub-threads devem esperar até que a thread principal emita um comando (o console imprime o Game Start) antes de executar
  2. O thread principal deve aguardar a conclusão dos 10 threads filhos antes de sair

Vamos ver como resolver esses dois problemas por meio de dois 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();  
    }
}

A saída do console é a seguinte:

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álise de método e API

CountDownLatch usa essencialmente o modo de bloqueio compartilhado AQS

  1. Passe a contagem ao construir e atribua a contagem ao estado AQS
  2. esperar: adiciona o thread atual à fila de sincronização e dorme.
  3. countDown: state--quando state = 0, desperta todos os threads bloqueados em espera para retomar a operação

Nota: Por que não usar bloqueios exclusivos aqui? Como pode haver várias posições de espera, você precisa acordar depois de state = 0

2.1 aguardar

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

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

No AQS, o método takingSharedInterruptibly acabará por chamar o método
Insira a descrição da imagem aqui
doAcquireInterruptibly doAcquireInterruptibly

  1. Encapsule o thread atual em um nó e, a seguir, junte-se à fila de sincronização.
  2. Se o nó do segmento atual não for do grupo dois e o estado não for igual a 0, o segmento entra no estado de bloqueio.
  3. Por meio do giro, é garantido que todos os threads ativados possam retomar a execução por vez.
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 contagem regressiva

Após a thread chamar o método countDown, ela executará AQS state-1; se state = 0, ela despertará todas as threads bloqueadas em await.

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. Exemplo de simulação de reembolso

  • Xiao Ming comprou um produto no Taobao e sentiu que não era bom, então ele devolveu o produto (o produto ainda não foi enviado, apenas o dinheiro foi devolvido). Chamamos isso de reembolso de produto único. Quando um único reembolso de produto é executado no sistema de segundo plano, o consumo geral O tempo é 30 milissegundos.
  • No Double 11, Xiao Ming comprou 40 itens no Taobao e gerou o mesmo pedido (na verdade, vários pedidos podem ser gerados, mas para fins de descrição, vamos dizer um). No dia seguinte, Xiao Ming descobriu que 30 dos itens foram consumidos impulsivamente. Sim, você precisa devolver 30 itens 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);
  }
}

Por meio do código acima, depois que o reembolso de 30 produtos for concluído, o tempo total demorado é de cerca de 200 milissegundos. No entanto, leva cerca de 1 segundo para reembolsar um único produto por meio do loop for e a diferença de desempenho é de cerca de 5 vezes. O código para o reembolso do loop for é o seguinte:

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

Ao final do artigo, uma questão é levantada como revisão e resumo do texto completo. Se um thread precisa esperar a execução de um grupo de threads antes de continuar, existe uma boa maneira? Como isso é alcançado?

Resposta: CountDownLatch fornece tal mecanismo. Por exemplo, se houver 5 threads em um grupo, você só precisa atribuir 5 ao estado do sincronizador ao inicializar CountDownLatch, o thread principal executa CountDownLatch.await e os threads filhos executam CountDownLatch.countDown. .

Acho que você gosta

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