国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > Java > 正文

Java中的各種鎖

2019-11-06 07:41:07
字體:
來源:轉載
供稿:網友

java程序中有時候我們可能需要推遲一些高開銷的對象初始化操作,等到使用到這些對象時再去初始化。但要正確實現線程安全的延時初始化需要一些技巧,否則可能會出現問題。比如下面使用雙重校驗鎖實現的演示加載的單例模式就是存在線程安全問題的:

/** * 使用雙重校驗鎖 */class Single4 { PRivate static Single4 single = null; private Single4() { } public static Single4 getSingleton() { if (single == null) { synchronized (Single4.class) { if (single == null) { single = new Single4();//問題就出現在這里 } } } return single; }}

問題的根源:single = new Single4();其實創建一個對象可以分為3步完成的:

1. memory = allocate();//1.為對象分配內存空間2. ctorInstance(single);//2.初始化對象3. single = memory;//3.設置single指向剛分配的內存地址

上面偽代碼中的2和3之間可能會被重排序,重排序后執行的順序是這樣的:

1. memory = allocate();//1.為對象分配內存空間2. single = memory;//3.設置single指向剛分配的內存地址 //這時候對象還沒有初始化!3. ctorInstance(single);//2.初始化對象
時間線程A線程B
t1A1:分配對象的內存空間 
t2A3:設置single指向內存空間 
t3 B1:判斷single是否為空
t4 B2:由于single不為null,線程B將訪問single引用的對象
t5A2:初始化對象 
t6A4:訪問single引用的對象

  由上表也可以看出,當線程A中初始化single發生了重排序,并且執行完t2后時間片用完,線程B獲得時間片,獲取single時候,判斷single不為null,接下來線程B將訪問single引用的對象,但此時對象還沒有進行初始化,所以就會引發異常!  聰明的你,一定想到了:如果不讓創建對象過程發生重排序,不就解決問題啦。是的其實只要對上面的代碼做一個小小的修改就能做到:

/** * 使用雙重校驗鎖 */class Single4 { private volatile static Single4 single = null; private Single4() { } public static Single4 getSingleton() { if (single == null) { synchronized (Single4.class) { if (single == null) { single = new Single4(); } } } return single; }

好像沒有什么改變啊,有的只是你沒注意到:private volatile static Single4 single = null;  對,就是在聲明引用的時候在前面加上volatile關鍵字,加上volatile后,在多線程環境下創建對象過程中的重排序是被禁止的。它是怎么做到的呢,那么下面就讓我們一起來看看volatile是怎么做到的。

1.volatile

  volatile是輕量級的synchronized,它在多處理器開發保證了共享變量的“可見性”。可見性的意思是當一個線程修改了共享變量時,另一個線程能讀到這個修改的值。

1.1volatile的實現原理

  Java語言規范第三版中對volatile的定義如下:Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。  意思就是說,如果一個變量被聲明為volatile,Java內存模型會確保所有線程所有的線程看到這個變量的值是一致的。  volatile是如何保證可見性的呢?下面我們看看我們在對volatile變量進行寫操作時,cpu會做什么?single = new Singleton();//single是volatile變量轉化成匯編代碼如下:0x01a3deld: movb $0×0,0×1104800(%esi);0x01a3de24: lock add1 $0×0,(%esp);其中Lock前綴的指令在多核處理器下會引發了兩件事:

將當前處理器緩存行的數據寫到系統內存;這個寫回緩存的操作會使在其他CPU里緩存了該內存地址的數據失效。

1.2volatile的內存語義

volatile寫-讀的內存語義

volatile的寫-讀與鎖的釋放-獲取有相同的內存效果volatile寫和鎖的釋放有相同的內存語義volatile讀與鎖的獲取有相同的內存語義

其實理解volatile特性的一個好方法:把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。  鎖的happens-before規則保證釋放鎖和獲取鎖的兩個線程之間的內存可見性,這意味這對一個volatile變量的讀,總是能看到任意線程對這個volatile變量最后的寫入值。  volatile變量自身有兩個特性:原子性:對于任意單個volatile變量的讀/寫具有原子性,但是類似與volatileVal++這種復合操作來說,它就不具有原子性。可見性:對于一個volatile變量的讀,總是能看到任意線程對這個volatile變量最后的寫入。

當寫一個volatile變量時,JMM會把線程對應的本地內存中的共享變量值刷新到主內存

對volatile變量寫過程

當讀一個volatile變量時,JMM會把線程對應的本地內存置為無效,線程接下來將從主內存中讀取共享變量。

對volatile變量讀的過程

注意:一些編程大牛往往會告誡我們說,盡量不要去使用volatile,因為使用volatile稍有不慎就會出現問題。如果嚴格遵循 volatile 的使用條件:

變量真正獨立于其他變量不依賴于自己以前的值

  如果是多個volatile操作或類似于volatile++這種復合操作,這些操作整體上不具有原子性。在某些情況下可以使用volatile代替synchronized來簡化代碼。然而,使用volatile的代碼往往比使用鎖的代碼更加容易出錯。(見volatileDemo)

2.synchronized

  在多線程并發編程中synchronized一直是元老級角色,也有很多人稱它為重量級鎖。但是隨著Java 1.6對aynchronized進行了各種優化之后,有些情況下他就并不那么重了。synchronized的使用場景有一下三種:

對普通方法的同步,鎖是當前實例的對象對靜態方法的同步,鎖是當前類的Class對象對代碼塊進行同步,鎖是synchronized括號里配置的對象

  JVM規范規定JVM基于進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另外一種方式實現的,細節在JVM規范里并沒有詳細說明,但是方法的同步同樣可以使用這兩個指令來實現。monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個 monitor 與之關聯,當且一個monitor 被持有后,它將處于鎖定狀態。線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的所有權,即嘗試獲得對象的鎖。

2.1 Java對象頭

  鎖存在Java對象頭里。如果對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,一字寬等于四字節,即32bit。

java對象頭

  Java對象頭里的Mark Word里默認存儲對象的HashCode,分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如下:

無鎖狀態的Mark Word

  在運行期間Mark Word里存儲的數據會隨著鎖標志位的變化而變化。Mark Word可能變化為存儲以下4種數據:

不同鎖下對象頭的狀態

2.2 幾種鎖的類型

  線程的阻塞和喚醒需要CPU從用戶態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。

  Java SE1.6為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6里鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。

  鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

2.2.1 偏向鎖

  Hotspot的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的目的是在某個線程獲得鎖之后,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程得到了偏護。

偏向鎖的進一步理解  偏向鎖的釋放不需要做任何事情,這也就意味著加過偏向鎖的MarkValue會一直保留偏向鎖的狀態,因此即便同一個線程持續不斷地加鎖解鎖,也是沒有開銷的。  另一方面,偏向鎖比輕量鎖更容易被終結,輕量鎖是在有鎖競爭出現時升級為重量鎖,而一般偏向鎖是在有不同線程申請鎖時升級為輕量鎖,這也就意味著假如一個對象先被線程1加鎖解鎖,再被線程2加鎖解鎖,這過程中沒有鎖沖突,也一樣會發生偏向鎖失效,不同的是這回要先退化為無鎖的狀態,再加輕量鎖,如圖:

偏向鎖的鎖定和解鎖

  另外,JVM對那種會有多線程加鎖,但不存在鎖競爭的情況也做了優化,聽起來比較拗口,但在現實應用中確實是可能出現這種情況,因為線程之前除了互斥之外也可能發生同步關系,被同步的兩個線程(一前一后)對共享對象鎖的競爭很可能是沒有沖突的。

偏向鎖的獲取  當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖,如果測試成功,表示線程已經獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),如果沒有設置,則使用CAS競爭鎖,如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

偏向鎖的撤銷  偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態,則將對象頭設置成無鎖狀態,如果線程仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。

偏向鎖的鎖定和解鎖

偏向鎖的設置  關閉偏向鎖:偏向鎖在Java 6和Java 7里是默認啟用的,但是它在應用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程序里所有的鎖通常情況下處于競爭狀態,可以通過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那么默認會進入輕量級鎖狀態。

2.2.2 輕量級鎖

輕量級鎖加鎖  線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,則自旋獲取鎖,當自旋獲取鎖仍然失敗時,表示存在其他線程競爭鎖(兩條或兩條以上的線程競爭同一個鎖),則輕量級鎖會膨脹成重量級鎖。

輕量級鎖解鎖  輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到對象頭,如果成功,則表示同步過程已完成。如果失敗,表示有其他線程嘗試過獲取該鎖,則要在釋放鎖的同時喚醒被掛起的線程。下圖是兩個線程同時爭奪鎖,導致鎖膨脹的流程圖。

輕量級鎖

2.2.3 重量級鎖

  重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具備Mutex互斥的功能,它還負責實現了Semaphore的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責做互斥,后一個用于做線程同步。

重量級鎖

2.2.4 鎖的優缺點對比

幾種鎖的比較

2.3 鎖的內存語義

  鎖是java并發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發送消息。下面是鎖釋放-獲取的示例代碼:

class MonitorExample { int a = 0; public synchronized void writer() { //1 a++; //2 } //3 public synchronized void reader() { //4 int i = a; //5 …… } //6}

  假設線程A執行writer()方法,隨后線程B執行reader()方法。根據happens before規則,這個過程包含的happens before 關系可以分為兩類:

根據程序次序規則,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。根據監視器鎖規則,3 happens before 4。根據happens before 的傳遞性,2 happens before 5。

上述happens before 關系的圖形化表現形式如下:

鎖的內存語義

  在上圖中,每一個箭頭鏈接的兩個節點,代表了一個happens before 關系。黑色箭頭表示程序順序規則;橙色箭頭表示監視器鎖規則;藍色箭頭表示組合這些規則后提供的happens before保證。

  上圖表示在線程A釋放了鎖之后,隨后線程B獲取同一個鎖。在上圖中,2 happens before 5。因此,線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個鎖之后,將立刻變得對B線程可見。

鎖釋放和獲取的內存語義  當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。以上面的MonitorExample程序為例,A線程釋放鎖后,共享數據的狀態示意圖如下:

釋放鎖過程

  當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須要從主內存中去讀取共享變量。下面是鎖獲取的狀態示意圖:

鎖的獲取過程

  對比鎖釋放-獲取的內存語義與volatile寫-讀的內存語義,可以看出:鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。

下面對鎖釋放和鎖獲取的內存語義做個總結:

   線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。  線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。  線程A釋放鎖,隨后線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。

3. java.util.concurrent.locks包下常用的類

  下面我們就來探討一下java.util.concurrent.locks包中常用的類和接口。

3.1 Lock

首先要說明的就是Lock,通過查看Lock的源碼可知,Lock是一個接口:

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()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。unLock()方法是用來釋放鎖的。

  在Lock中聲明了四個方法來獲取鎖,那么這四個方法有何區別呢?  首先lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。    由于在前面講到如果采用Lock,必須主動去釋放鎖,并且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,并且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:

Lock lock = ...;lock.lock();try{ //處理任務}catch(Exception ex){}finally{ lock.unlock(); //釋放鎖}

  tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。    tryLock(long time,TimeUnitunit)方法和tryLock()方法是類似的,只不過區別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  所以,一般情況下通過tryLock來獲取鎖時是這樣使用的:

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()一般的使用形式如下:   

public void method() throws InterruptedException { lock.lockInterruptibly(); try { //..... } finally { lock.unlock(); } }

  注意,當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。  因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。  而用synchronized修飾的話,當一個線程處于等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。   

3.2 ReentrantLock

  ReentrantLock,意思是“可重入鎖”,關于可重入鎖的概念在下一節講述。ReentrantLock是唯一實現了Lock接口的類,并且ReentrantLock提供了更多的方法。下面通過一些實例看具體看一下如何使用ReentrantLock。

  例子1,lock()的正確使用方法

public class Test { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); 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 = new ReentrantLock(); //注意這個地方 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(); } }}

小伙伴們先想一下這段代碼的輸出結果是什么?

Thread-0得到了鎖Thread-1得到了鎖Thread-0釋放了鎖Thread-1釋放了鎖

  也許有朋友會問,怎么會輸出這個結果?第二個線程怎么會在第一個線程釋放鎖之前得到了鎖?原因在于,在insert方法中的lock變量是局部變量,每個線程執行該方法時都會保存一個副本,那么理所當然每個線程執行到lock.lock()處獲取的是不同的鎖,所以就不會發生沖突。

  知道了原因改起來就比較容易了,只需要將lock聲明為類的屬性即可。   

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(); } }}

這樣就是正確地使用Lock的方法了。

  例子2,tryLock()的使用方法

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) { if(lock.tryLock()) { 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(); } } else { System.out.println(thread.getName()+"獲取鎖失敗"); } }}

輸出結果:

Thread-0得到了鎖Thread-1獲取鎖失敗Thread-0釋放了鎖

例子3,lockInterruptibly()響應中斷的使用方法:

public class Test { private Lock lock = new ReentrantLock(); public static void main(String[] args) { Test test = new Test(); MyThread thread1 = new MyThread(test); MyThread thread2 = new MyThread(test); thread1.start(); thread2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } thread2.interrupt(); } public void insert(Thread thread) throws InterruptedException{ lock.lockInterruptibly(); //注意,如果需要正確中斷等待鎖的線程,必須將獲取鎖放在外面,然后將InterruptedException拋出 try { System.out.println(thread.getName()+"得到了鎖"); long startTime = System.currentTimeMillis(); for( ; ;) { if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) break; //插入數據 } } finally { System.out.println(Thread.currentThread().getName()+"執行finally"); lock.unlock(); System.out.println(thread.getName()+"釋放了鎖"); } }}class MyThread extends Thread { private Test test = null; public MyThread(Test test) { this.test = test; } @Override public void run() { try { test.insert(Thread.currentThread()); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName()+"被中斷"); } }}

運行之后,發現thread2能夠被正確中斷。

3.3 ReadWriteLock

  ReadWriteLock也是一個接口,在它里面只定義了兩個方法:   

public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading. */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing. */ Lock writeLock();}

  一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。

3.4 ReentrantReadWriteLock

  ReentrantReadWriteLock里面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。

  下面通過幾個例子來看一下ReentrantReadWriteLock具體用法。

  假如有多個線程要同時進行讀操作的話,先看一下synchronized達到的效果:   

public class Test { private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public static void main(String[] args) { final Test test = new Test(); new Thread(){ public void run() { test.get(Thread.currentThread()); }; }.start(); new Thread(){ public void run() { test.get(Thread.currentThread()); }; }.start(); } public synchronized void get(Thread thread) { long start = System.currentTimeMillis(); while(System.currentTimeMillis() - start <= 1) { System.out.println(thread.getName()+"正在進行讀操作"); } System.out.println(thread.getName()+"讀操作完畢"); }}

這段程序的輸出結果會是,直到thread1執行完讀操作之后,才會打印thread2執行讀操作的信息。

Thread-0正在進行讀操作Thread-0正在進行讀操作Thread-0正在進行讀操作Thread-0正在進行讀操作Thread-0正在進行讀操作Thread-0正在進行讀操作Thread-0讀操作完畢Thread-1正在進行讀操作Thread-1正在進行讀操作Thread-1讀操作完畢

而改成用讀寫鎖的話:

public class Test { private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public static void main(String[] args) { final Test test = new Test(); new Thread(){ public void run() { test.get(Thread.currentThread()); }; }.start(); new Thread(){ public void run() { test.get(Thread.currentThread()); }; }.start(); } public void get(Thread thread) { rwl.readLock().lock(); try { long start = System.currentTimeMillis(); while(System.currentTimeMillis() - start <= 1) { System.out.println(thread.getName()+"正在進行讀操作"); } System.out.println(thread.getName()+"讀操作完畢"); } finally { rwl.readLock().unlock(); } }}

此時打印的結果為:

Thread-0正在進行讀操作Thread-0正在進行讀操作Thread-1正在進行讀操作Thread-1正在進行讀操作Thread-1正在進行讀操作Thread-0正在進行讀操作Thread-0正在進行讀操作Thread-0讀操作完畢Thread-1讀操作完畢

說明thread1和thread2在同時進行讀操作。

這樣就大大提升了讀操作的效率

  不過要注意的是,如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。

  如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。

  關于ReentrantReadWriteLock類中的其他方法感興趣的朋友可以自行查閱API文檔。   

3.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。所以說,在具體使用時要根據適當情況選擇。

參考文獻《Java并發編程藝術》http://ifeve.com/java-memory-model-5/http://blog.csdn.net/true100/article/details/51693715luojinping.com/2015/07/09/java%E9%94%81%E4%BC%98%E5%8C%96/http://blog.jobbole.com/104902/


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 昭苏县| 溧阳市| 常宁市| 浙江省| 揭东县| 江津市| 如东县| 沙坪坝区| 兴业县| 轮台县| 巢湖市| 丰宁| 城市| 康平县| 浏阳市| 开阳县| 宝鸡市| 文安县| 翁牛特旗| 金昌市| 揭西县| 怀安县| 长沙县| 酉阳| 常宁市| 大邑县| 读书| 龙州县| 涪陵区| 甘孜县| 小金县| 西峡县| 文山县| 乐东| 舟曲县| 长丰县| 忻州市| 闻喜县| 广宁县| 闽清县| 措勤县|