學習Lock總結

參考:Java併發編程與技術內幕:聊聊鎖的技術內幕(上)

           Java鎖--Lock實現原理(底層實現)

 

概述

跟synchronized一樣,lock也是常用的用於多線程開發線程同步的一種手段,有相似的點也有不同的點。

他是java的一個類,基本都是在java類中實現同步,而synchronized是在JVM中實現的。

synchronized和lock他們的不同點

  1. 一個是關鍵字標識符,一個是一個類,在代碼中的用法不同
  2. 一個是在JVM中實現的,一個是用java代碼實現的,實現的層面不同。
  3. sunchronized在jdk1.6以後,優化了很多,如自旋鎖,偏向鎖,輕量級鎖,鎖粗化,鎖消除等,性能方面有優勢。而lock則更多適用於應用層,比如開發出讀寫鎖(可同時讀,但不可同時寫和同時讀寫)、公平鎖等,只需要繼承和重寫一些方法即可。所以兩者在運用的適用方面也有着區別。
  4. 前者還不可以判斷是否獲得鎖,後者可以判斷
  5. 前者會阻塞,後者可能在未獲得鎖的情況下直接結束
  6. 前者不需要手動釋放鎖,自動的。而後者需要在finally中手動釋放鎖

相同點

  1. 都能夠解決多線程同步的問題
  2. 都能可以線程間相互作用,比如wait,notify和notifyAll方法。而lock則是Condition對象的:await, signal,signalAll 方法
  3. 他們內部大部分實現都通過基於CAS來確定更新安全的(自我感覺)
  4. 阻塞都是交給操作系統來實現,調用操作系統的pthread_mutex_lock,所以阻塞操作操作系統在內核態和用戶態切換,很浪費資源

Lock實現思想

簡單說來,AbstractQueuedSynchronizer會把所有的請求線程構成一個CLH隊列,當一個線程執行完畢(lock.unlock())時會激活自己的後繼節點,但正在執行的線程並不在隊列中,而那些等待執行的線程全部處於阻塞狀態,經過調查線程的顯式阻塞是通過調用LockSupport.park()完成,而LockSupport.park()則調用sun.misc.Unsafe.park()本地方法,再進一步,HotSpot在Linux中中通過調用pthread_mutex_lock函數把線程交給系統內核進行阻塞。

既當有線程請求獲得該鎖時會先嚐試,失敗了再加入到隊列(一般隊列先執行head節點,所以先進的先獲得鎖,後進的在尾部),所以可能後請求的未進入隊列就可以獲得鎖,既非公平鎖。當head獲得鎖之後會出隊列,然後next節點變成head節點,等待,當使用的線程解鎖後再下一位HEAD節點

 

LOCK源碼

 先貼出來可重入鎖大概的內部結構

package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
 
public interface Lock {
 
    //取得鎖,但是要注意lock()忽視interrupt(), 拿不到鎖就 一直阻塞
    void lock();
 
    //同樣也是取得鎖,但是lockInterruptibly()會響應打擾 interrupt()並catch到InterruptedException,從而跳出阻塞
    void lockInterruptibly() throws InterruptedException;
 
    //嘗試取得鎖,成功返回true
    boolean tryLock();
 
    //在規定的時間等待裏,如果取得鎖就返回tre
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
 
    //釋放鎖
    void unlock();
    //條件狀態,非常有用,Blockingqueue阻塞隊列就是用到它了
    Condition newCondition();
}

原始的鎖接口,常用的方法


public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    private final Sync sync; //就只有一個Sync變量,ReentrantLock的所有方法基本都是調用Sync的方法 

//構造函數
    public ReentrantLock() {
        sync = new NonfairSync(); //默認非公平鎖
    }
 
    public ReentrantLock(boolean fair) {
        sync = (fair)? new FairSync() : new NonfairSync();//公平鎖
    }
//其裏的公平鎖的意思是哪個線程先來等待,誰就先獲得這個鎖。而非公平鎖則是看操作系統的調度,有不確定//性。一般設置成非公平鎖的性能會好很多。


//然後看看部分lock方法
    public void lock() {
        sync.lock();
    }
//還有這個
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }


//在來看看一些內置類
//Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
}

//非公平鎖
final static class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
 
        final void lock() {
            if (compareAndSetState(0, 1)) //0未獲取,1已經獲取
                setExclusiveOwnerThread(Thread.currentThread());//設置獨佔模式,則一個鎖只能被一個線程持有,其他線程必須要等待。
            else
                acquire(1);//如果沒有取得鎖,嘗試使用信號量的方式
        }
 
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

//公平鎖 類似的,沒有深入查看,感興趣的自己去看源碼
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
            
        final void lock() {
            acquire(1);
        }
}
}

常用的ReentrantLock,有sync成員變量,從部分方法中可以看出,大部分功能的實現都是sync在實現.

sync有公平鎖與非公平鎖兩種,默認是非公平鎖。

lock()(示例代碼爲非公平鎖)

final void lock() {
            if (compareAndSetState(0, 1)) //0未獲取,1已經獲取
                setExclusiveOwnerThread(Thread.currentThread());//設置獨佔模式,則一個鎖只能被一個線程持有,其他線程必須要等待。
            else
                acquire(1);//如果沒有取得鎖,嘗試使用信號量的方式
        }

 使用CAS的原理進行把state換成1(CAS的原理,要改一個值時,先拷貝舊值,然後做修改,然後再回到內存中判斷,內存中的值如果等於之前取出來的舊值則認爲安全的,替換內存中的舊值,如若不同,則不修改。具體的可以百度CAS)

//設置狀態
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
 
    //可以看到, compareAndSwapInt不是用Java實現的, 而是通過JNI調用操作系統的原生程序.注意它是原子方法(C++寫的)
   public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);

所以lock方式使用CAS原理進行獲得鎖(0如果換成1則代表成功),獲得成功則設置當前鎖的佔有者爲該線程(獨佔),不成功則嘗試acquire方法,該方法最終也是使用的nonfairTryAcquire方法(下面有)獲取鎖,如果還是沒有獲取到,則加入到阻塞隊列。

TryLock()

public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
//再看sync的方法
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//取得狀態
            if (c == 0) {//0表示未獲取鎖
                if (compareAndSetState(0, acquires)) {//CAS設置狀態
                    setExclusiveOwnerThread(current);//設置獨佔線程
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//當前線程已有這個鎖了
                int nextc = c + acquires;//設置重入的次數,如果是一個線程在有鎖的情況下多次調用tryLock就有可能進入這個方法
                if (nextc < 0) // 重入數溢出了
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;//如果到這裏就是沒有取到鎖了,
        }

 trylock方法沒有lock方法這麼直接,他首先是判斷c的狀態

(c爲AbstractQueuedSynchronizer類中的一個變量private volatile int state;)

state爲0代表還沒有線程獲得鎖,爲1或者以上爲有線程獲得和代表重入的次數。

c==0代表沒有線程使用,可獲取,不等於0判斷獲得鎖的是不是自己,如果是則修改重入次數(state的值),如果不是則返回false,代表沒有獲得鎖,和Lock()相比是不會進入阻塞狀態

tryLock(long timeout, TimeUnit unit)

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
	if (Thread.interrupted())//檢測到當前線程的中斷標誌爲true
	    throw new InterruptedException();
	return tryAcquire(arg) ||
	    doAcquireNanos(arg, nanosTimeout);
    }


        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

//這是1.8的源碼,思路是一樣的
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);//取消阻塞和移出隊列
                //先是取消Node關聯的線程,再是移出隊列
        }
    }

這個方法首先是嘗試獲取鎖,如果獲取不到則放入到阻塞隊列中,死循環的獲取,時間到了還是未獲取的到則調用最後的cancelAcquire(node)函數退出獲取該鎖的行列。

解鎖

 釋放鎖的過程便是釋放鎖的過程(state置爲0),和喚醒head節點的過程,需要注意的是head節點喚醒之後只是要重新去嘗試獲取鎖,並不一定一定能獲取到鎖,也可能沒有獲取(有新線程加入,還沒進入隊列就可以競爭一次)到從而導致再次進入阻塞狀態,既非公平鎖

源碼

public final boolean release(int arg) {  
    if (tryRelease(arg)) {  
        Node h = head;  
        if (h != null && h.waitStatus != 0)  
            unparkSuccessor(h);  
        return true;  
    }  
    return false;  
}  


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

解鎖主要是tryRelease與tryAcquire,他們語義相同,把如何釋放的邏輯延遲到子類中。

tryRelease語義很明確:如果線程多次鎖定,則進行多次釋放,直至status==0則真正釋放鎖,所謂釋放鎖即設置status爲0,因爲無競爭所以沒有使用CAS。 
release的語義在於:如果可以釋放鎖,則喚醒隊列第一個線程(Head),具體喚醒代碼如下:


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

 這段代碼的意思在於找出第一個可以unpark的線程,一般說來head.next == head,Head就是第一個線程,但Head.next可能被取消或被置爲null,因此比較穩妥的辦法是從後往前找第一個可用線程。貌似回溯會導致性能降低,其實這個發生的機率很小,所以不會有性能影響。之後便是通知系統內核繼續該線程,在Linux下是通過pthread_mutex_unlock完成。之後,被解鎖的線程進入上面所說的重新競爭狀態。

 

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