Java多執行緒學習(六)Lock鎖的使用

Guide哥發表於2018-03-27

系列文章傳送門:

Java多執行緒學習(一)Java多執行緒入門

Java多執行緒學習(二)synchronized關鍵字(1)

Java多執行緒學習(二)synchronized關鍵字(2)

Java多執行緒學習(三)volatile關鍵字

Java多執行緒學習(四)等待/通知(wait/notify)機制

Java多執行緒學習(五)執行緒間通訊知識點補充

系列文章將被優先更新於微信公眾號“Java面試通關手冊”,歡迎廣大Java程式設計師和愛好技術的人員關注。

本節思維導圖:

本節思維導圖

思維導圖原始檔+思維導圖軟體關注微信公眾號:“Java面試通關手冊” 回覆關鍵字:“Java多執行緒” 免費領取。

一 Lock介面

1.1 Lock介面簡介

鎖是用於通過多個執行緒控制對共享資源的訪問的工具。通常,鎖提供對共享資源的獨佔訪問:一次只能有一個執行緒可以獲取鎖,並且對共享資源的所有訪問都要求首先獲取鎖。 但是,一些鎖可能允許併發訪問共享資源,如ReadWriteLock的讀寫鎖。

在Lock介面出現之前,Java程式是靠synchronized關鍵字實現鎖功能的。JDK1.5之後併發包中新增了Lock介面以及相關實現類來實現鎖功能。

雖然synchronized方法和語句的範圍機制使得使用監視器鎖更容易程式設計,並且有助於避免涉及鎖的許多常見程式設計錯誤,但是有時您需要以更靈活的方式處理鎖。例如,用於遍歷併發訪問的資料結構的一些演算法需要使用“手動”或“鏈鎖定”:您獲取節點A的鎖定,然後獲取節點B,然後釋放A並獲取C,然後釋放B並獲得D等。在這種場景中synchronized關鍵字就不那麼容易實現了,使用Lock介面容易很多。

Lock是synchronized關鍵字的進階,掌握Lock有助於學習併發包中的原始碼,在併發包中大量的類使用了Lock介面作為同步的處理方式。

Lock介面的實現類: ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock

1.2 Lock的簡單使用

  Lock lock=new ReentrantLock();
  lock.lock();
   try{
    }finally{
    lock.unlock();
    }
複製程式碼

因為Lock是介面所以使用時要結合它的實現類,另外在finall語句塊中釋放鎖的目的是保證獲取到鎖之後,最終能夠被釋放。

注意: 最好不要把獲取鎖的過程寫在try語句塊中,因為如果在獲取鎖時發生了異常,異常丟擲的同時也會導致鎖無法被釋放。

1.3 Lock介面的特性和常見方法

Lock介面提供的synchronized關鍵字不具備的主要特性:

特性 描述
嘗試非阻塞地獲取鎖 當前執行緒嘗試獲取鎖,如果這一時刻鎖沒有被其他執行緒獲取到,則成功獲取並持有鎖
能被中斷地獲取鎖 獲取到鎖的執行緒能夠響應中斷,當獲取到鎖的執行緒被中斷時,中斷異常將會被丟擲,同時鎖會被釋放
超時獲取鎖 在指定的截止時間之前獲取鎖, 超過截止時間後仍舊無法獲取則返回

Lock介面基本的方法:

方法名稱 描述
void lock() 獲得鎖。如果鎖不可用,則當前執行緒將被禁用以進行執行緒排程,並處於休眠狀態,直到獲取鎖。
void lockInterruptibly() 獲取鎖,如果可用並立即返回。如果鎖不可用,那麼當前執行緒將被禁用以進行執行緒排程,並且處於休眠狀態,和lock()方法不同的是在鎖的獲取中可以中斷當前執行緒(相應中斷)。
Condition newCondition() 獲取等待通知元件,該元件和當前的鎖繫結,當前執行緒只有獲得了鎖,才能呼叫該元件的wait()方法,而呼叫後,當前執行緒將釋放鎖。
boolean tryLock() 只有在呼叫時才可以獲得鎖。如果可用,則獲取鎖定,並立即返回值為true;如果鎖不可用,則此方法將立即返回值為false 。
boolean tryLock(long time, TimeUnit unit) 超時獲取鎖,當前執行緒在一下三種情況下會返回: 1. 當前執行緒在超時時間內獲得了鎖;2.當前執行緒在超時時間內被中斷;3.超時時間結束,返回false.
void unlock() 釋放鎖。

二 Lock介面的實現類:ReentrantLock

ReentrantLocksynchronized關鍵字一樣可以用來實現執行緒之間的同步互斥,但是在功能是比synchronized關鍵字更強大而且更靈活。

ReentrantLock類常見方法:

構造方法:

方法名稱 描述
ReentrantLock() 建立一個 ReentrantLock的例項。
ReentrantLock(boolean fair) 建立一個特定鎖型別(公平鎖/非公平鎖)的ReentrantLock的例項

ReentrantLock類常見方法(Lock介面已有方法這裡沒加上):

方法名稱 描述
int getHoldCount() 查詢當前執行緒保持此鎖定的個數,也就是呼叫lock()方法的次數。
protected Thread getOwner() 返回當前擁有此鎖的執行緒,如果不擁有,則返回 null
protected Collection getQueuedThreads() 返回包含可能正在等待獲取此鎖的執行緒的集合
int getQueueLength() 返回等待獲取此鎖的執行緒數的估計。
protected Collection getWaitingThreads(Condition condition) 返回包含可能在與此鎖相關聯的給定條件下等待的執行緒的集合。
int getWaitQueueLength(Condition condition) 返回與此鎖相關聯的給定條件等待的執行緒數的估計。
boolean hasQueuedThread(Thread thread) 查詢給定執行緒是否等待獲取此鎖。
boolean hasQueuedThreads() 查詢是否有執行緒正在等待獲取此鎖。
boolean hasWaiters(Condition condition) 查詢任何執行緒是否等待與此鎖相關聯的給定條件
boolean isFair() 如果此鎖的公平設定為true,則返回 true 。
boolean isHeldByCurrentThread() 查詢此鎖是否由當前執行緒持有。
boolean isLocked() 查詢此鎖是否由任何執行緒持有。

2.1 第一個ReentrantLock程式

ReentrantLockTest.java

public class ReentrantLockTest {

	public static void main(String[] args) {

		MyService service = new MyService();

		MyThread a1 = new MyThread(service);
		MyThread a2 = new MyThread(service);
		MyThread a3 = new MyThread(service);
		MyThread a4 = new MyThread(service);
		MyThread a5 = new MyThread(service);

		a1.start();
		a2.start();
		a3.start();
		a4.start();
		a5.start();

	}

	static public class MyService {

		private Lock lock = new ReentrantLock();

		public void testMethod() {
			lock.lock();
			try {
				for (int i = 0; i < 5; i++) {
					System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
				}
			} finally {
				lock.unlock();
			}

		}

	}

	static public class MyThread extends Thread {

		private MyService service;

		public MyThread(MyService service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.testMethod();
		}
	}
}
複製程式碼

執行結果:

執行結果
從執行結果可以看出,當一個執行緒執行完畢後才把鎖釋放,其他執行緒才能執行,其他執行緒的執行順序是不確定的

2.2 Condition介面簡介

我們通過之前的學習知道了:synchronized關鍵字與wait()和notify/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition介面與newCondition() 方法。Condition是JDK1.5之後才有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock物件中可以建立多個Condition例項(即物件監視器),執行緒物件可以註冊在指定的Condition中,從而可以有選擇性的進行執行緒通知,在排程執行緒上更加靈活

在使用notify/notifyAll()方法進行通知時,被通知的執行緒是有JVM選擇的,使用ReentrantLock類結合Condition例項可以實現“選擇性通知”,這個功能非常重要,而且是Condition介面預設提供的。

而synchronized關鍵字就相當於整個Lock物件中只有一個Condition例項,所有的執行緒都註冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處於等待狀態的執行緒這樣會造成很大的效率問題,而Condition例項的signalAll()方法 只會喚醒註冊在該Condition例項中的所有等待執行緒

Condition介面的常見方法:

方法名稱 描述
void await() 相當於Object類的wait方法
boolean await(long time, TimeUnit unit) 相當於Object類的wait(long timeout)方法
signal() 相當於Object類的notify方法
signalAll() 相當於Object類的notifyAll方法

2.3 使用Condition實現等待/通知機制

1. 使用單個Condition例項實現等待/通知機制:

UseSingleConditionWaitNotify.java

public class UseSingleConditionWaitNotify {

	public static void main(String[] args) throws InterruptedException {

		MyService service = new MyService();

		ThreadA a = new ThreadA(service);
		a.start();

		Thread.sleep(3000);

		service.signal();

	}

	static public class MyService {

		private Lock lock = new ReentrantLock();
		public Condition condition = lock.newCondition();

		public void await() {
			lock.lock();
			try {
				System.out.println(" await時間為" + System.currentTimeMillis());
				condition.await();
				System.out.println("這是condition.await()方法之後的語句,condition.signal()方法之後我才被執行");
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
		}

		public void signal() throws InterruptedException {
			lock.lock();
			try {				
				System.out.println("signal時間為" + System.currentTimeMillis());
				condition.signal();
				Thread.sleep(3000);
				System.out.println("這是condition.signal()方法之後的語句");
			} finally {
				lock.unlock();
			}
		}
	}

	static public class ThreadA extends Thread {

		private MyService service;

		public ThreadA(MyService service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.await();
		}
	}
}
複製程式碼

執行結果:

執行結果
在使用wait/notify實現等待通知機制的時候我們知道必須執行完notify()方法所在的synchronized程式碼塊後才釋放鎖。在這裡也差不多,必須執行完signal所在的try語句塊之後才釋放鎖,condition.await()後的語句才能被執行。

注意: 必須在condition.await()方法呼叫之前呼叫lock.lock()程式碼獲得同步監視器,不然會報錯。

2. 使用多個Condition例項實現等待/通知機制:

UseMoreConditionWaitNotify.java

public class UseMoreConditionWaitNotify {
	public static void main(String[] args) throws InterruptedException {

		MyserviceMoreCondition service = new MyserviceMoreCondition();

		ThreadA a = new ThreadA(service);
		a.setName("A");
		a.start();

		ThreadB b = new ThreadB(service);
		b.setName("B");
		b.start();

		Thread.sleep(3000);

		service.signalAll_A();

	}
	static public class ThreadA extends Thread {

		private MyserviceMoreCondition service;

		public ThreadA(MyserviceMoreCondition service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.awaitA();
		}
	}
	static public class ThreadB extends Thread {

		private MyserviceMoreCondition service;

		public ThreadB(MyserviceMoreCondition service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.awaitB();
		}
	}
	
}
複製程式碼

MyserviceMoreCondition.java

public class MyserviceMoreCondition {

	private Lock lock = new ReentrantLock();
	public Condition conditionA = lock.newCondition();
	public Condition conditionB = lock.newCondition();

	public void awaitA() {
		lock.lock();
		try {
			System.out.println("begin awaitA時間為" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionA.await();
			System.out.println("  end awaitA時間為" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void awaitB() {
		lock.lock();
		try {			
			System.out.println("begin awaitB時間為" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionB.await();
			System.out.println("  end awaitB時間為" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void signalAll_A() {
		lock.lock();
		try {			
			System.out.println("  signalAll_A時間為" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionA.signalAll();
		} finally {
			lock.unlock();
		}
	}

	public void signalAll_B() {
		lock.lock();
		try {		
			System.out.println("  signalAll_B時間為" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionB.signalAll();
		} finally {
			lock.unlock();
		}
	}
}
複製程式碼

執行結果:

執行結果:
只有A執行緒被喚醒了。

3. 使用Condition實現順序執行

ConditionSeqExec.java

public class ConditionSeqExec {

	volatile private static int nextPrintWho = 1;
	private static ReentrantLock lock = new ReentrantLock();
	final private static Condition conditionA = lock.newCondition();
	final private static Condition conditionB = lock.newCondition();
	final private static Condition conditionC = lock.newCondition();

	public static void main(String[] args) {

		Thread threadA = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 1) {
						conditionA.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadA " + (i + 1));
					}
					nextPrintWho = 2;
					//通知conditionB例項的執行緒執行
					conditionB.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};

		Thread threadB = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 2) {
						conditionB.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadB " + (i + 1));
					}
					nextPrintWho = 3;
					//通知conditionC例項的執行緒執行
					conditionC.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};

		Thread threadC = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 3) {
						conditionC.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadC " + (i + 1));
					}
					nextPrintWho = 1;
					//通知conditionA例項的執行緒執行
					conditionA.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};
		Thread[] aArray = new Thread[5];
		Thread[] bArray = new Thread[5];
		Thread[] cArray = new Thread[5];

		for (int i = 0; i < 5; i++) {
			aArray[i] = new Thread(threadA);
			bArray[i] = new Thread(threadB);
			cArray[i] = new Thread(threadC);

			aArray[i].start();
			bArray[i].start();
			cArray[i].start();
		}

	}
}
複製程式碼

執行結果:

Condition實現順序執行執行結果
通過程式碼很好理解,說簡單就是在一個執行緒執行完之後通過condition.signal()/condition.signalAll()方法通知下一個特定的執行執行,就這樣迴圈往復即可。

注意: 預設情況下ReentranLock類使用的是非公平鎖

2.4 公平鎖與非公平鎖

Lock鎖分為:公平鎖非公平鎖。公平鎖表示執行緒獲取鎖的順序是按照執行緒加鎖的順序來分配的,即先來先得的FIFO先進先出順序。而非公平鎖就是一種獲取鎖的搶佔機制,是隨機獲取鎖的,和公平鎖不一樣的就是先來的不一定先的到鎖,這樣可能造成某些執行緒一直拿不到鎖,結果也就是不公平的了。

FairorNofairLock.java

public class FairorNofairLock {

	public static void main(String[] args) throws InterruptedException {
		final Service service = new Service(true);//true為公平鎖,false為非公平鎖

		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				System.out.println("★執行緒" + Thread.currentThread().getName()
						+ "執行了");
				service.serviceMethod();
			}
		};

		Thread[] threadArray = new Thread[10];
		for (int i = 0; i < 10; i++) {
			threadArray[i] = new Thread(runnable);
		}
		for (int i = 0; i < 10; i++) {
			threadArray[i].start();
		}

	}
	static public class Service {

		private ReentrantLock lock;

		public Service(boolean isFair) {
			super();
			lock = new ReentrantLock(isFair);
		}

		public void serviceMethod() {
			lock.lock();
			try {
				System.out.println("ThreadName=" + Thread.currentThread().getName()
						+ "獲得鎖定");
			} finally {
				lock.unlock();
			}
		}

	}
}
複製程式碼

執行結果:

公平鎖執行結果
公平鎖的執行結果是有序的。

把Service的引數修改為false則為非公平鎖

final Service service = new Service(false);//true為公平鎖,false為非公平鎖

複製程式碼

非公平鎖執行結果
非公平鎖的執行結果是無序的。

三 ReadWriteLock介面的實現類:ReentrantReadWriteLock

3.1 簡介

我們剛剛接觸到的ReentrantLock(排他鎖)具有完全互斥排他的效果,即同一時刻只允許一個執行緒訪問,這樣做雖然雖然保證了例項變數的執行緒安全性,但效率非常低下。ReadWriteLock介面的實現類-ReentrantReadWriteLock讀寫鎖就是為了解決這個問題。

讀寫鎖維護了兩個鎖,一個是讀操作相關的鎖也成為共享鎖,一個是寫操作相關的鎖 也稱為排他鎖。通過分離讀鎖和寫鎖,其併發性比一般排他鎖有了很大提升。

多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥(只要出現寫操作的過程就是互斥的。)。在沒有執行緒Thread進行寫入操作時,進行讀取操作的多個Thread都可以獲取讀鎖,而進行寫入操作的Thread只有在獲取寫鎖後才能進行寫入操作。即多個Thread可以同時進行讀取操作,但是同一時刻只允許一個Thread進行寫入操作。

3.2 ReentrantReadWriteLock的特性與常見方法

ReentrantReadWriteLock的特性:

特性 說明
公平性選擇 支援非公平(預設)和公平的鎖獲取方式,吞吐量上來看還是非公平優於公平
重進入 該鎖支援重進入,以讀寫執行緒為例:讀執行緒在獲取了讀鎖之後,能夠再次獲取讀鎖。而寫執行緒在獲取了寫鎖之後能夠再次獲取寫鎖也能夠同時獲取讀鎖
鎖降級 遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級稱為讀鎖

ReentrantReadWriteLock常見方法: 構造方法

方法名稱 描述
ReentrantReadWriteLock() 建立一個 ReentrantReadWriteLock()的例項
ReentrantReadWriteLock(boolean fair) 建立一個特定鎖型別(公平鎖/非公平鎖)的ReentrantReadWriteLock的例項

常見方法: 和ReentrantLock類 類似這裡就不列舉了。

3.3 ReentrantReadWriteLock的使用

1. 讀讀共享

兩個執行緒同時執行read方法,你會發現兩個執行緒可以同時或者說是幾乎同時執行lock()方法後面的程式碼,輸出的兩句話顯示的時間一樣。這樣提高了程式的執行效率。


	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

	public void read() {
		try {
			try {
				lock.readLock().lock();
				System.out.println("獲得讀鎖" + Thread.currentThread().getName()
						+ " " + System.currentTimeMillis());
				Thread.sleep(10000);
			} finally {
				lock.readLock().unlock();
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
複製程式碼

2. 寫寫互斥

把上面的程式碼的

lock.readLock().lock();
複製程式碼

改為:

lock.writeLock().lock();
複製程式碼

兩個執行緒同時執行read方法,你會發現同一時間只允許一個執行緒執行lock()方法後面的程式碼

3. 讀寫互斥

	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

	public void read() {
		try {
			try {
				lock.readLock().lock();
				System.out.println("獲得讀鎖" + Thread.currentThread().getName()
						+ " " + System.currentTimeMillis());
				Thread.sleep(10000);
			} finally {
				lock.readLock().unlock();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void write() {
		try {
			try {
				lock.writeLock().lock();
				System.out.println("獲得寫鎖" + Thread.currentThread().getName()
						+ " " + System.currentTimeMillis());
				Thread.sleep(10000);
			} finally {
				lock.writeLock().unlock();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

複製程式碼

測試程式碼:


		Service service = new Service();

		ThreadA a = new ThreadA(service);
		a.setName("A");
		a.start();

		Thread.sleep(1000);

		ThreadB b = new ThreadB(service);
		b.setName("B");
		b.start();

複製程式碼

執行兩個使用同一個Service物件例項的執行緒a,b,執行緒a執行上面的read方法,執行緒b執行上面的write方法。你會發現同一時間只允許一個執行緒執行lock()方法後面的程式碼。記住:只要出現寫操作的過程就是互斥的。

4. 寫讀互斥

和讀寫互斥類似,這裡不用程式碼演示了。記住:只要出現寫操作的過程就是互斥的。

參考:

《Java多執行緒程式設計核心技術》

《Java併發程式設計的藝術》

相關文章