天天看點

java多線程之重入鎖ReentrantLock

在講重入鎖(ReentrantLock)之前,相信大家都synchronized很熟悉了,它也是同步控制的一個重要鎖,決定了一個線程是否可以通路臨界資源,同時synchronized配合Object.wait()和Object.notify()的配合使用起到了等待通知的作用。這裡如果大家不是很熟悉,可以查閱資料熟悉一下synchronized的使用。那麼有synchronized這個鎖,為什麼還要介紹ReentrantLock,這肯定是有原因,接下來我就會介紹ReentrantLock比synchronized鎖的優點。再說到優點之前,我們先看看ReentrantLock的基本使用。

ReenrantLock的基本使用

public class ReenterLock implements Runnable{
	public static ReentrantLock lock = new ReentrantLock();
	public static int i =0;
	@Override
	public void run() {

		for(int j = 0;j<10000;j++){
			lock.lock();
			try {
				i++;
			} 
			finally{
				lock.unlock();
			}
		}
	}
	
public static void main(String[] args) throws InterruptedException {
	ReenterLock t = new ReenterLock();
	Thread t1 =new Thread(t);
	Thread t2 =new Thread(t);
	t1.start();t2.start();
	t1.join();t2.join();
	System.out.println(i);
}
	
           

通過上面代碼我們可以看到ReentrantLock的lock()和unlock()方法要配合try/finally語句塊來完成,這是ReentrantLock的文法風格,希望記住。ReentrantLock之是以叫重入鎖不是根據它的名字翻譯過來的哦,下面我們再看下面代碼:

lock.lock();
lock.lock();
try{
	i++;
}finally{
	lock.unlock();
	lock.unlock();
}
           

在這種情況下,一個線程連續兩次獲得同一把鎖,是允許,如果不允許這麼操作,那麼同一個線程在第二次獲得鎖時,将會與自己産生死鎖。是以重入鎖之是以叫重入鎖,就是表示同一個線程可以多次獲得鎖,隻要在釋放鎖時候,與加鎖的次數必須相同。這裡強調一下synchronized也是可重入的。接下來就說說ReentrantLock相對synchronized的優點:

ReetrantLock的進階功能

  • 可以中斷響應

對于synchronized來說,如果一個線程在等待鎖,那麼結果隻有兩種情況,要麼它獲得鎖繼續執行,要麼它就保持等待。而重入鎖提供了另外一種可能,那就是線程可以中斷。也就是在等待鎖的過程,程式可以根據需要取消對鎖的等待,看如下代碼:

public class ReenterLock implements Runnable{

	public static ReentrantLock lock1 = new ReentrantLock();
	public static ReentrantLock lock2 = new ReentrantLock();
	int lock;
	public ReenterLock(int lock){
		this.lock = lock;
	}
	@Override
	public void run() {
		
		try{
			
			if (lock == 1){
				lock1.lockInterruptibly();
				try{
					Thread.sleep(500);
				}catch(InterruptedException e){}
				lock2.lockInterruptibly();}
			else{
				lock2.lockInterruptibly();
				try{
					Thread.sleep(500);
				}catch(InterruptedException e){}
				lock1.lockInterruptibly();
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}finally{
			if(lock1.isHeldByCurrentThread())
				lock1.unlock();
			if(lock2.isHeldByCurrentThread())
				lock2.unlock();
			System.out.println(Thread.currentThread().getId()+":線程退出");
		}
			
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReenterLock r1 = new ReenterLock(1);
		ReenterLock r2 = new ReenterLock(2);
		Thread t1 =new Thread(r1);
		Thread t2 =new Thread(r2);
		t1.start();t2.start();
		Thread.sleep(1000);
		t2.interrupt();
	}
	
}
           

再看看結果:

java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:896)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1221)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:340)
	at demo.ReenterLock.run(ReenterLock.java:29)
	at java.lang.Thread.run(Thread.java:745)
10:線程退出
9:線程退出
           

閱讀上面代碼和結果我們就會發現線程t1和t2啟動後,t1先占用lock1,再占用lock2;t2先占用lock2,再請求lock1。是以很容易形成t1和t2之間互相等待,變成死鎖。但是這裡我們使用了t2.interrupt()也就是上面代碼最後一條語句,由于我們使用的lockInterruptibly(),這個方法是可以對中斷進行響應的。是以t2就會放棄lock1的申請,同時釋放已獲得lock2。最後t1可以得到lock2繼續執行下去。

  • 鎖等待限時

除了等待外部通知之外,要避免死鎖還有一種方法,那就是限時等待。舉個例子,你等待朋友一起打球,在等待1小時,如果朋友遲遲不來,又無法聯系到他,那麼我們應該掃興離去。對線程來說也是這樣的,我們給一個線程等待鎖的時間,如果在這個時間等不到鎖,那麼線程就會放棄。

public class ReenterLock implements Runnable{

	public static ReentrantLock lock = new ReentrantLock();
	@Override
	public void run() {
		
		try{
			if(lock.tryLock(5, TimeUnit.SECONDS))
			Thread.sleep(6000);
			else{
			System.out.println("get this lock fialed");
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}finally{
			if(lock.isHeldByCurrentThread())
				lock.unlock();
		}
			
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReenterLock t = new ReenterLock();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();t2.start();
		
	}
	
}
           

在這裡,tryLock()方法接受兩個參數,一個表示等待時長,另外一個表示計時機關。這裡的機關為秒,時長為5,表示線程在這個鎖請求中,最多等待5秒。如果超過5秒還沒有得到鎖,就會傳回false。這裡再介紹一下,ReentrantLock.tryLock()方法也可以不帶參數直接運作。這種情況下,目前線程會嘗試獲得鎖,如果所并未被其他線程占用個,則申請所成功,傳回true。否則申請失敗,立即傳回false。

  • 公平鎖

在大多數情況下,鎖的申請都是非公平的。也就是說,線程1首先請求了鎖A,接着線程2頁請求了鎖A。那麼當鎖A可用時,是線程1可以獲得鎖還是線程2可以獲得鎖?在synchronized這個是不一定,系統會從這個鎖的等待隊列中随機挑選一個。不管誰先誰後,其實這就是不公平的。而公平鎖,則不是這樣,它會按照時間的先後順序,保證先到者先得。這樣就不會産生饑餓現象。雖然看起來公平鎖很好,但是公平鎖需要維護一個有序隊列,是以性能就會下降,是以預設情況下,ReentrantLock是非公平的。下面看一段代碼體驗一下公平鎖:

public class ReenterLock implements Runnable{

	public static ReentrantLock fairlock = new ReentrantLock(true);//設定為true開啟公平鎖,預設是false
	@Override
	public void run() {
		
		while(true){
			try{
				fairlock.lock();
				System.out.println(Thread.currentThread().getName()+"獲得鎖");
			}finally{
				fairlock.unlock();
			}
		}
			
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReenterLock t = new ReenterLock();
		Thread t1 = new Thread(t,"Thread_1");
		Thread t2 = new Thread(t,"Thread_2");
		t1.start();t2.start();
		
	}
	
}
           

上面代碼在通過ReentranLock(boolean fair)設定為true開啟公平鎖,接下來看看結果:

Thread_1獲得鎖
Thread_2獲得鎖
Thread_1獲得鎖
Thread_2獲得鎖
Thread_1獲得鎖
Thread_2獲得鎖
Thread_1獲得鎖
Thread_2獲得鎖
Thread_1獲得鎖
           

由于代碼有大量的輸出,這裡隻截取部分進行說明。這個輸出結果中,很明顯可以看到,兩個線程是交替執行的。

Condition條件

如果大家知道Object.wait 和 Object.notify方法的話,那麼就能很容易了解Condition 了,它們的作用大緻相同的。Object.wait 和 Object.notify方法的使用必須配合sysnchronized關鍵字合作使用。這個因為無論是wait()還是notify()都需要首先獲得目标對象的一個螢幕,這個螢幕也就是synchronized綁定的那個對象,而wait()和notify()方法執行後,就會釋放這個螢幕,下次執行還是需要獲得這個螢幕。而Condition是與重入鎖相關聯的。也是基于這個原因。下面就來通過一段代碼來進行解釋:

public class ReenterLock implements Runnable{

	public static ReentrantLock lock = new ReentrantLock();
	public static Condition condition = lock.newCondition();
	@Override
	public void run() {
			
		try{
			lock.lock();
			condition.await();
			System.out.println("Thread is going on");
			
		}catch(InterruptedException e){
			e.printStackTrace();
			
		}finally{
			lock.unlock();
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReenterLock t = new ReenterLock();
		Thread t1 = new Thread(t);
		t1.start();
		Thread.sleep(2000);
		//通知線程t1繼續執行
		lock.lock();
		condition.signal();
		lock.unlock();
		
	}
	
}
           

代碼第三行,通過lock生成一個與之綁定的Condition對象。代碼condition.await(),要求線程Condition對象上進行等待。代碼condition.signa()由主線程main發出通知,告知在Condition上的線程可以繼續執行了,這裡看到到和wait()和notify()一樣,當線程使用Condition.await()時,要求線程持有相關的重入鎖,在Condition.await()調用後,這個線程就會釋放。同理,在Conditon.signal()方法調用時,也要求線程先獲得鎖,在signal()方法調用之後,系統會從目前Condition等待對象中喚醒一個線程。