很多編碼者都會說,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。
注意 兩種不同的鎖機制,根據不同的情況來選擇。