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

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

一:模拟程序

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

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

import java.util.HashMap;import java.util.Map;/** * Created by Yiron on 3/30 0030. */public class HashMapManyThread {    static Mapmap =new HashMap(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[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行来看看:

Java多线程对map操作 多线程用map_java

以下是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 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个条件。

Java多线程对map操作 多线程用map_Java多线程对map操作_02

三:死锁的四个条件

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[] tab = table;                int index = hash & (tab.length - 1);                HashEntry first = tab[index];                HashEntry 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(key, hash, first, value);                    count = c; // write-volatile                }                return oldValue;            } finally {                unlock();            }        }

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

六:总结

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