一.synchronized的缺陷
synchronized是java中的一個關鍵字,也就是說是Java語言内置的特性。那麼為什麼會出現Lock呢?
在上面一篇文章中,我們了解到如果一個代碼塊被synchronized修飾了,當一個線程擷取了對應的鎖,并執行該代碼塊時,其他線程便隻能一直等待,等待擷取鎖的線程釋放鎖,而這裡擷取鎖的線程釋放鎖隻會有兩種情況:
1)擷取鎖的線程執行完了該代碼塊,然後線程釋放對鎖的占有;
2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。
那麼如果這個擷取鎖的線程由于要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便隻能幹巴巴地等待,試想一下,這多麼影響程式執行效率。
是以就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如隻等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。
再舉個例子:當有多個線程讀寫檔案時,讀操作和寫操作會發生沖突現象,寫操作和寫操作會發生沖突現象,但是讀操作和讀操作不會發生沖突現象。
但是采用synchronized關鍵字來實作同步的話,就會導緻一個問題:
如果多個線程都隻是進行讀操作,是以當一個線程在進行讀操作時,其他線程隻能等待無法進行讀操作。
是以就需要一種機制來使得多個線程都隻是進行讀操作時,線程之間不會發生沖突,通過Lock就可以辦到。
另外,通過Lock可以知道線程有沒有成功擷取到鎖。這個是synchronized無法辦到的。
總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:
1)Lock不是Java語言内置的,synchronized是Java語言的關鍵字,是以是内置特性。Lock是一個類,通過這個類可以實作同步通路;
2)Lock和synchronized有一點非常大的不同,采用synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之後,系統會自動讓線程釋放對鎖的占用;而Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導緻出現死鎖現象。
二.java.util.concurrent.locks包下常用的類
下面我們就來探讨一下java.util.concurrent.locks包中常用的類和接口。
1.Lock
首先要說明的就是Lock,通過檢視Lock的源碼可知,Lock是一個接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
下面來逐個講述Lock接口中每個方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來擷取鎖的。unLock()方法是用來釋放鎖的。newCondition()這個方法暫且不在此講述,會在後面的線程協作一文中講述。
在Lock中聲明了四個方法來擷取鎖,那麼這四個方法有何差別呢?
首先lock()方法是平常使用得最多的一個方法,就是用來擷取鎖。如果鎖已被其他線程擷取,則進行等待。
由于在前面講到如果采用Lock,必須主動去釋放鎖,并且在發生異常時,不會自動釋放鎖。是以一般來說,使用Lock必須在try{}catch{}塊中進行,并且将釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:
Lock lock = ...;
lock.lock();
try{
//處理任務
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,隻不過差別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之内如果還拿不到鎖,就傳回false。如果如果一開始拿到鎖或者在等待期間内拿到了鎖,則傳回true。tryLock()方法是有傳回值的,它表示用來嘗試擷取鎖,如果擷取成功,則傳回true,如果擷取失敗(即鎖已被其他線程擷取),則傳回false,也就說這個方法無論如何都會立即傳回。在拿不到鎖時不會一直在那等待。
是以,一般情況下通過tryLock來擷取鎖時是這樣使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//處理任務
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能擷取鎖,則直接做其他事情
}
lockInterruptibly()方法比較特殊,當通過這個方法去擷取鎖時,如果線程正在等待擷取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀态。也就使說,當兩個線程同時通過lock.lockInterruptibly()想擷取某個鎖時,假若此時線程A擷取到了鎖,而線程B隻有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
由于lockInterruptibly()的聲明中抛出了異常,是以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明抛出InterruptedException。
是以lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意,當一個線程擷取了鎖之後,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運作過程中的線程,隻能中斷阻塞過程中的線程。
是以當通過lockInterruptibly()方法擷取某個鎖時,如果不能擷取到,隻有進行等待的情況下,是可以響應中斷的。
而用synchronized修飾的話,當一個線程處于等待某個鎖的狀态,是無法被中斷的,隻有一直等待下去。
2.ReentrantLock
ReentrantLock,意思是“可重入鎖”,關于可重入鎖的概念在下一節講述。ReentrantLock是唯一實作了Lock接口的類,并且ReentrantLock提供了更多的方法。下面通過一些執行個體看具體看一下如何使用ReentrantLock。
例子1,lock()的正确使用方法
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
Lock lock = new ReentrantLock(); //注意這個地方
lock.lock();
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
}
}
各位朋友先想一下這段代碼的輸出結果是什麼?
Thread-0得到了鎖
Thread-1得到了鎖
Thread-0釋放了鎖
Thread-1釋放了鎖
也許有朋友會問,怎麼會輸出這個結果?第二個線程怎麼會在第一個線程釋放鎖之前得到了鎖?原因在于,在insert方法中的lock變量是局部變量,每個線程執行該方法時都會儲存一個副本,那麼理所當然每個線程執行到lock.lock()處擷取的是不同的鎖,是以就不會發生沖突。
知道了原因改起來就比較容易了,隻需要将lock聲明為類的屬性即可。
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意這個地方
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
lock.lock();
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
}
}
例子2,tryLock()的使用方法這樣就是正确地使用Lock的方法了。
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意這個地方
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
if(lock.tryLock()) {
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
} else {
System.out.println(thread.getName()+"擷取鎖失敗");
}
}
}
輸出結果:
Thread-0得到了鎖
Thread-1擷取鎖失敗
Thread-0釋放了鎖
例子3,lockInterruptibly()響應中斷的使用方法:
public class Test {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
Test test = new Test();
MyThread thread1 = new MyThread(test);
MyThread thread2 = new MyThread(test);
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意,如果需要正确中斷等待鎖的線程,必須将擷取鎖放在外面,然後将InterruptedException抛出
try {
System.out.println(thread.getName()+"得到了鎖");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
break;
//插入資料
}
}
finally {
System.out.println(Thread.currentThread().getName()+"執行finally");
lock.unlock();
System.out.println(thread.getName()+"釋放了鎖");
}
}
}
class MyThread extends Thread {
private Test test = null;
public MyThread(Test test) {
this.test = test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中斷");
}
}
}
運作之後,發現thread2能夠被正确中斷。
3.ReadWriteLock
ReadWriteLock也是一個接口,在它裡面隻定義了兩個方法:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一個用來擷取讀鎖,一個用來擷取寫鎖。也就是說将檔案的讀寫操作分開,分成2個鎖來配置設定給線程,進而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實作了ReadWriteLock接口。
4.ReentrantReadWriteLock
ReentrantReadWriteLock裡面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來擷取讀鎖和寫鎖。
下面通過幾個例子來看一下ReentrantReadWriteLock具體用法。
假如有多個線程要同時進行讀操作的話,先看一下synchronized達到的效果:
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public synchronized void get(Thread thread) {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在進行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
}
}
這段程式的輸出結果會是,直到thread1執行完讀操作之後,才會列印thread2執行讀操作的資訊。

Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0讀操作完畢
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1讀操作完畢
View Code
而改成用讀寫鎖的話:
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public void get(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在進行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
} finally {
rwl.readLock().unlock();
}
}
}
此時列印的結果為:

Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0讀操作完畢
Thread-1讀操作完畢
說明thread1和thread2在同時進行讀操作。
這樣就大大提升了讀操作的效率。
不過要注意的是,如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
關于ReentrantReadWriteLock類中的其他方法感興趣的朋友可以自行查閱API文檔。
5.Lock和synchronized的選擇
總結來說,Lock和synchronized有以下幾點不同:
1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是内置的語言實作;
2)synchronized在發生異常時,會自動釋放線程占有的鎖,是以不會導緻死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,是以使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
4)通過Lock可以知道有沒有成功擷取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個線程進行讀操作的效率。
在性能上來說,如果競争資源不激烈,兩者的性能是差不多的,而當競争資源非常激烈時(即有大量線程同時競争),此時Lock的性能要遠遠優于synchronized。是以說,在具體使用時要根據适當情況選擇。
三.鎖的相關概念介紹
在前面介紹了Lock的基本使用,這一節來介紹一下與鎖相關的幾個概念。
1.可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了鎖的配置設定機制:基于線程的配置設定,而不是基于方法調用的配置設定。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。
看下面這段代碼就明白了:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A擷取了這個對象的鎖,而由于method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因為線程A已經持有了該對象的鎖,而又在申請擷取該對象的鎖,這樣就會線程A一直等待永遠不會擷取到的鎖。
而由于synchronized和Lock都具備可重入性,是以不會發生上述現象。
2.可中斷鎖
可中斷鎖:顧名思義,就是可以相應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執行鎖中的代碼,另一線程B正在等待擷取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在别的線程中中斷它,這種就是可中斷鎖。
在前面示範lockInterruptibly()的用法時已經展現了Lock的可中斷性。
3.公平鎖
公平鎖即盡量以請求鎖的順序來擷取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的擷取是按照請求鎖的順序進行的。這樣就可能導緻某個或者一些線程永遠擷取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程擷取鎖的順序。
而對于ReentrantLock和ReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為公平鎖。
看一下這2個類的源代碼就清楚了:
在ReentrantLock中定義了2個靜态内部類,一個是NotFairSync,一個是FairSync,分别用來實作非公平鎖和公平鎖。
我們可以在建立ReentrantLock對象時,通過以下方式來設定鎖的公平性:
ReentrantLock lock = new ReentrantLock(true);
如果參數為true表示為公平鎖,為fasle為非公平鎖。預設情況下,如果使用無參構造器,則是非公平鎖。
另外在ReentrantLock類中定義了很多方法,比如:
isFair() //判斷鎖是否是公平鎖
isLocked() //判斷鎖是否被任何線程擷取了
isHeldByCurrentThread() //判斷鎖是否被目前線程擷取了
hasQueuedThreads() //判斷是否有線程在等待該鎖
在ReentrantReadWriteLock中也有類似的方法,同樣也可以設定為公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock并未實作Lock接口,它實作的是ReadWriteLock接口。
4.讀寫鎖
讀寫鎖将對一個資源(比如檔案)的通路分成了2個鎖,一個讀鎖和一個寫鎖。
正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發生沖突。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實作了這個接口。
可以通過readLock()擷取讀鎖,通過writeLock()擷取寫鎖。
上面已經示範過了讀寫鎖的使用方法,在此不再贅述。
參考資料:
http://blog.csdn.net/ns_code/article/details/17487337
http://houlinyan.iteye.com/blog/1112535
http://ifeve.com/locks/
http://ifeve.com/read-write-locks/
http://blog.csdn.net/fancyerii/article/details/6783224
http://blog.csdn.net/ghsau/article/details/7461369/
http://blog.csdn.net/zhaozhenzuo/article/details/37109015
熬夜不易,點選請老王喝杯烈酒!!!!!!!