インタビューJava-HashMapとConcurrentHashMapのコレクション

序文

フレンドリーなリマインダー、この記事は多くの知識ポイントを含み、より多くの脳力を消費します。今後この記事が見つからなくなる可能性がある場合は、最初に収集する
ことをお勧めしますレビューする必要がない場合は、インタビューの冒頭に直接スキップできます。
以下をご覧ください。コードはすべてJDK8からのものです

面接の前に、復習しましょう

HashMapputメソッド

public V put(K key, V value) {
        //这里已经对key进行一次哈希了
        return putVal(hash(key), key, value, false, true);
    }
    //扰动函数,主要功能:降低哈希冲突(详细内容不展开)
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;//桶数组
    	Node<K,V> p; //节点
    	int n, i;
		      //如果table为空,也就是没初始化,或者已经被初始化了,但是数组长度为0,即不是2的幂次方
        if ((tab = table) == null || (n = tab.length) == 0)
	        	//给tab扩容,分配空间,初始值为n=16
            n = (tab = resize()).length;
		    //如果桶i位置上没有节点
        if ((p = tab[i = (n - 1) & hash]) == null)
			      //那么就直接,创建节点,然后把节点放在桶的i位置上
            tab[i] = newNode(hash, key, value, null);
		    //如果桶i位置上有节点,p是指向节点的引用
        else {
            Node<K,V> e;
            K k;
			      //如果hash相等,且key的内存地址或key的值相等。那就
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
				        //如果p是红黑树上的节点,那就把节点加到红黑树上
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
				        //遍历链表
                for (int binCount = 0; ; ++binCount) {
					          //如果后一个节点为null
                    if ((e = p.next) == null) {
						            //就把新节点放在p的后一个节点
                        p.next = newNode(hash, key, value, null);
						            //如果bincount>=8-1,就是bincount==8时,链表转变红黑色
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
					          //如果该节点的hash,key和准备加的节点相等。在后面会进行替换操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
			      //替换值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
				        //如果onlyIfAbsent为true,就不改变value的值
                if (!onlyIfAbsent || oldValue == null)
					          //改变value值
                    e.value = value;
                //留给LinkedHashMap的空方法
                afterNodeAccess(e);
				        //返回oldValue
                return oldValue;
            }
        }
        ++modCount;
		// map中的元素数量大于threshold时,就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

要約すると、putメソッドの一般的なフローは次のとおりです。

  1. キーをハッシュしてハッシュ値を取得します
  2. ハッシュ値の残りと配列の長さを取得し、その値はキーのインデックス値です
  3. 配列のインデックス位置がnullの場合は、直接挿入するだけです
  4. 配列のインデックス位置に値がある場合、次の3つのケースがあります。
  • ケース1:ノードのキーが挿入するキーと等しい場合は、値を直接置き換えるだけです
  • ケース2:ノードが赤黒木のノードに属している場合は、赤黒木の更新または挿入の方法に従ってください。
  • ケース3:ノードがリンクリストのノードに属している場合は、リンクリストをトラバースして対応するノードを見つけ、値を置き換えるだけです。対応するノードが見つからない場合は、リンクリストの末尾に新しいノードを挿入します。

これは単なる一般的な説明です。具体的な実装の詳細については、ソースコードを直接確認してください。

HashMapのgetメソッド

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    //扰动函数,和put方法中使用的是同一个hash方法
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab;
        Node<K,V> first, e;
        int n;
        K k;
        //如果数组不为空,且数组的长度大于0,且头结点不为空;否则直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            //  如果头结点的哈希值和key的哈希值相等,且key的地址或key的内容相等;就直接返回头结点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
                //头结点不相等,如果有下一个节点,就遍历;如果没下一个节点,就返回null
            if ((e = first.next) != null) {
                //如果节点是红黑树,那么就按照红黑树的方式来获取节点,并返回
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //如果是链表,就遍历哈希值,key的地址或Key的内容,有符合条件的就立即返回,如果没,那么继续遍历下一个节点。如果遍历全部节点后,都没,那就返回null
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

要約すると、HashMapのgetメソッドの一般的なプロセスは次のとおりです。

  1. キーに応じてハッシュ値を取得します
  2. ハッシュ値と配列の長さに応じて余りを取り、インデックス値を取得します
  3. インデックス値に従って、対応するノードを取得し、ノードのキーとハッシュを比較します
  4. それらが等しい場合は、対応するノードを返します。等しくない場合は、トラバースを続行します。最後までトラバーサルがない場合は、nullを返します。

これは単なる一般的な説明です。具体的な実装の詳細については、ソースコードを直接確認してください。

ConcurrentHashMapのputメソッド

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

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //判空:key、value均不能为null
        if (key == null || value == null) throw new NullPointerException();
        //计算出hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        //遍历table
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // table为null,进行初始化工作
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果i位置没有节点,则直接插入,不需要加锁
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //CAS
                if (casTabAt(tab, i, null,
                        new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果有线程正在进行扩容操作,则先帮助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //对该节点进行加锁处理(hash值相同的链表的头节点),对性能有点儿影响
                //特别注意一下这个f,这个f是头结点,锁的粒度是节点
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //fh > 0 表示为链表,将该节点插入到链表尾部
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //hash 和 key 都一样,替换value
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    //putIfAbsent()
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //链表尾部  直接插入
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        //树节点,按照树的插入操作进行插入
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    // 如果链表长度已经达到临界值8 就需要把链表转换为树结构
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }

        //size + 1
        addCount(1L, binCount);
        return null;
    }

要約すると、ConcurrentHashMapのputメソッドの一般的なフローは次のとおりです。

  1. まず、nullと判断され、キーも値もnullにすることはできません。(補足知識1を参照)
  2. 次に、ハッシュ値を計算します。(補足知識4を参照)次のことについて話します。
  3. 次に、テーブルをトラバースしてノード挿入操作を実行します。具体的なプロセスは次のとおりです。
  • テーブルが空の場合は、ConcurrentHashMapが初期化されておらず、初期化操作が実行されていることを意味します。initTable()
  • ハッシュ値に従ってノードの位置iを取得します。位置が空の場合は、直接挿入します。このプロセスをロックする必要はありません。f位置を計算します:i =(n-1)&ハッシュ。(補足知識2を参照)
  • fh = f.hash == -1であることが検出された場合、fはForwardingNodeノードであり、他のスレッドが拡張操作を実行していることを示し、スレッドが一緒に拡張操作を実行するのを支援します。(補足知識3を参照)
  • f.hash> = 0がリンクリスト構造であることを意味する場合、リンクリストがトラバースされ、現在のキーノードがある場合は値が置き換えられます。それ以外の場合は、リンクリストの最後に挿入されます。fがTreeBinタイプのノードの場合、赤黒木法に従ってノードを更新または追加します。
  • リンクリストの長さが> TREEIFY_THRESHOLD(デフォルトは8)の場合、リンクリストは赤黒木構造に変換されます。

ConcurrentHashMapのgetメソッド

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 先计算hash
    int h = spread(key.hashCode());
    //如果数组不为空,且长度大于0,且节点不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        // 搜索到的节点key与传入的key相同且不为null,直接返回这个节点
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 树
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 链表,遍历
        while ((e = e.next) != null) {
            if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

要約すると、ConcurrentHashMapのgetメソッドの一般的なフローは次のとおりです。

  1. 最初にハッシュ値hを計算します
  2. (n-1)&h)でインデックス値を取得します
  3. 一致がヘッドノードの場合、対応する値を直接返します
  4. ツリーの場合は、赤黒木の読み取り操作に応じた戻り値
  5. リンクリストの場合は、マッチング、トラバーサルを実行し、対応する値を取得します

補足知識

  • 1. HashMapでは、キーと値の両方をnullにすることができます。
  • 2.このプロセスでは、CASを使用します。これは、ロック、スピンロックがあると理解できます。Synchronizedのようなオペレーティングシステムレベルのロックとは異なり、CASはハードウェア命令であるため、ロックフリーと理解することもできます。したがって、錠があるかどうかにかかわらず、慈悲深い人は慈悲深い人を見て、賢い人は知恵を見る。自分で理解してください。
  • 3. ForwardingNode:ハッシュ値が-1の特別なノードノードで、nextTableへの参照を格納します。テーブルが展開された場合にのみ、ForwardingNodeは、現在のノードがnullであるか移動されたことを示すためにテーブルに配置されるプレースホルダーとして、役割を果たします。
  • 4. HashMapのハッシュ関数と比較すると、これにはシフト操作があり、わずかに異なりますが、目的はハッシュの競合を減らすことです。

ええと、HashMapとConcurrentHashMapのgetメソッドとputメソッドを読んだ後、あなたはすでにとても疲れていると思います。私もとても疲れて痛い書きをしているからです。今、あなたは私のハードワークを慰めるために私に褒め言葉を与えることができます;後でそれをレビューできるように最初にそれを集めるのが最善です。疲れたら、水を飲めるように注文して見て、食後に戻ってきたら見てください。文章が良いと思うなら、それを共有して友達に転送してください。

広告は終わりました!続けます!インタビューが正式に始まります!

インタビュー開始

インタビュアー:HashMapのputメソッドのプロセスについて話していただけますか?

  • バラバラはたくさんあります。上記の一般的なプロセスを参照してください。繰り返しません。

インタビュアー:HashMapのリンクリストはどのような条件下でツリーになりますか?

  • リンクリストがツリーになるように、リンクリストの長さが8以上、HashMapの容量が64以上であるという2つの条件を満たす必要があります。リンクリストの長さが8以上で、HashMapの容量が64未満の場合、拡張操作が実行されます。拡張操作後、リンクリストの長さはそれに応じて短縮されます。(関連するソースコードは次のとおりです:)
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
            
            ...省略一堆代码

インタビュアー:HashMapのリンクリストの変数ツリーに8つのノードがあるのはなぜですか?

  • これは統計的な質問であることをインタビュアーに直接伝えます。公式にテストされた、ノードが8つを超える場合、競合の確率は1,000万分の1未満です。(気をつけるために、ソースコードからコメントを取りました)
    * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million //小于千万分之一

インタビュアー:HashMapのJDK7とJDK8の違いは何ですか?

  • JDK7でハッシュ競合が発生すると、リンクリストがどんどん長くなり、時間計算量がO(N)になります。逆に、JDK 8でハッシュ競合が発生すると、リンクリストが赤黒になります。特定の条件下でツリーを作成すると、時間計算量はO(LogN)になります。
  • JDK7は同時実行性の高い環境にあり、スレッドが安全ではないため、putメソッドを操作すると、拡張サイズ変更メソッドとヘッダー挿入メソッドのためにリンクリストがループし、cpu100%のセキュリティ問題が発生します。 getメソッド。JDK 8はスレッドセーフではありませんが、ヘッド挿入方法をテール挿入方法に変更しても、サイズ変更時にリンクリストがリングになりません。
  • 要約すると、より重要な違いは赤黒木の導入です。インタビュアーは、二分木の検索など、他のツリーの代わりに赤黒木が導入される理由を尋ねる場合があります。実際、それは主にデータ構造の理解を調査することです。インタビューの前に、読み取りと書き込みの時間計算量、およびバランスの取れた木、赤黒木、および二分木を検索します。

インタビュアー:SynchronizedMapとConcurrentHashMapの違いは何ですか?

  • SynchronizedMap
    は、スレッドの安全性を確保するために一度にテーブル全体をロックするため、一度に1つのスレッドのみがマップにアクセスできます。
  • ConcurrentHashMap
    は、CAS + Synchronizedを使用して、スレッドセーフを確保します。SynchronizedMapに関連します。ConcurrentHashMapはノードをロックし、SynchronizedMapはテーブル全体をロックします。これは、MySQLの行ロックやテーブルロックと比較できます。ConcurrentHashMapロックの粒度は小さくなります。
    さらに、ConcurrentHashMapは異なる反復法を使用します。この反復法では、イテレータの作成後にコレクションが変更された場合、ConcurrentModificationExceptionはスローされなくなります。代わりに、イテレータが変更されたときに新しいデータが元のデータに影響を与えることはありません。イテレータの完了後にヘッドポインタが変更されます。 。新しいデータに置き換えて、イテレータスレッドが古いデータを使用できるようにし、ライタースレッドも同時に変更を完了できるようにします。

インタビュアー:ConcurrentHashMapを読み取るためにロックする必要がないのはなぜですか?

  • この点に関しては、実際にはJDK7とJDK8のConcurrentHashMapを比較する必要があります。

  • JDK7以前

    • HashEntryのキー、ハッシュ、およびnextはすべて最終的なものであり、ヘッダーのみをノードに挿入または削除できます。
    • HashEntryの値は揮発性です。
    • キーと値としてnullを使用することは許可されていません。リーダースレッドがHashEntryの値フィールドの値をnullとして読み取ると、競合が発生したことがわかります-並べ替え現象が発生しました(putメソッドはのバイトコード命令を設定します新しい値オブジェクト)並べ替え)、ロック後に値を再読み取りする必要があります。
    • 揮発性変数のカウントは、読み取りスレッドと書き込みスレッドの間のメモリの可視性を調整します。カウントは書き込み操作の後に変更され、読み取り操作は最初にカウントを読み取ります。推移性の前に発生する原則に従って、書き込み操作の変更された読み取り操作見られます。
  • JDK8では

    • Nodeのvalとnextはどちらも揮発性タイプです。
    • tabAt()メソッドとcasTabAt()メソッドに対応するUnsafe操作は、揮発性セマンティクスを実装しているため、命令の並べ替えを禁止でき、Null値の読み取りについて心配する必要はありません。
    static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;
          volatile V val; 
          volatile Node<K,V> next;
    
          Node(int hash, K key, V val, Node<K,V> next) {
              this.hash = hash;
              this.key = key;
              this.val = val;
              this.next = next;
          }
    

    インタビュアー:インタビューは終わりました。次のインタビューに参加しておめでとうございます。

総括する

実際、Javaコレクションに関する多くのインタビューの質問があります-HashMapとConcurrentHashMapは、ここでは展開されません。

  • ConcurrentHashMapのイテレータは強いですか弱いですか?HashMapはどうですか?
  • HashMapはいつ拡張を開始しますか?拡大する方法は?
  • ConcurrentHashMap 7と8の違いは?

トーク

これを見ていただきありがとうございます。記事がうまく書かれていると思われる場合は、それに注意を払い、共有してください(私にとって非常に便利です)。
記事を改善する必要があるとお考えの場合は、ご提案をお待ちしております。メッセージを残してください。
あなたが何かを見たいのなら、私はあなたのメッセージを楽しみにしています。
あなたのサポートとサポートは私の創造の最大の動機です!

参照

  • 太郎のソースコード
  • XiaoMing兄弟-JUCのJava並行コンテナ

おすすめ

転載: blog.csdn.net/Aaron_Tang_/article/details/114703724