Tóm tắt: Viết mã đa luồng vừa làm việc tốt vừa bảo vệ được các ứng dụng
trước các lỗi là rất khó khăn — đó là lý do tại sao chúng ta có
java.util.concurrent. Ted Neward chỉ bạn thấy các
lớp của Các bộ sưu tập đồng thời như
CopyOnWriteArrayList,
BlockingQueue, và
ConcurrentMap bổ sung cho các lớp của Các bộ sưu
tập tiêu chuẩn để đáp ứng các yêu cầu lập trình đồng thời của bạn như thế nào.
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ế,
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 trong
java.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ĩa
CopyOnWriteArrayList 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.*;
import java.util.concurrent.*;
class Producer
implements Runnable
{
private BlockingQueue<String> drop;
List<String> messages = Arrays.asList(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you eat ivy too?");
public Producer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
for (String s : messages)
drop.put(s);
drop.put("DONE");
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
class Consumer
implements Runnable
{
private BlockingQueue<String> drop;
public Consumer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
String msg = null;
while (!((msg = drop.take()).equals("DONE")))
System.out.println(msg);
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
public class ABQApp
{
public static void main(String[] args)
{
BlockingQueue<String> drop = new ArrayBlockingQueue(1, true);
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
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ó).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 đầu get(), 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.
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.*;
import java.util.concurrent.*;
class Producer
implements Runnable
{
private BlockingQueue<String> drop;
List<String> messages = Arrays.asList(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you eat ivy too?");
public Producer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
for (String s : messages)
drop.put(s);
drop.put("DONE");
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
class Consumer
implements Runnable
{
private BlockingQueue<String> drop;
public Consumer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
String msg = null;
while (!((msg = drop.take()).equals("DONE")))
System.out.println(msg);
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
public class SynQApp
{
public static void main(String[] args)
{
BlockingQueue<String> drop = new SynchronousQueue<String>();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
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 |
Tài nguyên
Học tập
- "Lý thuyết và thực hành Java: Các lớp của Bộ sưu tập đồng
thời" (Brian Goetz, developerWorks, 07.2003): Tìm hiểu xem gói
util.concurrentcủa Doug Lea đem lại sức sống mới cho các kiểu sưu tập tiêu chuẩnListvàMapnhư thế nào. - Chương trình đồng thời Java trong thực hành (Brian Goetz, et. al. Addison-Wesley, 2006): Khả năng nổi bật của Brian để chắt lọc các khái niệm phức tạp với người đọc làm cho cuốn sách này trở thành một cuốn sách phải có trên giá sách của bất kỳ nhà phát triển Java nào
- "Thêm gia vị cho các bộ sưu tập với tổng quát và đồng thời" (John Zukowski, developerWorks, 04.2008): Giới thiệu các thay đổi trong Khung công tác các sưu tập Java trong Java 6.
- Gói java.util.concurrent, nền tảng Java SE 6: Tìm hiểu thêm về các lớp tiện ích được thảo luận trong bài viết này.
- Guarded Blocks: Thành ngữ phổ biến nhất cho các luồng phối hợp.
- "Giới thiệu về Khung công tác các bộ sưu tập" (MageLang Institute, Sun Developer Network, 1999): Bài hướng dẫn cũ nhưng tốt này là một bài giới thiệu đầy đủ về Khung công tác các bộ sưu tập Java trước khi có các bộ sưu tập đồng thời.
- "Khung công tác các bộ sưu tập": Đọc tài liệu của Khung công tác các sưu tập Java và API của Sun Microsystems.
- Thư viện đồng thời các phép kết nối: Microsoft® Research đưa ra thư viện này nhằm thực hiện khái niệm phép kết nối như một cơ chế đồng bộ; bài nghiên cứu có liên quan (dạng PDF) là một nguồn tốt để học tập về lý thuyết đằng sau các phép kết nối.
- Vùng công nghệ Java của developerWorks: Hàng trăm bài viết về mọi khía cạnh về lập trình Java.




0 comments:
Post a Comment