昨天写了一个多线程的程序,却发现了一个很奇特的问题,就是我的map对象明明put了,可是get的时候竟然会取到null,而且尝试多次,有时候成功,有时候取到null,并不确定。

程序代码如下:

  1. public class ThreadLocal {
  2. private static Map<Thread, Integer> map;
  3.  
  4. public static void main(String[] args) {
  5. map = new HashMap<Thread, Integer>();
  6. for (int i = 0; i < 2; i++) {
  7. new Thread(new Runnable() {
  8.  
  9. public void run() {
  10. int data = new Random().nextInt();
  11. map.put(Thread.currentThread(), data);
  12. System.out.println(Thread.currentThread() + ", data:"
  13. + data);
  14. new A().show();
  15. new B().show();
  16. }
  17. }).start();
  18. }
  19. }
  20.  
  21. static class A {
  22. public void show() {
  23. System.out.println(Thread.currentThread() + "调用A, data:" + map.get(Thread.currentThread()));
  24. }
  25. }
  26.  
  27. static class B {
  28. public void show() {
  29. System.out.println(Thread.currentThread() + "调用B, data:" + map.get(Thread.currentThread()));
  30. }
  31. }
  32. }

运行结果如下:

Thread[Thread-0,5,main], data:1164116165
Thread[Thread-1,5,main], data:196549485
Thread[Thread-0,5,main]调用A, data:null
Thread[Thread-1,5,main]调用A, data:196549485
Thread[Thread-0,5,main]调用B, data:null
Thread[Thread-1,5,main]调用B, data:196549485

我实在想不明白,我明明已经put了,为什么取不到呢?今天我查了资料,并大概看了看源码,大致理解了,这是由HashMap的非线程安全特性引起的。具体源码分析如下所示:

先看看get方法:

  1. public V get(Object key) {
  2.  
  3. if (key == null)
  4.  
  5. return getForNullKey();
  6.  
  7. int hash = hash(key.hashCode());
  8.  
  9. // indexFor方法取得key在table数组中的索引,table数组中的元素是一个链表结构,遍历链表,取得对应key的value
  10.  
  11. for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
  12.  
  13. Object k;
  14.  
  15. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
  16.  
  17. return e.value;
  18.  
  19. }
  20.  
  21. return null;
  22. }

再看看put方法:

  1. public V put(K key, V value) {
  2.  
  3. if (key == null)
  4.  
  5. return putForNullKey(value);
  6.  
  7. int hash = hash(key.hashCode());
  8.  
  9. int i = indexFor(hash, table.length);
  10.  
  11. for (Entry e = table[i]; e != null; e = e.next) {
  12.  
  13. Object k;
  14.  
  15. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  16.  
  17. V oldValue = e.value;
  18.  
  19. e.value = value;
  20.  
  21. e.recordAccess(this);
  22.  
  23. return oldValue;
  24.  
  25. }
  26.  
  27. }
  28.  
  29. modCount++;
  30.  
  31. // 若之前没有put进该key,则调用该方法
  32.  
  33. addEntry(hash, key, value, i);
  34.  
  35. return null;
  36. }

那我们再看看addEntry里面的实现:

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2.  
  3. Entry e = table[bucketIndex];
  4.  
  5. table[bucketIndex] = new Entry(hash, key, value, e);
  6.  
  7. if (size++ >= threshold)
  8.  
  9. resize(2 * table.length);
  10. }

map里的元素个数(size)大于一个阈值(threshold)时,map将自动扩容,容量扩大到原来的2倍;
阈值(threshold)是怎么计算的?如下源码:

threshold = (int)(capacity * loadFactor);

阈值 = 容量 X 负载因子;容量默认为16,负载因子(loadFactor)默认是0.75; map扩容后,要重新计算阈值;当元素个数大于新的阈值时,map再自动扩容;
以默认值为例,阈值=16*0.75=12,当元素个数大于12时就要扩容;那剩下的4(如果内部形成了Entry链则大于4)个数组位置还没有放置对象就要扩容,岂不是浪费空间了?
这是时间和空间的折中考虑;loadFactor过大时,map内的数组使用率高了,内部极有可能形成Entry链,影响查找速度;loadFactor过小时,map内的数组使用率旧低,不过内部不会生成Entry链,或者生成的Entry链很短,由此提高了查找速度,不过会占用更多的内存;所以可以根据实际硬件环境和程序的运行状态来调节loadFactor。

继续resize方法:

  1. void resize(int newCapacity) { //传入新的容量
  2. Entry[] oldTable = table; //引用扩容前的Entry数组
  3. int oldCapacity = oldTable.length;
  4.  
  5. if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
  6.  
  7. threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
  8. return;
  9.  
  10. }
  11.  
  12. Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组
  13. transfer(newTable); //!!将数据转移到新的Entry数组里
  14. table = newTable; //HashMap的table属性引用新的Entry数组
  15.  
  16. threshold = (int) (newCapacity * loadFactor); //修改阈值
  17. }

resize里面重新new一个Entry数组,其容量就是旧容量的2倍,这时候,需要重新根据hash方法将旧数组分布到新的数组中,也就是其中的transfer方法:

  1. void transfer(Entry[] newTable) {
  2.  
  3. Entry[] src = table; //src引用了旧的Entry数组
  4. int newCapacity = newTable.length;
  5.  
  6. for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
  7. Entry e = src[j]; //取得旧Entry数组的每个元素
  8. if (e != null) {
  9.  
  10. src[j] = null; //释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
  11.  
  12. do {
  13.  
  14. Entry next = e.next;
  15.  
  16. int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
  17.  
  18. e.next = newTable[i]; //标记[1]
  19.  
  20. newTable[i] = e; //将元素放在数组上
  21.  
  22. e = next; //访问下一个Entry链上的元素
  23. } while (e != null);
  24.  
  25. }
  26.  
  27. }
    }
注释标记[1]处,将newTable[i]的引用赋给了e.next,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾部(如果发生了hash冲突的话);
indexFor()是计算每个元素在数组中的位置,源码: 
static int indexFor(int h, int length) {
    return h & (length-1); //位AND计算
 }
这样,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上;
例如,旧数组容量为16,对象A的hash值是4,对象B的hash值是20,对象C的hash值是36;
通过indexFor()计算后,A、B、C对应的数组索引位置分别为4,4,4; 说明这3个对象在数组的同一位置上,形成了Entry链;
旧数组扩容后容量为16*2,重新计算对象所在的位置索引,A、B、C对应的数组索引位置分别为4,20,4; B对象已经被放到别处了;

所以,resize时,HashMap使用新数组代替旧数组,对原有的元素根据hash值重新就算索引位置,重新安放所有对象;resize是耗时的操作。

 
 
说回null的问题,在这个方法里,将旧数组赋值给src,遍历src,当src的元素非null时,就将src中的该元素置null,即将旧数组中的元素置null了,也就是这一句:
if (e != null) {  
        src[j] = null;  
}
此时若有get方法访问这个key,它取得的还是旧数组,当然就取不到其对应的value了。
 
 
下面,我们重现一下场景:
Java代码 
import java.util.HashMap;  
import java.util.Map;  
public class TestHashMap {  
    public static void main(String[] args) {  
        final Map map = new HashMap(4, 0.5f);  
        new Thread(){  
            public void run() {  
                while(true) {   
                    System.out.println(map.get("name1"));  
                    try {  
                        Thread.sleep(1000);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            }  
        }.start();  
        for(int i=0; i<3; i++) {  
            map.put("name" + i, "value" + i);  
        }  
Debug上面这段程序,在map.put处设置断点,然后跟进put方法中,当i=2的时候就会发生resize操作,在transfer将元素置null处停留片刻,此时线程打印的值就变成null了。
 
 
其它可能由未同步HashMap导致的问题:
1、多线程put后可能导致get死循环(主要问题在于put的时候transfer方法循环将旧数组中的链表移动到新数组)
2、多线程put的时候可能导致元素丢失(主要问题出在addEntry方法的new Entry(hash, key, value,e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失)
 
 
总结:HashMap在并发程序中会产生许多微妙的问题,难以从表层找到原因。所以使用HashMap出现了违反直觉的现象,那么可能就是并发导致的了。最简单的解决办法就是采用线程安全的HashTable。
 

由多线程引起的map取值为null的分析的更多相关文章

  1. Python_关于多线程下变量赋值取值的一点研究

    关于多线程下变量赋值取值的一点研究 by:授客 QQ:1033553122 1.代码实践1 #!/usr/bin/env python # -*- coding:utf-8 -*- __author_ ...

  2. @Value取值为NULL的解决方案------https://blog.csdn.net/zzmlake/article/details/54946346

    @Value取值为NULL的解决方案 https://blog.csdn.net/zzmlake/article/details/54946346

  3. java的map取值

    第一种方法根据键值的名字取值 import java.util.HashMap; import java.util.Map; /**   * @param args   */  public stat ...

  4. mybatis返回数据类型为map,值为null的key没返回

    创建mybatis-config.xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE ...

  5. ajax post请求request.getParameter("")取值为null

    今天在写提交一个json数据到后台,然后后台返回一个json数据类型.但是发现后台通过request.getParamter("")取到的值为null. 于是写一个简单的ajax ...

  6. Spring @Value取值为null或@Autowired注入失败

    @Value 用于注入.properties文件中定义的内容 @Autowired 用于装配bean 用法都很简单,很直接,但是稍不注意就会出错.下面就来说说我遇到的问题. 前两天在项目中遇到了一个问 ...

  7. @Value取值为NULL的解决方案

    在spring mvc架构中,如果希望在程序中直接使用properties中定义的配置值,通常使用一下方式来获取: @Value("${tag}") private String ...

  8. nacos作为配置中心动态刷新@RefreshScope添加后取值为null的一个问题

    之前springboot项目常量类如下形式: @Component @RefreshScope//nacos配置中心时添加上 public class Constants { @Value(" ...

  9. MyBatis resultType用Map 返回值中有NULL则缺少字段 返回值全NULL则map为null

    这个问题我大概花了2个小时才找到结果 总共需要2个设置 这里是对应springboot中的配置写法 @select("select sum(a) a,sum(b) b from XXX wh ...

随机推荐

  1. Android M新的运行时权限开发者需要知道的一切

    android M 的名字官方刚发布不久,最终正式版即将来临!android在不断发展,最近的更新 M 非常不同,一些主要的变化例如运行时权限将有颠覆性影响.惊讶的是android社区鲜有谈论这事儿, ...

  2. java问题小总结

    1.在使用equals的时候,把  "".equals(name);放在左边 如果右边的没有初始化,可以避免出错. 2.对于 ObjectId id; 在mongodb里面对其进行 ...

  3. 模拟placeholder支持ie8以下处理了密码框只读的问题

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. AWS CLI 中使用S3存储

    登录 通过控制面板, 在S3管理器中创建一个新的bucket 所有AWS服务 -> 安全&身份 -> IAM -> 组, 创建一个新的组, 例如 "s3-user& ...

  5. DLL放在指定目录 以及设置dll调用路径

    一.DLL放在指定目录 在编写C# winform程序中,不免一个项目会有多个工程文件,而这些工程文件之间是相互引用的,所以不想将工程的生成结果(exe或者dll)放在当前工程bin目录下的Debug ...

  6. Tomcat部署学习

    tomcat也可以称为catalina catalina_home就是tomcat安装路径:D:\Program Files\apache-tomcat-8.0.36\bin     windows下 ...

  7. 条件变量pthread_cond_t怎么用

    #include <pthread.h> #include <stdio.h> #include <stdlib.h> pthread_mutex_t mutex ...

  8. UDP的坏处

    众所周知,UDP是一个面向无连接的协议.通信时不可靠的.这就会出现一些问题 (1)数据报丢失 因为是无连接,的所以可以用recvfrom和sendto来接收和发送消息,如果socket是阻塞的,那么当 ...

  9. Spring 4.0.2 学习笔记(2) - 自动注入及properties文件的使用

    接上一篇继续, 学习了基本的注入使用后,可能有人会跟我一样觉得有点不爽,Programmer的每个Field,至少要有一个setter,这样spring配置文件中才能用<property> ...

  10. C# WebApi Xml序列化问题解决方法:“ObjectContent`1”类型未能序列化内容类型“application/xml;charset=utf-8"的响应正文。...

    在调试一个WebApi程序时,出现下面错误: 通过分析怀疑是未添加序列化属性引起的,实体类改为下面结构后,问题依旧: 通过查阅资料和不断尝试,修改实体类的属性注解搞定: