[JDK] 多線程高併發探祕之「鎖」

情人節快樂!

[JDK] 多線程高併發探祕之「鎖」

鎖做爲併發共享數據,保證一致性的工具,在JAVA平臺有多種實現(如 synchronized 和 ReentrantLock等等 ) 。這些已經寫好提供的鎖爲咱們開發提供了便利,可是鎖的具體性質以及類型卻不多被說起。html

1. 自旋鎖

自旋鎖是採用讓當前線程不停地的在循環體內執行實現的,當循環的條件被其餘線程改變時 才能進入臨界區。java

Ex1node

@RequestMapping(value = "lock/{index}", method = RequestMethod.GET)public void lock(@PathVariable Integer index) {   switch (index) {       case 0:
           concurrenceLock.doTest();           break;       case 1:
           concurrenceLock.doTest1();           break;       case 2:
           concurrenceLock.doTest2();           break;       default:           break;
   }
}
package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.Objects;import java.util.concurrent.atomic.AtomicReference;/** * <p> * 自旋鎖 * 自旋鎖是採用讓當前線程不停地的在循環體內執行實現的,當循環的條件被其餘線程改變時 才能進入臨界區。 * 自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減小線程上下文切換的消耗,缺點是循環會消耗CPU。 * </p> * * @author xiachaoyang * @version V1.0 * @date 2019年01月23日 15:33 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月23日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class SpinLock {    private AtomicReference<Thread> sign = new AtomicReference<>();    public void lock(){
        Thread current = Thread.currentThread();        //若是當前值{@code ==}爲指望值,原子地將值設置爲給定的更新值
        log.debug("{} spinLock start lock..........",  current.getName());        while (!sign.compareAndSet(null, current)) {
            log.debug("{} spinLock locking........", current.getName());
        }
        log.debug("{} spinLock quit lock..........",  current.getName());
    }    public void unlock() {
        Thread current = Thread.currentThread();        boolean flag = sign.compareAndSet(current, null);
        log.debug("{} spinLock unlock() >>>> {}",current.getName(),flag);
    }
}//ConcurrenceLockServiceImplpackage com.example.service.concurrence.impl;import com.example.concurrence.lock.SpinLock;import com.example.service.concurrence.ConcurrenceService;import lombok.extern.slf4j.Slf4j;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import org.springframework.stereotype.Service;import javax.annotation.Resource;/** * <p> * * </p> * * @author xiachaoyang * @version V1.0 * @date 2019年01月23日 15:42 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月23日 * @modify reason: {方法名}:{緣由} * ... */@Slf4j@Servicepublic class ConcurrenceLockServiceImpl implements ConcurrenceService.LockPart {    private static SpinLock spinLock = new SpinLock();    private Thread last;    @Resource
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;     @Override
    public void doTest() throws InterruptedException {
        spinLock.lock();
        Thread.sleep(10000);
        spinLock.unlock();
    }
}

每次執行完自旋的判斷後,sign的引用會被指向當前線程,下次進入判斷後,預測值和當前線程不一致,則會返回false,即進入自旋鎖循環體內
sign.compareAndSet(null, current)該方法,前者爲預測原來AtomicReference的引用值,後者爲更新值,預測正確的話,則更新爲更新值,返回true;預測錯誤則返回false。spring


使用了CAS原子操做,lock函數將owner設置爲當前線程,而且預測原來的值爲空。unlock函數將owner設置爲null,而且預測值爲當前線程。apache

  1. 當有第二個線程調用lock操做時因爲owner值不爲空,致使循環一直被執行,直至第一個線程調用unlock函數將owner設置爲null,第二個線程才能進入臨界區。編程

  2. 因爲自旋鎖只是將當前線程不停地執行循環體,不進行線程狀態的改變,因此響應速度更快。但當線程數不停增長時,性能降低明顯,由於每一個線程都須要執行,佔用CPU時間。若是線程競爭不激烈,而且保持鎖的時間段。適合使用自旋鎖。緩存

注:該例子爲非公平鎖,得到鎖的前後順序,不會按照進入lock的前後順序進行。微信

日誌多線程

//單次調用
2019-01-28 17:40:38.553 DEBUG 8496 --- [nio-8080-exec-2] com.example.concurrence.lock.SpinLock    : http-nio-8080-exec-2 spinLock start lock..........
2019-01-28 17:40:38.553 DEBUG 8496 --- [nio-8080-exec-2] com.example.concurrence.lock.SpinLock    : http-nio-8080-exec-2 spinLock quit lock..........
2019-01-28 17:40:48.553 DEBUG 8496 --- [nio-8080-exec-2] com.example.concurrence.lock.SpinLock    : http-nio-8080-exec-2 spinLock unlock() >>>> true
//併發調用
2019-01-28 17:44:07.431 DEBUG 8496 --- [nio-8080-exec-8] com.example.concurrence.lock.SpinLock    : http-nio-8080-exec-8 spinLock locking........
2019-01-28 17:44:07.432 DEBUG 8496 --- [nio-8080-exec-8] com.example.concurrence.lock.SpinLock    : http-nio-8080-exec-8 spinLock quit lock..........
2019-01-28 17:44:07.432 DEBUG 8496 --- [nio-8080-exec-6] com.example.concurrence.lock.SpinLock    : http-nio-8080-exec-6 spinLock unlock() >>>> true
2019-01-28 17:44:17.432 DEBUG 8496 --- [nio-8080-exec-8] com.example.concurrence.lock.SpinLock    : http-nio-8080-exec-8 spinLock unlock() >>>> true

2. 自旋鎖的其餘種類

除了前文提到的自旋鎖,在自旋鎖中另有三種常見的鎖形式:TicketLock,CLHlockMCSlock架構

2.1 TicketSpinLock

package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.atomic.AtomicInteger;/** * <p> * Ticket鎖主要解決的是訪問順序的問題,主要的問題是在多核cpu上 * 可是每次都要查詢一個serviceNum 服務號,影響性能(必需要到主內存讀取,並阻止其餘cpu修改) * </p> * * @author xiachaoyang * @version V1.0 * @date 2019年01月24日 10:58 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月24日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class TicketSpinLock {    private AtomicInteger serviceNum = new AtomicInteger();    private AtomicInteger ticketNum = new AtomicInteger();    private static final ThreadLocal<Integer> LOCAL = new ThreadLocal<Integer>();    public void lock() {
        log.debug("{} try lock..................",Thread.currentThread().getName());        int myticket = ticketNum.getAndIncrement();
        LOCAL.set(myticket);
        log.debug("{} set in LOCAL..................",myticket);        while (myticket != serviceNum.get()) {
            log.debug("{} locking..................",Thread.currentThread().getName());
        }
    }    public void unlock() {        int myticket = LOCAL.get();        boolean flag = serviceNum.compareAndSet(myticket, myticket + 1);
        log.debug("{} unlocked..................>>myticket is {}, and flag is {}",Thread.currentThread().getName(),myticket,flag);
    }    //淺析TicketLock(https://blog.csdn.net/yxc5463/article/details/78193991)
    //Java多線程編程排隊鎖(Ticket Lock詳解)(http://www.leftso.com/blog/466.html)}

測試代碼

@Overridepublic void doTest3() throws InterruptedException {
    ticketSpinLock.lock();
    Thread.sleep(10000);
    ticketSpinLock.unlock();
}//模擬多線程,利用PostMan觸發請求,調用接口;中間停頓10s是爲了模擬線程阻塞

日誌

2019-01-28 16:43:30.339 DEBUG 13280 --- [nio-8080-exec-9] c.e.concurrence.lock.TicketSpinLock      : http-nio-8080-exec-9 locking..................
2019-01-28 16:43:30.339 DEBUG 13280 --- [nio-8080-exec-9] c.e.concurrence.lock.TicketSpinLock      : http-nio-8080-exec-9 locking..................
2019-01-28 16:43:30.339 DEBUG 13280 --- [nio-8080-exec-9] c.e.concurrence.lock.TicketSpinLock      : http-nio-8080-exec-9 locking..................
2019-01-28 16:43:30.561 DEBUG 13280 --- [nio-8080-exec-7] c.e.concurrence.lock.TicketSpinLock      : http-nio-8080-exec-7 unlocked..................>>myticket is 3, and flag is true
2019-01-28 16:43:31.108 DEBUG 13280 --- [nio-8080-exec-7] o.s.b.w.s.f.OrderedRequestContextFilter  : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@b5f05b5
2019-01-28 16:43:41.108 DEBUG 13280 --- [nio-8080-exec-9] c.e.concurrence.lock.TicketSpinLock      : http-nio-8080-exec-9 unlocked..................>>myticket is 4, and flag is true

缺點

Ticket Lock 雖然解決了公平性的問題,可是多處理器系統上,每一個進程/線程佔用的處理器都在讀寫同一個變量serviceNum ,每次讀寫操做都必須在多個處理器緩存之間進行緩存同步,這會致使繁重的系統總線和內存的流量,大大下降系統總體的性能。

2.2 CLHLock & MCSLock

CLHLock 和MCSLock 則是兩種類型類似的公平鎖,採用鏈表的形式進行排序

CLHLock

package com.example.concurrence.lock;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * <p> *  CLHlock是不停的查詢前驅變量, 致使不適合在NUMA 架構下使用(在這種結構下,每一個線程分佈在不一樣的物理內存區域) * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年01月28日 17:04 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月28日 * @modify reason: {方法名}:{緣由} * ... */public class CLHLock {    public static class CLHNode {        private volatile boolean isLocked = true;
    }    @SuppressWarnings("unused")    private volatile CLHNode                                           tail;    private static final ThreadLocal<CLHNode>                          LOCAL   = new ThreadLocal<CLHNode>();    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,
            CLHNode.class, "tail");    public void lock() {
        CLHNode node = new CLHNode();
        LOCAL.set(node);
        CLHNode preNode = UPDATER.getAndSet(this, node);        if (preNode != null) {            while (preNode.isLocked) {
            }
            preNode = null;
            LOCAL.set(node);
        }
    }    public void unlock() {
        CLHNode node = LOCAL.get();        if (!UPDATER.compareAndSet(this, node, null)) {
            node.isLocked = false;
        }
        node = null;
    }
}

MCSLock

package com.example.concurrence.lock;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * <p> * MCSLock則是對本地變量的節點進行循環,不存在CLHlock 的問題。 * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年01月28日 17:11 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月28日 * @modify reason: {方法名}:{緣由} * ... */public class MCSLock {    public static class MCSNode {        volatile MCSNode next;        volatile boolean isLocked = true;
    }    private static final ThreadLocal<MCSNode>                          NODE    = new ThreadLocal<MCSNode>();    @SuppressWarnings("unused")    private volatile MCSNode                                           queue;    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,
            MCSNode.class, "queue");    public void lock() {
        MCSNode currentNode = new MCSNode();
        NODE.set(currentNode);
        MCSNode preNode = UPDATER.getAndSet(this, currentNode);        if (preNode != null) {
            preNode.next = currentNode;            while (currentNode.isLocked) {

            }
        }
    }    public void unlock() {
        MCSNode currentNode = NODE.get();        if (currentNode.next == null) {            if (UPDATER.compareAndSet(this, currentNode, null)) {

            } else {                while (currentNode.next == null) {
                }                // 釋放鎖
                currentNode.next.isLocked = false;
                currentNode.next = null;
            }
        } else {
            currentNode.next.isLocked = false;
            currentNode.next = null;
        }
    }
}

CAS

/**AtomicReferenceFieldUpdater*/public final boolean compareAndSet(T obj, V expect, V update) {
    accessCheck(obj);
    valueCheck(update);    /**this.offset = U.objectFieldOffset(field);初始化構造時寫入    public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,Class<W> vclass,String fieldName) {        return new AtomicReferenceFieldUpdaterImpl<U,W>            (tclass, vclass, fieldName, Reflection.getCallerClass());    }    AtomicReferenceFieldUpdaterImpl(final Class<T> tclass,                                        final Class<V> vclass,                                        final String fieldName,                                        final Class<?> caller)    */
    return U.compareAndSwapObject(obj, offset, expect, update);
}/** * 比較obj的offset處內存位置中的值和指望的值,若是相同則更新。此更新是不可中斷的。 * * @param obj 須要更新的對象 * @param offset obj中整型field的偏移量 * @param expect 但願field中存在的值 * @param update 若是指望值expect與field的當前值相同,設置filed的值爲這個新值 * @return 若是field的值被更改返回true */public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

CAS操做有3個操做數,內存值M,預期值E,新值U,若是M==E,則將內存值修改成B,不然啥都不作。

Java中具體的CAS操做類sun.misc.Unsafe。Unsafe類提供了硬件級別的原子操做,Java沒法直接訪問到操做系統底層(如系統硬件等),爲此Java使用native方法來擴展Java程序的功能。

日誌

2019-02-01 11:47:22.539 DEBUG 8948 --- [nio-8080-exec-1] com.example.concurrence.lock.MCSLock     : http-nio-8080-exec-1 MCSLock unlock()
2019-02-01 11:47:22.539 DEBUG 8948 --- [nio-8080-exec-5] com.example.concurrence.lock.MCSLock     : http-nio-8080-exec-5 MCSLock quit lock..........
2019-02-01 11:47:22.564 DEBUG 8948 --- [nio-8080-exec-1] o.s.b.w.s.f.OrderedRequestContextFilter  : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@27711663
2019-02-01 11:47:32.540 DEBUG 8948 --- [nio-8080-exec-5] com.example.concurrence.lock.MCSLock     : http-nio-8080-exec-5 MCSLock unlock()

分析

  • 從代碼上 看,CLH 要比 MCS 更簡單;

  • CLH 的隊列是隱式的隊列,沒有真實的後繼結點屬性。

  • MCS 的隊列是顯式的隊列,有真實的後繼結點屬性。

JUC ReentrantLock 默認內部使用的鎖 便是 CLH鎖(有不少改進的地方,將自旋鎖換成了阻塞鎖等等)。

3.阻塞鎖

阻塞鎖,與自旋鎖不一樣,改變了線程的運行狀態。在JAVA環境中,線程Thread有以下幾個狀態:

  • 新建狀態

  • 就緒狀態

  • 運行狀態

  • 阻塞狀態

  • 死亡狀態

阻塞鎖,能夠說是讓線程進入阻塞狀態進行等待,當得到相應的信號(喚醒,時間) 時,才能夠進入線程的準備就緒狀態,準備就緒狀態的全部線程,經過競爭,進入運行狀態。
JAVA中,可以進入\退出、阻塞狀態或包含阻塞鎖的方法有 ,synchronized 關鍵字(其中的重量鎖),ReentrantLockObject.wait()\notify(),LockSupport.park()/unpart()(j.u.c常用)

package com.example.concurrence.lock;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;import java.util.concurrent.locks.LockSupport;/** * <p> *  阻塞鎖,能夠說是讓線程進入阻塞狀態進行等待,當得到相應的信號(喚醒,時間) 時,才能夠進入線程的準備就緒狀態,準備就緒狀態的全部線程,經過競爭,進入運行狀態。 * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:00 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */public class CLHLockWithBlock {    public static class CLHNode {        private volatile Thread isLocked;
    }    @SuppressWarnings("unused")    private volatile CLHNode                                            tail;    private static final ThreadLocal<CLHNode>                           LOCAL   = new ThreadLocal<CLHNode>();    private static final AtomicReferenceFieldUpdater<CLHLockWithBlock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLockWithBlock.class,
            CLHNode.class, "tail");    public void lock() {
        CLHNode node = new CLHNode();
        LOCAL.set(node);
        CLHNode preNode = UPDATER.getAndSet(this, node);        if (preNode != null) {
            preNode.isLocked = Thread.currentThread();
            LockSupport.park(this);
            preNode = null;
            LOCAL.set(node);
        }
    }    public void unlock() {
        CLHNode node = LOCAL.get();        if (!UPDATER.compareAndSet(this, node, null)) {
            System.out.println("unlock\t" + node.isLocked.getName());
            LockSupport.unpark(node.isLocked);
        }
        node = null;
    }
}

在這裏咱們使用了LockSupport.unpark()的阻塞鎖。 該例子是將CLH鎖修改而成。

阻塞鎖的優點在於,阻塞的線程不會佔用CPU時間, 不會致使 CPU佔用率太高,但進入時間以及恢復時間都要比自旋鎖略慢。

在競爭激烈的狀況下 阻塞鎖的性能要明顯高於自旋鎖。

理想的狀況則是; 在線程競爭不激烈的狀況下,使用自旋鎖,競爭激烈的狀況下使用阻塞鎖

LockSupport

   public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

將一個線程進行掛起是經過park方法實現的,調用 park後,線程將一直阻塞直到超時或者中斷等條件出現。unpark能夠終止一個掛起的線程,使其恢復正常。整個併發框架中對線程的掛起操做被封裝在 LockSupport類中,LockSupport類中有各類版本pack方法,但最終都調用了Unsafe.park()方法。

4.可重入鎖

本文裏面講的是廣義上的可重入鎖,而不是單指JAVA下的ReentrantLock

可重入鎖,也叫作遞歸鎖,指的是同一線程外層函數得到鎖以後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在JAVA環境下 ReentrantLock 和synchronized 都是可重入鎖。

synchronized

package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;/** * <p> * * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:14 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class SynchronizedLock implements Runnable{    /**     * When an object implementing interface <code>Runnable</code> is used     * to create a thread, starting the thread causes the object's     * <code>run</code> method to be called in that separately executing     * thread.     * <p>     * The general contract of the method <code>run</code> is that it may     * take any action whatsoever.     *     * @see Thread#run()     */
    @Override
    public void run() {
        get();    
    }    private synchronized void get() {
        log.debug("synchronized current thread's id is {}",Thread.currentThread().getId());
        set();
    }    private synchronized void set() {
        log.debug("synchronized current thread's id is {}",Thread.currentThread().getId());
    }    public static void main(String[] args) {
        SynchronizedLock lock = new SynchronizedLock();        new Thread(lock).start();        new Thread(lock).start();        new Thread(lock).start();
    }
}

日誌

10:22:31.186 [Thread-0] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 13
10:22:31.191 [Thread-0] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 13
10:22:31.191 [Thread-2] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 15
10:22:31.191 [Thread-2] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 15
10:22:31.191 [Thread-1] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 14
10:22:31.191 [Thread-1] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 14

ReentrantLock

package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.ReentrantLock;/** * <p> * * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:19 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class ReentrantLockThread implements Runnable{

    ReentrantLock lock = new ReentrantLock();    /**     * When an object implementing interface <code>Runnable</code> is used     * to create a thread, starting the thread causes the object's     * <code>run</code> method to be called in that separately executing     * thread.     * <p>     * The general contract of the method <code>run</code> is that it may     * take any action whatsoever.     *     * @see Thread#run()     */
    @Override
    public void run() {
        get();
    }    private void get() {
        lock.lock();
        log.debug("ReentrantLockThread current thread's id is {}",Thread.currentThread().getId());
        set();
        lock.unlock();
    }    private void set() {
        lock.lock();
        log.debug("ReentrantLockThread current thread's id is {}",Thread.currentThread().getId());
        lock.unlock();
    }    public static void main(String[] args) {
        ReentrantLockThread lock = new ReentrantLockThread();        new Thread(lock).start();        new Thread(lock).start();        new Thread(lock).start();
    }
}

日誌

10:24:22.765 [Thread-0] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 13
10:24:22.770 [Thread-0] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 13
10:24:22.770 [Thread-1] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 14
10:24:22.770 [Thread-1] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 14
10:24:22.770 [Thread-2] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 15
10:24:22.770 [Thread-2] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 15

兩個例子最後的結果都是正確的,即 同一個線程id被連續輸出兩次。

可重入鎖最大的做用是避免死鎖,咱們以自旋鎖做爲例子:

public class SpinLock {    private AtomicReference<Thread> owner =new AtomicReference<>();    public void lock(){
        Thread current = Thread.currentThread();        while(!owner.compareAndSet(null, current)){
        }
    }    public void unlock (){
        Thread current = Thread.currentThread();
        owner.compareAndSet(current, null);
    }
}

對於自旋鎖來講,
一、如有同一線程兩調用lock() ,會致使第二次調用lock位置進行自旋,產生了死鎖
說明這個鎖並非可重入的。(在lock函數內,應驗證線程是否爲已經得到鎖的線程)
二、若1問題已經解決,當unlock()第一次調用時,就已經將鎖釋放了。實際上不該釋放鎖。
(採用計數次進行統計)

修改以後

public class SpinLock1 {    private AtomicReference<Thread> owner =new AtomicReference<>();    private int count =0;    public void lock(){
        Thread current = Thread.currentThread();        if(current==owner.get()) {
            count++;            return ;
        }        while(!owner.compareAndSet(null, current)){

        }
    }    public void unlock (){
        Thread current = Thread.currentThread();        if(current==owner.get()){            if(count!=0){
                count--;
            }else{
                owner.compareAndSet(current, null);
            }

        }

    }
}

該自旋鎖即爲可重入鎖。

5.ReentrantLock(重入鎖)以及公平性

ReentrantLock的實現不只能夠替代隱式的synchronized關鍵字,並且可以提供超過關鍵字自己的多種功能。
這裏提到一個鎖獲取的公平性問題,若是在絕對時間上,先對鎖進行獲取的請求必定被先知足,那麼這個鎖是公平的,反之,是不公平的,也就是說等待時間最長的線程最有機會獲取鎖,也能夠說鎖的獲取是有序的。ReentrantLock這個鎖提供了一個構造函數,可以控制這個鎖是不是公平的。
而鎖的名字也是說明了這個鎖具有了重複進入的可能,也就是說可以讓當前線程屢次的進行對鎖的獲取操做,這樣的最大次數限制是Integer.MAX_VALUE,約21億次左右。
事實上公平的鎖機制每每沒有非公平的效率高,由於公平的獲取鎖沒有考慮到操做系統對線程的調度因素,這樣形成JVM對於等待中的線程調度次序和操做系統對線程的調度之間的不匹配。對於鎖的快速且重複的獲取過程當中,連續獲取的機率是很是高的,而公平鎖會壓制這種狀況,雖然公平性得以保障,可是響應比卻降低了,可是並非任何場景都是以TPS做爲惟一指標的,由於公平鎖可以減小「飢餓」發生的機率,等待越久的請求越是可以獲得優先知足。

在ReentrantLock中,對於公平和非公平的定義是經過對同步器AbstractQueuedSynchronizer的擴展加以實現的,也就是在tryAcquire的實現上作了語義的控制。

非公平的獲取語義

final boolean nonfairTryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) {        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {        int nextc = c + acquires;                if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);        return true;
    }    return false;
}

上述邏輯主要包括:

  • 若是當前狀態爲初始狀態,那麼嘗試設置狀態;

  • 若是狀態設置成功後就返回;

  • 若是狀態被設置,且獲取鎖的線程又是當前線程的時候,進行狀態的自增;

  • 若是未設置成功狀態且當前線程不是獲取鎖的線程,那麼返回失敗。

公平的獲取語義

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

上述邏輯相比較非公平的獲取,僅加入了當前線程(Node)以前是否有前置節點在等待的判斷。hasQueuedPredecessors()方法命名有些歧義,其實應該是currentThreadHasQueuedPredecessors()更爲妥帖一些,也就是說當前面沒有人排在該節點(Node)前面時候隊且可以設置成功狀態,纔可以獲取鎖。

釋放語義

protected final boolean tryRelease(int releases) {    int c = getState() - releases;    if (Thread.currentThread() != getExclusiveOwnerThread())        throw new IllegalMonitorStateException();    boolean free = false;    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);    return free;
}

上述邏輯主要主要計算了釋放狀態後的值,若是爲0則徹底釋放,返回true,反之僅是設置狀態,返回false。
下面將主要的筆墨放在公平性和非公平性上,首先看一下兩者測試的對比:

package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * <p> * * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:39 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class ReentrantLockWithFailCondition {    private static Lock fairLock = new ReentrantLock(true);    private static Lock unfairLock = new ReentrantLock();    public void fair() {
        log.debug("fair version");        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(fairLock));
            thread.setName("" + i);
            thread.start();
        }        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }    public void unfair() {
        log.debug("unfair version");        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(unfairLock));
            thread.setName("" + i);
            thread.start();
        }        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }    private static class Job implements Runnable {        private Lock lock;        public Job(Lock lock) {            this.lock = lock;
        }        @Override
        public void run() {            for (int i = 0; i < 5; i++) {
                lock.lock();                try {
                    System.out.println("Lock by:"
                            + Thread.currentThread().getName());
                } finally {
                    lock.unlock();
                }
            }
        }
    }    public static void main(String[] args) {
        ReentrantLockWithFailCondition test = new ReentrantLockWithFailCondition();
        test.fair();
        log.debug("unfair --------------------------------------------------->>>");
        test.unfair();
    }
}

仔細觀察返回的結果(其中每一個數字表明一個線程),非公平的結果一個線程連續獲取鎖的狀況很是多,而公平的結果連續獲取的狀況基本沒有。那麼在一個線程獲取了鎖的那一刻,究竟鎖的公平性會致使鎖有什麼樣的處理邏輯呢?
經過以前的同步器(AbstractQueuedSynchronizer)的介紹,在鎖上是存在一個等待隊列,sync隊列,咱們經過複寫ReentrantLock的獲取當前鎖的sync隊列,輸出在ReentrantLock被獲取時刻,當前的sync隊列的狀態。

修改測試

package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.Collection;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * <p> * * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:39 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class ReentrantLockWithFailCondition{    private static Lock fairLock = new ReentrantLock(true);    private static Lock unfairLock = new ReentrantLock();    public void fair() {
        log.debug("fair version");        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(fairLock));
            thread.setName("" + i);
            thread.start();
        }        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }    public void unfair() {
        log.debug("unfair version");        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(unfairLock));
            thread.setName("" + i);
            thread.start();
        }        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }    private static class Job implements Runnable {        private Lock lock;        public Job(Lock lock) {            this.lock = lock;
        }        @Override
        public void run() {            for (int i = 0; i < 5; i++) {
                lock.lock();                try {                    //log.debug("Lock by:{}",Thread.currentThread().getName());
                    log.debug("Lock by:{} and {} waits.",Thread.currentThread().getName(),((ReentrantLock2) lock).getQueuedThreads());
                } finally {
                    lock.unlock();
                }
            }
        }
    }    private static Lock fairLock2 = new ReentrantLock2(true);    private static Lock unfairLock2 = new ReentrantLock2();    private static class ReentrantLock2 extends ReentrantLock {        // Constructor Override
        private static final long serialVersionUID = 1773716895097002072L;        /**         * Creates an instance of {@code ReentrantLock} with the         * given fairness policy.         *         * @param fair {@code true} if this lock should use a fair ordering policy         */
        public ReentrantLock2(boolean fair) {            super(fair);
        }        /**         * Creates an instance of {@code ReentrantLock}.         * This is equivalent to using {@code ReentrantLock(false)}.         */
        public ReentrantLock2() {
        }        @Override
        public Collection<Thread> getQueuedThreads() {            return super.getQueuedThreads();
        }
    }    public void fair2() {
        log.debug("fair version");        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(fairLock2));
            thread.setName("" + i);
            thread.start();
        }        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }    public void unfair2() {
        log.debug("unfair version");        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(unfairLock2));
            thread.setName("" + i);
            thread.start();
        }        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }    public static void main(String[] args) {
        ReentrantLockWithFailCondition test = new ReentrantLockWithFailCondition();       // test.fair();
        //log.debug("unfair --------------------------------------------------->>>");
        //test.unfair();

        test.fair2();
        log.debug("unfair2 --------------------------------------------------->>>");
        test.unfair2();
    }
}

能夠明顯看出,在非公平獲取的過程當中,「插隊」現象很是嚴重,後續獲取鎖的線程根本不顧及sync隊列中等待的線程,而是能獲取就獲取。反觀公平獲取的過程,鎖的獲取就相似線性化的,每次都由sync隊列中等待最長的線程(鏈表的第一個,sync隊列是由尾部結點添加,當前輸出的sync隊列是逆序輸出)獲取鎖。一個 hasQueuedPredecessors方法可以得到公平性的特性,

這點其實是由AbstractQueuedSynchronizer來完成的,看一下acquire方法:

public final void acquire(int arg) {    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

能夠看到,若是獲取狀態和在sync隊列中排隊是短路的判斷,也就是說若是tryAcquire成功,那麼是不會進入sync隊列的,能夠經過下圖來深入的認識公平性和AbstractQueuedSynchronizer的獲取過程。
非公平的,或者說默認的獲取方式以下圖所示:

對於狀態的獲取,能夠快速的經過tryAcquire的成功,也就是黃色的Fast路線,也能夠因爲tryAcquire的失敗,構造節點,進入sync隊列中排序後再次獲取。所以能夠理解爲Fast就是一個快速通道,當例子中的線程釋放鎖以後,快速的經過Fast通道再次獲取鎖,就算當前sync隊列中有排隊等待的線程也會被忽略。這種模式,能夠保證進入和退出鎖的吞吐量,可是sync隊列中過早排隊的線程會一直處於阻塞狀態,形成「飢餓」場景。
而公平性鎖,就是在tryAcquire的調用中顧及當前sync隊列中的等待節點(廢棄了Fast通道),也就是任意請求都須要按照sync隊列中既有的順序進行,先到先得。這樣很好的確保了公平性,可是能夠從結果中看到,吞吐量就沒有非公平的鎖高了。

REFERENCES

  • [java鎖的種類以及辨析(一):自旋鎖] (http://ifeve.com/java_lock_see1/)

  • [Java鎖的種類以及辨析(二):自旋鎖的其餘種類] (http://ifeve.com/java_lock_see2/)

  • [Java鎖的種類以及辨析(三):阻塞鎖] (http://ifeve.com/java_lock_see3/)

  • [Java鎖的種類以及辨析(四):可重入鎖]  (http://ifeve.com/java_lock_see4/)

  • [ReentrantLock(重入鎖)以及公平性] (http://ifeve.com/reentrantlock-and-fairness/)

  • [Java多線程系列—「JUC原子類」04之 AtomicReference原子類] (http://www.cnblogs.com/skywang12345/p/3514623.html)

  • [淺析TicketLock] (https://blog.csdn.net/yxc5463/article/details/78193991)

  • [Java多線程編程排隊鎖(Ticket Lock詳解)] (http://www.leftso.com/blog/466.html)

  • [Java中Unsafe類詳解] (https://www.cnblogs.com/mickole/articles/3757278.html)

更多



掃碼關注或搜索架構探險之道獲取最新文章,不積跬步無以致千里,堅持每週一更,堅持技術分享^_^


本文分享自微信公衆號 - 架構探險之道(zacsnz1314)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索