一段synchronized的代碼被一個線程執行以前,他要先拿到執行這段代碼的權限,在 java裏邊就是拿到某個同步對象的鎖(一個對象只有一把鎖); 若是這個時候同步對象的鎖被其餘線程拿走了,他(這個線程)就只能等了(線程阻塞在鎖池 等待隊列中)。 取到鎖後,他就開始執行同步代碼(被synchronized修飾的代碼);線程執行完同步代碼後立刻就把鎖還給同步對象,其餘在鎖池中 等待的某個線程就能夠拿到鎖執行同步代碼了。這樣就保證了同步代碼在統一時刻只有一個線程在執行。java
衆所周知,在Java多線程編程中,一個很是重要的方面就是線程的同步問題。
關於線程的同步,通常有如下解決方法:
1. 在須要同步的方法的方法簽名中加入synchronized關鍵字。
2. 使用synchronized塊對須要進行同步的代碼段進行同步。
3. 使用JDK 5中提供的java.util.concurrent.lock包中的Lock對象。
另外,爲了解決多個線程對同一變量進行訪問時可能發生的安全性問題,咱們不只能夠採用同步機制,更能夠經過JDK 1.2中加入的ThreadLocal來保證更好的併發性。
本篇中,將詳細的討論Java多線程同步機制,並對ThreadLocal作出探討。
本文大體的目錄結構以下:
1、線程的先來後到——問題的提出:爲何要有多線程同步?Java多線程同步的機制是什麼?
2、給我一把鎖,我能創造一個規矩——傳統的多線程同步編程方法有哪些?他們有何異同?
3、Lock來了,你們都讓開—— Java併發框架中的Lock詳解。
4、你有我有全都有—— ThreadLocal如何解決併發安全性?
5、總結——Java線程安全的幾種方法對比。
1、線程的先來後到
我 們來舉一個Dirty的例子:某餐廳的衛生間很小,幾乎只能容納一我的如廁。爲了保證不受干擾,如廁的人進入衛生間,就要鎖上房門。咱們能夠把衛生間想 象成是共享的資源,而衆多須要如廁的人能夠被視做多個線程。假如衛生間當前有人佔用,那麼其餘人必須等待,直到這我的如廁完畢,打開房門走出來爲止。這就 比如多個線程共享一個資源的時候,是必定要分出先來後到的。
有人說:那若是我沒有這道門會怎樣呢?讓兩個線程相互競爭,誰搶先了,誰就 能夠先幹活,這樣多好阿?可是咱們知道:若是廁所沒有門的話,如廁的人一塊兒涌向 廁所,那麼必然會發生爭執,正常的如廁步驟就會被打亂,頗有可能會發生意想不到的結果,例如某些人可能只好被迫在不正確的地方施肥……
正是由於有這道門,任何一個單獨進入如廁的人均可以順利的完成他們的如廁過程,而不會被幹擾,甚至發生之外的結果。這就是說,如廁的時候要講究先來後到。
那 麼在Java 多線程程序當中,當多個線程競爭同一個資源的時候,如何可以保證他們不會產生「打架」的狀況呢?有人說是使用同步機制。沒錯,像上面這個例子,就是典型的 同步案例,一旦第一位開始如廁,則第二位必須等待第一位結束,才能開始他的如廁過程。一個線程,一旦進入某一過程,必須等待正常的返回,並退出這一過程, 下一個線程才能開始這個過程。這裏,最關鍵的就是衛生間的門。其實,衛生間的門擔任的是資源鎖的角色,只要如廁的人鎖上門,就至關於得到了這個鎖,而當他 打開鎖出來之後,就至關於釋放了這個鎖。
也就是說,多線程的線程同步機制其實是靠鎖的概念來控制的。那麼在Java程序當中,鎖是如何體現的呢?
讓咱們從JVM的角度來看看鎖這個概念:
在Java程序運行時環境中,JVM須要對兩類線程共享的數據進行協調:
1)保存在堆中的實例變量
2)保存在方法區中的類變量
這兩類數據是被全部線程共享的。
(程序不須要協調保存在Java 棧當中的數據。由於這些數據是屬於擁有該棧的線程所私有的。)
在java虛擬機中,每一個對象和類在邏輯上都是和一個監視器相關聯的。
對於對象來講,相關聯的監視器保護對象的實例變量。
對於類來講,監視器保護類的類變量。
(若是一個對象沒有實例變量,或者一個類沒有變量,相關聯的監視器就什麼也不監視。)
爲了實現監視器的排他性監視能力,java虛擬機爲每個對象和類都關聯一個鎖。表明任什麼時候候只容許一個線程擁有的特權。線程訪問實例變量或者類變量不需鎖。
可是若是線程獲取了鎖,那麼在它釋放這個鎖以前,就沒有其餘線程能夠獲取一樣數據的鎖了。(鎖住一個對象就是獲取對象相關聯的監視器)
類鎖實際上用對象鎖來實現。當虛擬機裝載一個class文件的時候,它就會建立一個java.lang.Class類的實例。當鎖住一個對象的時候,實際上鎖住的是那個類的Class對象。
一個線程能夠屢次對同一個對象上鎖。對於每個對象,java虛擬機維護一個加鎖計數器,線程每得到一次該對象,計數器就加1,每釋放一次,計數器就減 1,當計數器值爲0時,鎖就被徹底釋放了。
java編程人員不須要本身動手加鎖,對象鎖是java虛擬機內部使用的。
在java程序中,只須要使用synchronized塊或者synchronized方法就能夠標誌一個監視區域。當每次進入一個監視區域時,java 虛擬機都會自動鎖上對象或者類。
看到這裏,我想大家必定都疲勞了吧?o(∩_∩)o...哈哈。讓咱們休息一下,可是在這以前,請大家必定要記着:
當一個有限的資源被多個線程共享的時候,爲了保證對共享資源的互斥訪問,咱們必定要給他們排出一個先來後到。而要作到這一點,對象鎖在這裏起着很是重要的做用。編程
在上一篇中,咱們講到了多線程是如何處理共享資源的,以及保證他們對資源進行互斥訪問所依賴的重要機制:對象鎖。
本篇中,咱們來看一看傳統的同步實現方式以及這背後的原理。
不少人都知道,在Java多線程編程中,有一個重要的關鍵字,synchronized。可是不少人看到這個東西會感到困惑:「都說同步機制是經過對象鎖來實現的,可是這麼一個關鍵字,我也看不出來Java程序鎖住了哪一個對象阿?「
沒錯,我一開始也是對這個問題感到困惑和不解。不過還好,咱們有下面的這個例程:安全
這個程序其實就是讓10個線程在控制檯上數數,從1數到9999。理想狀況下,咱們但願看到一個線程數完,而後纔是另外一個線程開始數數。可是這個程序的執行過程告訴咱們,這些線程仍是亂糟糟的在那裏搶着報數,絲毫沒有任何規矩可言。
可是細心的讀者注意到:run方法仍是加了一個synchronized關鍵字的,按道理說,這些線程應該能夠一個接一個的執行這個run方法纔對阿。
可是經過上一篇中,咱們提到的,對於一個成員方法加synchronized關鍵字,這其實是以這個成員方法所在的對象自己做爲對象鎖。在本例中,就是 以ThreadTest類的一個具體對象,也就是該線程自身做爲對象鎖的。一共十個線程,每一個線程持有本身 線程對象的那個對象鎖。這必然不能產生同步的效果。換句話說,若是要對這些線程進行同步,那麼這些線程所持有的對象鎖應當是共享且惟一的!
咱們來看下面的例程:多線程
咱們注意到,該程序經過在main方法啓動10個線程以前,建立了一個String類型的對象。並經過ThreadTest2的構造函數,將這個對象賦值 給每個ThreadTest2線程對象中的私有變量lock。根據Java方法的傳值特色,咱們知道,這些線程的lock變量實際上指向的是堆內存中的 同一個區域,即存放main函數中的lock變量的區域。
程序將原來run方法前的synchronized關鍵字去掉,換用了run方法中的一個synchronized塊來實現。這個同步塊的對象鎖,就是 main方法中建立的那個String對象。換句話說,他們指向的是同一個String類型的對象,對象鎖是共享且惟一的!
因而,咱們看到了預期的效果:10個線程再也不是爭先恐後的報數了,而是一個接一個的報數。
再來看下面的例程:併發
細心的讀者發現了:這段代碼沒有使用main方法中建立的String對象做爲這10個線程的線程鎖。而是經過在run方法中調用本線 程中一個靜態的同步 方法abc而實現了線程的同步。我想看到這裏,大家應該很困惑:這裏synchronized靜態方法是用什麼來作對象鎖的呢?
咱們知道,對於同步靜態方法,對象鎖就是該靜態放發所在的類的Class實例,因爲在JVM中,全部被加載的類都有惟一的類對象,具體到本例,就是惟一的 ThreadTest3.class對象。無論咱們建立了該類的多少實例,可是它的類實例仍然是一個!
這樣咱們就知道了:
一、對於同步的方法或者代碼塊來講,必須得到對象鎖纔可以進入同步方法或者代碼塊進行操做;
二、若是採用method級別的同步,則對象鎖即爲method所在的對象,若是是靜態方法,對象鎖即指method所在的
Class對象(惟一);
三、對於代碼塊,對象鎖即指synchronized(abc)中的abc;
四、由於第一種狀況,對象鎖即爲每個線程對象,所以有多個,因此同步失效,第二種共用同一個對象鎖lock,所以同步生效,第三個由於是
static所以對象鎖爲ThreadTest3的class 對象,所以同步生效。
如上述正確,則同步有兩種方式,同步塊和同步方法(爲何沒有wait和notify?這個我會在補充章節中作出闡述)
若是是同步代碼塊,則對象鎖須要編程人員本身指定,通常有些代碼爲synchronized(this)只有在單態模式才生效;
(本類的實例有且只有一個)
若是是同步方法,則分靜態和非靜態兩種 。
靜態方法則必定會同步,非靜態方法需在單例模式才生效,推薦用靜態方法(不用擔憂是否單例)。
因此說,在Java多線程編程中,最多見的synchronized關鍵字其實是依靠對象鎖的機制來實現線程同步的。
咱們彷佛能夠聽到synchronized在向咱們說:「給我一把 鎖,我能創造一個規矩」。框架