Java多線程學習(六)Lock鎖的使用

系列文章傳送門:java

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併發編程的藝術》

相關文章
相關標籤/搜索