---恢复内容开始---
前言:大多数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行来看看:
以下是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个条件。
三:死锁的四个条件
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迥然不同,可以保证线程安全。