在上一節(jié)中,
我們已經(jīng)了解了Java多線程編程中常用的關(guān)鍵字synchronized,以及與之相關(guān)的對象鎖機(jī)制。這一節(jié)中,讓
我們一起來認(rèn)識JDK 5中新引入的并發(fā)框架中的鎖機(jī)制。
我想很多購買了《Java程序員面試寶典》之類圖書的朋友一定對下面
這個(gè)面試題感到非常熟悉:
問:請對比synchronized與java.util.concurrent.locks.Lock
的異同。
答案:主要相同點(diǎn):Lock能完成synchronized所實(shí)現(xiàn)的所有功能
主要不同點(diǎn):Lock有比synchronized更精確的線程語義和更好的性能。synchronized會自動釋放
鎖,而Lock一定要求程序員手工釋放,并且必須在finally從句中釋放。
恩,讓我們先鄙視一下應(yīng)試教育。
言歸正傳,我們先來看一個(gè)多線程程序。它使用多個(gè)線程對一個(gè)Student對象進(jìn)行訪問,改變其中的變量值。
我們首先用傳統(tǒng)的synchronized 機(jī)制來實(shí)現(xiàn)它:
運(yùn)行結(jié)果:
顯然,在這個(gè)程序中,由于兩段synchronized塊使用了同樣的對象做為對象鎖,所以JVM優(yōu)先使剛剛釋放該鎖的線程重新獲得該
鎖。這樣,每個(gè)線程執(zhí)行的時(shí)間是10秒鐘,并且要徹底把兩個(gè)同步塊的動作執(zhí)行完畢,才能釋放對象鎖。這樣,加起來一共是
30秒。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
我想一定有人會說:如果兩段synchronized塊采用兩個(gè)不同的對象鎖,就可以提高程序的并發(fā)性,并且,這
兩個(gè)對象鎖應(yīng)該選擇那些被所有線程所共享的對象。
那么好。我們把第二個(gè)同步塊中的對象鎖改為student(此處略去代碼,讀
者自己修改),程序運(yùn)行結(jié)果為:
從 修改后的運(yùn)行結(jié)果來看,顯然,由于同步塊的對象鎖不同了,三個(gè)線程的執(zhí)行順序也發(fā)生了變化。在一個(gè)線程釋放第一個(gè)同步塊的同步鎖之
后,第二個(gè)線程就可以進(jìn)入第一個(gè)同步塊,而此時(shí),第一個(gè)線程可以繼續(xù)執(zhí)行第二個(gè)同步塊。這樣,整個(gè)執(zhí)行過程中,有10秒鐘
的時(shí)間是兩個(gè)線程同時(shí)工作的。另外十秒鐘分別是第一個(gè)線程執(zhí)行第一個(gè)同步塊的動作和最后一個(gè)線程執(zhí)行第二個(gè)同步塊的動作。相比較第一
個(gè)例程,整個(gè)程序的運(yùn)行時(shí)間節(jié)省了1/3。細(xì)心的讀者不難總結(jié)出優(yōu)化前后的執(zhí)行時(shí)間比例公式:(n+1)/2n,其中n為
線程數(shù)。如果線程數(shù)趨近于正無窮,則程序執(zhí)行效率的提高會接近50%。而如果一個(gè)線程的執(zhí)行階段被分割成m個(gè)
synchronized塊,并且每個(gè)同步塊使用不同的對象鎖,而同步塊的執(zhí)行時(shí)間恒定,則執(zhí)行時(shí)間比例公式可以寫作:((m-
1)n+1)/mn那么當(dāng)m趨于無窮大時(shí),線程數(shù)n趨近于無窮大,則程序執(zhí)行效率的提升幾乎可以達(dá)到100%。(顯然,我
們不能按照理想情況下的數(shù)學(xué)推導(dǎo)來給BOSS發(fā)報(bào)告,不過通過這樣的數(shù)學(xué)推導(dǎo),至少我們看到了提高多線程程序并發(fā)性的一種方案,而
這種方案至少具備數(shù)學(xué)上的可行性理論支持。)
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
可見,使用不同的對象鎖,在不同的同步塊中完成任務(wù),可以使性能大大提升。
很多人看到這不禁要問:這和新的Lock框 架有什么關(guān)系?
別著急。我們這就來看一看。
synchronized塊的確不錯(cuò),但是他有一些功能性的限制:
1. 它無法中斷一個(gè)正在等候獲得鎖的線程,也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖。
2.synchronized
塊對于鎖的獲得和釋放是在相同的堆棧幀中進(jìn)行的。多數(shù)情況下,這沒問題(而且與異常處理交互得很好),但是,確實(shí)存在一些更適合使用
非塊結(jié)構(gòu)鎖定的情況。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
java.util.concurrent.lock 中的 Lock 框架是鎖定的一個(gè)抽象,它允許把鎖定的實(shí)現(xiàn)作為 Java 類,而不是作為語言的特性來實(shí)現(xiàn)。這就為 Lock 的多種實(shí)現(xiàn)留下了空間,各種實(shí)現(xiàn)可能有不同的調(diào)度算法、性能特性或者鎖定語義。
JDK 官方文檔中提到:
ReentrantLock是“一個(gè)可重入的互斥鎖 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監(jiān)視器鎖相同的一些基本行為和語義,但功能更強(qiáng)大。
ReentrantLock 將由最近成功獲得鎖,并且還沒有釋放該鎖的線程所擁有。當(dāng)鎖沒有被另一個(gè)線程所擁有時(shí),調(diào)用 lock
的線程將成功獲取該鎖并返回。如果當(dāng)前線程已經(jīng)擁有該鎖,此方法將立即返回。可以使用 isHeldByCurrentThread() 和
getHoldCount() 方法來檢查此情況是否發(fā)生。 ”
簡單來說,ReentrantLock有一個(gè)與鎖相關(guān)的獲取計(jì)
數(shù)器,如果擁有鎖的某個(gè)線程再次得到鎖,那么獲取計(jì)數(shù)器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。這模仿了
synchronized 的語義;如果線程進(jìn)入由線程已經(jīng)擁有的監(jiān)控器保護(hù)的 synchronized
塊,就允許線程繼續(xù)進(jìn)行,當(dāng)線程退出第二個(gè)(或者后續(xù)) synchronized 塊的時(shí)候,不釋放鎖,只有線程退出它進(jìn)入的監(jiān)控器保護(hù)的第一個(gè)
synchronized 塊時(shí),才釋放鎖。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
ReentrantLock 類(重入鎖)實(shí)現(xiàn)了 Lock ,它擁有與 synchronized 相同的并發(fā)性和內(nèi)存語義,但是添加了類似鎖投票、定時(shí)鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性
能。(換句話說,當(dāng)許多線程都想訪問共享資源時(shí),JVM 可以花更少的時(shí)候來調(diào)度線程,把更多時(shí)間用在執(zhí)行線程上。)
我們把 上面的例程改造一下:
從上面這個(gè) 程序我們看到:
對象鎖的獲得和釋放是由手工編碼完成的,所以獲得鎖和釋放鎖的時(shí)機(jī)比使用同步塊具有更好的可定制性。并
且通過程序的運(yùn)行結(jié)果(運(yùn)行結(jié)果忽略,請讀者根據(jù)例程自行觀察),我們可以發(fā)現(xiàn),和使用同步塊的版本相比,結(jié)果是相同的。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
這說明兩點(diǎn)問題:
1. 新的ReentrantLock的確實(shí)現(xiàn)了和同步塊相同的語義功能。而對象鎖的獲得和釋放都可以由編碼
人員自行掌握。
2. 使用新的ReentrantLock,免去了為同步塊放置合適的對象鎖所要進(jìn)行的考量。
3. 使用新的ReentrantLock,最佳的實(shí)踐就是結(jié)合try/finally塊來進(jìn)行。在try塊之前使用lock方法,而
在finally中使用unlock方法。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
細(xì)心的讀者又發(fā)現(xiàn)了:
在我們的例程中,創(chuàng)建ReentrantLock實(shí)例的時(shí)候,我們的構(gòu)造函數(shù)里面?zhèn)鬟f的參數(shù)是false。那么如果傳遞
true又回是什么結(jié)果呢?這里面又有什么奧秘呢?
請看本節(jié)的續(xù) ———— Fair or Unfair? It is a question...
我想很多購買了《Java程序員面試寶典》
問:請對比synchronized與java.util.
答案:主要相同點(diǎn):
主要不同點(diǎn):
恩,讓我們先鄙視一下應(yīng)試教育。
言歸正傳,我們先來看一個(gè)多線程程序。
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)使用同一個(gè)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)使用同一個(gè)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!");
}
}
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.aygfsteel.com/zhangwei217245/
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)使用同一個(gè)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)使用同一個(gè)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!");
}
}
運(yùn)行結(jié)果:
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!
成功生成(總時(shí)間:30 秒)
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!
成功生成(總時(shí)間:30 秒)
顯然,在這個(gè)程序中,
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
我想一定有人會說:
那么好。我們把第二個(gè)同步塊中的對象鎖改為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!
成功生成(總時(shí)間:21 秒)
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!
成功生成(總時(shí)間:21 秒)
從 修改后的運(yùn)行結(jié)果來看,顯然,由于同步塊的對象鎖不同了,
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
可見,使用不同的對象鎖,在不同的同步塊中完成任務(wù),
很多人看到這不禁要問:這和新的Lock框 架有什么關(guān)系?
別著急。我們這就來看一看。
synchronized塊的確不錯(cuò),
1. 它無法中斷一個(gè)正在等候獲得鎖的線程,
2.
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
java.util.concurrent.lock 中的 Lock 框架是鎖定的一個(gè)抽象,它允許把鎖定的實(shí)現(xiàn)作為 Java 類,而不是作為語言的特性來實(shí)現(xiàn)。這就為 Lock 的多種實(shí)現(xiàn)留下了空間,各種實(shí)現(xiàn)可能有不同的調(diào)度算法、
JDK 官方文檔中提到:
ReentrantLock是“一個(gè)可重入的互斥鎖 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監(jiān)視器鎖相同的一些基本行為和語義,
ReentrantLock 將由最近成功獲得鎖,并且還沒有釋放該鎖的線程所擁有。
簡單來說,
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
ReentrantLock 類(重入鎖)實(shí)現(xiàn)了 Lock ,它擁有與 synchronized 相同的并發(fā)性和內(nèi)存語義,但是添加了類似鎖投票、
我們把 上面的例程改造一下:
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();//使用另外一個(gè)不同的重入鎖
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!");
}
}
}
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();//使用另外一個(gè)不同的重入鎖
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!");
}
}
}
從上面這個(gè) 程序我們看到:
對象鎖的獲得和釋放是由手工編碼完成的,
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
這說明兩點(diǎn)問題:
1. 新的ReentrantLock的確實(shí)現(xiàn)了和同步塊相同的語義功
2. 使用新的ReentrantLock,
3. 使用新的ReentrantLock,
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
細(xì)心的讀者又發(fā)現(xiàn)了:
在我們的例程中,創(chuàng)建ReentrantLock實(shí)例的時(shí)候,
請看本節(jié)的續(xù) ———— Fair or Unfair? It is a question...
使用 synchronized 其實(shí)仍由操作系統(tǒng)來調(diào)度線程的優(yōu)先級,Lock 則是掌控在 java 程序中
Lock 用 lock() 和 unlock() 取代了 JVM 的 monitorenter 和 monitorexit 操作指令。
感謝黃鶯的深入分析~~~
不清楚你怎樣推出你的公式的。不過博客上談這個(gè)確實(shí)不方便。有時(shí)間可以面談。呵呵。。。
有一個(gè)問題,如果在lock方法之后在try之前出現(xiàn)一些異常,會不會導(dǎo)致lock沒有unlock而出現(xiàn)問題。
為什么一定要在lock方法之后和try塊之前加入一些有可能拋出異常的語句,而不對這些語句進(jìn)行異常捕獲呢?這樣的編碼習(xí)慣難道可取嗎?
或者如果你覺得可以的話,也可以在try塊的第一行就寫lock方法。
這個(gè)與JVM實(shí)現(xiàn)有關(guān)吧?
樓主哪里得來的結(jié)論,請指教
請參閱:http://flierlu.spaces.live.com/Blog/cns!3B002B2C3F5C8E36!160.entry
文中介紹了一種基于預(yù)期的鎖定策略。
文中講到:
“事實(shí)上,JVM 可以根據(jù)運(yùn)行時(shí)信息選擇性合并同類型鎖。隨著現(xiàn)在機(jī)器自動代碼生成的廣泛引用,可以預(yù)期這種基于行為對鎖進(jìn)行合并的思路會非常有用。“、
“這些問題(同步很慢、把類和方法聲明為final可以提高性能、不可變對象是性能毒藥)的出現(xiàn),往往是因?yàn)槭褂谜邔?JVM 的實(shí)現(xiàn)和優(yōu)化思路不熟悉導(dǎo)致的。實(shí)際上 HotSpot 自從 JDK 1.3 版本以后,實(shí)際上有了非常大的進(jìn)步,無論是從功能還是性能上,都已經(jīng)遠(yuǎn)遠(yuǎn)超出了某些人的預(yù)期。而在可以預(yù)見的 Mustang 和 Dolphin 中,更高級和動態(tài)的優(yōu)化還會不斷加入進(jìn)來,并從 JVM 一級對應(yīng)用產(chǎn)生透明的性能提升。”
實(shí)際上,看一下我在本篇文章中的第一個(gè)例程的運(yùn)行結(jié)果,你就會發(fā)現(xiàn),JVM的確是從語義上合并了兩段同步塊的鎖。而從常理上講,這樣的鎖分配策略在保證線程運(yùn)行流暢性方面是相對比較優(yōu)化的方案。