併發編程的藝術 (學習筆記)-鎖

5.1 lock鎖

5.2 隊列同步器(AbstractQueuedSynchronizer)同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態同步器是實現鎖的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義,可以這樣理解二者之間的關係:鎖是面向使用者的,他定義了使用者與鎖交互的接口,隱藏了實現細節,同步器面向的是鎖的實現者,他簡化了鎖的實現方式,屏蔽了同步狀態管理,線程的排隊,等待與喚醒等底層操作,鎖和同步器很好地隔離了使用者和實現者所需要關注的領域。

5.2.1 隊列同步器的接口與示例 

1,自定義同步鎖 

package com.souche.sts.study;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
* @description: 自定義獨佔鎖
*
* @author: heqiang
*
* @create: 2019-09-25 10:16
**/
public class Mutex implements Lock {

//靜態內部類,自定義同步器
private static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -1776271486756692747L;

//是否處於佔用狀態
protected boolean isHeldExclusively() {
return getState() == 1;
}

//當狀態爲0的時候獲取鎖
public boolean tryAcquire(int acquire) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
    return true;
}
    return false;
}

//釋放鎖,將狀態設置爲0
protected boolean tryRelease(int release) {
if (getState() == 0) {
    throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//返回一個condition,每個condition都包含了一個condition隊列
Condition newCondition(){
return new ConditionObject();
}
}

private final Sync sync = new Sync();


@Override
public void lock() {
sync.acquire(1);
}

@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}

@Override
public void unlock() {
sync.release(1);
}

@Override
public Condition newCondition() {
return sync.newCondition();
}
}

2,獨佔式同步狀態獲取與釋放

/*
* 同步狀態獲取,節點構造,加入同步隊列以及同步隊列自旋
*/
public final void acquire(int arg) {
//tryAcquire保證安全的獲取同步狀態,獲取失敗,則通過addWaiter加入隊列尾部,
//最後調用acquireQueued,死循環獲取同步狀態
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}


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

//死循環的保證節點的正確添加
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;
}
}
}
}

//進入隊列後,自旋獲取同步狀態,獲得就退出自旋
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);
}
}

獨佔式同步狀態獲取流程,也就是acquire方法:

 

 

釋放鎖:
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釋放同步狀態,然後喚醒頭節點的後續節點。

3 共享式同步狀態獲取與釋放

共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態,

//
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
//如果大於等於表示能夠獲取狀態,所以退出自旋的條件就是大於等於0,
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

//釋放鎖
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

不同之處:

tryReleaseShared方法必須確保同步狀態線程安全釋放,一般是通過循環喝cas來保證,因爲釋放同步狀態的操作會同時來自多個線程。 
4, 獨佔式超時獲取同步狀態

//
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}


 

自定義只能2個線程獲得的鎖:在碼雲中
5.3 重入鎖(ReentrantLock)

synchronized關鍵字隱式的支持重進入。

5.3.1 實現重進入

重進入是指任意線程再獲取到鎖之後能夠再次獲取該鎖而不會被鎖阻塞。

兩個問題:

線程再次獲取鎖:

鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是,則再次成功獲取。

鎖的最終釋放:

線程重複n次獲取鎖,隨後在第n次釋放鎖後,其他線程能夠獲取到該鎖,鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放。 

//獲取鎖
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 tryRelease(int releases) {
    //自減
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //當線程最終全部釋放完後,狀態爲0,佔有線程設置爲null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

5.3.2 公平與非公平獲取鎖公平性與否是針對獲取鎖而言,

如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是fifo。

//構造函數,根據fair選擇公平鎖還是非公平鎖
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
//默認非公平鎖
public ReentrantLock() {
    sync = new NonfairSync();
}

//判斷獲取鎖
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //判斷加入了同步隊列中當前節點是否有前驅節點,這是與nonfairTryAcquire方法不同的地方。
        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;
}

公平鎖保證了鎖獲取按照fifo原則,而代價是進行了大量的線程切換,非公平鎖雖然可能造成線程飢餓,但極少線程切換,保證了其更大的吞吐量。

5.4 讀寫鎖(ReentrantReadWriteLock)

前面提到都是排他鎖,這些鎖同一時刻只允許一個線程進行訪問。而讀寫鎖在同一時刻可以允許多個讀線程訪問,而寫線程訪問時,其他的讀線程和其他線程都被阻塞,讀寫鎖維護了一對鎖。

讀寫鎖優點:保證了寫操作對讀操作的可見性以及併發性的提升,還能簡化讀寫交互場景的編程方式。

 

示例:
/**
 * @description: 讀寫鎖示例
 *
 * @create: 2019-09-26 11:35
 **/
public class Cache {

    static Map<String,Object> map = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    //獲取一個key對應的value
    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    //設置key對應value,並返回舊的value
    public static final Object put(String key,Object value){
        w.lock();
        try {
            return map.put(key,value);
        } finally {
            w.unlock();
        }
    }
    
    public static final void clear () {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }
}

5.4.2 分析實現

1,讀寫狀態的設計依舊是維護同步器的一個同步狀態,讀寫鎖的自定義同步器需要在同步狀態(一個整型變量)上維護多個讀線程和一個寫線程狀態。

讀寫鎖將變量切分了兩個部分,高16位表示讀,低16位表示寫。 

 

 2,寫鎖的獲取與釋放

//支持重入的排他鎖,如果當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則進入等待狀態。

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    //獲取當前寫鎖狀態
    int c = getState();
    //獲取當前讀鎖狀態
    int w = exclusiveCount(c);
    if (c != 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;
}

 

釋放:
protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

 

3,讀鎖的獲得與釋放
讀鎖時一個支持重入的共享鎖,可支持多線程同時獲取狀態並且可以重入,當前線程在獲取讀鎖時,如果寫鎖已經被其他線程獲取,則進入等待狀態。
簡化版的tryAcquireShared(便於理解):
protected final int tryAcquireShared(int unused) {
    for(;;){
        int c = getState();
        int nextc = c + (1<<16);
        if(nextc < c){
            throw new Err();
        }
        //如果其他線程已經獲取寫鎖,則當前線程獲取失敗進入等待
        if (exclusiveCount(c) != 0 && owner != Thread.currentThread) {
            return -1;
        }
        if (compareAndSetState(c,nextc)) {
            return 1;
        }
    }
}
實際的tryAcquireShared:
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}
4,鎖降級
定義:寫鎖降級成爲讀鎖:線程在擁有寫鎖的時候,再去獲取讀鎖,然後釋放寫鎖的過程
示例:
public void processData() {
volatile boolean update = true;
    r.lock();
    if(!update){
        //必須先釋放讀鎖
        r.unlock();
        //降級先需要獲取寫鎖
        w.lock();
        try {
            if(!update){
                //處理
                update = true;
            }
            r.lock();
        } finally {
            w.unlock();
        }
        //降級完成,
    }
}

5.6 Condition接口

Condition與lock配合可以實現等待/通知模式。

 

示例:
public class ConditionUseCase {
    
    Lock lock  = new ReentrantLock();
    Condition condition = lock.newCondition();

    /**
     * 等待後釋放鎖
     * @throws InterruptedException
     */
    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 通知當前線程,當前線程才從await返回,返回前已經獲取了鎖。
     * @throws InterruptedException
     */
    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

有界隊列:
public class BoundedQueue<T> {

    private Object[] items;

    private int addIndex,removeIndex,count;

    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();

    public BoundedQueue (int size) {
        items = new Object[size];
    }

    /**
     * 添加一個元素,如果數據滿,則添加線程進入等待狀態,直到有空位
     * @param t
     * @throws InterruptedException
     */
    public void add(T t) throws InterruptedException {
        lock.lock();

        try {
            while(count == items.length) {
                notFull.await();
            }
            items[addIndex] = t;
            if(++addIndex == items.length){
                addIndex = 0;
            }
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 由頭部刪除一個元素,如果數組爲空,則刪除線程進入等待
     * @return
     * @throws InterruptedException
     */
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count  == 0) {
                notEmpty.await();
            }
            Object x = items[removeIndex];
            if(++removeIndex == items.length) {
                removeIndex = 0;
            }
            --count;
            notFull.signal();
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

2,Condition的實現

Condition是同步器AbstractQueuedSynchronizer的內部類。每個Condition內部都有一個等待對列

2.1 等待隊列 

 

2.2 等待
調用await方法,會使當前線程進入等待隊列並釋放鎖。
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //當前線程進入等待隊列
    Node node = addConditionWaiter();
    //釋放同步狀態,也就是釋放鎖
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
3,通知
調用sinal方法,將會喚醒在等待隊列中等待時間最長的節點(首節點)
public final void signal() {
    檢查當前線程必須是獲取了鎖的線程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

 

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