<small id='u5SokPH'></small> <noframes id='dRYFp'>

  • <tfoot id='6ctzYVJf'></tfoot>

      <legend id='HPdm'><style id='X4r7H9ywJD'><dir id='f4w5'><q id='wDSumy'></q></dir></style></legend>
      <i id='gfis'><tr id='yZBUrnqN6f'><dt id='mSkMQL'><q id='eC9LkJnvVP'><span id='3sx9t'><b id='RLTF58hu'><form id='x5Tr'><ins id='q9v3'></ins><ul id='j5wGr8asq'></ul><sub id='IDOuEo'></sub></form><legend id='27GhqD1mNV'></legend><bdo id='KJHpFEOubY'><pre id='5HvM'><center id='F2MyoHvLpw'></center></pre></bdo></b><th id='BtGxOuYIhc'></th></span></q></dt></tr></i><div id='7wIyiSMN'><tfoot id='fMWDVp'></tfoot><dl id='nKvZ'><fieldset id='moNUC9'></fieldset></dl></div>

          <bdo id='R1qh'></bdo><ul id='g9PB7'></ul>

          1. <li id='ROq7khw'></li>
            登陆

            HashMap线程安全问题

            admin 2019-09-07 237人围观 ,发现0个评论

            HashMap是线程不安全的,在多线程环境下对某个目标中HashMap类型的实例变量进行操作时,或许会发生各种不符合预期的问题。

            本文具体阐明一下HashMap存在的几个线程安全问题。

            注:以下依据JDK1.8

            HashMap原理请走传送门

            经过简略例子来探究HashMap原理及内部存储结构

            1 多线程的put或许导致元素的丢掉

            1.1 实验代码如下

            注:仅作为或许会发生这个问题的样例代码,直接运转纷歧定会发生问题

            public class ConcurrentIssueDemo1 {
            private static Map map = new HashMap<>();
            public static void main(String[] args) {
            // 线程1 => t1
            new Thread(new Runnable() {
            @Override
            public void run() {
            for (int i = 0; i < 99999999; i++) {
            map.put("thread1_key" + i, "thread1_value" + i);
            }
            }
            }).start();
            // 线程2 => t2
            new Thread(new Runnable() {
            @Override
            public void run() {
            for (int i = 0; i < 99999999; i++) {
            map.put("thread2_key" + i, "thread2_value" + i);
            }
            }
            }).start();
            }
            }

            1.2 触发此问题的场景

            先来看一下put办法的源码

            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) {
            Node[] tab; Node p; int n, I;
            // 初始化hash表
            if ((tab = tableHashMap线程安全问题) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            // 经过hash值核算在hash表中的方位,并将这个方位上的元素赋值给p,假如是空的则new一个新的node放在这个方位上
            if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
            else {
            // hash表的当时index现已存在元素,向这个元素后追加链表
            Node e; K k;
            if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
            for (int binCount = 0; ; ++binCount) {
            // 新建节点并追加到链表
            if ((e = p.next) == null) {HashMap线程安全问题 // #1
            p.next = newNode(hash, key, value, null); // #2
            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
            break;
            }
            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;
            if (!onlyIfAbsent || oldValue == null)
            e.value = value;
            afterNodeAccess(e);
            return oldValue;
            }
            }
            ++modCount;
            if (++size > threshold)
            resize();
            afterNodeInsertion(evict);
            return null;
            }

            假定当时HashMap中的table状况如下:

            此刻t1和t2一起履行put,假定t1履HashMap线程安全问题行put(“key2”, “value2”),t2履行put(“key3”, “value3”),而且key2和key3的hash值与图中的key1相同。

            那么正常情况下,put完结后,table的状况应该是下图二者其一

            下面来看看异常情况

            假定线程1、线程2现在都履行到put源代码中#1的方位,且当时table状况如下

            然后两个线程都履行了if ((e = p.next) == null)这句代码,来到了#2这行代码。

            此刻假定t1先履行p.next = newNode(hash, key, value, null);

            那么table会变成如下状况

            紧接着t2履行p.next = newNode(hash, key, value, null);

            此刻table会变成如下状况

            这样一来,key2元素就丢了。

            2 put和get并发时,或许导致get为null

            场景:线程1履行put时,由于元素个数超出threshold而导致rehash,线程2此刻履行get,有或许导HashMap线程安全问题致这个问题。

            剖析如下:

            先看下resize办法源码

            大致意思是,先核算新的容量和threshold,在创立一个新hash表,最终将旧hash表中元素rehash到新的hash表中

            要点代码在于#1和#2两句

            // hash表
            transient Node[] table;
            final Node[] resize() {
            // 核算新hash表容量巨细,begin
            Node[] 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;
            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;
            // 核算新hash表容量巨细,end
            Node[] newTab = (Node[])new Node[newCap]; // #1
            // #2
            table = newTab;
            // rehash begin
            if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
            Node 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)e).split(this, newTab, j, oldCap);
            else { // preserve order
            Node loHead = null, loTail = null;
            Node hiHead = null, hiTail = null;
            Node 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;
            }
            }
            }
            }
            }
            // rehash end
            return newTab;
            }

            在代码#1方位,用新核算的容量new了一个新的hash表,#2将新创立的空hash表赋值给实例变量table。

            留意此刻实例变量table是HashMap线程安全问题空的。

            那么,假如此刻另一个线程履行get时,就会get出null。

            3 JDK7中HashMap并发put会构成循环链表,导致get时呈现死循环

            此问题在JDK8中现已处理。

            3.1 JDK7中循环链表的构成

            发生在多线程并发resize的情况下。

            相关源码如下:

            void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCa毒龙pacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
            }
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            table = newTable;
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
            }
            /**
            * Transfers all entries from current table to newTable.
            */
            // 关键在于这个transfer办法,这个办法的作用是将旧hash表中的元素rehash到新的hash表中
            void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry e : table) { // table变量即为旧hash表
            while(null != e) {
            // #1
            Entry next = e.next;
            if (rehash) {
            e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 用元素的hash值核算出这个元素在新hash表中的方位
            int i = indexFor(e.hash, newCapacity);
            // #2
            e.next = newTable[I];
            // #3
            newTable[i] = e;
            // #4
            e = next;
            }
            }
            }

            假定线程1(t1)和线程2(t2)一起resize,两个线程resize前,两个线程及hashmap的状况如下



            堆内存中的HashMap目标中的table字段指向旧的hash表,其间index为7的方位有两个元素,咱们以这两个元素的rehash为例,看看循环链表是怎么构成的。

            线程1和线程2别离new了一个hash表,用newTable字段表明。

            PS:假如将每一步的履行都以图的方式呈现出来,篇幅过大,这儿供给一下每次循环结束时的状况,能够依据代码一步一步计算。

            Step1: t2履行完#1代码后,CPU且走履行t1,而且t1履行完结

            这儿能够依据上图计算一下,此刻状况如下



            用t2.e表明线程2中的局部变量e,t2.next同理。

            Step2: t2持续向下履行完本次循环



            Step3: t2持续履行下一次循环



            Step4: t2持续下一次循环,循环链表呈现



            3.2 死循环的呈现

            HashMap.get办法源码如下:

            public V get(Object key) {
            if (key == null)
            return getForNullKey();
            Entry entry = getEntry(key);
            return null == entry ? null : entry.getValue();
            }
            final Entry getEntry(Object key) {
            if (size == 0) {
            return null;
            }
            int hash = (key == null) ? 0 : hash(key);
            // 遍历链表
            for (Entry e = table[indexFor(hash, table.length)];
            e != null;
            e = e.next) {
            Object k;
            // 假定这儿条件一向不成立
            if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
            }
            return null;
            }

            由上图可知,for循环中的e = e.next永久不会为空,那么,假如get一个在这个链表中不存在的key时,就会呈现死循环了。

            重视小编,小编会每天为你共享风趣的技术文章哦。悄悄告知你私信小编“学习”会有意想不到的惊喜哟~~!

          2. 章鱼彩票-中威电子9月19日快速反弹
          3. 5G赋能未来 中兴通讯助阵2019天翼智能生态博览会
          4. 请关注微信公众号
            微信二维码
            不容错过
            Powered By Z-BlogPHP