摘要:加鎖,多線程為了防止競爭資源,即防止對(duì)同一資源進(jìn)行并發(fā)操作。釋放占有的對(duì)象鎖,線程進(jìn)入等待池,釋放而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運(yùn)行程序。休眠結(jié)束,線程重新獲得執(zhí)行代碼。則是喚醒所有等待的線程。
先了解一下java 模型
(1)每個(gè)線程都有自己的本地內(nèi)存空間(java棧中的幀)。線程執(zhí)行時(shí),先把變量從內(nèi)存讀到線程自己的本地內(nèi)存空間,然后對(duì)變量進(jìn)行操作。
(2)對(duì)該變量操作完成后,在某個(gè)時(shí)間再把變量刷新回主內(nèi)存。
那么我們再了解下鎖提供的兩種特性:互斥(mutual exclusion) 和可見性(visibility):
(1)互斥(mutual exclusion):互斥即一次只允許一個(gè)線程持有某個(gè)特定的鎖,因此可使用該特性實(shí)現(xiàn)對(duì)共享數(shù)據(jù)的協(xié)調(diào)訪問協(xié)議,這樣,一次就只有一個(gè)線程能夠使用該共享數(shù)據(jù);
(2)可見性(visibility):簡單來說就是一個(gè)線程修改了變量,其他線程可以立即知道。保證可見性的方法:volatile,synchronized,final(一旦初始化完成其他線程就可見)。
加鎖,多線程為了防止競爭資源,即防止對(duì)同一資源進(jìn)行并發(fā)操作。
鎖機(jī)制存在以下問題
(1)在多線程競爭下,加鎖、釋放鎖會(huì)導(dǎo)致比較多的上下文切換和調(diào)度延時(shí),引起性能問題。
(2)一個(gè)線程持有鎖會(huì)導(dǎo)致其它所有需要此鎖的線程掛起。
(3)如果一個(gè)優(yōu)先級(jí)高的線程等待一個(gè)優(yōu)先級(jí)低的線程釋放鎖會(huì)導(dǎo)致優(yōu)先級(jí)倒置,引起性能風(fēng)險(xiǎn)。
獨(dú)占鎖是一種悲觀鎖,會(huì)導(dǎo)致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。
樂觀鎖是每次不加鎖而是假設(shè)沒有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直到成功為止。
volatile 2個(gè)特征
可見性:一個(gè)線程修改了某個(gè)共享變量的值,其他線程能夠立馬得知這個(gè)修改。
1.當(dāng)寫一個(gè)volatile變量的時(shí)候,JMM會(huì)把本地內(nèi)存中的共享變量刷新到主內(nèi)存。 2.當(dāng)讀一個(gè)volatile變量的是時(shí)候,JMM會(huì)把線程本地內(nèi)存的值設(shè)置為無效,然后從主內(nèi)存中讀取共享變量。
禁止特定的處理器重排序
1.當(dāng)?shù)诙€(gè)操作為volatile寫的時(shí)候,第一個(gè)操作不管是什么,都不允許重排序。 2.當(dāng)?shù)谝粋€(gè)操作為volatile讀的時(shí)候,第二個(gè)操作不管是什么,都不允許重排序。 3.當(dāng)?shù)谝粋€(gè)操作為volatile寫的時(shí)候,第二個(gè)操作是volatile讀的時(shí)候,不允許重排序。
除此以外的情況,都運(yùn)行重排序。而重排序的實(shí)現(xiàn)是靠加入內(nèi)存屏障來實(shí)現(xiàn)的。內(nèi)存屏障時(shí)用來禁止特定的重排序的cpu指令。包括4中,loadload,store store,store load與load/store。load可以理解為讀操作,store可以理解為寫操作,舉例說明,loadload是保證在第二個(gè)load和其他一系列操作之前要確保第一個(gè)load的讀操作完成。store store是保證在第二個(gè)store及寫操作之前,第一個(gè)store寫操作對(duì)其他處理器可見。其中store load的開銷最大,是個(gè)萬能屏障,兼具其他三個(gè)屏障的功能。
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("進(jìn)入到run方法中了"); while (isRunning == true) { } System.out.println("線程執(zhí)行完成了"); } } 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);將啟動(dòng)的線程RunThread中的共享變量設(shè)置為false,從而想讓RunThread.java的while循環(huán)結(jié)束。如果使用JVM -server參數(shù)執(zhí)行該程序時(shí),RunThread線程并不會(huì)終止,從而出現(xiàn)了死循環(huán)。
原因分析
現(xiàn)在有兩個(gè)線程,一個(gè)是main線程,另一個(gè)是RunThread。它們都試圖修改isRunning變量。按照J(rèn)VM內(nèi)存模型,main線程將isRunning讀取到本地線程內(nèi)存空間,修改后,再刷新回主內(nèi)存。
而在JVM設(shè)置成 -server模式運(yùn)行程序時(shí),線程會(huì)一直在私有堆棧中讀取isRunning變量。因此,RunThread線程無法讀到main線程改變的isRunning變量。從而出現(xiàn)了死循環(huán),導(dǎo)致RunThread無法終止。
解決方法
volatile private boolean isRunning = true;
原理
當(dāng)對(duì)volatile標(biāo)記的變量進(jìn)行修改時(shí),會(huì)將其他緩存中存儲(chǔ)的修改前的變量清除,然后重新讀取。一般來說應(yīng)該是先在進(jìn)行修改的緩存A中修改為新值,然后通知其他緩存清除掉此變量,當(dāng)其他緩存B中的線程讀取此變量時(shí),會(huì)向總線發(fā)送消息,這時(shí)存儲(chǔ)新值的緩存A獲取到消息,將新值穿給B。最后將新值寫入內(nèi)存。當(dāng)變量需要更新時(shí)都是此步驟,volatile的作用是被其修飾的變量,每次更新時(shí),都會(huì)刷新上述步驟。
synchronized
(1)synchronized 方法
方法聲明時(shí)使用,放在范圍操作符(public等)之后,返回類型聲明(void等)之前.這時(shí),線程獲得的是成員鎖,即一次只能有一個(gè)線程進(jìn)入該方法,其他線程要想在此時(shí)調(diào)用該方法,只能排隊(duì)等候,當(dāng)前線程(就是在synchronized方法內(nèi)部的線程)執(zhí)行完該方法后,別的線程才能進(jìn)入。
示例:
public synchronized void synMethod(){ }
如在線程t1中有語句obj.synMethod(); 那么由于synMethod被synchronized修飾,在執(zhí)行該語句前, 需要先獲得調(diào)用者obj的對(duì)象鎖, 如果其他線程(如t2)已經(jīng)鎖定了obj (可能是通過obj.synMethod,也可能是通過其他被synchronized修飾的方法obj.otherSynMethod鎖定的obj), t1需要等待直到其他線程(t2)釋放obj, 然后t1鎖定obj, 執(zhí)行synMethod方法. 返回之前之前釋放obj鎖。
(2)synchronized 塊
對(duì)某一代碼塊使用,synchronized后跟括號(hào),括號(hào)里是變量,這樣,一次只有一個(gè)線程進(jìn)入該代碼塊.此時(shí),線程獲得的是成員鎖。
(3)synchronized (this)
當(dāng)兩個(gè)并發(fā)線程訪問同一個(gè)對(duì)象object中的這個(gè)synchronized(this)同步代碼塊時(shí),一個(gè)時(shí)間內(nèi)只能有一個(gè)線程得到執(zhí)行。另一個(gè)線程必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。
當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),其他線程對(duì)object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
然而,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),另一個(gè)線程仍然可以訪問該object中的除synchronized(this)同步代碼塊以外的部分。
第三個(gè)例子同樣適用其它同步代碼塊。也就是說,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),它就獲得了這個(gè)object的對(duì)象鎖。結(jié)果,其它線程對(duì)該object對(duì)象所有同步代碼部分的訪問都被暫時(shí)阻塞。
以上規(guī)則對(duì)其它對(duì)象鎖同樣適用。
第三點(diǎn)舉例說明:
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被訪問時(shí),線程中m4t2()依然可以被訪問。
wait()sleep() notify()/notifyAll()
wait():釋放占有的對(duì)象鎖,線程進(jìn)入等待池,釋放cpu,而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運(yùn)行程序。
sleep():不同的是,線程調(diào)用此方法后,會(huì)休眠一段時(shí)間,休眠期間,會(huì)暫時(shí)釋放cpu,但并不釋放對(duì)象鎖。也就是說,在休眠期間,其他線程依然無法進(jìn)入此代碼內(nèi)部。休眠結(jié)束,線程重新獲得cpu,執(zhí)行代碼。
wait()和sleep()最大的不同在于wait()會(huì)釋放對(duì)象鎖,而sleep()不會(huì)
notify(): 該方法會(huì)喚醒因?yàn)檎{(diào)用對(duì)象的wait()而等待的線程,其實(shí)就是對(duì)對(duì)象鎖的喚醒,從而使得wait()的線程可以有機(jī)會(huì)獲取對(duì)象鎖。調(diào)用notify()后,并不會(huì)立即釋放鎖,而是繼續(xù)執(zhí)行當(dāng)前代碼,直到synchronized中的代碼全部執(zhí)行完畢,才會(huì)釋放對(duì)象鎖。JVM則會(huì)在等待的線程中調(diào)度一個(gè)線程去獲得對(duì)象鎖,執(zhí)行代碼。需要注意的是,wait()和notify()必須在synchronized代碼塊中調(diào)用。
notifyAll()則是喚醒所有等待的線程。
lock
synchronized的缺陷
1)synchronized是java中的一個(gè)關(guān)鍵字,也就是說是Java語言內(nèi)置的特性。那么為什么會(huì)出現(xiàn)Lock呢?
如果一個(gè)代碼塊被synchronized修飾了,當(dāng)一個(gè)線程獲取了對(duì)應(yīng)的鎖,并執(zhí)行該代碼塊時(shí),其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會(huì)有兩種情況:
1)獲取鎖的線程執(zhí)行完了該代碼塊,然后線程釋放對(duì)鎖的占有;
2)線程執(zhí)行發(fā)生異常,此時(shí)JVM會(huì)讓線程自動(dòng)釋放鎖。
那么如果這個(gè)獲取鎖的線程由于要等待IO或者其他原因(比如調(diào)用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能等待,試想一下,這多么影響程序執(zhí)行效率。
因此就需要有一種機(jī)制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時(shí)間或者能夠響應(yīng)中斷),通過Lock就可以辦到。
再舉個(gè)例子:當(dāng)有多個(gè)線程讀寫文件時(shí),讀操作和寫操作會(huì)發(fā)生沖突現(xiàn)象,寫操作和寫操作會(huì)發(fā)生沖突現(xiàn)象,但是讀操作和讀操作不會(huì)發(fā)生沖突現(xiàn)象。
但是采用synchronized關(guān)鍵字來實(shí)現(xiàn)同步的話,就會(huì)導(dǎo)致一個(gè)問題:
如果多個(gè)線程都只是進(jìn)行讀操作,所以當(dāng)一個(gè)線程在進(jìn)行讀操作時(shí),其他線程只能等待無法進(jìn)行讀操作。
因此就需要一種機(jī)制來使得多個(gè)線程都只是進(jìn)行讀操作時(shí),線程之間不會(huì)發(fā)生沖突,通過Lock就可以辦到。
另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個(gè)是synchronized無法辦到的。
總結(jié)一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點(diǎn):
1)Lock不是Java語言內(nèi)置的,synchronized是Java語言的關(guān)鍵字,因此是內(nèi)置特性。Lock是一個(gè)類,通過這個(gè)類可以實(shí)現(xiàn)同步訪問;
2)Lock和synchronized有一點(diǎn)非常大的不同,采用synchronized不需要用戶去手動(dòng)釋放鎖,當(dāng)synchronized方法或者synchronized代碼塊執(zhí)行完之后,系統(tǒng)會(huì)自動(dòng)讓線程釋放對(duì)鎖的占用;而Lock則必須要用戶去手動(dòng)釋放鎖,如果沒有主動(dòng)釋放鎖,就有可能導(dǎo)致出現(xiàn)死鎖現(xiàn)象。
(2)java.util.concurrent.locks包下常用的類
public interface Lock { //獲取鎖,如果鎖被其他線程獲取,則進(jìn)行等待 void lock(); //當(dāng)通過這個(gè)方法去獲取鎖時(shí),如果線程正在等待獲取鎖,則這個(gè)線程能夠響應(yīng)中斷,即中斷線程的等待狀態(tài)。也就使說,當(dāng)兩個(gè)線程同時(shí)通過lock.lockInterruptibly()想獲取某個(gè)鎖時(shí),假若此時(shí)線程A獲取到了鎖,而線程B只有在等待,那么對(duì)線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。 void lockInterruptibly() throws InterruptedException; /**tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成 *功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回 *false,也就說這個(gè)方法無論如何都會(huì)立即返回。在拿不到鎖時(shí)不會(huì)一直在那等待。*/ boolean tryLock(); //tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區(qū)別在于這個(gè)方法在拿不到鎖時(shí)會(huì)等待一定的時(shí)間,在時(shí)間期限之內(nèi)如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true。 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); //釋放鎖 Condition newCondition(); }
通常使用lock進(jìn)行同步:
Lock lock = ...; lock.lock(); try{ //處理任務(wù) }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 }
trylock使用方法:
Lock lock = ...; if(lock.tryLock()) { try{ //處理任務(wù) }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 } }else { //如果不能獲取鎖,則直接做其他事情 }
lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException { lock.lockInterruptibly(); try { //..... } finally { lock.unlock(); } }
注意:
當(dāng)一個(gè)線程獲取了鎖之后,是不會(huì)被interrupt()方法中斷的。因?yàn)楸旧碓谇懊娴奈恼轮兄v過多帶帶調(diào)用interrupt()方法不能中斷正在運(yùn)行過程中的線程,只能中斷阻塞過程中的線程。
而用synchronized修飾的話,當(dāng)一個(gè)線程處于等待某個(gè)鎖的狀態(tài),是無法被中斷的,只有一直等待下去。
(3)ReentrantLock
ReentrantLock,意思是“可重入鎖”,是唯一實(shí)現(xiàn)了Lock接口的類,并且ReentrantLock提供了更多的方法。
public class Test { private ArrayListarrayList = new ArrayList (); private Lock lock = new ReentrantLock(); //注意這個(gè)地方 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都是可重入鎖,可重入性在我看來實(shí)際上表明了鎖的分配機(jī)制:基于線程的分配,而不是基于方法調(diào)用的分配。舉個(gè)簡單的例子,當(dāng)一個(gè)線程執(zhí)行到某個(gè)synchronized方法時(shí),比如說method1,而在method1中會(huì)調(diào)用另外一個(gè)synchronized方法method2,此時(shí)線程不必重新去申請鎖,而是可以直接執(zhí)行方法method2。
代碼解釋:
class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { } }
上述代碼中的兩個(gè)方法method1和method2都用synchronized修飾了,假如某一時(shí)刻,線程A執(zhí)行到了method1,此時(shí)線程A獲取了這個(gè)對(duì)象的鎖,而由于method2也是synchronized方法,假如synchronized不具備可重入性,此時(shí)線程A需要重新申請鎖。但是這就會(huì)造成一個(gè)問題,因?yàn)榫€程A已經(jīng)持有了該對(duì)象的鎖,而又在申請獲取該對(duì)象的鎖,這樣就會(huì)線程A一直等待永遠(yuǎn)不會(huì)獲取到的鎖。
而由于synchronized和Lock都具備可重入性,所以不會(huì)發(fā)生上述現(xiàn)象。
volatile和synchronized區(qū)別
1)volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住.
2)volatile僅能使用在變量級(jí)別,synchronized則可以使用在變量,方法.
3)volatile僅能實(shí)現(xiàn)變量的修改可見性,而synchronized則可以保證變量的修改可見性和原子性.
《Java編程思想》上說,定義long或double變量時(shí),如果使用volatile關(guān)鍵字,就會(huì)獲得(簡單的賦值與返回操作)原子性。
4)volatile不會(huì)造成線程的阻塞,而synchronized可能會(huì)造成線程的阻塞.
5)當(dāng)一個(gè)域的值依賴于它之前的值時(shí),volatile就無法工作了,如n=n+1,n++等。如果某個(gè)域的值受到其他域的值的限制,那么volatile也無法工作,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。
6)使用volatile而不是synchronized的唯一安全的情況是類中只有一個(gè)可變的域。
synchronized和lock區(qū)別
1)Lock是一個(gè)接口,而synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語言實(shí)現(xiàn);
2)synchronized在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程占有的鎖,因此不會(huì)導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時(shí),如果沒有主動(dòng)通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時(shí)需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行,使用synchronized時(shí),等待的線程會(huì)一直等待下去,不能夠響應(yīng)中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個(gè)線程進(jìn)行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當(dāng)競爭資源非常激烈時(shí)(即有大量線程同時(shí)競爭),此時(shí)Lock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說,在具體使用時(shí)要根據(jù)適當(dāng)情況選擇。
CAS
CAS有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。
非阻塞算法 (nonblocking algorithms)
一個(gè)線程的失敗或者掛起不應(yīng)該影響其他線程的失敗或掛起的算法。
現(xiàn)代的CPU提供了特殊的指令,可以自動(dòng)更新共享數(shù)據(jù),而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。
拿出AtomicInteger來研究在沒有鎖的情況下是如何做到數(shù)據(jù)正確性的。
private volatile int value;
首先毫無以為,在沒有鎖的機(jī)制下可能需要借助volatile原語,保證線程間的數(shù)據(jù)是可見的(共享的)。
這樣才獲取變量的值的時(shí)候才能直接讀取。
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操作,每次從內(nèi)存中讀取數(shù)據(jù)然后將此數(shù)據(jù)和+1后的結(jié)果進(jìn)行CAS操作,如果成功就返回結(jié)果,否則重試直到成功為止。
而compareAndSet利用JNI來完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) {??? ??? return unsafe.compareAndSwapInt(this, valueOffset, expect, update); ??? }
整體的過程就是這樣子的,利用CPU的CAS指令,同時(shí)借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。
其中
unsafe.compareAndSwapInt(this, valueOffset, expect, update); 類似: if (this ==?expect) { ? this =?update ?return true; } else { return false; }
那么問題就來了,成功過程中需要2個(gè)步驟:比較this ==?expect,替換this =?update,compareAndSwapInt如何這兩個(gè)步驟的原子性呢? 參考CAS的原理。
CAS原理
CAS通過調(diào)用JNI的代碼實(shí)現(xiàn)的。JNI:Java Native Interface為JAVA本地調(diào)用,允許java調(diào)用其他語言。
而compareAndSwapInt就是借助C來調(diào)用CPU底層指令實(shí)現(xiàn)的。
Unsafe類中的compareAndSwapInt,是一個(gè)本地方法,該方法的實(shí)現(xiàn)位于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在內(nèi)存中的地址。
通過Atomic::cmpxchg實(shí)現(xiàn)比較替換,其中參數(shù)x是即將更新的值,參數(shù)e是原內(nèi)存的值。
下面從分析比較常用的CPU(intel x86)來解釋CAS的實(shí)現(xiàn)原理。
下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
可以看到這是個(gè)本地方法調(diào)用。這個(gè)本地方法在openjdk中依次調(diào)用的c++代碼為:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個(gè)本地方法的最終實(shí)現(xiàn)在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(對(duì)應(yīng)于windows操作系統(tǒng),X86處理器)。下面是對(duì)應(yīng)于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 } }
如上面源代碼所示,程序會(huì)根據(jù)當(dāng)前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運(yùn)行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運(yùn)行,就省略lock前綴(單處理器自身會(huì)維護(hù)單處理器內(nèi)的順序一致性,不需要lock前綴提供的內(nèi)存屏障效果)。
intel的手冊對(duì)lock前綴的說明如下:
確保對(duì)內(nèi)存的讀-改-寫操作原子執(zhí)行。在Pentium及Pentium之前的處理器中,帶有l(wèi)ock前綴的指令在執(zhí)行期間會(huì)鎖住總線,使得其他處理器暫時(shí)無法通過總線訪問內(nèi)存。很顯然,這會(huì)帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎(chǔ)上做了一個(gè)很有意義的優(yōu)化:如果要訪問的內(nèi)存區(qū)域(area of memory)在lock前綴指令執(zhí)行期間已經(jīng)在處理器內(nèi)部的緩存中被鎖定(即包含該內(nèi)存區(qū)域的緩存行當(dāng)前處于獨(dú)占或以修改狀態(tài)),并且該內(nèi)存區(qū)域被完全包含在單個(gè)緩存行(cache line)中,那么處理器將直接執(zhí)行該指令。由于在指令執(zhí)行期間該緩存行會(huì)一直被鎖定,其它處理器無法讀/寫該指令要訪問的內(nèi)存區(qū)域,因此能保證指令執(zhí)行的原子性。這個(gè)操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執(zhí)行開銷,但是當(dāng)多處理器之間的競爭程度很高或者指令訪問的內(nèi)存地址未對(duì)齊時(shí),仍然會(huì)鎖住總線。
禁止該指令與之前和之后的讀和寫指令重排序。
把寫緩沖區(qū)中的所有數(shù)據(jù)刷新到內(nèi)存中。
在這里可以看到是用嵌入的匯編實(shí)現(xiàn)的, 關(guān)鍵CPU指令是?cmpxchg
到這里沒法再往下找代碼了. 也就是說CAS的原子性實(shí)際上是CPU實(shí)現(xiàn)的. 其實(shí)在這一點(diǎn)上還是有排他鎖的. 只是比起用synchronized, 這里的排他時(shí)間要短的多. 所以在多線程情況下性能會(huì)比較好.
代碼里有個(gè)alternative?for?InterlockedCompareExchange
這個(gè)InterlockedCompareExchange是WINAPI里的一個(gè)函數(shù), 做的事情和上面這段匯編是一樣的
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)
關(guān)于CPU的鎖有如下3種:
處理器自動(dòng)保證基本內(nèi)存操作的原子性
首先處理器會(huì)自動(dòng)保證基本的內(nèi)存操作的原子性。處理器保證從系統(tǒng)內(nèi)存當(dāng)中讀取或者寫入一個(gè)字節(jié)是原子的,意思是當(dāng)一個(gè)處理器讀取一個(gè)字節(jié)時(shí),其他處理器不能訪問這個(gè)字節(jié)的內(nèi)存地址。奔騰6和最新的處理器能自動(dòng)保證單處理器對(duì)同一個(gè)緩存行里進(jìn)行16/32/64位的操作是原子的,但是復(fù)雜的內(nèi)存操作處理器不能自動(dòng)保證其原子性,比如跨總線寬度,跨多個(gè)緩存行,跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個(gè)機(jī)制來保證復(fù)雜內(nèi)存操作的原子性。?
使用總線鎖保證原子性
第一個(gè)機(jī)制是通過總線鎖保證原子性。如果多個(gè)處理器同時(shí)對(duì)共享變量進(jìn)行讀改寫(i++就是經(jīng)典的讀改寫操作)操作,那么共享變量就會(huì)被多個(gè)處理器同時(shí)進(jìn)行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值會(huì)和期望的不一致,舉個(gè)例子:如果i=1,我們進(jìn)行兩次i++操作,我們期望的結(jié)果是3,但是有可能結(jié)果是2。如下圖
原因是有可能多個(gè)處理器同時(shí)從各自的緩存中讀取變量i,分別進(jìn)行加一操作,然后分別寫入系統(tǒng)內(nèi)存當(dāng)中。那么想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時(shí)候,CPU2不能操作緩存了該共享變量內(nèi)存地址的緩存。
處理器使用總線鎖就是來解決這個(gè)問題的。所謂總線鎖就是使用處理器提供的一個(gè)LOCK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí),其他處理器的請求將被阻塞住,那么該處理器可以獨(dú)占使用共享內(nèi)存。
使用緩存鎖保證原子性
第二個(gè)機(jī)制是通過緩存鎖定保證原子性。在同一時(shí)刻我們只需保證對(duì)某個(gè)內(nèi)存地址的操作是原子性即可,但總線鎖定把CPU和內(nèi)存之間通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù),所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進(jìn)行優(yōu)化。
頻繁使用的內(nèi)存會(huì)緩存在處理器的L1,L2和L3高速緩存里,那么原子操作就可以直接在處理器內(nèi)部緩存中進(jìn)行,并不需要聲明總線鎖,在奔騰6和最近的處理器中可以使用“緩存鎖定”的方式來實(shí)現(xiàn)復(fù)雜的原子性。所謂“緩存鎖定”就是如果緩存在處理器緩存行中內(nèi)存區(qū)域在LOCK操作期間被鎖定,當(dāng)它執(zhí)行鎖操作回寫內(nèi)存時(shí),處理器不在總線上聲言LOCK#信號(hào),而是修改內(nèi)部的內(nèi)存地址,并允許它的緩存一致性機(jī)制來保證操作的原子性,因?yàn)榫彺嬉恢滦詸C(jī)制會(huì)阻止同時(shí)修改被兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當(dāng)其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時(shí)會(huì)起緩存行無效,在例1中,當(dāng)CPU1修改緩存行中的i時(shí)使用緩存鎖定,那么CPU2就不能同時(shí)緩存了i的緩存行。
但是有兩種情況下處理器不會(huì)使用緩存鎖定。第一種情況是:當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個(gè)緩存行(cache line),則處理器會(huì)調(diào)用總線鎖定。第二種情況是:有些處理器不支持緩存鎖定。對(duì)于Inter486和奔騰處理器,就算鎖定的內(nèi)存區(qū)域在處理器的緩存行中也會(huì)調(diào)用總線鎖定。
以上兩個(gè)機(jī)制我們可以通過Inter處理器提供了很多LOCK前綴的指令來實(shí)現(xiàn)。比如位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其他一些操作數(shù)和邏輯指令,比如ADD(加),OR(或)等,被這些指令操作的內(nèi)存區(qū)域就會(huì)加鎖,導(dǎo)致其他處理器不能同時(shí)訪問它。
CAS缺點(diǎn)
CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環(huán)時(shí)間長開銷大和只能保證一個(gè)共享變量的原子操作
ABA問題。因?yàn)镃AS需要在操作值的時(shí)候檢查下值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個(gè)值原來是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實(shí)際上卻變化了。ABA問題的解決思路就是使用版本號(hào)。在變量前面追加上版本號(hào),每次變量更新的時(shí)候把版本號(hào)加一,那么A-B-A 就會(huì)變成1A-2B-3A。
從Java1.5開始JDK的atomic包里提供了一個(gè)類AtomicStampedReference來解決ABA問題。這個(gè)類的compareAndSet方法作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。
關(guān)于ABA問題參考文檔:?http://blog.hesey.net/2011/09...
循環(huán)時(shí)間長開銷大。自旋CAS如果長時(shí)間不成功,會(huì)給CPU帶來非常大的執(zhí)行開銷。如果JVM能支持處理器提供的pause指令那么效率會(huì)有一定的提升,pause指令有兩個(gè)作用,第一它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會(huì)消耗過多的執(zhí)行資源,延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本,在一些處理器上延遲時(shí)間是零。第二它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執(zhí)行效率。
只能保證一個(gè)共享變量的原子操作。當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無法保證操作的原子性,這個(gè)時(shí)候就可以用鎖,或者有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來操作。比如有兩個(gè)共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對(duì)象之間的原子性,你可以把多個(gè)變量放在一個(gè)對(duì)象里來進(jìn)行CAS操作。
concurrent包的實(shí)現(xiàn)
由于java的CAS同時(shí)具有 volatile 讀和volatile寫的內(nèi)存語義,因此Java線程之間的通信現(xiàn)在有了下面四種方式:
A線程寫volatile變量,隨后B線程讀這個(gè)volatile變量。
A線程寫volatile變量,隨后B線程用CAS更新這個(gè)volatile變量。
A線程用CAS更新一個(gè)volatile變量,隨后B線程用CAS更新這個(gè)volatile變量。
A線程用CAS更新一個(gè)volatile變量,隨后B線程讀這個(gè)volatile變量。
Java的CAS會(huì)使用現(xiàn)代處理器上提供的高效機(jī)器級(jí)別原子指令,這些原子指令以原子方式對(duì)內(nèi)存執(zhí)行讀-改-寫操作,這是在多處理器中實(shí)現(xiàn)同步的關(guān)鍵(從本質(zhì)上來說,能夠支持原子性讀-改-寫指令的計(jì)算機(jī)器,是順序計(jì)算圖靈機(jī)的異步等價(jià)機(jī)器,因此任何現(xiàn)代的多處理器都會(huì)去支持某種能對(duì)內(nèi)存執(zhí)行原子性讀-改-寫操作的原子指令)。同時(shí),volatile變量的讀/寫和CAS可以實(shí)現(xiàn)線程之間的通信。把這些特性整合在一起,就形成了整個(gè)concurrent包得以實(shí)現(xiàn)的基石。如果我們仔細(xì)分析concurrent包的源代碼實(shí)現(xiàn),會(huì)發(fā)現(xiàn)一個(gè)通用化的實(shí)現(xiàn)模式:
首先,聲明共享變量為volatile;
然后,使用CAS的原子條件更新來實(shí)現(xiàn)線程之間的同步;
同時(shí),配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實(shí)現(xiàn)線程之間的通信。
AQS,非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎(chǔ)類都是使用這種模式來實(shí)現(xiàn)的,而concurrent包中的高層類又是依賴于這些基礎(chǔ)類來實(shí)現(xiàn)的。從整體來看,concurrent包的實(shí)現(xiàn)示意圖如下:
參考
https://blog.csdn.net/ztchun/...
http://www.cnblogs.com/imqsl/...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/71745.html
摘要:異步非阻塞方式,任務(wù)的完成的通知由其他線程發(fā)出。并發(fā)并行死鎖饑餓活鎖死鎖線程持有,線程持有。如等,在多線程情況下,該操作不是原子級(jí)別的而是原子的,所以一般用于狀態(tài)標(biāo)記。 同步/異步、阻塞/非阻塞 同步/異步是 API 被調(diào)用者的通知方式。阻塞/非阻塞則是 API 調(diào)用者的等待方式(線程掛機(jī)/不掛起)。 同步非阻塞 Future方式,任務(wù)的完成要主線程自己判斷。如NIO,后臺(tái)有多個(gè)任務(wù)在...
摘要:前情提要深入理解內(nèi)存模型四鎖的釋放獲取建立的關(guān)系鎖是并發(fā)編程中最重要的同步機(jī)制。鎖內(nèi)存語義的實(shí)現(xiàn)本文將借助的源代碼,來分析鎖內(nèi)存語義的具體實(shí)現(xiàn)機(jī)制。請看下篇深入理解內(nèi)存模型六 前情提要 深入理解Java內(nèi)存模型(四)—— volatile 鎖的釋放-獲取建立的happens before 關(guān)系 鎖是java并發(fā)編程中最重要的同步機(jī)制。鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向...
摘要:當(dāng)前線程在超時(shí)時(shí)間內(nèi)被中斷超時(shí)時(shí)間結(jié)束,返回釋放鎖獲取等待通知組件,該組件和當(dāng)前的鎖綁定,當(dāng)前線程只有獲取了鎖,才能調(diào)用該組件的方法,調(diào)用后,當(dāng)前線程將釋放鎖。同步器是實(shí)現(xiàn)鎖的關(guān)鍵,在鎖的實(shí)現(xiàn)中聚合同步器,利用同步器實(shí)現(xiàn)鎖的語義。 本文在參考java并發(fā)編程實(shí)戰(zhàn)后完成,參考內(nèi)容較多 Java中的鎖 鎖是用來控制多線程訪問共享資源的方式,一個(gè)鎖能夠防止多個(gè)線程同事訪問共享資源。在Lock...
摘要:線程的這種交叉操作會(huì)導(dǎo)致線程不安全。原子操作是在多線程環(huán)境下避免數(shù)據(jù)不一致必須的手段。如果聲明一個(gè)域?yàn)橐恍┣闆r就可以確保多線程訪問到的變量是最新的。并發(fā)要求一個(gè)線程對(duì)對(duì)象進(jìn)行了操作,對(duì)象發(fā)生了變化,這種變化應(yīng)該對(duì)其他線程是可見的。 雖是讀書筆記,但是如轉(zhuǎn)載請注明出處 http://segmentfault.com/blog/exploring/ .. 拒絕伸手復(fù)制黨 一個(gè)問題: ...
摘要:這個(gè)規(guī)則比較好理解,無論是在單線程環(huán)境還是多線程環(huán)境,一個(gè)鎖處于被鎖定狀態(tài),那么必須先執(zhí)行操作后面才能進(jìn)行操作。線程啟動(dòng)規(guī)則獨(dú)享的方法先行于此線程的每一個(gè)動(dòng)作。 1. 指令重排序 關(guān)于指令重排序的概念,比較復(fù)雜,不好理解。我們從一個(gè)例子分析: public class SimpleHappenBefore { /** 這是一個(gè)驗(yàn)證結(jié)果的變量 */ private st...
閱讀 2021·2019-08-30 15:52
閱讀 2975·2019-08-29 16:09
閱讀 1323·2019-08-28 18:30
閱讀 2453·2019-08-26 12:24
閱讀 1090·2019-08-26 12:12
閱讀 2273·2019-08-26 10:45
閱讀 565·2019-08-23 17:52
閱讀 810·2019-08-23 16:03