一、前言
多線程怎么防止競爭資源,即防止對同一資源進行並發操作,那就是使用加鎖機制。這是Java並發編程中必須要理解的一個知識點。其實使用起來還是比較簡單,但是一定要理解。
有幾個概念一定要牢記:
- 加鎖必須要有鎖
- 執行完后必須要釋放鎖
- 同一時間、同一個鎖,只能有一個線程執行
二、synchronized
synchronized的特點是自動釋放鎖,作用在方法時自動獲取鎖,任意對象都可做為鎖,它是最常用的加鎖機制,鎖定幾行代碼,如下:
//--------同步方法1 public synchronized void test(){ //一段代碼 } //--------同步方法2 private Object lock=new Object(); public void test2(){ synchronized(lock){ } }
2.1 synchronized獲取的鎖
synchronized可以手動指定鎖,當作用在方法時會自動獲取鎖:
- 作用於普通方法獲得當前對象鎖,等價於synchronized(this)
- 作用於靜態方法獲得類鎖,等價於synchronized(類.class)
三、Lock
Lock的特點是,必須自己創建鎖(鎖類型已經指定為Lock的實現類,不能使用其它對象),必須自己釋放鎖。代碼結構如下:
Lock l = ...; l.lock(); try { // 執行代碼 } finally { l.unlock(); }
注意一定要在finally中釋放鎖,保證即便拋出異常也可以釋放。
3.1 ReentrantLock詳解
這是一個Lock的一個實例。
3.1.1 構造方法
ReentrantLock(可重入鎖),只有一個屬性即是否公平。公平的含義是當有多個線程競爭鎖時,按先來后到獲得鎖,但使用公平策略時,對效率有一定的影響。
- ReentrantLock() :最常用,獲取一個不公平的鎖,
- ReentrantLock(boolean fair):獲取指定公平策略的鎖。
3.1.2 方法摘要
加鎖與解鎖:
- void lock() :獲取鎖,這是最常用的方法。
- void unlock() :釋放鎖,必須要的方法,使用完一定要釋放鎖。
- boolean tryLock() :僅在調用時鎖未被另一個線程保持的情況下,才獲取該鎖。會破壞公平性原則。
- boolean tryLock(long timeout, TimeUnit unit) :如果鎖在給定等待時間內沒有被另一個線程保持,且當前線程未被中斷,則獲取該鎖。
- void lockInterruptibly() :如果當前線程未被中斷,則獲取鎖。
查詢當前鎖的相關狀態:
- boolean isLocked() :查詢此鎖是否由任意線程保持。
- boolean isHeldByCurrentThread() :查詢當前線程是否保持此鎖。
- boolean isFair() :如果此鎖的公平設置為 true,則返回 true。
- int getHoldCount() :查詢當前線程保持此鎖的次數。
- int getQueueLength() :返回正等待獲取此鎖的線程估計數。
- boolean hasQueuedThread(Thread thread) :查詢給定線程是否正在等待獲取此鎖。
- boolean hasQueuedThreads() :查詢是否有些線程正在等待獲取此鎖。
Condition相關(見第五章):
- Condition newCondition() :返回用來與此 Lock 實例一起使用的 Condition 實例。
- int getWaitQueueLength(Condition condition):返回等待與此鎖相關的給定條件的線程估計數。
四、ReadWriteLock
當有一種情況,一個類中有多個方法需要同步,其中有讀有寫,如果所有的方法都使用同步,雖然可以保證數據的准確性,但當讀取次數遠大於寫入次數的時候,同步就會對性能產生較大的影響。這時候,就有一種同步策略,讀操作和讀操作不互斥,讀操作和寫操作互斥,寫操作和寫操作互斥,這樣可以提供性能。
雖然解釋的很通俗但是使用它們還是要考慮以下情況(全部來自jdk api):
- 在 writer 釋放寫入鎖時,reader 和 writer 都處於等待狀態,在這時要確定是授予讀取鎖還是授予寫入鎖。Writer 優先比較普遍,因為預期寫入所需的時間較短並且不那么頻繁。Reader 優先不太普遍,因為如果 reader 正如預期的那樣頻繁和持久,那么它將導致對於寫入操作來說較長的時延。公平或者“按次序”實現也是有可能的。
- 在 reader 處於活動狀態而 writer 處於等待狀態時,確定是否向請求讀取鎖的 reader 授予讀取鎖。Reader 優先會無限期地延遲 writer,而 writer 優先會減少可能的並發。
- 確定是否重新進入鎖:可以使用帶有寫入鎖的線程重新獲取它嗎?可以在保持寫入鎖的同時獲取讀取鎖嗎?可以重新進入寫入鎖本身嗎?
- 可以將寫入鎖在不允許其他 writer 干涉的情況下降級為讀取鎖嗎?可以優先於其他等待的 reader 或 writer 將讀取鎖升級為寫入鎖嗎?
4.1 創建與獲取鎖
創建ReentrantReadWriteLock:
- ReentrantReadWriteLock():創建默認非公平的鎖。
- ReentrantReadWriteLock(boolean fair):創建指定公平策略的鎖。
獲得讀或者寫鎖:
- readLock() :返回用於讀取操作的鎖。
- writeLock() :返回用於寫入操作的鎖。
4.2 其他方法
其它方法不怎么常用,若有具體需求可以查看API文檔。
4.3 示例
下面給一個簡單的例子,一個並發訪問的map:
class RWDictionary { private final Map<String, Data> m = new TreeMap<String, Data>(); private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); private final Lock w = rwl.writeLock(); public Data get(String key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } public String[] allKeys() { r.lock(); try { return m.keySet().toArray(); } finally { r.unlock(); } } public Data put(String key, Data value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } } public void clear() { w.lock(); try { m.clear(); } finally { w.unlock(); } } }
但是還是不建議這么用,因為已經有ConcurrentHashMap了。
4.4 ReentrantReadWriteLock理解
4.4.1 鎖順序是否可以按讀鎖或者寫鎖來優先指定
不可以,要么是隨機的,要么是按照公平策略,優先安排等待時間最長的線程獲取它想要的鎖。
4.4.2 什么是鎖重入
允許 reader 和 writer 按照 ReentrantLock 的樣式重新獲取讀取鎖或寫入鎖。在寫入線程保持的所有寫入鎖都已經釋放后,才允許重入 reader 使用它們。
此外,writer 可以獲取讀取鎖,但反過來則不成立。在其他應用程序中,當在調用或回調那些在讀取鎖狀態下執行讀取操作的方法期間保持寫入鎖時,重入很有用。如果 reader 試圖獲取寫入鎖,那么將永遠不會獲得成功。
4.4.3 什么是鎖降級
重入還允許從寫入鎖降級為讀取鎖,其實現方式是:先獲取寫入鎖,然后獲取讀取鎖,最后釋放寫入鎖。但是,從讀取鎖升級到寫入鎖是不可能的。
五、volatile
我覺得這個關鍵詞水比較深,輕易不要把它用在同步上,volatile的中文意思是不穩定的。先找個JDK源碼中的例子看一下(jdk1.8大約有130個類使用了volatile),Thread類中有:
private volatile Interruptible blocker;
這是線程的與中斷有關的變量,當一個線程獲得它需要中斷時會立即拋出異常。下面是HashMap里面的一個變量:
transient volatile int modCount;
這個用來變量是一個計數器,用在當迭代時若對容器修改,便拋出異常的一個操作。
5.1 小總結
在什么情況下使用volatile:當一個變量需要做為一個信號,具有各種狀態,改變狀態將會引發一種操作的時候,就用volatile。
簡單解釋一下,當線程讀取一個變量時,會對變量進行緩存,所以若對一種信號的變化比較敏感需要使用volatile,那就不能使用緩存,每次都需要讀取實際的值。最后說一遍企圖對volatile變量進行並發的i++,這樣沒有什么意義。
六、Condition
這是由新增Lock類而同時增加的類,畢竟對象的wait和notify方法要在synchronized語句塊中,既然現在用Lock了當然要新增一種新的等待喚醒機制了,JDK API已經說得很清楚了:
Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現組合使用,為每個對象提供多個等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。
而示例已經足夠說明用法了,所以java的api文檔是最好的參考資料:
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(); } } }
這個就是最基本的用法。
6.1 構造方法
沒有具體的構造方法,通過Lock實現對象來獲取Condition對象,Lock有下面的方法:newCondition() :返回用來與此 Lock 實例一起使用的 Condition 實例。
6.2 普通方法
Condition的方法和對象的等待喚醒類似:
- void await() :造成當前線程在接到信號或被中斷之前一直處於等待狀態。
- boolean await(long time, TimeUnit unit) :造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。
- long awaitNanos(long nanosTimeout) :造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。
- void awaitUninterruptibly() :造成當前線程在接到信號之前一直處於等待狀態。
- boolean awaitUntil(Date deadline) :造成當前線程在接到信號、被中斷或到達指定最后期限之前一直處於等待狀態。
- void signal() :喚醒一個等待線程。
- void signalAll() :喚醒所有等待線程。
等待變成了await方法,喚醒變成了signal方法。