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

Lập trình đồng thời có nghĩa là làm việc thông minh hơn, chứ không phải vất vả hơn

Ngoài Các bộ sưu tập thân thiện đồng thời, java.util.concurrent đã đưa vào thêm các thành phần dựng sẵn khác có thể trợ giúp bạn trong việc điều chỉnh và thi hành các luồng trong các ứng dụng đa luồng. Ted Neward giới thiệu thêm năm điều phải biết về lập trình Java™ từ gói java.util.concurrent mà ông lựa chọn.

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



06 03 2012

Concurrent Collections (Các bộ sưu tập đồng thời) làm cho việc lập trình đồng thời dễ dàng hơn bằng cách cung cấp các cấu trúc dữ liệu được xây dựng tốt, an toàn luồng. Tuy nhiên, trong một số trường hợp, các nhà phát triển cần phải tiến một bước xa hơn, và suy nghĩ về điều chỉnh và/hoặc điều tiết thực hiện luồng. Vì toàn bộ mục tiêu của java.util.concurrent là để làm đơn giản hóa việc lập trình đa luồng, bạn có thể hy vọng gói này sẽ bao gồm các tiện ích đồng bộ hóa — và đúng là nó có.

Bài viết này, tiếp theo Phần 1, giới thiệu một số ý tưởng đồng bộ hóa ở mức cao hơn so với các kiểu dựng sẵn (các trình theo dõi) của ngôn ngữ cốt lõi nhưng không cao đến mức bị chôn vùi bên trong một lớp Collection. Việc sử dụng các khóa và các cổng này khá đơn giản một khi bạn biết chúng dùng làm gì.

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.

1. Semaphore (Xê ma pho)

Trong một số hệ thống doanh nghiệp, không phải là hiếm trường hợp khi các nhà phát triển cần phải điều tiết số lượng các yêu cầu đang mở (các luồng/các hành động) đối với một tài nguyên cụ thể — thực vậy, đôi khi việc điều tiết có thể cải thiện khả năng thông qua của một hệ thống bằng cách giảm số lượng tranh chấp giành tài nguyên cụ thể đó. Mặc dù chắc chắn có thể thử viết mã điều khiển bằng tay, thì việc sử dụng lớp semaphore còn dễ dàng hơn, do lớp này lo việc điều tiết cho bạn, như thể hiện trong Liệt kê 1:

Liệt kê 1. Sử dụng Semaphore để điều tiết
import java.util.*;import java.util.concurrent.*;

public class SemApp
{
    public static void main(String[] args)
    {
        Runnable limitedCall = new Runnable() {
            final Random rand = new Random();
            final Semaphore available = new Semaphore(3);
            int count = 0;
            public void run()
            {
                int time = rand.nextInt(15);
                int num = count++;
                
                try
                {
                    available.acquire();
                    
                    System.out.println("Executing " + 
                        "long-running action for " + 
                        time + " seconds... #" + num);
                
                    Thread.sleep(time * 1000);

                    System.out.println("Done with #" + 
                        num + "!");

                    available.release();
                }
                catch (InterruptedException intEx)
                {
                    intEx.printStackTrace();
                }
            }
        };
        
        for (int i=0; i<10; i++)
            new Thread(limitedCall).start();
    }
}

Mặc dù 10 luồng trong ví dụ này đang chạy (bạn có thể xác minh bằng cách thi hành lệnh jstack dựa vào tiến trình Java đang chạy SemApp), nhưng chỉ có ba luồng đang hoạt động. Bảy luồng khác được giữ trong ngăn cho đến khi một trong các bộ đếm của semaphore được giải phóng. (Trên thực tế, lớp Semaphore hỗ trợ việc nhận và giải phóng nhiều hơn một giấy phép (permit) tại một thời điểm, nhưng điều đó sẽ không có ý nghĩa trong kịch bản này).


2. CountDownLatch

Nếu Semaphore là lớp đồng thời được thiết kế để cho phép các luồng "vào", mỗi cái một lần (có lẽ nó gợi nhớ đến những tay vệ sỹ gác ở các câu lạc bộ đêm nổi tiếng), thì CountDownLatch là cổng xuất phát của một cuộc đua ngựa. Lớp này nắm giữ tất cả các luồng ở trong ngăn của nó cho đến khi một điều kiện cụ thể được đáp ứng, lúc đó nó giải phóng tất cả cùng một lúc.

Liệt kê 2. CountDownLatch: Hãy vào cuộc đua!
import java.util.*;
import java.util.concurrent.*;

class Race
{
    private Random rand = new Random();
    
    private int distance = rand.nextInt(250);
    private CountDownLatch start;
    private CountDownLatch finish;
    
    private List<String> horses = new ArrayList<String>();
    
    public Race(String... names)
    {
        this.horses.addAll(Arrays.asList(names));
    }
    
    public void run()
        throws InterruptedException
    {
        System.out.println("And the horses are stepping up to the gate...");
        final CountDownLatch start = new CountDownLatch(1);
        final CountDownLatch finish = new CountDownLatch(horses.size());
        final List<String> places = 
            Collections.synchronizedList(new ArrayList<String>());
        
        for (final String h : horses)
        {
            new Thread(new Runnable() {
                public void run() {
                    try
                    {
                        System.out.println(h + 
                            " stepping up to the gate...");
                        start.await();
                        
                        int traveled = 0;
                        while (traveled < distance)
                        {
                            // In a 0-2 second period of time....
                            Thread.sleep(rand.nextInt(3) * 1000);
                            
                            // ... a horse travels 0-14 lengths
                            traveled += rand.nextInt(15);
                            System.out.println(h + 
                                " advanced to " + traveled + "!");
                        }
                        finish.countDown();
                        System.out.println(h + 
                            " crossed the finish!");
                        places.add(h);
                    }
                    catch (InterruptedException intEx)
                    {
                        System.out.println("ABORTING RACE!!!");
                        intEx.printStackTrace();
                    }
                }
            }).start();
        }

        System.out.println("And... they're off!");
        start.countDown();        

        finish.await();
        System.out.println("And we have our winners!");
        System.out.println(places.get(0) + " took the gold...");
        System.out.println(places.get(1) + " got the silver...");
        System.out.println("and " + places.get(2) + " took home the bronze.");
    }
}

public class CDLApp
{
    public static void main(String[] args)
        throws InterruptedException, java.io.IOException
    {
        System.out.println("Prepping...");
        
        Race r = new Race(
            "Beverly Takes a Bath",
            "RockerHorse",
            "Phineas",
            "Ferb",
            "Tin Cup",
            "I'm Faster Than a Monkey",
            "Glue Factory Reject"
            );
        
        System.out.println("It's a race of " + r.getDistance() + " lengths");
        
        System.out.println("Press Enter to run the race....");
        System.in.read();
        
        r.run();
    }
}

Lưu ý trong Liệt kê 2CountDownLatch dùng cho hai mục đích: Đầu tiên nó giải phóng tất cả các luồng đồng thời, mô phỏng sự bắt đầu của cuộc đua, nhưng sau đó một chốt khóa khác mô phỏng việc kết thúc cuộc đua, về cơ bản để cho luồng "main" có thể in ra các kết quả. Đối với một cuộc chạy đua có tường thuật nhiều hơn, bạn có thể thêm các CountDownLatch tại các điểm là "chỗ rẽ" và "nửa đường" của cuộc đua, khi những con ngựa vượt qua các giá trị một phần tư, một nửa, và ba phần tư quãng đường.


3. Executor

Các ví dụ trong Liệt kê 1Liệt kê 2 đều có một thiếu sót khá bực mình, đó là chúng buộc bạn phải trực tiếp tạo các đối tượng Thread (luồng). Đây là một cách làm mang lại rắc rối vì trong một số các JVM, việc tạo ra một Thread là một hoạt động nặng, và sử dụng lại các Thread hiện có tốt hơn rất nhiều so với tạo ra những luồng mới. Tuy nhiên, trong các JVM khác, hoàn toàn ngược lại: các Thread khá nhẹ, và tạo mới một Thread mỗi khi bạn cần nó lại tốt hơn rất nhiều. Tất nhiên, nếu Murphy đúng (mà ông thường là đúng), bất cứ cách tiếp cận nào mà bạn sử dụng sẽ chính là một cách làm sai đối với nền tảng mà cuối cùng bạn sẽ triển khai lên. (N.D: Nguyên văn luật Murphy như sau: “Anything that can go wrong, will go wrong" - Bất cứ điều gì mà có khả năng tồi tệ thì thế nào cũng sẽ thành tồi tệ).

Nhóm chuyên gia JSR-166 (xem Tài nguyên) đã biết trước tình huống này, ở một mức độ nào đó. Thay vì bắt các nhà phát triển Java tạo các Thread trực tiếp, họ đã đưa vào giao diện Executor một sự trừu tượng hóa để tạo các luồng mới. Như trong Liệt kê 3, Executor cho phép bạn tạo các luồng mà không phải tự mình gọi phương thức new để tạo mới đối tượng Thread:

Liệt kê 3. Executor
Executor exec = getAnExecutorFromSomeplace();
exec.execute(new Runnable() { ... });

Nhược điểm chính khi sử dụng Executor cũng chính là nhược điểm mà chúng ta gặp phải với tất cả các lớp nhà máy (factory): nhà máy phải đến từ nơi nào đó. Thật không may, không giống như CLR, JVM không phân phối kèm theo một nhóm luồng bao trùm-VM tiêu chuẩn.

Lớp Executorđã dùng làm một chỗ chung để nhận được các cá thể thực hiện-Executor, mà nó chỉ có các phương thức new (ví dụ, để tạo ra một nhóm luồng mới); nó đã không tạo sẵn các cá thể. Vì vậy, bạn tự chịu trách nhiệm nếu bạn muốn tạo ra và sử dụng các cá thể Executor trong mã của bạn. (Hoặc, trong một số trường hợp, bạn sẽ có thể sử dụng một cá thể cung cấp bởi thùng chứa/nền tảng ưa thích của bạn).

ExecutorService, đến phục vụ bạn

Dù có ích vì chẳng phải lo lắng về các Thread đến từ đâu, giao diện Executor còn thiếu một số chức năng mà một nhà phát triển Java có thể mong đợi, chẳng hạn như khả năng để bắt đầu một luồng được thiết kế để tạo ra một kết quả và chờ đợi theo kiểu không chặn ai cho đến khi kết quả đó trở nên có sẵn. (Đây là một nhu cầu phổ biến trong các ứng dụng máy tính để bàn, ở đây người dùng sẽ thực hiện một hoạt động giao diện người dùng đòi hỏi phải truy cập vào một cơ sở dữ liệu, và có thể muốn hủy bỏ hoạt động này trước khi nó hoàn thành nếu mất quá nhiều thời gian).

Về việc này, các chuyên gia JSR-166 đã tạo ra một sự trừu tượng hóa có ích hơn nhiều, đó là giao diện ExecutorService mô hình hóa nhà máy khởi đầu luồng như là một dịch vụ có thể được điều khiển chung. Ví dụ, thay vì gọi execute() một lần cho mỗi tác vụ ExecutorService có thể nhận một bộ sưu tập các tác vụ và trả về mộ List of Futures diễn tả các kết quả tương lai của mỗi tác vụ đó.


4. ScheduledExecutorServices

Dù giao diện ExecutorService là tuyệt vời, một số tác vụ nhất định cần phải được thực hiện theo kiểu có lịch trình, chẳng hạn như việc thi hành một tác vụ đã cho trong những khoảng thời gian xác định hoặc tại một thời điểm cụ thể. Đây là phạm vi hoạt động của ScheduledExecutorService, mở rộng ExecutorService.

Nếu mục tiêu của bạn là tạo ra một lệnh "heartbeat" (nhịp tim) để "ping" năm giây một lần, thì ScheduledExecutorService sẽ làm cho nó đơn giản như những gì bạn thấy trong Liệt kê 4:

Liệt kê 4. ScheduledExecutorService 'ping' theo lịch trình
import java.util.concurrent.*;

public class Ping
{
    public static void main(String[] args)
    {
        ScheduledExecutorService ses =
            Executors.newScheduledThreadPool(1);
        Runnable pinger = new Runnable() {
            public void run() {
                System.out.println("PING!");
            }
        };
        ses.scheduleAtFixedRate(pinger, 5, 5, TimeUnit.SECONDS);
    }
}

Bạn thấy thế nào? Không phiền phức với luồng, cũng không phiền phức với những gì phải làm nếu người dùng muốn hủy bỏ nhịp tim đó, không phải đặt luồng chạy ở nền trước hay nền sau một cách chi tiết; chỉ cần để lại tất cả những chi tiết lịch trình đó cho ScheduledExecutorService.

Bỗng nhiên, nếu người dùng muốn hủy bỏ nhịp tim, kết quả trả về từ cuộc gọi scheduleAtFixedRate sẽ là một cá thể ScheduledFuture, không chỉ bao bọc kết quả nếu có một kết quả, mà còn có một phương thức cancel (hủy bỏ) để tắt hoạt động theo lịch trình đó.


5. Các phương thức thời gian chờ

Khả năng để đặt một thời gian chờ cụ thể xung quanh các hoạt động có chặn (và do đó tránh các khóa chết) là một trong những tiến bộ quan trọng của thư viện java.util.concurrent so với các những người anh em đồng thời cũ hơn của nó, chẳng hạn như các trình theo dõi để khóa.

Những phương thức này hầu như luôn luôn được nạp chồng thêm một cặp int/TimeUnit, chỉ báo cho phương thức đó nên đợi bao lâu trước khi bỏ cuộc và trả điều khiển về cho chương trình. Nó đòi hỏi nhà phát triển phải làm việc nhiều hơn — bạn sẽ hồi phục như thế nào nếu không nhận được khóa? — Nhưng các kết quả hầu như luôn luôn chính xác hơn: ít các khóa chết hơn và nhiều mã an toàn sản xuất hơn. (Để biết thêm về cách viết mã sẵn sàng-sản xuất, hãy xem Release It! của Michael Nygard trong phần Tài nguyên.)


Kết luận

Gói java.util.concurrent chứa nhiều tiện ích tốt hơn là mở rộng vượt ra ngoài Các bộ sưu tập, đặc biệt là trong các gói .locks.atomic. Hãy đi sâu vào và bạn cũng sẽ tìm thấy các cấu trúc điều khiển có ích như CyclicBarrier và nhiều hơn nữa.

Cũng giống như nhiều khía cạnh của nền tảng Java, bạn không cần phải rất vất vả để có mã cơ sở hạ tầng có thể rất có ích. Bất cứ khi nào bạn đang viết mã đa luồng, hãy nhớ về các tiện ích được thảo luận trong bài viết này và bài viết trước.

Thời gian tới, chúng tôi sẽ chuyển sang một chủ đề mới: năm điều bạn chưa biết về các Jar.


Tải về

Mô tảTênKích thước
Sample code for this article5things5-src.zip10KB

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=792499
ArticleTitle=5 điều bạn chưa biết về ... java.util.concurrent, Phần 2
publish-date=03062012