Hôm nay chúng ta sẽ ngó sang một chút về Java, ví dụ một câu hỏi phỏng vấn mà bạn được hỏi như sau: yêu cầu cần ba luồng thực hiện tuần tự, có những cách triển khai nào để có thể đạt được nó.

Bài toán sẽ là ta có ba luồng: Luồng 1, Luồng 2, Luồng 3, yêu cầu là luồng 1 sẽ được xử lý trước, sau đó tới luồng 2, và cuối cùng là luồng 3. Chúng ta sẽ có một vài cách như sau:

Sử dụng phương thức Join

import java.util.Objects;

public class ThreadMain {

    public static void main(String[] args) {
        var t1 = new Thread(new Work(null));
        var t2 = new Thread(new Work(t1));
        var t3 = new Thread(new Work(t2));
        t1.start();
        t2.start();
        t3.start();
    }

    static class Work implements Runnable {
        private Thread beforeThread;

        public Work(Thread beforeThread) {
            this.beforeThread = beforeThread;
        }

        @Override
        public void run() {
            if (Objects.nonNull(beforeThread)) {
                try {
                    beforeThread.join();
                    System.out.println("Thread start : " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                System.out.println("Thread start : " + Thread.currentThread().getName());
            }
        }
    }
}

Đọc đoạn code trên các bạn có thể hiểu được phần nào logic, truyền tham số vào trong hàm khởi tạo của các luồng 1, 2, 3. Lúc này luồng 1 sẽ được xử lý trước, luồng 2 join với luồng 1 thì sẽ được xử lý tiếp tục, và cuối cùng luồng 3 join với luồng 2 thì sẽ được xử lý cuối cùng.

Start ứng dụng lên và xem kết quả:

ba-luong-tuan-tu-using-join-method
using join method

Sử dụng CountDown Latch

import java.util.concurrent.CountDownLatch;

public class CountDownLatchMain {

    public static void main(String[] args) {
        var countDownLatchOne = new CountDownLatch(0);
        var countDownLatchTwo = new CountDownLatch(1);
        var countDownLatchThree = new CountDownLatch(1);

        var t1 = new Thread(new Work(countDownLatchOne, countDownLatchTwo));
        var t2 = new Thread(new Work(countDownLatchTwo, countDownLatchThree));
        var t3 = new Thread(new Work(countDownLatchThree, countDownLatchThree));

        t1.start();
        t2.start();
        t3.start();


    }

    static class Work implements Runnable {
        CountDownLatch cOne;
        CountDownLatch cTwo;

        public Work(CountDownLatch cOne, CountDownLatch cTwo) {
            this.cOne = cOne;
            this.cTwo = cTwo;
        }

        @Override
        public void run() {
            try {
                cOne.await();
                System.out.println("Thread using countdown latch start : " + Thread.currentThread().getName());
                cTwo.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Ok, bây giờ start ứng dụng lên và xem kết quả, sau đó sẽ đi chi tiết về cách thực thi của nó:

ba-luong-tuan-tu-using-countdown-latch

Trong hàm main(), ba đối tượng CountDownLatch được tạo với các giá trị ban đầu là 0, 1 và 1.

  • countDownLatchOne được sử dụng để đánh dấu cho tất cả các thread đã sẵn sàng để bắt đầu
  • countDownLatchTwo được sử dụng để bắt đầu t2 sau khi t1 đã kết thúc
  • countDownLatchThree được sử dụng để bắt đầu t3 sau khi t2 đã kết thúc

Ba đối tượng Thread được tạo với ba đối tượng Work tương ứng được truyền vào. Sau đó, ba thread được bắt đầu bằng cách gọi phương thức start().

Ở trong class Work, việc thực thi thể hiện như sau:

  • Class Work là một lớp triển khai Runnable với mục đích thực hiện một số công việc được thực thi trong một thread riêng biệt.
  • Lớp Work có hai tham số đầu vào, đó là hai đối tượng của lớp CountDownLatch, cOnecTwo.
  • Trong phương thức run() của lớp Work, trước khi bắt đầu thực thi công việc, thread sẽ gọi phương thức await() trên cOne, đợi đến khi cOne giảm giá trị đếm ngược xuống 0.
  • Sau đó, thread sẽ in ra một thông báo để xác nhận rằng nó đã bắt đầu thực hiện công việc của nó. Đây là điểm đánh dấu cho việc t1 đã hoàn thành công việc của mình.
  • Cuối cùng, thread sẽ gọi phương thức countDown() trên cTwo để giảm giá trị đếm ngược của cTwo xuống 0. Việc giảm giá trị đếm ngược của cTwo sẽ đánh dấu cho việc t2 được bắt đầu thực thi.
  • Quá trình tương tự sẽ tiếp diễn cho t2t3 theo đúng thứ tự đưa vào. Tức là, t2 sẽ đợi đến khi cTwo giảm giá trị đếm ngược xuống 0 bằng cách gọi phương thức await() trên cTwo, sau đó in ra một thông báo xác nhận và cuối cùng giảm giá trị đếm ngược của cThree xuống 0 bằng cách gọi phương thức countDown() trên cThree.
  • Tương tự, t3 sẽ đợi đến khi cThree giảm giá trị đếm ngược xuống 0 bằng cách gọi phương thức await() trên cThree, in ra một thông báo xác nhận và cuối cùng cũng kết thúc thực thi của nó.

Sử dụng ExecutorService

Chúng ta có thể triển khai một nhóm luồng đơn bằng ExecutorService, tuy nhiên chỉ sử dụng đơn luồng, vì nếu triển khai nhóm nhiều luồng thì sẽ không đảm bảo được thứ tự của T1, T2, T3.

import java.util.concurrent.Executors;

public class ExecutorServiceMain {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> System.out.println("Thread Start : " + Thread.currentThread().getName() + " 1"));

        Thread t2 = new Thread(() -> System.out.println("Thread Start : " + Thread.currentThread().getName() + " 2"));

        Thread t3 = new Thread(() -> System.out.println("Thread Start : " + Thread.currentThread().getName() + " 3"));

//        var executor = Executors.newFixedThreadPool(1);
        var executor = Executors.newSingleThreadExecutor();
        executor.submit(t1);
        executor.submit(t2);
        executor.submit(t3);
        executor.shutdown();
    }
}
ba-luong-tuan-tu-using-executor-service
using executor service

Trên đây là một vài cách xử lý để có thể giải quyết bài toán về thứ tự xử lý của các luồng, chúc các bạn học tốt.

Xem thêm một số bài viết nổi bật: