Lập trình Đa luồng (Multithreading) Cơ bản
Chào mừng bạn đã đến với bài 14 của series "Chinh Phục Java"! Trong tất cả các bài học trước, các chương trình của chúng ta đều chạy theo một luồng duy nhất. Nó thực hiện công việc A, sau đó đến công việc B, rồi đến công việc C. Nhưng sẽ ra sao nếu công việc A rất tốn thời gian? Toàn bộ chương trình sẽ bị "đóng băng" cho đến khi A hoàn thành. Đây là lúc **Lập trình Đa luồng (Multithreading)** phát huy sức mạnh.
Đa luồng là một khái niệm nâng cao. Nó cho phép ứng dụng của bạn thực hiện **nhiều công việc cùng một lúc** một cách song song. Điều này giúp tận dụng tối đa sức mạnh của các bộ xử lý đa lõi (multi-core CPU) hiện đại. Nó làm cho ứng dụng của bạn có khả năng phản hồi tốt hơn và xử lý các tác vụ nặng một cách hiệu quả.
1. Luồng (Thread) là gì?
Bạn có thể hình dung mỗi ứng dụng Java đang chạy là một **tiến trình (process)**. Mặc định, mỗi tiến trình có ít nhất một **luồng (thread)** thực thi, đó là "luồng chính" (main thread). Đây chính là luồng đã chạy phương thức `main` của chúng ta từ trước đến nay. Một luồng là một đường đi (path) thực thi độc lập bên trong một tiến trình. Các luồng trong cùng một tiến trình có thể chia sẻ chung bộ nhớ.
Lập trình đa luồng có nghĩa là chúng ta sẽ tạo ra thêm các luồng mới bên cạnh luồng chính. Mỗi luồng sẽ được giao một công việc riêng. Ví dụ, trong một ứng dụng giao diện, luồng chính có thể xử lý tương tác của người dùng. Trong khi đó, một luồng khác có thể thực hiện việc tải một file lớn từ mạng. Nhờ vậy, giao diện sẽ không bị "đơ" trong khi file đang được tải.
2. Cách tạo Luồng trong Java
Java cung cấp hai cách chính để tạo ra một luồng mới. Cả hai cách đều yêu cầu bạn phải định nghĩa công việc mà luồng đó sẽ thực hiện. Công việc này được đặt bên trong một phương thức tên là run().
Cách 1: Kế thừa từ lớp `Thread`
Cách này đơn giản và trực quan. Bạn tạo một lớp mới kế thừa từ lớp `java.lang.Thread`. Sau đó, bạn ghi đè (override) phương thức `run()` của nó. Để bắt đầu thực thi luồng, bạn tạo một đối tượng từ lớp này và gọi phương thức start(). Lưu ý: bạn phải gọi `start()`, không phải `run()` trực tiếp. `start()` sẽ tạo ra một luồng mới và ra lệnh cho nó chạy phương thức `run()`.
// Tạo một lớp luồng bằng cách kế thừa
class MyThread extends Thread {
private String threadName;
public MyThread(String name) {
this.threadName = name;
}
@Override
public void run() {
// Đây là công việc mà luồng sẽ thực hiện
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + " is running, count: " + i);
try {
Thread.sleep(500); // Tạm dừng 500 mili giây
} catch (InterruptedException e) {
System.out.println("Thread interrupted.");
}
}
}
}
public class ThreadExample {
public static void main(String[] args) {
// Tạo và khởi chạy hai luồng
MyThread thread1 = new MyThread("Thread A");
MyThread thread2 = new MyThread("Thread B");
thread1.start(); // Bắt đầu luồng 1
thread2.start(); // Bắt đầu luồng 2
}
}
Cách 2: Implement Interface `Runnable` (Khuyến nghị)
Java không hỗ trợ đa kế thừa. Nếu lớp của bạn đã kế thừa từ một lớp khác, bạn không thể kế thừa thêm từ `Thread`. Đây là lúc interface `Runnable` trở nên hữu ích. Bạn tạo một lớp implement `Runnable` và định nghĩa phương thức `run()`. Sau đó, bạn tạo một đối tượng `Thread` và truyền đối tượng `Runnable` của bạn vào hàm khởi tạo của nó.
Cách này được **khuyến nghị** vì nó tách biệt "công việc cần làm" (`Runnable`) ra khỏi "công cụ thực thi" (`Thread`). Nó giúp code của bạn linh hoạt và có cấu trúc tốt hơn.
// Tạo một công việc bằng cách implement Runnable
class MyRunnable implements Runnable {
private String taskName;
public MyRunnable(String name) {
this.taskName = name;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + " is executing, count: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Task interrupted.");
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
// Tạo các đối tượng công việc
MyRunnable task1 = new MyRunnable("Task 1");
MyRunnable task2 = new MyRunnable("Task 2");
// Tạo các đối tượng Thread và gán công việc cho chúng
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
// Khởi chạy các luồng
thread1.start();
thread2.start();
}
}
3. Vấn đề Đồng bộ hóa (Synchronization)
Đa luồng rất mạnh mẽ, nhưng nó cũng đi kèm với những thách thức. Vấn đề lớn nhất là khi nhiều luồng cùng cố gắng truy cập và thay đổi một tài nguyên chia sẻ (ví dụ: một biến, một đối tượng). Điều này có thể dẫn đến dữ liệu bị sai lệch hoặc không nhất quán. Tình huống này được gọi là **"race condition" (cuộc đua điều kiện)**.
Để giải quyết vấn đề này, Java cung cấp cơ chế **đồng bộ hóa (synchronization)**. Cách đơn giản nhất là sử dụng từ khóa synchronized. Khi bạn đánh dấu một phương thức là `synchronized`, Java sẽ đảm bảo rằng tại một thời điểm, chỉ có **một luồng duy nhất** được phép thực thi phương thức đó trên cùng một đối tượng. Các luồng khác sẽ phải xếp hàng chờ.
class Counter {
private int count = 0;
// Phương thức này được đồng bộ hóa
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Đồng bộ hóa là một chủ đề rất lớn và phức tạp. Ở đây, chúng ta chỉ giới thiệu khái niệm cơ bản. Nó giúp đảm bảo tính toàn vẹn dữ liệu trong môi trường đa luồng.
Kết luận
Bạn vừa có cái nhìn đầu tiên về thế giới lập trình đa luồng trong Java. Bạn đã học cách tạo ra các luồng thực thi song song bằng `Thread` và `Runnable`. Đa luồng là một công cụ thiết yếu để xây dựng các ứng dụng hiện đại, có khả năng phản hồi cao. Tuy nhiên, hãy luôn cẩn thận với các vấn đề về đồng bộ hóa. Ở bài cuối cùng của series, chúng ta sẽ tổng kết lại toàn bộ kiến thức và vạch ra các hướng đi tiếp theo cho hành trình lập trình của bạn.
0 Bình luận
Vui lòng đăng nhập để gửi bình luận.