讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議127:Lock與synchronized是不一樣的 >

建議127:Lock與synchronized是不一樣的

很多編碼者都會說,Lock類和synchronized關鍵字用在代碼塊的並發性和內存上時語義是一樣的,都是保持代碼塊同時只有一個線程具有執行權。這樣的說法只對了一半,我們以一個任務提交給多個線程運行為例,來看看使用顯式鎖(Lock類)和內部鎖(synchronized關鍵字)有什麼不同。首先定義一個任務:


class Task{

public void doSomething(){

try{

//每個線程等待2秒鐘,注意將此時的線程轉為WAITING狀態

Thread.sleep(2000);

}catch(Exception e){

//異常處理

}

StringBuffer sb=new StringBuffer();

//線程名稱

sb.append(\"線程名稱:\"+Thread.currentThread().getName());

//運行的時間戳

sb.append(\",執行時間:\"+Calendar.getInstance().get(13)+\"s\");

System.out.println(sb);

}

}


該類模擬了一個執行時間比較長的計算,注意這裡使用的是模擬方式,在使用sleep方法時線程的狀態會從運行狀態轉變為等待狀態。該任務要具備多線程能力時必須實現Runnable接口,我們分別建立兩種不同的鎖實現機制,首先看顯式鎖實現:


//顯式鎖任務

class TaskWithLock extends Task implements Runnable{

//聲明顯式鎖

private fnal Lock lock=new ReentrantLock();

@Override

public void run(){

try{

//開始鎖定

lock.lock();

doSomething();

}finally{

//釋放鎖

lock.unlock();

}

}

}


這裡有一點需要說明的是,顯式鎖的鎖定和釋放必須在一個try……finally塊中,這是為了確保即使出現運行期異常也能正常釋放鎖,保證其他線程能夠順利執行。

內部鎖的處理也非常簡單,代碼如下:


//內部鎖任務

class TaskWithSync extends Task implements Runnable{

@Override

public void run(){

//內部鎖

synchronized(\"A\"){

doSomething();

}

}

}


這兩個任務看著非常相似,應該能夠產生相似的結果吧?我們建立一個模擬場景,保證同時有三個線程在運行,代碼如下:


public static void runTasks(Class<?extends Runnable>clz)throws Exception{

ExecutorService es=Executors.newCachedThreadPool();

System.out.println(\"***開始執行\"+clz.getSimpleName()+\"任務****\");

//啟動三個線程

for(int i=0;i<3;i++){

es.submit(clz.newInstance());

}

//等待足夠長的時間,然後關閉執行器

TimeUnit.SECONDS.sleep(10);

System.out.println(\"------\"+clz.getSimpleName()+\"任務執行完畢-----n\");

//關閉執行器

es.shutdown();

}

public static void main(Stringargs)throws Exception{

//運行顯式鎖任務

runTasks(TaskWithLock.class);

//運行內部鎖任務

runTasks(TaskWithSync.class);

}


按照一般的理解,Lock和synchronized的處理方式是相同的,輸出應該沒有差別,但是很遺憾的是,輸出差別其實很大。輸出如下:


*****開始執行TaskWithLock任務******

線程名稱:pool-1-thread-1,執行時間:33 s

線程名稱:pool-1-thread-2,執行時間:33 s

線程名稱:pool-1-thread-3,執行時間:33 s

------TaskWithLock任務執行完畢-----

*****開始執行TaskWithSync任務******

線程名稱:pool-2-thread-1,執行時間:43 s

線程名稱:pool-2-thread-3,執行時間:45 s

線程名稱:pool-2-thread-2,執行時間:47 s

------TaskWithSync任務執行完畢-----


注意看運行的時間戳,顯式鎖是同時運行的,很顯然在pool-1-thread-1線程執行到sleep時,其他兩個線程也會運行到這裡,一起等待,然後一起輸出,這還具有線程互斥的概念嗎?

而內部鎖的輸出則是我們的預期結果:pool-2-thread-1線程在運行時其他線程處於等待狀態,pool-2-thread-1執行完畢後,JVM從等待線程池中隨機獲得一個線程pool-2-thread-3執行,最後再執行pool-2-thread-2,這正是我們希望的。

現在問題來了:Lock鎖為什麼不出現互斥情況呢?

這是因為對於同步資源來說(示例中是代碼塊),顯式鎖是對像級別的鎖,而內部鎖是類級別的鎖,也就是說Lock鎖是跟隨對象的,synchronized鎖是跟隨類的,更簡單地說把Lock定義為多線程類的私有屬性是起不到資源互斥作用的,除非是把Lock定義為所有線程的共享變量。都說代碼是最好的解釋語言,我們來看一個Lock鎖資源的代碼:


public static void main(Stringargs){

//多個線程共享鎖

final Lock lock=new ReentrantLock();

//啟動三個線程

for(int i=0;i<3;i++){

new Thread(new Runnable(){

@Override

public void run(){

try{

lock.lock();

//休眠2秒鐘

Thread.sleep(2000);

System.out.println(Thread.currentThread().getName());

}catch(InterruptedException e){

e.printStackTrace();

}finally{

lock.unlock();

}

}

}).start();

}

}


讀者可以執行一下,會發現線程名稱Thread-0、Thread-1、Thread-2會逐漸輸出,也就是一個線程在執行時,其他線程就處於等待狀態。注意,這裡三個線程運行的實例對象是同一個類(都是Client$1類的實例)。

那除了這一點不同之外,顯式鎖和內部鎖還有什麼不同呢?還有以下4點不同:

(1)Lock支持更細粒度的鎖控制

假設讀寫鎖分離,寫操作時不允許有讀寫操作存在,而讀操作時讀寫可以並發執行,這一點內部鎖就很難實現。顯式鎖的示例代碼如下:


class Foo{

//可重入的讀寫鎖

private final ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();

//讀鎖

private final Lock r=rwl.readLock();

//寫鎖

private final Lock w=rwl.writeLock();

//多操作,可並發執行

public void read(){

try{

r.lock();

Thread.sleep(1000);

System.out.println(\"read……\");

}catch(InterruptedException e){

e.printStackTrace();

}finally{

r.unlock();

}

}

//寫操作,同時只允許一個寫操作

public void write(Object_obj){

try{

w.lock();

Thread.sleep(1000);

System.out.println(\"Writing……\");

}catch(InterruptedException e){

e.printStackTrace();

}finally{

w.unlock();

}

}

}


可以編寫一個Runnable的實現類,把Foo類作為資源進行調用(注意多線程是共享這個資源的),然後就會發現這樣的現象:讀寫鎖允許同時有多個讀操作但只允許有一個寫操作,也就是當有一個寫線程在執行時,所有的讀線程和寫線程都會阻塞,直到寫線程釋放鎖資源為止,而讀鎖則可以有多個線程同時執行。

(2)Lock是無阻塞鎖,synchronized是阻塞鎖

當線程A持有鎖時,線程B也期望獲得鎖,此時,如果程序中使用的是顯式鎖,則B線程為等待狀態(在通常的描述中,也認為此線程被阻塞了),若使用的是內部鎖則為阻塞狀態。

(3)Lock可實現公平鎖,synchronized只能是非公平鎖

什麼叫非公平鎖呢?當一個線程A持有鎖,而線程B、C處於阻塞(或等待)狀態時,若線程A釋放鎖,JVM將從線程B、C中隨機選擇一個線程持有鎖並使其獲得執行權,這叫做非公平鎖(因為它拋棄了先來後到的順序);若JVM選擇了等待時間最長的一個線程持有鎖,則為公平鎖(保證每個線程的等待時間均衡)。需要注意的是,即使是公平鎖,JVM也無法準確做到「公平」,在程序中不能以此作為精確計算。

顯式鎖默認是非公平鎖,但可以在構造函數中加入參數true來聲明出公平鎖,而synchronized實現的是非公平鎖,它不能實現公平鎖。

(4)Lock是代碼級的,synchronized是JVM級的

Lock是通過編碼實現的,synchronized是在運行期由JVM解釋的,相對來說synchronized的優化可能性更高,畢竟是在最核心部分支持的,Lock的優化則需要用戶自行考慮。

顯式鎖和內部鎖的功能各不相同,在性能上也稍有差別,但隨著JDK的不斷推進,相對來說,顯式鎖使用起來更加便利和強大,在實際開發中選擇哪種類型的鎖就需要根據實際情況考慮了:靈活、強大則選擇Lock,快捷、安全則選擇synchronized。

注意 兩種不同的鎖機制,根據不同的情況來選擇。