java线程(2)--同步和锁
1.線程的內存模型
?Java作為平臺無關性語言,JLS(Java語言規范)定義了一個統一的內存管理模型JMM(Java Memory Model),JMM屏蔽了底層平臺內存管理細節,在多線程環境中必須解決可見性和有序性的問題。JMM規定了jvm有主內存(Main Memory)和工作內存(Working Memory) ,主內存存放程序中所有的類實例、靜態數據等變量,是多個線程共享的,而工作內存存放的是該線程從主內存中拷貝過來的變量以及訪問方法所取得的局部變量,是每個線程私有的其他線程不能訪問,每個線程對變量的操作都是以先從主內存將其拷貝到工作內存再對其進行操作的方式進行,多個線程之間不能直接互相傳遞數據通信,只能通過共享變量來進行。
重要的圖片看三遍,從三個內存模型的文章中摘出的圖片含義是一致的。即:
1.所有線程共享主內存
2.每個線程有自己的工作內存
需要注意的是,首先你得明白什么是主內存,主內存就是我們平時所說的內存。那么哪些變量是共享的?答類變量(靜態變量),實例變量(成員變量)共享,是不安全的。而局部變量即方法體內的變量是不共享的,局部變量是安全的。
為什么會線程不安全?從上面的介紹可以看出每個線程從主內存里拿數據,改變了數據后放回主內存。當多個線程都改變主內存里的變量,這個變量的值就不確定了。再準確的說,線程1只想變量a加1,第二次取出a的時候發現a并不是自己想要的。這就是不安全!
2.什么是多線程
上一節已經學習了線程,多線程就是多個運行的線程。看起來解釋很搞笑,但我覺得多線程并沒有那么復雜,不要以為安全問題就頭大,多線程不一定是線程不安全的。上面已經說到,多個線程共享同一個變量就會出現線程安全問題,相反的,不出現共享變量的情況下就沒問題了。我有一次測試多線程沒問題,后來發現我測試中沒有共享變量,每個線程的主體都是新建的對象,于是不存在安全問題。然而,平時用到的多是共享的。即,多個線程的參數是同一個實例。
3.同步上鎖
3.1什么是上鎖
想要同步就必須要上鎖,只有鎖住以后,別人才不可以訪問我用的東西,我釋放了鎖后別人才可以用,這樣就保證了我使用范圍內的變量的絕對控制,即線程安全,也就是同步。那么什么是鎖?
Java中每個對象都有一個內置鎖。當程序運行到非靜態的synchronized同步方法上時,自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖。獲得一個對象的鎖也稱為獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味著任何其他線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
看完介紹,明白:
- 對象有個鎖,通過synchronize獲取;
- 對象只有一個鎖;
- 對象鎖住后別的線程不能訪問synchronize代碼塊;
- 鎖是針對對象的;
- this表示當前對象
3.2方法上鎖
下面是容易理解和看到的例子,就是在方法頭加上關鍵字synchronized
public synchronized void setName(String name){this.name = name;}3.3對象上鎖
對象上鎖用this,this代表當前對象。
public synchronized int getX() {return x++;} 與public int getX() {synchronized (this) {return x;}} 效果是完全一樣的。3.4靜態方法上鎖
要同步靜態方法,需要一個用于整個類對象的鎖,這個對象是就是這個類(XXX.class)。 例如: public static synchronized int setName(String name){Xxx.name = name; } 等價于 public static int setName(String name){synchronized(Xxx.class){Xxx.name = name;} }3.5如果線程得不到鎖會怎樣
如果線程試圖進入同步方法,而其鎖已經被占用,則線程在該對象上被阻塞。實質上,線程進入該對象的的一種池中,必須在哪里等待,直到其鎖被釋放,該線程再次變為可運行或運行為止。 當考慮阻塞時,一定要注意哪個對象正被用于鎖定: 1、調用同一個對象中非靜態同步方法的線程將彼此阻塞。如果是不同對象,則每個線程有自己的對象的鎖,線程間彼此互不干預。 2、調用同一個類中的靜態同步方法的線程將彼此阻塞,它們都是鎖定在相同的Class對象上。 3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法鎖定在Class對象上,非靜態方法鎖定在該類的對象上。 4、對于同步代碼塊,要看清楚什么對象已經用于鎖定(synchronized后面括號的內容)。在同一個對象上進行同步的線程將彼此阻塞,在不同對象上鎖定的線程將永遠不會彼此阻塞。看介紹明白:上鎖一定是對象的鎖。
3.6死鎖
死鎖,很熟悉的名字。死鎖是線程互相等待,a需要b的資源,但b的資源被b持有沒有釋放,a阻塞等待;b需要a的資源,但a的資源被a持有沒有釋放,b阻塞等待。就是我等你,你等我,死循環。實例:
?View Code這個實例中,要注意到兩個線程的聲明過程,都是針對同一個對象的,所以才有資源爭搶行為。
4小結
1、線程同步的目的是為了保護多個線程反問一個資源時對資源的破壞。 2、線程同步方法是通過鎖來實現,每個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他同步方法。 3、對于靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程獲得鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。 4、對于同步,要時刻清醒在哪個對象上同步,這是關鍵。 5、編寫線程安全的類,需要時刻注意對多個線程競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,并保證原子操作期間別的線程無法訪問競爭資源。 6、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。 7、死鎖是線程間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程序,不一定好使,呵呵。但是,一旦程序發生死鎖,程序將死掉。還有,同步通過上鎖來實現,即原子操作互不影響;上鎖是針對對象的,類對象或者實例對象。
?
以下轉載自:http://www.cnblogs.com/dolphin0520/p/3923167.html
?
?5 synchronized的缺陷
synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性。那么為什么會出現Lock呢?
如果一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,并執行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:
- 獲取鎖的線程執行完了該代碼塊,然后線程釋放對鎖的占有;
- 線程執行發生異常,此時JVM會讓線程自動釋放鎖。
那么如果這個獲取鎖的線程由于要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能干巴巴地等待,試想一下,這多么影響程序執行效率。
因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。
再舉個例子:當有多個線程讀寫文件時,讀操作和寫操作會發生沖突現象,寫操作和寫操作會發生沖突現象,但是讀操作和讀操作不會發生沖突現象。
但是采用synchronized關鍵字來實現同步的話,就會導致一個問題:
如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。
因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發生沖突,通過Lock就可以辦到。
另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。
總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:
- Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;
- Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
?6.java.util.concurrent.locks包下常用類
6.1 Lock
首先要說明的就是Lock,通過查看源碼可知,Lock是一個接口:
| 1 2 3 4 5 6 7 8 | public?interface?Lock { ????void?lock(); ????void?lockInterruptibly()?throws?InterruptedException; ????boolean?tryLock(); ????boolean?tryLock(long?time, TimeUnit unit)?throws?InterruptedException; ????void?unlock(); ????Condition newCondition(); } |
lock()方法是平常使用最多的一個方法,就是用來獲取鎖。如果鎖已經被其他線程獲取,則進行等待。如果采用Lock必須主動釋放鎖,并且發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,并且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class?C { ????????//鎖對象 ????????private?final?ReentrantLock lock =?new?ReentrantLock(); ????????...... ????????//保證線程安全方法 ????????public?void?method() { ????????????//上鎖 ????????????lock.lock(); ????????????try?{ ????????????????//保證線程安全操作代碼 ????????????}?catch() { ????????????? ????????????}?finally?{ ????????????????lock.unlock();//釋放鎖 ????????????} ????????} ????} |
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就是說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
所以,一般情況下tryLock獲取鎖時這樣使用:
| 1 2 3 4 5 6 7 8 9 10 11 12 | Lock lock = ...; if(lock.tryLock()) { ?????try{ ?????????//處理任務 ?????}catch(Exception ex){ ?????????? ?????}finally{ ?????????lock.unlock();???//釋放鎖 ?????} }else?{ ????//如果不能獲取鎖,則直接做其他事情 } |
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能響應中斷,即中斷線程的等待狀態。也就是說當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
由于lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
| 1 2 3 4 5 6 7 8 9 | public?void?method()?throws?InterruptedException { ????lock.lockInterruptibly(); ????try?{? ?????//..... ????} ????finally?{ ????????lock.unlock(); ????}? } |
注意,當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。而用synchronized修飾的話,當一個線程處于等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
6.2 ReentrantLock
ReentrantLock,意思是“可重入鎖”。ReentrantLock是唯一實現了Lock接口的類,并且ReentrantLock提供了更多的方法。下面通過一些實例看具體看一下如何使用ReentrantLock。
例子1,lock()的正確使用方法
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public?class?TestLock { ????private?ArrayList<Integer> arrayList =?new?ArrayList<>(); ????public?static?void?main(String[] args){ ????????final?TestLock testLock =?new?TestLock(); ????????new?Thread(){ ????????????public?void?run(){ ????????????????testLock.insert(Thread.currentThread()); ????????????} ????????}.start(); ????????for?(int?i =?0; i <?5; i++) { ????????????new?Thread(){ ????????????????public?void?run(){ ????????????????????testLock.insert(Thread.currentThread()); ????????????????} ????????????}.start(); ????????} ????} ????private?void?insert(Thread thread) { ????????Lock lock =?new?ReentrantLock();//注意這個地方 ????????lock.lock(); ????????try?{ ????????????System.out.println(thread.getName()+"得到了鎖"); ????????????for?(int?i =?0; i <5?; i++) { ????????????????arrayList.add(i); ????????????} ????????}catch?(Exception e){ ????????????//ToDo:hanle exception ????????}finally?{ ????????????System.out.println(thread.getName()+"釋放了鎖"); ????????????lock.unlock(); ????????} ????} } |
結果卻不是想象中的同步:
+ View Code為什么結果不是同步上鎖呢?因為這里的lock放在方法里,是局部變量。在開頭已經描述了,方法中的局部變量存儲在線程的工作區中,每個線程執行到該方法時都會保存一個副本,那么理所當然每個線程執行到lock.lock()處獲取的是不同的鎖,所以不會沖突。
因此,應該將lock聲明為類的屬性。共享:
+ View Code這樣就是正確的使用lock的方法了。
例子2,tryLock()的使用方法
+ View Code由于多線程運行不確定,結果不定,因此可以sleep一下更明顯。
+ View Code例子3,lockInterrptibly()響應中斷的使用方法:
+ View Code運行之后發現thread2能夠被中斷,但程序沒有結束。
+ View Code6.3 ReadWriteLock
ReadWriteLock也是一個接口,在它里面只定義了兩個方法:
+ View Code一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。
6.4 ReentrantReadWriteLock
ReentrantReadWriteLock里面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。
下面通過幾個例子來看一下ReentrantReadWriteLock具體用法。
假如有多個線程要同時進行讀操作的話,先看一下synchronized達到的效果:
+ View Code這段程序的輸出結果會是,直到thread0執行完讀操作之后,才會打印thread1執行讀操作的信息。
+ View Code而改成讀寫鎖的話:
+ View Code此時打印的結果為:
+ View Code說明thread0和thread1在同時進行讀操作。
這樣就大大提升了讀操作的效率。
不過要注意的是,如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
關于ReentrantReadWriteLock類中的其他方法感興趣的朋友可以自行查閱API文檔。
6.5 Lock和synchronized的選擇
總結來說,Lock和synchronized有以下幾點不同:
1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
2)synchronized在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個線程進行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優于synchronized。所以說,在具體使用時要根據適當情況選擇。
7. 鎖的相關概念介紹
在前面介紹了Lock的基本使用,這一節來介紹一下與鎖相關的幾個概念。
7.1.可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了鎖的分配機制:基于線程的分配,而不是基于方法調用的分配。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。
看下面這段代碼就明白了:
+ View Code? 上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而由于method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因為線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。
而由于synchronized和Lock都具備可重入性,所以不會發生上述現象。
7.2.可中斷鎖
可中斷鎖:顧名思義,就是可以相應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。
7.3.公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
而對于ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置為公平鎖。
看一下這2個類的源代碼就清楚了:
+ View Code
在ReentrantLock中定義了2個靜態內部類,一個是NotFairSync,一個是FairSync,分別用來實現非公平鎖和公平鎖。
我們可以在創建ReentrantLock對象時,通過以下方式來設置鎖的公平性:ReentrantLock lock =?new?ReentrantLock(true);如果參數為true表示為公平鎖,為fasle為非公平鎖。默認情況下,如果使用無參構造器,則是非公平鎖。
另外在ReentrantLock類中定義了很多方法,比如:
isFair()??????? //判斷鎖是否是公平鎖
isLocked()??? //判斷鎖是否被任何線程獲取了
isHeldByCurrentThread()?? //判斷鎖是否被當前線程獲取了
hasQueuedThreads()?? //判斷是否有線程在等待該鎖
在ReentrantReadWriteLock中也有類似的方法,同樣也可以設置為公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock并未實現Lock接口,它實現的是ReadWriteLock接口。
7.4.讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發生沖突。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。
上面已經演示過了讀寫鎖的使用方法,在此不再贅述。
?本文轉自Ryan.Miao博客園博客,原文鏈接:http://www.cnblogs.com/woshimrf/p/5221136.html,如需轉載請自行聯系原作者
總結
以上是生活随笔為你收集整理的java线程(2)--同步和锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jira在linux下面的安装和配置
- 下一篇: BCRAN课本命令回顾