java併發編程學習(五)

1.讀寫鎖ReadWriteLock

讀寫鎖有以下特點:1.讀鎖之間不互斥,2.寫鎖之間互斥,只能同時有一個寫鎖進行寫操作,3.寫鎖優先,喚醒線程時優先喚醒寫鎖。jdk中的ReadWriteLock就是讀寫鎖,ReentrantReadWriteLock是ReadWriteLock接口的一個實現類。

ReentrantReadWriteLock除了讀寫鎖的特性以外還有以下的特點:1.支持公平鎖和非公平鎖,2.可重入,讀鎖可以再次獲取讀鎖,寫鎖可以再次獲得寫鎖,3.支持鎖降級,寫鎖可以獲取讀鎖(不支持讀鎖升級爲寫鎖,4.支持響應中斷的方式加鎖,5.支持Condition

寫鎖和讀鎖之間互斥:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyReadWriteLock {
	public static void main(String[] args) throws InterruptedException {
		final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//默認爲非公平鎖 構造方法中可以增加布爾型參數true爲公平鎖,false爲非公平鎖
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				readWriteLock.writeLock().lock();
				System.out.println("線程1獲取寫鎖");
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("線程1釋放寫鎖");
				readWriteLock.writeLock().unlock();
			}
		}).start();
		Thread.sleep(1000);
		readWriteLock.readLock().lock();
		System.out.println("線程2獲取讀鎖");
		System.out.println("線程2釋放讀鎖");
		readWriteLock.readLock().unlock();
	}
}

剩餘三種讀鎖和讀鎖不互斥、寫鎖和寫鎖互斥、讀鎖和寫鎖互斥不再舉例。

鎖降級

在獲取寫鎖的時候可以獲取讀鎖,然後將寫鎖釋放,這樣就降級成了讀鎖

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyReadWriteLock2 {
	public static void main(String[] args) throws InterruptedException {
		final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
		System.out.println("獲取寫鎖");
		readWriteLock.writeLock().lock();
		System.out.println("獲取讀鎖");
		readWriteLock.readLock().lock();
		System.out.println("釋放寫鎖");
		readWriteLock.writeLock().unlock();
	}
}

獲取讀鎖後不能再獲取寫鎖,所以不支持讀鎖升級爲寫鎖。

ReadWriteLock繼承了Lock,Condition和可響應中斷的加鎖與Lock一致。

2.AQS(AbstractQueuedSynchronizer)

AQS中維護了一個先進先出(FIFO)隊列,線程要獲取鎖會被加入到隊列中,直到獲取鎖之後被移除。

AQS中的state是一個很重要的變量,我個人理解它代表的是獨佔方式和共享方式分別佔用的資源數。AQS有兩種資源獲取方式:Exclusive(獨佔方式)和Shared(共享方式)。獨佔方式獲取資源的方法時acquire(int arg),共享方式獲取資源的方法是acquireShared(int args),先看獨佔模式的源碼:

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

這個方法中先嚐試獲取鎖tryAcquire(arg),如果不成功將當前線程加入嘗試獲取鎖的隊列。AQS中沒有實現tryAcquire方法,而是將它交給子類來實現。這裏以ReentrantReadWriteLock中的內部類Sync爲例,看tryAcquire是怎麼實現的。Sync繼承了AQS。源碼如下:

        protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

首先拿到AQS的state,執行exclusiveCount(c)並賦值給w,exclusiveCount是計算state中的資源被獨佔的數量,在Sync中是這樣實現獲取state中代表的獨佔和共享資源的數量的:

        /*
         * Read vs write count extraction constants and functions.
         * Lock state is logically divided into two unsigned shorts:
         * The lower one representing the exclusive (writer) lock hold count,
         * and the upper the shared (reader) hold count.
         */

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shared holds represented in count  */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** Returns the number of exclusive holds represented in count  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

在Sync中,state的高16位代表着共享資源被佔用的數量,低16位代表獨佔資源被佔用的數量。SHARED_SHIFT是常量16,EXCLUSIVE_MASK是1帶符號左移16位減一,1帶符號左移16位就是第十七位爲1其餘爲0的正數,再減一就是後16位爲1的正數。exclusiveCount方法是將state與EXCLUSIVE_MASK做邏輯與運算,EXCLUSIVE_MASK高16位爲0,那麼結果的高16位就是0,EXCLUSIVE_MASK低16位爲1,那麼結果的低16位就是state的低16位,也就是這個方法將state的高16位置0,取的是低16位代表的獨佔資源被佔用的數量。sharedCount是將state無符號右移16位,取的就是state的高16位代表的共享資源被佔用的數量。

接着看Sync中的tryAcquire方法,如果state不爲0,並且w爲0,那麼就返回false,因爲w是state的後16位,w爲0,state不爲0,那麼state的前16位就不爲0,也就是讀寫鎖的讀鎖被佔用,那麼獲取寫鎖就無法獲取的實現原理。如果state不爲0,w不爲0,並且獲取獨佔資源的線程不是當前線程,也會返回false,這裏判斷是否當前線程就是爲了可重入,如果tryAcquire的數量和w加起來大於MAX_COUNT(MAX_COUNT在上面的代碼中表示2的16次方減一,資源的總數),超出了資源數量會拋出異常,沒有超出並且還是同一個線程獲取的寫鎖,那麼就將state的數量加上tryAcquire中參數的數量,並返回true,這就是讀寫鎖中寫鎖之間互斥的實現原理。如果c等於0,說明當前沒有線程獲取了資源,那麼將state再與c比較一次(防止這時候state改變了)並將state值設置爲c+acquires。最後將資源的線程擁有者設置爲當前線程(爲了之後判斷可重入)。

再回到AQS中的acquire方法,如果tryAcquire獲取成功了,那麼就結束,沒有獲取成功,會執行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先看addWaiter方法:

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

這個方法先以當前線程創建一個Node對象。Node是AQS中的一個內部類,Node類中有兩個Node類型的成員變量prev和next,分別是Node的前一個節點和後一個節點。AQS中有head和tail兩個Node類型的成員變量,分別是頭節點和尾節點。由Node的結構和AQS中定義的head和tail使AQS中形成了一個雙向的鏈表。這個雙向鏈表就是AQS中等待獲取資源的線程隊列。

在addWaiter方法中,獲取當前AQS的tail,如果不爲空,將這個Node設爲tail並設置原來tail的下一個節點和當前Node的前一個節點,並返回新的尾節點。如果AQS的尾節點爲空,那麼執行enq(node)方法,enq方法是將節點插入到隊列中,如果尾節點爲空就初始化這個隊列。

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

addWaiter方法就是將當前線程的Node塞到AQS的隊列的尾部並返回這個尾節點。在acquire方法中將addWaiter方法的返回值作爲參數執行acquireQueued方法:

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                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);
        }
    }

在acquireQueued中首先獲取當前節點的前一個節點,如果不是頭節點就循環一直獲取並判斷是不是頭節點。如果是頭節點就嘗試獲取鎖,獲取失敗就還是一直循環,直到獲取成功之後將當前節點設置爲頭節點,也就是說只有在當前節點的前一個是頭節點並且頭節點已經釋放鎖才能獲取到資源。因此,AQS保證了是從頭節點到尾節點一個個地獲取到資源的,也就是說AQS是一個先進先出(FIFO)的雙向鏈表。

至此AQS的acquire方法就已經看完了。總結來說acquire方法就是獲取鎖,並將當前線程構造的Node添加到AQS隊列的尾部。在ReentrantReadWriteLock中,就是使用繼承了AQS的Sync類來實現讀寫鎖的加鎖。

下面我們看release方法,這是用來釋放資源的方法,也就是鎖用來釋放鎖的實現原理:

    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

在這個方法中,先執行tryRelease嘗試釋放鎖,tryRelease方法也和tryAcquire方法一樣交由子類實現,這裏不再舉例說明。如果嘗試獲取成功就將當前的頭節點執行unparkSuccessor方法,,在unparkSuccessor方法中如果當前node的下一個node爲空,就從尾部開始找沒有被取消的node,並將這個node鎖住。

    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

當前節點釋放鎖之後,AQS中的隊列中的node一直在循環獲取鎖,其中頭節點的下個節點就能獲取到。

共享方式獲取和釋放資源分別是tryRelease和tryAcquire方法,實現也是類似,不再做介紹。

3.自旋鎖

一般來說,獲取鎖的時候如果鎖已經被佔用,那麼有兩種方式來一直等待鎖釋放,一種是阻塞等待喚醒,一種是不斷循環地判斷是否可以獲取鎖,第二種就是自旋鎖。

自旋鎖的優點是不需要由阻塞狀態切換到活躍狀態,鎖的效率高,切換快。缺點是佔用cpu多,消耗大。本文中的acquireQueued方法、enq方法都是通過自旋鎖實現的。

 

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