當 CPU 進入多核時代之後,並行編程將更加流行,但是編寫並行程序更容易 出錯。在開發過程中,工程師能注意到同一個程序在單線程運行時是正確的,但 是在多線程時,它會有可能出錯。和並行相關的錯誤的產生原因通常都非常隱晦 ,而且在一次測試中,它們的出現與否具有很強的隨機性。由於程序中多個線程 之間可能以任意的方式交錯執行,即使一個並行程序正確的運行了成百上千次, 下一次運行仍然可能出現新的錯誤。
Multi-Thread Run-time Analysis Tool 是由 IBM 為多線程 Java 程序開發 的運行時分析工具,它可用於分析並查找 Java 代碼中的一些不容易發現的潛在 並行程序錯誤,比如數據競爭 (Data Race) 和死鎖 (Deadlock),從而提高並行 程序的代碼質量。本文將介紹檢測 Java 程序中隨機並行錯誤的一種新工具 (http://alphaworks.ibm.com/tech/mtrat),檢查 Java 代碼中的潛在的並行程 序錯誤,從而提高代碼的安全性和穩定性,並演示其對於潛在而並未發生的錯誤 的發掘能力。
概述
Java 編程語言為編寫多線程應用程序提供強大的語言支持。但是,編寫有用 的、沒有錯誤的多線程程序仍然比較困難。編程語言中線程面臨很多挑戰。在這 些挑戰中,最主要的就是編程復雜度的提高。這些編程復雜度是由同步共享變量 的訪問,潛在的依賴於時序的錯誤和調試和優化並行程序的復雜性造成的。
MTRAT 只所以把不同的技術集成到了一個單一的開發工具中,是為了掩蓋工 具內部的復雜性,並使得 MTRAT 方便使用。 MTRAT 主要由以下部分組成,
簡單的命令行界面和 Eclipse 插件。輸出 MTRAT 檢查到的並行錯誤。
動態的 Java 字節碼修改引擎。可以在 Java 類文件被 Java 虛擬機加載的 時候,修改 Java 類。
程序運行時信息收集器。收集程序的動態信息,比如內存訪問,線程同步, 創建和結束。
高效的運行時分析引擎。收集到的運行時信息會被在線分析,如果發現潛在 的並行錯誤,將會通過界面報告給用戶。
檢測數據競爭
在並行程序中,當兩個並行的線程,在沒有任何約束的情況下,訪問一個共 享變量或者共享對象的一個域,而且至少要有一個操作是寫操作,就會發生數據 競爭錯誤。MTRAT 最強大的功能就是發現並行程序中潛在的數據競爭錯誤。在下 邊的 Java 程序就隱藏了一個潛在的數據競爭錯誤。
package sample;
class Value
{
private int x;
public Value()
{
x = 0;
}
public synchronized void add (Value v)
{
x = x + v.get();
}
public int get() {return x;}
}
class Task extends Thread
{
Value v1, v2;
public Task (Value v1, Value v2)
{
this.v1 = v1;
this.v2 = v2;
}
public void run() {v1.add(v2);}
}
public class DataRace
{
public static void main (String[] args) throws InterruptedException
{
Value v1 = new Value ();
Value v2 = new Value ();
Thread t1 = new Task(v1, v2);
Thread t2 = new Task (v2, v1);
t1.start();
t2.start();
t1.join();
t2.join();
}
}
類Value聲明一個整形域x,一個同步方法add修改這個域,和一個方法get返 回域x的值。類Task以兩個類Value的實例來構造。
以 MTRAT 運行類sample.DataRace,可以在運行時刻檢查程序中的潛在的數 據競爭錯誤
$ mtrat -cp . sample.DataRace
Data Race 1 : 3 : sample/Value : x
Thread Thread-3 id: 7 : WRITE
sample.Value : get : 15
sample.Value : add : 15
sample.Task : run : 32
Thread Thread-4 id: 8 : READ
sample.Value : get : 18
sample.Value : add : 15
sample.Task : run : 32
Data Race 2 : 4 : sample/Value : x
Thread Thread-3 id: 7 : READ
sample.Value : get : 15
sample.Value : add : 15
sample.Task : run : 32
Thread Thread-4 id: 8 : WRITE
sample.Value : get : 15
sample.Value : add : 15
sample.Task : run : 32
在圖形界面Eclipse中運行,得到以下結果:
Figure 1.
MTRAT 報告出了兩個數據競爭錯誤,因為類Task的兩個實例會訪問類Value的 對象,然而這個共享的對象卻沒有被一個共同的鎖保護。
例如,在並行程序執行過程中,可能存在這樣的時刻,一個線程執行方法get 讀域x的值,而另外一個線程執行執行方法add寫域x。
根據檢查結果,MTRAT 發現了兩個數據競爭錯誤,在類sample/Value域x。程 序員在得到這兩個數據競爭錯誤後,很容易就能發現程序中存在兩個線程並發訪 問同一個對象域的可能。如果兩個線程可以順序訪問這個對象域,這兩個數據競 爭問題就可以被消除了。
檢測死鎖
死鎖問題也是並行 Java 程序中常見的問題。在 Java 程序中出現死鎖,是 因為 synchronized 關鍵字會造成運行的線程等待關聯到某個一個對象上的鎖。 由於線程可能已經獲得了別的鎖,兩個線程就有可能等待對方釋放掉鎖。在這種 情況下,兩個線程將永遠等待下去。
在下邊的 Java 程序就隱藏了一個潛在死鎖問題,
class T3 extends Thread
{
StringBuffer L1;
StringBuffer L2;
public T3(StringBuffer L1, StringBuffer L2)
{
this.L1 = L1;
this.L2 = L2;
}
public void run()
{
synchronized (L1)
{
synchronized (L2)
{
}
}
}
}
public class Deadlock
{
void harness2() throws InterruptedException
{
StringBuffer L1 = new StringBuffer("L1");
StringBuffer L2 = new StringBuffer("L2");
Thread t1 = new T3(L1, L2);
Thread t2 = new T3(L2, L1);
t1.start();
t2.start();
t1.join();
t2.join();
}
public static void main(String[] args) throws InterruptedException
{
Deadlock dlt = new Deadlock();
dlt.harness2();
}
}
在類 Deadlock 的 harness2 方法中,類 Deadlock 的兩個實例被創建,作 為參數傳遞到類 T3 的構造函數中。在類 T3 的 run 方法中,線程會依次獲得 這個兩個對象的鎖,然後以相反的順序釋放這兩個鎖。由於兩個 StringBuffer 實例以不同的順序傳遞給類 T3,兩個線程會以不同的順序獲得這兩個鎖。這樣 ,死鎖就出現了。
以 MTRAT 運行類 sample.Deadlock,可以在運行時刻檢查程序中的潛在的死 鎖錯誤:
$ mtrat -Dcom.ibm.mtrat.deadlock=true -cp . sample.Deadlock
Thread 7 : Acquire L1 L2
Dead Lock 1
Thread 7, acquired lock1 -> try lock2 sample/T3 line 109
Thread 8, acquired lock2 -> try lock1 sample/T3 line 109
Thread 8 : Acquire L2 L1
在圖形界面Eclipse中運行,得到以下結果:
圖 2.
在 MTRAT 的死鎖檢查報告中我們可以發現,線程 Thread 7 已經獲得了鎖 lock1,在程序 109 行試圖獲得鎖 lock2。然而,線程 Thread 8 已經獲得了鎖 lock2,在程序 109 行試圖獲得鎖lock1。
根據 MTRAT 的死鎖檢查報告,程序員可以很容易得知道,這個死鎖問題是由 於兩個線程按照相反的順序上鎖造成的。避免這種問題的一種方法是讓代碼按固 定的全局順序獲取鎖。那麼如果兩個線程按照一致的順序去上鎖,死鎖錯誤就可 以被消除了。
void harness2() throws InterruptedException
{
StringBuffer L1 = new StringBuffer("L1");
StringBuffer L2 = new StringBuffer("L2");
Thread t1 = new T3(L1, L2);
Thread t2 = new T3(L1, L2);
t1.start();
t2.start();
t1.join();
t2.join();
}
結束語
在本文中,我們展示了如何檢查並行 Java 程序中潛在的錯誤,比如數據競 爭和死鎖。通過使用 MTRAT,您可以在程序開發階段發現用肉眼難以發現的並行 程序錯誤。該工具使開發正確和高質量的並行程序變得更加容易。