Đa luồng trong Java: Threads và Executors
Khi nói đến đa luồng trong Java, có thể hình dung như ngã 7 đường phố Sài Gòn, chật kín xe lớn bé, nếu không phân làn, chia luồng hợp lý sẽ gây kẹt xe dữ dội.
Bài viết này sẽ giúp khám phá sức mạnh của đa luồng trong Java, tập trung vào hai phần chính: Threads và Executors.
Giới thiệu về đa luồng
Đa luồng cho phép chương trình thực thi nhiều tác vụ cùng một lúc. Hãy tưởng tượng bạn là một đầu bếp trong nhà bếp, vừa luộc nước vừa cắt rau và nướng bánh cùng lúc. Trong Java, đa luồng cho phép chương trình thực hiện nhiều tác vụ tương tự như vậy, giúp tăng tốc độ và hiệu suất.
Tuy nhiên, nếu không quản lý tốt, đa luồng có thể trở thành một cơn ác mộng với các vấn đề như race conditions hay deadlock. Hãy cùng tìm hiểu cách giải quyết vấn đề này với Java Threads và Executors.
1. Threads trong Java
Thread là đơn vị thực thi nhỏ nhất trong một chương trình. Trong Java, mỗi thread hoạt động song song với các thread khác và chia sẻ tài nguyên như bộ nhớ. Điều này mang lại nhiều lợi ích về hiệu suất, nhưng cũng có thể gây ra một số rắc rối nếu không được quản lý cẩn thận.
1.1. Tạo Threads
Có hai cách chính để tạo thread trong Java:
- Kế thừa từ lớp Thread:
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread đang chạy...");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
Tuy đơn giản nhưng phương pháp này hạn chế vì Java không cho phép đa kế thừa. Nếu đã kế thừa từ Thread thì không thể kế thừa từ lớp khác.
- Implement từ interface Runnable:
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable đang chạy...");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
Đây là phương pháp ưu tiên hơn vì nó cho phép mở rộng một lớp khác nếu cần thiết.
1.2. Vòng đời của Thread
Một Thread trong Java trải qua nhiều giai đoạn trong vòng đời của nó, tương tự như cuộc sống con người nhưng diễn ra nhanh hơn nhiều:
New: Thread được tạo ra nhưng chưa bắt đầu hoạt động.Runnable: Thread đã sẵn sàng để chạy, nhưng có thể đang đợi CPU cấp thời gian thực thi.Running: Thread đang thực hiện tác vụ của nó.Blocked: Thread tạm thời bị dừng vì nó đang chờ tài nguyên.Terminated: Thread đã hoàn thành tác vụ và kết thúc.
1.3. Đồng bộ Threads
Khi nhiều thread cùng truy cập và thay đổi tài nguyên chung, có thể xảy ra tình trạng race condition, nơi các thread cạnh tranh để truy cập tài nguyên cùng lúc. Để giải quyết vấn đề này, Java cung cấp đồng bộ hóa.
Ví dụ về race condition khi hai thread cùng tăng giá trị của một biến đếm:
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
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Kết quả cuối cùng: " + counter.getCount());
}
}
Trong lý thuyết, ta kỳ vọng giá trị của biến đếm là 2000, nhưng do hai thread thực thi cùng lúc, kết quả có thể nhỏ hơn. Để giải quyết, chúng ta cần sử dụng đồng bộ hóa:
1
2
3
4
5
6
7
8
9
10
11
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Với synchronized, chỉ có một thread được phép thay đổi biến đếm tại một thời điểm. Thread khác phải đợi đến lượt.
2. Executors: Quản lý Threads
Quản lý thủ công các thread có thể khá rắc rối, nhất là khi cần xử lý nhiều tác vụ. Đây là lúc Executor Framework của Java phát huy tác dụng. Executors giúp quản lý việc tạo và tái sử dụng các thread một cách tự động.
- Fixed Thread Pool:
FixedThreadPool tạo một nhóm thread với số lượng cố định. Khi một thread hoàn thành nhiệm vụ, nó sẽ quay lại nhóm để thực hiện tác vụ khác.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
System.out.println("Tên thread: " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
Ở ví dụ này, chúng ta tạo một nhóm với 3 thread, nhưng có 10 tác vụ cần thực hiện. Các thread sẽ được tái sử dụng cho đến khi tất cả tác vụ hoàn thành.
- Cached Thread Pool:
CachedThreadPool tạo thread khi cần thiết và tái sử dụng các thread đã được tạo khi có thể. Nó hữu ích khi có nhiều tác vụ ngắn hạn.
1
ExecutorService executor = Executors.newCachedThreadPool();
Lưu ý rằng CachedThreadPool có thể tăng kích thước không giới hạn, nên cần cẩn thận để tránh tình trạng hết tài nguyên hệ thống.
- Scheduled Thread Pool:
ScheduledThreadPool cho phép lập lịch để thực hiện các tác vụ sau một khoảng thời gian hoặc định kỳ.
1
2
3
4
5
6
7
8
9
10
11
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Tác vụ được thực hiện sau một khoảng thời gian");
}, 2, 5, TimeUnit.SECONDS);
}
}
Ví dụ trên lập lịch cho tác vụ chạy sau 2 giây kể từ khi bắt đầu, và sau đó tiếp tục chạy cứ mỗi 5 giây.
3. Khi nào sử dụng Threads và Executors
Threads: Sử dụng khi chỉ cần quản lý một vài tác vụ đơn giản và muốn kiểm soát mọi thứ thủ công.Executors: Thích hợp khi có nhiều tác vụ cần thực thi hoặc khi muốn quản lý hiệu quả việc tái sử dụng và quản lý thread.
Kết luận
Đa luồng trong Java là một công cụ mạnh mẽ, nhưng cũng đầy thách thức nếu không quản lý tốt.
Threads cho phép thực thi các tác vụ đồng thời, nhưng việc quản lý chúng thủ công có thể phức tạp.
Executors giải quyết vấn đề này bằng cách cung cấp các nhóm thread được quản lý tự động, giúp tập trung vào logic nghiệp vụ thay vì quản lý thread.
Lưu ý là: synchronized là người bạn đáng tin cậy, Executors là người quản lý nhóm, và đừng để quá nhiều threads chạy lung tung trong mã nguồn!
Bài viết mang tính chất “ghi chú, lưu trữ, chia sẻ và phi lợi nhuận”.
Nếu bạn thấy hữu ích, đừng quên chia sẻ với bạn bè và đồng nghiệp của mình nhé!
Happy coding! 😎 👍🏻 🚀 🔥