在進行多線程編程時,最讓人頭痛的無非是線程安全問題,對共享資源的訪問控制,若是稍加不注意就可能致使莫名其名錯誤,主要體現有:
- 建立單例對象時,內存中可能存在多個實例。
- 一個線程正在讀取數據,因爲另外一個寫線程的介入,可能致使讀線程讀取到的數據髒亂不堪。
- 同一對象可能同時被多個線程使用,形成結果上面的誤差
爲了防止多線程形成須要單例化的對象存在多實例問題,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; }
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引用於: 原博客地址html
Case 1 :
在使用synchronized關鍵字的情形下,假如佔有鎖的線程因爲要等待IO或者其餘緣由(好比調用sleep方法)被阻塞了,可是又沒有釋放鎖,那麼其餘線程就只能一直等待,別無他法。這會極大影響程序執行效率。所以,就須要有一種機制能夠不讓等待的線程一直無期限地等待下去(好比只等待必定的時間 (解決方案:tryLock(long time, TimeUnit unit)) 或者 可以響應中斷 (解決方案:lockInterruptibly())),這種狀況能夠經過 Lock 解決。java
Case 2 :
咱們知道,當多個線程讀寫文件時,讀操做和寫操做會發生衝突現象,寫操做和寫操做也會發生衝突現象,可是讀操做和讀操做不會發生衝突現象。可是若是採用synchronized關鍵字實現同步的話,就會致使一個問題,即當多個線程都只是進行讀操做時,也只有一個線程在能夠進行讀操做,其餘線程只能等待鎖的釋放而沒法進行讀操做。所以,須要一種機制來使得當多個線程都只是進行讀操做時,線程之間不會發生衝突。一樣地,Lock也能夠解決這種狀況 (解決方案:ReentrantReadWriteLock) 。編程
Case 3 :
咱們能夠經過Lock得知線程有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized沒法辦到的。安全
前面講的都是在多線程狀況下,共享資源保持一致性,保證對象的惟一性和一致性。可是在某些情境中,同一對象須要在不一樣線程中相互獨立,即每個線程中都擁有該對象的一個副本。(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
semaphore (信號量) 控制線程的出入問題,建立該對象時指明可用的資源數(synchronized可用資源數爲1),當有資源空閒時,線程可進入,不然阻塞等待。項目中彈幕處理,維護彈幕池可用彈幕總數,當顯示的彈幕已經達到彈幕總數,信號量爲0,當某一彈幕移除屏幕,將彈幕控件放入彈幕控件池進行復用,並將信號量加1,定時器定時判斷信號量,當信號量不爲0時,從彈幕控制池取彈幕控件展現。