先了解一下java 模型html
(1)每一個線程都有本身的本地內存空間(java棧中的幀)。線程執行時,先把變量從內存讀到線程本身的本地內存空間,而後對變量進行操做。
(2)對該變量操做完成後,在某個時間再把變量刷新回主內存。java
那麼咱們再瞭解下鎖提供的兩種特性:互斥(mutual exclusion) 和可見性(visibility):
(1)互斥(mutual exclusion):互斥即一次只容許一個線程持有某個特定的鎖,所以可以使用該特性實現對共享數據的協調訪問協議,這樣,一次就只有一個線程可以使用該共享數據;
(2)可見性(visibility):簡單來講就是一個線程修改了變量,其餘線程能夠當即知道。保證可見性的方法:volatile,synchronized,final(一旦初始化完成其餘線程就可見)。c++
加鎖,多線程爲了防止競爭資源,即防止對同一資源進行併發操做。
鎖機制存在如下問題
(1)在多線程競爭下,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,引發性能問題。
(2)一個線程持有鎖會致使其它全部須要此鎖的線程掛起。
(3)若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。
獨佔鎖是一種悲觀鎖,會致使其它全部須要鎖的線程掛起,等待持有鎖的線程釋放鎖。
樂觀鎖是每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。算法
volatile 2個特徵編程
可見性:一個線程修改了某個共享變量的值,其餘線程可以立馬得知這個修改。windows
1.當寫一個volatile變量的時候,JMM會把本地內存中的共享變量刷新到主內存。 2.當讀一個volatile變量的是時候,JMM會把線程本地內存的值設置爲無效,而後從主內存中讀取共享變量。
禁止特定的處理器重排序緩存
1.當第二個操做爲volatile寫的時候,第一個操做無論是什麼,都不容許重排序。 2.當第一個操做爲volatile讀的時候,第二個操做無論是什麼,都不容許重排序。 3.當第一個操做爲volatile寫的時候,第二個操做是volatile讀的時候,不容許重排序。
除此之外的狀況,都運行重排序。而重排序的實現是靠加入內存屏障來實現的。內存屏障時用來禁止特定的重排序的cpu指令。包括4中,loadload,store store,store load與load/store。load能夠理解爲讀操做,store能夠理解爲寫操做,舉例說明,loadload是保證在第二個load和其餘一系列操做以前要確保第一個load的讀操做完成。store store是保證在第二個store及寫操做以前,第一個store寫操做對其餘處理器可見。其中store load的開銷最大,是個萬能屏障,兼具其餘三個屏障的功能。安全
public class RunThread extends Thread { private boolean isRunning = true; public boolean isRunning() { return isRunning; } public void setRunning(boolean isRunning) { this.isRunning = isRunning; } @Override public void run() { System.out.println("進入到run方法中了"); while (isRunning == true) { } System.out.println("線程執行完成了"); } } public class Run { public static void main(String[] args) { try { RunThread thread = new RunThread(); thread.start(); Thread.sleep(1000); thread.setRunning(false); } catch (InterruptedException e) { e.printStackTrace(); } } }
在main線程中,thread.setRunning(false);將啓動的線程RunThread中的共享變量設置爲false,從而想讓RunThread.java的while循環結束。若是使用JVM -server參數執行該程序時,RunThread線程並不會終止,從而出現了死循環。數據結構
緣由分析
如今有兩個線程,一個是main線程,另外一個是RunThread。它們都試圖修改isRunning變量。按照JVM內存模型,main線程將isRunning讀取到本地線程內存空間,修改後,再刷新回主內存。
而在JVM設置成 -server模式運行程序時,線程會一直在私有堆棧中讀取isRunning變量。所以,RunThread線程沒法讀到main線程改變的isRunning變量。從而出現了死循環,致使RunThread沒法終止。多線程
解決方法
volatile private boolean isRunning = true;
原理
當對volatile標記的變量進行修改時,會將其餘緩存中存儲的修改前的變量清除,而後從新讀取。通常來講應該是先在進行修改的緩存A中修改成新值,而後通知其餘緩存清除掉此變量,當其餘緩存B中的線程讀取此變量時,會向總線發送消息,這時存儲新值的緩存A獲取到消息,將新值穿給B。最後將新值寫入內存。當變量須要更新時都是此步驟,volatile的做用是被其修飾的變量,每次更新時,都會刷新上述步驟。
synchronized
(1)synchronized 方法
方法聲明時使用,放在範圍操做符(public等)以後,返回類型聲明(void等)以前.這時,線程得到的是成員鎖,即一次只能有一個線程進入該方法,其餘線程要想在此時調用該方法,只能排隊等候,當前線程(就是在synchronized方法內部的線程)執行完該方法後,別的線程才能進入。
示例:
public synchronized void synMethod(){ }
如在線程t1中有語句obj.synMethod(); 那麼因爲synMethod被synchronized修飾,在執行該語句前, 須要先得到調用者obj的對象鎖, 若是其餘線程(如t2)已經鎖定了obj (多是經過obj.synMethod,也多是經過其餘被synchronized修飾的方法obj.otherSynMethod鎖定的obj), t1須要等待直到其餘線程(t2)釋放obj, 而後t1鎖定obj, 執行synMethod方法. 返回以前以前釋放obj鎖。
(2)synchronized 塊
對某一代碼塊使用,synchronized後跟括號,括號裏是變量,這樣,一次只有一個線程進入該代碼塊.此時,線程得到的是成員鎖。
(3)synchronized (this)
當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程獲得執行。另外一個線程必須等待當前線程執行完這個代碼塊之後才能執行該代碼塊。
當一個線程訪問object的一個synchronized(this)同步代碼塊時,其餘線程對object中全部其它synchronized(this)同步代碼塊的訪問將被阻塞。
然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另外一個線程仍然能夠訪問該object中的除synchronized(this)同步代碼塊之外的部分。
第三個例子一樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就得到了這個object的對象鎖。結果,其它線程對該object對象全部同步代碼部分的訪問都被暫時阻塞。
以上規則對其它對象鎖一樣適用。
第三點舉例說明:
public class Thread2 { public void m4t1() { synchronized(this) { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } } public void m4t2() { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static void main(String[] args) { final Thread2 myt2 = new Thread2(); Thread t1 = new Thread( new Runnable() { public void run() { myt2.m4t1(); } }, "t1" ); Thread t2 = new Thread( new Runnable() { public void run() { myt2.m4t2(); } }, "t2" ); t1.start(); t2.start(); } }
含有synchronized同步塊的方法m4t1被訪問時,線程中m4t2()依然能夠被訪問。
wait()sleep() notify()/notifyAll()
wait():釋放佔有的對象鎖,線程進入等待池,釋放cpu,而其餘正在等待的線程便可搶佔此鎖,得到鎖的線程便可運行程序。
sleep():不一樣的是,線程調用此方法後,會休眠一段時間,休眠期間,會暫時釋放cpu,但並不釋放對象鎖。也就是說,在休眠期間,其餘線程依然沒法進入此代碼內部。休眠結束,線程從新得到cpu,執行代碼。
wait()和sleep()最大的不一樣在於wait()會釋放對象鎖,而sleep()不會
notify(): 該方法會喚醒由於調用對象的wait()而等待的線程,其實就是對對象鎖的喚醒,從而使得wait()的線程能夠有機會獲取對象鎖。調用notify()後,並不會當即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼所有執行完畢,纔會釋放對象鎖。JVM則會在等待的線程中調度一個線程去得到對象鎖,執行代碼。須要注意的是,wait()和notify()必須在synchronized代碼塊中調用。
notifyAll()則是喚醒全部等待的線程。
lock
synchronized的缺陷
1)synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性。那麼爲何會出現Lock呢?
若是一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,並執行該代碼塊時,其餘線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這裏獲取鎖的線程釋放鎖只會有兩種狀況:
1)獲取鎖的線程執行完了該代碼塊,而後線程釋放對鎖的佔有;
2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。
那麼若是這個獲取鎖的線程因爲要等待IO或者其餘緣由(好比調用sleep方法)被阻塞了,可是又沒有釋放鎖,其餘線程便只能等待,試想一下,這多麼影響程序執行效率。
所以就須要有一種機制能夠不讓等待的線程一直無期限地等待下去(好比只等待必定的時間或者可以響應中斷),經過Lock就能夠辦到。
再舉個例子:當有多個線程讀寫文件時,讀操做和寫操做會發生衝突現象,寫操做和寫操做會發生衝突現象,可是讀操做和讀操做不會發生衝突現象。
可是採用synchronized關鍵字來實現同步的話,就會致使一個問題:
若是多個線程都只是進行讀操做,因此當一個線程在進行讀操做時,其餘線程只能等待沒法進行讀操做。
所以就須要一種機制來使得多個線程都只是進行讀操做時,線程之間不會發生衝突,經過Lock就能夠辦到。
另外,經過Lock能夠知道線程有沒有成功獲取到鎖。這個是synchronized沒法辦到的。
總結一下,也就是說Lock提供了比synchronized更多的功能。可是要注意如下幾點:
1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,所以是內置特性。Lock是一個類,經過這個類能夠實現同步訪問;
2)Lock和synchronized有一點很是大的不一樣,採用synchronized不須要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完以後,系統會自動讓線程釋放對鎖的佔用;而Lock則必需要用戶去手動釋放鎖,若是沒有主動釋放鎖,就有可能致使出現死鎖現象。
(2)java.util.concurrent.locks包下經常使用的類
public interface Lock { //獲取鎖,若是鎖被其餘線程獲取,則進行等待 void lock(); //當經過這個方法去獲取鎖時,若是線程正在等待獲取鎖,則這個線程可以響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時經過lock.lockInterruptibly()想獲取某個鎖時,倘若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法可以中斷線程B的等待過程。 void lockInterruptibly() throws InterruptedException; /**tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,若是獲取成 *功,則返回true,若是獲取失敗(即鎖已被其餘線程獲取),則返回 *false,也就說這個方法不管如何都會當即返回。在拿不到鎖時不會一直在那等待。*/ boolean tryLock(); //tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不過區別在於這個方法在拿不到鎖時會等待必定的時間,在時間期限以內若是還拿不到鎖,就返回false。若是若是一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); //釋放鎖 Condition newCondition(); }
一般使用lock進行同步:
Lock lock = ...; lock.lock(); try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 }
trylock使用方法:
Lock lock = ...; if(lock.tryLock()) { try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 } }else { //若是不能獲取鎖,則直接作其餘事情 }
lockInterruptibly()通常的使用形式以下:
public void method() throws InterruptedException { lock.lockInterruptibly(); try { //..... } finally { lock.unlock(); } }
注意:
當一個線程獲取了鎖以後,是不會被interrupt()方法中斷的。由於自己在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程當中的線程,只能中斷阻塞過程當中的線程。
而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是沒法被中斷的,只有一直等待下去。
(3)ReentrantLock
ReentrantLock,意思是「可重入鎖」,是惟一實現了Lock接口的類,而且ReentrantLock提供了更多的方法。
public class Test { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); private Lock lock = new ReentrantLock(); //注意這個地方 public static void main(String[] args) { final Test test = new Test(); new Thread(){ public void run() { test.insert(Thread.currentThread()); }; }.start(); new Thread(){ public void run() { test.insert(Thread.currentThread()); }; }.start(); } public void insert(Thread thread) { lock.lock(); try { System.out.println(thread.getName()+"獲得了鎖"); for(int i=0;i<5;i++) { arrayList.add(i); } } catch (Exception e) { // TODO: handle exception }finally { System.out.println(thread.getName()+"釋放了鎖"); lock.unlock(); } } }
若是鎖具有可重入性,則稱做爲可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上代表了鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉個簡單的例子,當一個線程執行到某個synchronized方法時,好比說method1,而在method1中會調用另一個synchronized方法method2,此時線程沒必要從新去申請鎖,而是能夠直接執行方法method2。
代碼解釋:
class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { } }
上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而因爲method2也是synchronized方法,假如synchronized不具有可重入性,此時線程A須要從新申請鎖。可是這就會形成一個問題,由於線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。
而因爲synchronized和Lock都具有可重入性,因此不會發生上述現象。
volatile和synchronized區別
1)volatile本質是在告訴jvm當前變量在寄存器中的值是不肯定的,須要從主存中讀取,synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住.
2)volatile僅能使用在變量級別,synchronized則可使用在變量,方法.
3)volatile僅能實現變量的修改可見性,而synchronized則能夠保證變量的修改可見性和原子性.
《Java編程思想》上說,定義long或double變量時,若是使用volatile關鍵字,就會得到(簡單的賦值與返回操做)原子性。
4)volatile不會形成線程的阻塞,而synchronized可能會形成線程的阻塞.
5)當一個域的值依賴於它以前的值時,volatile就沒法工做了,如n=n+1,n++等。若是某個域的值受到其餘域的值的限制,那麼volatile也沒法工做,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。
6)使用volatile而不是synchronized的惟一安全的狀況是類中只有一個可變的域。
synchronized和lock區別
1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
2)synchronized在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生;而Lock在發生異常時,若是沒有主動經過unLock()去釋放鎖,則極可能形成死鎖現象,所以使用Lock時須要在finally塊中釋放鎖;
3)Lock可讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不可以響應中斷;
4)經過Lock能夠知道有沒有成功獲取鎖,而synchronized卻沒法辦到。
5)Lock能夠提升多個線程進行讀操做的效率。
在性能上來講,若是競爭資源不激烈,二者的性能是差很少的,而當競爭資源很是激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。因此說,在具體使用時要根據適當狀況選擇。
CAS
CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。
非阻塞算法 (nonblocking algorithms)
一個線程的失敗或者掛起不該該影響其餘線程的失敗或掛起的算法。
現代的CPU提供了特殊的指令,能夠自動更新共享數據,並且可以檢測到其餘線程的干擾,而 compareAndSet() 就用這些代替了鎖定。
拿出AtomicInteger來研究在沒有鎖的狀況下是如何作到數據正確性的。
private volatile int value;
首先毫無覺得,在沒有鎖的機制下可能須要藉助volatile原語,保證線程間的數據是可見的(共享的)。
這樣才獲取變量的值的時候才能直接讀取。
public final int get() { return value; }
而後來看看++i是怎麼作到的。
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
在這裏採用了CAS操做,每次從內存中讀取數據而後將此數據和+1後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。
而compareAndSet利用JNI來完成CPU指令的操做。
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
總體的過程就是這樣子的,利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。
其中
unsafe.compareAndSwapInt(this, valueOffset, expect, update); 相似: if (this == expect) { this = update return true; } else { return false; }
那麼問題就來了,成功過程當中須要2個步驟:比較this == expect,替換this = update,compareAndSwapInt如何這兩個步驟的原子性呢? 參考CAS的原理。
CAS原理
CAS經過調用JNI的代碼實現的。JNI:Java Native Interface爲JAVA本地調用,容許java調用其餘語言。
而compareAndSwapInt就是藉助C來調用CPU底層指令實現的。
Unsafe類中的compareAndSwapInt,是一個本地方法,該方法的實現位於unsafe.cpp中
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END
先想辦法拿到變量value在內存中的地址。
經過Atomic::cmpxchg實現比較替換,其中參數x是即將更新的值,參數e是原內存的值。
下面從分析比較經常使用的CPU(intel x86)來解釋CAS的實現原理。
下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
能夠看到這是個本地方法調用。這個本地方法在openjdk中依次調用的c++代碼爲:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實如今openjdk的以下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(對應於windows操做系統,X86處理器)。下面是對應於intel x86處理器的源代碼的片斷:
// Adding a lock prefix to an instruction on MP machine // VC++ doesn't like the lock prefix to be on a single line // so we can't insert a label after the lock prefix. // By emitting a lock prefix, we can define a label after it. #define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { / alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } }
如上面源代碼所示,程序會根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴。若是程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(lock cmpxchg)。反之,若是程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不須要lock前綴提供的內存屏障效果)。
intel的手冊對lock前綴的說明以下:
確保對內存的讀-改-寫操做原子執行。在Pentium及Pentium以前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其餘處理器暫時沒法經過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上作了一個頗有意義的優化:若是要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨佔或以修改狀態),而且該內存區域被徹底包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。因爲在指令執行期間該緩存行會一直被鎖定,其它處理器沒法讀/寫該指令要訪問的內存區域,所以能保證指令執行的原子性。這個操做過程叫作緩存鎖定(cache locking),緩存鎖定將大大下降lock前綴指令的執行開銷,可是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。
禁止該指令與以前和以後的讀和寫指令重排序。
把寫緩衝區中的全部數據刷新到內存中。
在這裏能夠看到是用嵌入的彙編實現的, 關鍵CPU指令是 cmpxchg
到這裏無法再往下找代碼了. 也就是說CAS的原子性其實是CPU實現的. 其實在這一點上仍是有排他鎖的. 只是比起用synchronized, 這裏的排他時間要短的多. 因此在多線程狀況下性能會比較好.
代碼裏有個alternative for InterlockedCompareExchange
這個InterlockedCompareExchange是WINAPI裏的一個函數, 作的事情和上面這段彙編是同樣的
http://msdn.microsoft.com/en-...
最後再貼一下x86的cmpxchg指定
Opcode CMPXCHG CPU: I486+ Type of Instruction: User Instruction: CMPXCHG dest, src Description: Compares the accumulator with dest. If equal the "dest" is loaded with "src", otherwise the accumulator is loaded with "dest". Flags Affected: AF, CF, OF, PF, SF, ZF CPU mode: RM,PM,VM,SMM +++++++++++++++++++++++ Clocks: CMPXCHG reg, reg 6 CMPXCHG mem, reg 7 (10 if compartion fails)
關於CPU的鎖有以下3種:
處理器自動保證基本內存操做的原子性
首先處理器會自動保證基本的內存操做的原子性。處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其餘處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行裏進行16/32/64位的操做是原子的,可是複雜的內存操做處理器不能自動保證其原子性,好比跨總線寬度,跨多個緩存行,跨頁表的訪問。可是處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操做的原子性。
使用總線鎖保證原子性
第一個機制是經過總線鎖保證原子性。若是多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操做)操做,那麼共享變量就會被多個處理器同時進行操做,這樣讀改寫操做就不是原子的,操做完以後共享變量的值會和指望的不一致,舉個例子:若是i=1,咱們進行兩次i++操做,咱們指望的結果是3,可是有可能結果是2。以下圖
緣由是有可能多個處理器同時從各自的緩存中讀取變量i,分別進行加一操做,而後分別寫入系統內存當中。那麼想要保證讀改寫共享變量的操做是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操做緩存了該共享變量內存地址的緩存。
處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其餘處理器的請求將被阻塞住,那麼該處理器能夠獨佔使用共享內存。
使用緩存鎖保證原子性
第二個機制是經過緩存鎖定保證原子性。在同一時刻咱們只需保證對某個內存地址的操做是原子性便可,但總線鎖定把CPU和內存之間通訊鎖住了,這使得鎖按期間,其餘處理器不能操做其餘內存地址的數據,因此總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。
頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存裏,那麼原子操做就能夠直接在處理器內部緩存中進行,並不須要聲明總線鎖,在奔騰6和最近的處理器中可使用「緩存鎖定」的方式來實現複雜的原子性。所謂「緩存鎖定」就是若是緩存在處理器緩存行中內存區域在LOCK操做期間被鎖定,當它執行鎖操做回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並容許它的緩存一致性機制來保證操做的原子性,由於緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其餘處理器回寫已被鎖定的緩存行的數據時會起緩存行無效,在例1中,當CPU1修改緩存行中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行。
可是有兩種狀況下處理器不會使用緩存鎖定。第一種狀況是:當操做的數據不能被緩存在處理器內部,或操做的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。第二種狀況是:有些處理器不支持緩存鎖定。對於Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。
以上兩個機制咱們能夠經過Inter處理器提供了不少LOCK前綴的指令來實現。好比位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其餘一些操做數和邏輯指令,好比ADD(加),OR(或)等,被這些指令操做的內存區域就會加鎖,致使其餘處理器不能同時訪問它。
CAS缺點
CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操做
ABA問題。由於CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
關於ABA問題參考文檔: http://blog.hesey.net/2011/09...
循環時間長開銷大。自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,pause指令有兩個做用,第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。
只能保證一個共享變量的原子操做。當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。
concurrent包的實現
因爲java的CAS同時具備 volatile 讀和volatile寫的內存語義,所以Java線程之間的通訊如今有了下面四種方式:
A線程寫volatile變量,隨後B線程讀這個volatile變量。
A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。
A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。
A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。
Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操做,這是在多處理器中實現同步的關鍵(從本質上來講,可以支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,所以任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操做的原子指令)。同時,volatile變量的讀/寫和CAS能夠實現線程之間的通訊。把這些特性整合在一塊兒,就造成了整個concurrent包得以實現的基石。若是咱們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:
首先,聲明共享變量爲volatile;
而後,使用CAS的原子條件更新來實現線程之間的同步;
同時,配合以volatile的讀/寫和CAS所具備的volatile讀和寫的內存語義來實現線程之間的通訊。
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從總體來看,concurrent包的實現示意圖以下:
參考
https://blog.csdn.net/ztchun/...
http://www.cnblogs.com/imqsl/...