在上一節中,我們已經了解了Java多線程編程中常用的關鍵字synchronized,以及與之相關的對象鎖機制。這一節中,讓我們一起來認 識JDK 5中新引入的並發框架中的鎖機制。
我想很多購買了《Java程序員面試寶典》之類圖書的朋友一定對下面這個面試題感到非常熟悉:
問:請對比synchronized與java.util.concurrent.locks.Lock 的異同。
答案:主要相同點:Lock能完成synchronized所實現的所有功能
主要不同點:Lock有比synchronized更精確的線程語義和更好的性能。synchronized會自動釋放鎖,而Lock一定要求程序員手工釋放, 並且必須在finally從句中釋放。
恩,讓我們先鄙視一下應試教育。
言歸正傳,我們先來看一個多線程程序。它使用多個線程對一個Student對象進行訪問,改變其中的變量值。我們首先用傳統的 synchronized 機制來實現它:
public class ThreadDemo implements Runnable {
class Student {
private int age = 0;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Student student = new Student();
int count = 0;
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
Thread t1 = new Thread(td, "a");
Thread t2 = new Thread(td, "b");
Thread t3 = new Thread(td, "c");
t1.start();
t2.start();
t3.start();
}
public void run() {
accessStudent();
}
public void accessStudent() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running!");
synchronized (this) {//(1)使用同一個ThreadDemo對象作為同步鎖
System.out.println(currentThreadName + " got lock1@Step1!");
try {
count++;
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(currentThreadName + " first Reading count:" + count);
}
}
System.out.println(currentThreadName + " release lock1@Step1!");
synchronized (this) {//(2)使用同一個ThreadDemo對象作為同步鎖
System.out.println(currentThreadName + " got lock2@Step2!");
try {
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread " + currentThreadName + " set age to:" + age);
this.student.setAge(age);
System.out.println("thread " + currentThreadName + " first read age is:" + this.student.getAge());
Thread.sleep(5000);
} catch (Exception ex) {
ex.printStackTrace();
} finally{
System.out.println("thread " + currentThreadName + " second read age is:" + this.student.getAge());
}
}
System.out.println(currentThreadName + " release lock2@Step2!");
}
}
運行結果:
a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count:1
a release lock1@Step1!
a got lock2@Step2!
thread a set age to:76
thread a first read age is:76
thread a second read age is:76
a release lock2@Step2!
c got lock1@Step1!
c first Reading count:2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to:35
thread c first read age is:35
thread c second read age is:35
c release lock2@Step2!
b got lock1@Step1!
b first Reading count:3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to:91
thread b first read age is:91
thread b second read age is:91
b release lock2@Step2!
成功生成(總時間:30 秒)
顯然,在這個程序中,由於兩段synchronized塊使用了同樣的對象做為對象鎖,所以JVM優先使剛剛釋放該鎖的線程重新獲得該鎖。這 樣,每個線程執行的時間是10秒鐘,並且要徹底把兩個同步塊的動作執行完畢,才能釋放對象鎖。這樣,加起來一共是 30秒。
我想一定有人會說:如果兩段synchronized塊采用兩個不同的對象鎖,就可以提高程序的並發性,並且,這兩個對象鎖應該選擇那些被 所有線程所共享的對象。
那麼好。我們把第二個同步塊中的對象鎖改為student(此處略去代碼,讀者自己修改),程序運行結果為:
a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count:1
a release lock1@Step1!
a got lock2@Step2!
thread a set age to:73
thread a first read age is:73
c got lock1@Step1!
thread a second read age is:73
a release lock2@Step2!
c first Reading count:2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to:15
thread c first read age is:15
b got lock1@Step1!
thread c second read age is:15
c release lock2@Step2!
b first Reading count:3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to:19
thread b first read age is:19
thread b second read age is:19
b release lock2@Step2!
成功生成(總時間:21 秒)
從修改後的運行結果來看,顯然,由於同步塊的對象鎖不同了,三個線程的執行順序也發生了變化。在一個線程釋放第一個同步塊的同 步鎖之後,第二個線程就可以進入第一個同步塊,而此時,第一個線程可以繼續執行第二個同步塊。這樣,整個執行過程中,有10秒鐘的 時間是兩個線程同時工作的。另外十秒鐘分別是第一個線程執行第一個同步塊的動作和最後一個線程執行第二個同步塊的動作。相比較第 一個例程,整個程序的運行時間節省了1/3。細心的讀者不難總結出優化前後的執行時間比例公式:(n+1)/2n,其中n為線程數。如果線程 數趨近於正無窮,則程序執行效率的提高會接近50%。而如果一個線程的執行階段被分割成m個 synchronized塊,並且每個同步塊使用不同 的對象鎖,而同步塊的執行時間恆定,則執行時間比例公式可以寫作:((m- 1)n+1)/mn那麼當m趨於無窮大時,線程數n趨近於無窮大,則 程序執行效率的提升幾乎可以達到100%。(顯然,我們不能按照理想情況下的數學推導來給BOSS發報告,不過通過這樣的數學推導,至少 我們看到了提高多線程程序並發性的一種方案,而這種方案至少具備數學上的可行性理論支持。)
可見,使用不同的對象鎖,在不同的同步塊中完成任務,可以使性能大大提升。
很多人看到這不禁要問:這和新的Lock框架有什麼關系?
別著急。我們這就來看一看。
synchronized塊的確不錯,但是他有一些功能性的限制:
1. 它無法中斷一個正在等候獲得鎖的線程,也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖。
2.synchronized 塊對於鎖的獲得和釋放是在相同的堆棧幀中進行的。多數情況下,這沒問題(而且與異常處理交互得很好),但是, 確實存在一些更適合使用非塊結構鎖定的情況。
java.util.concurrent.lock 中的 Lock 框架是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。 這就為 Lock 的多種實現留下了空間,各種實現可能有不同的調度算法、性能特性或者鎖定語義。
JDK 官方文檔中提到:
ReentrantLock是“一個可重入的互斥鎖 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖相同的一些基本行為 和語義,但功能更強大。
ReentrantLock 將由最近成功獲得鎖,並且還沒有釋放該鎖的線程所擁有。當鎖沒有被另一個線程所擁有時,調用 lock 的線程將成功 獲取該鎖並返回。如果當前線程已經擁有該鎖,此方法將立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法來檢查 此情況是否發生。 ”
簡單來說,ReentrantLock有一個與鎖相關的獲取計數器,如果擁有鎖的某個線程再次得到鎖,那麼獲取計數器就加1,然後鎖需要被釋 放兩次才能獲得真正釋放。這模仿了 synchronized 的語義;如果線程進入由線程已經擁有的監控器保護的 synchronized 塊,就允許線 程繼續進行,當線程退出第二個(或者後續) synchronized 塊的時候,不釋放鎖,只有線程退出它進入的監控器保護的第一個 synchronized 塊時,才釋放鎖。
ReentrantLock 類(重入鎖)實現了 Lock ,它擁有與 synchronized 相同的並發性和內存語義,但是添加了類似鎖投票、定時鎖等 候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 可 以花更少的時候來調度線程,把更多時間用在執行線程上。)
我們把上面的例程改造一下:
public class ThreadDemo implements Runnable {
class Student {
private int age = 0;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Student student = new Student();
int count = 0;
ReentrantLock lock1 = new ReentrantLock(false);
ReentrantLock lock2 = new ReentrantLock(false);
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
for (int i = 1; i <= 3; i++) {
Thread t = new Thread(td, i + "");
t.start();
}
}
public void run() {
accessStudent();
}
public void accessStudent() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running!");
lock1.lock();//使用重入鎖
System.out.println(currentThreadName + " got lock1@Step1!");
try {
count++;
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(currentThreadName + " first Reading count:" + count);
lock1.unlock();
System.out.println(currentThreadName + " release lock1@Step1!");
}
lock2.lock();//使用另外一個不同的重入鎖
System.out.println(currentThreadName + " got lock2@Step2!");
try {
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread " + currentThreadName + " set age to:" + age);
this.student.setAge(age);
System.out.println("thread " + currentThreadName + " first read age is:" + this.student.getAge());
Thread.sleep(5000);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
System.out.println("thread " + currentThreadName + " second read age is:" + this.student.getAge());
lock2.unlock();
System.out.println(currentThreadName + " release lock2@Step2!");
}
}
}
從上面這個程序我們看到:
對象鎖的獲得和釋放是由手工編碼完成的,所以獲得鎖和釋放鎖的時機比使用同步塊具有更好的可定制性。並且通過程序的運行結果( 運行結果忽略,請讀者根據例程自行觀察),我們可以發現,和使用同步塊的版本相比,結果是相同的。
這說明兩點問題:
1. 新的ReentrantLock的確實現了和同步塊相同的語義功能。而對象鎖的獲得和釋放都可以由編碼人員自行掌握。
2. 使用新的ReentrantLock,免去了為同步塊放置合適的對象鎖所要進行的考量。
3. 使用新的ReentrantLock,最佳的實踐就是結合try/finally塊來進行。在try塊之前使用lock方法,而在finally中使用unlock方法 。
細心的讀者又發現了:
在我們的例程中,創建ReentrantLock實例的時候,我們的構造函數裡面傳遞的參數是false。那麼如果傳遞 true又回是什麼結果呢? 這裡面又有什麼奧秘呢?
請看本節的續 ———— Fair or Unfair? It is a question...