java執行緒同步:synchronized關鍵字,Lock介面以及可重

船頭尺發表於2021-09-09

多執行緒環境下,必須考慮執行緒同步的問題,這是因為多個執行緒同時訪問變數或者資源時會有執行緒爭用,比如A執行緒讀取了一個變數,B執行緒也讀取了這個變數,然後他們同時對這個變數做了修改,寫回到記憶體中,由於是同時做修改,就會導致修改的狀態不一致.

用一個實際的例子來說明執行緒同步的必要性:

package cn.outofmemory.locks; public class LockDemo implements Runnable { private int counter = 0; public void run() { int loopTimes = 10000; while (loopTimes > 0) {   counter ++; loopTimes --; } } public static void main(String[] args) throws InterruptedException { LockDemo demo = new LockDemo(); Thread[] threads = new Thread[]{ new Thread(demo), new Thread(demo),new Thread(demo), new Thread(demo),new Thread(demo) }; for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } System.out.println("demo's counter is " + demo.counter); } }

這段程式碼中的LockDemo類實現了Runnable介面,在run方法中對其私有變數counter遞加了10000次。在main方法中我們首先初始化了一個LockDemo物件,然後初始化了5個執行緒,這5個執行緒公用一個LockDemo的例項。 
然後我們一次啟動這5個執行緒,然後透過join等待所有執行緒結束,最後輸出demo例項的counter值來。 

執行程式,我這兒得到這樣一個輸出結果:

demo's counter is 44041

本來5個執行緒每個執行緒遞加10000次,應該得到的結果是50000,而實際的結果是44041. 

如果你也執行此程式有可能會得到不一樣的結果。這取決於這5個執行緒造成了多少次的衝突。從我的輸出結果看,這段程式的5個執行緒造成了大約6000次的記憶體爭用衝突。 
在實際應用中,這是不可用的。

改程式序,避免衝突

我們可以分析一下,這5個執行緒的衝突出現在什麼地方,他們公用了demo物件,同時對demo物件的成員變數counter做遞加,也就是說衝突出現在對counter遞加這一步上。 
我們在這一步操作上加上synchronzied關鍵字,讓5個執行緒執行到對counter++這步程式碼時單獨執行,應該就可以解決問題了。 

修改後的run方法程式碼:

 public void run() { int loopTimes = 10000; while (loopTimes > 0) { synchronized (this) { counter ++; } loopTimes --; } }

我們再次執行程式會得到如下確定的輸出結果:

demo's counter is 50000

這次得到的結果是符合我們的預期的,我們透過synchronized關鍵字解決了問題。

synchronized關鍵字是jvm虛擬機器的關鍵字,在java.util.concurrent.locks名稱空間中還有一個Lock介面,和Lock介面的實現類ReentrantLock(可重入鎖)。 ReentrantLock可以實現和synchronized關鍵字相同的功能,而且更為靈活,在極端的情況下效能會更好一些。

我們看下使用可重入鎖ReentrantLock解決執行緒同步的方法:

 private final Lock lock = new ReentrantLock(); public void run() { int loopTimes = 10000; while (loopTimes > 0) { try { lock.lock(); counter ++; } finally { lock.unlock(); } loopTimes --; } }


我們在LockDemo中新增了一個final的成員變數lock,它是一個ReentrantLock的例項。 在run方法中,在counter++這行程式碼兩邊加上了try .. finally ..語句, 
當執行緒執行到try塊之後,首先透過lock.lock()獲得鎖,獲得鎖之後再執行counter++,最後在finally語句塊中透過lock的unlock方法釋放鎖。 

我們可以執行修改後的程式碼,輸出如下:

demo's counter is 50000

輸出結果符合邏輯預期。

synchronized獲得的內部鎖存在一定的侷限

1. 不能中斷一個正在試圖獲得鎖的執行緒 

2. 試圖獲得鎖時不能像trylock那樣設定超時時間 

3. 每個鎖只有單一的條件,不像condition那樣可以設定多個

synchronzied關鍵字和可重入鎖ReentrantLock選擇的最佳實踐:

1. 如果synchronized關鍵字適合程式,儘量使用它,可以減少程式碼出錯的機率和程式碼數量 
2. 如果特別需要Lock/Condition結構提供的獨有特性時,才使用他們 
3. 許多情況下可以使用java.util.concurrent包中的一種機制,它會為你處理所有的加鎖情況

原文連結:http://outofmemory.cn/java/java.util.concurrent/synchronized-locks-Lock-ReentrantLock

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3016/viewspace-2811197/,如需轉載,請註明出處,否則將追究法律責任。

相關文章