5 điều bạn chưa biết về ... java.util.concurrent, Phần 1

Lập trình đa luồng với Các bộ sưu tập đồng thời

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.

Ted Neward, Giám đốc, Neward & Associates

Ted Neward là một nhà tư vấn cho ThoughtWorks, một văn phòng tư vấn toàn cầu và là giám đốc của Neward & Associates, nơi ông tư vấn, khuyên bảo, dạy và trình bày về các dịch vụ Java, .NET, XML và các nền tảng khác. Ông cư trú gần Seattle, Washington



14 12 2011

Giới thiệu về loạt bài này

Vậy, bạn nghĩ rằng bạn biết về lập trình Java phải không? Thực tế là, hầu hết các nhà phát triển mới chỉ xới qua bề mặt của nền tảng Java, tìm hiểu vừa đủ để làm xong việc. Trong loạt bài này, Ted Neward đi sâu vào các chức năng cốt lõi của nền tảng Java để khám phá ra sự thật ít được biết đến là nó có thể giúp bạn giải quyết ngay cả những thách thức lập trình khó khăn nhất.

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ư CopyOnWriteArrayListBlockingQueue 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ừ MILLISECONDSMICROSECONDS lên đến DAYSHOURS, 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ó).

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 đầ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ênKích thước
Sample code for this articlej-5things4-src.zip23KB

Tài nguyên

Học tập

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=776133
ArticleTitle=5 điều bạn chưa biết về ... java.util.concurrent, Phần 1
publish-date=12142011