Cấp bậc tác giả:

TRAINING

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

Được viết bởi QuangIT ngày 13/08/2012 lúc 08:30 PM
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.
  • 0
  • 9882

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

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ư CopyOnWriteArrayListBlockingQueue, 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 sangMILLISECONDS 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 đọcArrayList 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ộtArrayBlockingQueue 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ướcPhương thức tải
Sample code for this articlej-5things4-src.zip23KBHTTP


Tài nguyên
Học tập

Nguồn bài viết: IBM

BÌNH LUẬN BÀI VIẾT

Bài viết mới nhất

LIKE BOX

Bài viết được xem nhiều nhất

HỌC HTML