Java集合總結,從源碼到併發一路狂飆!!!

概述

說起集合,算是三顧茅廬了,在我初學Java的時候,曾接觸過集合,那個時候只會用像Collection接口下的List(ArrayList、Vector、LinkedList)和Set(HashSet、TreeSet)還有Map接口下的HashMap,當時只是會用,第二次學習集合是看到了網上一些大佬的文章,感覺自己差的太多,然後又瞭解了一點LinkedHashSet、TreeMap、HashTable的知識,直到今天的第三次,我意識到了集合和多線程之間的關係,這篇文章主要是把集合的知識點串起來,並加入併發下的集合問題。

List

在這裏插入圖片描述
List集合的底層是數組和鏈表,其存儲順序是有序的。

ArrayList

ArrayList是List集合的一個實現類,其底層實現是數組transient Object[] elementData;,數組的查詢是直接通過索引,查詢速度比較快,時間複雜度是O(1),增刪的話,根據增刪的位置,時間複雜度有所不同,如果是中間第i個位置,時間複雜度就是O(n-i),簡單理解其時間複雜度是O(n),舉個例子,一個有8個元素的數組,要在第6個位置插入元素,那麼,需要把後2個元素往後移,來保證原來的順序不變,其空間複雜度的話,爲了防止數組越界,尾部會有一部分的空閒空間。
默認初始化容量
在這裏插入圖片描述
當構造方法中沒有指定數組的大小時,其默認初始容量是10。
擴容機制
既然默認值是10,那麼當超過這個默認值的時候,一定有一個擴容機制,其擴容機制是,當集合中元素的個數大於集合容量的時候,也就是add的時候集合放不下了,就會觸發擴容機制,擴容後的新集合容量是舊集合容量的1.5倍,源碼:int newCapacity = oldCapacity + (oldCapacity >> 1);
線程問題
在這裏插入圖片描述
ArrayList是線程不安全的,在add()方法的時候,首先會檢查一下數組的容量是否夠用,如果夠用,那麼就會執行elementData[size++] = e;方法,該語句執行了兩大步,第一大步是,將e放到elementData緩衝區,第二大步是,將size的大小進行加1操作,也就是說,這個操作並非原子性操作。當在併發的情況時,就會出現問題。

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                for (int j = 0; j < 10; j++) {
                    list.add("執行添加元素操作");
                }
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

在這裏插入圖片描述

LinkedList

在這裏插入圖片描述
LinkedList也是List的一個實現類,其底層是雙向鏈表,其內部有一個next指針指向下一個節點,一個prev指針,指向上一個節點,由於是鏈表的數據結構,所以在查詢的時候相對就比較慢了,時間複雜度是O(n),因爲當我們需要查詢某個元素的時候,需要從第一個節點開始遍歷,直到查詢結束。而他的增刪就比較快了,如果增加一個N節點,直接將後一個節點的prev指向N節點,N節點的next指向後一個節點,前一個節點的next指向N節點,N節點的prev指針指向前一個節點即可,時間複雜度爲O(1),空間複雜度一般比ArrayList大,因爲每個節點都要存儲兩個指針。
線程問題
在這裏插入圖片描述在這裏插入圖片描述
LinkedList也是線程不安全的,其添加元素的操作,通過linklast方法在尾部進行添加的,添加完之後,並把size的大小加1。其他的不說,單單一個size++就不是原子性了。
下面一個例子來演示加1操作。

import java.util.LinkedList;
import java.util.List;

public class Main {
    static int a = 0;
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new LinkedList<>();

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    a ++;
                }
            }).start();
        }
        Thread.sleep(1000);// 主線程延遲是爲了保證那100線程先執行完
        System.out.println(a);
    }
}

在這裏插入圖片描述
通過反編譯下面的代碼,來分析加1操作

public class Main {
    public static void main(String[] args) {
        int a = 0;
        a ++;
    }
}

在這裏插入圖片描述
簡單的加1操作會執行三步:
0:把a的值加載到內存
1:將內存中的值,存儲到變量中
2:然後進行加1操作
兜了一大圈子,記住一句話,LinkedList是線程不安全的。

Vector

Vector也是List的一個實現類,其底層也是一個數組protected Object[] elementData;,底層ArrayList差不多,也就是加了synchronized的ArrayList,線程是安全的,效率沒有ArrayList高,一般不建議使用。
默認初始化容量
在這裏插入圖片描述
擴容機制
在這裏插入圖片描述
在這裏插入圖片描述
這裏的擴容機制是通過capacityIncrement參數來實現的。
線程問題
在這裏插入圖片描述
線程是安全的,因爲它用了synchronized關鍵字,將整個方法進行了包裹,所以效率比較低,不建議使用。

import java.util.Vector;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Vector<String> vector = new Vector<>();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                for (int j = 0; j < 10; j++) {
                    vector.add("添加元素");
                }
            }
        }).start();
        Thread.sleep(1000);// 避免主線程先執行輸出
        System.out.println(vector.size());
    }
}

在這裏插入圖片描述

CopyOnWriteArrayList

既然不建議使用Vector,那麼併發的時候怎麼辦呢,不要忘了,有一個包java.util.concurrent(JUC)包,它下面有一個類CopyOnWriteArrayList也是線程安全的。CopyOnWriteArrayList也是List的一個實現類。
在這裏插入圖片描述
add方法用Lock鎖來解決併發問題,其中在進行添加數據的時候,可以看到,用了copyOf方法,也就是複製了一份,然後再set進去。
在這裏插入圖片描述
CopyOnWriteArrayList底層也是用的數組,但是它的數組是用volatile修飾了,主要是保證了數據的可見性。
在這裏插入圖片描述
上圖是CopyOnWriteArrayList的get操作,並沒有加鎖,因爲volatile保證了數據的可見性,當數據被修改的時候,讀操作能立刻知道。
在這裏插入圖片描述
而vector的get操作加了鎖,所以效率肯定就沒有CopyOnWriteArrayList高。

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new CopyOnWriteArrayList<>();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                for (int j = 0; j < 10; j++) {
                    list.add("添加元素");
                }
            }
        }).start();
        Thread.sleep(1000);// 避免主線程先執行輸出
        System.out.println(list.size());
    }
}

在這裏插入圖片描述

Set

在這裏插入圖片描述
Set集合中的元素是唯一的,不能有重複的元素。

HashSet

在這裏插入圖片描述
HashSet是Set集合的一個實現類,其底層實現是HashMap的key,後面會詳細講解HashMap。不保證數據的存儲順序(即存的順序和取的順序不一定一樣)。其線程不安全。

LinkedHashSet

在這裏插入圖片描述在這裏插入圖片描述
在這裏插入圖片描述
LinkedHashSet的初始容量也是16,默認加載因子也是0.75,繼承了HashSet,底層基於LinkedHashMap,有一個維護順序的雙向鏈表,使得LinkedHashSet有序(存取有序)。其線程不安全。

TreeSet

在這裏插入圖片描述
可以看出TreeSet是基於TreeMap的。它是有序的,是數值有順序,通過樹來維護的。其線程不安全。

CopyOnWriteArraySet

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
import java.util.concurrent.CopyOnWriteArraySet;
這個是併發包下的,線程安全。
說了這麼多Set集合,感覺大多都是基於Map的,接下來看看Map到底是怎麼實現的!

Map

在這裏插入圖片描述

HashMap

底層數據結構
底層是JDK1.7是通過數組+鏈表JDK1.8是通過數組+鏈表+紅黑樹組成。所有的數據都是通過一個Node節點進行封裝,其中Node節點中封裝了hash值,key,value,和next指針。hash是通過key計算出的hashCode值進行對數組容量減一求餘得到的(官方的求餘方式是通過&運算進行的)。不同的key計算出來的hash值可能相同,解決衝突是通過拉鍊法(鏈表和紅黑樹)進行處理。正是因爲這種存儲形勢,所以HashMap的存取順序是無序的。
在這裏插入圖片描述
HashMap的底層是通過Node節點來維護的。
加載機制
在這裏插入圖片描述
懶加載機制,在put值的時候會判斷數組是否爲空,如果是就初始化數組,而不是new的時候就初始化。
默認初始化容量
在這裏插入圖片描述
HashMap是Map的一個實現類,其默認初始化容量大小是16。
擴容機制
在這裏插入圖片描述
擴容機制是根據擴容因子來擴容的,當容量的使用量達到總容量的0.75時,就會觸發擴容,舉例說就是,當總容量是16時,使用量達到12,就會觸發擴容機制。
在這裏插入圖片描述
擴容的新空間大小時就空間的2倍。
在這裏插入圖片描述
擴容操作最消耗性能的地方就是,原數組中的數據要重新存放,也就是resize(),即重新計算索引,重新分配。
處理衝突
在這裏插入圖片描述
在這裏插入圖片描述
當我們put一個值的時候,通過key來計算出hash值,計算出來的hash值做爲數組的索引,Node節點中封裝了hash值,key,value和next。
在這裏插入圖片描述
在這裏插入圖片描述
當鏈表的長度小於8的時候,處理衝突的方式是鏈表。大於等於8的時候,就會觸發紅黑樹方式存儲。
在這裏插入圖片描述
在這裏插入圖片描述
當元素個數小於等於6的時候,會觸發紅黑樹轉化爲鏈表的形式,爲什麼不是小於等於7,是因爲給一個過度,也就是防止添加一個剛好爲8,刪除一個剛好爲7,這樣來回轉化。
線程問題
HashMap是線程不安全的。

LinkedHashMap

在這裏插入圖片描述
LinkedHashMap繼承了HashMap
在這裏插入圖片描述
LinkedHashMap解決了HashMap不能保證存取順序的問題。內部增加了一個鏈表用於維護元素存取順序。

Hashtable

在這裏插入圖片描述
Hashtable也是Map的一個實現類。
在這裏插入圖片描述
Hashtable的初始默認容量是11,擴容閾值也是0.75。
在這裏插入圖片描述
在這裏插入圖片描述
該類的出現主要是解決了HashMap的線程安全問題,直接用了synchronized鎖,所以效率上不高,不建議使用(發現JDK1.0的線程問題,解決都很暴力)。

TreeMap

在這裏插入圖片描述
TreeMap也是Map的一個實現類
在這裏插入圖片描述
其底層是通過紅黑樹實現的,所以有序,這裏的有序不是指存取順序,而是數據大小的順序。

ConcurrentHashMap

在這裏插入圖片描述
ConcurrentHashMap是java.util.concurrent包下的,併發包下的。他就是對HashMap進行了一個擴展,也就是解決了他的線程安全問題。
在這裏插入圖片描述在這裏插入圖片描述
然後是它用了大量的CAS來進行優化,上圖中的U是private static final sun.misc.Unsafe U;,屬於Unsafe下的。
在這裏插入圖片描述
這裏是節點上鎖,這裏的節點可以理解爲hash值相同組成的鏈表(紅黑樹)的頭節點,鎖的粒度爲頭節點。可以看到,鎖的力度明顯比Hashtable的小。如果想要學習多線程和併發編程的可以看我這篇文章。
爲了在簡歷上寫掌握Java多線程和併發編程,做了兩萬字總結!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章