獨占鎖(寫鎖) / 共享鎖(讀鎖) / 互斥鎖
概念
獨占鎖:指該鎖一次只能被一個線程所持有。對ReentrantLock和Synchronized而言都是獨占鎖
共享鎖:指該鎖可以被多個線程鎖持有
對ReentrantReadWriteLock其讀鎖是共享,其寫鎖是獨占
寫的時候只能一個人寫,但是讀的時候,可以多個人同時讀
為什么會有寫鎖和讀鎖
原來我們使用ReentrantLock創建鎖的時候,是獨占鎖,也就是說一次只能一個線程訪問,但是有一個讀寫分離場景,讀的時候想同時進行,因此原來獨占鎖的並發性就沒這么好了,因為讀鎖並不會造成數據不一致的問題,因此可以多個人共享讀
多個線程 同時讀一個資源類沒有任何問題,所以為了滿足並發量,讀取共享資源應該可以同時進行,但是如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
讀-讀:能共存
讀-寫:不能共存
寫-寫:不能共存
代碼實現
實現一個讀寫緩存的操作,假設開始沒有加鎖的時候,會出現什么情況
/**
* 讀寫鎖
* 多個線程 同時讀一個資源類沒有任何問題,所以為了滿足並發量,讀取共享資源應該可以同時進行
* 但是,如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
*
*/
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* 資源類
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// private Lock lock = null;
/**
* 定義寫操作
* 滿足:原子 + 獨占
* @param key
* @param value
*/
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "\t 正在寫入:" + key);
try {
// 模擬網絡擁堵,延遲0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 寫入完成");
}
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "\t 正在讀取:");
try {
// 模擬網絡擁堵,延遲0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 讀取完成:" + value);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 線程操作資源類,5個線程寫
for (int i = 0; i < 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 線程操作資源類, 5個線程讀
for (int i = 0; i < 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
我們分別創建5個線程寫入緩存
// 線程操作資源類,5個線程寫
for (int i = 0; i < 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
5個線程讀取緩存,
// 線程操作資源類, 5個線程讀
for (int i = 0; i < 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
最后運行結果:
0 正在寫入:0
4 正在寫入:4
3 正在寫入:3
1 正在寫入:1
2 正在寫入:2
0 正在讀取:
1 正在讀取:
2 正在讀取:
3 正在讀取:
4 正在讀取:
2 寫入完成
4 寫入完成
4 讀取完成:null
0 寫入完成
3 讀取完成:null
0 讀取完成:null
1 寫入完成
3 寫入完成
1 讀取完成:null
2 讀取完成:null
我們可以看到,在寫入的時候,寫操作都沒其它線程打斷了,這就造成了,還沒寫完,其它線程又開始寫,這樣就造成數據不一致
解決方法
上面的代碼是沒有加鎖的,這樣就會造成線程在進行寫入操作的時候,被其它線程頻繁打斷,從而不具備原子性,這個時候,我們就需要用到讀寫鎖來解決了
/**
* 創建一個讀寫鎖
* 它是一個讀寫融為一體的鎖,在使用的時候,需要轉換
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
當我們在進行寫操作的時候,就需要轉換成寫鎖
// 創建一個寫鎖
rwLock.writeLock().lock();
// 寫鎖 釋放
rwLock.writeLock().unlock();
當們在進行讀操作的時候,在轉換成讀鎖
// 創建一個讀鎖
rwLock.readLock().lock();
// 讀鎖 釋放
rwLock.readLock().unlock();
這里的讀鎖和寫鎖的區別在於,寫鎖一次只能一個線程進入,執行寫操作,而讀鎖是多個線程能夠同時進入,進行讀取的操作
完整代碼:
/**
* 讀寫鎖
* 多個線程 同時讀一個資源類沒有任何問題,所以為了滿足並發量,讀取共享資源應該可以同時進行
* 但是,如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
*
*/
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 資源類
*/
class MyCache {
/**
* 緩存中的東西,必須保持可見性,因此使用volatile修飾
*/
private volatile Map<String, Object> map = new HashMap<>();
/**
* 創建一個讀寫鎖
* 它是一個讀寫融為一體的鎖,在使用的時候,需要轉換
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
/**
* 定義寫操作
* 滿足:原子 + 獨占
* @param key
* @param value
*/
public void put(String key, Object value) {
// 創建一個寫鎖
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在寫入:" + key);
try {
// 模擬網絡擁堵,延遲0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 寫入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 寫鎖 釋放
rwLock.writeLock().unlock();
}
}
/**
* 獲取
* @param key
*/
public void get(String key) {
// 讀鎖
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在讀取:");
try {
// 模擬網絡擁堵,延遲0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 讀取完成:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 讀鎖釋放
rwLock.readLock().unlock();
}
}
/**
* 清空緩存
*/
public void clean() {
map.clear();
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 線程操作資源類,5個線程寫
for (int i = 1; i <= 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 線程操作資源類, 5個線程讀
for (int i = 1; i <= 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
運行結果:
1 正在寫入:1
1 寫入完成
2 正在寫入:2
2 寫入完成
3 正在寫入:3
3 寫入完成
4 正在寫入:4
4 寫入完成
5 正在寫入:5
5 寫入完成
2 正在讀取:
3 正在讀取:
1 正在讀取:
4 正在讀取:
5 正在讀取:
2 讀取完成:2
1 讀取完成:1
4 讀取完成:4
3 讀取完成:3
5 讀取完成:5
從運行結果我們可以看出,寫入操作是一個一個線程進行執行的,並且中間不會被打斷,而讀操作的時候,是同時5個線程進入,然后並發讀取操作
