(1) Vấn đề Nhà sản xuất - Người tiêu dùng
Hàng đợi thường được coi là một container để luân chuyển dữ liệu giữa các luồng. Nhà sản xuất đặt dữ liệu đã "sản xuất" vào container, trong khi người tiêu dùng chỉ cần lấy dữ liệu từ container đó. Hàng đợi chặn (BlockingQueue) được sử dụng rộng rãi trong giải quyết "vấn đề nhà sản xuất - người tiêu dùng", bởi vì nó cung cấp các phương thức chặn khi chèn và khi loại bỏ dữ liệu. Khi container hàng đợi đã đầy, luồng chèn sẽ bị chặn cho đến khi có vị trí trống. Khi container hàng đợi rỗng, luồng loại bỏ sẽ bị chặn cho đến khi container không còn rỗng.
(2) Giao diện BlockingQueue
public interface BlockingQueue<E> extends Queue<E> {
boolean add(E e);
boolean offer(E e);
void put(E e) throws InterruptedException;
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
E take() throws InterruptedException;
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
int remainingCapacity();
boolean remove(Object o);
public boolean contains(Object o);
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}
Vì giao diện BlockingQueue kế thừa từ giao diện Queue, nó cũng chứa các phương thức của Queue. Tổng hợp như sau:
2.1 Phương thức trong giao diện Queue
2.1.1 Chèn phần tử
| Phương thức | Chức năng |
|---|---|
| add(E e) | Chèn phần tử vào hàng đợi, sẽ ném ra IllegalStateException khi chèn |
| offer(E e) | Chèn phần tử vào hàng đợi, trả về true nếu chèn thành công, ngược lại trả về false. Khi hàng đợi đầy không ném ra ngoại lệ |
2.1.2 Loại bỏ phần tử
| Phương thức | Chức năng |
|---|---|
| remove(Object e) | Xóa dữ liệu khỏi hàng đợi, trả về true nếu thành công, ngược lại trả về false |
| poll | Xóa dữ liệu, khi hàng đợi rỗng trả về null |
2.1.3 Xem phần tử
| Phương thức | Chức năng |
|---|---|
| element | Lấy phần tử đầu hàng đợi, nếu hàng đợi rỗng sẽ ném ra NoSuchElementException |
| peek | Lấy phần tử đầu hàng đợi, nếu rỗng trả về null, không ném ra ngoại lệ |
2.2 Phương thức trong giao diện BlockingQueue
2.2.1 Chèn phần tử
| Phương thức | Chức năng |
|---|---|
| put() | Khi hàng đợi chặn đã đầy, luồng chèn dữ liệu vào hàng đợi sẽ bị chặn cho đến khi có vị trí trống |
| offer(E e, long timeout, TimeUnit unit) | Khi hàng đợi chặn đã đầy, luồng chèn dữ liệu cũng sẽ bị chặn cho đến khi có vị trí trống. Tuy nhiên, phương thức này có thể giới hạn thời gian chờ, nếu vượt quá thời gian sẽ trả về ngay |
2.2.2 Loại bỏ dữ liệu
| Phương thức | Chức năng |
|---|---|
| take() | Khi hàng đợi chặn rỗng, luồng lấy dữ liệu đầu hàng đợi sẽ bị chặn |
| poll(long timeout, TimeUnit unit) | Khi hàng đợi chặn rỗng, luồng lấy dữ liệu sẽ bị chặn. Nếu vượt quá thời gian cho trước, sẽ trả về ngay |
(3) Triển khai cụ thể của BlockingQueue
3.1 ArrayBlockingQueue
ArrayBlockingQueue là hàng đợi chặn có giới hạn được triển khai bằng mảng, có đặc tính FIFO. ArrayBlockingQueue có thể được dùng làm "bộ đệm dữ liệu có giới hạn", nơi nhà sản xuất chèn dữ liệu vào hàng đợi chặn, và người tiêu dùng lấy dữ liệu. ArrayBlockingQueue một khi đã tạo ra, không thể thay đổi được.
Mặc định, ArrayBlockingQueue không đảm bảo tính công bằng của hàng đợi, tức là người tiêu dùng chờ lâu nhất không nhất thiết là ưu tiên nhận "sản phẩm". Có khả năng người tiêu dùng đã chờ lâu nhất không thể nhận được tài nguyên, gây ra tình trạng đói. Tuy nhiên, nếu đảm bảo tính công bằng, sẽ làm giảm thông lượng (throughput) của BlockingQueue. Có thể thực hiện tính công bằng bằng cách như sau:
BlockingQueue<Integer> hangDoi = new ArrayBlockingQueue<Integer>(10, true);
3.2 LinkedBlockingQueue
LinkedBlockingQueue tương tự như ArrayBlockingQueue, nhưng được triển khai bằng danh sách liên kết, cũng có đặc tính FIFO, so sánh lại có thông lượng tốt hơn. Để ngăn LinkedBlockingQueue tăng dung lượng quá nhanh, thường sẽ chỉ định độ dài. Nếu không chỉ định độ dài, thường sẽ sử dụng Integer.MAX_VALUE.
3.3 PriorityBlockingQueue
PriorityBlockingQueue là hàng đợi chặn không giới hạn hỗ trợ sắp xếp theo thứ tự ưu tiên, thỏa mãn đặc tính FIFO. Nếu không chỉ định, sẽ sử dụng thứ tự mặc định để sắp xếp, cũng có thể chỉ định quy tắc sắp xếp phần tử bằng cách triển khai phương thức compareTo() trong lớp tùy chỉnh, hoặc chỉ định bộ so sánh (Comparator) qua hàm khởi tạo.
3.4 SynchronousQueue
Thực tế, SynchronousQueue không lưu trữ phần tử thực, chỉ khi có luồng đang xóa dữ liệu, các luồng khác mới có thể chèn dữ liệu. Tương tự, chỉ khi có luồng đang chèn dữ liệu, các luồng khác mới có thể xóa dữ liệu.
3.5 LinkedTransferQueue
LinkedTransferQueue là hàng đợi chặn không giới hạn được cấu trúc bởi danh sách liên kết, vì hàng đợi này triển khai giao diện TransferQueue, so với các hàng đợi chặn khác, nó chủ yếu có những phương thức khác nhau sau:
transfer(E e)
Nếu hiện tại có luồng (người tiêu dùng) đang gọi phương thức take() hoặc poll() có thời gian chờ để tiêu thụ dữ liệu, luồng nhà sản xuất có thể gọi phương thức transfer để truyền dữ liệu cho luồng người tiêu dùng. Nếu hiện tại không có luồng người tiêu dùng, luồng nhà sản xuất sẽ chèn dữ liệu vào cuối hàng đợi, cho đến khi có người tiêu dùng có thể tiêu thụ thì mới thoát;
tryTransfer(E e)
Phương thức tryTransfer nếu hiện tại có luồng người tiêu dùng (gọi phương thức take hoặc phương thức poll có đặc tính thời gian chờ) đang tiêu thụ dữ liệu, phương thức này có thể ngay lập tức truyền dữ liệu cho luồng người tiêu dùng. Nếu hiện tại không có luồng người tiêu dùng tiêu thụ dữ liệu, sẽ ngay lập tức trả về false. Do đó, so với phương thức transfer, phương thức transfer phải đợi đến khi có luồng người tiêu dùng tiêu thụ dữ liệu thì luồng nhà sản xuất mới có thể trả về. Trong khi phương thức tryTransfer có thể ngay lập tức trả về kết quả và thoát.
tryTransfer(E e, long timeout, TimeUnit unit)
Cơ bản giống như phương thức transfer, chỉ thêm đặc tính thời gian chờ, nếu dữ liệu trong thời gian chờ quy định không có người tiêu dùng tiêu thụ thì sẽ trả về false.
3.6 LinkedBlockingDeque
LinkedBlockingDeque là hàng đợi chặn hai đầu (double-ended queue) có giới hạn được cấu trúc bởi danh sách liên kết. Nếu tạo ra không chỉ định kích thước, giá trị mặc định là Integer.MAX_VALUE. Điểm khác biệt của hàng đợi chặn này là nó là một hàng đợi hai đầu.
Dưới đây là so sánh giữa LinkedBlockingQueue và LinkedBlockingDeque:
3.7 DelayDeque
DelayDeque là hàng đợi chặn không giới hạn thực thi giao diện Delay, chỉ khi đối tượng dữ liệu đạt đến thời gian chờ mới có thể chèn vào hàng đợi để lưu trữ. Nếu tất cả dữ liệu hiện tại đều chưa đạt đến thời gian chờ được chỉ định khi tạo, thì hàng đợi không có phần tử đầu, và luồng thông qua các phương thức như poll để lấy phần tử dữ liệu sẽ trả về null. Thời gian chờ của đối tượng dữ liệu được xác định qua phương thức getDelay(TimeUnit.NANOSECONDS) của giao diện Delayed, nếu phương thức này trả về giá trị nhỏ hơn hoặc bằng 0 thì nghĩa là thời gian chờ của phần tử dữ liệu đã hết.
Tài liệu tham khảo: Container đồng thời - BlockingQueue