hashMap非线程安全问题和concurrenthashmap线程安全的比较

    concurrenthashmap这个是线程安全的,使用hashmap的线程不安全主要体现在多线程调
用时,其中一个线程修改了映射结构(如添加了一个映射或删除),内容的变化是不同步的,
其他线程再调用时数据内容就出现混乱了。


HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决
的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入


此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修
改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关
系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自
然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 
Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止
对映射进行意外的非同步访问,如下所示:


   Map m = Collections.synchronizedMap(new HashMap(...));
当多个线程都往一个HashMap里面put值的时候,如果HashMap的size大小在即将被占满时会
触发Resize()方法去重新计算HashMap容器的size大小,由于Resize()方法会重新new 
一个table去指向原来的HashMap容器,老的table会被丢弃掉。当恰巧(概率极低)有其他
线程往老的table里面put值的时候数据会丢失,在下一次需要用到这个数据时取到一个null
值,程序会在此时报空指针异常。


    concurrenthashmap是线程安全的(有加锁,实现同步效果),锁它引入了一个“分段锁”
的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来
决定把key放到哪个HashTable中。在ConcurrentHashMap中,就是把Map分成了N个Segment,
put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中,通过把整个Map分为
N个Segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认提升16
倍。


在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况
下,其性能下降很严重,此时ReentrantLock是个不错的方案。



***************************************关于HashMap非线程安全机制的说明***************************************************************************

1、关于HashMap
       public V put(K key, V value) {
      /******************************
      * 省略的代码:计算hash值,
* 确认key值是否已经存在,存在则直接替换并返回老值
      ******************************/
        addEntry(hash, key, value, i);
        return null;
    }
       上述put方法,在新插入一个key-value时,会调用addEntry方法完成数据的插入更新;
       void addEntry(int hash, K key, V value, int bucketIndex) {
   Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }
       addEntry方法在完成数据插入后,会判断当前数据量是否已经超过阈值,如果超过阈值,则调用resize方法将内部存储空间扩大至两倍;
       void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }


        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
}
resize方法是线程非安全的!上述标黄部分是造成线程非安全的最主要原因。考虑如下场景:
线程A正在执行table = newTable,线程B正在执行table[bucketIndex] = new Entry<K,V>(hash, key, value, e) ,则线程B新增的key-value插入到老的table中,没有生效,数据丢失了
2、关于HashMapCacheStrategy
   private Map cacheMap = new HashMap();
    private Map cacheTimeMap = new HashMap();
    public boolean isObsolete(Object cacheKey) {
        if (!cacheMap.containsKey(cacheKey))
            return true;
        
        Long prevTime = (Long) cacheTimeMap.get(cacheKey);
        
        return System.currentTimeMillis() - prevTime.longValue() > cacheObsoleteTime;
}
本次问题是在出现多线程同时对HashMap执行put操作,使得cacheTimeMap在put上次数据加载时间时数据丢失,但cacheMap在put上次刷新的数据却成功导致的。
所以上述代码中标黄部分获取的值是null,标绿部分对null进行\dot操作时报nullPointException,这会致使:
1. 每次获取缓存值的时候都抛出nullPointException;
2. 每次尝试刷新缓存值时,都会调用isObsolete方法判断是否缓存过期,而每次调用均抛异常导致数据始终无法刷新,致使错误无法自动恢复;


3、结论:
       1. HashMap非线程安全,当存在两个线程同时执行put操作,而HashMap存储数据量已接近阈值触发resize操作时,容易导致数据丢失;
       2. HashMap非线程安全,当存在一个线程put操作,一个线程get操作时,当数据量已接近阈值触发resize操作时,get操作可能返回null(和本次问题无关,可以自行研究源码了解)
       3. HashMapCacheStrategy本身非线程安全,当有多个缓存同时加载,导致内部cacheTimeMap的存储量接近阈值触发resize操作时,容易导致数据丢失,且该数据丢失不可恢复;
       4. 由于HashMap非线程安全是因resize操作导致,故在系统初始化时,因频繁resize HashMap的内部存储空间,所以比较容易发生1.中所述的线程问题,稳定运行后出现1.问题的概率会极度降低。


4、改进措施:
       1. 凡是涉及到多线程操作的Map,尽可能定义为ConcurrentHashMap,对于方法内的局部变量,可以使用HashMap
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值