2/27/2014
2:31:00 PM 0

Java 8 雙冒號 Method Reference

Java 8 加入了 lambda expression 賦予了 Java 新的能力, 在使用 lambda 的同時常發現神奇的 :: 雙冒號 (double colon) 穿插其中, :: 代表什麼意義呢?

舉例來說, 在過去我們要做個由小到大的數列排序,當然使用 java.util.Arrays.sort(Object[] a) 是最快的方式, 如果要自訂排序方式使用 anonymous inner class 是最簡便的方式

例 1 : 使用 anonymous inner class
Integer[] ary = new Integer[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 };

// anonymous inner class
Arrays.sort(ary, new Comparator() {
    @Override
    public int compare(Integer a, Integer b) {
        return Integer.compare(a, b);
    }
});

System.out.println(Arrays.toString(ary));
Java 8 之後你可以這樣寫

例 2 : 使用 lambda expression
Integer[] ary = new Integer[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 };

// lambda expression
Arrays.sort(ary, (a, b) -> Integer.compare(a, b));

System.out.println(Arrays.toString(ary));
是不是簡潔了很多呢? 但是我們發現 (a, b) -> Integer.compare(a, b)), 傳入兩個 Integer parameter 再 return Integer 不就跟 Integer.compare 一樣嗎? (a, b) -> 感覺好像是多餘的,因此有更精簡的做法, 就是使用 lambda expression 的 method reference

例 3 : 使用 method reference
Integer[] ary = new Integer[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 };

// method reference
Arrays.sort(ary, Integer::compare);

System.out.println(Arrays.toString(ary));

:: 雙冒號 (double colon) 是 Java 8 加入的新 operator, 稱作 Method Reference, Method Reference 提供式四個種類的用法

1. Reference to a static method
前面例3 Arrays.sort(ary, Integer::compare) 就屬此類, Integer.compare 是 static method

2. Reference to an instance method of a particular object
使用 instance method, 傳入參數與回傳型別必須與 interface 的 compare method 定義一致
class MyCompare {
    public int compareInt(int a, int b) {
        return a - b;
    }
}

Integer[] ary = new Integer[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 };

MyCompare my = new MyCompare();
Arrays.sort(ary, my::compareInt);

System.out.println(Arrays.toString(ary));
3. Reference to an instance method of an arbitrary object of a particular type
使用 instance method, 與前例比較原本是傳入兩個參數 (int a, int b) 回傳一個 int, 轉換為 a.compareTo(b) 的形式, 同樣都回傳 int
Integer[] ary = new Integer[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 };

Arrays.sort(ary, Integer::compareTo);

System.out.println(Arrays.toString(ary));
4. Reference to a Constructor
Reference to a Constructor 相較其他三個比較複雜些,用下面的例子來說明,首先創建 ICarFactory interface 和 CarBean class
interface ICarFactory {
    CarBean getCar(String color, int year);
}

class CarBean {
    private String name;
    private int maxSpeed;

    CarBean(String name, int maxSpeed) {
        this.name = name;
        this.maxSpeed = maxSpeed;
    }

    String getName() {
        return name;
    }

    int getMaxSpeed() {
        return maxSpeed;
    }
}
要實作 ICarFactory, 傳統的方法是使用 "implements" 關鍵字來實作介面

Java 8 可使用 lambda expression, 實作可改寫成如下的方式,簡潔許多
ICarFactory f = (String name, int maxSpeed) -> { return new CarBean(name, maxSpeed); };
仔細觀察可發現,Interface 所定義的 getCar(String color, int year) 和 CarBean(String name, int maxSpeed) 傳入參數型別正好一樣,因此可使用 method reference => Reference to a Constructor 的方式,再把寫法簡化
ICarFactory f = CarBean::new;
測試 Reference to a Constructor
//ICarFactory f = (String name, int maxSpeed) -> { return new CarBean(name, maxSpeed); };
ICarFactory f = CarBean::new;
CarBean c = f.getCar("GTR", 300);
System.out.println("Car Name:" + c.getName() + " / Max Speed:" + c.getMaxSpeed());
使用 lambda 或 method reference 可有效的讓程式碼更簡短,也提供程式更大的彈性,不管用以上哪一種寫法都是由編譯器來做推斷和解釋完成,這是使用上需要留意的部分
2/21/2014

Race Condition

Race Condition 在多執行緒的開發中是一個重要的概念,當兩個 thread 同一時間點存取同一個共用變數,因為同步執行造成運算結果相互覆蓋的情形就是 race condition

wikipedia 上有一個很棒的表格,非常清楚的說明 race condition 的概念
以下表格引用自 wikipedia : http://en.wikipedia.org/wiki/Race_condition

狀況一 : Thread 1 & Thread 2 依序執行, 得到一個正確的運算結果
Thread 1 Thread 2 Integer value
0
read value 0
increase value 0
write back 1
read value 1
increase value 1
write back 2

狀況二 : Thread 1 & Thread 2 同時執行, Thread 2 的運算結果覆蓋掉 Thread 1 的運算結果, 最終得到一個錯誤的值
Thread 1 Thread 2 Integer value
0
read value 0
read value 0
increase value 0
increase value 0
write back 1
write back 1

因此在多執行緒的程式中使用 lock 來避免產生 race condition

在 Java 中使用 lock 的兩個主要作用
  1. mutex : 建立 critical section, 避免 race condition
  2. visibility : 維持記憶體一致(memory consistency)
2/20/2014
4:48:00 PM 0

Java Volatile

在 Java 多執行緒的環境中 synchronized 是相當常用的關鍵字之一,使用 synchronized 有兩個效果 mutex & visibility
mutex 主要目的是建立 critical section, 而 visibility 的作用是保持記憶體的一致性(memory consistency)

什麼是記憶體一致性呢? 我們知道一般物件是擺在記憶體內的,如果要對物件資料做運算,CPU 通常會將要做運算的資料複製一份到 CPU 的 register 加快處理速度,而運算完的資料有時並不會立刻寫回主記憶體,而是留在 register 內,在多執行緒的環境下,如果此時另一個 CPU 來讀取同一份資料就會產生記憶體不一致的問題,因此 Java 定義了 volatile 關鍵字來處理記憶體資料一致性的問題

Java Language Specification 對 volatile 的定義

Java Specification 裡只定義規範, 也就是使用 volatile 之後可保證所有的 threads 看到變數的值是一致的,並沒有硬性規定如何實作,給了實作 JVM 彈性,因此實作的方式可能有很多種,譬如說如果使用 volatile 關鍵字的變數就不可對變數使用 cache memory,直接對主記憶體資料作讀寫,這也是一種方式,當然實際實作可能沒那麼簡單

引用 Java Concurrency in Practice 的範例
public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
這個例子沒有使用 synchronized,有可能會因為記憶體不一致而造成無法預期的結果, 變數 ready 可能一直是 false, 也可能印出 0 的值,但在我的 PC 上怎麼測都測不出印出 0 的結果,正確的寫法是使用 synchronized 或使用 volatile 關鍵字, 將 ready, number 都標示為 volatile

至於在什麼狀況下可使用 volatile 呢? 可參考這篇文章 Java theory and practice: Managing volatility
  1. status flags
  2. one-time safe publication
  3. independent observations
  4. volatile bean
  5. The cheap read-write lock trick
使用 volatile 可獲得比使用鎖更高的執行效率,例如在讀取動作遠遠大於寫入動作的狀況下,就非常適合使用 volatile,但如果對於 volatile 用法不是很熟悉,使用鎖是比較不容易出錯的方法

參考 : http://www.ibm.com/developerworks/library/j-jtp06197
2/14/2014
1:31:00 PM 0

Mutex

Mutex 是 mutual exclusion 的縮寫, 中文譯為 "互斥",在多執行緒環境下,防止兩條以上的執行緒對一共用資源做同時存取的動作,一般做法是透過程式代碼 lock 的方式, 建立 critical section, 達到 mutex 的目的

下圖簡單表示 mutual exclusion 的意義
在 Java 1.4 可使用 synchronized 關鍵字來達到 mutex 的目的,但使用 synchronized 有些功能不易做到,例如
  1. 如果發現 lock 已被佔用,就只能等待,無法去處理其他的工作
  2. 在等待獲取鎖的狀況下,無法被 interrupt
  3. 自訂取得鎖的規則(公平鎖,非公平鎖)
Java 1.5 後新增 java.util.concurrent.locks package 補強 synchronized 的不足之處
  • ReentrantLock中文翻譯為"重入鎖",就字面上的意義代表當某個 thread 獲取某個鎖後,在未釋放鎖的情況下,可以獲得同一把鎖多次,因為 ReentrantLock 內部存在著一個計數器,計算取得鎖的次數,如果取得 lock 兩次,就必須 unlock 兩次才可真正的釋放鎖,這樣的設計可方便使用遞迴的方式呼叫(recursive call) lock()

    基本使用方式
    final Lock lock = new ReentrantLock();
    lock.lock();
    try { 
      // do something ...
    }
    finally {
      lock.unlock(); 
    }
    
    遞迴呼叫
    private final ReentrantLock lock = new ReentrantLock();
    
    public void execute() {
        try {
            lock.lock();//This lock supports a maximum of 2147483647 recursive locks by the same thread.
    
            System.out.println("lock|HoldCount=" + lock.getHoldCount());
    
            if (lock.getHoldCount() < 10)
                execute();//recursive locking by a single thread
    
        } finally {
            lock.unlock();
            System.out.println("unlock|HoldCount=" + lock.getHoldCount()
                + "|isHeldByCurrentThread=" + lock.isHeldByCurrentThread());
        }
    }
    
    從 Java 的 API Specification 我們可以看到 ReentrantLock 的 constructor 在不傳值的狀況下的狀況下,預設是個非公平鎖,如果我們要讓獲得鎖公平,就必需維持 thread 的排隊順序,這需要額外開銷,對執行效率是不利的,雖然獲取鎖的原則是不公平的,但 JVM 保證所有 thread 最終會獲得它們等待的鎖,避免 thread starvation

    ReentrantLock 有 lock, tryLock, lockInterruptibly 幾個獲取 lock 的 method
    • lock() : 如果拿不到 lock 就一直等待,直到拿到 lock 為止
    • tryLock() : 如果拿到鎖就 return true, 否則就 return false
    • tryLock(long timeout, TimeUnit unit) : 如果拿不到 lock, 則等待一段時間
    • lockInterruptibly() : 如果拿不到 lock 就一直等待, 在等待的過程中 thread 可以被 interrupted
    public void testLock() throws Exception {
        private final Lock lock = new ReentrantLock();
        lock.lock();
    
        Thread t = new Thread(new Runnable() {
    
            public void run() {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " acquires the lock");
            }
        });
    
        t.start();
        Thread.sleep(3000);
        t.interrupt();//無作用,因 thread 並非在 wait,join,sleep 狀態
        Thread.sleep(Long.MAX_VALUE);
    }
    
    public void testLockInterruptibly() throws Exception {
        private final Lock lock = new ReentrantLock();
        lock.lock();
    
        Thread t = new Thread(new Runnable() {
    
            public void run() {
                try {
                    lock.lockInterruptibly();
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + " interrupted.");
                }
            }
        });
    
        t.start();
        Thread.sleep(3000);
        t.interrupt();//thread 在等待獲得 lock 的過程中被中斷
        Thread.sleep(Long.MAX_VALUE);
    }
    
  • ReentrantReadWriteLock 讀寫鎖內部維護一對鎖, 一把用於讀取的操作, 另一把用於寫入操作, 讀取鎖可同時被多個讀取者擁有, 而此時鎖所保護的資料處於唯讀狀態, 讀寫鎖是互斥的, 因此當沒有任何讀取或寫入鎖定時, 才可以取得寫入鎖定, 但是對於讀取來說沒有互斥性,可參考 Java Doc 對於 Interface ReadWriteLock 的說明 , 另外使用 ReentrantReadWriteLock 時, 在讀取 thread 很多, 寫入 thread 很少的狀況下, 可能會使得寫入 thread 一直處於等待狀態, 因此必需留意 thread starvation 的問題
2/12/2014
2:18:00 PM 0

Semaphore

提供平行運算環境中,控制多個 thread 存取共享資源的一種策略模式,semaphore 是一個計數器的概念,用於保護共享的資源

譬如說有多個 thread 想要對同一塊記憶體變數進行讀寫時,在讀寫這塊記憶體之前,先獲得一個保護變數的 semaphore,當獲得這個 semaphore 時,其他人都不能對這塊記憶體進行讀寫,直到 semaphore 持有者釋放 semaphore 為止,在這種狀況下 semaphore 的值只有二進位的 0 或 1,稱為 binary semaphores,當受保護的資源數量大於 1 的任意數量時,稱作 counting semaphores,semaphore 常用於實作 publisher subscriber model & resource pool 中

Java Semaphore example
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class TestSemaphore {
    public static void main(String[] args) {

        final int CONSUMER_COUNT = 20;
        final int RESOURCE_COUNT = 10;
        final ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(RESOURCE_COUNT);

        for (int i = 0; i < CONSUMER_COUNT; i++) {   
            final int INDEX = i;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        if (!semaphore.tryAcquire(2, TimeUnit.SECONDS))
                        {
                            throw new RuntimeException("timeout");
                        }

                        System.out.println("Acquire:" + INDEX + " / AvailablePermits:" + semaphore.availablePermits());

                        Thread.sleep(1000);
 
                        System.out.println("Release:" + INDEX + " / AvailablePermits:" + semaphore.availablePermits());
                        semaphore.release();
                   } 
                   catch (InterruptedException e) {
                   }
               }
           };

           exec.execute(run);
        }

        exec.shutdown();
    }
}