
Chào các bạn Gen Z tài năng! Hôm nay, anh Creyt sẽ cùng các bạn "đập hộp" một khái niệm nghe có vẻ "hàn lâm" nhưng lại "cool ngầu" và cực kỳ thiết yếu trong Java: Runnable interface. Tưởng tượng nhé, cuộc sống của chúng ta bây giờ là đa nhiệm. Bạn vừa lướt TikTok, vừa chat với crush, vừa nghe podcast và thi thoảng lại check mail. Máy tính của chúng ta cũng vậy, nó cần làm nhiều việc cùng lúc để không bị "đơ" khi bạn đang "cày" game hay render video. Đó chính là lúc đa luồng (multithreading) lên ngôi, và Runnable là một trong những "át chủ bài" của nó!
1. Runnable Interface là gì và để làm gì?
Nếu coi một chương trình Java là một công ty, thì các Thread chính là những "nhân viên" chăm chỉ, và Runnable chính là "bản mô tả công việc" hoặc "kế hoạch hành động" mà mỗi nhân viên sẽ thực hiện. Đơn giản không?
Runnable trong Java là một functional interface (interface chỉ có một phương thức trừu tượng duy nhất) nằm trong gói java.lang. Phương thức duy nhất đó là:
public void run();
Để làm gì? Nó định nghĩa một tác vụ (task) mà một luồng (Thread) có thể thực thi. Khi bạn tạo một Thread và truyền vào nó một đối tượng Runnable, bạn đang nói với Thread đó rằng: "Ê bạn ơi, hãy chạy cái run() method trong đối tượng này đi!".
Tại sao lại cần nó mà không extends Thread luôn? Đây mới là cái hay! Việc sử dụng Runnable giúp bạn:
- Tách biệt trách nhiệm:
Runnablechỉ lo "cái gì sẽ chạy", cònThreadlo "ai sẽ chạy" và "làm thế nào để chạy". Giống như bạn có một đầu bếp (Runnable) chuyên nấu ăn, còn người phục vụ (Thread) chuyên bưng món ra vậy. Mỗi người một việc, rõ ràng, rành mạch. - Linh hoạt hơn: Java không cho phép đa kế thừa (multi-inheritance). Nếu class của bạn đã
extendsmột class khác rồi thì "hết cửa"extends Threadnữa. Lúc đó,implements Runnablelà "cứu tinh" của bạn. - Tái sử dụng: Một đối tượng
Runnablecó thể được dùng bởi nhiềuThreadkhác nhau, mỗiThreadsẽ thực thi cùng một tác vụ. Tiết kiệm tài nguyên và code.
2. Code Ví Dụ Minh Hoạ Rõ Ràng
Anh Creyt sẽ cho các bạn hai ví dụ, một "cổ điển" và một "hiện đại" hơn (dùng lambda expression).
Ví dụ 1: Class riêng implements Runnable
class MyTask implements Runnable {
private String taskName;
public MyTask(String name) {
this.taskName = name;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("[" + taskName + "] Đang thực hiện bước " + i + " bởi " + Thread.currentThread().getName());
try {
Thread.sleep(500); // Giả lập công việc tốn thời gian
} catch (InterruptedException e) {
System.out.println("[" + taskName + "] Bị gián đoạn!");
Thread.currentThread().interrupt(); // Đặt lại trạng thái ngắt
}
}
System.out.println("[" + taskName + "] Hoàn thành!");
}
}
public class RunnableDemo {
public static void main(String[] args) {
System.out.println("Bắt đầu chương trình chính.");
// Tạo 2 tác vụ Runnable
MyTask task1 = new MyTask("Tác vụ A");
MyTask task2 = new MyTask("Tác vụ B");
// Tạo 2 Thread và gán tác vụ cho chúng
Thread thread1 = new Thread(task1, "Worker-1");
Thread thread2 = new Thread(task2, "Worker-2");
// Bắt đầu các Thread
thread1.start();
thread2.start();
System.out.println("Chương trình chính kết thúc (nhưng các luồng con vẫn đang chạy).");
}
}
Kết quả có thể thấy (thứ tự có thể khác nhau do đa luồng):
Bắt đầu chương trình chính.
Chương trình chính kết thúc (nhưng các luồng con vẫn đang chạy).
[Tác vụ A] Đang thực hiện bước 0 bởi Worker-1
[Tác vụ B] Đang thực hiện bước 0 bởi Worker-2
[Tác vụ A] Đang thực hiện bước 1 bởi Worker-1
[Tác vụ B] Đang thực hiện bước 1 bởi Worker-2
[Tác vụ A] Đang thực hiện bước 2 bởi Worker-1
[Tác vụ B] Đang thực hiện bước 2 bởi Worker-2
[Tác vụ A] Hoàn thành!
[Tác vụ B] Hoàn thành!
Các bạn thấy đó, chương trình chính "đi tiếp" ngay lập tức mà không chờ Tác vụ A và Tác vụ B hoàn thành. Đó là sức mạnh của đa luồng!
Ví dụ 2: Dùng Lambda Expression (Gen Z thích sự gọn gàng)
Vì Runnable là một functional interface, bạn có thể dùng lambda expression để tạo đối tượng Runnable "on the fly" (tức thì) mà không cần tạo class riêng. Tiện lợi cực kỳ!
public class RunnableLambdaDemo {
public static void main(String[] args) {
System.out.println("Bắt đầu chương trình chính với Lambda.");
// Tạo tác vụ Runnable bằng Lambda Expression
Runnable myLambdaTask = () -> {
for (int i = 0; i < 3; i++) {
System.out.println("[Lambda Task] Đang thực hiện bước " + i + " bởi " + Thread.currentThread().getName());
try {
Thread.sleep(700);
} catch (InterruptedException e) {
System.out.println("[Lambda Task] Bị gián đoạn!");
Thread.currentThread().interrupt();
}
}
System.out.println("[Lambda Task] Hoàn thành!");
};
// Tạo và chạy Thread với Lambda Task
Thread lambdaThread = new Thread(myLambdaTask, "Lambda-Worker");
lambdaThread.start();
// Hoặc ngắn gọn hơn nữa, tạo Thread trực tiếp với Lambda:
new Thread(() -> {
for (int i = 0; i < 2; i++) {
System.out.println("[Quick Task] Đang chạy " + i + " bởi " + Thread.currentThread().getName());
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("[Quick Task] Xong!");
}, "Quick-Worker").start();
System.out.println("Chương trình chính kết thúc (Lambda).");
}
}

3. Mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế
Anh Creyt có vài "chiêu" nhỏ giúp các bạn "master" Runnable:
- Ưu tiên
implements Runnablehơnextends Thread: Đây là "quy tắc vàng" của dân lập trình Java.Runnablegiúp code của bạn linh hoạt hơn, dễ bảo trì hơn và tránh được vấn đề "độc quyền" kế thừa. Hãy nhớ: "Task là Task, Thread là Thread!". - Giữ
run()method "gọn gàng": Phương thứcrun()chỉ nên chứa logic cụ thể của tác vụ cần chạy song song. Tránh nhét quá nhiều thứ vào đây, đặc biệt là những logic không liên quan đến tác vụ chính. - Xử lý ngoại lệ (Exception Handling) trong
run(): Các ngoại lệ không được xử lý trongrun()sẽ khiến luồng đó chết và có thể làm crash cả ứng dụng. Luôn luôntry-catchnhững đoạn code có thể ném ra ngoại lệ bên trongrun(). Đặc biệt làInterruptedExceptionkhi gọiThread.sleep(),wait(),join(). - Khi "chuyên nghiệp" hơn, dùng
ExecutorService: Khi bạn cần quản lý nhiều luồng, tái sử dụng luồng (thread pooling) hoặc lên lịch tác vụ, hãy tìm hiểuExecutorService. Nó là một "ông trùm" quản lý cácRunnablecủa bạn một cách hiệu quả và an toàn hơn rất nhiều. CoiExecutorServicenhư một "đội trưởng" Thread, cònRunnablelà "binh sĩ" vậy.
4. Ví dụ thực tế các ứng dụng/website đã ứng dụng
Runnable không phải là thứ "trên trời" đâu, nó "ngấm" vào rất nhiều ứng dụng bạn dùng hàng ngày:
- Ứng dụng di động (Android/iOS): Khi bạn cuộn feed Instagram, ảnh/video mới được tải về ở chế độ nền (background) thông qua các
Runnableđể giao diện chính không bị giật lag. Nếu không cóRunnable, bạn sẽ thấy ứng dụng "đứng hình" mỗi khi tải ảnh. - Server Web (ví dụ: Spring Boot): Khi hàng ngàn người dùng truy cập một website cùng lúc, mỗi yêu cầu của người dùng có thể được xử lý bởi một
Threadchạy mộtRunnableđể lấy dữ liệu từ database, xử lý logic, và trả về kết quả. Điều này giúp server có thể phục vụ nhiều người dùng đồng thời. - Phần mềm Desktop (Java Swing/JavaFX): Khi bạn thực hiện một tác vụ "nặng" như xuất báo cáo, nén file, hoặc tính toán phức tạp, tác vụ đó sẽ chạy trong một
Runnabletrên mộtThreadriêng biệt để giao diện người dùng không bị "đóng băng" (UI freezing). - Game: Tải tài nguyên (assets) như hình ảnh, âm thanh trong khi game vẫn chạy màn hình loading animation mượt mà.
5. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào
Anh Creyt từng có "kinh nghiệm xương máu" khi mới vào nghề, cứ nghĩ "code tuần tự là ổn" cho đến khi làm một ứng dụng desktop xử lý file Excel cả ngàn dòng. Mỗi lần nhấn nút "Xử lý", cả cái app "chết cứng" mấy chục giây, người dùng cứ tưởng "treo máy". Sau đó, anh học được cách "ném" tác vụ xử lý Excel vào một Runnable và chạy trên một Thread riêng. Kết quả: giao diện vẫn mượt mà, người dùng vẫn có thể làm việc khác hoặc thấy thanh tiến trình "nhảy múa". Đó là lúc anh "ngộ" ra sức mạnh của Runnable.
Vậy, khi nào nên dùng Runnable?
- Khi bạn muốn thực thi một tác vụ bất đồng bộ (asynchronous task): Những tác vụ không cần phải hoàn thành ngay lập tức để chương trình chính tiếp tục chạy. Ví dụ: gửi email thông báo, ghi log, tải dữ liệu từ mạng.
- Khi tác vụ đó tốn nhiều thời gian và bạn không muốn chặn luồng chính (main thread): Đặc biệt quan trọng với các ứng dụng có giao diện người dùng (UI), để tránh tình trạng "Not Responding" (không phản hồi).
- Khi bạn muốn tách biệt logic của tác vụ khỏi cơ chế quản lý luồng: Như anh đã nói,
Runnableđịnh nghĩa "cái gì",Threadđịnh nghĩa "làm thế nào". Sự phân tách này giúp code của bạn sạch sẽ và dễ hiểu hơn. - Khi bạn cần sử dụng một Thread Pool (
ExecutorService):ExecutorServiceđược thiết kế để nhận các đối tượngRunnable(hoặcCallable) để thực thi.
Nhớ nhé các bạn, Runnable là một công cụ cực kỳ mạnh mẽ để xây dựng các ứng dụng nhanh hơn, mượt mà hơn và "thân thiện" hơn với người dùng. Hãy thực hành thật nhiều để biến nó thành "vũ khí" của riêng mình!
Thuộc Series: Java – OOP
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!