前言java
咱們能夠在計算機上運行各類計算機軟件程序。每個運行的程序可能包括多個獨立運行的線程(Thread)。 線程(Thread)是一份獨立運行的程序,有本身專用的運行棧。線程有可能和其餘線程共享一些資源,好比,內存,文件,數據庫等。 當多個線程同時讀寫同一份共享資源的時候,可能會引發衝突。這時候,咱們須要引入線程「同步」機制,即各位線程之間要有個先來後到,不能一窩蜂擠上去搶做一團。 同步這個詞是從英文synchronize(使同時發生)翻譯過來的。我也不明白爲何要用這個很容易引發誤解的詞。既然你們都這麼用,我們也就只好這麼將就。 線程同步的真實意思和字面意思剛好相反。線程同步的真實意思,實際上是「排隊」:幾個線程之間要排隊,一個一個對共享資源進行操做,而不是同時進行操做。 c++
關於線程同步,須要緊緊記住的第一點是:線程同步就是線程排隊。同步就是排隊。線程同步的目的就是避免線程「同步」執行。這可真是個無聊的繞口令。
關於線程同步,須要緊緊記住的第二點是:「共享」這兩個字。只有共享資源的讀寫訪問才須要同步。若是不是共享資源,那麼就根本沒有同步的必要。
關於線程同步,須要緊緊記住的第三點是:只有「變量」才須要同步訪問。若是共享的資源是固定不變的,那麼就至關於「常量」,線程同時讀取常量也不須要同步。至少一個線程修改共享資源,這樣的狀況下,線程之間就須要同步。
關於線程同步,須要緊緊記住的第四點是:多個線程訪問共享資源的代碼有多是同一份代碼,也有多是不一樣的代碼;不管是否執行同一份代碼,只要這些線程的代碼訪問同一份可變的共享資源,這些線程之間就須要同步。數據庫
1、同步鎖加在哪裏編程
同步鎖加在哪裏呢?固然是加在共享資源上了。反應快的讀者必定會搶先回答。沒錯,若是可能,咱們固然儘可能把同步鎖加在共享資源上。一些比較完善的共享資源,好比,文件系統,數據庫系統等,自身都提供了比較完善的同步鎖機制。咱們不用另外給這些資源加鎖,這些資源本身就有鎖。數組
可是,大部分狀況下,咱們在代碼中訪問的共享資源都是比較簡單的共享對象。這些對象裏面沒有地方讓咱們加鎖。 讀者可能會提出建議:爲何不在每個對象內部都增長一個新的區域,專門用來加鎖呢?這種設計理論上固然也是可行的。問題在於,線程同步的狀況並非很廣泛。若是由於這小几率事件,在全部對象內部都開闢一塊鎖空間,將會帶來極大的空間浪費。得不償失。 安全
因而,現代的編程語言的設計思路都是把同步鎖加在代碼段上。確切的說,是把同步鎖加在訪問共享資源的代碼段上。這一點必定要記住,同步鎖是加在代碼段上的。 多線程
同步鎖加在代碼段上,就很好地解決了上述的空間浪費問題。可是卻增長了模型的複雜度,也增長了咱們的理解難度。 編程語言
2、同步鎖加在代碼段函數
如今咱們就來仔細分析「同步鎖加在代碼段上 」的線程同步模型。 首先,咱們已經解決了同步鎖加在哪裏的問題。咱們已經肯定,同步鎖不是加在共享資源上,而是加在訪問共享資源的代碼段上。 其次,咱們要解決的問題是,咱們應該在代碼段上加什麼樣的鎖。這個問題是重點中的重點。這是咱們尤爲要注意的問題:訪問同一份共享資源的不一樣代碼段,應該加上同一個同步鎖;若是加的是不一樣的同步鎖,那麼根本就起不到同步的做用,沒有任何意義。 優化
這就是說,同步鎖自己也必定是多個線程之間的共享對象。
3、Java語言的synchronized關鍵字
爲了加深理解,舉幾個代碼段同步的例子,synchronized整個語法表現形式:
synchronized(同步鎖) {
// 訪問共享資源,須要同步的代碼段
}
這裏尤爲要注意的就是,同步鎖自己必定要是共享的對象。
… f1() {
Object lock1 = new Object(); // 產生一個同步鎖
synchronized(lock1){
// 代碼段 A
// 訪問共享資源 resource1
// 須要同步
}
}
上面這段代碼沒有任何意義。由於那個同步鎖是在函數體內部產生的。每一個線程調用這段代碼的時候,都會產生一個新的同步鎖。那麼多個線程之間,使用的是不一樣的同步鎖。根本達不到同步的目的。同步代碼必定要寫成以下的形式,纔有意義:
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 A
// 訪問共享資源 resource1
// 須要同步
}}
你不必定要把同步鎖聲明爲static或者public,可是你必定要保證相關的同步代碼之間,必定要使用同一個同步鎖。 講到這裏,你必定會好奇,這個同步鎖究竟是個什麼東西。爲何隨便聲明一個Object對象,就能夠做爲同步鎖?
在Java裏面,同步鎖的概念就是這樣的。任何一個Object Reference均可以做爲同步鎖。咱們能夠把Object Reference理解爲對象在內存分配系統中的內存地址。所以,要保證同步代碼段之間使用的是同一個同步鎖,咱們就要保證這些同步代碼段的synchronized關鍵字使用的是同一個Object Reference,同一個內存地址。這也是爲何我在前面的代碼中聲明lock1的時候,使用了final關鍵字,這就是爲了保證lock1的Object Reference在整個系統運行過程當中都保持不變。
一些求知慾強的讀者可能想要繼續深刻了解synchronzied(同步鎖)的實際運行機制。Java虛擬機規範中(你能夠在google用「JVM Spec」等關鍵字進行搜索),有對synchronized關鍵字的詳細解釋。synchronized會編譯成 monitor enter, … monitor exit之類的指令對。Monitor就是實際上的同步鎖。每個Object Reference在概念上都對應一個monitor。
咱們繼續看幾個例子,加深對同步鎖模型的理解。
public static final byte[] lock1 = new byte[0]; // 特殊的instance變量
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 A
// 訪問共享資源 resource1
// 須要同步
}
}
… f2() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 B
// 訪問共享資源 resource1
// 須要同步
}
}
注:零長度的byte數組對象建立起來將比任何對象都經濟,查看編譯後的字節碼:生成零長度的byte[]對象只需3條操做碼,而Object lock = new Object()則須要7行操做碼。
上述的代碼中,代碼段A和代碼段B就是同步的。由於它們使用的是同一個同步鎖lock1。 若是有10個線程同時執行代碼段A,同時還有20個線程同時執行代碼段B,那麼這30個線程之間都是要進行同步的。
這30個線程都要競爭一個同步鎖lock1。同一時刻,只有一個線程可以得到lock1的全部權,只有一個線程能夠執行代碼段A或者代碼段B。其餘競爭失敗的線程只能暫停運行,進入到該同步鎖的就緒(Ready)隊列。
每個同步鎖下面都掛了幾個線程隊列,包括就緒(Ready)隊列,待召(Waiting)隊列等。好比,lock1對應的就緒隊列就能夠叫作lock1 - ready queue。每一個隊列裏面均可能有多個暫停運行的線程。注意,競爭同步鎖失敗的線程進入的是該同步鎖的就緒(Ready)隊列,而不是後面要講述的待召隊列(Waiting Queue,也能夠翻譯爲等待隊列)。就緒隊列裏面的線程老是時刻準備着競爭同步鎖,時刻準備着運行。而待召隊列裏面的線程則只能一直等待,直到等到某個信號的通知以後,纔可以轉移到就緒隊列中,準備運行。 成功獲取同步鎖的線程,執行完同步代碼段以後,會釋放同步鎖。該同步鎖的就緒隊列中的其餘線程就繼續下一輪同步鎖的競爭。成功者就能夠繼續運行,失敗者仍是要乖乖地待在就緒隊列中。
所以,線程同步是很是耗費資源的一種操做。咱們要儘可能控制線程同步的代碼段範圍。同步的代碼段範圍越小越好 。咱們用一個名詞「同步粒度 」來表示同步代碼段的範圍。
4、同步粒度
在Java語言裏面,咱們能夠直接把synchronized關鍵字直接加在函數的定義上。好比:
… synchronized … f1() {
// f1 代碼段
}
這段代碼就等價於
… f1() {
synchronized(this){ // 同步鎖就是對象自己
// f1 代碼段
}
}
一樣的原則適用於靜態(static)函數,好比:
… static synchronized … f1() {
// f1 代碼段
}
這段代碼就等價於
…static … f1() {
synchronized(Class.forName(…)){ // 同步鎖是類定義自己
// f1 代碼段}
}
可是,咱們要儘可能避免這種直接把synchronized加在函數定義上的偷懶作法。由於咱們要控制同步粒度。同步的代碼段越小越好。synchronized控制的範圍越小越好。 咱們不只要在縮小同步代碼段的長度上下功夫,咱們同時還要注意細分同步鎖。好比,下面的代碼:
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 A
// 訪問共享資源 resource1
// 須要同步
}
}
… f2() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 B
// 訪問共享資源 resource1
// 須要同步
}
}
… f3() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 C
// 訪問共享資源 resource2
// 須要同步
}
}
… f4() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 D
// 訪問共享資源 resource2
// 須要同步
}
}
上述的4段同步代碼,使用同一個同步鎖lock1。全部調用4段代碼中任何一段代碼的線程,都須要競爭同一個同步鎖lock1。 咱們仔細分析一下,發現這是沒有必要的。 由於f1()的代碼段A和f2()的代碼段B訪問的共享資源是resource1,f3()的代碼段C和f4()的代碼段D訪問的共享資源是resource2,它們沒有必要都競爭同一個同步鎖lock1。咱們能夠增長一個同步鎖lock2。f3()和f4()的代碼能夠修改成:
public static final Object lock2 = new Object();
… f3() {
synchronized(lock2){ // lock2 是公用同步鎖
// 代碼段 C
// 訪問共享資源 resource2
// 須要同步
}
}
… f4() {
synchronized(lock2){ // lock2 是公用同步鎖
// 代碼段 D
// 訪問共享資源 resource2
// 須要同步
}
}
這樣,f1()和f2()就會競爭lock1,而f3()和f4()就會競爭lock2。這樣,分開來分別競爭兩個鎖,就能夠大大較少同步鎖競爭的機率,從而減小系統的開銷。
5、信號量(wait()/notify()/notifyAll()使用)
同步鎖模型只是最簡單的同步模型。同一時刻,只有一個線程可以運行同步代碼。 有的時候,咱們但願處理更加複雜的同步模型,好比生產者/消費者模型、讀寫同步模型等。這種狀況下,同步鎖模型就不夠用了。咱們須要一個新的模型。這就是咱們要講述的信號量模型。
信號量模型的工做方式以下:線程在運行的過程當中,能夠主動停下來,等待某個信號量的通知;這時候,該線程就進入到該信號量的待召(Waiting)隊列當中;等到通知以後,再繼續運行。 不少語言裏面,同步鎖都由專門的對象表示,對象名一般叫Monitor。 一樣,在不少語言中,信號量一般也有專門的對象名來表示,好比,Mutex,Semphore。
信號量模型要比同步鎖模型複雜許多。一些系統中,信號量甚至能夠跨進程進行同步。另一些信號量甚至還有計數功能,可以控制同時運行的線程數。
咱們沒有必要考慮那麼複雜的模型。全部那些複雜的模型,都是最基本的模型衍生出來的。只要掌握了最基本的信號量模型——「等待/通知」模型,複雜模型也就迎刃而解了。
以Java語言爲例。Java語言裏面的同步鎖和信號量概念都很是模糊,沒有專門的對象名詞來表示同步鎖和信號量,只有兩個同步鎖相關的關鍵字—volatile和synchronized。 這種模糊雖然致使概念不清,但同時也避免了Monitor、Mutex、Semphore等名詞帶來的種種誤解。咱們沒必要執着於名詞之爭,能夠專一於理解實際的運行原理。 在Java語言裏面,任何一個Object Reference均可以做爲同步鎖。一樣的道理,任何一個Object Reference也能夠做爲信號量。 Object對象的wait()方法就是等待通知,Object對象的notify()方法就是發出通知。 具體調用方法爲 :
(1)等待某個信號量的通知
public static final Object signal = new Object();
… f1() {
synchronized(singal) { // 首先咱們要獲取這個信號量。這個信號量同時也是一個同步鎖
// 只有成功獲取了signal這個信號量兼同步鎖以後,咱們纔可能進入這段代碼
signal.wait(); // 這裏要放棄信號量。本線程要進入signal信號量的待召(Waiting)隊列
// 可憐。辛辛苦苦爭取到手的信號量,就這麼被放棄了
// 等到通知以後,從待召(Waiting)隊列轉到就緒(Ready)隊列裏面
// 轉到了就緒隊列中,離CPU核心近了一步,就有機會繼續執行下面的代碼了。
// 仍然須要把signal同步鎖競爭到手,纔可以真正繼續執行下面的代碼。命苦啊。
…
}
}
須要注意的是,上述代碼中的signal.wait()的意思。signal.wait()很容易致使誤解。signal.wait()的意思並非說,signal開始wait,而是說,運行這段代碼的當前線程開始wait這個signal對象,即進入signal對象的待召(Waiting)隊列。
(2)發出某個信號量的通知
… f2() {
synchronized(singal) { // 首先,咱們一樣要獲取這個信號量。同時也是一個同步鎖。
// 只有成功獲取了signal這個信號量兼同步鎖以後,咱們纔可能進入這段代碼
signal.notify(); // 這裏,咱們通知signal的待召隊列中的某個線程。
// 若是某個線程等到了這個通知,那個線程就會轉到就緒隊列中
// 可是本線程仍然繼續擁有signal這個同步鎖,本線程仍然繼續執行
// 嘿嘿,雖然本線程好心通知其餘線程,
// 可是,本線程可沒有那麼高風亮節,放棄到手的同步鎖
// 本線程繼續執行下面的代碼
…
}
}
須要注意的是,signal.notify()的意思。signal.notify()並非通知signal這個對象自己。而是通知正在等待signal信號量的其餘線程。
以上就是Object的wait()和notify()的基本用法。 實際上,wait()還能夠定義等待時間,當線程在某信號量的待召隊列中,等到足夠長的時間,就會等無可等,無需再等,本身就從待召隊列轉移到就緒隊列中了。 另外,還有一個notifyAll()方法,表示通知待召隊列裏面的全部線程。 這些細節問題,並不對大局產生影響。
6、總結
1. wait方法:
該方法屬於Object的方法,wait方法的做用是使得當前調用wait方法所在部分(代碼塊)的線程中止執行,並釋放當前得到的調用wait所在的代碼塊的鎖,並在其餘線程調用notify或者notifyAll方法時恢復到競爭鎖狀態(一旦得到鎖就恢復執行)。
調用wait方法須要注意幾點:
第一點:wait被調用的時候必須在擁有鎖(即synchronized修飾的)的代碼塊中。
第二點:恢復執行後,從wait的下一條語句開始執行,於是wait方法老是應當在while循環中調用,以避免出現恢復執行後繼續執行的條件不知足卻繼續執行的狀況。
第三點:若wait方法參數中帶時間,則除了notify和notifyAll被調用能激活處於wait狀態(等待狀態)的線程進入鎖競爭外,在其餘線程中interrupt它或者參數時間到了以後,該線程也將被激活到競爭狀態。
第四點:wait方法被調用的線程必須得到以前執行到wait時釋放掉的鎖從新得到纔可以恢復執行。
2. notify方法和notifyAll方法:
notify方法通知調用了wait方法,可是還沒有激活的一個線程進入線程調度隊列(即進入鎖競爭),注意不是當即執行。而且具體是哪個線程不能保證。另一點就是被喚醒的這個線程必定是在等待wait所釋放的鎖。
notifyAll方法則喚醒全部調用了wait方法,還沒有激活的進程進入競爭隊列。
3. synchronized關鍵字:
第一點:synchronized用來標識一個普通方法時,表示一個線程要執行該方法,必須取得該方法所在的對象的鎖。
第二點:synchronized用來標識一個靜態方法時,表示一個線程要執行該方法,必須得到該方法所在的類的類鎖。
第三點:synchronized修飾一個代碼塊。相似這樣:synchronized(obj) { //code.... }。表示一個線程要執行該代碼塊,必須得到obj的鎖。這樣作的目的是減少鎖的粒度,保證當不一樣塊所需的鎖不衝突時不用對整個對象加鎖。利用零長度的byte數組對象作obj很是經濟。
4. atomic action(原子操做):
在JAVA中,如下兩點操做是原子操做。可是c和c++中並不如此。
第一點:對引用變量和除了long和double以外的原始數據類型變量進行讀寫。
第二點:對全部聲明爲volatile的變量(包括long和double)的讀寫。
另外:在java.util.concurrent和java.util.concurrent.atomic包中提供了一些不依賴於同步機制的線程安全的類和方法。
關於volatile,咱們知道,在Java中設置變量值的操做,除了long和double類型的變量外都是原子操做,也就是說,對於變量值的簡單讀寫操做沒有必要進行同步。這在JVM 1.2以前,Java的內存模型實現老是從主存讀取變量,是不須要進行特別的注意的。而隨着JVM的成熟和優化,如今在多線程環境下volatile關鍵字的使用變得很是重要。在當前的Java內存模型下,線程能夠把變量保存在本地內存(好比機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能形成一個線程在主存中修改了一個變量的值,而另一個線程還繼續使用它在寄存器中的變量值的拷貝,形成數據的不一致。要解決這個問題,只須要像在本程序中的這樣,把該變量聲明爲volatile(不穩定的)便可,這就指示JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。通常說來,多任務環境下各任務間共享的標誌都應該加volatile修飾。
Volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。並且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存。這樣在任什麼時候刻,兩個不一樣的線程老是看到某個成員變量的同一個值。
Java語言規範中指出:爲了得到最佳速度,容許線程保存共享成員變量的私有拷貝,並且只當線程進入或者離開同步代碼塊時才與共享成員變量的原始值對比。這樣當多個線程同時與某個對象交互時,就必需要注意到要讓線程及時的獲得共享成員變量的變化。 而volatile關鍵字就是提示VM:對於這個成員變量不能保存它的私有拷貝,而應直接與共享成員變量交互。
使用建議:在兩個或者更多的線程訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者爲常量時,沒必要使用。 因爲使用volatile屏蔽掉了VM中必要的代碼優化,因此在效率上比較低,所以必定在必要時才使用此關鍵字。
5. lock方法和unlock方法: