CountDownLatch được sử dụng như thế nào trong đa luồng Java?


183

Ai đó có thể giúp tôi hiểu Java CountDownLatchlà gì và khi nào sử dụng nó không?

Tôi không có một ý tưởng rất rõ ràng về cách thức hoạt động của chương trình này. Theo tôi hiểu, cả ba luồng bắt đầu cùng một lúc và mỗi luồng sẽ gọi CountDownLatch sau 3000ms. Vì vậy, đếm ngược sẽ giảm từng cái một. Sau khi chốt trở thành số không, chương trình sẽ in "Đã hoàn thành". Có lẽ cách tôi hiểu là không chính xác.

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class Processor implements Runnable {
    private CountDownLatch latch;

    public Processor(CountDownLatch latch) {
        this.latch = latch;
    }

    public void run() {
        System.out.println("Started.");

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        latch.countDown();
    }
}

// ------------------------------------------------ -----

public class App {

    public static void main(String[] args) {

        CountDownLatch latch = new CountDownLatch(3); // coundown from 3 to 0

        ExecutorService executor = Executors.newFixedThreadPool(3); // 3 Threads in pool

        for(int i=0; i < 3; i++) {
            executor.submit(new Processor(latch)); // ref to latch. each time call new Processes latch will count down by 1
        }

        try {
            latch.await();  // wait until latch counted down to 0
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Completed.");
    }

}


8
Tôi chỉ sử dụng mã mẫu câu hỏi của bạn cho một lô dịch vụ song song của Android và nó hoạt động như một bùa mê. Cảm ơn bạn rất nhiều!
Roisgoen

Đến đây từ video này từ năm 2012, cho thấy sự tương đồng đáng chú ý với ví dụ được hiển thị ở đây. Đối với bất kỳ ai quan tâm, đây là một phần của loạt bài hướng dẫn đa luồng Java từ một anh chàng tên John. Tôi thích John. Rất khuyến khích.
Elia Grady

Câu trả lời:


194

Vâng, bạn đã hiểu chính xác. CountDownLatchhoạt động theo nguyên tắc chốt, luồng chính sẽ đợi cho đến khi cổng được mở. Một luồng chờ cho n luồng, được chỉ định trong khi tạo CountDownLatch.

Bất kỳ luồng nào, thường là luồng chính của ứng dụng, các cuộc gọi CountDownLatch.await()sẽ đợi cho đến khi số đếm bằng 0 hoặc bị gián đoạn bởi luồng khác. Tất cả các chủ đề khác được yêu cầu đếm ngược bằng cách gọi CountDownLatch.countDown()một khi chúng đã hoàn thành hoặc sẵn sàng.

Ngay khi số đếm đạt đến 0, chuỗi chờ đợi tiếp tục. Một trong những nhược điểm / lợi thế của CountDownLatchnó là nó không thể tái sử dụng: một khi số lượng đạt đến 0 bạn không thể sử dụng CountDownLatchnữa.

Biên tập:

Sử dụng CountDownLatchkhi một luồng (như luồng chính) yêu cầu đợi một hoặc nhiều luồng hoàn thành, trước khi nó có thể tiếp tục xử lý.

Một ví dụ cổ điển về việc sử dụng CountDownLatchtrong Java là một ứng dụng Java lõi của máy chủ sử dụng kiến ​​trúc dịch vụ, trong đó nhiều dịch vụ được cung cấp bởi nhiều luồng và ứng dụng không thể bắt đầu xử lý cho đến khi tất cả các dịch vụ đã bắt đầu thành công.

Câu hỏi của PS OP có một ví dụ khá đơn giản nên tôi không bao gồm một câu hỏi.


1
Cảm ơn bạn đã trả lời. Bạn có thể cho tôi một ví dụ về nơi áp dụng chốt CountDown?
Amal

11
một hướng dẫn về cách sử dụng CountDownLatch là ở đây howtodoinjava.com/2013/07/18/...
thiagoh

1
@NikolaB Nhưng trong ví dụ này, chúng ta có thể đạt được kết quả tương tự bằng cách sử dụng phương thức nối không?
Vikas Verma

3
Tôi sẽ coi khả năng không thể tái sử dụng là một lợi thế: bạn chắc chắn rằng không ai có thể thiết lập lại hoặc tăng số lượng.
ataulm

3
Giải thích tốt đẹp. Nhưng tôi sẽ không đồng ý ở điểm này One thread waits for n number of threads specified while creating CountDownLatch in Java. Nếu bạn cần cơ chế như vậy, thì nó là thận trọng để sử dụng CyclicBarrier. Sự khác biệt về khái niệm cơ bản giữa hai điều này, như được đưa ra Java concurrency in Practicelà : Latches are for waiting for events; barriers are for waiting for other threads. cyclicBarrier.await()đi vào trạng thái chặn.
Rahul Dev Mishra

43

CountDownLatchtrong Java là một loại đồng bộ hóa cho phép một người Thread chờ một hoặc nhiều Threadgiây trước khi bắt đầu xử lý.

CountDownLatchHoạt động theo nguyên tắc chốt, luồng sẽ đợi cho đến khi cổng được mở. Một luồng chờ nsố lượng luồng được chỉ định trong khi tạo CountDownLatch.

ví dụ final CountDownLatch latch = new CountDownLatch(3);

Ở đây chúng tôi đặt bộ đếm thành 3.

Bất kỳ luồng nào, thường là luồng chính của ứng dụng, các cuộc gọi CountDownLatch.await()sẽ đợi cho đến khi số đếm bằng 0 hoặc bị gián đoạn bởi một luồng khác Thread. Tất cả các chủ đề khác được yêu cầu đếm ngược bằng cách gọi CountDownLatch.countDown()một khi chúng đã hoàn thành hoặc sẵn sàng cho công việc. ngay khi số đếm đạt đến 0, sự Threadchờ đợi bắt đầu chạy.

Ở đây số đếm được giảm theo CountDownLatch.countDown()phương pháp.

Các Threadtrong đó kêu gọi các await()phương pháp sẽ đợi cho đến khi đạt đến số lượng ban đầu bằng không.

Để làm cho số không, các chủ đề khác cần gọi countDown()phương thức. Khi số đếm trở thành 0, luồng được gọi, await()phương thức sẽ tiếp tục (bắt đầu thực hiện).

Nhược điểm của CountDownLatchnó là nó không thể tái sử dụng: một khi số đếm trở thành số 0 thì nó không còn sử dụng được nữa.


chúng ta có sử dụng new CountDownLatch(3)như chúng ta có 3 luồng từ newFixedThreadPool định nghĩa không?
Chaklader Asfak Arefe

không nên "trước khi nó bắt đầu xử lý" được "trước khi nó tiếp tục xử lý"?
Maria Ines Parnisari

@Arefe Vâng, đó là số lượng chủ đề đi qua khối mã của bạn
Vishal Akkalkote

23

NikolaB đã giải thích nó rất tốt, tuy nhiên ví dụ sẽ rất hữu ích để hiểu, vì vậy đây là một ví dụ đơn giản ...

 import java.util.concurrent.*;


  public class CountDownLatchExample {

  public static class ProcessThread implements Runnable {

    CountDownLatch latch;
    long workDuration;
    String name;

    public ProcessThread(String name, CountDownLatch latch, long duration){
        this.name= name;
        this.latch = latch;
        this.workDuration = duration;
    }


    public void run() {
        try {
            System.out.println(name +" Processing Something for "+ workDuration/1000 + " Seconds");
            Thread.sleep(workDuration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name+ "completed its works");
        //when task finished.. count down the latch count...

        // basically this is same as calling lock object notify(), and object here is latch
        latch.countDown();
    }
}


public static void main(String[] args) {
    // Parent thread creating a latch object
    CountDownLatch latch = new CountDownLatch(3);

    new Thread(new ProcessThread("Worker1",latch, 2000)).start(); // time in millis.. 2 secs
    new Thread(new ProcessThread("Worker2",latch, 6000)).start();//6 secs
    new Thread(new ProcessThread("Worker3",latch, 4000)).start();//4 secs


    System.out.println("waiting for Children processes to complete....");
    try {
        //current thread will get notified if all chidren's are done 
        // and thread will resume from wait() mode.
        latch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("All Process Completed....");

    System.out.println("Parent Thread Resuming work....");



     }
  }

22

Nó được sử dụng khi chúng ta muốn chờ nhiều hơn một luồng để hoàn thành nhiệm vụ của nó. Nó tương tự như tham gia vào chủ đề.

Nơi chúng tôi có thể sử dụng CountDownLatch

Hãy xem xét một kịch bản trong đó chúng tôi có yêu cầu trong đó chúng tôi có ba luồng "A", "B" và "C" và chúng tôi chỉ muốn bắt đầu luồng "C" khi các luồng "A" và "B" hoàn thành hoặc hoàn thành một phần nhiệm vụ của chúng.

Nó có thể được áp dụng cho kịch bản CNTT thế giới thực

Hãy xem xét một kịch bản trong đó người quản lý chia các mô-đun giữa các nhóm phát triển (A và B) và anh ta muốn gán nó cho nhóm QA để chỉ thử nghiệm khi cả hai nhóm hoàn thành nhiệm vụ.

public class Manager {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        MyDevTeam teamDevA = new MyDevTeam(countDownLatch, "devA");
        MyDevTeam teamDevB = new MyDevTeam(countDownLatch, "devB");
        teamDevA.start();
        teamDevB.start();
        countDownLatch.await();
        MyQATeam qa = new MyQATeam();
        qa.start();
    }   
}

class MyDevTeam extends Thread {   
    CountDownLatch countDownLatch;
    public MyDevTeam (CountDownLatch countDownLatch, String name) {
        super(name);
        this.countDownLatch = countDownLatch;       
    }   
    @Override
    public void run() {
        System.out.println("Task assigned to development team " + Thread.currentThread().getName());
        try {
                Thread.sleep(2000);
        } catch (InterruptedException ex) {
                ex.printStackTrace();
        }
    System.out.println("Task finished by development team Thread.currentThread().getName());
            this.countDownLatch.countDown();
    }
}

class MyQATeam extends Thread {   
    @Override
    public void run() {
        System.out.println("Task assigned to QA team");
        try {
                Thread.sleep(2000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("Task finished by QA team");
    }
}

Đầu ra của mã trên sẽ là:

Nhiệm vụ được giao cho nhóm phát triển devB

Nhiệm vụ được giao cho nhóm phát triển devA

Nhiệm vụ hoàn thành bởi nhóm phát triển devB

Nhiệm vụ hoàn thành bởi nhóm phát triển devA

Nhiệm vụ được giao cho nhóm QA

Nhiệm vụ được hoàn thành bởi nhóm QA

Ở đây phương thức await () chờ cờ đếm ngược trở thành 0 và phương thức CountDown () làm giảm cờ đếm ngược bằng 1.

Giới hạn của THAM GIA: Ví dụ trên cũng có thể đạt được với THAM GIA, nhưng THAM GIA không thể được sử dụng trong hai trường hợp:

  1. Khi chúng tôi sử dụng ExecutorService thay vì lớp Thread để tạo chủ đề.
  2. Sửa đổi ví dụ trên, nơi Người quản lý muốn bàn giao mã cho nhóm QA ngay khi Phát triển hoàn thành nhiệm vụ 80% của họ. Điều đó có nghĩa là CountDownLatch cho phép chúng tôi sửa đổi triển khai có thể được sử dụng để chờ một luồng khác thực hiện một phần.

3

CoundDownLatch cho phép bạn tạo một luồng chờ cho đến khi tất cả các luồng khác được thực hiện với việc thực thi của chúng.

Mã giả có thể là:

// Main thread starts
// Create CountDownLatch for N threads
// Create and start N threads
// Main thread waits on latch
// N threads completes there tasks are returns
// Main thread resume execution

Bạn có thể muốn chuyển tất cả các mô tả của mình khỏi khối mã
Paul Lo

Nhận xét tốt nhất mặc dù. Tôi thích những bình luận "đến điểm" này thay vì những giải thích lý thuyết.
renatoaraujoc

2

Một ví dụ điển hình về việc khi nào nên sử dụng một cái gì đó như thế này là với Trình kết nối nối tiếp đơn giản Java, truy cập các cổng nối tiếp. Thông thường, bạn sẽ viết một cái gì đó lên cổng và một cách không đồng bộ, trên một luồng khác, thiết bị sẽ phản hồi trên SerialPortEventListener. Thông thường, bạn sẽ muốn tạm dừng sau khi viết vào cổng để chờ phản hồi. Xử lý các khóa luồng cho kịch bản này theo cách thủ công là cực kỳ khó khăn, nhưng sử dụng Countdownlatch thì dễ dàng. Trước khi bạn nghĩ rằng bạn có thể làm điều đó theo cách khác, hãy cẩn thận về các điều kiện chủng tộc mà bạn không bao giờ nghĩ tới !!

Mã giả:

CountDownLatch latch;
void writeData() { 
   latch = new CountDownLatch(1);
   serialPort.writeBytes(sb.toString().getBytes())
   try {
      latch.await(4, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
   }
}
class SerialPortReader implements SerialPortEventListener {
    public void serialEvent(SerialPortEvent event) {
        if(event.isRXCHAR()){//If data is available
            byte buffer[] = serialPort.readBytes(event.getEventValue());
            latch.countDown();
         }
     }
}


2

Nếu bạn thêm một số gỡ lỗi sau khi gọi đến latch.countDown (), điều này có thể giúp bạn hiểu rõ hơn về hành vi của nó.

latch.countDown();
System.out.println("DONE "+this.latch); // Add this debug

Đầu ra sẽ hiển thị Đếm bị giảm. 'Count' này thực sự là số lượng tác vụ Runnable (đối tượng Bộ xử lý) mà bạn đã bắt đầu mà CountDown () chưa được gọi và do đó bị chặn luồng chính trong lệnh gọi đến latch.await ().

DONE java.util.concurrent.CountDownLatch@70e69696[Count = 2]
DONE java.util.concurrent.CountDownLatch@70e69696[Count = 1]
DONE java.util.concurrent.CountDownLatch@70e69696[Count = 0]

2

Từ tài liệu tiên tri về CountDownLatch :

Một trợ giúp đồng bộ hóa cho phép một hoặc nhiều luồng chờ cho đến khi một tập hợp các hoạt động được thực hiện trong các luồng khác hoàn thành.

A CountDownLatchđược khởi tạo với một số lượng nhất định. Các awaitphương thức chặn cho đến khi số lượng hiện tại đạt đến 0 do các yêu cầu của countDown()phương thức, sau đó tất cả các chuỗi chờ được phát hành và bất kỳ lệnh gọi tiếp theo nào đang chờ trả về ngay lập tức. Đây là hiện tượng một lần - không thể thiết lập lại số đếm.

CountDownLatch là một công cụ đồng bộ hóa linh hoạt và có thể được sử dụng cho một số mục đích.

Một CountDownLatchkhởi tạo với số lượng một phục vụ như một chốt bật / tắt đơn giản hoặc cổng: tất cả các luồng đang chờ đợi chờ ở cổng cho đến khi nó được mở bởi một luồng gọi ra CountDown ().

Một CountDownLatchkhởi tạo thành N có thể được sử dụng để làm cho một luồng chờ cho đến khi N luồng hoàn thành một số hành động hoặc một số hành động đã được hoàn thành N lần.

public void await()
           throws InterruptedException

Làm cho luồng hiện tại phải đợi cho đến khi chốt được đếm xuống 0, trừ khi luồng bị gián đoạn.

Nếu số hiện tại bằng không thì phương thức này trả về ngay lập tức.

public void countDown()

Giảm số lượng chốt, giải phóng tất cả các chuỗi chờ nếu số đếm bằng không.

Nếu số lượng hiện tại lớn hơn 0 thì nó giảm dần. Nếu số mới là 0 thì tất cả các luồng chờ được kích hoạt lại cho mục đích lập lịch của luồng.

Giải thích về ví dụ của bạn.

  1. Bạn đã đặt số đếm là 3 cho latchbiến

    CountDownLatch latch = new CountDownLatch(3);
  2. Bạn đã chuyển chia sẻ này latchcho chủ đề Công nhân:Processor

  3. Ba Runnabletrường hợp Processorđã được gửi tớiExecutorService executor
  4. Chủ đề chính ( App) đang chờ đếm trở thành số 0 với câu lệnh dưới đây

     latch.await();  
  5. Processor luồng ngủ trong 3 giây và sau đó nó giảm giá trị đếm với latch.countDown()
  6. Trường Processhợp đầu tiên sẽ thay đổi số lượng chốt là 2 sau khi hoàn thành latch.countDown().

  7. Trường Processhợp thứ hai sẽ thay đổi số lượng chốt là 1 sau khi hoàn thành latch.countDown().

  8. Trường Processhợp thứ ba sẽ thay đổi số lượng chốt là 0 sau khi hoàn thành latch.countDown().

  9. Không có số lượng trên chốt làm cho chủ đề chính Appđi ra từawait

  10. Chương trình ứng dụng in đầu ra này ngay bây giờ: Completed


2

Ví dụ này từ Java Doc đã giúp tôi hiểu rõ các khái niệm:

class Driver { // ...
  void main() throws InterruptedException {
    CountDownLatch startSignal = new CountDownLatch(1);
    CountDownLatch doneSignal = new CountDownLatch(N);

    for (int i = 0; i < N; ++i) // create and start threads
      new Thread(new Worker(startSignal, doneSignal)).start();

    doSomethingElse();            // don't let run yet
    startSignal.countDown();      // let all threads proceed
    doSomethingElse();
    doneSignal.await();           // wait for all to finish
  }
}

class Worker implements Runnable {
  private final CountDownLatch startSignal;
  private final CountDownLatch doneSignal;
  Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
     this.startSignal = startSignal;
     this.doneSignal = doneSignal;
  }
  public void run() {
     try {
       startSignal.await();
       doWork();
       doneSignal.countDown();
     } catch (InterruptedException ex) {} // return;
  }

  void doWork() { ... }
}

Giải thích trực quan:

nhập mô tả hình ảnh ở đây

Rõ ràng, CountDownLatch cho phép một luồng (ở đây Driver) chờ cho đến khi một loạt các luồng đang chạy (ở đây Worker) được thực hiện với sự thực thi của chúng.


1

Như đã đề cập trong JavaDoc ( https://docs.oracle.com/javase/7/docs/api/java/util/concản/CountDownLatch.html ), CountDownLatch là công cụ hỗ trợ đồng bộ hóa, được giới thiệu trong Java 5. Ở đây, đồng bộ hóa không có nghĩa là hạn chế truy cập vào một phần quan trọng. Nhưng hành động thay vì trình tự của các chủ đề khác nhau. Kiểu đồng bộ hóa đạt được thông qua CountDownLatch tương tự như kiểu Tham gia. Giả sử rằng có một luồng "M" cần chờ các luồng công nhân khác "T1", "T2", "T3" để hoàn thành các nhiệm vụ của nó Trước Java 1.5, cách này có thể được thực hiện là, M chạy mã sau

    T1.join();
    T2.join();
    T3.join();

Đoạn mã trên đảm bảo rằng luồng M tiếp tục công việc của nó sau khi T1, T2, T3 hoàn thành công việc của nó. T1, T2, T3 có thể hoàn thành công việc của họ theo bất kỳ thứ tự nào. Điều tương tự có thể đạt được thông qua CountDownLatch, trong đó T1, T2, T3 và luồng M chia sẻ cùng một đối tượng CountDownLatch.
Yêu cầu "M": countDownLatch.await();
trong đó như "T1", "T2", "T3" không countDownLatch.countdown();

Một nhược điểm với phương thức nối là M phải biết về T1, T2, T3. Nếu có một luồng công nhân mới T4 được thêm vào sau đó, thì M cũng phải biết về nó. Điều này có thể tránh được với CountDownLatch. Sau khi thực hiện, chuỗi hành động sẽ là [T1, T2, T3] (thứ tự của T1, T2, T3 có thể là dù sao) -> [M]



0
package practice;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch c= new CountDownLatch(3);  // need to decrements the count (3) to zero by calling countDown() method so that main thread will wake up after calling await() method 
        Task t = new Task(c);
        Task t1 = new Task(c);
        Task t2 = new Task(c);
        t.start();
        t1.start();
        t2.start();
        c.await(); // when count becomes zero main thread will wake up 
        System.out.println("This will print after count down latch count become zero");
    }
}

class Task extends Thread{
    CountDownLatch c;

    public Task(CountDownLatch c) {
        this.c = c;
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(1000);
            c.countDown();   // each thread decrement the count by one 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.