zoukankan      html  css  js  c++  java
  • 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迥然不同,可以保证线程安全。

  • 相关阅读:
    内核笔记之内存寻址
    熟悉了HDFS的基本架构,了解这方面的专业术语(持续更新)
    第一步:ubuntu下android实验环境的建立,以及真机调试环境搭建
    关于hadoop的一些研究优化方向
    第三步:熟悉android的相关控件
    第一步系统环境的搭建
    第二步:关于布局文件中的大小设置使用问题
    Hibernate笔记
    Java学习之路——用dom4j解析xml
    利用Servlet和jsp实现客户端与服务器端的用户登录信息验证
  • 原文地址:https://www.cnblogs.com/wyq178/p/8676655.html
Copyright © 2011-2022 走看看