淺談synchronized、Lock、ThreadLocal和semaphore - 格式化版本
1. 背景
在進行多線程編程時,最讓人頭痛的無非是線程安全問題,對共享資源的訪問控制,如果稍加不注意就可能導致莫名其名錯誤,主要體現有:
- 創建單例對象時,內存中可能存在多個實例。
- 一個線程正在讀取數據,由於另一個寫線程的介入,可能導致讀線程讀取到的數據臟亂不堪。
- 同一對象可能同時被多個線程使用,造成結果上面的偏差
2. synchronized 的介紹
為了防止多線程造成需要單例化的對象存在多實例問題,synchronized作為懶漢式模式創建實例的常使用的關鍵字,使用如下:
private SocketManager() {
}
private static SocketManager INSTANCE;
public static SocketManager getInstance() {
if (INSTANCE == null) {
synchronized (SocketManager.class) {
if (INSTANCE == null) {
INSTANCE = new SocketManager();
}
}
}
return INSTANCE;
}
3. Lock的介紹
Lock是java中鎖操作接口,比synchronized使用上面更為靈活。其主要實現類分為ReentrantLock (重入鎖)和ReentrantReadWriteLock(讀寫鎖)。其中ReentrantLock(重入鎖)構造時,由於布爾參數不同又分為公平重入鎖和非公平重入鎖,其中非公平的重入鎖處理效率比公平重入鎖高,所以在創建時,一般使用ReentrantLock(false)。 另一個ReentrantReadWriteLock專門用於對讀寫操作的加鎖(兩個讀線程不會沖突,兩個寫線程會沖突,一個讀一個寫線程會沖突,但是兩個讀線程不會沖突),如果ReentrantLock處理能力就不夠,再這個情況下使用ReentrantLock。總之,一般情況下,ReentrantLock基本就能處理問題,在讀寫上就可以選擇使用ReentrantLock處理。
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
//HashMap 非線程安全
public static HashMap<Integer, String> pairs = new HashMap<>();
public static void setPair(int key, String value) {
reentrantReadWriteLock.writeLock().lock();
pairs.put(key, value);
reentrantReadWriteLock.writeLock().unlock();
}
public static String getValue(int key) {
reentrantReadWriteLock.readLock().lock();
String value = pairs.get(key);
reentrantReadWriteLock.readLock().unlock();
return value;
}
- 以下case引用於: 原博客地址
Case 1 :
在使用synchronized關鍵字的情形下,假如占有鎖的線程由於要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,那么其他線程就只能一直等待,別無他法。這會極大影響程序執行效率。因此,就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間 (解決方案:tryLock(long time, TimeUnit unit)) 或者 能夠響應中斷 (解決方案:lockInterruptibly())),這種情況可以通過 Lock 解決。
Case 2 :
我們知道,當多個線程讀寫文件時,讀操作和寫操作會發生沖突現象,寫操作和寫操作也會發生沖突現象,但是讀操作和讀操作不會發生沖突現象。但是如果采用synchronized關鍵字實現同步的話,就會導致一個問題,即當多個線程都只是進行讀操作時,也只有一個線程在可以進行讀操作,其他線程只能等待鎖的釋放而無法進行讀操作。因此,需要一種機制來使得當多個線程都只是進行讀操作時,線程之間不會發生沖突。同樣地,Lock也可以解決這種情況 (解決方案:ReentrantReadWriteLock) 。
Case 3 :
我們可以通過Lock得知線程有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized無法辦到的。
4. ThreadLocal的介紹
前面講的都是在多線程情況下,共享資源保持一致性,保證對象的唯一性和一致性。但是在某些情境中,同一對象需要在不同線程中相互獨立,即每一個線程中都擁有該對象的一個副本。(PS: SimpleDateForma非線程安全)
// 測試代碼
public class Main {
public static void main(String... args) {
for (int i = 0; i < 5; i++) {
new Thread() {
@Override
public void run() {
CountUtils.addCount();
}
}.start();
}
}
}
// 沒有使用ThreadLocal
public class CountUtils {
private static int countNum = 0;
public static void addCount() {
synchronized (CountUtils.class) {
countNum++;
System.out.println(Thread.currentThread().getName() + ":" + countNum);
}
}
}
// 輸出結果:
Thread-1:1
Thread-3:2
Thread-2:3
Thread-0:4
Thread-4:5
- 靜態字段位於全局區,同時能夠被多個線程修改。
public class CountUtils {
private static ThreadLocal<Integer> integerThreadLocal = new InheritableThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void addCount() {
synchronized (CountUtils.class) {
int countNum = integerThreadLocal.get();
countNum ++ ;
System.out.println(Thread.currentThread().getName() + ":" + countNum);
}
}
}
// 輸出結果:
Thread-2:1
Thread-1:1
Thread-3:1
Thread-0:1
Thread-4:1
- 總結: ThreadLocal采用Map<ThreadInfo,E>方式將線程操作的對象進行區分,不同的線程取值並非同一個。
5. semaphore的介紹
semaphore (信號量) 控制線程的出入問題,創建該對象時指明可用的資源數(synchronized可用資源數為1),當有資源空閑時,線程可進入,否則阻塞等待。項目中彈幕處理,維護彈幕池可用彈幕總數,當顯示的彈幕已經達到彈幕總數,信號量為0,當某一彈幕移除屏幕,將彈幕控件放入彈幕控件池進行復用,並將信號量加1,定時器定時判斷信號量,當信號量不為0時,從彈幕控制池取彈幕控件展示。
- tryAcquire() : 僅在調用時此信號量存在一個可用許可,才從信號量獲取許可。
- acquire() : 從此信號量獲取一個許可,在提供一個許可前一直將線程阻塞,否則線程被中斷。
- release() : 釋放一個許可,將其返回給信號量。