Java線程安全策略知識總結(jié)-0

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

  • 1.對象創(chuàng)建以后其狀態(tài)不能修改(話外音:final數(shù)據(jù)可以是編譯時不改變值的常量,在對這個常量進行定義的時候,必須對其賦值。變量的值一旦初始化,就不能改變。如果final變量是引用類型的變量,那么代表你不能去改變它的引用,一旦引用指向被初始化指向一個對象,就無法再把它改為指向另一個對象,但是可以改變這個變量所引用的對象的屬性)
  • 2.對象所有域都是final類型。
  • 3.對象是正確創(chuàng)建的(在對象創(chuàng)建期間,this引用沒有逸出)。

Collections.unmodifiableXXX:Collection、List、Set、Map...
Guava: ImmutabeXXX:Collection、List、Set、Map...
這些方法都能將指定對象設置為不可變,只讀。

newHashMap已經(jīng)是不可變,不能再添加新元素了。如果再進行put操作,會拋出java.lang.UnsupportedOperationException異常。

public class C {

    public static void main(String[] args) {
        Map<String, String> hashMap = Maps.newHashMap();
        hashMap.put("boy", "cmazxiaoma");

        Map<String, String> newHashMap = Collections.unmodifiableMap(hashMap);
        newHashMap.put("girl", "doudou");
    }
}

這里提一個概念"線程封閉":就是將對象封裝到一個線程里,只有一個線程能看到這個對象,就算這個對象就算不是線程安全的,也不會出現(xiàn)線程安全方面的問題。SpringMVC、Shiro框架都用到了線程封閉的思想。

堆棧封閉:就是方法中定義局部變量,不存在并發(fā)問題,因為是線程私有的。多個線程訪問一個方法時,方法中的布局變量都會被拷貝一份到線程的棧中,所以局部變量時不會被多個線程所共享的。

ThreadLocal線程封閉:這就不多說了(之前文章提到過)。ThreadLocal為每個線程提供了一個變量副本,從而解決了多線程訪問變量的沖突。話外音:如果系統(tǒng)CPU資源有空閑,但是內(nèi)存使用緊張,可以考慮使用時間換空間的策略(synchronized就是采取該策略)。反之,CPU資源緊張,內(nèi)存資源空閑,則可以使用空間換時間策略(ThreadLocal就是采取該策略),提高整體性能。

image.png

SimpleDateFormat是線程不安全的,可以通過ThreadLocal或者JDK1.8的LocalDateTime和DateTimeFormatter。

public class MyThreadLocal {

    public static final ThreadLocal<DateFormat> dateFormatThreadLocal = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String format(Date date) {
        return dateFormatThreadLocal.get().format(date);
    }

    public static Date parse(String date) throws ParseException {
        return dateFormatThreadLocal.get().parse(date);
    }

    public static void main(String[] args) {
        String date = format(new Date());
        System.out.println(date);
    }
}
public class LocalTimeTest {

    public static void main(String[] args) {
        // jdk1.8 線程安全獲取系統(tǒng)時間
        System.out.println(LocalDateTime.now());

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

        // parse
        LocalDateTime localDateTime = LocalDateTime.parse("2018-11-23 21:58:00", dateTimeFormatter);
        System.out.println(localDateTime);

        // format
        System.out.println(LocalDateTime.now().format(dateTimeFormatter));

    }
}

Java中是有提供了同步容器的,不過性能很差。

ArrayList ===>Vector, Stack


image.png

image.png

HashMap(key和value都能為空) ===> Hashtable(key,value不能為空)

寫到這里,就來講講HashMap和Hashtable之間的區(qū)別把:
1.Hashtable中key和value都不允許出現(xiàn)null值,但是在HashMap中null是可以作為key的,這樣的key只有一個??梢杂幸粋€key或者多個key所對應的值為null。當get()方法返回null時,即可以表示HashMap中沒有該key,也可以表示該key所對應的值為null。因此在HashMap中不能由get()方法來判斷HashMap是否存在某個key,而應該用containsKey()來判斷。
2.兩個在遍歷方式的內(nèi)部實現(xiàn)上有所不同。兩者都使用了Iterator,而Hashtable由于歷史原因,還使用了Enumeration。
3.哈希值的使用不同。Hashtable直接使用了對象的hashcode,而HashMap需要重新計算hash值。
4.兩者內(nèi)部實現(xiàn)的數(shù)組的初始化大小和擴容方式不同。Hashtable中hash數(shù)組默認大小是11,擴容方式是2*old+1。而HashMap中hash數(shù)組默認大小是16,擴容后大小一定是2的指數(shù)。
5.HashSet依靠hashCode()和equals()方法來區(qū)分重復元素。HashSet內(nèi)部使用Map來保存數(shù)據(jù)。HashSet的數(shù)據(jù)作為Map中的key值保存,這也是HashSet中元素不能重復的原因。而Map保存key值時,會去判斷當前Map中是否會有該key。先通過key的hashcode,如果有相同的hashcode(hash沖突),再用equals()方法判斷是否相同。

Collections.synchronizedXXX(List、Set、Map)

雖然說Vector是同步容器,但是也無法保證多線程讀寫數(shù)據(jù)情況下的線程安全(不支持邊遍歷邊修改)。

public class VectorTest {

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

    public static void main(String[] args) {

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

            Thread getThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.get(i);
                    }
                }
            });

            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0 ; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            });
            getThread.start();
            removeThread.start();
        }
    }
}
image.png

來看看并發(fā)容器把。

ArrayList對應著CopyOnWriteArrayList

HashSet、TreeSet對應著CopyOnWriteArraySet、ConCurrentSkipListSet

HashMap、TreeMap對應著ConcurrentHashMap、ConcurrentSkipListMap

CopyOnWriteArrayList當有新元素的時候添加時,它會將原來數(shù)組拷貝一份。然后在新的數(shù)組上面做寫操作,寫完之后,將原來的數(shù)組指向新的數(shù)組。當元素的內(nèi)容過多的時候,會造成YGC或者FGC。因為拷貝數(shù)據(jù)是需要時間的,我們讀的可能不是最新的數(shù)據(jù),所以無法滿足實時讀。也就是說不是強一致性,而是追求最終一致性。CopyOnWriteArrayList用到了讀寫分離思想,適合讀多寫少的場景。CopyOnWriteArray讀是不需要加鎖的,是在原來的數(shù)組上面讀。而寫卻是要加鎖,避免多線程并發(fā)修改拷貝出多個副本。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容