在並發環境下,解決共用資源衝突問題時,可以考慮使用鎖機制。
1.對象的鎖
所有對象都自動含有單一的鎖。
JVM負責跟蹤對象被加鎖的次數。如果一個對象被解鎖,其計數變為0。在任務(線程)第一次給對象加鎖的時候,計數變為1。每當這個相同的任務(線程)在此對象上獲得鎖時,計數會遞增。
只有首先獲得鎖的任務(線程)才能繼續擷取該對象上的多個鎖。
每當任務離開一個synchronized方法,計數遞減,當計數為0的時候,鎖被完全釋放,此時別的任務就可以使用此資源。
2.synchronized同步塊
synchronized有兩種格式:
格式1:
synchronized(任何對象){
//訪問共用變數的臨界區(程式段),又稱同步代碼塊
}
格式2:同步化方法。在方法的前面加上synchronized,如:
public synchronized void add() {
//臨界區
}
共用變數所關聯的對象鎖是如何選擇的。即
synchronized (任何對象) {
//臨界區
}
1.synchronized鎖定的是一個具體對象,通常是共用變數的對象。用synchronized括起來的程式段是訪問該共用變數的臨界區,即synchronized代碼塊。由於所有鎖定同一個對象的線程之間,在synchronized代碼塊上是互斥的,也就是說,這些線程的synchronized代碼塊之間是串列執行的,不再是互相交替穿插並發執行,因而保證了synchronized 代碼塊操作的原子性。但synchronized代碼塊與所有線程的非synchronized 代碼塊之間以及非synchronized代碼塊與非synchronized代碼塊之間都是互相交替穿插並發執行的,故synchronized代碼塊操作的原子性是邏輯上的,而不是物理上的不可打斷。
2.每一個Java對象都有且只有一個對象鎖。任何時刻,一個對象鎖只能被一個線程所擁有。若兩個或多個線程鎖定的不是同一個對象,則它們的synchronized代碼塊可以互相交替穿插並發執行。
3.所有的非synchronized代碼塊或方法,都可自由調用。如線程A獲得了對象鎖,調用需要該對象鎖的synchronized代碼塊,其他線程仍然可以自由調用所有非synchronized方法和代碼
4.若線程A獲得了對象O的對象鎖,調用對象O的synchronized代碼塊或方法,則線程A仍然可以調用其他任何需要對象O的鎖的synchronized代碼塊或方法,這是因為線程A已經獲得了對象O的對象鎖了。線程A同時可以調用需要另一個對象K的鎖的synchronized代碼塊或方法,這意味著線程A同時擁有對象O和對象K的對象鎖。
5.只有當一個線程執行完它所調用的synchronized代碼塊或方法時,無論是正常執行完,還是異常拋出,該線程才會釋放所擷取的對象鎖。synchronized並不必然地保護資料。程式員應該仔細分析,識別出程式中所有的臨界區,並對這些臨界區施加synchronized機制。若有一處或多處遺漏,則共用變數中資料就會產生錯誤
6.臨界區中的共用變數應定義為private型。否則,其他類的方法可能直接存取和操作該共用變數,這樣synchronized的保護就失去了意義。所以只能通過臨界區訪問共用變數。故鎖定的對象通常是this,即通常格式都是:synchronized(this){…}
7.一定要保證,所有對共用變數的訪問與操作均在synchronized代碼塊中進行。
8.通常共用變數都是執行個體變數。若臨界區中的共用變數是一個類變數,則問題複雜化了,因為類方法與執行個體方法均可訪問類變數。而synchronized鎖定的必須是對象,不能是類。建議是若臨界區中的共用變數是一個類變數,則應該用類方法來訪問操作該類變數。這個類方法成為一個臨界區,必須將該類方法定義為synchronized方法。所有要訪問該共用類變數的執行個體方法,都應該調用定義為synchronized的類方法進行。若執行個體方法一定要想在自己的代碼內部,不通過synchronized的類方法訪問共用類變數,則可通過synchronized(類名.class){…來訪問類鎖。Java中,每一個類都有一個類對象,這個類對象實際上是java.lang.Class的一個執行個體對象,所謂類鎖就是這個類對象的一把鎖。注意類鎖與這個類的執行個體對象的對象鎖雖然都是對象鎖,卻是不同的兩把鎖。所有像synchronized(類名.class()){=同步代碼塊} 這樣鎖定類對象(注意:不是鎖定類的某一個執行個體對象),其中的synchronized代碼塊,都是串列執行的,訪問或使用類鎖要仔細考慮和權衡
9.當一個線程進入死亡狀態,線程擁有的所有的對象鎖都被釋放。
3.Lock對象鎖
Java SE5引入了java.util.concurrent.lock類庫,這是解決互斥問題的第二種機制。用ReentrantLock類建立一個Lock對象,來保護臨界區。用ReentrantLock保護代碼塊的基本結構如下。
private Lock locker =new ReentrantLock();
locker.lock(); // 加鎖
try{
…
}finally{
locker.unlock(); // 解鎖
}
lock()與unlock()必須配套使用。必須確保lock()對應的unlock()一定會得到執行。因此,必須把unlock()放到finally塊中,確保無論是正常執行,還是異常拋出,unlock()一定會得到執行。
synchronized和lock的區別:
Lock 的鎖定是通過代碼實現的,而 synchronized 是在 JVM 層面上實現的
synchronized 在鎖定時如果方法塊拋出異常,JVM 會自動將鎖釋放掉,不會因為出了異常沒有釋放鎖造成線程死結。但是 Lock 的話就享受不到 JVM 帶來自動的功能,出現異常時必須在 finally 將鎖釋放掉,否則將會引起死結。
在資源競爭不是很激烈的情況下,偶爾會有同步的情形下,synchronized是很合適的。原因在於,編譯器通常會儘可能的進行最佳化synchronize,另外可讀性非常好,不管用沒用過5.0多線程包的程式員都能理解。
ReentrantLock:
ReentrantLock提供了多樣化的同步,比如有時間限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在資源競爭不激烈的情形下,效能稍微比synchronized差點點。但是當同步非常激烈的時候,synchronized的效能一下子能下降好幾十倍。而ReentrantLock確還能維持常態。
Atomic:
和上面的類似,不激烈情況下,效能比synchronized略遜,而激烈的時候,也能維持常態。激烈的時候,Atomic的效能會優於ReentrantLock一倍左右。但是其有一個缺點,就是只能同步一個值,一段代碼中只能出現一個Atomic的變數,多於一個同步無效。因為他不能在多個Atomic之間同步。