java並發編程11.原子變數與非阻塞同步機制

來源:互聯網
上載者:User

標籤:原子變數   都對   nod   定時   push   value   部分   mic   中斷   

在非阻塞演算法中不存在死結和其他活躍性問題。

在基於鎖的演算法中,如果一個線程在休眠或自旋的同時持有一個鎖,那麼其他線程都無法執行下去,而非阻塞演算法不會受到單個線程失敗的影響。

鎖的劣勢

許多JVM都對非競爭鎖擷取和釋放操作進行了極大的最佳化,但如果有多個線程同時請求鎖,那麼JVM就需要藉助作業系統地功能。如果出現了這種情況,那麼一些線程將被掛起並且在稍後恢複運行。當線程恢複執行時,必須等待其他線程執行完它們的時間片以後,才能被調度執行。在掛起和恢複線程等過程中存在著很大的開銷,並且通常存在著較大時間的中斷。如果在基於鎖的類中包含細粒度的操作(例如同步器類,在其大多數方法中只包含了少量操作),那麼當在鎖上存在著激烈的競爭時,調度開銷與工作開銷的比值會非常高。

另外,當一個線程正在等待鎖時,它不能做任何其他事情。如果一個線程在持有鎖的情況下被順延強制,那麼所有需要這個鎖的線程都無法執行下去。如果被阻塞線程的優先順序高,而持有鎖的線程優先順序低,那麼將是一個嚴重的問題。

 

比較並交換CAS

 

CAS包含了3個運算元---需要讀寫的記憶體位置V,進行比較的值A和擬寫入的新值B。若且唯若V的值等於A時,CAS才會通過原子的方式用新值B來更新V的值,否則不會執行任何操作。無論位置V的值是否等於A,都將返回V原有的值。

 

CAS的含義:我認為V的值應該為A,如果是,那麼將V的值更新為B,否則不修改並告訴V的值實際為多少。CAS是一種樂觀的態度,它希望能成功地執行更新操作,並且如果有另一個線程在最近一次檢查後更新了該變數,那麼CAS能檢測到這個錯誤。

/** * 當多個線程嘗試使用CAS同時更新同一個變數時,只有其中一個線程能更新變數的值,而其他線程都將失敗。 * 然而,失敗的線程並不會被掛起,而是被告知在這次競爭中失敗,並可以再次嘗試。 * 由於一個線程在競爭CAS時不會阻塞,因此它可以決定是否重新嘗試,或者執行一些恢複操作,也或者不執行任何操作。 */public class SimulatedCAS {     private int value;        public synchronized int get(){        return value;    }        public synchronized int compareAndSwap(int expectedValue,int newValue){        int oldValue = value;        if(oldValue == expectedValue){            value = newValue;        }        return oldValue;    }        public synchronized boolean compareAndSet(int expectedValue,int newValue){        return (expectedValue == compareAndSwap(expectedValue,newValue));    }}

CAS的典型使用模式是:首先從V中讀取值A,並根據A計算新值B,然後再通過CAS以原子方式將V中的值由A變成B。由於CAS能檢測到來自其他線程的幹擾,因此即使不使用鎖也能夠實現原子的讀--改--寫操作。
非阻塞的計算機

/** * 通常,反覆重試是一種合理的策略,但在一些競爭很激烈的情況下, * 更好的方式是在重試之前首先等待一段時間或者回退,從而避免造成活鎖問題。 *  * 雖然java語言的鎖定語句比較簡潔,但JVM和操作在管理鎖時需要完成的工作卻並不簡單。 * 在實現鎖定時需要遍曆JVM中一條非常複雜的代碼路徑,並可能導致作業系統級的鎖定、線程掛起以及上下文卻換等動作。 * 在最好的情況下,在鎖定時至少需要一次CAS,因此雖然在使用鎖時沒有用到CAS,但實際上也無法節約任何執行開銷。 * 另外,在程式內部執行CAS不需要執行JVM代碼、系統調用或線程調度操作。 * 在應用級上看起來越長的代碼路徑,如果加上JVM和作業系統中的代碼調用,那麼事實上卻變得更短。 * CAS的主要缺點是,它將使調用者處理競爭問題,而在鎖中能自動處理競爭問題 * */public class CasCounter {    private SimulatedCAS value;        public int getValue(){        return value.get();    }        public int increment(){        int v;        do{            v = value.get();        }while(v != value.compareAndSwap(v, v + 1));        return v + 1;    }}

JVM對CAS的支援

Java5.0中引入了底層的支援,在int,long和對象引用等類型上都公開了CAS操作,並且JVM把它們編譯為底層硬體提供的最有效方法。在原子變數類中,使用了這些底層的JVM支援為數字類型和參考型別提供一種高效的CAS操作,而在java.util.concurrent中的大多數類在實現時都直接或間接地使用了這些原子變數類。

非阻塞的棧

 

/** * 棧是由Node元素構成的一個鏈表,根節點為棧頂yop,每個元素中都包含了一個值以及指向下一個元素的連結。 * push方法建立一個新的節點,該節點的next域指向當前的棧頂,然後使用CAS把這個新節點放入棧頂。 */public class ConcurrentStack<E> {        AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();        public void push(E item){        Node<E> newHead = new Node<E>(item);        Node<E> oldHead;        do{            oldHead = top.get();            newHead.next = oldHead;        }while(!top.compareAndSet(oldHead, newHead));    }        public E pop(){        Node<E> newHead;        Node<E> oldHead;        do{            oldHead = top.get();            if(oldHead == null){                return null;            }            newHead = oldHead.next;        }while(!top.compareAndSet(oldHead, newHead));        return oldHead.item;    }    private static class Node<E>{        public final E item;        public Node<E> next;                public Node(E item){            this.item = item;        }    }}

 

非阻塞鏈表

鏈表隊列比棧複雜,它必須支援對前端節點和尾節點的快速存取。它需要單獨維護頭指標和尾指標。

對於尾部的插入,有兩個點需要更新:將當前尾節點的next指向要插入的節點,和將尾節點更新為新插入的節點。這兩個更新操作需要不同的CAS操作,不好通過原子變數來實現。需要使用一些策略:

策略一是,即使在一個包含多個步驟的更新操作中,也要確保資料結構總是處於抑制的狀態。這樣,線程B到達時,如果發現A正在執行更新,那麼線程B就可以知道有一個操作已部分完成,並且不能立即執行自己的更新操作。然後B可以等待並直到A完成更新。雖然能使不同的線程輪流訪問資料結構,並且不會造成破壞,但如果有一個線程在更新操作中失敗了,那麼其他的線程都無法再方位隊列。

策略二是,如果B到達時發現A正在修改資料結構,那麼在資料結構中應該有足夠多的資訊,使得B能完成A的更新操作。如果B協助A完成了更新操作,那麼B可以執行自己的操作,而不用等待A的操作完成。當A恢複後再試圖完成其操作時,會發現B已經替它完成了。

 

 

/** * 實現的關鍵點在於: * 當隊列處於穩定點時,未節點的next域將為空白,如果隊列處於中間狀態,那麼tail.next將為非空。 * 因此,任何線程都能夠通過檢查tail.next來擷取隊列當前的狀態。 * 而且,當隊列處於中間狀態時,可以通過將尾節點向前移動一個節點, * 從而結束其他線程正在執行的插入元素操作,並使得隊列恢複為穩定點。 */public class LinkedQueue<E> {    private static class Node<E>{        final E item;        final AtomicReference<Node<E>> next;        public Node(E item,Node<E> next){            this.item = item;            this.next = new AtomicReference<Node<E>>(next);        }        private final Node<E> dummy = new Node<E>(null,null);        private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(dummy);        private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(dummy);         private boolean put(E item){            Node<E> newNode = new Node<E>(item,null);            while(true){                Node<E> curTail = tail.get();                Node<E> tailNext = curTail.next.get();                if(curTail == tail.get()){                    if(tailNext != null){                        //隊列處於中間狀態,推進尾節點                        tail.compareAndSet(curTail, tailNext);                    }else{                        //處於穩定點。嘗試插入新節點                        if(curTail.next.compareAndSet(null, newNode)){                            //插入成功,嘗試推進尾節點,這一步如果未來得及完成,可由別的線程幫忙                            tail.compareAndSet(curTail, newNode);                            return true;                        }                    }                }            }        }    }}

 

ABA問題
在某些演算法中,如果V的值首先由A變成B,再由B變成A。

解決辦法是:不是更新某個引用的值,而是更新兩個值,包括一個引用和一個版本號碼。AtomicStampedReference以及AtomicMarkableReference支援在兩個變數上執行原子的條件更新。

 

#筆記內容來自 《 java並發編程實戰》

 

java並發編程11.原子變數與非阻塞同步機制

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.