在上一節(jié)中,
我們已經(jīng)了解了Java多線程編程中常用的關(guān)鍵字synchronized,以及與之相關(guān)的對(duì)象鎖機(jī)制。這一節(jié)中,讓 我們一起來(lái)認(rèn)識(shí)JDK 5中新引入的并發(fā)框架中的鎖機(jī)制。

我想很多購(gòu)買了《Java程序員面試寶典》之類圖書的朋友一定對(duì)下面 這個(gè)面試題感到非常熟悉:

問:請(qǐng)對(duì)比synchronized與java.util.concurrent.locks.Lock 的異同。
答案:主要相同點(diǎn):Lock能完成synchronized所實(shí)現(xiàn)的所有功能
     主要不同點(diǎn):Lock有比synchronized更精確的線程語(yǔ)義和更好的性能。synchronized會(huì)自動(dòng)釋放 鎖,而Lock一定要求程序員手工釋放,并且必須在finally從句中釋放。

恩,讓我們先鄙視一下應(yīng)試教育。

言歸正傳,我們先來(lái)看一個(gè)多線程程序。它使用多個(gè)線程對(duì)一個(gè)Student對(duì)象進(jìn)行訪問,改變其中的變量值。 我們首先用傳統(tǒng)的synchronized 機(jī)制來(lái)實(shí)現(xiàn)它:

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對(duì)象作為同步鎖
            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對(duì)象作為同步鎖
            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/
運(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 秒)

顯然,在這個(gè)程序中,由于兩段synchronized塊使用了同樣的對(duì)象做為對(duì)象鎖,所以JVM優(yōu)先使剛剛釋放該鎖的線程重新獲得該 鎖。這樣,每個(gè)線程執(zhí)行的時(shí)間是10秒鐘,并且要徹底把兩個(gè)同步塊的動(dòng)作執(zhí)行完畢,才能釋放對(duì)象鎖。這樣,加起來(lái)一共是 30秒。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
我想一定有人會(huì)說(shuō):如果兩段synchronized塊采用兩個(gè)不同的對(duì)象鎖,就可以提高程序的并發(fā)性,并且,這 兩個(gè)對(duì)象鎖應(yīng)該選擇那些被所有線程所共享的對(duì)象。

那么好。我們把第二個(gè)同步塊中的對(duì)象鎖改為student(此處略去代碼,讀 者自己修改),程序運(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:
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é)果來(lái)看,顯然,由于同步塊的對(duì)象鎖不同了,三個(gè)線程的執(zhí)行順序也發(fā)生了變化。在一個(gè)線程釋放第一個(gè)同步塊的同步鎖之 后,第二個(gè)線程就可以進(jìn)入第一個(gè)同步塊,而此時(shí),第一個(gè)線程可以繼續(xù)執(zhí)行第二個(gè)同步塊。這樣,整個(gè)執(zhí)行過程中,有10秒鐘 的時(shí)間是兩個(gè)線程同時(shí)工作的。另外十秒鐘分別是第一個(gè)線程執(zhí)行第一個(gè)同步塊的動(dòng)作和最后一個(gè)線程執(zhí)行第二個(gè)同步塊的動(dòng)作。相比較第一 個(gè)例程,整個(gè)程序的運(yùn)行時(shí)間節(jié)省了1/3。細(xì)心的讀者不難總結(jié)出優(yōu)化前后的執(zhí)行時(shí)間比例公式:(n+1)/2n,其中n為 線程數(shù)。如果線程數(shù)趨近于正無(wú)窮,則程序執(zhí)行效率的提高會(huì)接近50%。而如果一個(gè)線程的執(zhí)行階段被分割成m個(gè) synchronized塊,并且每個(gè)同步塊使用不同的對(duì)象鎖,而同步塊的執(zhí)行時(shí)間恒定,則執(zhí)行時(shí)間比例公式可以寫作:((m- 1)n+1)/mn那么當(dāng)m趨于無(wú)窮大時(shí),線程數(shù)n趨近于無(wú)窮大,則程序執(zhí)行效率的提升幾乎可以達(dá)到100%。(顯然,我 們不能按照理想情況下的數(shù)學(xué)推導(dǎo)來(lái)給BOSS發(fā)報(bào)告,不過通過這樣的數(shù)學(xué)推導(dǎo),至少我們看到了提高多線程程序并發(fā)性的一種方案,而 這種方案至少具備數(shù)學(xué)上的可行性理論支持。)
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
可見,使用不同的對(duì)象鎖,在不同的同步塊中完成任務(wù),可以使性能大大提升。

很多人看到這不禁要問:這和新的Lock框 架有什么關(guān)系?

別著急。我們這就來(lái)看一看。

synchronized塊的確不錯(cuò),但是他有一些功能性的限制:
1. 它無(wú)法中斷一個(gè)正在等候獲得鎖的線程,也無(wú)法通過投票得到鎖,如果不想等下去,也就沒法得到鎖。
2.synchronized 塊對(duì)于鎖的獲得和釋放是在相同的堆棧幀中進(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 類,而不是作為語(yǔ)言的特性來(lái)實(shí)現(xiàn)。這就為 Lock 的多種實(shí)現(xiàn)留下了空間,各種實(shí)現(xiàn)可能有不同的調(diào)度算法、性能特性或者鎖定語(yǔ)義。

JDK 官方文檔中提到:
ReentrantLock是“一個(gè)可重入的互斥鎖 Lock,它具有與使用 synchronized  方法和語(yǔ)句所訪問的隱式監(jiān)視器鎖相同的一些基本行為和語(yǔ)義,但功能更強(qiáng)大。
ReentrantLock 將由最近成功獲得鎖,并且還沒有釋放該鎖的線程所擁有。當(dāng)鎖沒有被另一個(gè)線程所擁有時(shí),調(diào)用 lock 的線程將成功獲取該鎖并返回。如果當(dāng)前線程已經(jīng)擁有該鎖,此方法將立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法來(lái)檢查此情況是否發(fā)生。 ”

簡(jiǎn)單來(lái)說(shuō),ReentrantLock有一個(gè)與鎖相關(guān)的獲取計(jì) 數(shù)器,如果擁有鎖的某個(gè)線程再次得到鎖,那么獲取計(jì)數(shù)器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。這模仿了 synchronized 的語(yǔ)義;如果線程進(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)存語(yǔ)義,但是添加了類似鎖投票、定時(shí)鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭(zhēng)用情況下更佳的性 能。(換句話說(shuō),當(dāng)許多線程都想訪問共享資源時(shí),JVM 可以花更少的時(shí)候來(lái)調(diào)度線程,把更多時(shí)間用在執(zhí)行線程上。)

我們把 上面的例程改造一下:

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!");
        }

    }
}


從上面這個(gè) 程序我們看到:

對(duì)象鎖的獲得和釋放是由手工編碼完成的,所以獲得鎖和釋放鎖的時(shí)機(jī)比使用同步塊具有更好的可定制性。并 且通過程序的運(yùn)行結(jié)果(運(yùn)行結(jié)果忽略,請(qǐng)讀者根據(jù)例程自行觀察),我們可以發(fā)現(xiàn),和使用同步塊的版本相比,結(jié)果是相同的。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.aygfsteel.com/zhangwei217245/
這說(shuō)明兩點(diǎn)問題:
1. 新的ReentrantLock的確實(shí)現(xiàn)了和同步塊相同的語(yǔ)義功能。而對(duì)象鎖的獲得和釋放都可以由編碼 人員自行掌握。
2. 使用新的ReentrantLock,免去了為同步塊放置合適的對(duì)象鎖所要進(jìn)行的考量。
3. 使用新的ReentrantLock,最佳的實(shí)踐就是結(jié)合try/finally塊來(lái)進(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é)果呢?這里面又有什么奧秘呢?

請(qǐng)看本節(jié)的續(xù) ———— Fair or Unfair? It is a question...