本身動手寫把」鎖」---終極篇

鎖是整個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將有可能會永遠處於阻塞不被喚醒。 

 

如下是本系列其餘的文章:php

本身動手寫把」鎖」之---鎖的做用html

本身動手寫把」鎖」之---JMM和volatilenode

本身動手寫把」鎖」---原子性操做安全

本身動手寫把」鎖」---LockSupport深刻淺出多線程

 

-------------------------------------------------併發

有興趣的朋友,能夠加入個人知識圈,一塊兒研究討論。ide

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

https://t.zsxq.com/EUn6IIE測試

相關文章
相關標籤/搜索