JUC鎖——可重入互斥鎖ReentrantLock

ReentrantLock介紹

ReentrantLock是一個可重入的互斥鎖,又被稱爲“獨佔鎖”。顧名思義,ReentrantLock鎖在同一個時間點只能被一個線程所持有;而可重入的意思是,ReentrantLock鎖可以被單個線程多次獲取。ReentrantLock分爲“公平鎖”和“非公平鎖”,它們的區別體現在獲取鎖的機制上是否公平。“鎖”是爲了保護競爭資源,防止多個線程同時操作同一個資源而出錯,ReentrantLock在同一個時間點只能被一個線程獲取(當某線程獲取到“鎖”時,其它線程就必須等待),ReentraantLock是通過一個FIFO(先進先出)的等待隊列來管理需要獲取該鎖的所有線程的。在“公平鎖”的機制下,線程依次排隊獲取鎖;而“非公平鎖”在鎖是可獲取狀態時,不管自己是不是在隊列的開頭都會獲取鎖

ReentrantLock方法列表
// 創建一個 ReentrantLock ,默認是“非公平鎖”
ReentrantLock()
// 創建策略是fair的 ReentrantLock,fair爲true表示是公平鎖,fair爲false表示是非公平鎖
ReentrantLock(boolean fair)
// 查詢當前線程保持此鎖的次數
int getHoldCount()
// 返回目前擁有此鎖的線程,如果此鎖不被任何線程擁有,則返回 null
protected Thread getOwner()
// 返回一個 collection,它包含可能正等待獲取此鎖的線程
protected Collection<Thread> getQueuedThreads()
// 返回正等待獲取此鎖的線程估計數
int getQueueLength()
// 返回一個 collection,它包含可能正在等待與此鎖相關給定條件的那些線程
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待與此鎖相關的給定條件的線程估計數
int getWaitQueueLength(Condition condition)
// 查詢給定線程是否正在等待獲取此鎖
boolean hasQueuedThread(Thread thread)
// 查詢是否有些線程正在等待獲取此鎖
boolean hasQueuedThreads()
// 查詢是否有些線程正在等待與此鎖有關的給定條件
boolean hasWaiters(Condition condition)
// 如果是“公平鎖”返回true,否則返回false
boolean isFair()
// 查詢當前線程是否保持此鎖
boolean isHeldByCurrentThread()
// 查詢此鎖是否由任意線程保持
boolean isLocked()
// 獲取鎖
void lock()
// 如果當前線程未被中斷,則獲取鎖
void lockInterruptibly()
// 返回用來與此 Lock 實例一起使用的 Condition 實例
Condition newCondition()
// 僅在調用時鎖未被另一個線程保持的情況下,才獲取該鎖
boolean tryLock()
// 如果鎖在給定等待時間內沒有被另一個線程保持,且當前線程未被中斷,則獲取該鎖
boolean tryLock(long timeout, TimeUnit unit)
// 試圖釋放此鎖
void unlock()
ReentrantLock示例

示例1:

// 倉庫
public class Depot {
	private int size;// 當前庫存
	private Lock lock;

	public Depot() {
		this.size = 0;
		this.lock = new ReentrantLock();// 默認非公平
	}

	// 生產
	public void produce(int val) {
		System.out.println(Thread.currentThread().getName() + "進入生產");
		lock.lock();
		try {
			Thread.sleep(2000);
			System.out.println(Thread.currentThread().getName() + "生產:" + val + "開始");
			size += val;
			System.out.println(Thread.currentThread().getName() + "生產:" + val + "結束,庫存:" + size);
		} catch (Exception e) {

		} finally {
			lock.unlock();
		}
	}

	// 消費
	public void consume(int val) {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "消費:" + val + "開始");
			Thread.sleep(2000);
			size -= val;
			System.out.println(Thread.currentThread().getName() + "消費:" + val + "結束,庫存:" + size);
		} catch (Exception e) {

		} finally {
			lock.unlock();
		}
	}

}

class Producer {
	private Depot depot;

	public Producer(Depot depot) {
		this.depot = depot;
	}

	// 生產
	public void produce(final int val) {
		new Thread() {
			public void run() {
				depot.produce(val);
			}
		}.start();
	}
}

class Customer {
	private Depot depot;

	public Customer(Depot depot) {
		this.depot = depot;
	}

	// 消費
	public void consume(final int val) {
		new Thread() {
			public void run() {
				depot.consume(val);
			}
		}.start();
	}
}

// 測試
public class MainTest {

	public static void main(String args[]) {
		Depot depot = new Depot();
		Producer p = new Producer(depot);
		Customer c = new Customer(depot);

		p.produce(60);
		p.produce(120);
		c.consume(90);
		c.consume(150);
		p.produce(110);
	}
}

結果

Thread-0進入生產
Thread-1進入生產
Thread-4進入生產
Thread-0生產:60開始
Thread-0生產:60結束,庫存:60
Thread-1生產:120開始
Thread-1生產:120結束,庫存:180
Thread-2消費:90開始
Thread-2消費:90結束,庫存:90
Thread-3消費:150開始
Thread-3消費:150結束,庫存:-60
Thread-4生產:110開始
Thread-4生產:110結束,庫存:50

分析
1、在測試代碼中調用了3次produce生產的方法,2次consume消費的方法,每個方法都新開一個線程,但是produce和consume這兩個方法使用的是同一個ReentrantLock鎖
2、從結果中可以看出來,線程可以同時進入上鎖之前(調用lock.lock()之前)的代碼區域,但同一把鎖上鎖之後到解鎖之前(調用lock.unlock()之前)的這段代碼則只能同時有一個線程在執行,這一點從每個生產線程的開始和該線程生產的結束相鄰(消費線程也是如此)中可以看出來,這也說明ReentrantLock是互斥鎖

示例2:

public class Depot {
	private int size;// 當前庫存

	public Depot() {
		this.size = 0;
	}

	// 生產
	public void produce(int val) {
		try {
			Thread.sleep(1000);
			size += val;
			System.out.println(Thread.currentThread().getName() + "生產:" + val + "結束,庫存:" + size);
		} catch (Exception e) {

		}
	}

	// 消費
	public void consume(int val) {
		try {
			Thread.sleep(2000);
			size -= val;
			System.out.println(Thread.currentThread().getName() + "消費:" + val + "結束,庫存:" + size);
		} catch (Exception e) {

		}
	}

}

class Producer {
	private Depot depot;

	public Producer(Depot depot) {
		this.depot = depot;
	}

	// 生產
	public void produce(final int val) {
		new Thread() {
			public void run() {
				depot.produce(val);
			}
		}.start();
	}
}

class Customer {
	private Depot depot;

	public Customer(Depot depot) {
		this.depot = depot;
	}

	// 消費
	public void consume(final int val) {
		new Thread() {
			public void run() {
				depot.consume(val);
			}
		}.start();
	}
}

// 測試
public class MainTest {

	public static void main(String args[]) {
		Depot depot = new Depot();
		Producer p = new Producer(depot);
		Customer c = new Customer(depot);

		p.produce(60);
		p.produce(120);
		c.consume(90);
		c.consume(150);
		p.produce(110);
	}
}

結果:

Thread-4生產:110結束,庫存:180
Thread-0生產:60結束,庫存:180
Thread-1生產:120結束,庫存:180
Thread-3消費:150結束,庫存:-60
Thread-2消費:90結束,庫存:-60

分析:
按照我們的測試,共生產了60+120+110=290,消費了90+150=240,最後應該剩餘50,但所有的生產和消費執行完之後的結果卻是-60,這就是線程問題,示例2是將示例1中的鎖去除了,這樣就無法保證多個線程在訪問size這個共享資源的時候是互斥的,這時候就會出現數據髒讀的問題,A線程剛獲取到size的值,但是B線程卻將size的值改變了,而A線程並沒有發覺,導致A線程和B線程都是在另外一個線程修改數據之前修改的size值,這是不正確的,導致最後的結果是不正確,究其原因就是我們沒有對size實現互斥訪問
示例1的問題
1、現實中,倉庫的容量不可能爲負數。但是,此模型中的倉庫容量可以爲負數,這與現實相矛盾
2、現實中,倉庫的容量是有限制的。但是,此模型中的容量確實沒有限制的

示例3:
通過Condition去解決示例1中的兩個問題,Condition需要和Lock聯合使用:通過Condition中的await()方法能讓線程阻塞(類似於wait()),通過Condition的signal()方法能喚醒線程(類似於notify())

public class Depot {
	private int capacity;// 倉庫容量
	private int size;// 實際數量
	private Lock lock;
	private Condition fullCondition;// 生產條件
	private Condition emptyCondition;// 消費條件

	public Depot(int capacity) {
		this.capacity = capacity;
		this.size = 0;
		// 互斥鎖
		this.lock = new ReentrantLock();
		this.fullCondition = lock.newCondition();
		this.emptyCondition = lock.newCondition();
	}

	public void produce(int val) {
		lock.lock();
		try {
			int left = val;
			while (left > 0) {
				while (size >= capacity)
					fullCondition.await();// 滿了即停止生產
				// 最多應生產數量
				int inc = (size + left) > capacity ? (capacity - size) : left;
				size += inc;
				left -= inc;
				System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n", Thread.currentThread().getName(),
						val, left, inc, size);
				// 通知消費
				emptyCondition.signal();
			}
		} catch (Exception e) {
		} finally {
			lock.unlock();
		}
	}

	public void consume(int val) {
		lock.lock();
		try {
			int left = val;
			while (left > 0) {
				while (size <= 0)
					emptyCondition.await();// 空了即停止消費
				// 最多能消費數量
				int dec = (size < left) ? size : left;
				size -= dec;
				left -= dec;
				System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n", Thread.currentThread().getName(),
						val, left, dec, size);
				// 通知生產
				fullCondition.signal();
			}
		} catch (Exception e) {
		} finally {
			lock.unlock();
		}
	}

	public String toString() {
		return "capacity:" + capacity + ", actual size:" + size;
	}
}

// 生產者
class Producer {
	private Depot depot;

	public Producer(Depot depot) {
		this.depot = depot;
	}

	public void produce(final int val) {
		new Thread() {
			public void run() {
				depot.produce(val);
			}
		}.start();
	}
}

// 消費者
class Consumer {
	private Depot depot;

	public Consumer(Depot depot) {
		this.depot = depot;
	}

	public void consume(final int val) {
		new Thread() {
			public void run() {
				depot.consume(val);
			}
		}.start();
	}
}

// 測試
public class MainTest {

	public static void main(String args[]) {
		Depot depot = new Depot(100);
		Producer p = new Producer(depot);
		Consumer c = new Consumer(depot);
		c.consume(90);
		p.produce(60);
        p.produce(120);
        c.consume(150);
        p.produce(110);
	}
}

// 結果
Thread-2 produce(120) --> left= 20, inc=100, size=100
Thread-0 consume( 90) <-- left=  0, dec= 90, size= 10
Thread-2 produce(120) --> left=  0, inc= 20, size= 30
Thread-3 consume(150) <-- left=120, dec= 30, size=  0
Thread-1 produce( 60) --> left=  0, inc= 60, size= 60
Thread-3 consume(150) <-- left= 60, dec= 60, size=  0
Thread-4 produce(110) --> left= 10, inc=100, size=100
Thread-3 consume(150) <-- left=  0, dec= 60, size= 40
Thread-4 produce(110) --> left=  0, inc= 10, size= 50

分析
  由結果可知,解決了示例1中的兩個問題,但這裏有個疑問:生產和消費的方法中,都使用到了兩個Condition對象——fullCondition和emptyCondition,看起來這兩個Condition對象並沒有和線程之間(或者說生產者和消費者之間)有關聯關係,都是通過lock.newCondition()獲取的,僅僅是名字不同而已,拿生產方法produce來說,在檢測到滿了即調用fullCondition.await()停止生產,一旦有生產又調用emptyCondition.signal()去喚醒消費線程消費,那爲什麼調用fullCondition.await()方法阻塞的是生產線程而不是消費線程,同樣地爲什麼調用emptyCondition.signal()喚醒的是消費線程而非生產線程呢?這兩個線程之間唯一的聯繫就是lock對象,在生產和消費方法中使用的是同一個lock對象,而fullCondition和emptyCondition又都是由這個lock對象通過newCondition方法獲取到的,因此應該是當調用condition對象的await()方法時會阻塞當前獲取到lock對象的線程,當調用condition對象的signal()方法時會喚醒使用該condition對象阻塞的線程,雖然此處的condition對象可以只使用一個,如下例子,看似沒有影響結果,實際上會有一些影響。

示例4:

public class Depot {
	private int capacity;// 倉庫容量
	private int size;// 實際數量
	private Lock lock;
	private Condition condition;// 條件

	public Depot(int capacity) {
		this.capacity = capacity;
		this.size = 0;
		// 互斥鎖
		this.lock = new ReentrantLock();
		this.condition = lock.newCondition();
	}

	public void produce(int val) {
		lock.lock();
		try {
			int left = val;
			while (left > 0) {
				while (size >= capacity)
					condition.await();// 滿了即停止生產
				// 最多應生產數量
				int inc = (size + left) > capacity ? (capacity - size) : left;
				size += inc;
				left -= inc;
				System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n", Thread.currentThread().getName(),
						val, left, inc, size);
				// 通知消費
				condition.signal();
			}
		} catch (Exception e) {
		} finally {
			lock.unlock();
		}
	}

	public void consume(int val) {
		lock.lock();
		try {
			int left = val;
			while (left > 0) {
				while (size <= 0)
					condition.await();// 空了即停止消費
				// 最多能消費數量
				int dec = (size < left) ? size : left;
				size -= dec;
				left -= dec;
				System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n", Thread.currentThread().getName(),
						val, left, dec, size);
				// 通知生產
				condition.signal();
			}
		} catch (Exception e) {
		} finally {
			lock.unlock();
		}
	}

	public String toString() {
		return "capacity:" + capacity + ", actual size:" + size;
	}
}

// 生產者
class Producer {
	private Depot depot;

	public Producer(Depot depot) {
		this.depot = depot;
	}

	public void produce(final int val) {
		new Thread() {
			public void run() {
				depot.produce(val);
			}
		}.start();
	}
}

// 消費者
class Consumer {
	private Depot depot;

	public Consumer(Depot depot) {
		this.depot = depot;
	}

	public void consume(final int val) {
		new Thread() {
			public void run() {
				depot.consume(val);
			}
		}.start();
	}
}

  測試和結果與示例3看似相同,但是其實是不同的。在阻塞的時候和示例3是一樣的,都是阻塞的當前獲取到lock鎖的線程;但是喚醒時就會有些不同,因爲這兩個方法中的喚醒使用的condition對象和阻塞是同一個,那麼在喚醒時每次都會同時將生產和消費這兩個線程都喚醒,而不是像示例3中的在生產線程中喚醒消費線程,在消費線程中喚醒生產線程。結論就是調用通過某一個Lock對象的newConditon()方法產生的所有Condition對象的await()方法都會使當前獲取到該Lock對象的線程阻塞,調用通過某一個Lock對象的newCondition()方法產生的所有Condition對象的signal()方法會喚醒所有使用該Condition對象阻塞的線程(該線程肯定也使用lock作爲鎖)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章