Nền tảng Java được thiết kế ngay từ đầu hỗ trợ lập trình đồng thời (concurrent programming), bao gồm các lệnh, từ khóa, lớp đối tượng ngay trong ngôn ngữ lập trình lẫn thông qua chức năng cung cấp bởi các thư viện hỗ trợ. Java là một trong những ngôn ngữ lập trình đầu tiên làm cho việc lập trình đồng thời trở nên dễ dàng và thuận tiện. Java sử dụng cơ chế multithreading để thực hiện lập trình đồng thời: trong một chương trình cho phép tạo và chạy nhiều Thread đồng thời. Mỗi thread, còn được gọi là một tiến trình gọn nhẹ (lightweight process), là luồng thực thi chạy trong lòng một chương trình và được phép sử dụng và chia sẻ với nhau tài nguyên chung của chương trình đó.
Vòng đời của một Thread
Một Thread chạy trong một chương trình Java sẽ trải qua nhiều giai đoạn khác nhau, mỗi giai đoạn tương ứng với một trạng thái trong biểu đồ trạng thái trong hình dưới đây.
- New: Khi một thread được tạo ra (bằng toán tử new) nó sẽ ở trạng thái New, là trạng thái đầu tiên trong vòng đời của nó. Thread giữ ở trạng thái này cho đến khi nó được chương trình kích hoạt thông qua lời gọi phương thức start().
- Runnable: Sau khi được chương trình kích hoạt, thread chuyển sang trạng thái runnable. Ở trạng thái này, thread đang thực thi các lệnh của nó.
- Waiting: Thread ở trạng thái waiting (chờ đợi) khi nó bị tạm thời ngừng thực thi và chờ trong khi một thread khác được thực thi. Thread này quay trở lại trạng thái runnable khi nhận được tín hiệu cho phép được thực thi tiếp.
- Timed waiting: Thread ở trạng thái timed waiting khi nó bị tạm thời dừng thực thi trong một khoảng thời gian định trước. Thread này quay lại trạng thái runnable khi hết thời gian chờ hoặc nhận được tín hiệu cho phép được thực thi tiếp.
- Blocked: Thread rơi vào trạng thái blocked khi nó thực thi đến lệnh/khối lệnh được đồng bộ (synchronized statement/block) mà lệnh/khối lệnh này đang được một thread khác thực thi. Thread này quay lại trạng thái runnable khi nó đến lượt thực thi khối lệnh được đồng bộ.
- Terminated: Thread chuyển từ trạng thái runnable sang trạng thái terminated khi nó thực hiện xong công việc. Terminated là trạng thái cuối cùng trong vòng đời của thread. Những thread ở trạng thái này sẽ bị chương trình kết thúc và loại bỏ.
Lợi ích khi sử dụng multithreading
Chương trình chủ động đáp ứng tốt hơn
Nhờ multithreading, chương trình có khả năng đáp ứng đồng thời nhiều yêu cầu cùng một lúc, tránh tình trạng các yêu cầu phải xếp hàng chờ đợi lẫn nhau. Các chức năng trong chương trình được “chuyên môn hóa” và phân công cho các Thread khác nhau, chẳng hạn Thread chuyên lắng nghe yêu cầu và phân phối yêu cầu vừa nhận được ngay cho các Thread chuyên xử lý, tăng khá năng tiếp nhận và đáp ứng yêu cầu.
Giúp cho ứng dụng chạy nhanh hơn
Các thread trong ứng dụng chia sẻ và cạnh tranh sử dụng tài nguyên của ứng dụng do đó khai thác triệt để thời gian nhàn rỗi của bộ vi xử lý cũng như khai thác đồng thời nhiều bộ vi xử lý (nếu máy tính có nhiều bộ vi xử lý), do đó giúp cho ứng dụng chạy nhanh hơn.
Bất lợi khi sử dụng multithreading
Tốn kém do chuyển đổi ngữ cảnh thread
Mỗi thread chạy trong chương trình đều có thông số trạng thái của nó: vị trí chỉ thị lệnh đang thực thi, dữ liệu cục bộ đang được xử lý,… Các thông số trạng thái này được gọi là ngữ cảnh (context). Khi bộ xử lý hoán chuyển việc thực thi từ một thread cho một thread khác, nó sẽ lưu lại ngữ cảnh của thread hiện thời để đánh dấu hiện trạng, ngừng thực thi (còn gọi là ngắt - interrupt) thread này, nạp ngữ cảnh của thread đang chờ để thực thi nó. Thread bị ngắt sẽ tạm dừng và chờ đến lượt nó được tái nạp và thực thi tiếp từ vị trí đã được đánh dấu. Quá trình này được gọi là chuyển đổi ngữ cảnh (context switch) của thread. Việc chuyện đổi ngữ cảnh rõ ràng cũng tốn kém thời gian, do đó chúng ta nên cân nhắc khi sử dụng multithreading nếu lợi ích thu được không nhiều so với những tốn kém của quá trình chuyển đổi ngữ cảnh trên.
Tốn kém tài nguyên quản lý thread
Bên cạnh thời gian tốn kém cho việc chuyển đổi ngữ cảnh, chương trình còn phải tốn bộ nhớ để lưu trữ thông số trạng thái tạm thời. Ngoài ra, hệ điều hành cũng mất tài nguyên phục vụ cho việc quản lý các thread.
Ứng dụng trở nên phức tạp hơn
Rõ ràng việc nấu nhiều món ăn đồng thời sẽ phức tạp hơn so với việc nấu tuần tự từng món ăn. Việc lập trình cho nhiều thread thực thi đồng thời cũng giống như vậy. Người lập trình phải đặc biệt lưu ý đến phần mã lệnh truy cập đến vùng dữ liệu chia sẻ được chạy trên nhiều thread. Các lệnh này, nếu không được đồng bộ, có thể cạnh tranh nhau hỗn loạn trong việc cập nhật dữ liệu dùng chung, dẫn đến sai lệch kết quả. Lỗi phát sinh từ đồng bộ hóa thread không chính xác có thể rất khó để phát hiện, tái tạo và sửa chữa.
Các vấn đề thường gặp với multithreading
Phần này mô tả sơ lược một số vấn đề thường gặp cũng như các kỹ thuật áp dụng để xử lý các vấn đề này. Mỗi vấn đề hoặc kỹ thuật sẽ được trình bày cụ thể trong chuỗi bài viết về Java Concurrency này.
Tình huống tương tranh
Tình huống tương tranh (Race condition) xảy ra khi có hai hay nhiều thread cùng tranh giành truy cập vào tài nguyên chung của chương trình, trong khi tài nguyên chung này đòi hỏi phải được truy cập theo trình tự. Đoạn mã lệnh bên trong một thread gây ra tình huống tương tranh được gọi là đoạn mã tới hạn (critical section - một số tài liệu gọi là vùng tương trực). Chúng ta có thể tránh được tình huống tương tranh bằng cách đồng bộ hóa các đoạn mã tới hạn một cách đúng đắn, sao cho tài nguyên chung không được phép truy cập đồng thời bởi nhiều hơn 1 thread.
Tình trạng tắc nghẽn
Tắc nghẽn (Deadlock) là tình huống hai hay nhiều thread bị chặn để chờ được truy cập vào tài nguyên, trong khi tài nguyên này đang bị chiếm giữ bởi một thread cũng đang bị chặn.
Hình vẽ bên trên minh họa trường hợp tắc nghẽn đơn giản nhất, trong đó hai thread đều bị chặn và chờ được sử dụng tài nguyên mà thread kia đang nắm giữ.
Tình trạng đói tài nguyên
Khi một thread không được cấp đủ tài nguyên để thực thi do bị các thread khác chiếm đoạt hết tài nguyên, thì được gọi là đói tài nguyên (Resource starvation). Giải pháp giải quyết cho tình trạng đói tài nguyên này là tạo ra sự công bằng, tức là tạo ra một cơ chế đồng bộ công bằng hơn để tất cả các thread đều có cơ hội thực thi.
Các kỹ thuật đồng bộ thread
- Đồng bộ hóa bằng khối lệnh synchronized: để đồng bộ các đoạn mã tới hạn nhằm giải quyết trình huống tương tranh, cách đơn giản nhất là sử dụng các khối lệnh synchronized bao bọc các đoạn mã tới hạn.
- Đồng bộ hóa bằng cơ chế Monitor: Sử dụng các phương thức wait(), notify(), notifyAll() sẵn có trong lớp Object để gửi tín hiệu đồng bộ giữa các thread.
- Đồng bộ hóa bằng đối tượng Lock: Cơ chế đồng bộ thông qua đối tượng Lock tương tự như đồng bồ bằng synchronized, mặc dù việc thiết lập khoảng đồng bộ (khối lệnh từ lời gọi lock() đến unlock()) linh hoạt hơn khối synchronized, do đó có thể sử dụng để lập trình những đoạn mã phức tạp hơn. Một điều cần lưu ý là đối tượng Lock được tạo ra từ khối đồng bộ synchronized. Từ phiên bản Java 1.5, Java cung cấp gói thư viện java.util.concurrent.locks chứa sẵn các cài đặt Lock, thuận tiện cho người lập trình sử dụng.
- Đồng bộ hóa bằng cơ chế Semaphore: Semaphore là một cấu trúc đồng bộ thread, được sử dụng để gửi tín hiệu qua lại giữa các thread (giống như Monitor), hoặc dùng để đảm bảo các đoạn mã tới hạn không bị xung đột với nhau (giống như đối tượng lock).