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() : 釋放一個許可,將其返回給訊號量。