【java併發編程】ReentrantLock源碼分析

1.爲什麼使用鎖,不使用鎖會有什麼影響?

public class Test {
    public static int count=0;
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        for(int i = 0 ;i<10000;i++){
            new Thread(()->{
                try{
                    lock.lock();
    				//count++不是原子操作
                    count++;
                }finally {
                    //unlock一定要在finally中,避免死鎖
                    lock.unlock();
                }
            }).start();
        }
        try {
            Thread.sleep(2000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

​ 當count++lock.lock()/lock.unlock()中間時,輸出的count就是想要得到的結果;

​ 不加lock的時候多執行幾次,經常會比預期值小。因爲count++不是原子操作,這裏就體現了lock的作用。

2.ReentrantLock源碼分析

2.1思考實現原理

ReentrantLock作用如此強大,他主要完成了幾個功能點。

  1. 記錄鎖是否被佔用
  2. 記錄佔用鎖的線程(處理鎖重入)
  3. 通過某種方式記錄鎖的順序(要考慮公平鎖)
  4. 所有的鎖進入阻塞狀態。

大概完成了這麼幾個功能點。如此就可以主要考慮ReentrantLock是如何實現這些功能的。

ReentrantLock類圖
在這裏插入圖片描述
​ 如此所示,很明顯,ReentrantLock有三個內部類,公平鎖類FairSync和非公平鎖類NonfairSync而且這兩個類都繼承了Sync類,重寫了Sync的lock方法。

Sync類繼承AbstractQueuedSynchronizer類,AbstractQueuedSynchronizer類通常說AQS,實現線程排隊阻塞的一個機制。AQS主要是維護了互斥變量,維護雙向鏈表,線程阻塞等。

new ReentrantLock()是非公平鎖,new ReentrantLock(true)是公平鎖。

ReentrantLock構造器源碼:

    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

2.2ReentrantLock源碼

在這裏插入圖片描述

2.2.1 NonfairSync.lock()方法

ReentrantLock 中有抽象類Sync,根據構造方法區分執行FairSync還是NonfairSync的實現。

NonfairSync實現爲例。

  final void lock() {
	//在state預期值爲0的時候,修改值爲1,此方法線程安全
      if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

compareAndSetState方法類似數據庫中的樂觀鎖,預計值爲0的時候修改值爲1,如果修改成功,則成功獲取鎖,返回true,並通過setExclusiveOwnerThread方法記錄獲取鎖的線程;如果修改失敗,說明鎖已經被佔用,通過acquire(1)方法嘗試獲得鎖。

2.2.2 compareAndSetState(0,1)方法

 protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
     //如果內存中的值爲expect則修改爲update,修改成功返回true,修改失敗返回false
     return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    // stateOffset 等於state的值
    //state=0表示沒有線程獲取鎖,state=1表示有線程獲取鎖 state>1表示鎖重入
  stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));

compareAndSwapInt屬於CAS相關(樂觀鎖。ConcurrentHashMapConcurrentLinkedQueue也有用CAS來實現樂觀鎖),Unsafe類提供了手動管理內容的能力,可以直接對內容進行處理。

​ 至此,已經瞭解了ReentrantLock通過AbstractQueuedSynchronizer.state判斷是否佔用鎖,state=0表示沒有線程獲取鎖,state=1表示有線程獲取鎖 state>1表示鎖重入;

​ 通過AbstractOwnableSynchronizer.exclusiveOwnerThread記錄佔用鎖的線程。

2.2.3 acquire(1)方法

    public final void acquire(int arg) {
        //判斷是否是重入鎖
        if (!tryAcquire(arg) &&
            //addWaiter加入隊列
            //acquireQueued線程阻塞
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //響應線程中斷
            selfInterrupt();
    }

2.2.4 tryAcquire(arg)方法

  final boolean nonfairTryAcquire(int acquires) {
      		// 獲取當前線程
            final Thread current = Thread.currentThread();
     		 // 通過state狀態
            int c = getState();
      		// 如果沒有線程獲取鎖
            if (c == 0) {
                //通過CAS操作獲取鎖,如果獲取成功,則設置佔用鎖的線程
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
      		// 判斷是否是鎖重入getExclusiveOwnerThread 方法返回佔用鎖的線程
            else if (current == getExclusiveOwnerThread()) {
                //state++
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //設置state的值
                setState(nextc);
                return true;
            }
      		//如果鎖已經被其他線程佔用,返回false
            return false;
        }

2.2.5 addWaiter(Node.EXCLUSIVE)方法

既然獲取鎖失敗,那麼就要將線程記錄起來,如下所示,通過鏈表的形式將所有線程保存起來(通過附錄查看Node屬性)。

   /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */ 
// mode 傳入Node.EXCLUSIVE 表示互斥鎖   Node.SHARED 表示共享鎖(讀寫鎖中的讀鎖)
	private Node addWaiter(Node mode) {
    	// 創建一個節點保存當前線程
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
    	//獲取節點的尾節點。
    	//如果是線程B是第一個阻塞的節點,這裏是空值,通過enq方法進行設置head節點和tail節點。
        Node pred = tail;
        if (pred != null) {
            //如果線程C在線程B之後,線程C執行到這裏,那麼這裏的pred是B
            node.prev = pred;
            //這時C爲最後一個節點,設置尾節點爲C
            if (compareAndSetTail(pred, node)) {
                //如果設置成功,設置B節點的next節點爲C
                pred.next = node;
                return node;
            }
        }
    	//第一個阻塞的線程進入這裏
    	//當然這裏可能B和C同時進入enq方法
        enq(node);
        return node;
    }

2.2.5 enq(node)方法

//這裏可能很多線程一起進入。
private Node enq(final Node node) {
    //for(;;)和while(true)效果是一樣的,但是一般是用for(;;)因爲指令少
        for (;;) {
            //獲取尾節點
            Node t = tail;
            if (t == null) { // Must initialize
                //這裏又是CAS操作,預計head節點爲空時,修改值爲new Node()
                //不管幾個線程執行此方法,但是隻有一個線程能執行成功。也就是第一個阻塞的線程
                if (compareAndSetHead(new Node()))
                    //創建頭節點成功之後,因爲此時只有一個節點,所以這個節點即使頭節點也是尾節點。
                    tail = head;
            } else {
                //如果只有B線程進入enq方法,第一次循環設置頭節點,尾節點。
              	//第二次循環將線程B設置爲尾節點,並且把頭節點的next節點設置程B
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

這裏比較複製,如下所示:
在這裏插入圖片描述
通過A,B,C同時執行lock.lock()方法,A最快,成功獲取鎖,線程B,C阻塞;

​ B和C都會執行addWaiter方法,假如B的速度比C的速度快,而且在B已經執行完addWaiter方法之後,C才進入addWaiter方法;

​ 那麼B會執行到enq方法中進行for循環,第一次循環創建頭節點,尾節點,第二次循環將B節點和頭節點進行關聯;
在這裏插入圖片描述

2.2.6 acquireQueued方法

// node爲當前線程節點的前一個節點
// arg爲1,表示想要修改state=1進行搶佔鎖
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 獲取當前節點的prev節點
                final Node p = node.predecessor();
                //如果當前節點的prev節點是head節點,證明當前線程馬上就要獲取到鎖。
                // tryAcquire 方法與2.2.4方法效果一致
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //判斷是否阻塞線程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                // 清理狀態
                cancelAcquire(node);
        }
    }

2.2.7 shouldParkAfterFailedAcquire方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
    	// SIGNAL狀態說明線程準備好阻塞,等待喚醒
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            // wx>0說明放棄獲取鎖,通過循環將這些節點從鏈表中剔除。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //通過CAS嘗試修改pred狀態爲SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

2.2.8 parkAndCheckInterrupt方法

    private final boolean parkAndCheckInterrupt() {
        //阻塞當前線程 this表示當前線程
        LockSupport.park(this);
        // 清理中斷狀態
        return Thread.interrupted();
    }

附錄:

AbstractQueuedSynchronizer與Node的關係。

 public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    	    /**
    			省略其他代碼
 		   **/
 		   //記錄頭節點
 	      private transient volatile Node head;
 	      //記錄尾節點
          private transient volatile Node tail;
          //記錄鎖狀態
          private volatile int state;
          static final class Node {
          	//記錄前一個節點
             volatile Node prev;
             // 記錄後一個節點
             volatile Node next;
             //取消獲取鎖
             static final int CANCELLED =  1;
             //準備好阻塞,等待線程喚醒
             static final int SIGNAL    = -1;
             // 指示線程正在等待條件
             static final int CONDITION = -2;
             // 共享鎖中用到
             static final int PROPAGATE = -3;
          }
    }

數據庫樂觀鎖簡單展示:

場景:商品表中有有一個商品,庫存爲1,如果有N個線程同時查到還有一個商品,進行下單,就會有超賣等現象,必須加鎖進行控制。

id goodsname goodsnum version
1 手機 1 1
  1. 線程A,B,C同時查到商品表中查到還有手機一部。
  2. 三個線程減庫存操作。
  3. 線程A通過執行 update t_goods set goodsnum=goodsnum-1,version=version+1 where id=1 and version=1 and goodsnum>0 修改成功。這時線程B,C修改的時候version已經變成了2,不滿足where條件,所以修改失敗。
  4. 線程A修改成功,可以進行支付操作,線程B,C修改庫存失敗,報異常說明庫存不足。

LockSupport.park/unpark

​ 關於線程阻塞並喚醒,最快會想到wait/notify組合。但是在此場景有缺陷,notify只能隨機喚醒一個線程,notifyAll喚醒所有線程,park/unpark可以精準的實現喚醒某一個線程。

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