JAVA并發編程與高并發解決方案 - 并發編程 三 之 線程安全策略

JAVA并發編程與高并發解決方案 - 并發編程 三

版本 作者 內容
2018.5.17 chuIllusions 線程安全策略

相關文章

JAVA并發編程與高并發解決方案 - 并發編程 一 之 并發相關知識
JAVA并發編程與高并發解決方案 - 并發編程 二 之 線程安全性、安全發布對象
JAVA并發編程與高并發解決方案 - 并發編程 四 之 J.U.C之AQS
JAVA并發編程與高并發解決方案 - 并發編程 五 之 J.U.C組件拓展
JAVA并發編程與高并發解決方案 - 并發編程 六 之 線程池

線程安全策略

? 創建后狀態不能被修改的對象叫作不可變對象。不可變對象天生就是線程安全的。它們的常量(變量)是在構造函數中創建的,既然它們的狀態無法被修改,那么這些常量永遠不會被改變——不可變對象永遠是線程安全的。

不可變對象需要滿足的條件

  • 對象創建以后其狀態就不能修改
  • 對象所有域都是final類型
  • 對象是正確創建的(在對象創建期間,this引用沒有逸出)

不可變對象

final

? final關鍵字:類、方法、變量

  • 修飾類:不能被繼承,final類中的成員屬性可以根據需要設置為final,但final類中所有的成員方法都被隱式指定為final方法。一般不建議將類設置為final類型。可以參考String類。
  • 修飾方法:1)鎖定方法不被繼承類修改;2)效率
  • 修飾變量:1)基本數據類型變量,初始化后便不能進行修改;2)引用類型變量,初始化之后不能再指向別的引用
@Slf4j
@NotThreadSafe
public class ImmutableExample1 {

    private final static Integer a = 1;
    private final static String b = "2";
    //引用類型不允許引用指向改變,但是對象值還是可以進行修改的  
    private final static Map<Integer, Integer> map = Maps.newHashMap();

    static {
        map.put(1, 2);
        map.put(3, 4);
        map.put(5, 6);
    }

    public static void main(String[] args) {
//        a = 2;              //編譯時報錯
//        b = "3";            //編譯時報錯
//        map = Maps.newHashMap();   //編譯時報錯
        map.put(1, 3);       //容易引發線程安全問題
        log.info("{}", map.get(1));
    }

    //可以修飾參數
    private void test(final int a) {
//        a = 1;
    }
}
Collections

? java提供Collections工具類,在類中提供了多種不允許修改的方法

? Collections.unmodifiableXXX:Collection、List、Set、Map...

@Slf4j
@ThreadSafe
public class ImmutableExample2 {

    private static Map<Integer, Integer> map = Maps.newHashMap();

    static {
        map.put(1, 2);
        map.put(3, 4);
        map.put(5, 6);
        //處理過后的map是不可以再進行修改的
        map = Collections.unmodifiableMap(map);
    }

    public static void main(String[] args) {
        //允許操作,但是操作會報錯,扔出異常
        map.put(1, 3);
        log.info("{}", map.get(1));
    }

}
public class Collections {
    public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
        return new UnmodifiableMap<>(m);
    }
    private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
        @Override
        public boolean remove(Object key, Object value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean replace(K key, V oldValue, V newValue) {
            throw new UnsupportedOperationException();
        }
    }
}
Guava

? 谷歌的Guava提供類似Java中的Collections

? ImmutableXXX:Collection、List、Set、Map...

pom.xml

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>23.0</version>
</dependency>
@ThreadSafe
public class ImmutableExample3 {

    private final static ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);
    
    private final static List<Integer> lists = ImmutableList.of(1, 2, 3);

    private final static ImmutableSet set = ImmutableSet.copyOf(list);

    private final static ImmutableMap<Integer, Integer> map = ImmutableMap.of(1, 2, 3, 4);

    private final static ImmutableMap<Integer, Integer> map2 = ImmutableMap.<Integer, Integer>builder()
            .put(1, 2).put(3, 4).put(5, 6).build();

    public static void main(String[] args) {
        System.out.println(map2.get(3));
    }
}

? 介紹了不可變對象,通過在某些情況下,將不能被修改的類對象,設置為不可變對象,來讓對象在多個線程間是線程安全的。歸根到底,其實是躲避開了并發的問題。除了不可變對象,還存在一個方法 就是線程封閉

線程封閉

? 把對象封裝到一個線程里,只有這一個線程能看到該對象,那么就算這個對象不是線程安全的,也不會出現任何線程安全的問題,因為它只能在一個線程中被訪問,如何實現線程封閉:

  • Ad-hoc 線程封閉:程序控制實現,非常脆弱、最糟糕,忽略
  • 堆棧封閉:簡單的說就是局部變量,無并發問題。多個線程訪問同一個方式的時候,方法中的局部變量都會被拷貝一份到線程棧中,方法的局部變量是不被多個線程共享的,因此不會出現線程安全問題,能用局部變量就不推薦使用全局變量,全局變量容易引起并發問題,注意,全局的變量而不是全局的常量。
  • ThreadLocal 線程封閉:特別好的封閉方法
ThreadLocal
/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */
public class ThreadLocal<T> {}

? 從類描述上:ThreadLocal提供線程級別的變量.這些變量不同于它們正常下的變量副本,在每一個線程中都有它自己獲取方式(通過它的get和set方法),不依賴變量副本的初始化。它的實例通常都是私有的靜態的,用于關聯線程的上下文。

? 這些變量在多線程環境下訪問(通過get或set方法訪問)時能保證各個線程里的變量相對獨立于其他線程內的變量

? 總結:ThreadLocal的作用是提供線程內部的局部變量,這種變量只存在線程的生命周期。

聲明方式:private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>;

類分析

? ThreadLocal涉及到的類結構:

(C)ThreadLocal
    -> (C)ThreadLocalMap
        -> (C)Entry
(C)Thread
    -> (f)ThreadLocal.ThreadLocalMap

Thread.java

public class Thread implements Runnable {
    /* 
     * ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. 
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

? 其中ThreadLocalMap類的定義是在ThreadLocal類中,真正的引用卻是在Thread類中。同時,ThreadLocalMap中用于存儲數據的entry定義:

ThreadLocal.java

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
           //key為ThreadLocal對象,value為存儲的值
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

ThreadLocalMap的keyThreadLocal類的實例對象,value為用戶的值

public class ThreadLocal<T> {
    //設置值的方法
    public void set(T value) {
        //1.獲取當前線程
        Thread t = Thread.currentThread();
        //2.從線程中獲取該線程的成員屬性 ThreadLocal.ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //將值放入Map中
        if (map != null)
            map.set(this, value);
        else
            //先創建,在設置值
            createMap(t, value);
    }
    
    //獲取值的方法
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        
        //如果沒有設置,會調用,設置一個value為null
        return setInitialValue();
    }
}
工作原理

? 從上面的源碼分析,我們可以得出ThreadLocal的工作原理如下

  • 聲明全局的ThreadLocal變量,private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>;

  • 每個線程中都有屬于自己的ThreadLocalMap,互不干擾

  • 全局只有一個threadLocal,當通過set填充數據時,通過獲取當前操作線程的threadLocalMap,將threadLocal作為threadLocalMap中的key,需要填充的值作為value

  • 當需要從threadLocal獲取值時,通過獲取當前操作線程的threadLocalMap,并返回keythreadLocal對象的value

      那么就可以理解為:`ThreadLocal`的活動范圍是具體的某一個線程,并且是該線程獨有的。它不是用來解決共享變量的多線程安全問題。
    
      但是,有一點需要說明的是,如果`ThreadLocal`通過`set`方法放進去的值,這個值是共享對象,那么還是會存在線程安全問題。
    
多個 ThreadLocal
public class ThreadLocal<T> {
    
    //用于唯一確認一個ThreadLocal對象
    private final int threadLocalHashCode = nextHashCode();
    
    private static AtomicInteger nextHashCode = new AtomicInteger();
    
    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
}

引用于徹底理解ThreadLocal

如何保證兩個同時實例化的ThreadLocal對象有不同的threadLocalHashCode屬性:在ThreadLocal類中,還包含了一個static修飾的AtomicInteger([??t?m?k]提供原子操作的Integer類)成員變量(即類變量)和一個static final 修飾的常量(作為兩個相鄰nextHashCode的差值)。由于nextHashCode是類變量,所以每一次調用ThreadLocal類都可以保證nextHashCode被更新到新的值,并且下一次調用ThreadLocal類這個被更新的值仍然可用,同時AtomicInteger保證了nextHashCode自增的原子性。

? ThreadLocal中的ThreadLocalMap中的keyThreadLocal對象,由于每個實例化的ThreadLocal對象都是不相同的,所以不會存在key沖突,所以一個線程存在多個ThreadLocal對象作為key是完全沒有問題的。也就是說,一個線程中的ThreadLocalMap可以存在多個key

? 為什么使用ThreadLocal作為ThreadLocalMapkey? 上面的解析已經很明確了。

? 試試使用線程id作為ThreadLocalMapkey? 如果使用線程id作為key,如果存在兩個ThreadLocal對象,一個存放String類型,另一個存放Integer類型,而在單個線程中只存在一個ThreadLocalMap,當存放數據時,key永遠只會有一個(線程id),存入數據的時候先存會被后存覆蓋,獲取數據時候可能會發生錯誤。

應用場景

? ThreadLocal中存放的變量只在線程的生命周期內起作用,應用場景只要有兩個方面:

  1. 提供一個線程內公共變量(比如本次請求的用戶信息、實體參數),減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的復雜度
  2. 為線程提供一個私有的變量副本,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。
關于內存泄露

? 首先,得分析一下內存泄露是什么東西,Java內存泄露又是怎么定義的?

內存泄漏(Memory Leak)是指程序中己動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。

? 在Java程序中,我們通常使用new為對象分配內存,而這些內存空間都在堆(Heap)上。

? JAVA內存的分配是由程序完成的,而內存的釋放是由GC完成。在JAVA達到內存泄露的存在兩個特點,滿足以下兩個條件,即可認為是JAVA內存泄露,這些對象不被GC管理、回收,占用內存。

  1. 對象是可達的,即對象引用存在
  2. 對象無用的,即對象已經不再使用

當達到內存泄露時,扔出的異常:java.lang.OutOfMemoryError:Java heap space

ThreadLocal對象之間的引用關系圖

image

下面引用知乎的一篇文章(ThreadLocal和synchronized的區別?)進行說明:

ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用引用他,那么系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:ThreadLocal Ref -> Thread -> ThreadLocalMap -> Entry -> value永遠無法回收,造成內存泄露。

分析ThreadLocalMap中的源碼

private Entry[] table;
/**
 * 根據ThreadLocal對象獲取Entry
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
private Entry getEntry(ThreadLocal<?> key) {
     //計算索引位置
     int i = key.threadLocalHashCode & (table.length - 1);
     Entry e = table[i];
     if (e != null && e.get() == key)
          return e;
     else
         //沒有找到相應的entry
          return getEntryAfterMiss(key, i, e);
}

/**
 * 
 * Version of getEntry method for use when key is not found in
 * its direct hash slot.
 *
 * @param  key the thread local object
 * @param  i the table index for key's hash code
 * @param  e the entry at table[i] 可能為null或者不為null
 * @return the entry associated with key, or null if no such
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        //獲取ThreadLocal對象
        ThreadLocal<?> k = e.get();
        //如果e為null或者key不一致則向下一個位置查詢
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
private int expungeStaleEntry(int staleSlot) {
    //如果key值為null,則擦除該位置的Entry,否則繼續向下一個位置查詢
    Entry[] tab = table;
    int len = tab.length;
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

在這個過程中遇到的key為null的Entry都會被擦除,那么Entry內的value也就沒有強引用鏈,自然會被回收。仔細研究代碼可以發現,set操作也有類似的思想,將key為null的這些Entry都刪除,防止內存泄露。 但是光這樣還是不夠的,上面的設計思路依賴一個前提條件:要調用ThreadLocalMap的genEntry函數或者set函數。這當然是不可能任何情況都成立的,所以很多情況下需要使用者手動調用ThreadLocal的remove函數,手動刪除不再需要的ThreadLocal,防止內存泄露。所以JDK建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命周期就更長,由于一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然后remove它,防止內存泄露。

項目應用

? 為了避免每個封裝后的參數從controller層傳遞到service層,再從service層傳遞到dao層,或者從service層傳遞到其他的工具類當中。我在項目中使用ThreadLocal的思路是這樣的:

? 由于避免參數復雜的傳遞,在controller中將已經封裝好的參數放入ThreadLocal中,在其他層調用時直接通過ThreadLocal對象獲取。在方法結束時,定義攔截器(或者Filter)進行ThreadLocal的remove方法。

常見線程不安全類與寫法

? 什么是線程不安全的類呢?簡單的說,如果一個類的對象同時可以被多個線程訪問,如果不做特殊的同步或并發處理,那么就很容易表現出線程不安全的現象,比如異常、邏輯處理錯誤等等,這種類稱之為線程不安全的類。

StringBuilder 與 StringBuffer
StringBuilder
@Slf4j
@NotThreadSafe
public class StringExample1 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    public static StringBuilder stringBuilder = new StringBuilder();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", stringBuilder.length());
    }

    private static void update() {
        stringBuilder.append("1");
    }
}

? main函數中輸出的結果不為預期的5000,并且每次結果可能會不一致,因此StringBuilder是線程不安全類

StringBuffer
@Slf4j
@ThreadSafe
public class StringExample2 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    public static StringBuffer stringBuffer = new StringBuffer();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", stringBuffer.length());
    }

    private static void update() {
        stringBuffer.append("1");
    }
}

? StringBuffer每次輸出的結果與預期結果一致,因此它是線程安全的類

總結

? 通過以上兩個例子可以知道,StringBuffer為線程安全類,StringBuilder為線程不安全類。

? StringBuffer在方法的實現上使用了synchronized關鍵字對方法進行同步,因此是線程安全的,而StringBuilder則沒有進行特殊的同步或并發處理。

? StringBuffer使用了同步鎖,同一時間只能有一個線程進行訪問,因為在系統性能會有損耗,適用于多線程環境下使用。通常情況下,字符串拼接出現在方法內,使用StringBuilder進行字符串的拼接會大大提高性能,屬于堆棧封閉,單個線程的操作對象,因此不存在線程不安全問題,優先選擇使用StringBuilder。兩種字符串拼接類分別適用不同的場景,這就是為什么JAVA同時提供了這兩種類。

SimpleDateFormat 與 JodaTime
SimpleDateFormat

? SimpleDateFormat是JAVA提供的一個日期轉換類。

@Slf4j
@NotThreadSafe
public class DateFormatExample1 {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
    }

    private static void update() {
        try {
            simpleDateFormat.parse("20180208");
        } catch (Exception e) {
            log.error("parse exception", e);
        }
    }
}

? 當方法運行的時候,則會拋出異常,原因是SimpleDateFormat在多線程下共享使用就會出現線程不安全情況。建議將SimpleDateFormat聲明為局部變量,這樣才會避免線程不安全所帶來的異常

JodaTime

? 線程安全的日期格式化

引入依賴

<dependency>
  <groupId>joda-time</groupId>
  <artifactId>joda-time</artifactId>
  <version>2.9.9</version>
</dependency>
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

@Slf4j
@ThreadSafe
public class DateFormatExample3 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
    }

    private static void update(int i) {
        log.info("{}, {}", i, DateTime.parse("20180208", dateTimeFormatter).toDate());
    }
}

輸出結果為線程安全的。

總結

? 在使用日期轉換的時候,更建議使用JodaTime所提供的日期轉換類,不僅是因為它是線程安全的,而且在類實際處理轉換中有其他的優勢。

ArrayList、HashSet、HashMap 等 Collections

? 通常使用以上類,都是聲明在方法內,作為局部變量使用,一般很少碰上線程不安全的問題。但如果定義為可以多個線程修改的時候,就會出現線程安全問題。

List

多線程訪問ArrayList會存在線程安全問題。

@Slf4j
@NotThreadSafe
public class ArrayListExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        // 如果線程安全的話,理論上 list.size == clientTotal
        // 最后輸出結果不為總產長度
        log.info("size:{}", list.size());
    }

    private static void update(int i) {
        list.add(i);
    }
}
Set

多線程操作HashSet也會存在線程安全問題

@Slf4j
@NotThreadSafe
public class HashSetExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static Set<Integer> set = new HashSet<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        // 如果線程安全的話,理論上 set.size == clientTotal
        // 輸出的長度不一致
        log.info("size:{}", set.size());
    }

    private static void update(int i) {
        //存在線程不安全問題
        set.add(i);
    }
}
Map

多線程操作HashMap也會存在線程安全問題

@Slf4j
@NotThreadSafe
public class HashMapExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static Map<Integer, Integer> map = new HashMap<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        // 如果線程安全的話,理論上 map.size == clientTotal
        // 輸出結果不一致,并且少于預期值
        log.info("size:{}", map.size());
    }

    private static void update(int i) {
        map.put(i, i);
    }
}
先檢查在執行:if(condition(a)){ handle(a) }

? 假設a為線程安全類或屬性,如AtomicInteger。當存在兩個線程都通過了condition(a)返回true,接下來分別處理a,即會觸發線程不安全問題。這里,它的不安全的點在于分成兩個操作之后,即使condition(a)handle(a)兩個操作都是線程安全的,但在執行的時候,并不是原子性的,因此則會引發線程不安全問題。

? 如果在項目中遇到這種處理,a為多線程共享,則需要在上面代碼之外進行加鎖,或者保證這兩個連續的操作時原子性的。

同步容器

? 在上面線程不安全類中,提到了ArrayListHashSetHashMap非線程安全的容器,如果有多個線程并發的訪問,就會出現線程安全問題,因此在編寫程序的時候,必須要求開發人員手動的在任何訪問這些容器的地方進行同步處理,導致使用這些容器非常不便,因此JAVA中提供同步容器。

  • ArrayList -> Vector、Stack

  • HashMap -> HashTable(key、value均不能為null)

  • Collections.synchronizedXXX(List、Set、Map)

      `Vector`實現`List`接口,底層和`ArrayList`類似,但是`Vector`中的方法都是使用`synchronized`修飾,即進行了同步的措施。 但是,`Vector`并不是線程安全的。
    
      `Stack`也是一個同步容器,也是使用`synchronized`進行同步,繼承與`Vector`,是數據結構中的,先進后出。
      
      `HashTable`和`HashMap`很相似,但`HashTable`進行了同步處理。
      
      `Collections`工具類提供了大量的方法,比如對集合的排序、查找等常用的操作。同時也通過了相關了方法創建同步容器類
    
Vector
//例子的寫法是線程安全的
@Slf4j
@ThreadSafe
public class VectorExample1 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static List<Integer> list = new Vector<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", list.size());
    }

    //這里是線程安全的
    private static void update(int i) {
        list.add(i);
    }
}

同步容器不一定是線程安全的。

@NotThreadSafe
public class VectorExample2 {

    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {

        while (true) {

            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }

            Thread thread1 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            };

            Thread thread2 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.get(i);
                    }
                }
            };
            thread1.start();
            thread2.start();
        }
    }
}

? VectorExample2程序的運行,在get()中會不斷的拋出ArrayIndexOutOfBoundsExceptionVector是線程同步容器,size()get()remove()都是被synchronized修飾的,但是為什么還是會存在線程安全問題呢?

? 首先,get()拋出的異常肯定是remove()引起的,Vector雖然能保證同一時刻,只能有一個線程進入訪問。但是不排除有以下可能:

//1. 線程1和線程2都執行完vector.size(),獲得的size大小相同,并且當兩個線程都是i = 9
//2. 線程1執行remove操作,刪除索引為9的數據
//3. 線程2執行get操作,獲取索引為9的數據,那么就會拋出數組越界異常,

for (int i = 0; i < vector.size(); i++) {
    //線程1
      vector.remove(i);
}

for (int i = 0; i < vector.size(); i++) {
    //線程2
       vector.get(i);
}

? 在使用同步容器的時候,并不是所有的場合下都能夠做到線程安全。

HashTable
@Slf4j
@ThreadSafe
public class HashTableExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static Map<Integer, Integer> map = new Hashtable<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        //輸出結果與預期一致
        log.info("size:{}", map.size());
    }
    //此寫法是線程安全的
    private static void update(int i) {
        map.put(i, i);
    }
}
Collections
List
@Slf4j
@ThreadSafe
public class CollectionsExample1 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;
    
    // List同步容器構造
    private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", list.size());
    }

    private static void update(int i) {
        list.add(i);
    }
}
Set
@Slf4j
@ThreadSafe
public class CollectionsExample2 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    //構造同步HashSet
    private static Set<Integer> set = Collections.synchronizedSet(Sets.newHashSet());

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", set.size());
    }

    private static void update(int i) {
        set.add(i);
    }
}
Map
@Slf4j
@ThreadSafe
public class CollectionsExample3 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    //構造同步HashMap
    private static Map<Integer, Integer> map = Collections.synchronizedMap(new HashMap<>());

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", map.size());
    }

    private static void update(int i) {
        map.put(i, i);
    }
}
集合的刪除
public class VectorExample3 {

    // java.util.ConcurrentModificationException
    private static void test1(Vector<Integer> v1) { // foreach
        for(Integer i : v1) {
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }
    // java.util.ConcurrentModificationException
    private static void test2(Vector<Integer> v1) { // iterator
        Iterator<Integer> iterator = v1.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }
    /**
     * 如果在使用foreach或iterator進集合的遍歷,
     * 盡量不要在操作的過程中進行remove等相關的更新操作。
     * 如果非要進行操作,則可以在遍歷的過程中記錄需要操作元素的序號,
     * 待遍歷結束后方可進行操作,讓這兩個動作分開進行
     */
    

    // success
    private static void test3(Vector<Integer> v1) { // for
        for (int i = 0; i < v1.size(); i++) {
            if (v1.get(i).equals(3)) {
                v1.remove(i);
            }
        }
    }

    public static void main(String[] args) {

        Vector<Integer> vector = new Vector<>();
        vector.add(1);
        vector.add(2);
        vector.add(3);
        test1(vector);
    }
}

? 在單線程會出現以上錯誤,在多線程情況下,并且集合時共享的,出現異常的概率會更大,需要特別的注意。解決方案是希望在foreach或iterator時,對要操作的元素進行標記,待循環結束之后,在執行相關操作。

? 以上例子中,for循環是能正確的進行,因此推薦使用for循環做來做包含更新操作的便利

同步容器總結

? 同步容器中的方法主要采取synchronized進行同步,因此執行的性能會收到受到影響,并且同步容器并不一定能做到真正的線程安全。

并發容器 J.U.C

? 所謂的J.U.C其實是JDK所提供的一個包名,全程為java.util.concurrent,里面提供了許多線程安全的集合。

CopyOnWriteArrayList
Introduction

? ArrayList -> CopyOnWriteArrayList , ,CopyOnWriteArrayList相比于ArrayList是線程安全的,從字面意思理解,即為寫操作時復制。CopyOnWriteArrayList使用了一種叫寫時復制的方法,當有新元素添加到CopyOnWriteArrayList時,先從原有的數組中拷貝一份出來,然后在新的數組做寫操作,寫完之后,再將原來的數組引用指向到新數組。

? CopyOnWriteArrayList的整個add操作都是在鎖的保護下進行的。 這樣做是為了避免在多線程并發add的時候,復制出多個副本出來,把數據搞亂了,導致最終的數組數據不是我們期望的。

? 本節介紹的內容,大部分參考來源于線程安全的CopyOnWriteArrayList介紹

Shortcoming
  1. 由于寫操作的時候,需要拷貝數組,會消耗內存,如果原數組的內容比較多的情況下,可能導致young gc或者full gc

  2. 不能用于實時讀的場景,像拷貝數組、新增元素都需要時間,所以調用一個set操作后,讀取到數據可能還是舊的,雖然CopyOnWriteArrayList能做到最終一致性,但是還是沒法滿足實時性要求;

    ? CopyOnWriteArrayList 合適讀多寫少的場景,不過這類慎用 因為誰也沒法保證CopyOnWriteArrayList 到底要放置多少數據,萬一數據稍微有點多,每次add/set都要重新復制數組,這個代價實在太高昂了。在高性能的互聯網應用中,這種操作分分鐘引起故障。

Design Thinking
  1. 讀寫分離,讀和寫分開
  2. 最終一致性。最終保證List的結果是對的
  3. 使用另外開辟空間的思路,來解決并發沖突
Read Operation
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 添加元素的操作
    public boolean add(E e) {
        // 獲得鎖
        final ReentrantLock lock = this.lock;
        //上鎖
        lock.lock();
        try {
            Object[] elements = getArray();//獲得當前的數組
            int len = elements.length;//獲取數組長度
            //進行數組的復制
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //添加新元素
            newElements[len] = e;
            //引用指向更改
            setArray(newElements);
            return true;
        } finally {
            //解鎖
            lock.unlock();
        }
    }

}

? 由于所有的寫操作都是在新數組進行的,這個時候如果有線程并發的寫,則通過鎖來控制,如果有線程并發的讀,則分幾種情況:

  1. 如果寫操作未完成,那么直接讀取原數組的數據;
  2. 如果寫操作完成,但是引用還未指向新數組,那么也是讀取原數組數據;
  3. 如果寫操作完成,并且引用已經指向了新的數組,那么直接從新數組中讀取數據。

注意:CopyOnWriteArrayList的讀操作是可以不用加鎖的。

public E get(int index) {
    return get(getArray(), index);
}
Using
@Slf4j
@ThreadSafe
public class CopyOnWriteArrayListExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static List<Integer> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", list.size());
    }
    
    private static void update(int i) {
        list.add(i);
    }
}

CopyOnWriteArraySet

? HashSet -> CopyOnWriteArraySet

CopyOnWriteArraySet底層實現是采用CopyOnWriteArrayList,合適比較小的集合,其中所有可變操作(add、set、remove等等)都是通過對底層數組進行一次新的復制來實現的,一般需要很大的開銷。迭代器支持hasNext(), next()等不可變操作,不支持可變的remove操作;使用迭代器進行遍歷的速度很快,并且不會與其他線程發生沖突。在構造迭代器時,迭代器依賴于不變的數組快照。

@Slf4j
@ThreadSafe
public class CopyOnWriteArraySetExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static Set<Integer> set = new CopyOnWriteArraySet<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", set.size());
    }

    private static void update(int i) {
        set.add(i);
    }
}
ConcurrentSkipListSet

? TreeSet -> ConcurrentSkipListSet

  • ConcurrentSkipListSet<E>是jdk6新增的類,位于java.util.concurrent并發庫下
  • ConcurrentSkipListSet<E>TreeSet一樣,都是支持自然排序,并且可以在構造的時候定義Comparator<E>的比較器,該類的方法基本和TreeSet中方法一樣(方法簽名一樣)
  • 和其他的Set集合一樣,ConcurrentSkipListSet<E>都是基于Map集合的,ConcurrentSkipListMap便是它的底層實現
  • 在多線程的環境下,ConcurrentSkipListSet<E>中的containsaddremove操作是安全的,多個線程可以安全地并發執行插入、移除和訪問操作。但是對于批量操作addAllremoveAllretainAllcontainsAll并不能保證以原子方式執行。理由很簡單,因為addAllremoveAllretainAll底層調用的還是containsaddremove的方法,在批量操作時,只能保證每一次的containsaddremove的操作是原子性的(即在進行containsaddremove三個操作時,不會被其他線程打斷),而不能保證每一次批量的操作都不會被其他線程打斷。因此,在addAllremoveAllretainAllcontainsAll操作時,需要添加額外的同步操作。
  • 此類不允許使用 null 元素,因為無法可靠地將 null 參數及返回值與不存在的元素區分開來
@Slf4j
@ThreadSafe
public class ConcurrentSkipListSetExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static Set<Integer> set = new ConcurrentSkipListSet<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", set.size());
    }

    private static void update(int i) {
        set.add(i);
    }
}
ConcurrentHashMap

? HashMap -> ConcurrentHashMap ,不允許null值,絕大部分使用Map都是讀取操作,而且讀操作大多數都是成功的,因此,ConcurrentHashMap針對讀操作進行了大量的優化。在高并發的場景下,有很大的優勢。

? 內容參考深入并發包 ConcurrentHashMap

? 因為多線程環境下,使用Hashmap進行put操作會引起死循環,導致CPU利用率接近100%,所以在并發情況下不能使用HashMapHashMap在put的時候,插入的元素超過了容量(由負載因子決定)的范圍就會觸發擴容操作,就是rehash,這個會重新將原數組的內容重新hash到新的擴容數組中,在多線程的環境下,存在同時其他的元素也在進行put操作,如果hash值相同,可能出現同時在同一數組下用鏈表表示,造成閉環,導致在get時會出現死循環,所以HashMap是線程不安全的。

? HashTable,它是線程安全的,它在所有涉及到多線程操作的都加上了synchronized關鍵字來鎖住整個table,這就意味著所有的線程都在競爭一把鎖,在多線程的環境下,它是安全的,但是無疑是效率低下的。

? 其實HashTable有很多的優化空間,鎖住整個table這么粗暴的方法可以變相的柔和點,比如在多線程的環境下,對不同的數據集進行操作時其實根本就不需要去競爭一個鎖,因為他們不同hash值,不會因為rehash造成線程不安全,所以互不影響,這就是鎖分離技術,將鎖的粒度降低,利用多個鎖來控制多個小的table,多線程訪問容器里不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高并發訪問效率,這就是ConcurrentHashMapJDK1.7版本的核心思想。

@Slf4j
@ThreadSafe
public class ConcurrentHashMapExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static Map<Integer, Integer> map = new ConcurrentHashMap<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        // 線程安全,輸出結果準確并一致
        log.info("size:{}", map.size());
    }
    
    private static void update(int i) {
        map.put(i, i);
    }
}
ConcurrentSkipListMap

? TreeMap -> ConcurrentSkipListMap,內部使用``SkipList`結構實現的。跳表是一個鏈表,但是通過使用“跳躍式”查找的方式使得插入、讀取數據時復雜度變成了O(log n)。

? 跳表(SkipList):使用“空間換時間”的算法,令鏈表的每個結點不僅記錄next結點位置,還可以按照level層級分別記錄后繼第level個結點。

參考文章:Java并發容器——ConcurrentSkipListMap和ConcurrentHashMap

@Slf4j
@ThreadSafe
public class ConcurrentSkipListMapExample {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時并發執行的線程數
    public static int threadTotal = 200;

    private static Map<Integer, Integer> map = new ConcurrentSkipListMap<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", map.size());
    }

    private static void update(int i) {
        map.put(i, i);
    }
}
concurrentHashMap與ConcurrentSkipListMap性能測試

? 內容引用于Java多線程(四)之ConcurrentSkipListMap深入分析

? 在4線程1.6萬數據的條件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。

? 但ConcurrentSkipListMap有幾個ConcurrentHashMap不能比擬的優點

  1. ConcurrentSkipListMap 的key是有序的,而ConcurrentHashMap是做不到的

  2. ConcurrentSkipListMap 支持更高的并發。ConcurrentSkipListMap的存取時間是log(N),和線程數幾乎無關。也就是說在數據量一定的情況下,并發的線程越多,ConcurrentSkipListMap越能體現出他的優勢。

     在非多線程情況下,盡量使用`TreeMap`,此外,對于并發性較低的程序,可以使用`Collections`工具所提供的方法`synchronizedSortMap`,它是將`TreeMap`進行包裝。對于高并發場景下,應使用`ConcurrentSkipListMap`提供更高的并發度。并且,如果在多線程環境下,需要對`Map`的鍵值進行排序時,也要盡量使用`ConcurrentSkipListMap`
    
J.U.C 內容概覽
image

安全共享策略總結

? 以下策略是通過線程安全策略中的不可變對象、線程封閉、同步容器以及并發容器相關知識總結而得:

  1. 線程限制:一個被線程限制的對象,由線程獨占,并且只能被占有它的線程修改
  2. 共享只讀:一個共享只讀的對象,在沒有額外同步的情況下,可以被多個線程并發訪問,但是任何線程都不能修改它
  3. 線程安全對象:一個線程安全的對象或容器,在內部通過同步機制來保證線程安全,所以其他線程無需額外的同步就可以通過公共接口隨意訪問它
  4. 被守護對象:被守護對象只能通過獲取特定的鎖來訪問
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容

  • 前言 ThreadLocal很多同學都搞不懂是什么東西,可以用來干嘛。但面試時卻又經常問到,所以這次我和大家一起學...
    liangzzz閱讀 12,482評論 14 228
  • Java-Review-Note——4.多線程 標簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,667評論 2 17
  • 新年不僅僅是和家人的團聚,也是同學好友最美好的相聚時光,在沒有攀比沒有禮貌性隨禮,沒有面具沒有勾心斗角,就這么簡簡...
    上班狗哈哈閱讀 164評論 0 0
  • 2017/2/19 春初。和朋友散步。有杏花開得正好,妍而不艷,潔雅有之,清芬陣陣。 杏比桃櫻皆早,臘梅依然,迎春...
    泠生閱讀 319評論 0 2
  • 我覺得對于人物的刻畫,有一個框子把這個人大致框住之后,要有細節去填充。細節可以有若干,但必有一個或兩個地方,足以給...
    mysupercc閱讀 23,279評論 0 0