2007年4月24日 星期二

Synchronized造成效能低落嗎? 試試樂觀同步化吧!!

前陣子調效一個Web Application的效能,在貢獻了許多咖啡因到腦袋後,我找到了原因-- Synchronized

是的,在某支Servlet中的某函式使用了Synchronized關鍵字,造成系統在此排隊執行,因而效能低落,於是立刻招開小組會議,但幾經測試及討論後,卻發現此區塊一定得同步化,否則發生Race Condition時,會造成資料錯誤…

我效能調效的工作遇到了瓶頸…Orz

...
...


幾經思量,我決定將此函式改造成使用樂觀同步化技術!!

什麼是樂觀同步化?
使用Java寫作的程式,通常在可能會發生同時存取共用資料的情形時,使用Synchronized關鍵字,以確保同一時間只會有一個Thread執行這段程式,但這種作法就是所謂的悲觀同步化,問題解決了,但代價可能是會有多個Thread在排隊等候,造成效能低落。

樂觀同步化則樂觀的認為同時存取的情形很少發生,萬一發生的話,再補救就好了。

說明看起來很簡單,但使用較早期版本的JDK要實作可不簡單,幸運的是,JDK從1.5之後引進了Atomic類別庫,讓你可以方便的實作樂觀同步化。

Race Condition
在開始之前,先來看看什麼是「同時存取共用資料」,這個問題學名叫「Race Condition」,就是指共用的資料,幾乎同時間被許多不同的Thread存取,造成資料不一致的問題。

讓我們來看個例子:
有個定貨系統,使用了三個執行緒在處理同一個產品的訂單,訂單在產生之前必須先更新產品的資料。

產品物件的定義如下:




public class MyProduct {
private String productId;
private double price;
private String description;

public MyProduct(String productId, double price, String description) {
this.productId = productId;
this.price = price;
this.description = description;
}

public String toString() {
return productId +
"(NT$ " + price +
", " + description + ")";
}

public boolean equals(Object anObject) {
MyProduct newProduct = (MyProduct) anObject;
return productId.equals(newProduct.getProductId()) &&
price == newProduct.getPrice() &&
description.equals(newProduct.getDescription());
}

public void createOrderTO() {
// 用一段sleep來模擬產生訂單需要執行的時間
try {
long rnd = (long)(java.lang.Math.random() * 100);
Thread.sleep(rnd);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}

public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
}



處理訂單的執行緒程式如下:

public class ProductProcessor implements Runnable {

public ProductProcessor(int id) {
processorId = id;
}

private int processorId;

public int getProcessorId() {
return processorId;
}

private MyProduct product;
public void setProduct(MyProduct product) {
this.product = product;
}

/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
// 每個thread對此產品產生5個訂單
for (int i = 0; i < 5; i++) {
// 更新產品資料
product.setPrice(processorId * 10);
product.setDescription(
"modified by processor: " + processorId
);

// 跟據product產生訂單物件(Order Transfer Object)
product.createOrderTO();

// 列印訂單
System.out.println("Processor " + processorId +
" Create Order for: " + product.toString());
}

}

public static void main(String args[]) {
// 產生一個product
MyProduct prod = new MyProduct("my_product_01", 9999999, "this is my product!");

ProductProcessor p1 = new ProductProcessor(1);
ProductProcessor p2 = new ProductProcessor(2);
ProductProcessor p3 = new ProductProcessor(3);

// 讓三個thread都一起存取同一個Product
p1.setProduct(prod);
p2.setProduct(prod);
p3.setProduct(prod);

Thread t1 = new Thread(p1);
Thread t2 = new Thread(p2);
Thread t3 = new Thread(p3);

t1.start();
t2.start();
t3.start();
}


}


執行結果如下:


Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 2 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 3 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 1 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 3 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 2 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 3 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 2 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 3 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)



從結果可以發現許多衝突,例如第2行Processor 2產生的訂單,其說明卻註記說是Processor 3更新的。
這種衝突的情現就叫Race Condition。

悲觀同步化
要如何解決Race condition呢? 最簡單的方法就是使用Synchronized區塊了:
在有可能發生Race Condition的地方,使用Synchronize {}把這些程式碼都包起來,就可以解決問題,像下面這樣:

// use synchronized block to solve race condition
synchronized (product) {
// 更新產品資料
product.setPrice(processorId * 10);
product.setDescription(
"modified by processor: " + processorId
);

// 跟據product產生訂單物件(Order Transfer Object)
product.createOrderTO();

// 列印訂單
System.out.println("Processor " + processorId +
" Create Order for: " + product.toString());
}


再執行一次,我們可以得到完美的結果:


Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
...
...



這個技巧就是悲觀同步化,Java保證synchronized區塊同時只會有一個執行緒進入存取product物件,因此Race Condition不會再發生,但有時這種解法會造成效能低落。


樂觀同步化
在大多數情況下,其實Race Condition是很少發生的;當衝突很少發生,卻使用悲觀管控,就好像是「交通部長要求交通警察限制同一時間只能有一台車上高速公路一樣」,可以想見交流道口會塞的又臭又長。

所以樂觀同步化就派上用場了,基本原理很簡單:「如果發生衝突,就重做」!

JDK 1.5之後提供了AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference…等類別讓你可以不使用lock卻能解決Race Condition。

這些類別的特色是裡面的方法都是Atomic的,也就是所有的方法都可以視作單一的一種操作,不會受其它Thread的影響。例如:

AtomicInteger ival = new AtomicInteger(1);
ival.set(5); // 這個操作是Atomic的

// 如果ival==5的話就把ival的值改成10 (這個操作也是Atomic的)
ival.compareAndSet(5, 10);


(注:你可能覺得ival.set(5)和宣告成一般的int然後執行ival=5;有什麼不同,但其實用一般的int,其操作還是沒有Atomic,這遷涉到byte code的內容和register的載入方式,以後有機會再聊聊。)


使用AtomicReference
思考一下前述發生問題的程式,如果以下三個步驟:

// 更新產品資料
...
// 跟據product產生訂單物件(Order Transfer Object)
...
// 列印訂單
...


可以結合成一個Atomic的動作,那不就不需要再使用synchronized關鍵字了嗎?


問題是,要怎麼做呢? 我們可以使用AtomicReference將MyProduct包裝一下,像這樣:


import java.util.concurrent.atomic.AtomicReference;

public class MyAtomicProduct{
private AtomicReference product;

public MyAtomicProduct(String productId, double price, String description) {
product = new AtomicReference
( new MyProduct(productId, price, description) );
}

public void updateProductAndMakeOrder(int processorId,
double price,
String description) {
MyProduct origVal, newVal;
String origProductId;

// 這是一個無窮迴圈,會一直重做到沒有發生Race Condition為止
while (true) {
origVal = product.get();
origProductId = origVal.getProductId();

// 記住使用AtomicReference在設定值的時候一定要指派一個新的
newVal = new MyProduct(origProductId, price, description);

// 跟據product產生訂單物件(Order Transfer Object)
newVal.createOrderTO();

// compareAndSet會確保product的值和原來(origVal)一樣時
// ,才會將product的值更新,否則就不做事且傳回false
// (如果不一樣表示過程中有其他的thread進來,也就是Race Condition
// 當發生Race Condition時,迴圈就會重做)
if (product.compareAndSet(origVal, newVal)) {
System.out.println("Processor " + processorId +
" Create Order for: " + newVal.toString());
break; // 產生的訂單是正確的,所以結束迴圈
} else {
// Debug用,在此特別將發生Race Condition而重做的情形印出來
System.out.println("** [Redo] because of Race Condition! **");
}

}
}
}



然後把void main中產生product的那行改成如下

MyAtomicProduct prod = new MyAtomicProduct("my_product_01", 9999999, "this is my product!");



執行結果如下:


Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
** [Redo] because of Race Condition! **
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
** [Redo] because of Race Condition! **
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)



可以發現當Race Condition發生時,迴圈會重做以確保資料正確,在衝突不常發生的情形下,使用樂觀同步化是有可能解決效能低落的狀況的,不過如果Race Condition常常發生,則值不值得就見人見智了,更何況實作樂觀同步化會使你的系統更複雜了。

沒有留言: