006-線程同步解決【ReentrantLock】

來源:互聯網
上載者:User

標籤:logs   條件   blog   開發   boolean   www   指令   重排序   非同步   

一、解決方案

004-線程同步問題引出、同步問題解決、死結、生產者與消費者

通過以上文章可知,通過原子性AtomicLong 、以及內部鎖(synchronized)機制可以解決安全執行緒問題。以下是一些進階用法。

1、回顧synchronized :

  核心類庫包含一個 Thread 類,可以用它來構建、啟動和操縱線程,Java 語言套件括了跨線程傳達並發性約束的構造 。

  synchronized 和 volatile 。在簡化與平台無關的並發類的開發的同時,它決沒有使並發類的編寫工作變得更繁瑣,只是使它變得更容易了。

  把代碼塊聲明為 synchronized,有兩個重要後果,通常是指該代碼具有 原子性(atomicity)和 可見度(visibility)。原子性意味著一個線程一次只能執行由一個指定監控對象(lock)保護的代碼,從而防止多個線程在更新共用狀態時相互衝突。可見度則更為微妙;它要對付記憶體緩衝和編譯器最佳化的各種反常行為。一般來說,線程以某種不必讓其他線程立即可以看到的方式(不管這些線程在寄存器中、在處理器特定的緩衝中,還是通過指令重排或者其他編譯器最佳化),不受緩衝變數值的約束,但是如果開發人員使用了同步,如下面的代碼所示,那麼運行庫將確保某一線程對變數所做的更新先於對現有 synchronized 塊所進行的更新,當進入由同一監控器(lock)保護的另一個 synchronized 塊時,將立刻可以看到這些對變數所做的更新。類似的規則也存在於 volatile 變數上。

  所以,實現同步操作需要考慮安全更新多個共用變數所需的一切,不能有競爭條件,不能破壞資料(假設同步的邊界位置正確),而且要保證正確同步的其他線程可以看到這些變數的最新值。通過定義一個清晰的、跨平台的記憶體模型(該模型在 JDK 5.0 中做了修改,改正了原來定義中的某些錯誤),通過遵守下面這個簡單規則,構建“一次編寫,隨處運行”的並發類是有可能的:不論什麼時候,只要您將編寫的變數接下來可能被另一個線程讀取,或者您將讀取的變數最後是被另一個線程寫入的,那麼您必須進行同步。

synchronized (lockObject) {     // update object state  }   

  不過現在好了一點,在最近的 JVM 中,沒有爭用的同步(一個線程擁有鎖的時候,沒有其他線程企圖獲得鎖)的效能成本還是很低的。(也不總是這樣;早期 JVM 中的同步還沒有最佳化,所以讓很多人都這樣認為,但是現在這變成了一種誤解,人們認為不管是不是爭用,同步都有很高的效能成本。)

  同步缺陷,它無法中斷一個正在等候獲得鎖的線程,也無法通過輪詢得到鎖,如果不想等下去,也就沒法得到鎖。同步還要求鎖的釋放只能在與獲得鎖所在的堆疊框架相同的堆疊框架中進行,多數情況下,這沒問題(而且與異常處理互動得很好),但是,確實存在一些非塊結構的鎖定更合適的情況。

2、第三種 重進入(ReentrantLock)

  重進入是一種基於per-thread的機制,並不是一種獨立的同步方法 。基本實現是這樣的:每個鎖關聯一個請求計數器和一個佔有它的線程,當計數器為0時,鎖是未被佔有的,線程請求時,JVM將記錄鎖的佔有者,並將計數器增1,當同一線程再次請求這個鎖時,計數器遞增;線程退出時,計數器減1,直到計數器為0時,鎖被釋放。

可見度和到期資料

  可見度,可以說是一種原始概念,並不是一種單獨的同步方法,就是說,同步可以實現資料的可見度,和避免到期資料的出現。關於可見度方面,同步機制看下面的Volatile變數。

  java.util.concurrent.lock 中的 Lock 架構是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。這就為Lock 的多種實現留下了空間,各種實現可能有不同的調度演算法、效能特性或者鎖定語義。 ReentrantLock 類實現了 Lock ,它擁有與synchronized 相同的並發性和記憶體語義,但是添加了類似輪詢鎖、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的效能。(換句話說,當許多線程都想訪問共用資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上。)

  reentrant 鎖意味著什麼呢?簡單來說,它有一個與鎖相關的擷取計數器,如果擁有鎖的某個線程再次得到鎖,那麼擷取計數器就加1,然後鎖需要被釋放兩次才能獲得真正釋放。這模仿了 synchronized 的語義;如果線程進入由線程已經擁有的監控器保護的 synchronized 塊,就允許線程繼續進行,當線程退出第二個(或者後續) synchronized 塊的時候,不釋放鎖,只有線程退出它進入的監控器保護的第一個 synchronized 塊時,才釋放鎖。
顯示鎖
  顯示鎖表面意思就是顯示的調用鎖,且釋放鎖。它提供了與synchronized基本一致的機制。但是有synchronized不能達到的效果,如:定時鎖的等待、可中斷鎖的等待、公平性、及實現非塊結構的鎖。但是為什麼還用synchronized呢?其實,用顯示鎖會比較複雜,且容易出錯,如下面的代碼:

Lock lock = new ReentrantLock();   ...  lock.lock();  try{       ...  }finally{       lock.unlock();  }

  可以看到 Lock 和 synchronized 有一點明顯的區別 —— lock 必須在 finally 塊中釋放。否則,如果受保護的代碼將拋出異常,鎖就有可能永遠得不到釋放!而使用同步【內部鎖synchronized】,JVM 將確保鎖會獲得自動釋放。

  除此之外,與目前的 synchronized 實現相比,爭用下的 ReentrantLock 實現更具延展性。(在未來的 JVM 版本中,synchronized 的爭用效能很有可能會獲得提高。)這意味著當許多線程都在爭用同一個鎖時,使用 ReentrantLock 的總體開支通常要比 synchronized 少得多。
讀寫鎖
  有的時候,資料是需要被頻繁讀取的,但不排除偶爾的寫入,我們只要保證:在讀取線程讀取資料的時候,能夠讀到最新的資料就不會問題。此時符合讀-寫鎖的特點:一個資源能夠被多個線程讀取,或者一個線程寫入,二者不同時進行。這種特點,在特定的情況下有很好的效能!

Volatile變數
  這是一種輕量級的同步機制,和前面說的可見度有很大關係,可以說,volatile變數,可以保證變數資料的可見度。在Java中設定變數值的操作,對於變數值的簡單讀寫操作沒有必要進行同步,都是原子操作。只有long和double類型的變數是非原子操作的。JVM將二者(long和double都是64位的)的讀寫劃分為兩個32位的操作,這樣就有可能就會不安全,只有聲明為volatile,才會使得64位的long和double成為安全執行緒的。當一個變數聲明為volatile類型後,編譯器會對其進行監控,保證其不會與其它記憶體操作一起被重排序(重排序:舉個例子,num=num+1;flag=true;JVM在執行這兩條語句時,不一定先執行num=num+1,也許在num+1賦值給num之前,flag就已經為true了,這就是一種重排序),同時,volatile變數不會被進行緩衝,所以,每當讀取volatile變數時,總能得到最新的值!為什麼會這樣?我們來看下面這段話:在當前的Java記憶體模型下,線程可以把變數儲存在本地記憶體(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變數的值,而另外一個線程還繼續使用它在寄存器中的變數值的拷貝,造成資料的不一致。要解決這個問題,只有把該變數聲明為volatile,這就指示JVM,這個變數是不穩定的,每次使用它都到主存中進行讀取。而且,當成員變數發生變化時,強迫線程將變化值回寫到共用記憶體。這樣在任何時刻,兩個不同的線程總是看到某個成員變數的同一個值。Java語言規範中指出:為了獲得最佳速度,允許線程儲存共用成員變數的私人拷貝,而且只當線程進入或者離開同步代碼塊時才與共用成員變數的原始值對比。這樣當多個線程同時與某個對象互動時,就必須要注意到要讓線程及時的得到共用成員變數的變化。

  volatile關鍵字就是提示JVM:對於這個成員變數不能儲存它的私人拷貝,而應直接與共用成員變數互動。
  此處注意:volatile關鍵字只能保證線程的可見度,但不能保證原子性,試圖用volatile保證原子性會很複雜!
  一般情況,volatile關鍵字用於修飾一些變數,如:被當做完成標識、中斷、狀態等。滿足一下三個條件的情況,比較符合volatile的使用情景:
    1、寫入變數時並不依賴變數的當前值(否則就和value++類似了),或者能夠確保只有單一線程修改變數的值。
    2、變數不需要與其他的狀態變數共同參與不變約束。
    3、訪問變數時,沒有其它原因需要加鎖。(畢竟加鎖是個耗效能的操作)
  使用建議:在兩個或者更多的線程訪問的成員變數上使用volatile。當要訪問的變數已在synchronized代碼塊中,或者為常量時,不必使用。由於使用volatile屏蔽掉了JVM中必要的代碼最佳化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。

Semaphore(訊號量)
  訊號量的意思就是設定一個最大值,來控制有限個對象同時對資源進行訪問。因為有的時候有些資源並不是只能由一個線程同時訪問的,舉個例子,我這兒有5個碗,只能滿足5個人同時用餐,那麼我可以設定一個最大值5,線程訪問時,用acquire() 擷取一個許可,如果沒有就等待,用完時用release() 釋放一個許可。這樣就保證了最多5個人同時用餐,不會造成安全問題,這是一種很簡單的同步機制。

臨界區
  如果有多個線程試圖同時訪問臨界區,那麼在有一個線程進入後,其他所有試圖訪問此臨界區的線程將被掛起,並一直持續到進入臨界區的線程離開。臨界區在被釋放後,其他線程可以繼續搶佔,並以此達到用原子方式操作共用資源的目的。在使用臨界區時,一般不允許其已耗用時間過長,只要進入臨界區的線程還沒有離開,其他所有試圖進入此臨界區的線程都會被掛起而進入到等待狀態,並會在一定程度上影響程式的運行效能。尤其需要注意的是不要將等待使用者輸入或是其他一些外界幹預的操作包含到臨界區。如果進入了臨界區卻一直沒有釋放,同樣也會引起其他線程的長時間等待。

同步容器
  Java為我們提供非常完整的線程同步機制,這包括jdk1.5後新增的java.util.concurrent包,裡麵包含各種各樣出色的安全執行緒的容器(即集合類)。如ConcurrentHashMap,CopyOnWriteArrayList、LinkedBlockingDeque等,這些容器有的在效能非常出色,也是值得我們程式員慶幸的事兒!

Collections位集合類提供安全執行緒的支援
  對於有些非安全執行緒的集合類,如HashMap,我們可以通過Collections的一些方法,使得HashMap變為安全執行緒的類,如:Collections.synchronizedMap(new HashMap());

excutor架構

  Java中excutor只是一個介面,但它為一個強大的同步架構做好了基礎,其實現可以用於非同步任務執行,支援很多不同類型的任務執行策略。excutor架構適用於生產者-消費者模式,是一個非常成熟的架構,此處不多講,在後續的文章中,我會細細分析它!

事件驅動
  事件驅動的意思就是一件事情辦完後,喚醒其它線程去幹另一件。這樣就保證:1、資料可見度。在A線程執行的時候,B線程處於睡眠狀態,不可能對共用變數進行修改。2、互斥性。相當於上鎖,不會有其它線程幹擾。常用的方法有:sleep()、wait()、notify()等等。
參考:《JAVA CONCURRENCY IN PRACTICE》 Brian Goetz 著

條件變數

  根類 Object 包含某些特殊的方法,用來線上程的 wait() 、 notify() 和 notifyAll() 之間進行通訊。這些是進階的並發性特性,許多開發人員從來沒有用過它們 —— 這可能是件好事,因為它們相當微妙,很容易使用不當。幸運的是,隨著 JDK 5.0 中引入 java.util.concurrent,開發人員幾乎更加沒有什麼地方需要使用這些方法了。
  通知與鎖定之間有一個互動 —— 為了在對象上 wait 或 notify ,您必須持有該對象的鎖。就像 Lock 是同步的概括一樣, Lock 架構套件含了對wait 和 notify 的概括,這個概括叫作 條件(Condition) 。 Lock 對象則充當綁定到這個鎖的條件變數的工廠對象,與標準的 wait 和notify 方法不同,對於指定的 Lock ,可以有不止一個條件變數與它關聯。這樣就簡化了許多並發演算法的開發。例如, 條件(Condition) 的 Javadoc 顯示了一個有界緩衝區實現的樣本,該樣本使用了兩個條件變數,“not full”和“not empty”,它比每個 lock 只用一個 wait 設定的實現方式可讀性要好一些(而且更有效)。 Condition 的方法與 wait 、 notify 和 notifyAll 方法類似,分別命名為 await 、 signal 和 signalAll,因為它們不能覆蓋 Object 上的對應方法。

公平鎖和不公平鎖

  如果查看 Javadoc,您會看到, ReentrantLock 構造器的一個參數是 boolean 值,它允許您選擇想要一個 公平(fair)鎖,還是一個 不公平(unfair)鎖。公平鎖使線程按照請求鎖的順序依次獲得鎖;而不公平鎖則允許直接擷取鎖,在這種情況下,線程有時可以比先請求鎖的其他線程先得到鎖。

  為什麼我們不讓所有的鎖都公平呢?畢竟,公平是好事,不公平是不好的,不是嗎?(當孩子們想要一個決定時,總會叫嚷“這不公平”。我們認為公平非常重要,孩子們也知道。)在現實中,公平保證了鎖是非常健壯的鎖,有很大的效能成本。要確保公平所需要的記帳(bookkeeping)和同步,就意味著被爭奪的公平鎖要比不公平鎖的吞吐率更低。作為預設設定,應當把公平設定為 false ,除非公平對您的演算法至關重要,需要嚴格按照線程排隊的順序對其進行服務。

  那麼同步又如何呢?內建的監控器鎖是公平的嗎?答案令許多人感到大吃一驚,它們是不公平的,而且永遠都是不公平的。但是沒有人抱怨過線程饑渴,因為 JVM 保證了所有線程最終都會得到它們所等候的鎖。確保統計上的公平性,對多數情況來說,這就已經足夠了,而這花費的成本則要比絕對的公平保證的低得多。所以,預設情況下 ReentrantLock 是“不公平”的,這一事實只是把同步中一直是事件的東西表面化而已。如果您在同步的時候並不介意這一點,那麼在 ReentrantLock 時也不必為它擔心。

  

 

006-線程同步解決【ReentrantLock】

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.