淺談synchronized、Lock、ThreadLocal和semaphore

1004145468發表於2018-09-01

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

相關文章