【併發程式設計】4.JUC中常用的鎖
阿新 • • 發佈:2020-07-14
JUC及java.util.concurrent的簡稱,在這個包中增加了在併發程式設計中很常用的工具類,用於定義類似於執行緒的自定義子系統,包括執行緒池,非同步 IO 和輕量級任務框架,還提供了設計用於多執行緒上下文中。通過她們能夠很好地幫助我們在開發中提高一些程式的效能。
1.Lock與Condition
- condition
Lock與condition是Java中管程模型除了synchronized的另一套實現,不同是的支援多條件佇列。Lock&Condition 實現的管程裡只能使用前面的 await()、signal()、signalAll()。
synchronized的管程模型
Lock&Condition的管程模型
Condition的使用 需要獲取到鎖
public class BlockedQueue<T>{ final Lock lock = new ReentrantLock(); // 條件變數:佇列不滿 final Condition notFull = lock.newCondition(); // 條件變數:佇列不空 final Condition notEmpty = lock.newCondition(); // 入隊 void enq(T x) { lock.lock(); try { while (佇列已滿){ // 等待佇列不滿 notFull.await(); } // 省略入隊操作... // 入隊後, 通知可出隊 notEmpty.signal(); }finally { lock.unlock(); } } // 出隊 void deq(){ lock.lock(); try { while (佇列已空){ // 等待佇列不空 notEmpty.await(); } // 省略出隊操作... // 出隊後,通知可入隊 notFull.signal(); }finally { lock.unlock(); } } }
上述程式碼中就是有兩個條件變數,對應兩個條件佇列,分別進行入隊和出隊。 佇列已滿通知等待出隊操作的執行緒,佇列已空則通知等待入隊操作的執行緒。
如果是使用synchronized關鍵字實現的話呼叫notify/notifyAll方法應該是通知唯一的等待佇列裡的所有執行緒,然後判斷是執行入隊或者是出隊。
- ReentrantLock
上述程式碼中使用的鎖就是 ReentrantLock 即為可重入鎖,就是再已經獲取鎖的情況下可以再次獲取到同一把鎖。
class X { private final Lock rtl = new ReentrantLock(); int value; public int get() { // 獲取鎖 rtl.lock(); try { return value; } finally { // 保證鎖能釋放 rtl.unlock(); } } public void addOne() { // 獲取鎖 rtl.lock(); try { value = 1 + get(); //get方法再次獲取鎖 } finally { // 保證鎖能釋放 rtl.unlock(); } } }
- ReadWriteLock
讀寫鎖,適用於讀多寫少的場景,例如快取
ReadWriteLock 是介面
ReentrantReadWriteLock 是實現類
- 允許多個執行緒同時讀共享變數; (優於互斥鎖的關鍵) 如果有執行緒正在讀,寫執行緒需要等待讀執行緒釋放鎖後才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖。
- 只允許一個執行緒寫共享變數;
- 如果一個寫執行緒正在執行寫操作,此時禁止讀執行緒讀共享變數 (區別於對讀操作不加鎖)
/**
* className: Cache
* create by: zhujun
* description: 使用讀寫鎖實現的快取工具類
* 讀寫鎖 使用於讀多寫少的併發場景
* create time: 2019/7/23 11:21
*/
public class Cache<K,V> {
//hashmap 儲存資料
final HashMap<K,V> hashMap = new HashMap<>();
final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
final Lock readLock = reentrantReadWriteLock.readLock();
final Lock writeLock = reentrantReadWriteLock.writeLock();
/**
* 存入資料
* @param key
* @param value
*/
void set(K key,V value){
writeLock.lock();
try{
hashMap.put(key,value);
}finally {
writeLock.unlock();
}
}
/**
* 讀取資料
* @param key
* @return
*/
V get(K key){
//讀鎖
readLock.lock();
try{
return hashMap.get(key);
}finally {
readLock.unlock();
}
}
}
注意點:1.寫鎖支援條件變數,讀鎖不支援條件變數
2.支援鎖的降級,不支援鎖的升級
鎖的升級
read.lock();
try {
v = m.get(key);//驗證值是否存在
//獲取寫鎖從資料庫中更新快取 //鎖的升級
} finally{
read.unlock();
}
鎖的降級
writeLock.lock();
try{
....
readLock.lock();//釋放寫鎖之前 釋放讀鎖
}finally{
writeLock.unlock();
readLock.lock()
}
- StampedLock
Java 1.8提供,效能優於ReadWriteLock 支援三種鎖模式:寫鎖,樂觀讀鎖,悲觀讀鎖。
因為 ReadWriteLock 是悲觀讀鎖,讀取的時候不允許寫入,StampedLock為了提高效能提供了樂觀讀鎖,讀的過程中大概率不會有寫入。
首先我們通過tryOptimisticRead()獲取一個樂觀讀鎖,並返回版本號。
接著進行讀取,讀取完成後,我們通過validate()去驗證版本號,如果在讀取過程中沒有寫入,版本號不變,驗證成功,我們就可以放心地繼續後續操作。
如果在讀取過程中有寫入,版本號會發生變化,驗證將失敗。在失敗的時候,我們再通過獲取悲觀讀鎖再次讀取。
/**
* className: Point
* create by: zhujun
* description:StampLock 的樂觀讀與悲觀讀鎖
* create time: 2019/7/30 17:01
*/
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
final StampedLock s1 = new StampedLock();
//計算帶到原點的舉例
double getDistance() throws InterruptedException {
long stamp = s1.tryOptimisticRead();//樂觀讀
System.out.println("樂觀讀stamp:"+stamp);
int curX = x;
int cutY = y;
System.out.println("讀取成功:"+x+","+y);
Thread.sleep(1000);//睡眠 方便測試時進行寫操作
if(!s1.validate(stamp)){//通過驗證stamp
//期間有寫操作 升級為悲觀讀鎖 等待寫操作完成
stamp = s1.readLock();
try{
System.out.println("存在寫操作,重新讀取x,y座標");
System.out.println("此時stamp:"+stamp);
curX = x;
cutY = y;
}finally {
s1.unlockRead(stamp);
}
}
return Math.sqrt(curX*curX+cutY*cutY);
}
void reLocation(int x,int y){
//寫操作 寫鎖
long stamp = s1.writeLock();
System.out.println("寫鎖stamp:"+stamp);
try{
this.x = x;
this.y = y;
System.out.println("重定位成功:"+x+","+y);
}finally {
s1.unlockWrite(stamp);
}
}
}
public class PointTest {
public static void main(String[] args) throws InterruptedException {
Point p = new Point(1,2);
//執行緒1 計算距離
Thread th1 = new Thread(new Runnable() {
@Override
public void run() {
try {
double distance = p.getInstance();
System.out.println("距離原點的舉例:"+distance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//執行緒2 重寫座標
Thread th2 = new Thread(new Runnable() {
@Override
public void run() {
p.reLocation(3,4);
}
});
th1.start();
th2.start();
}
}
注意:1.StampedLock 不支援可重入
2.如果需要支援中斷功能,一定使用可中斷的悲觀讀鎖 readLockInterruptibly()和寫鎖 writeLockInterruptibly()。