由多线程引起的map取值为null的分析
昨天写了一个多线程的程序,却发现了一个很奇特的问题,就是我的map对象明明put了,可是get的时候竟然会取到null,而且尝试多次,有时候成功,有时候取到null,并不确定。
程序代码如下:
public class ThreadLocal {
private static Map<Thread, Integer> map;
public static void main(String[] args) {
map = new HashMap<Thread, Integer>();
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
public void run() {
int data = new Random().nextInt();
map.put(Thread.currentThread(), data);
System.out.println(Thread.currentThread() + ", data:"
+ data);
new A().show();
new B().show();
}
}).start();
}
}
static class A {
public void show() {
System.out.println(Thread.currentThread() + "调用A, data:" + map.get(Thread.currentThread()));
}
}
static class B {
public void show() {
System.out.println(Thread.currentThread() + "调用B, data:" + map.get(Thread.currentThread()));
}
}
}
运行结果如下:
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方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
// indexFor方法取得key在table数组中的索引,table数组中的元素是一个链表结构,遍历链表,取得对应key的value
for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
再看看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) {
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++;
// 若之前没有put进该key,则调用该方法
addEntry(hash, key, value, i);
return null;
}
那我们再看看addEntry里面的实现:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
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方法:
void resize(int newCapacity) { //传入新的容量
Entry[] oldTable = table; //引用扩容前的Entry数组
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return;
}
Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组
transfer(newTable); //!!将数据转移到新的Entry数组里
table = newTable; //HashMap的table属性引用新的Entry数组
threshold = (int) (newCapacity * loadFactor); //修改阈值
}
resize里面重新new一个Entry数组,其容量就是旧容量的2倍,这时候,需要重新根据hash方法将旧数组分布到新的数组中,也就是其中的transfer方法:
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了旧的Entry数组
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
Entry e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null; //释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i]; //标记[1]
newTable[i] = e; //将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
}
indexFor()是计算每个元素在数组中的位置,源码:
return h & (length-1); //位AND计算
}
例如,旧数组容量为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是耗时的操作。
由多线程引起的map取值为null的分析的更多相关文章
- Python_关于多线程下变量赋值取值的一点研究
关于多线程下变量赋值取值的一点研究 by:授客 QQ:1033553122 1.代码实践1 #!/usr/bin/env python # -*- coding:utf-8 -*- __author_ ...
- @Value取值为NULL的解决方案------https://blog.csdn.net/zzmlake/article/details/54946346
@Value取值为NULL的解决方案 https://blog.csdn.net/zzmlake/article/details/54946346
- java的map取值
第一种方法根据键值的名字取值 import java.util.HashMap; import java.util.Map; /** * @param args */ public stat ...
- mybatis返回数据类型为map,值为null的key没返回
创建mybatis-config.xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE ...
- ajax post请求request.getParameter("")取值为null
今天在写提交一个json数据到后台,然后后台返回一个json数据类型.但是发现后台通过request.getParamter("")取到的值为null. 于是写一个简单的ajax ...
- Spring @Value取值为null或@Autowired注入失败
@Value 用于注入.properties文件中定义的内容 @Autowired 用于装配bean 用法都很简单,很直接,但是稍不注意就会出错.下面就来说说我遇到的问题. 前两天在项目中遇到了一个问 ...
- @Value取值为NULL的解决方案
在spring mvc架构中,如果希望在程序中直接使用properties中定义的配置值,通常使用一下方式来获取: @Value("${tag}") private String ...
- nacos作为配置中心动态刷新@RefreshScope添加后取值为null的一个问题
之前springboot项目常量类如下形式: @Component @RefreshScope//nacos配置中心时添加上 public class Constants { @Value(" ...
- MyBatis resultType用Map 返回值中有NULL则缺少字段 返回值全NULL则map为null
这个问题我大概花了2个小时才找到结果 总共需要2个设置 这里是对应springboot中的配置写法 @select("select sum(a) a,sum(b) b from XXX wh ...
随机推荐
- ef操作类
基于Hi博客的类库 20160811 using Model; using System; using System.Collections.Generic; using System.Data.En ...
- Java集合系列:-----------07Map架构
我们之前了解了Collection框架,我们再来了解一下Map架构.前面我们讲了Conllection中的List结合,没有讲Collection中的Set集合, 因为Collection框架中的Se ...
- C# Winform应用程序占用内存较大解决方法整理(转)
原文:http://www.jb51.net/article/56682.htm 背景: 微软的 .NET FRAMEWORK 现在可谓如火如荼了.但是,.NET 一直所为人诟病的就是“胃口太大”,狂 ...
- BZOJ 1096 【ZJOI2007】 仓库建设
Description L公司有N个工厂,由高到底分布在一座山上.如图所示,工厂1在山顶,工厂N在山脚.由于这座山处于高原内陆地区(干燥少雨),L公司一般把产品直接堆放在露天,以节省费用.突然有一天, ...
- CSS3 动画效果带来的bug
css3 动画效果比如transition:all 2s linear;这种用来计算及时的物体坐标的话会带来一定的问题 比如把一个DIV从A点移动到B点.JS为DIV.style.left=B; 但是 ...
- 【转】CSS样式覆盖规则
大家都知道CSS的全称叫做“层叠样式表”,但估计很多人都不知道“层叠”二字的含义.其实,“层叠”指的就是样式的覆盖,当一个元素被运用上多种样式,并且出现重名的样式属性时,浏览器必须从中选择一个属性值, ...
- DispatcherHelper
DispatcherHelper 通常,WPF 应用程序从两个线程开始:一个用于处理呈现, 一个用于管理 UI.呈现线程有效地隐藏在后台运行,而 UI 线程则接收输入.处理事件.绘制屏幕 以及运行应用 ...
- ROS系统python代码测试之rostest
ROS系统中提供了测试框架,可以实现python/c++代码的单元测试,python和C++通过不同的方式实现, 之后的两篇文档分别详细介绍各自的实现步骤,以及测试结果和覆盖率的获取. ROS系统中p ...
- Android开发自学笔记(Android Studio1.3.1)—2.开始第一个Android应用
一.前言 使用Android Studio开发Android应用是一件非常简单的事情,因为它会帮你自动完成很多工作.本篇我们主要完成一个单击按钮在文本框显示当前时间的简单应用,借此来演示一下 ...
- alarm
AlarmManager的使用机制有的称呼为全局定时器,有的称呼为闹钟.通过对它的使用,它的作用和Timer有点相似.都有两种相似的用法:(1)在指定时长后执行某项操作 (2)周期性的执行某项操作 在 ...