Google Code Prettify

2015年8月16日 星期日

java.util.concurrent - ExecutorService

Java 1.x 時 Java 可以說只是個寫 applet 的語言,Java 1.2 之後成為寫 web 後端程式最重要的語言,Java 1.4 引入 NIO 讓讀寫更有效率,Java 5 最大的改變應該就是增加 java.util.concurrent 這個 package,對平行處理 (多執行緒) 提供完整的支援,這個 package 的內容一直到 Java 8 都持續增加,使其更加完善。這一篇要介紹的是 ExecutorService,類別圖如下: (如往例,這不是完整的類別圖,只是這篇會說明到的部份。)
ExecutorService 是個介面,繼承了 Executor 介面,Executor 介面只定義了一個稱為 execute() 的 method,實作 Runnable 的類別物件,可以將本身委派給實作 ExecutorService 的類別物件產生另一個執行緒,進行平行運算。
接下來看看範例程式;
 1 package idv.steven.concurrency;
 2 
 3 import java.util.concurrent.ExecutorService;
 4 import java.util.concurrent.Executors;
 5 
 6 public class ExecutorDemo {
 7 
 8     public static void main(String[] args) {
 9         ExecutorService executorService = Executors.newSingleThreadExecutor();
10 
11         executorService.execute(new Runnable() {
12             public void run() {
13                 System.out.println("Asynchronous task");
14             }
15         });
16 
17         executorService.shutdown();
18     }
19 }
程式說明:
  • Executors 是一個工廠類別,定義了許多 static 的 method,用來產生與 java.util.cuncurrent 套件中相關的類別,這裡 (line 9) 用來產生一個 ExecutorService 的物件。
  • 在第 11~15 行中,有一個匿名的 Runnable 物件,當然,如果程式很大,就不要用匿名的寫法,這裡因為只是一行 output,用匿名比較簡單。這個物件傳入 executorService 物件中會產生一個新的 thread。
  • executorService 執行完要自行關閉,即第 17 行呼叫 shutdown(),呼叫 shutdown() 後,就不能再傳別的 Runnable 給這個 executorService 物件了,否則會產生 exception。 
  • 第 9 行也可以改寫成 ExecutorService executorService = Executors.newFixedThreadPool(10); 一般來說,會使用 thread pool 的狀況是,程式會有很多 thread,且這些 thread 的執行時間很短,在這種狀況下要系統一直建立新的 thread 顯然會花很多時間,所以使用 thread pool。另外,任何一個系統也不能無限制的建立 thread,否則各個 thread 間頻繁的 context switch 反而會拖垮系統效能,所以會給它一個最大值。
  • ExecutorService 中的 shutdown() 和 shutdownNow() 這兩個 method 有什麼差別? shutdown() 會執行完已委派的 Runnable 物件後才將 ExecutorService 關閉,shutdownNow 則會立刻關閉,尚未被執行的 Runnable 物件則以 List<Runnable> 傳回。




上面的程式我們要來改寫一下,注意看類別圖,ExecutorService 中有兩個 submit() method,一個傳入的參數是  Runnable 物件,另一個傳入的是 Callable 物件,使用 submit 也可以產生一個新的執行緒,先看一下傳入 Runnable 物件的程式。
 1 package idv.steven.concurrency;
 2 
 3 import java.util.concurrent.ExecutorService;
 4 import java.util.concurrent.Executors;
 5 import java.util.concurrent.Future;
 6 
 7 public class ExecutorDemo {
 8 
 9     public static void main(String[] args) {
10         ExecutorService executorService = Executors.newSingleThreadExecutor();
11 
12         Future future = executorService.submit(new Runnable() {
13             public void run() {
14                 System.out.println("Asynchronous task");
15             }
16         });
17 
18         executorService.shutdown();
19     }
20 }
修改的部份是第 12~ 16 行,得到的結果與第一個程式是完全一樣的! 再來看一下傳入 Callable 物件的程式。
 1 package idv.steven.concurrency;
 2 
 3 import java.util.concurrent.Callable;
 4 import java.util.concurrent.ExecutionException;
 5 import java.util.concurrent.ExecutorService;
 6 import java.util.concurrent.Executors;
 7 import java.util.concurrent.Future;
 8 
 9 public class ExecutorDemo {
10 
11     public static void main(String[] args) throws InterruptedException, ExecutionException {
12         ExecutorService executorService = Executors.newSingleThreadExecutor();
13 
14         Future future = executorService.submit(new Callable(){
15             public Object call() throws Exception {
16                 System.out.println("Asynchronous Callable");
17                 return "Callable Result";
18             }
19         });
20 
21         System.out.println("future.get() = " + future.get());
22 
23         executorService.shutdown();
24     }
25 }
傳入 Runnable 物件和傳入 Callable 物件最大的差別就在於,傳入 Callable 物件可以有傳回值,所以當 21 行呼叫 future.get() 時,可以取得 call() 的傳回值 (line 17),如果是傳入 Runnable 物件,呼叫 get() method 會傳回 null。

2015年8月15日 星期六

java.util.concurrent - Future & Callable

在 Java 5 之前,寫多執行緒程式,唯一的辦法就是使用 Thread 類別及 Runnable 介面,Java 5 之後新增加 java.util.concurrent 這個 package,提供了豐富的類別、介面,在這之後 Java 的多執行緒程式設計才真的趨於完整,這篇先介紹 Callable 和 Future,類別圖如下。

在看程式之前,先對上圖說明如下:
  • Callable 介面只定義了一個 call() method,實作這個介面的好處是當執行緒執行結束後,可以傳回值,且值的型別可以由我們自定。 
  • FutureTask 並非 Future 唯一實作的類別,之後還會介紹別的實作 Future 介面的類別,在這篇我先介紹 FutureTask。
接下來看範例程式,這個程式會算出費式數列,傳入的參數是指出程式要列出幾個值,例如傳入 10,則列出 0 1 1 2 3 5 8 13 21 34 共 10 個值。
 1 package idv.steven.concurrency;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import java.util.concurrent.Callable;
 6 import java.util.concurrent.ExecutionException;
 7 import java.util.concurrent.FutureTask;
 8 
 9 public class FibonacciDemo implements Callable<List<Long>> {
10     private long number;
11     
12     public FibonacciDemo(long number) {
13         this.number = number;
14     }
15 
16     @Override
17     public List<Long> call() throws Exception {
18         List<Long> fib = new ArrayList<Long>();
19         fib.add(0L);
20         fib.add(1L);
21         for(int i=2; i<number; i++) {
22             Long f = fib.get(fib.size()-1) + fib.get(fib.size()-2);
23             fib.add(f);
24         }
25         
26         return fib;
27     }
28 
29     public static void main(String[] args) {
30         Callable<List<Long>> fibonacci = new FibonacciDemo(10);
31         FutureTask<List<Long>> fibonacciTask = new FutureTask<List<Long>>(fibonacci);
32         
33         Thread t = new Thread(fibonacciTask);
34         t.start();
35         
36         try {
37             t.sleep(10);
38             
39             boolean canceled = fibonacciTask.cancel(false);
40             System.out.println("canceled = " + canceled);
41             
42             if (!fibonacciTask.isCancelled()) {
43                 List<Long> fib = fibonacciTask.get();
44                 for(Long f:fib) {
45                     System.out.print(f + " ");
46                 }
47             }
48             
49 //            if (fibonacciTask.isDone()) {
50 //                List<Long> fib = fibonacciTask.get();
51 //                for(Long f:fib) {
52 //                    System.out.print(f + " ");
53 //                }
54 //            }
55 //            else {
56 //                System.out.println("unfinished");
57 //            }
58         }
59         catch (InterruptedException | ExecutionException  e) {
60             e.printStackTrace();
61         }
62     }
63 }




程式說明如下:

  • FutureTask 有兩個建構式,一個接受實作 Callable 的類別,另一個接受實作 Runnable 的類別,這個程式實作了 Callable (line 9),並指定傳回的值為 List<Long>,也就是所有計算所得的數字。大多數的人會選擇實作 Callable,因為實作 Runnable 的話,還要傳入一個變數,用來儲存傳回值。
  • FutureTask 類別是一個實作 Runnable 介面的類別,要建立執行緒,仍需透過 Thread 類別,所以可以看到第 31 行傳入我們實作 Callable 的類別物件給 FutureTask 後,為了建立一個執行緒,第 33 行再將 FutureTask 類別的物件傳給 Thread,然後在第 34 行啟動一個新的執行緒。
  • 第 17~27 行實作 call() method,計算費式數列後,傳回給主程式。
  • 第 39~47 行及第 49~57 行是不同兩個版本,執行出來的結果是一樣的。
  • 要如何取得 call() method 傳回的值呢? 使用 FutureTask 的 get() method ! 就算是實作 Runnable 介面,也是透過 get() 取得傳回值。(line 43、50)
  • 先說明第一個版本,第 39 行是什麼意思呢? cancel() method 是試著強制中斷 FutureTask 的執行緒! 傳入的參數 true 表示,不管這個執行緒處於什麼狀態,都將它中斷結束,如果傳入的是 false,則是當執行緒已經進入 call() method 且還沒執行完離開 call() method 則不要中斷,否則都中斷。至於傳回值即是 true 表示程式沒有執行完就被中斷,false 則是有執行完。這也是為什麼會有第 37 行睡了 10 毫秒的原因,如果主程式不略微停頓一下,讓 FutureTask 的那個執行緒計算一下費式數列,第 39 行有可能傳回 true,也就是程式被中斷了!
  • 第 42 行判斷是否有被中斷,如果沒有,就是有順利的計算完傳回值,所以第 43 行呼叫 get() 取得傳回值,接下來的 for 迴圈當然就是印出結果。
  • 再來說明第二個版本,同樣的要保留第 37 行,讓費式數列有時間被計算,第 49 行和第 42 行剛好相反,它會判斷執行緒是否有被執行完? 有的話就傳回 true,沒有就傳回 false。傳回 true 的話,第 50 行取得計算結果然後印出來。
Callable 這種會傳回值的架構,在有一個計算很耗時,還蠻好用的,可以先產生一個 Thread 去計算這個值,然後做點別的事,之後再回來取得計算結果。當然,即使用只用 Thread、Runnable 也可以做到同樣的功能,只是會比較麻煩,現在 Java 直接提供相關的 API。

2015年8月13日 星期四

java.util.concurrent.locks - 臨界區間的讀寫

java 在推出時,提供 synchronized、notify、wait 的簡單方式,讓多執行緒程式可以控制臨界區間的存取,到了 Java 5 之後加入  java.util.concurrent 這個 package,又提供了一些 interface (介面) 及 class (類別),這些新的 interface、class 比之前的方法更有彈性、更有效率,這裡簡要的說明 java.util.concurrent.locks 下的主要類別與介面。下圖的 class diagram (類別圖) 僅是主要的 interface、class 及其包含的主要 method,要了解全貌,請參考 Java Documentation。

  • ReadWriteLock

ReadWriteLock lock = new ReentrantReadWriteLock();
//...
lock.writeLock().lock(); //取得寫入鎖定
try {
    ...
}
finally {
    lock.writeLock().unlock(); //解除寫入鎖定
}
上面的程式稱不上是一個範例,主要說明使用 ReadWriteLock 的優缺點及注意事項:
  1. 跟 synchronized 比起來,解除了只能針一個 method 或一個 block 設定臨界區間的限制,但是要注意一定要記得 unlock,否則會防礙到其他執行緒進入臨界區間,所以上面的程式,將 unlock() 放在 finally 以確保其最後一定會被執行。
  2. 請回頭看一下類別圖中的 ReadWriteLock,它提供的是有 read lock 和 write lock,這可以改善 synchronized 的效率,synchronized 只要有一個 thread 搶到了 lock,就得等它執行完臨界區間內所有程式,並離開後,其它等著進入臨界區間的 thread 才能進入。現在分出了 read lock 及 write lock,當取得 write lock 的 thread 進入了臨界區間,其它 thread 也只能等待,但是,如果是取得 read lock 的 thread 進入臨界區間,是可以有多個 read lock thread 同時進入,這就改善了效率,又不會讓資料不一致。 
  • Lock
ReentrantLock lock = new ReentrantLock();

try {
    if (lock.tryLock()) {
        //臨界區間 - do something
    }
}
finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
interface Lock 只有一個實作類別 ReetrantLock,這裡要特別注意 tryLock() 這個 method,它相當有趣,因為當遇到臨界區間時,使用這個 method,如果取得 lock 就傳 true,沒有取得就傳回 false,程式可以依傳回值決定做什麼事,以免有 thread 呆呆的一直等著進臨界區間而浪費時間。
要記得,unlock 前也要先用 isHeldByCurrentThread() 這個 method 判斷一下目前是否有取得 lock,有的話才 unlock。
  • Condition
 前面提到的 lock 是可用來取代 synchronized 的新解法,Condition 的 signal、await 則是與 notify、wait 相當。使用的方法可以參考官網 Condition 的範例如下: 
 1  class BoundedBuffer {
 2    final Lock lock = new ReentrantLock();
 3    final Condition notFull  = lock.newCondition(); 
 4    final Condition notEmpty = lock.newCondition(); 
 5 
 6    final Object[] items = new Object[100];
 7    int putptr, takeptr, count;
 8 
 9    public void put(Object x) throws InterruptedException {
10      lock.lock();
11      try {
12        while (count == items.length)
13          notFull.await();
14        items[putptr] = x;
15        if (++putptr == items.length) putptr = 0;
16        ++count;
17        notEmpty.signal();
18      } finally {
19        lock.unlock();
20      }
21    }
22 
23    public Object take() throws InterruptedException {
24      lock.lock();
25      try {
26        while (count == 0)
27          notEmpty.await();
28        Object x = items[takeptr];
29        if (++takeptr == items.length) takeptr = 0;
30        --count;
31        notFull.signal();
32        return x;
33      } finally {
34        lock.unlock();
35      }
36    }
37  }
這是一個很簡單的"生產者"、"消費者"的例子,當陣列中沒有任何物件時,消費者 (take) 就需等待 (line 27),當陣列滿了時,生產者 (put) 也需等待 (line 13); 如果有新的物件產生生產者會通知消費者 (line 17),如果陣列還有空間,消費者也會通知生產者 (line 31)。
特別注意一下 2~4 行,Condition 是由 lock 的 newCondition() 產生,還有,就如 notify、wait 必需位於 synchronized 區間內一樣,Condition 的 await、signal 也要位於 lock、unlock 之間。




  • StampedLock
前面提到的 ReadWriteLock 可允許多個 readLock 的執行緒同時進入臨界區間,但只允許一個 writeLock 的執行緒進入臨界區間,且當有 writeLock 的執行緒位於臨界區間內,即不允許其它執行緒取得 readLock、writeLock,這會有個問題,當程式有很多讀取的執行緒,只有很少的寫入執行緒,臨界區間大部份時間被取得 readLock  執行緒佔據,寫入的執行緒會很難取得 writeLock 而長期處於等待狀態。為了解決這個問題,Java 8 提供了 StampedLock 這個新類別。
 1 public class BankAccountStampedLock {
 2   private final StampedLock sl = new StampedLock();
 3   private long balance;
 4 
 5   public BankAccountStampedLock(long balance) {
 6     this.balance = balance;
 7   }
 8 
 9   public void deposit(long amount) {
10     long stamp = sl.writeLock();
11     try {
12       balance += amount;
13     } finally {
14       sl.unlockWrite(stamp);
15     }
16   }
17 
18   public void withdraw(long amount) {
19     long stamp = sl.writeLock();
20     try {
21       balance -= amount;
22     } finally {
23       sl.unlockWrite(stamp);
24     }
25   }
26 
27   public long getBalance() {
28     long stamp = sl.readLock();
29     try {
30       return balance;
31     } finally {
32       sl.unlockRead(stamp);
33     }
34   }
35 
36   public long getBalanceOptimisticRead() {
37     long stamp = sl.tryOptimisticRead();
38     long balance = this.balance;
39     if (!sl.validate(stamp)) {
40       stamp = sl.readLock();
41       try {
42         balance = this.balance;
43       } finally {
44         sl.unlockRead(stamp);
45       }
46     }
47     return balance;
48   }
49 }
這是一個存、提款的範例程式,來源是 javaspecialists 網站。這個程式和完全只使用 ReadWriteLock 最大差別在於 getBalanceOptimisticRead(),第 37 行先呼叫 tryOptimisticRead 取得一個樂觀讀取鎖定,第 38 行取得存款餘額,第 39 行判斷看看是否在取得樂觀讀取鎖定後,有臨界區間有取得寫入鎖定的執行緒進入,沒有的話,直接回傳存款餘額 (line 47),萬一有的話,balance 的值有可能已經被改變,所以就要謹慎的取得讀取鎖定 (line 40),再取得真正最新的值後再解鎖 (line 44)。

2015年8月1日 星期六

NIO.2: TCP 網路程式設計 (non-blocking)

上一篇說明了 blocking 程式,這一篇要說明如何寫 non-blocking 程式,首先看一下兩者的差別:
  • blocking 模式一個 thread 只能有一個 channel,所以,server 端要服務多個 client 的話,就要為每個 client 建立一個 thread。
  • non-blocking 模式引進一個新的類別 - Selector,這個類別的用處在於,讓 non-blocking 成為事件驅動的運作模式。在 blocking 模式下,都是由程式本身決定何時連線與讀寫,這造成的限制就是當要同時有多個連線同時運作時,就要有多個 thread; Selector 會從 ServerSocketChannel 中選擇一個目前需要服務的 channel 進行服務,傳給程式 channel 及這個 channel 需要什麼類型的服務。
了解了 non-blocking 的運作方式後,要先說明另一個類別 - SelectionKey,這個類別中定義了四個運作模式 (也可視為四種事件),也就是 Selector 要告訴程式,這個 channel 需要什麼服務,如下:
  • SelectionKey.OP_ACCEPT: 這事件通常是用在 server 端,當 client 連線到 server 時,產生這個運作模式。
  • SelectionKey.OP_CONNECT: 這通常用在 client 端,當 server 端接受了 client 端的連線,client 端就會收到這個 event。
  • SelectionKey.OP_READ: 當有資料進來,可以讀的時候,會收到這個事件。
  • SelectionKey.OP_WRITE: 當收到這個事件,即可以寫出資料到該事件所關聯的 channel。
要看程式前,還是要先說明,non-blocking 是建立在 blocking 的基礎上,增加的這些類別,是為將運作方式轉換成事件驅動,由 ServerSocketChannel 傳訊息給 Selector,再由 Selector 傳給程式,了解這些觀念之後再來看下面的程式,應該就很容易了解了。
(感覺 non-blocking 有點像 IoC,把程式的主動權交出去,反而讓程式得到更多彈性。)




 1 package idv.steven.async;
  2 
  3 import java.io.IOException;
  4 import java.net.InetSocketAddress;
  5 import java.net.StandardSocketOptions;
  6 import java.nio.ByteBuffer;
  7 import java.nio.channels.SelectionKey;
  8 import java.nio.channels.Selector;
  9 import java.nio.channels.ServerSocketChannel;
 10 import java.nio.channels.SocketChannel;
 11 import java.util.Arrays;
 12 import java.util.Iterator;
 13 import java.util.Set;
 14 import java.util.concurrent.ExecutorService;
 15 import java.util.concurrent.Executors;
 16 
 17 public class EchoServer2 implements Runnable {
 18     public static int DEFAULT_PORT = 5555;
 19 
 20     @Override
 21     public void run() {
 22         ServerSocketChannel serverChannel;
 23         Selector selector;
 24         
 25         try {
 26             serverChannel = ServerSocketChannel.open();
 27             selector = Selector.open();
 28             
 29             if (serverChannel.isOpen() && selector.isOpen()) {
 30                 // 設定為 false 即表示要用 non-blocking 模式
 31                 serverChannel.configureBlocking(false);
 32                 
 33                 serverChannel.setOption(StandardSocketOptions.SO_RCVBUF, 256 * 1024);
 34                 serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
 35                 
 36                 serverChannel.bind(new InetSocketAddress(DEFAULT_PORT));
 37                 // 註冊 OP_ACCEPT 給 ServerSocketChannel,這樣 ServerSocketChannel 接到 client 端連線時,就會有一個值為 OP_ACCEPT 的 SelectionKey。
 39                 serverChannel.register(selector, SelectionKey.OP_ACCEPT);
 40                 
 41                 while (true) {
 42                     // 程式會在這裡停下,等待關心的"selection operation"出現。這個 method 有另一個版本,可以傳入 timeout 值,以免程式一直停在這裡。
 44                     selector.select();
 45                     
 46                     Set<SelectionKey> readyKeys = selector.selectedKeys();
 47                     Iterator<SelectionKey> iterator = readyKeys.iterator();
 48                     while (iterator.hasNext()) {
 49                         SelectionKey key = iterator.next();
 50                         // 移除將處理的 selection operation 很重要!
 51                         // 避免之後又重複接收到。
 52                         iterator.remove();
 53                         
 54                         try {
 55                             if (key.isAcceptable()) {
 56                                 // ServerSocketChannel 的用處就只是接受 client 連線,當收到 OP_ACCEPT 時,表示有 client 要連線,所以用 ServerSocketChannel 接收。
 58                                 ServerSocketChannel server = (ServerSocketChannel) key.channel();
 59                                 // 等待 client 連線
 60                                 SocketChannel client = server.accept();
 61                                 // 連線後設定這個連線為 non-blocking
 62                                 client.configureBlocking(false);
 63                                 // 這是一個 echo server,連線後緊接著要收 client 送來的訊息,所以註冊這個 socket channel 要關注 OP_READ。
 65                                 SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ);
 66                                 ByteBuffer buffer = ByteBuffer.allocate(1024);
 67                                 // non-blocking 模式有點像事件模式,每次都是待 selection operation 出現,程式才會接手做相關的事,在這不同的 selection operation 之間,要如何保留讀入的資料? 使用 attach() 及 attachment(),讓這些資料保存在 SelectionKey 中。 
 71                                 clientKey.attach(buffer);
 72                             }
 73                             else if (key.isReadable()) {
 74                                 SocketChannel client = (SocketChannel) key.channel();
 75 //在 OP_ACCEPT 階段,程式有預先產生一個 ByteBuffer,這時將它取出,用來儲存讀入的資料。
 77                                 ByteBuffer output = (ByteBuffer) key.attachment();
 78                                 client.read(output);
 79                                 System.out.println("recv: " + new String(Arrays.copyOfRange(output.array(), 0, output.limit())));
 80                                 // echo server 讀到資料後,當然接著要寫回給 client,所以註冊 OP_WRITE。
 81                                 SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE);
 82                             }
 83                             else if (key.isWritable()) {
 84                                 SocketChannel client = (SocketChannel) key.channel();
 85                                 // 在 OP_READ 階段讀取到的資料,現在將它取出。
 86                                 ByteBuffer output = (ByteBuffer) key.attachment();
 87                                 if (output != null) {
 88                                     output.flip();
 89                                     if (output.hasRemaining()) {
 90                                         // 把讀取到的資料寫回給 client。
 91                                         client.write(output);
 92                                         output.compact();
 93                                     }
 94                                     else {
 95                                         System.out.println("output has not remaining");
 96                                     }
 97                                 }
 98                             }
 99                         } catch (IOException e) {
100                             key.cancel();
101                             try {
102                                 key.channel().close();
103                             } catch (IOException ex) { }
104                         }
105                     }
106                 }
107             }
108         } catch (IOException e) {
109             e.printStackTrace();
110         }
111     }
112     
113     public static void main(String[] args) {
114         ExecutorService svc = Executors.newSingleThreadExecutor();
115         EchoServer2 echo = new EchoServer2();
116         svc.execute(echo);
117         svc.shutdown();
118     }
119 }
使用上一篇的 client 來測試,得到如下結果:

我開兩個視窗,編寫了批次程式,讓兩邊都有 client 程式一直送資料給 server,可以看到都能得到正確回應。