Java併發編程 - ReentrantLock(重入鎖)以及公平性

轉載自併發編程網,連接地址: ReentrantLock(重入鎖)以及公平性java

簡介

ReentrantLock的實現不只能夠替代隱式的synchronized關鍵字,並且可以提供超過關鍵字自己的多種功能。編程

這裏提到一個鎖獲取的公平性問題,若是在絕對時間上,先對鎖進行獲取的請求必定被先知足,那麼這個鎖是公平的,反之,是不公平的,也就是說等待時間最長的線程最有機會獲取鎖,也能夠說鎖的獲取是有序的。ReentrantLock這個鎖提供了一個構造函數,可以控制這個鎖是不是公平的。 而鎖的名字也是說明了這個鎖具有了重複進入的可能,也就是說可以讓當前線程屢次的進行對鎖的獲取操做,這樣的最大次數限制是Integer.MAX_VALUE,約21億次左右。併發

事實上公平的鎖機制每每沒有非公平的效率高,由於公平的獲取鎖沒有考慮到操做系統對線程的調度因素,這樣形成JVM對於等待中的線程調度次序和操做系統對線程的調度之間的不匹配。對於鎖的快速且重複的獲取過程當中,連續獲取的機率是很是高的,而公平鎖會壓制這種狀況,雖然公平性得以保障,可是響應比卻降低了,可是並非任何場景都是以TPS做爲惟一指標的,由於公平鎖可以減小「飢餓」發生的機率,等待越久的請求越是可以獲得優先知足。ide

實現分析

在ReentrantLock中,對於公平和非公平的定義是經過對同步器AbstractQueuedSynchronizer的擴展加以實現的,也就是在tryAcquire的實現上作了語義的控制。函數

非公平的獲取方法:

final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	} else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
                if (nextc < 0) // overflow
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

上述邏輯主要包括:測試

  • 若是當前狀態爲初始狀態,那麼嘗試設置狀態;
  • 若是狀態設置成功後就返回;
  • 若是狀態被設置,且獲取鎖的線程又是當前線程的時候,進行狀態的自增;
  • 若是未設置成功狀態且當前線程不是獲取鎖的線程,那麼返回失敗。

公平的獲取方法:

protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	} else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0)
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

上述邏輯相比較非公平的獲取,僅加入了當前線程(Node)以前是否有前置節點在等待的判斷。hasQueuedPredecessors()方法命名有些歧義,其實應該是currentThreadHasQueuedPredecessors()更爲妥帖一些,也就是說當前面沒有人排在該節點(Node)前面時候隊且可以設置成功狀態,纔可以獲取鎖。ui

釋放方法:

protected final boolean tryRelease(int releases) {
	int c = getState() - releases;
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}

上述邏輯主要主要計算了釋放狀態後的值,若是爲0則徹底釋放,返回true,反之僅是設置狀態,返回false。this

下面將主要的筆墨放在公平性和非公平性上,首先看一下兩者測試的對比:操作系統

測試用例以下:線程

public class ReentrantLockTest {
	private static Lock fairLock = new ReentrantLock(true);
	private static Lock unfairLock = new ReentrantLock();

	@Test
	public void fair() {
		System.out.println("fair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(fairLock));
			thread.setName("" + i);
			thread.start();
		}

		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	@Test
	public void unfair() {
		System.out.println("unfair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(unfairLock));
			thread.setName("" + i);
			thread.start();
		}

		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private static class Job implements Runnable {
		private Lock lock;
		public Job(Lock lock) {
			this.lock = lock;
		}

		@Override
		public void run() {
			for (int i = 0; i < 5; i++) {
				lock.lock();
				try {
					System.out.println("Lock by:"
							+ Thread.currentThread().getName());
				} finally {
					lock.unlock();
				}
			}
		}
	}
}

調用非公平的測試方法,返回結果(部分):

unfair version
Lock by:0
Lock by:0
Lock by:2
Lock by:2
Lock by:2
Lock by:2
Lock by:2
Lock by:0
Lock by:0
Lock by:0
Lock by:1
Lock by:1
Lock by:1

調用公平的測試方法,返回結果:

fair version
Lock by:0
Lock by:1
Lock by:0
Lock by:2
Lock by:3
Lock by:4
Lock by:1
Lock by:0
Lock by:2
Lock by:3
Lock by:4

仔細觀察返回的結果(其中每一個數字表明一個線程),非公平的結果一個線程連續獲取鎖的狀況很是多,而公平的結果連續獲取的狀況基本沒有。那麼在一個線程獲取了鎖的那一刻,究竟鎖的公平性會致使鎖有什麼樣的處理邏輯呢?

經過以前的同步器(AbstractQueuedSynchronizer)的介紹,在鎖上是存在一個等待隊列,sync隊列,咱們經過複寫ReentrantLock的獲取當前鎖的sync隊列,輸出在ReentrantLock被獲取時刻,當前的sync隊列的狀態。

修改測試以下:

public class ReentrantLockTest {
	private static Lock fairLock = new ReentrantLock2(true);
	private static Lock unfairLock = new ReentrantLock2();
	@Test
	public void fair() {
		System.out.println("fair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(fairLock)) {
				public String toString() {
					return getName();
				}
			};
			thread.setName("" + i);
			thread.start();
		}
		// sleep 5000ms
	}

	@Test
	public void unfair() {
		System.out.println("unfair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(unfairLock)) {
				public String toString() {
					return getName();
				}
			};
			thread.setName("" + i);
			thread.start();
		}
		// sleep 5000ms
	}

	private static class Job implements Runnable {
		private Lock lock;

		public Job(Lock lock) {
			this.lock = lock;
		}

		@Override
		public void run() {
			for (int i = 0; i < 5; i++) {
				lock.lock();
				try {
					System.out.println("Lock by:"
							+ Thread.currentThread().getName() + " and "
							+ ((ReentrantLock2) lock).getQueuedNames()
							+ " waits.");
				} finally {
					lock.unlock();
				}
			}
		}
	}

	private static class ReentrantLock2 extends ReentrantLock {
		public ReentrantLock2() {
			super();
		}
		
		public ReentrantLock2(boolean fair) {
			super(fair);
		}

		private static final long serialVersionUID = 1773716895097002072L;

		public List<String> getQueuedNames() {
			List<String> names = new ArrayList<String>();
			Collection<Thread> threads = super.getQueuedThreads();
			for(Thread thread : threads) {
				names.add(thread.getName());
			}
			return names;
		}
	}
}

上述邏輯主要是經過構造ReentrantLock2用來輸出在sync隊列中的線程內容,並且每一個線程的toString方法被重寫,這樣當一個線程獲取到鎖時,sync隊列裏的內容也就能夠得知了,運行結果以下:

調用非公平方法,返回結果:

unfair version
Lock by:0 and [] waits.
Lock by:0 and [] waits.
Lock by:3 and [2, 1] waits.
Lock by:3 and [4, 2, 1] waits.
Lock by:3 and [4, 2, 1] waits.
Lock by:3 and [0, 4, 2, 1] waits.
Lock by:3 and [0, 4, 2, 1] waits.
Lock by:1 and [0, 4, 2] waits.
Lock by:1 and [0, 4, 2] waits.

調用公平方法,返回結果:

fair version
Lock by:0 and [] waits.
Lock by:1 and [0, 4, 3, 2] waits.
Lock by:2 and [1, 0, 4, 3] waits.
Lock by:3 and [2, 1, 0, 4] waits.
Lock by:4 and [3, 2, 1, 0] waits.
Lock by:0 and [4, 3, 2, 1] waits.
Lock by:1 and [0, 4, 3, 2] waits.
Lock by:2 and [1, 0, 4, 3] waits.

能夠明顯看出,在非公平獲取的過程當中,「插隊」現象很是嚴重,後續獲取鎖的線程根本不顧及sync隊列中等待的線程,而是能獲取就獲取。反觀公平獲取的過程,鎖的獲取就相似線性化的,每次都由sync隊列中等待最長的線程(鏈表的第一個,sync隊列是由尾部結點添加,當前輸出的sync隊列是逆序輸出)獲取鎖。一個 hasQueuedPredecessors方法可以得到公平性的特性,這點其實是由AbstractQueuedSynchronizer來完成的,看一下acquire方法:

public final void acquire(int arg) {
	if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

能夠看到,若是獲取狀態和在sync隊列中排隊是短路的判斷,也就是說若是tryAcquire成功,那麼是不會進入sync隊列的,能夠經過下圖來深入的認識公平性和AbstractQueuedSynchronizer的獲取過程。 非公平的,或者說默認的獲取方式以下圖所示:

獲取方式

對於狀態的獲取,能夠快速的經過tryAcquire的成功,也就是黃色的Fast路線,也能夠因爲tryAcquire的失敗,構造節點,進入sync隊列中排序後再次獲取。所以能夠理解爲Fast就是一個快速通道,當例子中的線程釋放鎖以後,快速的經過Fast通道再次獲取鎖,就算當前sync隊列中有排隊等待的線程也會被忽略。這種模式,能夠保證進入和退出鎖的吞吐量,可是sync隊列中過早排隊的線程會一直處於阻塞狀態,形成「飢餓」場景。

而公平性鎖,就是在tryAcquire的調用中顧及當前sync隊列中的等待節點(廢棄了Fast通道),也就是任意請求都須要按照sync隊列中既有的順序進行,先到先得。這樣很好的確保了公平性,可是能夠從結果中看到,吞吐量就沒有非公平的鎖高了。

相關文章
相關標籤/搜索