HashMap多线程并发的问题

---恢复内容开始---

前言:大多数javaer都知道HashMap是线程不安全的,多线程环境下数据可能会发生错乱,一定要谨慎使用。这个结论是没错,可是HashMap的线程不安全远远不是数据脏读这么简单,它还有可能会发生死锁,造成内存飙升100%的问题,情况十分严重(别问我是怎么知道的,我刚把机器重启了一遍!)今天就来探讨一下这个问题,HashMap在多线程环境下究竟会发生什么?

一:模拟程序

温馨提示:咳咳,以下代码需在家长陪同下使用,非战斗人员请速速退场,否则带来的一切后果请自己负责!

言归正传,我们先来写个程序先:

import java.util.HashMap;
import java.util.Map;

/**
 * Created by Yiron on 3/30 0030.
 */
public class HashMapManyThread {

    static Map<String,String > map =new HashMap<String, String>(16);//初始化容量

    public static  class TestHashMapThread implements Runnable{

        int start=0;

       public TestHashMapThread(int start){

           this.start=start;
        }

        @Override
        public void run() {

            for (int i = 0; i <100000 ; i+=2) {

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

                map.put(Integer.toString(i),String.valueOf(Math.random()*100));
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        
        Thread[] threads =new Thread[100];

        for (int i = 0; i <threads.length ; i++) {
            
            threads[i]=new Thread(new TestHashMapThread(i));
            
        }


        for (int i = 0; i <100 ; i++) {
            
            threads[i].start();
        }

        System.out.println(map.size());
    }
}

上面的程序开了100个线程去访问给HashMap去put不同的值,如果是线程安全的,最后肯定会输出5000,可惜事与愿违,在尝试了几次以后,竟然程序给卡死了,紧接着打开任务管理器,发现cpu飙升至100%,而内存使用也有88%,简直丧心病狂!无奈下只能重启!

二:原因分析

 在cmd中打开,然后输入jps,可以查看所有的java进程,然后可以看到所有的线程都在运行中,一直在无限循环状态,可以看到抛异常在at java.util.HashMap.put(HashMap.java:374)行,我们打开374行来看看:

 

以下是put方法的源码:

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) { //374行
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

可以看到这里是遍历数组的过程,其中遍历它的元素过程中,有一个e.next也就是指针往下移动,这里就很容易出现问题了。假如我们有两个线程Thread1和Thread2,假如在遍历的过程中,Thread1此时在链表的节点上e1,next指针会下一层指向e2;而此时Thread2遍历在e2节点上,它往回遍历next指针指向e1,那么此时的链表结构就被破坏了,形成了双向指针,构成了一个闭环(如图所示),就造成“死锁了”,我们来复习一下造成死锁的4个条件。

 

三:死锁的四个条件

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

 我们来分析一下链表的互相引用符不符合上面四个条件:

①互斥条件:链表上的节点同一时间此时被两个线程占用,两个线程占用访问节点的权利,符合该条件

②请求和保持条件:Thread1保持着节点e1,又提出了占用节点e2(此时尚未释放e2);而Thread2此时占用e2,又提出了占用节点e1,Thread1占用着Thread2接下来要用的e1,而Thread2又占用着Thread1接下来要用的e2,符合该条件

③:不剥夺条件:线程是由自己的退出的,此时并没有任何中断机制(sleep或者wait方法或者interuppted中断),只能由自己释放,满足条件

④:环路等待条件:e1、e2、e3等形成了资源的环形链条,满足该条件

五:解决方法

5.1:使用Collections.synchronizedMap(Map map)方法,可以将HashMap变成一个同步的容器(拥有锁限制的同步机制)

 static Map<String,String > map = Collections.synchronizedMap(new HashMap<String, String>());

 

 synchronizedMap这个方法的原理的话,其实是把这个参数里面的hashMap注入到Collections的内部维护着的一个成员变量Map中,


final Object   mutex;
public V put(K key, V value) {
        synchronized(mutex) {return m.put(key, value);}
 }

其中的mutex,是个不可变的成员变量,通过synchronized这个同步锁块就把整个代码锁住了,从而加上了同步规则。这个方法优点是简单粗暴,缺点就是性能不是很好,因为是阻塞的方式。

5.2:使用concurrentHashMap

static Map<String,String > map = new ConcurrentHashMap<String, String>();

这个方式是使用ConcurrentHashMap,它是线程安全的,

  public V put(K key, V value) {
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key.hashCode());
        return segmentFor(hash).put(key, hash, value, false);
    }

 V put(K key, int hash, V value, boolean onlyIfAbsent) {
            lock();//上锁
            try {
                int c = count;
                if (c++ > threshold) // ensure capacity
                    rehash();
                HashEntry<K,V>[] tab = table;
                int index = hash & (tab.length - 1);
                HashEntry<K,V> first = tab[index];
                HashEntry<K,V> e = first;
                while (e != null && (e.hash != hash || !key.equals(e.key)))
                    e = e.next;

                V oldValue;
                if (e != null) {
                    oldValue = e.value;
                    if (!onlyIfAbsent)
                        e.value = value;
                }
                else {
                    oldValue = null;
                    ++modCount;
                    tab[index] = new HashEntry<K,V>(key, hash, first, value);
                    count = c; // write-volatile
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

可以看到,concurrentHashMap的put方法是加锁的,它是同步的(采用了ReentrantLock可重入锁),可以保证线程安全。

六:总结

     本文分析了HashMap在并发环境下的严重的问题,并没有我们想象中的那么轻易和简单,会造成的严重的cpu飙升问题,从而产生内存泄露,所以在多线程的环境下一定要慎重慎重!最好不要用,可以取而代之用ConcurrentHashMap,它的内部数据结构与HashMap迥然不同,可以保证线程安全。

 

posted @ 2018-03-30 20:30  Yrion  阅读(20167)  评论(4编辑  收藏  举报