在我們的實際應用當中可能經常會遇到這樣一個場景:多個線程讀或者、寫相同的數據,訪問相同的文件等等。對於這種情況如果我們不加以控制,是非常容易導致錯誤的。在java中,為了解決這個問題,引入臨界區概念。所謂臨界區是指一個訪問共用資源的程序片段,而這些共用資源又無法同時被多個線程訪問。
在java中為了實現臨界區提供了同步機制。當一個線程試圖訪問一個臨界區時,他將使用一種同步機制來查看是不是已經有其他線程進入臨界區。如果沒有則他就可以進入臨界區,否則他就會被同步機制掛起,指定進入的線程離開這個臨界區。
臨界區規定:每次只准許一個進程進入臨界區,進入后不允許其他進程進入。調度法則為(百度百科):
1、如果有若干進程要求進入空閑的臨界區,一次僅允許一個進程進入。
2、任何時候,處於臨界區內的進程不可多於一個。如已有進程進入自己的臨界區,則其它所有試圖進入臨界區的進程必須等待。
3、進入臨界區的進程要在有限時間內退出,以便其它進程能及時進入自己的臨界區。
4、如果進程不能進入自己的臨界區,則應讓出CPU,避免進程出現“忙等”現象。
下面介紹使用synchronized關鍵字來實現同步機制。
一、synchronized關鍵字
1.1、簡介
synchronized,我們謂之鎖,主要用來給方法、代碼塊加鎖。當某個方法或者代碼塊使用synchronized時,那么在同一時刻至多僅有有一個線程在執行該段代碼。當有多個線程訪問同一對象的加鎖方法/代碼塊時,同一時間只有一個線程在執行,其余線程必須要等待當前線程執行完之后才能執行該代碼段。但是,其余線程是可以訪問該對象中的非加鎖代碼塊的。
synchronized主要包括兩種方法:synchronized 方法、synchronized 塊。
1.2、synchronized 方法
通過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。如:
public synchronized void getResult();
synchronized方法控制對類成員變量的訪問。它是如何來避免類成員變量的訪問控制呢?我們知道方法使用了synchronized關鍵字表明該方法已加鎖,在任一線程在訪問改方法時都必須要判斷該方法是否有其他線程在“獨占”。每個類實例對應一個把鎖,每個synchronized方法都必須調用該方法的類實例的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨占該鎖,直到從該方法返回時才將鎖釋放,被阻塞的線程方能獲得該鎖。
其實synchronized方法是存在缺陷的,如果我們將一個很大的方法聲明為synchronized將會大大影響效率的。如果多個線程在訪問一個synchronized方法,那么同一時刻只有一個線程在執行該方法,而其他線程都必須等待,但是如果該方法沒有使用synchronized,則所有線程可以在同一時刻執行它,減少了執行的總時間。所以如果我們知道一個方法不會被多個線程執行到或者說不存在資源共享的問題,則不需要使用synchronized關鍵字。但是如果一定要使用synchronized關鍵字,那么我們可以synchronized代碼塊來替換synchronized方法。
1.3、synchronized 塊
synchronized代碼塊所起到的作用和synchronized方法一樣,只不過它使臨界區變的盡可能短了,換句話說:它只把需要的共享數據保護起來,其余的長代碼塊留出此操作。語法如下:
synchronized(object) { //允許訪問控制的代碼 }
如果我們需要以這種方式來使用synchronized關鍵字,那么必須要通過一個對象引用來作為參數,通常這個參數我們常使用為this.
synchronized (this) { //允許訪問控制的代碼 }
對於synchronized(this)有如下理解:
1、當兩個並發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊。
2、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問object中的非synchronized(this)同步代碼塊。
3、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其他synchronized(this)同步代碼塊得訪問將被阻塞。
4、第三個例子同樣適用其他同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其他線程對該object對象所有同步代碼部分的訪問都將被暫時阻塞。
5、以上規則對其他對象鎖同樣適用
http://freewxy.iteye.com/blog/978159,這篇博客使用實例對上面四點進行了較為詳細的說明,這里就不多闡述了。
http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html這篇博客對synchronized的使用舉了一個很不錯的例子(拿鑰匙進房間)。這里由於篇幅問題LZ就不多闡述了,下面我們來刨刨synchronized稍微高級點的東西。
1.4、進階
在java多線程中存在一個“先來后到”的原則,也就是說誰先搶到鑰匙,誰先用。我們知道為避免資源競爭產生問題,java使用同步機制來避免,而同步機制是使用鎖概念來控制的。那么在Java程序當中,鎖是如何體現的呢?這里我們需要弄清楚兩個概念:
什么是鎖?
什么是鎖?在日常生活中,它就是一個加在門、箱子、抽屜等物體上的封緘器,防止別人偷窺或者偷盜,起到一個保護的作用。在java中同樣如此,鎖對對象起到一個保護的作用,一個線程如果獨占了某個資源,那么其他的線程別想用,想用?等我用完再說吧!
在java程序運行環境中,JVM需要對兩類線程共享的數據進行協調:
1、保存在堆中的實例變量
2、保存在方法區中的類變量。
在java虛擬機中,每個對象和類在邏輯上都是和一個監視器相關聯的。對於對象來說,相關聯的監視器保護對象的實例變量。 對於類來說,監視器保護類的類變量。如果一個對象沒有實例變量,或者說一個類沒有變量,相關聯的監視器就什么也不監視。
為了實現監視器的排他性監視能力,java虛擬機為每一個對象和類都關聯一個鎖。代表任何時候只允許一個線程擁有的特權。線程訪問實例變量或者類變量不需鎖。 如果某個線程獲取了鎖,那么在它釋放該鎖之前其他線程是不可能獲取同樣鎖的。一個線程可以多次對同一個對象上鎖。對於每一個對象,java虛擬機維護一個加鎖計數器,線程每獲得一次該對象,計數器就加1,每釋放一次,計數器就減 1,當計數器值為0時,鎖就被完全釋放了。
java編程人員不需要自己動手加鎖,對象鎖是java虛擬機內部使用的。在java程序中,只需要使用synchronized塊或者synchronized方法就可以標志一個監視區域。當每次進入一個監視區域時,java 虛擬機都會自動鎖上對象或者類。(摘自java的鎖機制)。
鎖的是什么?
在這個問題之前我們必須要明確一點:無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象。在java中每一個對象都可以作為鎖,它主要體現在下面三個方面:
首先我們先看下面例子:
public class ThreadTest_01 implements Runnable{ @Override public synchronized void run() { for(int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + "run......"); } } public static void main(String[] args) { for(int i = 0 ; i < 5 ; i++){ new Thread(new ThreadTest_01(),"Thread_" + i).start(); } } }
部分運行結果:
Thread_2run......
Thread_2run......
Thread_4run......
Thread_4run......
Thread_3run......
Thread_3run......
Thread_3run......
Thread_2run......
Thread_4run......
這個結果與我們預期的結果有點不同(這些線程在這里亂跑),照理來說,run方法加上synchronized關鍵字后,會產生同步效果,這些線程應該是一個接着一個執行run方法的。在上面LZ提到,一個成員方法加上synchronized關鍵字后,實際上就是給這個成員方法加上鎖,具體點就是以這個成員方法所在的對象本身作為對象鎖。但是在這個實例當中我們一共new了10個ThreadTest對象,那個每個線程都會持有自己線程對象的對象鎖,這必定不能產生同步的效果。所以:如果要對這些線程進行同步,那么這些線程所持有的對象鎖應當是共享且唯一的!
這個時候synchronized鎖住的是那個對象?它鎖住的就是調用這個同步方法對象。就是說threadTest這個對象在不同線程中執行同步方法,就會形成互斥。達到同步的效果。所以將上面的new Thread(new ThreadTest_01(),"Thread_" + i).start(); 修改為new Thread(threadTest,"Thread_" + i).start();就可以了。
對於同步方法,鎖是當前實例對象。
上面實例是使用synchronized方法,我們在看看synchronized代碼塊:
public class ThreadTest_02 extends Thread{ private String lock ; private String name; public ThreadTest_02(String name,String lock){ this.name = name; this.lock = lock; } @Override public void run() { synchronized (lock) { for(int i = 0 ; i < 3 ; i++){ System.out.println(name + " run......"); } } } public static void main(String[] args) { String lock = new String("test"); for(int i = 0 ; i < 5 ; i++){ new ThreadTest_02("ThreadTest_" + i,lock).start(); } } }
運行結果:
ThreadTest_0 run......
ThreadTest_0 run......
ThreadTest_0 run......
ThreadTest_1 run......
ThreadTest_1 run......
ThreadTest_1 run......
ThreadTest_4 run......
ThreadTest_4 run......
ThreadTest_4 run......
ThreadTest_3 run......
ThreadTest_3 run......
ThreadTest_3 run......
ThreadTest_2 run......
ThreadTest_2 run......
ThreadTest_2 run......
在main方法中我們創建了一個String對象lock,並將這個對象賦予每一個ThreadTest2線程對象的私有變量lock。我們知道java中存在一個字符串池,那么這些線程的lock私有變量實際上指向的是堆內存中的同一個區域,即存放main函數中的lock變量的區域,所以對象鎖是唯一且共享的。線程同步!!
在這里synchronized鎖住的就是lock這個String對象。
對於同步方法塊,鎖是Synchonized括號里配置的對象。
public class ThreadTest_03 extends Thread{ public synchronized static void test(){ for(int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + " run......"); } } @Override public void run() { test(); } public static void main(String[] args) { for(int i = 0 ; i < 5 ; i++){ new ThreadTest_03().start(); } } }
運行結果:
Thread-0 run...... Thread-0 run...... Thread-0 run...... Thread-4 run...... Thread-4 run...... Thread-4 run...... Thread-1 run...... Thread-1 run...... Thread-1 run...... Thread-2 run...... Thread-2 run...... Thread-2 run...... Thread-3 run...... Thread-3 run...... Thread-3 run......
在這個實例中,run方法使用的是一個同步方法,而且是static的同步方法,那么這里synchronized鎖的又是什么呢?我們知道static超脫於對象之外,它屬於類級別的。所以,對象鎖就是該靜態放發所在的類的Class實例。由於在JVM中,所有被加載的類都有唯一的類對象,在該實例當中就是唯一的 ThreadTest_03.class對象。不管我們創建了該類的多少實例,但是它的類實例仍然是一個!所以對象鎖是唯一且共享的。線程同步!!
對於靜態同步方法,鎖是當前對象的Class對象。
如果一個類中定義了一個synchronized的static函數A,也定義了一個synchronized的instance函數B,那么這個類的同一對象Obj,在多線程中分別訪問A和B兩個方法時,不會構成同步,因為它們的鎖都不一樣。A方法的鎖是Obj這個對象,而B的鎖是Obj所屬的那個Class。
鎖的升級
java中鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。下面主要部分主要是對博客:聊聊並發(二)Java SE1.6中的Synchronized的總結。
鎖自旋
我們知道在當某個線程在進入同步方法/代碼塊時若發現該同步方法/代碼塊被其他現在所占,則它就要等待,進入阻塞狀態,這個過程性能是低下的。
在遇到鎖的爭用或許等待事,線程可以不那么着急進入阻塞狀態,而是等一等,看看鎖是不是馬上就釋放了,這就是鎖自旋。鎖自旋在一定程度上可以對線程進行優化處理。
偏向鎖
偏向鎖主要為了解決在沒有競爭情況下鎖的性能問題。在大多數情況下鎖鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當某個線程獲得鎖的情況,該線程是可以多次鎖住該對象,但是每次執行這樣的操作都會因為CAS(CPU的Compare-And-Swap指令)操作而造成一些開銷消耗性能,為了減少這種開銷,這個鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
當有其他線程在嘗試着競爭偏向鎖時,持有偏向鎖的線程就會釋放鎖。
鎖膨脹
多個或多次調用粒度太小的鎖,進行加鎖解鎖的消耗,反而還不如一次大粒度的鎖調用來得高效。
輕量級鎖
輕量級鎖能提升程序同步性能的依據是“對於絕大部分的鎖,在整個同步周期內都是不存在競爭的”,這是一個經驗數據。輕量級鎖在當前線程的棧幀中建立一個名為鎖記錄的空間,用於存儲鎖對象目前的指向和狀態。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。
1.5參考資料
1、《Java 7 並發編程實戰手冊》
3、聊聊並發(二)Java SE1.6中的Synchronized
4、java的鎖機制