高併發之——淺談AQS中的ReentrantLock、ReentrantReadWriteLock、StampedLock與Condition

ReentrantLock

概述

Java中主要分爲兩類鎖,一類是synchronized修飾的鎖,另一類就是J.U.C中提供的鎖。J.U.C中提供的核心鎖就是ReentrantLock。java

ReentrantLock(可重入鎖)與synchronized區別:

(1)可重入性
兩者都是同一個線程進入1次,鎖的計數器就自增1,須要等到鎖的計數器降低爲0時,才能釋放鎖。markdown

(2)鎖的實現
synchronized是基於JVM實現的,而ReentrantLock是JDK實現的。多線程

(3)性能的區別
synchronized優化以前性能比ReentrantLock差不少,可是自從synchronized引入了偏向鎖,輕量級鎖也就是自旋鎖後,性能就差很少了。併發

(4)功能區別ide

  • 便利性

synchronized使用起來比較方便,而且由編譯器保證加鎖和釋放鎖;ReentrantLock須要手工聲明加鎖和釋放鎖,最好是在finally代碼塊中聲明釋放鎖。工具

  • 鎖的靈活度和細粒度

在這點上ReentrantLock會優於synchronized。性能

ReentrantLock獨有的功能
  • ReentrantLock可指定是公平鎖仍是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先得到鎖。優化

  • 提供了一個Condition類,能夠分組喚醒須要喚醒的線程。而synchronized只能隨機喚醒一個線程,或者喚醒所有的線程ui

  • 提供可以中斷等待鎖的線程的機制,lock.lockInterruptibly()。ReentrantLock實現是一種自旋鎖,經過循環調用CAS操做來實現加鎖,性能上比較好是由於避免了使線程進入內核態的阻塞狀態。atom

synchronized能作的事情ReentrantLock都能作,而ReentrantLock有些能作的事情,synchronized不能作。

在性能上,ReentrantLock不會比synchronized差。

synchronized的優點
  • 不用手動釋放鎖,JVM自動處理,若是出現異常,JVM也會自動釋放鎖。

  • JVM用synchronized進行管理鎖定請求和釋放時,JVM在生成線程轉儲時可以鎖定信息,這些對調試很是有價值,由於它們能標識死鎖或者其餘異常行爲的來源。而ReentrantLock只是普通的類,JVM不知道具體哪一個線程擁有lock對象。

  • synchronized能夠在全部JVM版本中工做,ReentrantLock在某些1.5以前版本的JVM中可能不支持。

ReentrantLock中的部分方法說明
  • boolean tryLock():僅在調用時鎖定未被另外一個線程保持的狀況下才獲取鎖定。

  • boolean tryLock(long, TimeUnit): 若是鎖定在給定的等待時間內沒有被另外一個線程保持,且當前線程沒有被中斷,則獲取這個鎖定。

  • void lockInterruptibly():若是當前線程沒有被中斷,就獲取鎖定;若是被中斷,就拋出異常。

  • boolean isLocked():查詢此鎖定是否由任意線程保持。

  • boolean isHeldByCurrentThread(): 查詢當前線程是否保持鎖定狀態。

  • boolean isFair():判斷是不是公平鎖。

  • boolean hasQueuedThread(Thread):查詢指定線程是否在等待獲取此鎖定。

  • boolean hasQueuedThreads():查詢是否有線程正在等待獲取此鎖定。

  • boolean getHoldCount():查詢當前線程保持鎖定的個數。

代碼示例

示例代碼以下:

package io.binghe.concurrency.example.lock;
 
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class LockExample {
    //請求總數
    public static int clientTotal = 5000;
    //同時併發執行的線程數
    public static int threadTotal = 200;
    public static int count = 0;
    private static final Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++){
            executorService.execute(() -> {
                try{
                    semaphore.acquire();
                    add();
                    semaphore.release();
                }catch (Exception e){
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }
    private static void add(){
        lock.lock();
        try{
            count ++;
        }finally {
            lock.unlock();
        }
    }
}

ReentrantReadWriteLock

概述

在沒有任何讀寫鎖的時候,才能夠取得寫鎖。若是一直有讀鎖存在,則沒法執行寫鎖,這就會致使寫鎖飢餓。

代碼示例

示例代碼以下:

package io.binghe.concurrency.example.lock;
 
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
public class LockExample {
 
    private final Map<String, Data> map = new TreeMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();
 
    public Data get(String key){
        readLock.lock();
        try{
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }
 
    public Set<String> getAllKeys(){
        readLock.lock();
        try{
            return map.keySet();
        }finally {
            readLock.unlock();
        }
    }
 
    public Data put(String key, Data value){
        writeLock.lock();
        try{
            return map.put(key, value);
        }finally {
            writeLock.unlock();
        }
    }
 
    class Data{
 
    }
}

StampedLock

概述

控制鎖三種模式:寫、讀、樂觀讀。

StampedLock的狀態由版本和模式兩個部分組成,鎖獲取方法返回的是一個數字做爲票據,用相應的鎖狀態來表示並控制相關的訪問,數字0表示沒有寫鎖被受權訪問。

在讀鎖上分爲悲觀鎖和樂觀鎖,樂觀讀就是在讀操做不少,寫操做不多的狀況下,能夠樂觀的認爲寫入和讀取同時發生的概率很小。所以,不悲觀的使用徹底的讀取鎖定。程序能夠查看讀取資料以後,是否遭到寫入進行了變動,再採起後續的措施,這樣的改進能夠大幅度提高程序的吞吐量。

總之,在讀線程愈來愈多的場景下,StampedLock大幅度提高了程序的吞吐量。

StampedLock源碼中的案例以下,這裏加上了註釋。

class Point {
	private double x, y;
	private final StampedLock sl = new StampedLock();
 
	void move(double deltaX, double deltaY) { // an exclusively locked method
		long stamp = sl.writeLock();
		try {
			x += deltaX;
			y += deltaY;
		} finally {
			sl.unlockWrite(stamp);
		}
	}
 
	//下面看看樂觀讀鎖案例
	double distanceFromOrigin() { // A read-only method
		long stamp = sl.tryOptimisticRead(); //得到一個樂觀讀鎖
		double currentX = x, currentY = y;  //將兩個字段讀入本地局部變量
		if (!sl.validate(stamp)) { //檢查發出樂觀讀鎖後同時是否有其餘寫鎖發生?
			stamp = sl.readLock();  //若是沒有,咱們再次得到一個讀悲觀鎖
			try {
				currentX = x; // 將兩個字段讀入本地局部變量
				currentY = y; // 將兩個字段讀入本地局部變量
			} finally {
				sl.unlockRead(stamp);
			}
		}
		return Math.sqrt(currentX * currentX + currentY * currentY);
	}
 
	//下面是悲觀讀鎖案例
	void moveIfAtOrigin(double newX, double newY) { // upgrade
		// Could instead start with optimistic, not read mode
		long stamp = sl.readLock();
		try {
			while (x == 0.0 && y == 0.0) { //循環,檢查當前狀態是否符合
				long ws = sl.tryConvertToWriteLock(stamp); //將讀鎖轉爲寫鎖
				if (ws != 0L) { //這是確認轉爲寫鎖是否成功
					stamp = ws; //若是成功 替換票據
					x = newX; //進行狀態改變
					y = newY;  //進行狀態改變
					break;
				} else { //若是不能成功轉換爲寫鎖
					sl.unlockRead(stamp);  //咱們顯式釋放讀鎖
					stamp = sl.writeLock();  //顯式直接進行寫鎖 而後再經過循環再試
				}
			}
		} finally {
			sl.unlock(stamp); //釋放讀鎖或寫鎖
		}
	}
}

代碼示例

示例代碼以下:

package io.binghe.concurrency.example.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.StampedLock;
@Slf4j
public class LockExample {
    //請求總數
    public static int clientTotal = 5000;
    //同時併發執行的線程數
    public static int threadTotal = 200;
 
    public static int count = 0;
 
    private static final StampedLock lock = new StampedLock();
 
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++){
            executorService.execute(() -> {
                try{
                    semaphore.acquire();
                    add();
                    semaphore.release();
                }catch (Exception e){
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }
 
    private static void add(){
	//加鎖時返回一個long類型的票據
        long stamp = lock.writeLock();
        try{
            count ++;
        }finally {
	    //釋放鎖的時候帶上加鎖時返回的票據
            lock.unlock(stamp);
        }
    }
}

咱們能夠這樣選擇使用synchronozed鎖仍是ReentrantLock鎖:

  • 當只有少許競爭者時,synchronized是一個很好的通用鎖實現

  • 競爭者很多,可是線程的增加趨勢是可預估的,此時,ReentrantLock是一個很好的通用鎖實現

  • synchronized不會引起死鎖,其餘的鎖使用不當可能會引起死鎖。

Condition

概述

Condition是一個多線程間協調通訊的工具類,Condition除了實現wait和notify的功能之外,它的好處在於一個lock能夠建立多個Condition,能夠選擇性的通知wait的線程

特色:

  • Condition 的前提是Lock,由AQS中newCondition()方法 建立Condition的對象

  • Condition await方法表示線程從AQS中移除,並釋放線程獲取的鎖,並進入Condition等待隊列中等待,等待被signal

  • Condition signal方法表示喚醒對應Condition等待隊列中的線程節點,並加入AQS中,準備去獲取鎖。

代碼示例

示例代碼以下

package io.binghe.concurrency.example.lock;
 
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class LockExample {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();
 
        new Thread(() -> {
            try {
                reentrantLock.lock();
                log.info("wait signal"); // 1
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("get signal"); // 4
            reentrantLock.unlock();
        }).start();
 
        new Thread(() -> {
            reentrantLock.lock();
            log.info("get lock"); // 2
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll();
            log.info("send signal ~ "); // 3
            reentrantLock.unlock();
        }).start();
    }
}
相關文章
相關標籤/搜索