自己動手寫把”鎖”---終極篇


鎖是整個Java並發包的實現基礎,通過學習本系列文章,將對你理解Java並發包的本質有很大的幫助。
 
前邊幾篇中,我已經把實現鎖用到的技術,進行了一一講述。這其中有原子性、內存模型、LockSupport還有CAS,掌握了這些技術,即使沒有本篇,你也完全有能力自己寫一把鎖出來。但為了本系列的完整性,我在這里還是把最后這一篇補上。
 
先說一下鎖的運行流程:多個線程搶占同一把鎖,只有一個線程能搶占成功,搶占成功的線程繼續執行下邊的邏輯,搶占失敗的線程進入阻塞等待。搶占成功的線程執行完畢后,釋放鎖,並從等待的線程中挑一個喚醒,讓它繼續競爭鎖。
 
轉變成程序實現:我們首先定一個state變量,state=0表示未被加鎖,state=1表示被加鎖。多個線程在搶占鎖時,競爭將state變量從0修改為1,修改成功的線程則加鎖成功。state從0修改為1的過程,這里使用cas操作,以保證只有一個線程加鎖成功,同時state需要用volatile修飾,已解決線程可見的問題。加鎖成功的線程執行完業務邏輯后,將state從1修改回0,同時從等待的線程中選擇一個線程喚醒。所以加鎖失敗的線程,在加鎖失敗時需要將自己放到一個集合中,以等待被喚醒。這個集合需要支持多線程並發安全,在這里我通過一個鏈表來實現,通過CAS操作來實現並發安全。
 
把思路說清楚了,咱們看下代碼實現。
 
首先咱們實現一個ThreadList,這是一個鏈表結合,用來存放等待的處於等待喚醒的線程:

public class ThreadList{
    private volatile Node head = null;
    private static  long headOffset;
    private static Unsafe unsafe;
    static {
        try {
            Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor(new Class<?>[0]);
            constructor.setAccessible(true);
            unsafe = constructor.newInstance(new Object[0]);
            headOffset = unsafe.objectFieldOffset(ThreadList.class.getDeclaredField("head"));
        }catch (Exception e){
        }
    }
    /**
     *
     * @param thread
     * @return 是否只有當前一個線程在等待
     */
    public boolean insert(Thread thread){
        Node node = new Node(thread);
        for(;;){
            Node first = getHead();
            node.setNext(first);
            if(unsafe.compareAndSwapObject(this, headOffset,first,node)){
                return first==null?true:false;
            }
        }
    }
    public Thread pop(){
        Node first = null;
        for(;;){
            first = getHead();
            Node next = null;
            if(first!=null){
                next = first.getNext();
            }
            if(unsafe.compareAndSwapObject(this, headOffset,first,next)){
                break;
            }
        }
        return first==null?null:first.getThread();
    }
    private Node getHead(){
        return this.head;
    }
    private static class Node{
        volatile Node next;
        volatile Thread thread;
        public Node(Thread thread){
            this.thread = thread;
        }
        public void setNext(Node next){
            this.next = next;
        }
        public Node getNext(){
            return next;
        }
        public Thread getThread(){
            return this.thread;
        }
    }
}
加鎖失敗的線程,調用insert方法將自己放入這個集合中,insert方法里將線程封裝到Node中,然后使用cas操作將node添加到列表的頭部。同樣為了線程可見的問題,Node里的thread和next都用volatile修飾。
加鎖成功的線程,調用pop方法獲得一個線程,進行喚醒,這里邊同樣使用了cas操作來保證線程安全。
 
接下來在看看鎖的實現:
public class MyLock {
    private volatile int state = 0;
    private ThreadList threadList = new ThreadList();
    private static  long stateOffset;
    private static Unsafe unsafe;
    static {
       try {
           Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor(new Class<?>[0]);
           constructor.setAccessible(true);
           unsafe = constructor.newInstance(new Object[0]);
           stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("state"));
       }catch (Exception e){
       }

    }
    public void lock(){
        if(compareAndSetState(0,1)){
        }else{
            addNodeAndWait();
        }
    }
    public void unLock(){
        compareAndSetState(1,0);
        Thread thread = threadList.pop();
        if(thread != null){
            LockSupport.unpark(thread);
        }
    }
    private void addNodeAndWait(){
        //如果當前只有一個等待線程時,重新獲取一下鎖,防止永遠不被喚醒。
        boolean isOnlyOne = threadList.insert(Thread.currentThread());
        if(isOnlyOne && compareAndSetState(0,1)){
            return;
        }
        LockSupport.park(this);//線程被掛起
        if(compareAndSetState(0,1)){//線程被喚醒后繼續競爭鎖
            return;
        }else{
            addNodeAndWait();
        }
    }
    private boolean compareAndSetState(int expect,int update){
        return unsafe.compareAndSwapInt(this,stateOffset,expect,update);
    }
}

 

線程調用lock方法進行加鎖,cas將state從0修改1,修改成功則加鎖成功,lock方法返回,否則調用addNodeAndWait方法將線程加入ThreadList隊列,並使用LockSupport將線程掛起。(ThreadList的insert方法,返回一個boolean類型的值,用來處理一個特殊情況的,稍后再說。)
獲得鎖的線程執行完業務邏輯后,調用unLock方法釋放鎖,即通過cas操作將state修改回0,同時從ThreadList拿出一個等待線程,調用LockSupport的unpark方法,來將它喚醒。
 
 
將我們在《自己動手寫把"鎖"---鎖的作用》的例子修改為如下,來測試下咱們的鎖的效果:
public class TestMyLock {
    private static  List<Integer> list = new ArrayList<>();
    private static MyLock myLock = new MyLock();
    public static void main(String[] args){
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10000;i++){
                    add(i);
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                print();
            }
        });
        t1.start();
        t2.start();
    }
    private static void add(int i){
        myLock.lock();
        list.add(i);
        myLock.unLock();
    }
    private static void print(){
        myLock.lock();
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
        myLock.unLock();
    }
}
ok,正常運行了,不在報錯。
 
到這里咱們的一個簡單地鎖已經實現了。接下來我再把上邊的,一個沒講的細節說一下。即如下這段代碼:

boolean isOnlyOne = threadList.insert(Thread.currentThread());
        if(isOnlyOne && compareAndSetState(0,1)){
            return;
        }
ThreadList的insert方法,在插入成功后,會判斷當前鏈表中是否只有自己一個線程在等待,如果是則返回true。從而進入后邊的if語句。這個邏輯的用意就是:如果只有自己一個線程在等待時,則試着通過cas操作重新獲取鎖,如果獲取失敗才進入阻塞等待。它是用來解決以下邊界情況:

在只有線程A和線程B兩個線程的時候,如果沒有以上判斷邏輯,線程B將有可能會永遠處於阻塞不被喚醒。 

 

以下是本系列其他的文章:

自己動手寫把”鎖”之---鎖的作用

自己動手寫把”鎖”之---JMM和volatile

自己動手寫把”鎖”---原子性操作

自己動手寫把”鎖”---LockSupport深入淺出

 

-------------------------------------------------

有興趣的朋友,可以加入我的知識圈,一起研究討論。

我正在「JAVA互聯網技術」和朋友們討論有趣的話題,你一起來吧?

https://t.zsxq.com/EUn6IIE

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM