Trong bài viết này, chúng ta sẽ cùng tìm hiểu cách tạo một luồng và sau đó là cách thực hiện các hoạt động cơ bản trên một luồng như bắt đầu, tạm dừng, ngắt và nối. Bạn sẽ có thể hiểu chính xác cách các luồng hoạt động trong Java ở mức thấp.

1. Cách tạo một luồng trong Java

Có hai cách để tạo một luồng trong Java: bằng cách mở rộng lớp Thread ; và bằng cách triển khai giao diện Runnable . Cả hai đều nằm trong gói java.lang nên bạn không cần phải sử dụng câu lệnh nhập.

Sau đó, bạn đặt mã mà cần phải được thực hiện trong một thread riêng biệt bên trong run () phương pháp được ghi đè từ Chủ đề / Runnable . Và gọi phương thức start () trên một đối tượng Thread để đưa luồng vào trạng thái đang chạy (còn sống).

Lớp sau, ThreadExample1 , trình bày theo cách đầu tiên:

public class ThreadExample1 extends Thread { 
    public void run() {    
    System.out.println("My name is: " + getName());    }    
    public static void main(String[] args) {   
    ThreadExample1 t1 = new ThreadExample1();        
    t1.start();         
    System.out.println("My name is: " + Thread.currentThread().getName());    }
 }

Hãy để tôi giải thích cho bạn cách mã này hoạt động. Bạn thấy rằng lớp ThreadExample1 mở rộng lớp Thread và ghi đè phương thức run () . Bên trong phương thức run () , nó chỉ cần in một thông báo bao gồm tên của luồng, được trả về từ phương thức getName () của lớp Thread .

Và bây giờ chúng ta hãy xem phương thức main () được gọi khi chương trình bắt đầu. Nó tạo một thể hiện của lớp ThreadExample1 và gọi phương thức start () của nó để đưa luồng vào trạng thái đang chạy. Và dòng cuối cùng in ra một thông báo bao gồm tên của luồng chính - mọi chương trình Java đều được bắt đầu từ một luồng có tên là main . Phương thức static currentThread () trả về đối tượng Thread được liên kết với luồng hiện tại.

Chạy chương trình này và bạn sẽ thấy kết quả như sau:

My name is: Thread-0

My name is: main

Bạn thấy đấy, thực tế có 2 chủ đề:

Thread-0 : là tên của thread mà chúng ta đã tạo.

main : là tên của luồng chính khởi động chương trình Java.

Luồng Thread-0 kết thúc ngay sau khi phương thức run () của nó chạy hoàn tất và luồng chính kết thúc sau khi phương thức main () hoàn thành việc thực thi.

Có một điểm thú vị là, nếu bạn chạy lại chương trình này vài lần, bạn sẽ thấy đôi khi luồng Thread-0 chạy trước, đôi khi luồng chính chạy trước. Điều này có thể được nhận ra bởi thứ tự của tên luồng trong đầu ra thay đổi ngẫu nhiên. Điều đó có nghĩa là không có gì đảm bảo luồng nào chạy trước vì cả hai đều được khởi động đồng thời. Bạn nên ghi nhớ hành vi này liên quan đến ngữ cảnh đa luồng.

Bây giờ, chúng ta hãy xem cách thứ hai sử dụng giao diện Runnable . Trong đoạn mã dưới đây, lớp ThreadExample2 triển khai giao diện Runnable và ghi đè phương thức run () :

public class ThreadExample2 implements Runnable {
    public void run() {
        System.out.println("My name is: " + Thread.currentThread().getName());
    }
    public static void main(String[] args) {
        Runnable task = new ThreadExample2();
        Thread t2 = new Thread(task);
        t2.start();
        System.out.println("My name is: " + Thread.currentThread().getName());
    }
}

Như bạn có thể thấy, có một sự khác biệt nhỏ so với chương trình trước: Một đối tượng kiểu Runnable ( lớp ThreadExample2 ) được tạo và chuyển đến phương thức khởi tạo của đối tượng Thread ( t2 ). Các Runnable đối tượng có thể được xem như là một nhiệm vụ mà được tách ra khỏi chủ đề mà thực hiện nhiệm vụ.

Hai chương trình hoạt động giống nhau. Vậy ưu nhược điểm của hai cách tạo luồng này là gì?

Đây là câu trả lời:

- Mở rộng lớp Thread có thể sử dụng cho các trường hợp đơn giản. Nó không thể được sử dụng nếu lớp của bạn cần mở rộng một lớp khác vì Java không cho phép nhiều lớp kế thừa.

- Việc triển khai giao diện Runnable linh hoạt hơn vì Java cho phép một lớp vừa có thể mở rộng lớp khác vừa triển khai một hoặc nhiều giao diện.

Và hãy nhớ rằng luồng kết thúc sau khi phương thức run () của nó trả về. Nó được đưa vào trạng thái chết và không thể bắt đầu lại. Bạn không bao giờ có thể khởi động lại một chuỗi đã chết.

Bạn cũng có thể đặt tên cho một luồng thông qua phương thức khởi tạo của lớp Thread hoặc thông qua phương thức setter setName () . Ví dụ:

Thread t1 = new Thread("First Thread");
Thread t2 = new Thread();
t2.setName("Second Thread");

2. Cách tạm dừng một chuỗi

Bạn có thể làm cho luồng hiện đang chạy tạm dừng việc thực thi của nó bằng cách gọi phương thức tĩnh (mili giây) của lớp Thread . Sau đó, luồng hiện tại được đưa vào trạng thái ngủ. Đây là cách tạm dừng chuỗi hiện tại:

try {
    Thread.sleep(2000);
} catch (InterruptedException ex) {
    // code to resume or terminate...
}

Mã này tạm dừng luồng hiện tại trong khoảng 2 giây (hoặc 2000 mili giây). Sau khoảng thời gian đó, luồng sẽ trở lại tiếp tục chạy bình thường.

InterruptException là một ngoại lệ đã được kiểm tra nên bạn phải xử lý nó. Ngoại lệ này được ném ra khi luồng bị gián đoạn bởi một luồng khác.

Hãy xem một ví dụ đầy đủ. Chương trình NumberPrint sau được cập nhật để in 5 số, mỗi số sau mỗi 2 giây:

public class NumberPrint implements Runnable {
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {
                System.out.println("I'm interrupted");
            }
        }
    }
    public static void main(String[] args) {
        Runnable task = new NumberPrint();
        Thread thread = new Thread(task);
        thread.start();
    }
}

Lưu ý rằng bạn không thể tạm dừng một chuỗi từ một chuỗi khác. Chỉ bản thân luồng mới có thể tạm dừng thực thi của nó. Và không có gì đảm bảo rằng luồng luôn ngủ chính xác trong thời gian được chỉ định vì nó có thể bị gián đoạn bởi một luồng khác, được mô tả trong phần tiếp theo.

3. Cách ngắt một chuỗi

Việc ngắt một luồng có thể được sử dụng để dừng hoặc tiếp tục thực thi luồng đó từ một luồng khác. Ví dụ: câu lệnh sau ngắt luồng t1 khỏi luồng hiện tại:

t1.interrupt();

Nếu t1 đang ngủ, thì việc gọi ngắt () trên t1 sẽ khiến ngoại lệ gián đoạn được ném ra. Và liệu luồng nên dừng hay tiếp tục tùy thuộc vào mã xử lý trong khối bắt.

Trong ví dụ mã sau, chuỗi t1 in một thông báo sau mỗi 2 giây và chuỗi chính ngắt t1 sau 5 giây:

public class ThreadInterruptExample implements Runnable {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println("This is message #" + i);
            try {
                Thread.sleep(2000);
                continue;
            } catch (InterruptedException ex) {
                System.out.println("I'm resumed");
            }
        }
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(new ThreadInterruptExample());
        t1.start();
        try {
            Thread.sleep(5000);
            t1.interrupt();
        } catch (InterruptedException ex) {
            // do nothing
        }
    }
}

Như bạn có thể thấy trong khối catch trong phương thức run () , nó tiếp tục vòng lặp for khi luồng bị ngắt:

try {
    Thread.sleep(2000);
} catch (InterruptedException ex) {
    System.out.println("I'm resumed");
    continue;
}

Điều đó có nghĩa là luồng tiếp tục chạy trong khi nó đang ngủ.

Để dừng luồng, chỉ cần thay đổi mã trong khối bắt để trả về từ phương thức run () như sau:

try {
    Thread.sleep(2000);
} catch (InterruptedException ex) {
    System.out.println("I'm about to stop");
    return;
}

Bạn thấy đấy, câu lệnh return khiến phương thức run () trả về, điều đó có nghĩa là luồng kết thúc và chuyển sang trạng thái chết.

Điều gì sẽ xảy ra nếu một luồng không ngủ (không xử lý InterruptException )?

Trong trường hợp này, bạn cần phải kiểm tra trạng thái ngắt của luồng hiện tại bằng cách sử dụng một trong các phương thức sau của lớp Thread :

- bị gián đoạn () : đây trở về phương pháp tĩnh đúng nếu các chủ đề hiện nay đã bị gián đoạn, hoặc giả khác. Lưu ý rằng phương thức này xóa trạng thái ngắt, nghĩa là nếu nó trả về true , thì trạng thái ngắt được đặt thành false .

isInterrupt () : phương thức không tĩnh này kiểm tra trạng thái ngắt của luồng hiện tại và nó không xóa trạng thái ngắt.

Các ThreadInterruptExample trên có thể được sửa đổi để sử dụng phương pháp kiểm tra như sau:

public class ThreadInterruptExample implements Runnable {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println("This is message #" + i);
            if (Thread.interrupted()) {
                System.out.println("I'm about to stop");
                return;
            }
        }
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(new ThreadInterruptExample());
        t1.start();
        try {
            Thread.sleep(5000);
            t1.interrupt();
        } catch (InterruptedException ex) {
            // do nothing
        }
    }
}

Tuy nhiên, phiên bản này không hoạt động giống như phiên bản trước vì luồng t1 kết thúc rất nhanh vì nó không ngủ và các câu lệnh in được thực thi rất nhanh. Vì vậy, ví dụ này chỉ để cho bạn thấy nó được sử dụng như thế nào. Trong thực tế, loại kiểm tra trạng thái ngắt này nên được áp dụng cho các hoạt động chạy lâu dài như IO, mạng, cơ sở dữ liệu, v.v.

Và hãy nhớ rằng khi InterruptException được ném, trạng thái ngắt sẽ bị xóa.

Nếu bạn nhìn vào lớp Thread trong Javadocs, bạn sẽ thấy có 4 phương thức:

	destroy() - stop() - suspend() - resume()

Tuy nhiên, tất cả các phương pháp này không được dùng nữa, có nghĩa là bạn không nên sử dụng chúng. Hãy sử dụng cơ chế ngắt mà tôi đã mô tả cho đến nay:

4. Làm thế nào để tạo một luồng chờ các luồng khác (tham gia)?

Điều này được gọi là tham gia và hữu ích trong trường hợp bạn muốn luồng hiện tại đợi các luồng khác hoàn thành. Sau đó, luồng hiện tại tiếp tục chạy. Ví dụ:

t1.join();

Câu lệnh này làm cho luồng hiện tại đợi luồng t1 hoàn thành trước khi nó tiếp tục. Trong chương trình sau, luồng hiện tại (chính) đợi luồng t1 hoàn thành:

public class ThreadJoinExample implements Runnable {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println("This is message #" + i);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {
                System.out.println("I'm about to stop");
                return;
            }
        }
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(new ThreadJoinExample());
        t1.start();
        try {
            t1.join();
        } catch (InterruptedException ex) {
            // do nothing
        }
        System.out.println("I'm " + Thread.currentThread().getName());
    }
}

Trong chương trình này, luồng hiện tại (chính) luôn kết thúc sau khi luồng t1 hoàn thành. Do đó, bạn thấy thông báo “ Tôi là chính ” luôn được in sau cùng:

This is message #1

This is message #2

This is message #3

This is message #4

This is message #5

This is message #6

This is message #7

This is message #8

This is message #9

This is message #10

I'm main

Lưu ý rằng phương thức join () ném InterruptException nếu luồng hiện tại bị gián đoạn, vì vậy bạn cần nắm bắt nó.

Có 2 quá tải của phương thức join () :

          - join(milliseconds)

          - join(milliseconds,  nanoseconds)

Các phương thức này làm cho luồng hiện tại chờ nhiều nhất trong thời gian được chỉ định. Điều đó có nghĩa là nếu hết thời gian và luồng đã tham gia chưa hoàn thành, luồng hiện tại vẫn tiếp tục chạy bình thường.

Bạn cũng có thể tham gia nhiều chuỗi với chuỗi hiện tại, ví dụ:

t1.join();

t2.join();

t3.join();

Trong trường hợp này, luồng hiện tại phải đợi cho cả ba luồng t1 , t2 và t3 hoàn thành trước khi nó có thể tiếp tục chạy.

Đó là nguyên tắc cơ bản của việc sử dụng các luồng trong Java.

Bây giờ bạn có thể tạo, bắt đầu, ngắt (để dừng hoặc tiếp tục) và tham gia (chờ) các chuỗi.

Tham khảo tại đây.