JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 三

JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 三

版本 作者 內(nèi)容
2018.5.17 chuIllusions 線程安全策略

相關(guān)文章

JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 一 之 并發(fā)相關(guān)知識(shí)
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 二 之 線程安全性纺弊、安全發(fā)布對(duì)象
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 四 之 J.U.C之AQS
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 五 之 J.U.C組件拓展
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 六 之 線程池

線程安全策略

? 創(chuàng)建后狀態(tài)不能被修改的對(duì)象叫作不可變對(duì)象。不可變對(duì)象天生就是線程安全的。它們的常量(變量)是在構(gòu)造函數(shù)中創(chuàng)建的,既然它們的狀態(tài)無(wú)法被修改课幕,那么這些常量永遠(yuǎn)不會(huì)被改變——不可變對(duì)象永遠(yuǎn)是線程安全的拐袜。

不可變對(duì)象需要滿足的條件

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

不可變對(duì)象

final

? final關(guān)鍵字:類龟劲、方法、變量

  • 修飾類:不能被繼承轴或,final類中的成員屬性可以根據(jù)需要設(shè)置為final昌跌,但final類中所有的成員方法都被隱式指定為final方法。一般不建議將類設(shè)置為final類型照雁〔戏撸可以參考String類答恶。
  • 修飾方法:1)鎖定方法不被繼承類修改;2)效率
  • 修飾變量:1)基本數(shù)據(jù)類型變量萍诱,初始化后便不能進(jìn)行修改悬嗓;2)引用類型變量,初始化之后不能再指向別的引用
@Slf4j
@NotThreadSafe
public class ImmutableExample1 {

    private final static Integer a = 1;
    private final static String b = "2";
    //引用類型不允許引用指向改變裕坊,但是對(duì)象值還是可以進(jìn)行修改的  
    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;              //編譯時(shí)報(bào)錯(cuò)
//        b = "3";            //編譯時(shí)報(bào)錯(cuò)
//        map = Maps.newHashMap();   //編譯時(shí)報(bào)錯(cuò)
        map.put(1, 3);       //容易引發(fā)線程安全問(wèn)題
        log.info("{}", map.get(1));
    }

    //可以修飾參數(shù)
    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);
        //處理過(guò)后的map是不可以再進(jìn)行修改的
        map = Collections.unmodifiableMap(map);
    }

    public static void main(String[] args) {
        //允許操作,但是操作會(huì)報(bào)錯(cuò)饵蒂,扔出異常
        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));
    }
}

? 介紹了不可變對(duì)象彼乌,通過(guò)在某些情況下,將不能被修改的類對(duì)象渊迁,設(shè)置為不可變對(duì)象慰照,來(lái)讓對(duì)象在多個(gè)線程間是線程安全的。歸根到底宫纬,其實(shí)是躲避開了并發(fā)的問(wèn)題焚挠。除了不可變對(duì)象,還存在一個(gè)方法 就是線程封閉

線程封閉

? 把對(duì)象封裝到一個(gè)線程里漓骚,只有這一個(gè)線程能看到該對(duì)象蝌衔,那么就算這個(gè)對(duì)象不是線程安全的,也不會(huì)出現(xiàn)任何線程安全的問(wèn)題蝌蹂,因?yàn)樗荒茉谝粋€(gè)線程中被訪問(wèn)噩斟,如何實(shí)現(xiàn)線程封閉:

  • Ad-hoc 線程封閉:程序控制實(shí)現(xiàn),非常脆弱孤个、最糟糕剃允,忽略
  • 堆棧封閉:簡(jiǎn)單的說(shuō)就是局部變量,無(wú)并發(fā)問(wèn)題齐鲤。多個(gè)線程訪問(wèn)同一個(gè)方式的時(shí)候斥废,方法中的局部變量都會(huì)被拷貝一份到線程棧中,方法的局部變量是不被多個(gè)線程共享的给郊,因此不會(huì)出現(xiàn)線程安全問(wèn)題牡肉,能用局部變量就不推薦使用全局變量,全局變量容易引起并發(fā)問(wèn)題淆九,注意统锤,全局的變量而不是全局的常量毛俏。
  • 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提供線程級(jí)別的變量.這些變量不同于它們正常下的變量副本,在每一個(gè)線程中都有它自己獲取方式(通過(guò)它的get和set方法),不依賴變量副本的初始化饲窿。它的實(shí)例通常都是私有的靜態(tài)的煌寇,用于關(guān)聯(lián)線程的上下文。

? 這些變量在多線程環(huán)境下訪問(wèn)(通過(guò)get或set方法訪問(wèn))時(shí)能保證各個(gè)線程里的變量相對(duì)獨(dú)立于其他線程內(nèi)的變量

? 總結(jié):ThreadLocal的作用是提供線程內(nèi)部的局部變量逾雄,這種變量只存在線程的生命周期阀溶。

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

類分析

? ThreadLocal涉及到的類結(jié)構(gòu):

(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類中嘲驾。同時(shí)淌哟,ThreadLocalMap中用于存儲(chǔ)數(shù)據(jù)的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對(duì)象,value為存儲(chǔ)的值
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

ThreadLocalMap的keyThreadLocal類的實(shí)例對(duì)象辽故,value為用戶的值

public class ThreadLocal<T> {
    //設(shè)置值的方法
    public void set(T value) {
        //1.獲取當(dāng)前線程
        Thread t = Thread.currentThread();
        //2.從線程中獲取該線程的成員屬性 ThreadLocal.ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //將值放入Map中
        if (map != null)
            map.set(this, value);
        else
            //先創(chuàng)建徒仓,在設(shè)置值
            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;
            }
        }

        //如果沒有設(shè)置,會(huì)調(diào)用誊垢,設(shè)置一個(gè)value為null
        return setInitialValue();
    }
}

工作原理

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

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

  • 每個(gè)線程中都有屬于自己的ThreadLocalMap喂走,互不干擾

  • 全局只有一個(gè)threadLocal,當(dāng)通過(guò)set填充數(shù)據(jù)時(shí)乎芳,通過(guò)獲取當(dāng)前操作線程的threadLocalMap帖池,將threadLocal作為threadLocalMap中的key睡汹,需要填充的值作為value

  • 當(dāng)需要從threadLocal獲取值時(shí)肴甸,通過(guò)獲取當(dāng)前操作線程的threadLocalMap囚巴,并返回keythreadLocal對(duì)象的value

      那么就可以理解為:`ThreadLocal`的活動(dòng)范圍是具體的某一個(gè)線程彤叉,并且是該線程獨(dú)有的秽浇。它不是用來(lái)解決共享變量的多線程安全問(wèn)題兔辅。
    
      但是维苔,有一點(diǎn)需要說(shuō)明的是,如果`ThreadLocal`通過(guò)`set`方法放進(jìn)去的值轧简,這個(gè)值是共享對(duì)象,那么還是會(huì)存在線程安全問(wèn)題褐澎。
    
    
多個(gè) ThreadLocal
public class ThreadLocal<T> {

    //用于唯一確認(rèn)一個(gè)ThreadLocal對(duì)象
    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

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

? ThreadLocal中的ThreadLocalMap中的keyThreadLocal對(duì)象,由于每個(gè)實(shí)例化的ThreadLocal對(duì)象都是不相同的梳毙,所以不會(huì)存在key沖突萌业,所以一個(gè)線程存在多個(gè)ThreadLocal對(duì)象作為key是完全沒有問(wèn)題的。也就是說(shuō)抱婉,一個(gè)線程中的ThreadLocalMap可以存在多個(gè)key

? 為什么使用ThreadLocal作為ThreadLocalMapkey? 上面的解析已經(jīng)很明確了患亿。

? 試試使用線程id作為ThreadLocalMapkey? 如果使用線程id作為key,如果存在兩個(gè)ThreadLocal對(duì)象漱抓,一個(gè)存放String類型,另一個(gè)存放Integer類型,而在單個(gè)線程中只存在一個(gè)ThreadLocalMap范删,當(dāng)存放數(shù)據(jù)時(shí),key永遠(yuǎn)只會(huì)有一個(gè)(線程id)添忘,存入數(shù)據(jù)的時(shí)候先存會(huì)被后存覆蓋,獲取數(shù)據(jù)時(shí)候可能會(huì)發(fā)生錯(cuò)誤仲器。

應(yīng)用場(chǎng)景

? ThreadLocal中存放的變量只在線程的生命周期內(nèi)起作用涕侈,應(yīng)用場(chǎng)景只要有兩個(gè)方面:

  1. 提供一個(gè)線程內(nèi)公共變量(比如本次請(qǐng)求的用戶信息木张、實(shí)體參數(shù))鹃彻,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或者組件之間一些公共變量的傳遞的復(fù)雜度
  2. 為線程提供一個(gè)私有的變量副本蛛株,這樣每一個(gè)線程都可以隨意修改自己的變量副本,而不會(huì)對(duì)其他線程產(chǎn)生影響。
關(guān)于內(nèi)存泄露

? 首先害捕,得分析一下內(nèi)存泄露是什么東西,Java內(nèi)存泄露又是怎么定義的盾沫?

內(nèi)存泄漏(Memory Leak)是指程序中己動(dòng)態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無(wú)法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果渐苏。

? 在Java程序中仪吧,我們通常使用new為對(duì)象分配內(nèi)存,而這些內(nèi)存空間都在堆(Heap)上出皇。

? JAVA內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由GC完成。在JAVA達(dá)到內(nèi)存泄露的存在兩個(gè)特點(diǎn)狞贱,滿足以下兩個(gè)條件,即可認(rèn)為是JAVA內(nèi)存泄露佑颇,這些對(duì)象不被GC管理、回收,占用內(nèi)存解藻。

  1. 對(duì)象是可達(dá)的,即對(duì)象引用存在
  2. 對(duì)象無(wú)用的巷嚣,即對(duì)象已經(jīng)不再使用

當(dāng)達(dá)到內(nèi)存泄露時(shí),扔出的異常:java.lang.OutOfMemoryError:Java heap space

ThreadLocal對(duì)象之間的引用關(guān)系圖

image

下面引用知乎的一篇文章(ThreadLocal和synchronized的區(qū)別?)進(jìn)行說(shuō)明:

ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個(gè)ThreadLocal沒有外部強(qiáng)引用引用他嗤放,那么系統(tǒng)gc的時(shí)候搔涝,這個(gè)ThreadLocal勢(shì)必會(huì)被回收蜕煌,這樣一來(lái),ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒有辦法訪問(wèn)這些key為null的Entry的value因块,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:ThreadLocal Ref -> Thread -> ThreadLocalMap -> Entry -> value永遠(yuǎn)無(wú)法回收,造成內(nèi)存泄露。

分析ThreadLocalMap中的源碼

private Entry[] table;
/**
 * 根據(jù)ThreadLocal對(duì)象獲取Entry
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
private Entry getEntry(ThreadLocal<?> key) {
     //計(jì)算索引位置
     int i = key.threadLocalHashCode & (table.length - 1);
     Entry e = table[i];
     if (e != null && e.get() == key)
          return e;
     else
         //沒有找到相應(yīng)的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對(duì)象
        ThreadLocal<?> k = e.get();
        //如果e為null或者key不一致則向下一個(gè)位置查詢
        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,否則繼續(xù)向下一個(gè)位置查詢
    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;
}

在這個(gè)過(guò)程中遇到的key為null的Entry都會(huì)被擦除届垫,那么Entry內(nèi)的value也就沒有強(qiáng)引用鏈误债,自然會(huì)被回收。仔細(xì)研究代碼可以發(fā)現(xiàn)箫老,set操作也有類似的思想,將key為null的這些Entry都刪除,防止內(nèi)存泄露各薇。 但是光這樣還是不夠的,上面的設(shè)計(jì)思路依賴一個(gè)前提條件:要調(diào)用ThreadLocalMap的genEntry函數(shù)或者set函數(shù)林螃。這當(dāng)然是不可能任何情況都成立的完残,所以很多情況下需要使用者手動(dòng)調(diào)用ThreadLocal的remove函數(shù),手動(dòng)刪除不再需要的ThreadLocal,防止內(nèi)存泄露。所以JDK建議將ThreadLocal變量定義成private static的刊愚,這樣的話ThreadLocal的生命周期就更長(zhǎng),由于一直存在ThreadLocal的強(qiáng)引用,所以ThreadLocal也就不會(huì)被回收地回,也就能保證任何時(shí)候都能根據(jù)ThreadLocal的弱引用訪問(wèn)到Entry的value值畅买,然后remove它,防止內(nèi)存泄露。

項(xiàng)目應(yīng)用

? 為了避免每個(gè)封裝后的參數(shù)從controller層傳遞到service層,再?gòu)膕ervice層傳遞到dao層蹂季,或者從service層傳遞到其他的工具類當(dāng)中。我在項(xiàng)目中使用ThreadLocal的思路是這樣的:

? 由于避免參數(shù)復(fù)雜的傳遞睬辐,在controller中將已經(jīng)封裝好的參數(shù)放入ThreadLocal中,在其他層調(diào)用時(shí)直接通過(guò)ThreadLocal對(duì)象獲取。在方法結(jié)束時(shí),定義攔截器(或者Filter)進(jìn)行ThreadLocal的remove方法。

常見線程不安全類與寫法

? 什么是線程不安全的類呢逗概?簡(jiǎn)單的說(shuō)枚钓,如果一個(gè)類的對(duì)象同時(shí)可以被多個(gè)線程訪問(wèn)星掰,如果不做特殊的同步或并發(fā)處理,那么就很容易表現(xiàn)出線程不安全的現(xiàn)象,比如異常晋渺、邏輯處理錯(cuò)誤等等木西,這種類稱之為線程不安全的類。

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

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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函數(shù)中輸出的結(jié)果不為預(yù)期的5000随静,并且每次結(jié)果可能會(huì)不一致八千,因此StringBuilder是線程不安全類

StringBuffer
@Slf4j
@ThreadSafe
public class StringExample2 {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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每次輸出的結(jié)果與預(yù)期結(jié)果一致,因此它是線程安全的類

總結(jié)

? 通過(guò)以上兩個(gè)例子可以知道恋捆,StringBuffer為線程安全類,StringBuilder為線程不安全類重绷。

? StringBuffer在方法的實(shí)現(xiàn)上使用了synchronized關(guān)鍵字對(duì)方法進(jìn)行同步沸停,因此是線程安全的,而StringBuilder則沒有進(jìn)行特殊的同步或并發(fā)處理昭卓。

? StringBuffer使用了同步鎖愤钾,同一時(shí)間只能有一個(gè)線程進(jìn)行訪問(wèn),因?yàn)樵谙到y(tǒng)性能會(huì)有損耗候醒,適用于多線程環(huán)境下使用能颁。通常情況下,字符串拼接出現(xiàn)在方法內(nèi)倒淫,使用StringBuilder進(jìn)行字符串的拼接會(huì)大大提高性能伙菊,屬于堆棧封閉,單個(gè)線程的操作對(duì)象敌土,因此不存在線程不安全問(wèn)題镜硕,優(yōu)先選擇使用StringBuilder。兩種字符串拼接類分別適用不同的場(chǎng)景纯赎,這就是為什么JAVA同時(shí)提供了這兩種類谦疾。

SimpleDateFormat 與 JodaTime
SimpleDateFormat

? SimpleDateFormat是JAVA提供的一個(gè)日期轉(zhuǎn)換類南蹂。

@Slf4j
@NotThreadSafe
public class DateFormatExample1 {

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

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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);
        }
    }
}

? 當(dāng)方法運(yùn)行的時(shí)候犬金,則會(huì)拋出異常,原因是SimpleDateFormat在多線程下共享使用就會(huì)出現(xiàn)線程不安全情況六剥。建議將SimpleDateFormat聲明為局部變量晚顷,這樣才會(huì)避免線程不安全所帶來(lái)的異常

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 {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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());
    }
}

輸出結(jié)果為線程安全的。

總結(jié)

? 在使用日期轉(zhuǎn)換的時(shí)候疗疟,更建議使用JodaTime所提供的日期轉(zhuǎn)換類该默,不僅是因?yàn)樗蔷€程安全的,而且在類實(shí)際處理轉(zhuǎn)換中有其他的優(yōu)勢(shì)策彤。

ArrayList栓袖、HashSet匣摘、HashMap 等 Collections

? 通常使用以上類,都是聲明在方法內(nèi)裹刮,作為局部變量使用音榜,一般很少碰上線程不安全的問(wèn)題。但如果定義為可以多個(gè)線程修改的時(shí)候捧弃,就會(huì)出現(xiàn)線程安全問(wèn)題赠叼。

List

多線程訪問(wèn)ArrayList會(huì)存在線程安全問(wèn)題。

@Slf4j
@NotThreadSafe
public class ArrayListExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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
        // 最后輸出結(jié)果不為總產(chǎn)長(zhǎng)度
        log.info("size:{}", list.size());
    }

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

Set

多線程操作HashSet也會(huì)存在線程安全問(wèn)題

@Slf4j
@NotThreadSafe
public class HashSetExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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
        // 輸出的長(zhǎng)度不一致
        log.info("size:{}", set.size());
    }

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

Map

多線程操作HashMap也會(huì)存在線程安全問(wèn)題

@Slf4j
@NotThreadSafe
public class HashMapExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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
        // 輸出結(jié)果不一致买鸽,并且少于預(yù)期值
        log.info("size:{}", map.size());
    }

    private static void update(int i) {
        map.put(i, i);
    }
}

先檢查在執(zhí)行:if(condition(a)){ handle(a) }

? 假設(shè)a為線程安全類或?qū)傩越Ы迹?code>AtomicInteger。當(dāng)存在兩個(gè)線程都通過(guò)了condition(a)返回true眼五,接下來(lái)分別處理a底燎,即會(huì)觸發(fā)線程不安全問(wèn)題。這里弹砚,它的不安全的點(diǎn)在于分成兩個(gè)操作之后双仍,即使condition(a)handle(a)兩個(gè)操作都是線程安全的桌吃,但在執(zhí)行的時(shí)候朱沃,并不是原子性的,因此則會(huì)引發(fā)線程不安全問(wèn)題茅诱。

? 如果在項(xiàng)目中遇到這種處理逗物,a為多線程共享,則需要在上面代碼之外進(jìn)行加鎖瑟俭,或者保證這兩個(gè)連續(xù)的操作時(shí)原子性的翎卓。

同步容器

? 在上面線程不安全類中,提到了ArrayList摆寄、HashSet失暴、HashMap非線程安全的容器,如果有多個(gè)線程并發(fā)的訪問(wèn)微饥,就會(huì)出現(xiàn)線程安全問(wèn)題逗扒,因此在編寫程序的時(shí)候,必須要求開發(fā)人員手動(dòng)的在任何訪問(wèn)這些容器的地方進(jìn)行同步處理欠橘,導(dǎo)致使用這些容器非常不便矩肩,因此JAVA中提供同步容器。

  • ArrayList -> Vector肃续、Stack

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

  • Collections.synchronizedXXX(List叉袍、Set、Map)

      `Vector`實(shí)現(xiàn)`List`接口刽酱,底層和`ArrayList`類似畦韭,但是`Vector`中的方法都是使用`synchronized`修飾,即進(jìn)行了同步的措施肛跌。 但是艺配,`Vector`并不是線程安全的。
    
      `Stack`也是一個(gè)同步容器衍慎,也是使用`synchronized`進(jìn)行同步转唉,繼承與`Vector`,是數(shù)據(jù)結(jié)構(gòu)中的稳捆,先進(jìn)后出赠法。
    
      `HashTable`和`HashMap`很相似,但`HashTable`進(jìn)行了同步處理乔夯。
    
      `Collections`工具類提供了大量的方法砖织,比如對(duì)集合的排序、查找等常用的操作末荐。同時(shí)也通過(guò)了相關(guān)了方法創(chuàng)建同步容器類
    
    
Vector
//例子的寫法是線程安全的
@Slf4j
@ThreadSafe
public class VectorExample1 {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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程序的運(yùn)行,在get()中會(huì)不斷的拋出ArrayIndexOutOfBoundsException甲脏。Vector是線程同步容器眶熬,size()get()remove()都是被synchronized修飾的块请,但是為什么還是會(huì)存在線程安全問(wèn)題呢娜氏?

? 首先,get()拋出的異扯招拢肯定是remove()引起的贸弥,Vector雖然能保證同一時(shí)刻,只能有一個(gè)線程進(jìn)入訪問(wèn)海渊。但是不排除有以下可能:

//1\. 線程1和線程2都執(zhí)行完vector.size()绵疲,獲得的size大小相同,并且當(dāng)兩個(gè)線程都是i = 9
//2\. 線程1執(zhí)行remove操作切省,刪除索引為9的數(shù)據(jù)
//3\. 線程2執(zhí)行g(shù)et操作最岗,獲取索引為9的數(shù)據(jù)帕胆,那么就會(huì)拋出數(shù)組越界異常朝捆,

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

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

? 在使用同步容器的時(shí)候,并不是所有的場(chǎng)合下都能夠做到線程安全懒豹。

HashTable
@Slf4j
@ThreadSafe
public class HashTableExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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();
        //輸出結(jié)果與預(yù)期一致
        log.info("size:{}", map.size());
    }
    //此寫法是線程安全的
    private static void update(int i) {
        map.put(i, i);
    }
}

Collections
List
@Slf4j
@ThreadSafe
public class CollectionsExample1 {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    public static int threadTotal = 200;

    // List同步容器構(gòu)造
    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 {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    public static int threadTotal = 200;

    //構(gòu)造同步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 {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    public static int threadTotal = 200;

    //構(gòu)造同步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進(jìn)集合的遍歷芙盘,
     * 盡量不要在操作的過(guò)程中進(jìn)行remove等相關(guān)的更新操作驯用。
     * 如果非要進(jìn)行操作,則可以在遍歷的過(guò)程中記錄需要操作元素的序號(hào)儒老,
     * 待遍歷結(jié)束后方可進(jìn)行操作蝴乔,讓這兩個(gè)動(dòng)作分開進(jìn)行
     */

    // 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);
    }
}

? 在單線程會(huì)出現(xiàn)以上錯(cuò)誤,在多線程情況下驮樊,并且集合時(shí)共享的薇正,出現(xiàn)異常的概率會(huì)更大,需要特別的注意囚衔。解決方案是希望在foreach或iterator時(shí)挖腰,對(duì)要操作的元素進(jìn)行標(biāo)記,待循環(huán)結(jié)束之后练湿,在執(zhí)行相關(guān)操作猴仑。

? 以上例子中,for循環(huán)是能正確的進(jìn)行肥哎,因此推薦使用for循環(huán)做來(lái)做包含更新操作的便利

同步容器總結(jié)

? 同步容器中的方法主要采取synchronized進(jìn)行同步辽俗,因此執(zhí)行的性能會(huì)收到受到影響,并且同步容器并不一定能做到真正的線程安全篡诽。

并發(fā)容器 J.U.C

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

CopyOnWriteArrayList
Introduction

? ArrayList -> CopyOnWriteArrayList 杈女, 坐漏,CopyOnWriteArrayList相比于ArrayList是線程安全的,從字面意思理解碧信,即為寫操作時(shí)復(fù)制赊琳。CopyOnWriteArrayList使用了一種叫寫時(shí)復(fù)制的方法,當(dāng)有新元素添加到CopyOnWriteArrayList時(shí)砰碴,先從原有的數(shù)組中拷貝一份出來(lái)躏筏,然后在新的數(shù)組做寫操作,寫完之后呈枉,再將原來(lái)的數(shù)組引用指向到新數(shù)組趁尼。

? CopyOnWriteArrayList的整個(gè)add操作都是在鎖的保護(hù)下進(jìn)行的。 這樣做是為了避免在多線程并發(fā)add的時(shí)候猖辫,復(fù)制出多個(gè)副本出來(lái),把數(shù)據(jù)搞亂了酥泞,導(dǎo)致最終的數(shù)組數(shù)據(jù)不是我們期望的。

? 本節(jié)介紹的內(nèi)容啃憎,大部分參考來(lái)源于線程安全的CopyOnWriteArrayList介紹

Shortcoming
  1. 由于寫操作的時(shí)候芝囤,需要拷貝數(shù)組,會(huì)消耗內(nèi)存,如果原數(shù)組的內(nèi)容比較多的情況下悯姊,可能導(dǎo)致young gc或者full gc

  2. 不能用于實(shí)時(shí)讀的場(chǎng)景羡藐,像拷貝數(shù)組、新增元素都需要時(shí)間悯许,所以調(diào)用一個(gè)set操作后仆嗦,讀取到數(shù)據(jù)可能還是舊的,雖然CopyOnWriteArrayList能做到最終一致性,但是還是沒法滿足實(shí)時(shí)性要求;

    ? CopyOnWriteArrayList 合適讀多寫少的場(chǎng)景先壕,不過(guò)這類慎用 因?yàn)檎l(shuí)也沒法保證CopyOnWriteArrayList 到底要放置多少數(shù)據(jù)瘩扼,萬(wàn)一數(shù)據(jù)稍微有點(diǎn)多,每次add/set都要重新復(fù)制數(shù)組垃僚,這個(gè)代價(jià)實(shí)在太高昂了邢隧。在高性能的互聯(lián)網(wǎng)應(yīng)用中,這種操作分分鐘引起故障冈在。

Design Thinking
  1. 讀寫分離倒慧,讀和寫分開
  2. 最終一致性。最終保證List的結(jié)果是對(duì)的
  3. 使用另外開辟空間的思路包券,來(lái)解決并發(fā)沖突
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();//獲得當(dāng)前的數(shù)組
            int len = elements.length;//獲取數(shù)組長(zhǎng)度
            //進(jìn)行數(shù)組的復(fù)制
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //添加新元素
            newElements[len] = e;
            //引用指向更改
            setArray(newElements);
            return true;
        } finally {
            //解鎖
            lock.unlock();
        }
    }

}

? 由于所有的寫操作都是在新數(shù)組進(jìn)行的纫谅,這個(gè)時(shí)候如果有線程并發(fā)的寫,則通過(guò)鎖來(lái)控制溅固,如果有線程并發(fā)的讀付秕,則分幾種情況:

  1. 如果寫操作未完成,那么直接讀取原數(shù)組的數(shù)據(jù)侍郭;
  2. 如果寫操作完成询吴,但是引用還未指向新數(shù)組,那么也是讀取原數(shù)組數(shù)據(jù)亮元;
  3. 如果寫操作完成猛计,并且引用已經(jīng)指向了新的數(shù)組,那么直接從新數(shù)組中讀取數(shù)據(jù)爆捞。

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

public E get(int index) {
    return get(getArray(), index);
}

Using
@Slf4j
@ThreadSafe
public class CopyOnWriteArrayListExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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底層實(shí)現(xiàn)是采用CopyOnWriteArrayList,合適比較小的集合煮甥,其中所有可變操作(add盗温、set、remove等等)都是通過(guò)對(duì)底層數(shù)組進(jìn)行一次新的復(fù)制來(lái)實(shí)現(xiàn)的,一般需要很大的開銷成肘。迭代器支持hasNext(), next()等不可變操作卖局,不支持可變的remove操作;使用迭代器進(jìn)行遍歷的速度很快双霍,并且不會(huì)與其他線程發(fā)生沖突砚偶。在構(gòu)造迭代器時(shí)批销,迭代器依賴于不變的數(shù)組快照。

@Slf4j
@ThreadSafe
public class CopyOnWriteArraySetExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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并發(fā)庫(kù)下
  • ConcurrentSkipListSet<E>TreeSet一樣风钻,都是支持自然排序顷蟀,并且可以在構(gòu)造的時(shí)候定義Comparator<E>的比較器酒请,該類的方法基本和TreeSet中方法一樣(方法簽名一樣)
  • 和其他的Set集合一樣,ConcurrentSkipListSet<E>都是基于Map集合的鸣个,ConcurrentSkipListMap便是它的底層實(shí)現(xiàn)
  • 在多線程的環(huán)境下羞反,ConcurrentSkipListSet<E>中的containsadd囤萤、remove操作是安全的昼窗,多個(gè)線程可以安全地并發(fā)執(zhí)行插入、移除和訪問(wèn)操作涛舍。但是對(duì)于批量操作addAll澄惊、removeAllretainAllcontainsAll并不能保證以原子方式執(zhí)行富雅。理由很簡(jiǎn)單掸驱,因?yàn)?code>addAll、removeAll没佑、retainAll底層調(diào)用的還是contains毕贼、addremove的方法蛤奢,在批量操作時(shí)鬼癣,只能保證每一次的containsadd啤贩、remove的操作是原子性的(即在進(jìn)行contains待秃、addremove三個(gè)操作時(shí)痹屹,不會(huì)被其他線程打斷)锥余,而不能保證每一次批量的操作都不會(huì)被其他線程打斷。因此痢掠,在addAll驱犹、removeAllretainAllcontainsAll操作時(shí)足画,需要添加額外的同步操作雄驹。
  • 此類不允許使用 null 元素,因?yàn)闊o(wú)法可靠地將 null 參數(shù)及返回值與不存在的元素區(qū)分開來(lái)
@Slf4j
@ThreadSafe
public class ConcurrentSkipListSetExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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都是讀取操作,而且讀操作大多數(shù)都是成功的,因此蔬将,ConcurrentHashMap針對(duì)讀操作進(jìn)行了大量的優(yōu)化爷速。在高并發(fā)的場(chǎng)景下,有很大的優(yōu)勢(shì)霞怀。

? 內(nèi)容參考深入并發(fā)包 ConcurrentHashMap

? 因?yàn)槎嗑€程環(huán)境下惫东,使用Hashmap進(jìn)行put操作會(huì)引起死循環(huán),導(dǎo)致CPU利用率接近100%毙石,所以在并發(fā)情況下不能使用HashMap廉沮。 HashMap在put的時(shí)候,插入的元素超過(guò)了容量(由負(fù)載因子決定)的范圍就會(huì)觸發(fā)擴(kuò)容操作徐矩,就是rehash滞时,這個(gè)會(huì)重新將原數(shù)組的內(nèi)容重新hash到新的擴(kuò)容數(shù)組中,在多線程的環(huán)境下滤灯,存在同時(shí)其他的元素也在進(jìn)行put操作坪稽,如果hash值相同,可能出現(xiàn)同時(shí)在同一數(shù)組下用鏈表表示鳞骤,造成閉環(huán)窒百,導(dǎo)致在get時(shí)會(huì)出現(xiàn)死循環(huán),所以HashMap是線程不安全的弟孟。

? HashTable贝咙,它是線程安全的,它在所有涉及到多線程操作的都加上了synchronized關(guān)鍵字來(lái)鎖住整個(gè)table拂募,這就意味著所有的線程都在競(jìng)爭(zhēng)一把鎖庭猩,在多線程的環(huán)境下,它是安全的陈症,但是無(wú)疑是效率低下的蔼水。

? 其實(shí)HashTable有很多的優(yōu)化空間,鎖住整個(gè)table這么粗暴的方法可以變相的柔和點(diǎn)录肯,比如在多線程的環(huán)境下趴腋,對(duì)不同的數(shù)據(jù)集進(jìn)行操作時(shí)其實(shí)根本就不需要去競(jìng)爭(zhēng)一個(gè)鎖,因?yàn)樗麄儾煌?code>hash值论咏,不會(huì)因?yàn)?code>rehash造成線程不安全优炬,所以互不影響,這就是鎖分離技術(shù)厅贪,將鎖的粒度降低蠢护,利用多個(gè)鎖來(lái)控制多個(gè)小的table,多線程訪問(wèn)容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí)养涮,線程間就不會(huì)存在鎖競(jìng)爭(zhēng)葵硕,從而可以有效的提高并發(fā)訪問(wèn)效率眉抬,這就是ConcurrentHashMapJDK1.7版本的核心思想。

@Slf4j
@ThreadSafe
public class ConcurrentHashMapExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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();
        // 線程安全懈凹,輸出結(jié)果準(zhǔn)確并一致
        log.info("size:{}", map.size());
    }

    private static void update(int i) {
        map.put(i, i);
    }
}

ConcurrentSkipListMap

? TreeMap -> ConcurrentSkipListMap蜀变,內(nèi)部使用``SkipList`結(jié)構(gòu)實(shí)現(xiàn)的。跳表是一個(gè)鏈表介评,但是通過(guò)使用“跳躍式”查找的方式使得插入库北、讀取數(shù)據(jù)時(shí)復(fù)雜度變成了O(log n)。

? 跳表(SkipList):使用“空間換時(shí)間”的算法威沫,令鏈表的每個(gè)結(jié)點(diǎn)不僅記錄next結(jié)點(diǎn)位置贤惯,還可以按照l(shuí)evel層級(jí)分別記錄后繼第level個(gè)結(jié)點(diǎn)洼专。

參考文章:Java并發(fā)容器——ConcurrentSkipListMap和ConcurrentHashMap

@Slf4j
@ThreadSafe
public class ConcurrentSkipListMapExample {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    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性能測(cè)試

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

? 在4線程1.6萬(wàn)數(shù)據(jù)的條件下棒掠,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。

? 但ConcurrentSkipListMap有幾個(gè)ConcurrentHashMap不能比擬的優(yōu)點(diǎn)

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

  2. ConcurrentSkipListMap 支持更高的并發(fā)烟很。ConcurrentSkipListMap的存取時(shí)間是log(N),和線程數(shù)幾乎無(wú)關(guān)蜡镶。也就是說(shuō)在數(shù)據(jù)量一定的情況下雾袱,并發(fā)的線程越多,ConcurrentSkipListMap越能體現(xiàn)出他的優(yōu)勢(shì)官还。

     在非多線程情況下芹橡,盡量使用`TreeMap`,此外望伦,對(duì)于并發(fā)性較低的程序林说,可以使用`Collections`工具所提供的方法`synchronizedSortMap`,它是將`TreeMap`進(jìn)行包裝屯伞。對(duì)于高并發(fā)場(chǎng)景下腿箩,應(yīng)使用`ConcurrentSkipListMap`提供更高的并發(fā)度。并且劣摇,如果在多線程環(huán)境下珠移,需要對(duì)`Map`的鍵值進(jìn)行排序時(shí),也要盡量使用`ConcurrentSkipListMap`
    
    
J.U.C 內(nèi)容概覽
image

安全共享策略總結(jié)

? 以下策略是通過(guò)線程安全策略中的不可變對(duì)象末融、線程封閉钧惧、同步容器以及并發(fā)容器相關(guān)知識(shí)總結(jié)而得:

  1. 線程限制:一個(gè)被線程限制的對(duì)象,由線程獨(dú)占勾习,并且只能被占有它的線程修改
  2. 共享只讀:一個(gè)共享只讀的對(duì)象浓瞪,在沒有額外同步的情況下,可以被多個(gè)線程并發(fā)訪問(wèn)语卤,但是任何線程都不能修改它
  3. 線程安全對(duì)象:一個(gè)線程安全的對(duì)象或容器追逮,在內(nèi)部通過(guò)同步機(jī)制來(lái)保證線程安全酪刀,所以其他線程無(wú)需額外的同步就可以通過(guò)公共接口隨意訪問(wèn)它
  4. 被守護(hù)對(duì)象:被守護(hù)對(duì)象只能通過(guò)獲取特定的鎖來(lái)訪問(wèn)

原文鏈接:http://www.reibang.com/p/c6ce651e802d

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市钮孵,隨后出現(xiàn)的幾起案子骂倘,更是在濱河造成了極大的恐慌,老刑警劉巖巴席,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件历涝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡漾唉,警方通過(guò)查閱死者的電腦和手機(jī)荧库,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赵刑,“玉大人分衫,你說(shuō)我怎么就攤上這事“愦耍” “怎么了蚪战?”我有些...
    開封第一講書人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)铐懊。 經(jīng)常有香客問(wèn)我邀桑,道長(zhǎng),這世上最難降的妖魔是什么科乎? 我笑而不...
    開封第一講書人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任壁畸,我火速辦了婚禮,結(jié)果婚禮上茅茂,老公的妹妹穿的比我還像新娘溜歪。我一直安慰自己感猛,他們只是感情好盾致,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開白布言津。 她就那樣靜靜地躺著,像睡著了一般进副。 火紅的嫁衣襯著肌膚如雪这揣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,792評(píng)論 1 290
  • 那天影斑,我揣著相機(jī)與錄音给赞,去河邊找鬼。 笑死矫户,一個(gè)胖子當(dāng)著我的面吹牛片迅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播皆辽,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼柑蛇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼芥挣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起耻台,我...
    開封第一講書人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤空免,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后盆耽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蹋砚,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年摄杂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坝咐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡析恢,死狀恐怖墨坚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情氮昧,我是刑警寧澤框杜,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布浦楣,位于F島的核電站袖肥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏振劳。R本人自食惡果不足惜椎组,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望历恐。 院中可真熱鬧寸癌,春花似錦、人聲如沸弱贼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吮旅。三九已至溪烤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庇勃,已是汗流浹背檬嘀。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留责嚷,地道東北人鸳兽。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像罕拂,于是被迫代替她去往敵國(guó)和親揍异。 傳聞我的和親對(duì)象是個(gè)殘疾皇子全陨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 三 相關(guān)文章 JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 一 ...
    chuIllusions丶閱讀 2,750評(píng)論 1 7
  • 1 線程封閉 多線程訪問(wèn)共享可變數(shù)據(jù)時(shí),涉及到線程間數(shù)據(jù)同步的問(wèn)題衷掷。并不是所有時(shí)候烤镐,都要用到共享數(shù)據(jù),所以線程封閉...
    JavaEdge閱讀 1,460評(píng)論 0 7
  • 一棍鳖、線程狀態(tài)轉(zhuǎn)換新建(New)可運(yùn)行(Runnable)阻塞(Blocking)無(wú)限期等待(Waiting)限期等...
    達(dá)微閱讀 570評(píng)論 1 2
  • Java SE 基礎(chǔ): 封裝炮叶、繼承、多態(tài) 封裝: 概念:就是把對(duì)象的屬性和操作(或服務(wù))結(jié)合為一個(gè)獨(dú)立的整體渡处,并盡...
    Jayden_Cao閱讀 2,103評(píng)論 0 8
  • 如果真的打起來(lái)了······我們會(huì)死吧镜悉! 至少會(huì)死一大半吧。 王汝殿師兄第一個(gè)拔了劍——魚腸劍医瘫。那把劍看起來(lái)很短侣肄,...
    翁與溫閱讀 177評(píng)論 0 0