1. 程式人生 > >Java併發程式設計——ReentrantLock重入鎖解析

Java併發程式設計——ReentrantLock重入鎖解析

重入鎖

所謂重入鎖,即支援重入性,表示能夠對共享資源重複加鎖,即當前執行緒獲取該鎖再次獲取不會被阻塞。

重入性

線上程獲取鎖的時候,如果已經獲取鎖的執行緒是當前執行緒的話則直接再次獲取成功; 由於鎖會被獲取n次,那麼只有鎖在被釋放同樣的n次之後,該鎖才算是完全釋放成功。

ReentrantLock分為公平鎖與非公平鎖,預設選擇的是非公平鎖(無參建構函式),另一種構造方式,可傳入一個boolean值。true時為公平鎖。

以非公平鎖為例,來看看它是如何獲取鎖。核心方法為其內部的抽象靜態類Sync中的nonfairTryAcquire

/**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //state為0表示該鎖沒有被任何執行緒佔有,可以被獲取
            if (c == 0) {
                //如果執行緒當前狀態值accquires為0
                if (compareAndSetState(0, acquires)) {
                   //設定當前擁有獨佔訪問許可權的執行緒。
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果執行緒被佔有,檢查佔有執行緒是否為當前執行緒
            else if (current == getExclusiveOwnerThread()) {
                //再次獲取鎖,計數加1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //如果被其他執行緒佔有,則獲取鎖失敗
            return false;
        }

每次重新獲取鎖都會對同步狀態加1操作,那麼釋放鎖相對的,就會進行減一操作,釋放鎖核心方法tryRelease

protected final boolean tryRelease(int releases) {
             //同步狀態減1
            int c = getState() - releases;
           //如果當前執行緒,不是鎖佔有執行緒,丟擲異常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //只有當同步狀態c等於0,鎖才能被成功釋放。
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //c不等於0,鎖釋放失敗 返回false
            setState(c);
            return free;
        }

每次鎖重新獲取都會對同步狀態進行加一的操作,同樣釋放鎖tryRelease執行減一操作,必須等到同步狀態為0時,鎖才算成功釋放。也就是鎖被獲取了n次,只有釋放n次才算成功釋放。

公平鎖與非公平鎖

ReentrantLock支援兩種鎖,公平鎖與非公平鎖。
ReentrantLock的建構函式可以傳遞一個boolean值,true時為公平鎖,false為非公平鎖。

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

所謂公平鎖,就是鎖的獲取順序符合請求上的絕對時間順序,滿足FIFO(先入先出)。

ReentrantLock還有無參建構函式,為非公平鎖。

public ReentrantLock() {
        sync = new NonfairSync();
    }

上面講了非公平鎖的nonfairTryAcquire方法,這裡看看公平鎖的獲取邏輯,tryAcquire方法。

 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;
        }
    }

上述程式碼可以看出,與非公平鎖的區別就在多了一個hasQueuedPredecessors()的判斷。該方法是用來判斷當前節點在佇列中是否有前驅節點。如果有,說明有執行緒比當前執行緒更早的請求資源,根據先入先出原則,所以當前執行緒請求鎖失敗。

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        //當前佇列不為空,當前節點不是頭節點,當前節點的執行緒不是當前執行緒
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

公平鎖&非公平鎖

公平鎖每次獲取到鎖為同步佇列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的執行緒下次繼續獲取該鎖,則有可能導致其他執行緒永遠無法獲取到鎖,造成“飢餓”現象。

公平鎖為了保證時間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會降低一定的上下文切換,降低效能開銷。因此,ReentrantLock預設選擇的是非公平鎖,則是為了減少一部分上下文切換,保證了系統更大的吞吐量。

一個小栗子

package com.lw.study.thread;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: 
 * @Date: 16:46 2018/8/30
 */
public class LockDemo extends Thread{
    private static ReentrantLock lock = new ReentrantLock();
    private static int count = 0;

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

    public static void main(String[] args) throws InterruptedException {
        LockDemo lockDemo = new LockDemo();
        LockDemo lockDemo2 = new LockDemo();
        lockDemo.start();
        lockDemo2.start();
        lockDemo.join();
        lockDemo2.join();
        System.out.println(count);
    }
}

輸出結果

hreadName : Thread-0 : 0
ThreadName : Thread-0 : 1
ThreadName : Thread-0 : 2
ThreadName : Thread-0 : 3
ThreadName : Thread-0 : 4
ThreadName : Thread-1 : 5
ThreadName : Thread-1 : 6
ThreadName : Thread-1 : 7
ThreadName : Thread-1 : 8
ThreadName : Thread-1 : 9
10

可以看到,在Thread-0執行完後,Thread-1才開始執行。如果不加鎖,結果將是兩個執行緒交替隨機執行。

ThreadName : Thread-0 : 0
ThreadName : Thread-0 : 1
ThreadName : Thread-1 : 0
ThreadName : Thread-0 : 2
ThreadName : Thread-1 : 3
ThreadName : Thread-0 : 4
ThreadName : Thread-1 : 5
ThreadName : Thread-0 : 6
ThreadName : Thread-1 : 7
ThreadName : Thread-1 : 9
10