1. Luồng thực thi (thread) là gì
Luồng như một dòng công việc độc lập
Trong Java (và trong lập trình nói chung), luồng thực thi là một chuỗi lệnh độc lập chạy song song với các luồng khác trong cùng một chương trình. Hãy hình dung một nhà máy: mỗi thợ may có bàn làm việc và nhiệm vụ riêng, họ làm độc lập, nhưng tất cả cùng tạo nên kết quả chung.
Mặc định, chương trình Java khởi chạy với một luồng — luồng chạy phương thức main. Nhưng không có gì ngăn cản chúng ta tạo thêm các luồng để những phần khác nhau của chương trình thực thi đồng thời.
Tiến trình và luồng: khác nhau ở đâu?
- Tiến trình — là đơn vị thực thi “nặng”. Mỗi tiến trình có vùng nhớ riêng, biến riêng, tài nguyên riêng. Các tiến trình cách ly hoàn toàn với nhau — nếu một cái “hỏng”, những cái khác không bị ảnh hưởng.
- Luồng (thread) — là đơn vị thực thi “nhẹ” bên trong một tiến trình. Tất cả các luồng của cùng một tiến trình chia sẻ bộ nhớ và tài nguyên. Điều đó có nghĩa là chúng dễ trao đổi dữ liệu (và tiếc là cũng dễ cản trở nhau).
So sánh:
Tiến trình — như căn hộ riêng: có tường và cư dân riêng.
Các luồng — như những người sống trong cùng một căn hộ: mỗi người có việc riêng, nhưng bếp và phòng tắm dùng chung.
Trong Java điều này trông ra sao?
Khi bạn chạy chương trình, JVM tạo ít nhất một luồng — luồng chính (main). Nhưng bạn có thể tạo thêm luồng để thực hiện các nhiệm vụ song song.
2. Vì sao cần đa luồng
Tính phản hồi: UI không được “đơ”
Giả sử bạn viết một ứng dụng đồ họa — ví dụ, trình soạn thảo văn bản. Người dùng bấm nút “Lưu”, còn bạn bắt đầu ghi tệp xuống đĩa rất lâu. Nếu làm tất cả trong luồng chính, cửa sổ ứng dụng sẽ “đóng băng”: người dùng không bấm được gì, con trỏ không di chuyển, giao diện không phản hồi. Còn nếu lưu tệp trong một luồng riêng — giao diện vẫn phản hồi, và người dùng thậm chí có thể đổi ý và đóng chương trình.
Ví dụ đời thực:
Bạn mở trình duyệt và bắt đầu tải xuống một tệp lớn. Nếu trình duyệt không dùng luồng, bạn sẽ không thể mở thẻ mới hay cuộn trang cho đến khi tải xong!
Xử lý dữ liệu song song
Giả sử bạn có danh sách một nghìn tệp cần xử lý (ví dụ, tính lại hash hoặc thay thế văn bản). Tại sao không xử lý song song? Mỗi luồng lấy một tệp và làm việc độc lập, và toàn bộ công việc hoàn tất nhanh hơn nhiều lần.
Ví dụ:
Máy chủ xử lý yêu cầu từ hàng trăm khách hàng. Nếu máy chủ làm điều này trong một luồng, những khách hàng còn lại sẽ phải chờ rất lâu. Với các luồng, mỗi yêu cầu được xử lý độc lập!
Tận dụng CPU đa lõi
Bộ xử lý hiện đại không chỉ có một “bộ não”, mà là cả một đội (các lõi) có thể làm việc song song. Nếu chương trình của bạn chỉ dùng một luồng, các lõi còn lại sẽ rảnh rỗi và “chơi Minesweeper”. Còn nếu bạn chạy nhiều luồng — tất cả các lõi đều bận rộn, chương trình chạy nhanh hơn.
Sự thật thú vị:
Ngay cả điện thoại của bạn cũng có nhiều lõi, còn laptop và máy chủ — có thể hàng chục! Không tận dụng hết chúng thì chẳng khác nào mua xe buýt nhưng chỉ đi một mình.
3. Ví dụ thực tế
| Lĩnh vực | Ví dụ đa luồng |
|---|---|
| Tải tệp | Tải xuống nhiều tệp đồng thời |
| Giao diện người dùng (UI) | Ứng dụng không “đơ” khi tải/lưu dữ liệu |
| Máy chủ | Xử lý nhiều yêu cầu mạng song song |
| Trò chơi | Các luồng riêng cho vật lý, đồ họa, âm nhạc, AI |
| Ứng dụng nhắn tin | Nhận tin nhắn, gửi tệp, cập nhật giao diện |
| Xử lý video | Xử lý khung hình song song |
Minh họa nhỏ:
Đầu bếp nấu súp, đồng thời lò nướng nướng bánh, và robot hút bụi dọn sàn — mọi việc diễn ra cùng lúc, nên bữa tối chuẩn bị nhanh hơn!
4. Những khó khăn tiềm ẩn của đa luồng
Race condition (trạng thái tranh chấp)
Khi nhiều luồng đồng thời thay đổi cùng một biến, kết quả có thể khó lường. Ví dụ, nếu hai luồng cùng lúc tăng một bộ đếm dùng chung, giá trị cuối cùng có thể sai. Chúng ta sẽ nói chi tiết hơn trong một bài giảng sau.
Đồng bộ hóa
Để các luồng không cản trở nhau, phải nghĩ cách “thỏa thuận” — ai được phép thay đổi dữ liệu và khi nào. Đó gọi là đồng bộ hóa. Có các từ khóa và cấu trúc chuyên dụng (synchronized, khóa và v.v.), chúng ta sẽ bàn sau.
Deadlock (khóa lẫn nhau)
Đôi khi các luồng “chờ nhau mãi mãi” và chương trình bị treo. Đó gọi là deadlock — một trong những lỗi hiểm hóc nhất trong lập trình đa luồng.
Gỡ lỗi và kiểm thử
Lỗi trong chương trình đa luồng rất khó bắt: lúc thì chạy, lúc thì không. Có khi lỗi chỉ xuất hiện trên máy chủ hay ở máy người dùng, còn trên máy bạn lại hoàn hảo. Điều này khiến việc kiểm thử và gỡ lỗi code đa luồng trở thành một thử thách thực sự.
5. Tóm lược: chương trình đa luồng trông như thế nào
Ví dụ không dùng luồng:
public class Main {
public static void main(String[] args) {
// Đếm đến 5
for (int i = 1; i <= 5; i++) {
System.out.println(i);
}
// In ra các chữ cái
for (char c = 'A'; c <= 'E'; c++) {
System.out.println(c);
}
}
}
Kết quả luôn giống nhau:
1
2
3
4
5
A
B
C
D
E
Ví dụ có luồng:
public class Main {
public static void main(String[] args) {
Thread numbers = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
try {
Thread.sleep(100); // Tạm dừng một chút
} catch (InterruptedException e) {
// Bỏ qua
}
}
});
Thread letters = new Thread(() -> {
for (char c = 'A'; c <= 'E'; c++) {
System.out.println(c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Bỏ qua
}
}
});
numbers.start();
letters.start();
}
}
Kết quả sẽ bị trộn lẫn:
1
A
2
B
3
C
4
D
5
E
hoặc, nếu các luồng “tranh nhau”, thứ tự có thể khác. Điều quan trọng — cả hai vòng lặp chạy song song!
6. Những điểm hữu ích
Sơ đồ trực quan: các luồng phối hợp ra sao
+-------------------+ +-------------------+
| Glavnyy potok | | Vtoroy potok |
+-------------------+ +-------------------+
| 1 | 2 | 3 | 4 | 5 | | A | B | C | D | E |
+-------------------+ +-------------------+
| |
| Oba rabotayut |
| odnovremenno |
+-------------------------+
Java dùng luồng “dưới nắp máy” ở đâu
- Bộ gom rác (Garbage Collector) — một luồng riêng dọn các đối tượng không còn dùng.
- Luồng I/O — đọc và ghi tệp, kết nối mạng.
- Máy chủ và ứng dụng web — mỗi yêu cầu của khách hàng được xử lý trong một luồng riêng.
- Bộ hẹn giờ, bộ lập lịch tác vụ — thực thi tác vụ theo lịch.
7. Lỗi thường gặp của người mới
Lỗi số 1: Kỳ vọng rằng luồng luôn tăng tốc chương trình.
Thực tế, nếu máy của bạn chỉ có một CPU hoặc bạn tổ chức công việc chưa đúng, đa luồng có thể làm chậm chương trình do “xáo trộn” và chi phí chuyển ngữ cảnh giữa các luồng.
Lỗi số 2: Bỏ qua vấn đề đồng bộ.
Nhiều người nghĩ: “Mình chỉ chạy hai luồng thôi, có gì mà sai được?” Nhưng nếu cả hai luồng cùng sửa một biến, kết quả có thể hoàn toàn bất ngờ.
Lỗi số 3: Dùng luồng cho mọi thứ.
Đừng tạo một luồng riêng cho mọi việc lặt vặt. Luồng cũng là tài nguyên; quá nhiều luồng có thể khiến chương trình chậm và thậm chí sập.
Lỗi số 4: Không xử lý lỗi.
Các luồng có thể ném ngoại lệ (ví dụ khi làm việc với tệp hoặc mạng). Nếu không xử lý lỗi, chương trình có thể kết thúc đột ngột hoặc “treo”.
GO TO FULL VERSION