關於Java中鎖的總結
多個進程或線程同時(或著說在同一段時間內)訪問同一資源會產生並發(線程安全)問題。解決並發問題可以用鎖。
java的內置鎖:
每個java對象都可以用做一個實現同步的鎖,這些鎖稱為內置鎖。線程進入同步代碼塊或方法的時候會自動獲得該鎖,在退出同步代碼塊或方法時會釋放該鎖。獲得內置鎖的唯一途徑就是進入這個鎖保護的同步代碼塊或方法。java內置鎖是一個互斥鎖,這就意味著最多只有一個線程能夠獲得該鎖,當線程A嘗試去獲得線程B持有的內置鎖時,線程A必須等待或者阻塞,直到線程B釋放這個鎖,如果線程B不釋放這個鎖,那麽線程A將永遠等待下去。
java的對象鎖和類鎖:
java的內置鎖基本上可以分為對象鎖和類鎖,對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對象上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不幹擾的,但是每個類只有一個類鎖。有一點必須註意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定實例方法和靜態方法的區別的。
synchronized只是一個內置鎖的加鎖機制,當某個方法加上synchronized關鍵字後,就表明要獲得該內置鎖才能執行,並不能阻止其他線程訪問不需要獲得該內置鎖的方法。
一、synchronized的用法
1、對象鎖:同步方法,是對該對象加鎖,其他線程將無法訪問需要獲取該對象鎖的方法和代碼塊。
public class Test{ public synchronized void print(){ System.out.println("hello world!"); } }
2、對象鎖:同步代碼塊,這種寫法也是鎖住該對象,和1效果一樣,其他線程將無法訪問需要獲取該對象鎖的方法和代碼塊。
public class Test{ public void print(){ synchronized(this){ System.out.println("hello world!"); } } }
3、對象鎖:同步代碼塊,這種寫法鎖住傳入的對象,不影響需要獲取當前對象鎖的方法和代碼塊的訪問。
public class Test{ private String a = "test"; public void print(){synchronized(a){ System.out.println("hello world!"); } } public synchronized void print1(){ System.out.println("123456"); } }
執行print()裏面的同步代碼塊,會給對象a加鎖,註意不是給Test的對象加鎖,也就是說Test對象的其它synchronized方法和代碼塊不會因為print()而被鎖。同步代碼塊執行完,則釋放對a的鎖。
為了鎖住一個對象的代碼塊而不影響該對象其它synchronized塊的高性能寫法:
public class Test{ private byte[] lock = new byte[0]; public void print(){ synchronized(lock){ System.out.println("hello world!"); } } public synchronized void print1(){ System.out.println("123456"); } }
4、類鎖:用於靜態方法。
public class Test{ public synchronized static void print(){ System.out.println("hello world!"); } }
效果等同於同步代碼塊傳入該類的class對象。
public class Test{ public void print(){ synchronized(Test.class){ System.out.println("hello world!"); } } }
類鎖修飾方法和代碼塊的效果和對象鎖是一樣的,因為類鎖只是一個抽象出來的概念,只是為了區別靜態方法的特點,因為靜態方法是所有對象實例共用的,所以對應著synchronized修飾的靜態方法的鎖也是唯一的,所以抽象出來個類鎖。類鎖和對象鎖是互不幹擾的。同樣,線程獲得對象鎖的同時,也可以獲得該類鎖,即同時獲得兩個鎖,這是允許的。
二、鎖的釋放
一般是執行完畢同步代碼塊(鎖住的代碼塊)後就釋放鎖,也可以用wait()方式半路上釋放鎖。wait()方式就好比蹲廁所到一半,突然發現下水道堵住了,不得已必須出來站在一邊,好讓修下水道師傅(準備執行notify的一個線程)進去疏通馬桶,疏通完畢,師傅大喊一聲: "已經修好了"(notify),剛才出來的同誌聽到後就重新排隊。註意啊,必須等師傅出來啊,師傅不出來,誰也進不去。也就是說notify後,不是其它線程馬上可以進入封鎖區域活動了,而是必須還要等notify代碼所在的封鎖區域執行完畢從而釋放鎖以後,其它線程才可進入。
由於wait()操作而半路出來的同誌沒收到notify信號前是不會再排隊的,他會在旁邊看著這些排隊的人(其中修水管師傅也在其中)。註意,修水管的師傅不能插隊,也得跟那些上廁所的人一樣排隊,不是說一個人蹲了一半出來後,修水管師傅就可以突然冒出來然後立刻進去搶修了,他要和原來排隊的那幫人公平競爭,因為他也是個普通線程。如果修水管師傅排在後面,則前面的人進去後,發現堵了,就wait,然後出來站到一邊,再進去一個,再wait,出來,站到一邊,直到師傅進去修好後執行notify。這樣,一會兒功夫,排隊的旁邊就站了一堆人,等著notify。
終於,師傅進去,然後修好了,接著notify了,接下來呢?
1. 有一個wait()的人(線程)被通知到。
2. 為什麽被通知到的是他而不是另外一個wait()的人?取決於JVM。我們無法預先判斷出哪一個會被通知到。也就是說,優先級高的不一定被優先喚醒,等待時間長的也不一定被優先喚醒,一切不可預知!(當然,如果你了解該JVM的實現,則可以預知)。
3. 他(被通知到的線程)要重新排隊。
4. 他會排在隊伍的第一個位置嗎?回答是:不一定。他會排最後嗎?也不一定。但如果該線程優先級設的比較高,那麽他排在前面的概率就比較大。
5. 輪到他重新進入廁位時,他會從上次wait()的地方接著執行,不會重新執行。惡心點說就是,他會接著拉粑粑,不會重新拉。
6. 如果師傅notifyAll()。則那一堆半途而廢出來的人全部重新排隊,順序不可知。
三、Lock的使用
用synchronized關鍵字可以對資源加鎖。用Lock關鍵字也可以。它是JDK1.5中新增內容。
public class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
(註:這是JavaDoc裏的例子,是一個阻塞隊列的實現例子。所謂阻塞隊列,就是一個隊列如果滿了或者空了,都會導致線程阻塞等待。Java裏的 ArrayBlockingQueue提供了現成的阻塞隊列,不需要自己專門再寫一個了。)
一個對象的lock.lock()和lock.unlock()之間的代碼將會被鎖住。這種方式比起synchronize好在什麽地方?
簡而言之,就是對wait的線程進行了分類。用廁位理論來描述,則是那些蹲了一半而從廁位裏出來等待的人原因可能不一樣,有的是因為馬桶堵了,有的是因為馬桶沒水了。通知(notify)的時候,就可以喊:因為馬桶堵了而等待的過來重新排隊(比如馬桶堵塞問題被解決了),或者喊,因為馬桶沒水而等待的過來重新排隊(比如馬桶沒水問題被解決了)。這樣可以控制得更精細一些。不像synchronize裏的wait和notify,不管是馬桶堵塞還是馬桶沒水都只能喊:剛才等待的過來排隊!假如排隊的人進來一看,發現原來只是馬桶堵塞問題解決了,而自己渴望解決的問題(馬桶沒水)還沒解決,只好再回去等待(wait),白進來轉一圈,浪費時間與資源。
Lock與synchronized對應關系:
synchronized | wait | notify | notifyAll |
Lock | await | signal | signalAll |
註意:不要在Lock方式鎖住的塊裏調用wait、notify、notifyAll。
四、volatile的使用
volatile真正解決的問題是 JVM 在-server模式下(註意普通運行模式下沒有此問題),線程優先取用自己的線程私有stack中的變量值,而不是公共堆中的值,造成變量值老舊的問題。換句話說,volatile強制要求了所有線程在使用volatile修飾的變量的時候要去公共內存堆中獲取值,不可以偷懶使用自己的。volatile絕對不保證原子性,原子性只能用synchronized同步修飾符實現。
我們知道,在Java中設置變量值的操作,除了long和double類型的變量外都是原子操作,也就是說,對於變量值的簡單讀寫操作沒有必要進行同步。這在JVM 1.2之前,Java的內存模型實現總是從主存讀取變量,是不需要進行特別的註意的。而隨著JVM的成熟和優化,現在在多線程環境下volatile關鍵字的使用變得非常重要。
在當前的Java內存模型下,線程可以把變量保存在本地內存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致。要解決這個問題,只需要把該變量聲明為volatile(不穩定的)即可,這就指示JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。一般說來,多任務環境下各任務間共享的標誌都應該加volatile修飾。
volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。而且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值。
Java語言規範中指出:為了獲得最佳速度,允許線程保存共享成員變量的私有拷貝,而且只當線程進入或者離開同步代碼塊時才與共享成員變量的原始值對比。這樣當多個線程同時與某個對象交互時,就必須要註意到要讓線程及時的得到共享成員變量的變化。而volatile關鍵字就是提示VM:對於這個成員變量不能保存它的私有拷貝,而應直接與共享成員變量交互。
使用建議:在兩個或者更多的線程訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者為常量時,不必使用。由於使用volatile屏蔽掉了VM中必要的代碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。
關於Java中鎖的總結