Đa luồng (Multithreading) trong Java

5 minute read

VI. Đa luồng (Multithreading) trong Java:

A. Khởi tạo và thực thi một luồng:

Trong Java, bạn có thể tạo và thực thi một luồng bằng cách mở rộng lớp Thread hoặc triển khai giao diện Runnable. Dưới đây là cách bạn có thể làm điều đó:

  1. Kế thừa lớp Thread:

    • Bước 1: Tạo một lớp con kế thừa từ lớp Thread.
    • Bước 2: Override phương thức run() để xác định công việc mà luồng sẽ thực hiện khi được khởi tạo.
    • Bước 3: Tạo một đối tượng của lớp con đã tạo và gọi phương thức start() để bắt đầu thực thi luồng.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    class MyThread extends Thread {
        public void run() {
            System.out.println("MyThread is running");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyThread thread = new MyThread();
            thread.start();
        }
    }
    
  2. Triển khai giao diện Runnable:

    • Bước 1: Tạo một lớp triển khai giao diện Runnable.
    • Bước 2: Override phương thức run() để xác định công việc mà luồng sẽ thực hiện khi được khởi tạo.
    • Bước 3: Tạo một đối tượng của lớp triển khai Runnable, sau đó chuyển đối tượng này vào một đối tượng của lớp Thread.
    • Bước 4: Gọi phương thức start() trên đối tượng Thread để bắt đầu thực thi luồng.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    class MyRunnable implements Runnable {
        public void run() {
            System.out.println("MyRunnable is running");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable);
            thread.start();
        }
    }
    

Bằng cách này, bạn có thể khởi tạo và thực thi một luồng trong Java. Điều này cho phép bạn thực hiện các tác vụ song song và tận dụng được khả năng xử lý đa luồng của hệ thống.

B. Synchronization và Deadlock:

  1. Synchronization:

    • Trong Java, synchronization được sử dụng để đảm bảo rằng chỉ có một luồng có thể truy cập vào một phần của mã tại một thời điểm.
    • Điều này là cần thiết khi nhiều luồng cố gắng cập nhật hoặc sử dụng các tài nguyên chia sẻ, như biến toàn cục hoặc phương thức của một đối tượng.
    • Bạn có thể sử dụng khối synchronized hoặc từ khóa synchronized để đạt được đồng bộ hóa.
      1
      2
      3
      
      public synchronized void synchronizedMethod() {
          // Các lệnh được đảm bảo chỉ được thực thi bởi một luồng tại một thời điểm
      }
      
  2. Deadlock:

    • Deadlock là tình trạng khi hai hoặc nhiều luồng bị kẹt trong một vòng lặp vô hạn, vì mỗi luồng đang chờ đợi một tài nguyên mà luồng khác đang giữ.
    • Deadlock có thể xảy ra khi một luồng cố gắng giữ một tài nguyên và yêu cầu một tài nguyên khác đã được giữ bởi một luồng khác, và ngược lại.
    • Để tránh deadlock, bạn nên sử dụng các phương thức synchronized một cách cẩn thận và tránh việc giữ tài nguyên trong thời gian dài.

Ví dụ về deadlock:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1: Waiting for resource 2...");
                synchronized (resource2) {
                    System.out.println("Thread 1: Holding resource 1 and 2...");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 2: Waiting for resource 1...");
                synchronized (resource1) {
                    System.out.println("Thread 2: Holding resource 2 and 1...");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

Trong ví dụ này, thread1 sẽ giữ resource1 và yêu cầu resource2, trong khi thread2 đang giữ resource2 và yêu cầu resource1. Điều này dẫn đến deadlock.

C. Xử lý Exception trong đa luồng:

Xử lý ngoại lệ trong đa luồng đôi khi có thể trở nên phức tạp vì mỗi luồng có thể có riêng một cơ chế xử lý ngoại lệ. Dưới đây là một số cách để xử lý ngoại lệ trong đa luồng trong Java:

  1. Xử lý ngoại lệ trong từng luồng:

    • Mỗi luồng có thể có một cơ chế xử lý ngoại lệ riêng bằng cách sử dụng khối try-catch trong phương thức run().
    • Bất kỳ ngoại lệ nào xảy ra trong một luồng sẽ được xử lý bởi khối catch của chính luồng đó.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    class MyThread extends Thread {
        public void run() {
            try {
                // Code có thể gây ra ngoại lệ
            } catch (Exception e) {
                // Xử lý ngoại lệ ở đây
            }
        }
    }
    
  2. Xử lý ngoại lệ toàn cục (Uncaught Exception Handler):

    • Bạn có thể thiết lập một bộ xử lý ngoại lệ toàn cục cho tất cả các luồng sử dụng phương thức setDefaultUncaughtExceptionHandler() của lớp Thread.
    • Bộ xử lý này sẽ được gọi mỗi khi một luồng sinh ra ngoại lệ và không có bất kỳ khối try-catch nào để xử lý.
    1
    2
    3
    4
    5
    
    Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
        public void uncaughtException(Thread t, Throwable e) {
            // Xử lý ngoại lệ ở đây
        }
    });
    
  3. Xử lý ngoại lệ trong ExecutorService:

    • Nếu bạn sử dụng ExecutorService để quản lý luồng, bạn có thể sử dụng phương thức submit() để gửi một Callable hoặc Runnable và sau đó sử dụng Future để nhận kết quả hoặc xử lý ngoại lệ.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    ExecutorService executor = Executors.newFixedThreadPool(5);
    Future<?> future = executor.submit(() -> {
        // Code có thể gây ra ngoại lệ
    });
    
    try {
        future.get(); // Lấy kết quả hoặc xử lý ngoại lệ
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
    

Quá trình xử lý ngoại lệ trong đa luồng cần phải cẩn thận để đảm bảo rằng mọi ngoại lệ đều được xử lý một cách an toàn và không gây ra tình trạng không ổn định cho hệ thống.