ArrayList、LinkedList、Vector、 hashMap、hashtable、ConcurrentHashMap的原理以及區別

 

ArrayList、LinkedList、Vector區別和實現原理。

ArrayList、LinkedList、Vector是集合中經常拿來比較和麪試的一個問題,我這裏簡要概括一下他們的區別和實現原理。這裏需要區別jdk1.6和jdk1.8。我們從三個方面去闡述:

存儲結構

        ArrayList和Vector是按照順序將元素存儲(從下標爲0開始),刪除元素時,刪除操作完成後,需要使部分元素移位,默認的初始容量都是10(但jdk1.6確實初始容量爲10),但jdk1.8,如果只是初始化無參構造函數時,初始容量爲0,當第一次添加add()時,會擴容到10。

      1.當創建方式爲 List list = new ArrayList(0)時,默認調用EMPTY_ELEMENTDATA初始化容量爲0,當首次添加元素時,容量擴爲 1;

ArrayList源碼:

//被用於空實例的共享空數組實例
 private static final Object[] EMPTY_ELEMENTDATA = {};

//當創建爲ArrayList(0),默認調用EMPTY_ELEMENTDATA初始化容量爲0
 public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
//首次添加add()數據時,擴容變爲1;
 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

     2.當創建方式爲 List list = new ArrayList()時,默認調用DEFAULTCAPACITY_EMPTY_ELEMENTDATA 初始化容量爲0,當首次添加元素時,容量擴爲 10;

//默認初始容量
    private static final int DEFAULT_CAPACITY = 10;

 
//被用於默認大小的空實例的共享數組實例。其與EMPTY_ELEMENTDATA的區別是:當我們向數組中添加第一個元素時,知道數組該擴充多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//創建ArrayList()時,即無參構造方法時:默認調用DEFAULTCAPACITY_EMPTY_ELEMENTDATA初始化容量爲0
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

//執行添加add()數據之後,查詢集合的size是否爲0
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
//如果size不爲0,調用calculateCapacity()方法
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }



//首次添加add()數據時擴容爲10
private static int calculateCapacity (Object[] elementData, int minCapacity) {  
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {  
        return Math.max(DEFAULT_CAPACITY, minCapacity);  
    }  
    return minCapacity;  
    }  

ArrayList和Vector是基於動態數組實現的,LinkedList是基於雙向鏈表實現的(含有頭結點)。

 

線程安全性

ArrayList不具有有線程安全性,在單線程的環境中,LinkedList也是線程不安全的,如果在併發環境下使用它們,可以用Collections類中的靜態方法synchronizedList()對ArrayList和LinkedList進行調用即可,即可達到線程安全問題。

//調用Collections的靜態方法,即可達到線程安全
 public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }

Vector實現線程安全的,即他的方法大都包含關鍵字synchronized,但是Vector的效率沒有ArraykList和LinkedList高。

//大部分方法被synchronized修飾
 public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

擴容機制


       從內部實現機制來講,ArrayList和Vector都是使用Object的數組形式來存儲的,當向這兩種類型中增加元素的時候,若容量不夠,需要進行擴容。ArrayList擴容後的容量是之前的1.5倍,然後把之前的數據拷貝到新建的數組中去。而Vector默認情況下擴容後的容量是之前的2倍。

Vector可以設置容量增量,而ArrayList不可以。在Vector中,有capacityIncrement:當大小大於其容量時,容量自動增加的量。如果在創建Vector時,指定了capacityIncrement的大小,則Vector中動態數組容量需要增加時,如果容量的增量大於0,則增加的是大小是capacityIncrement,如果增量小於0,則增大爲之前的2倍。

在這裏需要說一下可變長度數組的原理:當元素個數超過數組的長度時,會產生一個新的數組,將原數組的數據複製到新數組,再將新的元素添加到新數組中。

 

增刪改查的效率


           ArrayList和Vector中,從指定的位置檢索一個對象,或在集合的末尾插入,刪除一個元素的時間是一樣的,時間複雜度都是O(1)。但是如果在其他位置增加或者刪除元素花費的時間是O(n),LinkedList中,在插入、刪除任何位置的元素所花費的時間都是一樣的,時間複雜度都爲O(1),但是他在檢索一個元素的時間複雜度爲O(n)。所以如果只是查找特定位置的元素或只在集合的末端增加移動元素,那麼使用ArrayList或Vector都是一樣的。如果是在指定位置的插入、刪除元素,最好選擇LinkedList。

        總結:ArrayList:動態數組結構,線程非安全,查詢速度較快,

                   LinkedList:雙向鏈表結構,線程非安全,增刪比較塊,

                   Vector :動態數組結構,線程安全。

        java中數據存儲方式最底層的兩種結構,一種是數組,另一種就是鏈表,數組的特點:連續空間,尋址迅速,但是在刪除或者添加元素的時候需要有較大幅度的移動,所以查詢速度快,增刪較慢。而鏈表正好相反,由於空間不連續,尋址困難,增刪元素只需修改指針,所以查詢慢、增刪快。有沒有兩者的結合呢?有,哈希表具有較快(常量級)的查詢速度,及相對較快的增刪速度。

 

 

--------------------------------------以下內容部分摘抄於這位大神的博客------------------------------------

https://blog.csdn.net/zhangerqing/article/details/8193118

https://www.cnblogs.com/heyonggang/p/9112731.html

hashMap hashtable ConcurrentHashMap區別

HashMap

1.從類定義上:HashMap 繼承自 AbstractMap

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable{};

2.hashMap內部存儲結構:

    數組加鏈表結構:


從上圖中,我們可以發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標爲12的位置。它的內部其實是用一個Entity數組來實現的,屬性有key、value、next。接下來我會從初始化階段詳細的講解HashMap的內部結構。                                    

3.初始容量以及擴容:

       初始化容量爲16,擴容:newsize = oldsize*2,size一定爲2的n次冪,底層調用rehash()方法進行擴容。                          擴容機制:當Map中元素總數超過Entry數組的75%,觸發擴容機制;但,插入元素後才判斷該不該擴容,有可能無效擴容(插入後如果擴容,如果沒有再次插入,就會產生無效擴容)。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 4.線程是否安全:

    沒有被synchronized修飾,線程非安全;

 public boolean isEmpty() { }
 public V get(Object key) { }
 public boolean containsKey(Object key) { }
 public V put(K key, V value){ }
 ......

     5.hashMap允許鍵值爲空:而在 HashMap 的 put 方法中,調用了 putVal ()方法,該方法需要有一個 int 類型的 hash 值,這個值是利用內部的 hash 方法產生的。從下面的源代碼可以看出,當 key 爲 null 時,返回的 hash 值爲 0,說明在 HashMap 中是允許 key=null 的情況存在的。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict){
}
    
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

6.HashMap的初始值還要考慮加載因子:                                                                                                                                            1)哈希衝突:就是在Entry數組中的位置若干Key的哈希值按數組大小取模後,如果落在同一個數組下標上,將組成一條Entry鏈,對Key的查找需要遍歷Entry鏈上的每個元素執行equals()比較。                                                                                           

2)加載因子:爲了降低哈希衝突的概率,默認當HashMap中的鍵值對達到數組大小的75%時,即會觸發擴容。因此,如果預估容量是100,即需要設定100/0.75=134的數組大小。                                                                                                                        3)間換時間:如果希望加快Key查找的時間,還可以進一步降低加載因子,加大初始大小,以降低哈希衝突的概率

Hashtable

1.從類定義上:

  Hashtable繼承Dictionary;

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {};

2.存儲結構上:數組加鏈表結構,和hashMap基本相同

3.初始容量以及擴容:

       初始size爲11,擴容:newsize = olesize*2+1,通過rehash()方法進行擴容,

       計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

4.線程是否安全:

Hashtable 在很多方法定義時都會加上 synchronized關鍵字,說明 Hashtable 是線程安全的

  public synchronized int size() {   return count; }
  public synchronized boolean isEmpty() {  return count == 0;}
  public synchronized V get(Object key){}
  public synchronized boolean contains(Object value) {}
  ......

5.是否允許鍵值爲空:

在 Hashtable 添加元素源碼中,我們可以發現,如果添加元素的 value 爲 null 時,會拋出 NullPointerException。在程序內部,有這樣一行代碼 int hash = key.hashCode ,如果添加的 key 爲 null 時,此時也會拋出空指針異常,因此,在 Hashtable 中,是不允許 key 和 value 爲 null 的

  public V setValue(V value) {
            if (value == null)
                throw new NullPointerException();

            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

ConcurrentHashMap

1.底層採用分段的數組+鏈表實現,底層先調用lock(),lock是ReentrantLock類的一個方法,因此是線程安全

2.通過把整個Map分爲N個Segment,可以提供相同的線程安全,但是效率提升N倍,默認提升16倍。(讀操作不加鎖,由於HashEntry的value變量是 volatile的,也能保證讀取到最新的值。)

3.Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨佔,ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術,分段加鎖。

4.有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖

5.擴容:段內擴容(段內元素超過該段對應Entry數組長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不需要擴容,有效避免無效擴容

注:

ConcurrentHashMap是使用了鎖分段技術來保證線程安全的。

鎖分段技術:首先將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。 

ConcurrentHashMap提供了與Hashtable和SynchronizedMap不同的鎖機制。Hashtable中採用的鎖機制是一次鎖住整個hash表,從而在同一時刻只能由一個線程對其進行操作;而ConcurrentHashMap中則是一次鎖住一個桶。

ConcurrentHashMap默認將hash表分爲16個桶,諸如get、put、remove等常用操作只鎖住當前需要用到的桶。這樣,原來只能一個線程進入,現在卻能同時有16個寫線程執行,併發性能的提升是顯而易見的。

 

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