HashMap,HashSet

HashMapHashSet html

摘自:https://www.cnblogs.com/skywang12345/p/3310887.html#a1 java

 

目錄算法

1、    HashMap(鍵值對形式存取,鍵值不能相同)    2 編程

1.    HashMap的數據結構    2數組

2.    HashMap的存取實現    3緩存

3.    疑問:若是兩個key經過hash%Entry[].length獲得的 index相同,會不會有覆蓋的危險?    4安全

4.    解決hash衝突的方法    5數據結構

5.    Hash衝突是什麼?    5多線程

6.    如何解決哈希衝突?    5併發

7.    HashMapput()remove()方法    6

2、    HashSet(是一個沒有重複元素的集合)    6

1.    HashSet簡介    6

2.    HashSet的數據結構    7

3.    HashSet源碼解析(基於JDK1.6.0_45    8

4.    HashSet的遍歷方式    12

3、    TreeMapHashMap的區別和共同點    13

4、    HashMapHashTable的區別    14

5、    HashMapHashTable的區別    14

1.    Hashtable的遍歷:    15

6、    ConcurrentHashMap的應用    16

1.    concurrentHashMap的優點    16

2.    深刻理解ConcurrentHashMap原理分析即線程安全問題    18

1)    ConcurrentHashMapHashTable的區別    18

2)    ConcurrentHashMap詳解    19

7、    HashMapHashTableConcurrentHashMap的區別    21

1.    HashMapConcurrentHashMap的區別    21

2.    ConcurrentHashMap vs Hashtable vs Synchronized Map區別    21

 

 

  1. HashMap(鍵值對形式存取,鍵值不能相同)
  1. HashMap的數據結構

    數組的特色是:尋址容易,插入和刪除困難。

    鏈表的特色是:尋址困難,插入和刪除容易。

    綜合這二者的特性,獲得一種尋址容易,插入刪除也容易的數據結構:這就是咱們要提起的哈希表。

    哈希表有多種不一樣的實現方法,咱們接下來解釋的是最經常使用的方法——拉鍊法,咱們能夠理解爲"鏈表的數組":如圖:

    從上圖咱們能夠發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中的呢?通常狀況下是經過hash(key)%len得到,也就是元素的key的哈希值對數組的長度取餘獲得。好比上述哈希表中,12%16=1228%16=12108%16=12140%16=12。因此1228108140都存儲在數組下標爲12的鏈表的位置。

    HashMap其實也是一個線性的數組實現的,因此能夠理解爲其存儲數據的容器就是一個線性數組,這可能讓咱們很不解,一個線性的數據怎麼實現按鍵值對來存取數據呢?這裏HashMap有作一些處理。

    首先HahsMap裏面實現了一個靜態內部類,其重要的屬性有keyvaluenext,從屬性keyvalue咱們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,咱們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[]Map裏面的內容都保存在Entry[]裏面

  2. HashMap的存取實現

    既然是線性數組,爲何能隨機存取呢?這裏HashMap用了一個小算法,大體HashMap 採用一種所謂的"Hash 算法"來決定每一個元素的存儲位置。當程序執行 map.put(String,Obect)方法時,系統將調用String hashCode() 方法獲得其 hashCode 值——每一個 Java 對象都有 hashCode() 方法,均可經過該方法得到它的 hashCode 值。獲得這個對象的 hashCode 值以後,系統會根據該 hashCode 值來決定該元素的存儲位置。是這樣實現:

     

//存儲時:

int hash = key.hashCode();// 這個hashCode方法這裏不詳述,只要理解每一個key的hash是一個固定的int值

int index = hash % Entry[].length;

Entry[index] = value;

 

//取值時:

int hash = key.hashCode();

int index = hash % Entry[].length;

return Entry[index];

這裏的話咱們:

對於存儲:

  1. 經過hashCode()計算keyhash值;
  2. 經過keyhash值對數組長度取餘獲得該keyvalue在數組中的下標;
  3. value賦值給Entry[index]實現鍵值對的存儲

對於取值:

  1. 首先也是計算keyhash值;
  2. 計算keyhash值對數組長度取餘獲得該keyvalue在數組中的下標;
  3. 經過返回return Entry[index]獲得鍵key所對應的值。
  1. 疑問:若是兩個key經過hash%Entry[].length獲得的 index相同,會不會有覆蓋的危險?

    這樣佔用的內存會很大

    這裏HashMap裏面用到鏈式數據結構的一個概念。上面咱們提到過Entry類裏面有一個next屬性,做用是指向下一個Entry。打個比方,第一個鍵值對A進來,經過計算其keyhash獲得的index=0,記做:Entry[0]=A。一會又進來一個鍵值對B,經過計算其index也等於0;如今怎麼辦?HashMap會這樣作:B.next = A,Entry[0] = B,若是又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣咱們發現index=0的地方其實存取了A,B,C三個鍵值對,他們經過next這個屬性連接在一塊兒。因此疑問不用擔憂。也就是說數組中存儲的是最後插入的元素。到這裏爲止,HashMap的大體實現,咱們應該已經清楚了。

    固然HashMap裏面也包含一些優化方面的實現,這裏也說一下。好比:Entry[]的長度必定後,隨着map裏面數據的愈來愈長,這樣同一個index的鏈就會很長,會不會影響性能?HashMap裏面設置一個因素(也稱爲因子),隨着mapsize愈來愈大,Entry[]會以必定的規則加長長度。

  2. 解決hash衝突的方法
    1. 開放定址法(線性探測再散列,二次探測再散列,僞隨機探測再散列)
    2. 再哈希法
    3. 鏈地址法
    4. 創建一個公共溢出區

    JavahashMap的解決方法就是採用鏈地址法。

  3. Hash衝突是什麼?

    若兩個不相等的 key 產生了相等的哈希值,這時則須要採用哈希衝突

    首先,HashMap是由線性數組組成的,如今咱們假設初始數組的長度爲5;而後咱們存儲數據,假設存儲的第一個數據的鍵值的hashcode計算出來的值爲6,而後咱們經過hashcode計算出來的值與數組長度取餘獲得存儲第一個數據的下標,即6%5=1;當咱們存儲另外的數據,若是經過鍵值的hashcode計算出來的值是11,那麼此時計算出數據的下標11%5=1也是1。這就是哈希衝突。

  4. 如何解決哈希衝突?

    Java採用拉鍊法解決哈希衝突。

    1. 獲得一個key
    2. 計算keyhashValue
    3. 根據 hashValue 值定位到 data[hashValue] ( data[hashValue] 是一條鏈表)
    4. data[hashValue] 爲空則直接插入,否則則添加到鏈表頭部。
  5. HashMapput()remove()方法

HashMap<String, Integer> map = new HashMap<String, Integer>();

        map.put("wang", 01);

        map.put("wang",02);

        System.out.println(map.get("wang"));

        System.out.println("----------------");

        map.remove("wang");

        System.out.println(map.get("wang"));

***************

2

----------------

null

 

  • 兩次插入的鍵相同時不是哈希衝突

    方法:則直接更新該鍵的值

  • 兩次插入的鍵不一樣時,可是獲得相同的hashValue時,是哈希衝突

    方法:將值插入到單鏈表的頭結點。

  • 刪除關鍵字值爲k的記錄,應先在該關鍵字值的哈希地址處的單鏈表中找到該記錄,而後刪除之。

     

  1. HashSet(是一個沒有重複元素的集合)
  1. HashSet簡介

    HashSet是一個沒有重複元素的集合,它是由HashMap實現的,不保證元素的順序,並且HashSet容許使用null元素。

    HashSet是非同步的,若是多個線程同時訪問一個HashSet,而其中至少一個線程修改了該set,那麼它必須保持外部同步。這一般是經過對天然封裝該set的對象執行同步操做來完成的。若是不存在這樣的對象,則應該使用Collections.synchronizedSet方法來包裝set,最好在建立完成時完成這一操做,以防止對該set進行意外的不一樣步訪問:

    Set s = Collections.synchronizedSet(new HashSet(...));

    HashSet經過iterator()迭代器進行訪問。

  2. HashSet的數據結構

    HashSet的繼承關係以下:

    java.lang.Object

    java.util.AbstractCollection<E>

    java.util.AbstractSet<E>

    java.util.HashSet<E>

     

    public class HashSet<E>

    extends AbstractSet<E>

    implements Set<E>, Cloneable, java.io.Serializable { }

    從上圖能夠看出:

    1. HashSet繼承與AbstractSet,而且實現了Set接口
    2. HashSet的本質是一個"沒有重複元素"的集合,它是經過HashMap實現的。HashSet中含有一個"HashMap類型的成員變量"mapHashSet的操做函數,實際上都是經過map實現的。
  3. HashSet源碼解析(基於JDK1.6.0_45

    爲了更瞭解HashSet的原理,下面對HashSet源碼代碼做出分析。

package java.util;

 

public class HashSet<E>

extends AbstractSet<E>

implements Set<E>, Cloneable, java.io.Serializable

{

static final long serialVersionUID = -5024744406713321676L;

 

// HashSet是經過map(HashMap對象)保存內容的

private transient HashMap<E,Object> map;

 

// PRESENT是向map中插入key-value對應的value

// 由於HashSet中只須要用到key,而HashMapkey-value鍵值對;

// 因此,向map中添加鍵值對時,鍵值對的值固定是PRESENT

private static final Object PRESENT = new Object();

 

// 默認構造函數

public HashSet() {

// 調用HashMap的默認構造函數,建立map

map = new HashMap<E,Object>();

}

 

// 帶集合的構造函數

public HashSet(Collection<? extends E> c) {

// 建立map

// 爲何要調用Math.max((int) (c.size()/.75f) + 1, 16),從 (c.size()/.75f) + 1 16 中選擇一個比較大的樹呢?

// 首先,說明(c.size()/.75f) + 1

// 由於從HashMap的效率(時間成本和空間成本)考慮,HashMap的加載因子是0.75

// HashMap"閾值"(閾值=HashMap總的大小*加載因子) < "HashMap實際大小"時,

// 就須要將HashMap的容量翻倍。

// 因此,(c.size()/.75f) + 1 計算出來的正好是總的空間大小。

// 接下來,說明爲何是 16

// HashMap的總的大小,必須是2的指數倍。若建立HashMap時,指定的大小不是2的指數倍;

// HashMap的構造函數中也會從新計算,找出比"指定大小"大的最小的2的指數倍的數。

// 因此,這裏指定爲16是從性能考慮。避免重複計算。

map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));

// 將集合(c)中的所有元素添加到HashSet

addAll(c);

}

 

// 指定HashSet初始容量和加載因子的構造函數

public HashSet(int initialCapacity, float loadFactor) {

map = new HashMap<E,Object>(initialCapacity, loadFactor);

}

 

// 指定HashSet初始容量的構造函數

public HashSet(int initialCapacity) {

map = new HashMap<E,Object>(initialCapacity);

}

 

HashSet(int initialCapacity, float loadFactor, boolean dummy) {

map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);

}

 

// 返回HashSet的迭代器

public Iterator<E> iterator() {

// 實際上返回的是HashMap"key集合的迭代器"

return map.keySet().iterator();

}

 

public int size() {

return map.size();

}

 

public boolean isEmpty() {

return map.isEmpty();

}

 

public boolean contains(Object o) {

return map.containsKey(o);

}

 

// 將元素(e)添加到HashSet

public boolean add(E e) {

return map.put(e, PRESENT)==null;

}

 

// 刪除HashSet中的元素(o)

public boolean remove(Object o) {

return map.remove(o)==PRESENT;

}

 

public void clear() {

map.clear();

}

 

// 克隆一個HashSet,並返回Object對象

public Object clone() {

try {

HashSet<E> newSet = (HashSet<E>) super.clone();

newSet.map = (HashMap<E, Object>) map.clone();

return newSet;

} catch (CloneNotSupportedException e) {

throw new InternalError();

}

}

 

// java.io.Serializable的寫入函數

// HashSet"總的容量,加載因子,實際容量,全部的元素"都寫入到輸出流中

private void writeObject(java.io.ObjectOutputStream s)

throws java.io.IOException {

// Write out any hidden serialization magic

s.defaultWriteObject();

// Write out HashMap capacity and load factor

s.writeInt(map.capacity());

s.writeFloat(map.loadFactor());

// Write out size

s.writeInt(map.size());

// Write out all elements in the proper order.

for (Iterator i=map.keySet().iterator(); i.hasNext(); )

s.writeObject(i.next());

}

// java.io.Serializable的讀取函數

// HashSet"總的容量,加載因子,實際容量,全部的元素"依次讀出

private void readObject(java.io.ObjectInputStream s)

throws java.io.IOException, ClassNotFoundException {

// Read in any hidden serialization magic

s.defaultReadObject();

 

// Read in HashMap capacity and load factor and create backing HashMap

int capacity = s.readInt();

float loadFactor = s.readFloat();

map = (((HashSet)this) instanceof LinkedHashSet ?

new LinkedHashMap<E,Object>(capacity, loadFactor) :

new HashMap<E,Object>(capacity, loadFactor));

// Read in size

int size = s.readInt();

// Read in all elements in the proper order.

for (int i=0; i<size; i++) {

E e = (E) s.readObject();

map.put(e, PRESENT);

}

}

}

說明 HashSet的代碼實際上很是簡單,經過上面的註釋應該很可以看懂。它是經過HashMap實現的,若對HashSet的理解有困難,建議先學習如下HashMap;學完HashMap以後,在學習HashSet就很是容易了。

  1. HashSet的遍歷方式
    1. 經過Iterator遍歷HashSet

第一步:根據iterator()獲取HashSet的迭代器

遍歷迭代器獲取各個元素

// 假設set是HashSet對象

for(Iterator iterator = set.iterator();

iterator.hasNext(); ) {

iterator.next();

}

  1. 經過for-each遍歷HashSet
  • 第一步:根據toArray()獲取HashSet的元素集合對應的數組
  • 遍歷數組,獲取各個元素

// 假設set是HashSet對象,而且set中元素是String類型

String[] arr = (String[])set.toArray(new String[0]);

for (String str:arr)

System.out.printf("for each : %s\n", str);

 

HashSet中添加元素,若是set中元素已存在,則返回false;若是不存在,則返回true

  1. TreeMapHashMap的區別和共同點

     

TreeMap

HashMap

TreeMap實現了SortMap接口,是基於紅黑樹的

HashMap實現了Map接口,是基於哈希散列表的

TreeMap默認按鍵的升序排序

HashMap隨機存儲

TreeMap的遍歷是Iterator按順序遍歷的

HahsMap的遍歷是Iterator隨機遍歷的

TreeMap鍵和值都不能爲空

HashMap鍵只能有一個null,值能夠有多個null

TreeMap插入刪除查找的效率比較低

HashMap插入刪除查找的效率比較高

非線程安全的

非線程安全的

 

  1. HashMapHashTable的區別

    HashSet中,元素都存到HashMap鍵值對的Key上面,而Value時有一個統一的值private static final Object PRESENT = new Object();

    當有新值加入時,底層的HashMap會判斷Key值是否存在(HashMap細節請移步深刻理解HashMap),若是不存在,則插入新值,同時這個插入的細節會依照HashMap插入細節;若是存在就不插入

HashMap

HashSet

HashMap實現了Map接口

HashSet實現了Set接口

HashMap存儲鍵值對

HashSet僅僅存儲對象,存儲的是鍵,他們的值是相同的。

使用pub()方法將元素放入map

使用add()方法將元素放入set

HashMap中使用鍵對象來計算hashcode值(不會返回truefalse

HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false

HashMap查找比較快,由於是使用惟一的鍵來獲取對象

HashSetHashMap來講比較慢

 

  1. HashMapHashTable的區別

HashMap

HashTable

HashMap是基於AbstractMap

HashTable基於Dictionary

HashMap能夠容許存在一個爲nullkey和任意個爲nullvalue

HashTable中的keyvalue都不容許爲null

HashMap時單線程安全的,多線程是不安全的

Hashtable是多線程安全的

HashMap僅支持Iterator的遍歷方式

Hashtable支持IteratorEnumeration兩種遍歷方式

Hashtable 的函數都是同步的,這意味着它是線程安全的。它的keyvalue都不能夠爲nullHashtable的方法都用synchronized來修飾,因此它是線程同步的。 
  1. Hashtable的遍歷:
    1. 遍歷Hashtable的鍵值對
  • 第一步:根據entrySet()獲取Hashtable的"鍵值對"的Set集合。
  • 經過Iterator迭代器遍歷"第一步"獲得的集合。
    
// 假設table是Hashtable對象 
// table中的key是String類型,value是Integer類型
Integer integ = null; 
Iterator iter = table.entrySet().iterator(); 
while(iter.hasNext()) { 
 Map.Entry entry = (Map.Entry)iter.next(); 
 // 獲取key
 key = (String)entry.getKey(); 
 // 獲取value
 integ = (Integer)entry.getValue(); 
}

 

  1. 經過Iterator遍歷Hashtable的鍵
  • 第一步:根據keySet()獲取Hashtable的"鍵"的Set集合。
  • 第二步:經過Iterator迭代器遍歷"第一步"獲得的集合。

// 假設table是Hashtable對象

// table中的key是String類型,value是Integer類型

String key = null;

Integer integ = null;

Iterator iter = table.keySet().iterator();

while (iter.hasNext()) {

// 獲取key

key = (String)iter.next();

// 根據key,獲取value

integ = (Integer)table.get(key);

}

 

  1. 經過Iterator遍歷Hashtable的值
  • 第一步:根據value()獲取Hashtable的"值"的集合。
  • 第二步:經過Iterator迭代器遍歷"第一步"獲得的集合

// 假設table是Hashtable對象

// table中的key是String類型,value是Integer類型

Integer value = null;

Collection c = table.values();

Iterator iter= c.iterator();

while (iter.hasNext()) {

value = (Integer)iter.next();

}

 

  1. 經過Enumeration遍歷Hashtable的鍵
  • 第一步:根據keys()獲取Hashtable的集合。
  • 第二步:經過Enumeration遍歷"第一步"獲得的集合

Enumeration enu = table.elements();

while(enu.hasMoreElements()) {

System.out.println(enu.nextElement());

}

 

  1. 經過Enumeration遍歷Hashtable的值
  • 第一步:根據elements()獲取Hashtable的集合。
  • 第二步:經過Enumeration遍歷"第一步"獲得的集合

Enumeration enu = table.elements();

while(enu.hasMoreElements()) {

System.out.println(enu.nextElement());

}

 

  1. ConcurrentHashMap的應用
  1. concurrentHashMap的優點

    首先經常使用的三種HashMap包括HashMapHashTableconcurrentHashMap

    1. HashMap在併發編程過程當中使用可能致使死循環,由於插入過程不是原子操做,每一個HashEntry是一個鏈表節點,極可能在插入的過程當中,已經設置了後節點,實際還未插入,最終反而插入在後節點以後,形成鏈中出現環,破壞了鏈表的性質,失去了尾節點,出現死循環。
    2. HashTable由於內部是採用synchronized來保證線程安全的,但在線程競爭激烈的狀況下HashTable的效率降低得很快由於synchronized關鍵字會形成代碼塊或方法成爲爲臨界區(對同一個對象加互斥鎖),當一個線程訪問臨界區的代碼時,其餘線程也訪問同一臨界區時,會進入阻塞或輪詢狀態。究其緣由,其實是有獲取鎖意向的線程的數目增長,可是鎖仍是隻有單個,致使大量的線程處於輪詢或阻塞,致使同一時間段有效執行的線程的增量遠不及線程整體增量。
    3. 在查詢時,尤爲可以體現出CocurrentHashMap在效率上的優點,HashTable使用Sychronized關鍵字,會致使同時只能有一個查詢在執行,而Cocurrent則不採起加鎖的方法,而是採用volatile關鍵字,雖然也會犧牲效率,可是因爲Sychronized,於該文末尾繼續討論。
    4. CocurrentHashMap利用鎖分段技術增長了鎖的數目,從而使爭奪同一把鎖的線程的數目獲得控制。
  • 鎖分段技術就是對數據集進行分段,每段競爭一把鎖,不一樣數據段的數據不存在鎖競爭,從而有效提升高併發訪問效率。
  • CocurrentHashMapget方法是無需加鎖的,由於用到的共享變量都採用volatile關鍵字修飾,巴證共享變量在線程之間的可見性(每次讀取都先同步緩存和內存,直接從內存中獲取值,雖然不是原子操做,但根據JAVA內存模型的happen before原則,對volatile字段的寫入操做先於讀操做,可以保證不會髒讀),volatile爲了讓變量提供線程之間的內存可見性,會禁止程序執行結果的重排序(致使緩存優化的效果下降)
  1. 深刻理解ConcurrentHashMap原理分析即線程安全問題
  1. ConcurrentHashMapHashTable的區別

HashTable put()源代碼

從代碼能夠看出來在全部put 的操做的時候都須要用 synchronized 關鍵字進行同步。而且key 不能爲空。

這樣至關於每次進行put 的時候都會進行同步10個線程同步進行操做的時候,就會發現當第一個線程進去其餘線程必須等待第一個線程執行完成,才能夠進行下去。性能特別差。

 

  1. ConcurrentHashMap詳解

    分段鎖技術ConcurrentHashMap相比 HashTable而言解決的問題就是它不是鎖所有數據,而是鎖一部分數據,這樣多個線程訪問的時候就不會出現競爭關係。不須要排隊等待了。

從圖中能夠看出來ConcurrentHashMap的主幹是個Segment數組。

它把區間按照併發級別(concurrentLevel),分紅了若干個segment。默認狀況下內部按併發級別爲16來建立。對於每一個segment的容量,默認狀況也是16

ConcurrentHashMap是由Segment數組和HashEntry數組組成.

Segment是一種可重入鎖,ConcurrentHashMap裏扮演鎖的角色;

HashEntry則用於存儲鍵值對數據.

一個ConcurrentHashMap裏包含一個Segment數組.

Segment的結構和HashMap相似,是一種數組和鏈表結構.

一個Segment裏包含一個HashEntry數組,每一個HashEntry是一個鏈表結構的元素,每一個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,

必須首先得到與它對應的Segment

這就是爲何ConcurrentHashMap支持容許多個修改同時併發進行,緣由就是採用的Segment分段鎖功能,每個Segment 都想的於小的hash table而且都有本身鎖,只要修改再也不同一個段上就不會引發併發問題

 

  1. HashMapHashTableConcurrentHashMap的區別
  1. HashMapConcurrentHashMap的區別
  • 他們之間的第一個重要的區別就是ConcurrentHashMap是線程安全的和在併發環境下不須要加額外的同步。
  • 你可使用Collections.synchronizedMap(HashMap)來包裝HashMap做爲同步容器,這時它的做用幾乎與Hashtable同樣,當每次對Map作修改操做的時候都會鎖住這個Map對象,而ConcurrentHashMap會基於併發的等級來劃分整個Map來達到線程安全,它只會鎖操做的那一段數據而不是整個Map都上鎖。
  • ConcurrentHashMap有很好的擴展性,在多線程環境下性能方面比作了同步的HashMap要好,可是在單線程環境下,HashMap會比ConcurrentHashMap好一點。
  1. ConcurrentHashMap vs Hashtable vs Synchronized Map區別

    雖然三個集合類在多線程併發應用中都是線程安全的,可是他們有一個重大的差異,就是他們各自實現線程安全的方式。

  • Hashtablejdk1的一個遺棄的類,它把全部方法都加上synchronized關鍵字來實現線程安全,全部的方法都同步這樣形成多個線程訪問效率特別低。
  • Synchronized MapHashTable差異不大,也是在併發中做相似的操做,二者的惟一區別就是Synchronized Map沒被遺棄,它能夠經過使用Collections.synchronizedMap()來包裝Map做爲同步容器使用。
  • ConcurrentHashMap的設計有點特別,表如今多個線程操做上。ConcurrentHashMap不須要鎖整個Map,相反它劃分了多個段(segments),要操做哪一段才上鎖那段數據。
相關文章
相關標籤/搜索