国产成人精品久久免费动漫-国产成人精品天堂-国产成人精品区在线观看-国产成人精品日本-a级毛片无码免费真人-a级毛片毛片免费观看久潮喷

您的位置:首頁技術文章
文章詳情頁

Java多線程中Lock鎖的使用總結

瀏覽:2日期:2022-08-27 15:25:29

多核時代

摩爾定律告訴我們:當價格不變時,集成電路上可容納的晶體管數目,約每隔18個月便會增加一倍,性能也將提升一倍。換言之,每一美元所能買到的電腦性能,將每隔18個月翻兩倍以上。然而最近摩爾定律似乎遇到了麻煩,目前微處理器的集成度似乎到了極限,在目前的制造工藝和體系架構下很難再提高單個處理器的速度了,否則它就被燒壞了。所以現在的芯片制造商改變了策略,轉而在一個電路板上集成更多的處理器,也就是我們現在常見的多核處理器。

這就給軟件行業帶來麻煩(也可以說帶來機會,比如說就業機會,呵呵)。原來的情況是:我買一臺頻率比原來快一倍的處理器,那么我的程序就比原來快一倍,軟件工程師什么也不用干。現在不一樣了,我買一臺雙核的處理器,我的程序和原來一樣慢,當然這條機器同時處理的任務可以變多了,但是對于單個任務來說并沒有幫助。

在幾年前,并發(Concurrent)和并行(Paralleling)程序設計還是在少量的地方使用,現在在個人的PC機上已經是很常見了。(Concurrency and parallelism的區別參考 這個帖子)

造個諸葛亮的價錢遠遠高于造三個臭皮匠!多核是在一臺機器上的并發,但是單機也是會到極限,所以分布式的計算也是類似的思路,用大量普通的機器協作完成一項任務。

但是要想編寫一個正確并且高效的能利用多核的多線程程序不是件容易的是,更別說分布式的情況(網絡問題,機器故障,負載均衡,。。。)。現在的編譯器沒有辦法把單線程的程序自動編譯成一個多線程的版本(如果到了那一天,估計所有的程序員就失業了)。所以只能提供一些語言上的支持(比如scala/erlang)或者mapreduce這樣的框架。

Java雖然沒有提供scala那樣的基于消息的模型,但是也提供了豐富的concurrent特性,并且屏蔽了平臺的相關性(這不是件容易的事,比如多個處理器有自己的緩存,他們寫的東西不會離開被其它處理器看到),下面我們看看java的內存模型(JMM)

JMM(Java Memory Model)

并行程序有很多模型,比如共享內存模型,消息傳遞模型等等。這些模型或多或少的利用了平臺相關的特性(在并行程序設計里很難回避平臺的特性以便高效的通信),Java抽象出了自己的內存模型,使得開放人員看不到平臺的差異(這不是件容易的事),不過即使這樣,和傳統程序不同,我們還是不能完全不了解一些體系架構的細節問題,至少我們得了解一些。

在共享內存的多處理器體系架構里(我們現在用的服務器甚至筆記本都是),每個處理器都有自己的局部緩存并定期的使之與內存同步。不同的處理器架構保證了不同程度的緩存一致性(cache coherence),所以操作系統,編譯器和運行時環境必須一起努力來彌補平臺的差異性。

讓每個處理器都知道其它處理器的狀態的代價是非常昂貴的,所以大多數架構都不會保證一致性,這通常不會有什么問題:進程/線程直接并不共享信息,編譯器可以調整代碼執行順序以便提高效率,我們都很開心。當然也有需要在線程之間進行同步的時候,比如某個線程要讀取到另一個線程寫入的信息,這個時候緩存里的數據就得同步到內存里才行。所以這些體系架構都提供了一些指令來完成數據的同步(當然這些指令是非常費時的,能不做就盡量不做)。這些指令一般叫做memory barriers or fences。當然只是很底層的一些東西,所幸Java提供了一些高層的抽象,讓我們的生活變得容易一些。

sequential consistency: 我們假設一個線程執行(可能在多個處理器上切換),每個變量讀取到的值都是最新的修改(也就是Cache里的立馬生效),這樣得到的結果是我們預期的。

但是讓我們意外的事情是:如果我們不做任何事情,那么很可能會出現錯誤,比如下面的這個例子:

public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready)Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; }}

我們在主線程里先讓number=42(初始值是0),然后讓ready=true,而另一個線程不斷堅持是否ready,如果ready,那么讀出number。很自然的我們期望子線程打印出42,但是很可能結果會另我們失望。編譯器可能會調換number=42 和 ready=true的順序(思考一下為什么它要這么干?為什么在單線程的情況下沒有問題?),另外子線程可能永遠在while里死循環。為什么?子線程會永遠看不到ready的變化?這也許讓很多人吃驚,事實確實如此,JSR并不保證這一點(雖然大多數時候子線程能夠退出),參考這個帖子和JMM的文章

vilatile和snychronized(intrinsic Lock)

vilatile關鍵字告訴編譯器,一個線程對某個變量的修改立即對所有其它線程看見,加上這個能保證上面的程序不會死循環。但是不能保證讀到42,也就是保證number=42和ready=true的執行順序,要保證這點就要用到synchronized。

synchronized能夠保證執行的順序,除此之外,它也能保證可見性。

public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { boolean r=false; while (true){synchronized(NoVisibility.class){ r=ready;}if(r) break;else Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); synchronized(NoVisibility.class){ number = 42; ready = true; } }}

synchronized(NoVisibility.class){ number = 42; ready = true; }

這段代碼保證了兩個語句的執行順序

synchronized(NoVisibility.class){ r=ready; }

這保證子線程能看到ready的變化 注意他們必須synchronized同一個對象,如果是下面的代碼,則不能有任何保障。為什么?試想任何synchronized里的變量必須立即對所有的可見,那么代價太大, 比如我有這樣的需求:我只要求兩個語句順序執行,它是否對別人可見我并不關心。

synchronized(AnotherObject){ r=ready; }

每個對象都有個Monitor,所以synchronized也經常叫Monitor Lock,另外這個鎖是語言內置的,所以也叫Intrinsic Lock。 這兩個關鍵字是java1.5之前就有了,在java1.5之后新引進了java.util.concurrent包,這里有我們需要關注的很多東西,這里我們只關心Lock相關的接口和類。 不過synchronized來解決互斥不是很完美嗎?我為什么要花力氣搞這些新鮮東西呢?下面我們來看看synchronized解決不了(或者很難解決)的問題

銀行轉賬的例子

// Warning: deadlock-prone!public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { synchronized (fromAccount) { synchronized (toAccount) { if (fromAccount.getBalance().compareTo(amount) < 0)throw new InsufficientFundsException(); else {fromAccount.debit(amount);toAccount.credit(amount); } } }}

比如我要在兩個用戶之間轉賬,為了防止意外,我必須同時鎖定兩個賬戶。但是這可能造成死鎖。比如:

A: transferMoney(myAccount, yourAccount, 10);B: transferMoney(yourAccount, myAccount, 20);

當線程A鎖住myAccount時,B鎖住了toAccount,這個時候A嘗試鎖住toAccount,但是已經被B鎖住,所以A不能繼續運行,同理B也不能運行,造成死鎖。

怎么解決呢?你也許回想,我先鎖住一個賬戶,然后'嘗試'鎖定另一個賬戶,如果“失敗”,那么我釋放所有的鎖,“休息”一下再繼續嘗試,當然兩個線程節拍一致的話,可能造成“活鎖”

可惜synchronized不能提供這樣的語義,它一旦嘗試加鎖,只能拿到鎖,你不能控制它,比如你可能有這樣的需求:嘗試拿鎖30s,如果拿不到就算了,synchronized是沒辦法滿足這樣的需求的。另外你使用“鴕鳥”策略來解決死鎖:什么也不干,如果死鎖了,kill他們,重啟他們。這種策略看起來很瘋狂,不過如果死鎖的概率很多,而避免死鎖的算法很復雜,那這也是可以一試的策略(那一堆死鎖發生的充分必要條件太麻煩了!!!)。下面我們仔細的來看看java1.5后提供的Lock接口及其相關類。

Lock接口

Lock的基本用法如下,為了防止異常退出時沒有釋放鎖,一般都在拿到鎖后立馬try,try住所有臨界區的代碼,然后finally釋放鎖。

主要和synchronized的區別,synchronized里我們不用操心這些,如果synchronized保護的代碼拋出異常,那么jvm會釋放掉Monitor Lock。

Lock l = ... l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }

Lock.lock()在鎖定成功后釋放鎖之前,它所保護的代碼段必須與使用synchronized保護的代碼段有相同的語義(可見性,順序性)。

所以從這個角度來說,Lock完全可以代替synchronized,那么是否應該拋棄掉synchronized呢?答案是否定的。

是否應該拋棄synchronized?

在java5引進Lock后,實現了Lock接口的類就是ReentrantLock(呆會再解釋Reentrant),因為java5之前synchronized的實現很爛,同樣是為了實現互斥,ReentrantLock會比synchronized速度上快很多,不過到了jdk6之后就不是這樣了,下面是一個測試結果: from book 'Java Concurrency in Practice' 橫軸是線程數,縱軸是ReentrantLock的吞吐量/IntrinsicLock的吞吐量。

可以看出,jdk5中,ReentrantLock快很多,但是到了jdk6,他們就沒什么大的差別了。

synchronized的優點:鎖的釋放是語言內置的,不會出現忘記釋放鎖的情況,另外由于是語言內置的支持,調試是能很快知道鎖被哪個線程持有,它加鎖的次數。而Lock只是util.concurrent一個普通的類,所以調試器并不知道這個鎖的任何信息,它只是一個普通的對象(當然你可以仔細觀察每個線程的stack frame來看它在等待鎖)。

所以建議:如果只是為了實現互斥,那么使用synchronized(扔掉jdk5吧,現在都java7了),如果想用Lock附加的功能,那么才使用Lock。

下面回來繼續看Lock接口。

Interface Lock

public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition();}

void lock();

嘗試獲取鎖。如果鎖被別人拿著,那么當前線程不在執行,也不能被調度,直到拿到鎖為止。

void lockInterruptibly() throws InterruptedException

嘗試獲取鎖,除非被interrupted。如果鎖可以獲取,那么立刻返回。

如果無非獲取鎖,那么線程停止執行,并且不能被再調度,直到:

當前線程獲得鎖 如果鎖的實現支持interruption,并且有其它線程interrupt當前線程。

仔細閱讀javadoc的第二個情況:Lock接口并不要求Lock的實現支持interruption,不過sun jdk的實現都是支持的。 這個函數在下面兩個情況下拋出InterruptedException:

如果鎖的實現支持interruption,并且有其它線程interrupt當前線程。 線程調用這個函數之前就被設置了interrupted狀態位

可以發現這個方法并不區分這個interrupted狀態位是之前就有的還是lock過程中產生的。不管如果,拋出異常后會清除interrupted標記。

使用這個方法,我們可以中斷某個等鎖的線程,比如我們檢測到了死鎖,那么我們可以中斷這個線程

boolean tryLock()

嘗試獲取鎖,如果可以,那么鎖住對象然后返回true,否則返回false,不管怎么樣,這個方法會立即返回。下面的例子展示了用這個方法來解決前面轉賬的死鎖:

public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException { long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModulusNanos(timeout, unit); long stopTime = System.nanoTime() + unit.toNanos(timeout); while (true) { if (fromAcct.lock.tryLock()) { try {if (toAcct.lock.tryLock()) { try { if (fromAcct.getBalance().compareTo(amount)< 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); return true; } } finally { toAcct.lock.unlock(); } } } finally { fromAcct.lock.unlock(); } } if (System.nanoTime() < stopTime) return false; NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); }}

tryLock boolean tryLock(long time, TimeUnit unit) throws InterruptedException

和tryLock類似,不過不是立即返回,而是嘗試一定時間后還拿不到鎖就返回

unlock

釋放鎖

newCondition

暫且不管

Class ReentrantLock

這是sun jdk(open jdk)里唯一直接實現了Lock接口的類,所以如果你想用Lock的那些特性,比如tryLock,那么就應該首先考慮它

首先我們解釋一下Reentrant

Reentrant翻譯成中文應該是“可重入”,對于鎖來說,可重入是指如果一個線程已拿到過一把鎖,那么它可以再次拿到鎖。

聽起來似乎沒有什么意思,讓我們來看看“不可重入”鎖可能的一些問題和需要使用”可重入“鎖的場景吧。

public class Widget { public synchronized void doSomething() { ... }} public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ': calling doSomething'); super.doSomething(); }} Widget widget=new LoggingWidget(); widget.doSomething();

設想這樣一個應用場景:我們有一個圖的數據結構,我們需要遍歷所有節點,找到滿足某些條件的節點,鎖定所有這些節點,然后對他們進行一些操作。由于圖的遍歷可能重復訪問某個節點,如果簡單的鎖定每個滿足條件的節點,那么可能死鎖。當然我們可以自己用程序記下哪些節點已經訪問過了,不過也可以把這就事情交給ReentrantLock,第二次鎖定某個對象也會成功并立即返回。那么你可能會問,我釋放鎖的時候怎么記得它鎖定過了多少次呢?如果釋放少了,那么會死鎖;釋放多了,可能也會有問題(有些鎖實現會拋出異常,但是JMM好像沒有定義)。

【上面的場景參考http://stackoverflow.com/questions/1312259/what-is-the-re-entrant-lock-and-concept-in-general】 不用擔心,ReentrantLock提供了getHoldCount方法,最后釋放這么多次就好了。

ReentrantLock會記下當前拿鎖的線程,已經拿鎖的次數,每次unlock都會減一,如果為零了,那么釋放鎖,另一個線程到鎖并且計數器值為一。

ReentrantLock的構造函數可以接受一個fairness的參數。如果為true,那么它會傾向于把鎖給等待時間最長的線程。但是這樣的代價也是巨大的: 橫軸是并發線程數,參考方法是ConcurrentHashMap,另外分別用Nonfair Lock和 fair Lock封裝普通的HashMap,可以看到,是否fair的差別是非常巨大的。 正如前面所說的,ReentrantLock是支持Interrupted的。

Interface ReadWriteLock

有的應用場景下,有兩類角色:Reader和Writer。Reader讀取數據,Writer更新數據。多個Reader同時讀取是沒有問題的,但是Reader們和Writer是互斥的,并且Writer和Writer也是互斥的。而且很多應用中,Reader會很多,而Writer會比較少。這個接口就是為了解決這類特殊場景的。

public interface ReadWriteLock { Lock readLock(); Lock writeLock();} 用法:ReadWriteLock rwl = ...;//Reader threadsread(){ rwl.readLock().lock(); try{ //entering critical setion }finally{ rwl.readLock().unlock(); }}write(){ rwl.writeLock().lock(); try{ //entering critical setion }finally{ rwl.writeLock().unlock(); }}

Class ReentrantReadWriteLock

這是Sun jdk里唯一實現ReadWriteLock接口的類。 這個類的特性:

獲取鎖的順序

這個類并不傾向Reader或者Writer,不過有個fairness的策略 非公平模式(默認)

如果很多Reader和Writer的話,很可能Reder一直能獲取鎖,而Writer可能會饑餓

公平模式

這種模式下,會盡量以請求鎖的順序來保證公平性。當前鎖釋放以后,等待時間最長的Writer或者一組Reader(Reader是一伙的!)獲取鎖。 如果鎖被拿著,這時Writer來了,他會開始排隊;如果Reader來了,如果它之前沒有Writer并且當前拿鎖的是Reader,那么它直接就拿到鎖,當然如果是Writer拿著,那么它也只能排 隊等鎖。 不過如果Reader拿著鎖,Writer排隊,然后Reader排在Writer后,但是Writer放棄了排隊(比如它用的是tryLock 30s),那么Reader直接拿到鎖而不用排隊。

還有就是ReentrantReadWriteLock.ReadLock.tryLock() 和 ReentrantReadWriteLock.WriteLock.tryLock()方法不管這些,一旦調用的時候能拿到鎖,那么它們就會插隊!!

Reentrancy

從名字就知道它支持可重入。

以前拿過鎖的Reader和Writer可以繼續拿鎖。另外拿到WriteLock的線程可以拿到ReadLock,但是反之不然。

Lock downgrading

拿到WriteLock的可以直接變成ReadLock,不用釋放WriteLock再從新請求ReadLock(這樣需要重新排隊),實現的方法是先拿到WriteLock,接著拿ReadLock(上面的特性保證了不會死鎖),然后釋放WriteLock,這樣就得到一個ReadLock并立馬持有。

Interruption of lock acquisition

支持

一個使用讀寫鎖的例子

class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } }

一個Cache數據的例子,讀取數據時首先拿讀鎖,如果cache是有效的(volatile boolean cacheValid),直接使用數據。

如果失效了,那么釋放讀鎖,獲取寫鎖【這個類不支持upgrading】,然后double check一下是否cache有效,如果還是無效(說明它應該更新),那么更新數據,并且修改變量cacheValid,讓其它線程看到。

臭名昭著的double check

前面提到了double check,這里也順便討論一下:

@NotThreadSafepublic class DoubleCheckedLocking { private static Resource resource; public static Resource getInstance() { if (resource == null) { synchronized (DoubleCheckedLocking.class) {if (resource == null) resource = new Resource(); } } return resource; }}

很多“hacker”再提到延遲加載的時候都會提到它,上面的代碼看起來沒有什么問題:首先檢查一些resource,如果為空,那么加鎖,因為檢查resource==null沒有加鎖,所以可能同時兩個線程進入if并且請求加鎖,所以第一個拿到鎖的初始化一次,第二次拿鎖的會再次check。這看起來很完美:大多數情況下resouce不為空,很少的情況(剛開始時)resource為空,那么再加鎖,這比一上來就加鎖要高效很多不過千萬別高興地太早了,因為編譯器對引用的賦值可能會做優化,可能這個對象還沒有正確的構造好,值已經賦好了(為什么要這么做?也許構造對象需要IO,io等待的時間把值賦好了能提高速度)。這個時候別的線程就慘了!另外很多講延遲加載的文章都比較早(早于jdk6),那個年代java的synchronized確實很不給力。如果你實在在乎這點性能的話,應該用jvm的靜態類加載機制來實現:

@ThreadSafepublic class ResourceFactory { private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource ; }}

到此這篇關于Java多線程中Lock鎖的使用總結的文章就介紹到這了,更多相關Java多線程 Lock鎖的使用內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!

標簽: Java
相關文章:
主站蜘蛛池模板: 免费观看性欧美一级 | 一级一片免费视频播放 | 国产原创91 | 国产日本韩国不卡在线视频 | 在线成人精品国产区免费 | 亚洲欧美日韩久久精品第一区 | 成人网18免费网 | 欧美日韩亚洲另类 | 三级网站视频在线观看 | 足恋玩丝袜脚视频免费网站 | 欧美激情一区二区三区高清视频 | 国产成人国产在线观看入口 | 午夜性色福利视频在线视频 | 久久精品7| 99re5久久在热线播放 | 玖玖精品视频在线观看 | 欧美亚洲欧美区 | 日韩国产在线 | 国产亚洲精品网站 | 国产精品视频免费观看调教网 | 在线亚洲精品国产波多野结衣 | 亚洲精品456在线播放无广告 | 成年人视频在线免费看 | 亚洲综合日韩精品欧美综合区 | 国产精品久久久亚洲 | 99精品一区二区三区 | 在线私拍国产福利精品 | 人妖欧美一区二区三区四区 | 黄色三级网站 | 亚洲久草在线 | 免费一级毛片在线观看 | 在线观看中文字幕国产 | 色综合在| 国产九九精品视频 | 亚洲美女视频网址 | 福利社色 | 一区一精品| 日本激情视频在线观看 | 成年人免费观看网站 | 欧美一级视频 | 成年免费a级毛片 |