Các bộ sưu tập đồng thời là một bổ sung to lớn cho Java™ 5, nhưng nhiều nhà phát triển Java đã không thấy chúng vì tất cả những om sòm về chú giải (annotations) và tổng quát (generics). Ngoài ra (và có lẽ trung thực hơn), nhiều nhà phát triển tránh gói này vì họ cho rằng nó, giống như những vấn đề mà nó cố gắng giải quyết, phải rất phức tạp.
Trong thực tế,
1. TimeUnit
Mặc dù thực chất nó không phải là một lớp của bộ sưu tập đồng thời, kiểu liệt kê
2. CopyOnWriteArrayList
Việc tạo một bản sao mới của một mảng là một hoạt động quá tốn kém, về cả chi phí thời gian lẫn chi phí bộ nhớ, khi xem xét để sử dụng thông thường; các nhà phát triển thường đành phải sử dụng một
Điều này làm cho cấu trúc chi phí không theo kịp với các tình huống ở nơi có rất nhiều người đọc đang đọc
Bộ sưu tập này sao chép nội bộ các nội dung của nó vào một mảng mới khi có bất kỳ sự thay đổi nào, do đó những người đọc đang truy cập vào các nội dung của mảng không phải chịu chi phí đồng bộ hóa (bởi vì họ sẽ không bao giờ hoạt động trên dữ liệu có thể thay đổi).
Về cơ bản,
3. BlockingQueue
Giao diện
Bất chấp sự thật là mã trong bài hướng dẫn Guarded Blocks làm việc được, nhưng nó dài, lộn xộn, và không hoàn toàn trực quan. Đúng là quay lại những ngày đầu của nền tảng Java, các nhà phát triển Java đã phải bối rối với mã như vậy, nhưng bây giờ là năm 2010 — chắc chắn mọi thứ đã được cải thiện rồi phải không?
Liệt kê 1 cho thấy một phiên bản viết lại của mã nguồn Guarded Blocks, ở đây tôi đã sử dụng một
Liệt kê 1. BlockingQueue
Nhân tiện, bạn hoàn toán đúng nếu đã nhận thấy rằng Guarded Blocks chứa một lỗi rất lớn — điều gì sẽ xảy ra nếu một nhà phát triển đã đồng bộ hóa trên cá thể
4. ConcurrentMap
Khi một
Nếu hai luồng gọi một phương thức chính xác tại cùng thời điểm, mỗi luồng sẽ kiểm tra và sau đó mỗi luồng sẽ đặt giá trị, làm mất đi giá trị của luồng đầu tiên trong quá trình này. May mắn thay, giao diện
Về đầu trang
5. SynchronousQueues
Về cơ bản,
Liệt kê 2. SynchronousQueue
Mã thực hiện trông gần như giống nhau, nhưng ứng dụng này có một lợi ích gia tăng, trong đó
Trong thực tế,
Kết luận
Tại sao phải phấn đấu để đưa thêm hoạt động đồng thời vào các lớp trong Các bộ sưu tập của bạn khi thư viện thời gian chạy Java cung cấp các thứ tương đương dựng sẵn, dễ sử dụng? Bài viết tiếp theo trong loạt bài này khám phá sâu hơn về vùng tên
Tải về
Trong thực tế,
java.util.concurrent
chứa nhiều lớp giải quyết có hiệu quả các vấn đề đồng thời phổ biến, mà không đòi hỏi bạn phải toát mồ hôi. Hãy đọc để tìm hiểu xem các lớp trongjava.util.concurrent
như CopyOnWriteArrayList
vàBlockingQueue
sẽ giúp bạn giải quyết những thách thức nguy hiểm của lập trình đa luồng như thế nào.1. TimeUnit
Mặc dù thực chất nó không phải là một lớp của bộ sưu tập đồng thời, kiểu liệt kê
java.util.concurrent.TimeUnit
làm cho mã dễ đọc hơn rất nhiều. Việc sử dụng TimeUnit
(Đơn vi thời gian) giải phóng các nhà phát triển khỏi gánh nặng về mili giây khi sử dụng phương thức hoặc API của bạn.TimeUnit
kết hợp tất cả các đơn vị thời gian, bắt đầu từ MILLISECONDS
và MICROSECONDS
lên đến DAYS
và HOURS
, có nghĩa là nó xử lý hầu như tất cả các kiểu khoảng thời gian mà một nhà phát triển có thể cần đến. Và, nhờ các phương thức chuyển đổi đã khai báo cho enum (kiểu liệt kê) này, thậm chí chuyển đổi HOURS
sang MILLISECONDS
là rất dễ dàng khi thời gian gấp gáp.2. CopyOnWriteArrayList
Việc tạo một bản sao mới của một mảng là một hoạt động quá tốn kém, về cả chi phí thời gian lẫn chi phí bộ nhớ, khi xem xét để sử dụng thông thường; các nhà phát triển thường đành phải sử dụng một
ArrayList
có đồng bộ để thay thế. Tuy nhiên, đó cũng là một tùy chọn tốn kém, vì mỗi khi bạn lặp duyệt qua các nội dung của bộ sưu tập, bạn phải đồng bộ hóa tất cả các hoạt động, bao gồm cả việc đọc và viết, để đảm bảo tính nhất quán.Điều này làm cho cấu trúc chi phí không theo kịp với các tình huống ở nơi có rất nhiều người đọc đang đọc
ArrayList
trừ một vài người đang sửa đổi nó.CopyOnWriteArrayList
là viên ngọc nhỏ tuyệt vời để giải quyết vấn đề này. Javadoc của nó định nghĩaCopyOnWriteArrayList
như một "biến thể an toàn-luồng của ArrayList
trong đó tất cả các hoạt động đột biến (thêm, thiết lập, v.v..) được thực hiện bằng cách tạo một bản sao mới của mảng".Bộ sưu tập này sao chép nội bộ các nội dung của nó vào một mảng mới khi có bất kỳ sự thay đổi nào, do đó những người đọc đang truy cập vào các nội dung của mảng không phải chịu chi phí đồng bộ hóa (bởi vì họ sẽ không bao giờ hoạt động trên dữ liệu có thể thay đổi).
Về cơ bản,
CopyOnWriteArrayList
là lý tưởng cho kịch bản chính xác ở nơi mà ArrayList
của chúng ta thất bại, đó là các bộ sưu tập thường được đọc nhiều, hiếm khi viết, chẳng hạn như các Listener
(trình nghe) của một sự kiện JavaBean.3. BlockingQueue
Giao diện
BlockingQueue
nói rằng nó là một Queue
(hàng đợi), có nghĩa là các mục của nó được lưu trữ theo thứ tự vào trước, ra trước (FIFO). Các mục được chèn vào theo một thứ tự cụ thể được lấy ra theo cùng thứ tự đó — nhưng với sự đảm bảo thêm là bất kỳ nỗ lực nào để lấy ra một mục từ một hàng đợi rỗng sẽ chặn luồng đang gọi cho đến khi mục này trở nên sẵn sàng để được lấy ra. Tương tự như vậy, bất kỳ sự cố gắng nào để chèn một mục vào trong một hàng đợi đã đầy sẽ chặn luồng đang gọi cho đến khi có sẵn chỗ để lưu trữ vào hàng đợi.BlockingQueue
giải quyết gọn vấn đề làm thế nào để "chuyển vùng" các mục được thu thập bởi một luồng, đưa sang luồng khác để xử lý, mà không phải quan tâm chi tiết đến các vấn đề đồng bộ hóa. Theo vết Guarded Blocks (Các khối được bảo vệ) trong Hướng dẫn Java là một ví dụ tốt. Nó xây dựng một bộ đệm một khe cắm đơn có giới hạn bằng cách sử dụng đồng bộ hóa thủ công và các phương thức wait()
/notifyAll()
để báo hiệu giữa các luồng khi một mục mới có sẵn để dùng, và khi khe cắm đã sẵn sàng để được điền bằng một mục mới. (Xem Công cụ Guarded Blocks để biết thêm chi tiết).Bất chấp sự thật là mã trong bài hướng dẫn Guarded Blocks làm việc được, nhưng nó dài, lộn xộn, và không hoàn toàn trực quan. Đúng là quay lại những ngày đầu của nền tảng Java, các nhà phát triển Java đã phải bối rối với mã như vậy, nhưng bây giờ là năm 2010 — chắc chắn mọi thứ đã được cải thiện rồi phải không?
Liệt kê 1 cho thấy một phiên bản viết lại của mã nguồn Guarded Blocks, ở đây tôi đã sử dụng một
ArrayBlockingQueue
thay cho Drop
được viết bằng tay.Liệt kê 1. BlockingQueue
import java.util.*; |
ArrayBlockingQueue
cũng thể hiện "sự công bằng" — có nghĩa là nó có thể mang lại cho các luồng đọc và các luồng viết quyền truy cập vào trước, ra trước. Một cách thay thế có thể là một chính sách hiệu quả hơn nhưng có nguy cơ bỏ đói một số luồng. (Nghĩa là, sẽ hiệu quả hơn khi cho phép những luồng đọc được chạy trong khi những luồng đọc khác nắm giữ khóa, nhưng bạn có nguy cơ là một dòng cố định các luồng đọc chặn giữ luồng viết không bao giờ làm được công việc của nó).Theo dõi lỗi!
Nhân tiện, bạn hoàn toán đúng nếu đã nhận thấy rằng Guarded Blocks chứa một lỗi rất lớn — điều gì sẽ xảy ra nếu một nhà phát triển đã đồng bộ hóa trên cá thể
Drop
bên trong main()
?BlockingQueue
cũng hỗ trợ các phương thức để lấy ra một tham số thời gian, chỉ báo luồng này nên bị chặn bao lâu trước khi trả về tín hiệu thất bại không được chèn hoặc lấy ra các mục theo yêu cầu. Làm việc này tránh chờ đợi vô thời hạn, có thể kết liễu một hệ thống sản xuất, vì biết rằng một sự chờ đợi vô thời hạn có thể quá dễ dàng biến thành việc treo hệ thống, đòi hỏi phải khởi động lại.4. ConcurrentMap
Map
chứa đựng một lỗi xảy ra đồng thời khó thấy, dễ làm một nhà phát triển Java không cảnh giác lạc đường. ConcurrentMap
là giải pháp dễ dàng.Khi một
Map
được truy cập từ nhiều luồng, thường phổ biến là sử dụng hoặc containsKey()
hoặc get()
để tìm hiểu xem một từ khóa (key) đã cho có mặt hay không trước khi lưu trữ cặp từ khóa/giá trị. Nhưng ngay cả với một Map
, đã đồng bộ hóa, một luồng có thể lẻn vào trong quá trình này và nắm quyền điều khiển Map
. Vấn đề là khóa đồng thời (lock) được nhận lúc bắt đầuget()
, rồi được giải phóng trước khi khóa đồng thời này có thể được nhận lại, trong cuộc gọi đến put()
. Kết quả là một điều kiện chạy đua: đó là một cuộc chạy đua giữa hai luồng, và kết quả sẽ khác nhau tùy vào ai sẽ chạy đầu tiên.Nếu hai luồng gọi một phương thức chính xác tại cùng thời điểm, mỗi luồng sẽ kiểm tra và sau đó mỗi luồng sẽ đặt giá trị, làm mất đi giá trị của luồng đầu tiên trong quá trình này. May mắn thay, giao diện
ConcurrentMap
hỗ trợ một số phương thức bổ sung được thiết kế để làm hai việc dưới một khóa đồng thời duy nhất, ví dụ: putIfAbsent()
, đầu tiên kiểm tra từ khóa đã có mặt chưa, sau đó chỉ đặt nếu từ khóa (key) này còn chưa được lưu trữ trong Map
.Về đầu trang
5. SynchronousQueues
SynchronousQueue
(hàng đợi đồng bộ) là một tạo vật thú vị, theo Javadoc:Một hàng đợi có chặn trong đó mỗi hoạt động chèn phải chờ một hoạt động gỡ bỏ tương ứng bởi một luồng khác, và ngược lại. Một hàng đợi đồng bộ không có bất kỳ dung lượng bên trong nào, thậm chí ngay cả dung lượng là một.
Về cơ bản,
SynchronousQueue
là một việc triển khai thực hiện khác của BlockingQueue
nói trên. Nó cung cấp cho chúng ta một cách rất gọn nhẹ để trao đổi các phần tử đơn lẻ từ một luồng này sang luồng khác khác, khi sử dụng ngữ nghĩa có chặn màArrayBlockingQueue
sử dụng. Trong Liệt kê 2, tôi đã viết lại mã từ Liệt kê 1 bằng cách sử dụng SynchronousQueue
thay thế cho ArrayBlockingQueue
:Liệt kê 2. SynchronousQueue
import java.util.*; |
Mã thực hiện trông gần như giống nhau, nhưng ứng dụng này có một lợi ích gia tăng, trong đó
SynchronousQueue
sẽ cho phép chèn vào hàng đợi chỉ khi có một luồng đang chờ để dùng nó.Trong thực tế,
SynchronousQueue
là tương tự như "các kênh hẹn gặp” có sẵn trong các ngôn ngữ như Ada hoặc CSP. Đôi khi chúng được biết đến như là "các kết nối" trong các môi trường khác, bao gồm .NET (xem Tài nguyên).Kết luận
Tại sao phải phấn đấu để đưa thêm hoạt động đồng thời vào các lớp trong Các bộ sưu tập của bạn khi thư viện thời gian chạy Java cung cấp các thứ tương đương dựng sẵn, dễ sử dụng? Bài viết tiếp theo trong loạt bài này khám phá sâu hơn về vùng tên
java.util.concurrent
.Tải về
Mô tả | Tên | Kích thước | Phương thức tải |
---|---|---|---|
Sample code for this article | j-5things4-src.zip | 23KB | HTTP |
Đăng nhận xét