スケッチのHashMap
Javaコレクションの簡単な知識を終える前に、HashMapのようにHashMapを整理するために専門知識を、いくつかの単語を理解することができます話すが見つかりません。
HashMapのストレージ構造
データ構造のマップHashMapのタイプは、内部があれば、エントリタイプのアレイではなく、ハッシュテーブルの実装です。エントリタイプは、多くの場合、ペアを言われています。
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
あなたが必要も表2のn乗から長さを定義したい場合はHashMapのソースでは、16のデフォルトの長さは、テーブルを初期化します。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
ハッシュマップでは、ノードエントリタイプは、キーと値のペアに加えて、実装クラスであり、同様にハッシュ値と次のポインタ。ハッシュテーブル、描画することは困難ではない、すべての位置は、ノードのリストを保持することができます。
その限られた量のデータ、のいずれかより良いクエリのパフォーマンスという、ハッシュテーブルの長さを短くするだけでなく、スペースの無駄を減らすために、このような構造を持ちます。
大量のデータだけでなく、ハッシュテーブルのデフォルトの長さを使用する場合は、それは長いリスト、クエリのパフォーマンスにつながります。幸いHashMapのは、動的な拡張によってサポートされています。
操作の動作原理にはHashMap
HashMapのストレージ構造を学んだ後、私たちの前に2つの問題があります。まず、HashMapのは、要素を挿入するテーブルにどのような位置を決定する方法、二つ目は行くには、リスト内のどの位置に挿入されています。
まず最初の質問に答えるために、HashMapのハッシュコードは、まず、オブジェクトを取得で行うには剰余演算、ハッシュコード%の長テーブルで長いテーブルをハッシュコードになる、結果の値は、テーブルの代わりに挿入される要素です。
2番目の質問への答えは、HashMapのは、新しい要素がリストの先頭に挿入されて頭に補間が使用されますです。
ここで特殊なケースであり、HashMapのキー支持体としてヌルであるが、ヌルハッシュコードを得ることができないので、強制的にゼロの位置にnullに挿入しました。
//HashMap哈希函数的实现
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//
おさるのHashMapのハッシュ関数が働き、なぜハッシュテーブルの長さは2人の学生のn乗である必要があり、次の回答を参照することができます。
HashMapのJDKのソースコードは、原則として、ハッシュ法は何ですか? - 脂肪6月の答え - ほとんど知っています
だから、ときHashMapのは、2つのステップの要素を見つけます。
- ハッシュテーブル内の要素の位置が算出されます
- 要素を見つけるために、リストをトラバース
HashMapの展開
私たちは、長すぎるクエリの効率の低下、片方向リンクリストにさえ縮退を、デフォルトのHashMapのハッシュテーブルの長さで使用されるデータの膨大な量であれば、上記のリストをリードします。幸いなことにHashMap自体が動的拡張をサポートし、HashMapのは、動的にはHashMapの検索効率とスペース効率を保証できるようにするためには、ハッシュテーブルの長さのサイズを調整するために、データの量に基づいて行われます。
いくつかの重要なパラメータを導入します:
- 容量:ハッシュテーブルの長さは、デフォルトは16です。2のn乗のために保証されなければならない注
- サイズ:キーと値のペアの数
- 閾値:サイズが閾値よりも大きい場合、閾値は、拡張のために必要です
- loadFactor:負荷率、ハッシュテーブルの比率を用いることができ、閾値=(INT)(容量* loadFactor)
LoadFactor初期長さとハッシュテーブルは、初期化のHashMap、loadFactorデフォルト値0.75Fに定義されています。新しい要素を挿入するときに、拡張トリガ条件としきい値の大きさを確認し、大きさが閾値よりも大きい場合、それは拡大をトリガーします。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;//如果表长已经超过最大值,就会将threshold设置成一个很大的数来阻止HashMap继续扩容
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//下面是将旧表中的数据转移到新表中去,使用了两个嵌套的循环,开销非常大
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
計算アレイ容量
入ってくるn番目の電力容量が2でない場合は、HashMapを作成すると、HashMapのは、自動的に2 ^ nに変換します。
木々は赤と黒のリストを回します
8つ以上の鎖長場合JDK 8を開始、リストは赤黒木に変換されます。
ハッシュテーブルと比較すると
- ハッシュテーブルが同期して動作する同期使用して
- HashMapのキーを挿入することができ、ヌルエントリです
- あなたは、要素や反復拡張を追加することはできませんのHashMap
- 時間が経つにつれて、HashMapの中の要素の順番は変更される場合があります
ConcurrentHashMapの
ストレージ構造
ConcurrentHashMapのはハッシュマップに比べ、ストレージ構造が類似しているが、ConcurrentHashMapのロックセグメントの導入は、各セグメントが異なるスレッドが同時にアクセス異なるセグメントは、ハッシュテーブルをロックすることができるように、テーブルのロックを維持します。大幅にアクセス効率を向上させることができます。デフォルトのロックセグメント16。
サイズ操作
各セグメントは、セグメント内のキーと値のペアの数をカウントするカウント変数を保持します。
/**
* The number of elements. Accessed only either within locks
* or among other volatile reads that maintain visibility.
*/
transient int count;
サイズを実行するとき、あなたは、すべてのセグメントを横断し、カウントをアップ追加する必要があります。
二つの連続した結果が同じ操作で得られた場合にはロックを解除ロックされていないをしようとする操作の実装におけるConcurrentHashMapのサイズは、あなたは、この結果が正しいことを考えることができます。
定義使用するRETRIES_BEFORE_LOCK試み、値2は、-1の初期値を再試行し、したがって3しようとします。
3回以上の試行回数場合は、セグメントごとにロックする必要があります。
/**
* Number of unsynchronized retries in size and containsValue
* methods before resorting to locking. This is used to avoid
* unbounded retries if tables undergo continuous modification
* which would make it impossible to obtain an accurate result.
*/
static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
// 超过尝试次数,则对每个 Segment 加锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 连续两次得到的结果一致,则认为这个结果是正确的
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
変更点JDK 8
JDK 1.7は、同時更新操作、セグメントコアのクラスを実装するために使用されるロック機構をセグメント化し、それはReentrantLockの重量のロックを継承し、同時セグメントの数に等しいです。
JDK 1.8用途CAS操作内蔵のCAS操作が失敗したときに、同期ロックを使用し、同時実行の高い学位をサポートします。
赤黒木に変換し、JDK 1.8を実現すると、リストが長すぎます。
Laidakedःashanap
ストレージ構造
LinkedHashMapは、したがって、すぐに同じ特性を見つけるのHashMap、HashMapのから継承されました。
加えて、内部のLinkedHashMapまた、二重リンクリストデータを順次最低使用頻度(LRU)順序挿入または記録されている維持します。
記録の順序を決定accessOrderは、デフォルトはfalse、記録の挿入順序です。
/**
* The iteration ordering method for this linked hash map: {@code true}
* for access-order, {@code false} for insertion-order.
*
* @serial
*/
final boolean accessOrder;
リストを維持するために、次の2つのメソッドを使用してのLinkedHashMap
- afterNodeInsertion
- afterNodeAccess
afterNodeInsertion
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
置く他の操作が行われた後、リターン真removeEldestEntry()メソッドが最初に最新のノード、すなわち、ノードリスト・ヘッダを削除したとき。
地図をfalseに構築された唯一の追い出しは、ここに真です。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
removeEldestEntry()デフォルトはfalse、あなたはそれが真実にする必要がある場合は、十分なバッファスペースを確保するように、必要性は、最低使用のノードを削除することで、カバレッジのLinkedHashMapとLRUキャッシュが達成で特に有用であるこのメソッドの実装を拡張するために、そして、キャッシュされたデータがホットデータです。
afterNodeAccess
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
accessOrderがtrueの場合、移動ノードのアクセス・ノードの後にノード最近の訪問になることを保証するために、尾、リストの尾を一覧表示し、その後、ヘッダは最低使用ノードでリストアップ。
LRUキャッシュ
継承のLinkedHashMapはすぐにLRUキャッシュを実装することができ、次のようにLRUの例です。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 3;
//重写removeEldestEntry方法使元素数量大于3时将最近最久未使用的元素移除
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>();
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
cache.get(1);//访问1使(1,"a")重新置于链表尾部
cache.put(4, "d");//加入4使数量大于3而将最近最少使用的2移除
System.out.println(cache.keySet());
}
[3, 1, 4]
WeakHashMap
ストレージ構造
エントリのWeakHashMapには弱い参照から継承された次のガベージコレクションは、関連するオブジェクトの弱い参照するとき、それはリサイクルされます。
WeakHashMap主のWeakHashMap、JVMによってキャッシュされた回復のこの部分を使用して、オブジェクトを参照するために、キャッシュをキャッシュを実装するために使用されます。
ConcurrerntCache
ConcurrentCacheのWeakHashMapでTomcatはキャッシュ機能を実装するために使用されます。
ConcurrentCacheは、世代キャッシュを採用しました。
- エデンで頻繁に使用されるオブジェクト、エデン使用ConcurrentHashMapの実装、(エデンの園)回収されるの恐れなしに、
- 一般的に長期的にオブジェクトを使用していない、長期の使用は、これらの古い目的を達成するためのWeakHashMapゴミが収集されます。
- あなたは()メソッドを取得呼び出すと、アクセスノードは、多くの場合、容易にリサイクルされていないことを確認するために、エデンにオブジェクトを配置する長期から得られたときに、長期を取得する言葉や、その後を見つけていない場合、それは、エデン地区の取得を開始します。
- あなたが置く()メソッドを呼び出すときのサイズが大き上エデン場合ので、すべてのオブジェクトがエデン長期に配置されているだろう、、、オブジェクトの一部オフ状態の仮想マシンの復旧の使用は、頻繁に使用されていません。
public final class ConcurrentCache<K, V> {
private final int size;
private final Map<K, V> eden;
private final Map<K, V> longterm;
public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
}
public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
v = this.longterm.get(k);
if (v != null)
this.eden.put(k, v);
}
return v;
}
public void put(K k, V v) {
if (this.eden.size() >= size) {
this.longterm.putAll(this.eden);
this.eden.clear();
}
this.eden.put(k, v);
}
}