一、前言

      其实这一面来的挺突然,也是意想不到的,这个要起源于BOSS直聘,很巧,其实也算是一种缘分吧,谢谢BOSS那个哥们,还是那句话来滨江我请你吃饭,身怀感激你总会遇到帮助你的人,只是这个我没有想到,我转Java也没多久,很多东西也没有搞清楚,没想到菜鸟会给我这个电话,内心是震惊的,但是也感谢给我这个机会让我真正认清自己,谢谢菜鸟,那我现在将这份迟到答卷交一交吧。

二、试卷

1、HashMap和Hashtable区别在哪?

2、Synchronized是怎么实现同步的?

3、Spring IOC的启动流程?另外还说一下BeanFactory和ApplicationContext的区别?

4、拦截器和过滤器的区别?

5、单点登录?

6、RabbitMQ怎么保证消息到达的?

这6个问题其实我感觉就第5个问题回答的比较好,剩下都是似懂非懂,这可能是对于我的自满的一个警钟,还是那句话深挖基础,来年再战,接下来我们来用来深耕一下这些问题,给自己再交一份满意的答卷。

第一个问题我们从基层结构、对null值的处理、数据结构以及线程安全这4个方面结合源码来谈一下:

     1.继承结构

      Map总体继承:

HashMap的继承结构:

HashTable的继承结构:

对比下实现,不同点主要在基类上继承上,对比下暴露出的API主要是Hashtable多elements和contains方法,elemets主要是返回所有的value的值,为空的时候返回空枚举对象,而contains方法和containsValue方法都是查询是否包含value的值,剩下方法基本上都是相同。

2.对null的处理

Hashtable对null的处理,如果发现为null会报空引用异常;

public synchronized V put(K key, V value) {
// value为空引用异常
if (value == null) {
throw new NullPointerException();
} // Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
//key计算的时候同样为空引用异常
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
} addEntry(hash, key, value, index);
return null;
}

HashMap对Null的处理以及添加元素的整个流程:

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
//key为null则默认hashcode为0
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断一下是否为第一次插入,如果是第一次插入则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//对应的节点存在
Node<K,V> e; K k;
//判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 该链为链表,就用链地址法
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//创建头部链表
p.next = newNode(hash, key, value, null);
//如果个数大于8则转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果key值存在覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//找到或者新建key和hashcode相等的键值进行插入
if (e != null) { // existing mapping for key
V oldValue = e.value;
//onlyIfAbsent为false或旧值为null时,允许替换旧值
//这个地方有点不明,明明啥时候都为false
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

3.数据结构

总体上两者的数据结构都是通过数组+链表实现,但是JDK1.8以后当链表的长度超过8以后会更改为红黑树,都是通过继承Entry数组实现哈希表;

//Hashtable节点定义
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next; protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
} @SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
} // Map.Entry Ops public K getKey() {
return key;
} public V getValue() {
return value;
} public V setValue(V value) {
if (value == null)
throw new NullPointerException(); V oldValue = this.value;
this.value = value;
return oldValue;
} public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o; return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
} public int hashCode() {
return hash ^ Objects.hashCode(value);
} public String toString() {
return key.toString()+"="+value.toString();
}
}
//HashMap节点定义
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
} public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; } public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
} public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

初始化Hashtable默认初始化的长度为11,加载因子为0.75;HashMap默认为16,加载因子为0.75

扩容Hashtable为2n+1,HashMap为2倍,另外HashMap如果为整数的最大值则就不会扩容,这块是JDK8实现的别的还请自己去查看一下;

另外就是算法问题,JDK8中实现高位运算的算法,相比Hashtable的去模算法来具有更高的效率;具体看下https://tech.meituan.com/java-hashmap.html,

扩容算法也有必要看下;

//Hashtable默认构造函数
public Hashtable() {
this(11, 0.75f);
}
//Hashtable扩容
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table; // overflow-conscious code
//位运算相当于2n+1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap; for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
//HashMap扩容
final Node<K,V>[] resize() {
//保存旧的值
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//如果超过最大的值则碰撞吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新容量调整旧容量的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//初始化默认为16
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

4.线程安全性

主要针对这块说一说,我就是死在这里的,首先明确的是Hashtable肯定是线程安全的,因为使用Synchronized修饰,所以内部调用的时候是线程安全的;接下主要说下HashMap,这个其实面试官感觉我能回答上来,还好几次问我你确定嘛,我说我确定,哈哈当时也真是,我没说出死循环这个点来,接下来我们分析下为什么会照成死循环?同样上面那个美团技术博客也是有这部分讲解的,这部分JDK8确实写的很麻烦,我们使用JDK7来看下怎么照成的,站到巨人的肩膀上,消化为自己的;

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, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

以上为主要代码,接下来我们来谈下照成死循环的过程,下面这段代码是主要的罪魁祸首:

首先来看下单线程下运行状况,代码如下,过程如图:

1.扩容的时候首先遍历数组中的元素;

2.对链表的每一个节点进行遍历,next指向下一个要移动元素,将e转向转向Hash表的头部,使用头插入元素;

3.循环到链表为null结束这次循环;

4.最终等数组循环完毕,完成HashMap扩容;

        HashMap<Integer,String> map = new HashMap<>(2,0.75f);
map.put(5, "C");
map.put(7, "B");
map.put(3, "A");

思考一个问题,当多线程操作做时候,这块不是原子操作,所以肯定会出现问题,假设这样一种情况,代码如下:

        HashMap<Integer,String> map = new HashMap<>(2,0.75f);
map.put(5, "C");
new Thread("Thread1") {
public void run() {
map.put(7, "B");
System.out.println(map);
};
}.start();
new Thread("Thread2") {
public void run() {
map.put(3, "A");
System.out.println(map);
};
}.start();
map.put(11,"D");

当线程1和线程2同时进入put方法,进入transfer()该方法时候,线程1执行到"Entry<k,v> next = e.next"然后被挂起,线程2开始执行resize()方法,最终形成如下:这部分可以使用多线程调试进行模拟,不会的请参考:http://blog.csdn.net/kevindai007/article/details/71412324

这个时候线程1被唤醒,执行步骤如下:

1.执行e.next = newTable[i],这个时候 key(3)的 next 指向了线程1的新 Hash 表,所以e.next=null;

2.执行newTable[i]=e,e = next,导致e指向了key(7);

3.下一次循环的next = e.next导致了next指向了key(3)。

4.e.next = newTable[i] 导致 key(3).next 指向了 key(7),环形链表就这样出现了。

另外还有一种情况,也就是常见的更新丢失问题的,当2个线程同时插入到数组同一个位置的时候,线程A也写入,线程B也写入,则会照成B写入的覆盖A写入的,照成更新丢失,执行如下代码,这种情况不是很容易出现,多执行几次代码会发现,截图如下:

        HashMap<String,String> map=new HashMap<>();
//线程1
Thread t1 = new Thread(){
public void run() {
for(int i=0; i<25; i++){
map.put(String.valueOf(i), String.valueOf(i));
}
}
};
//线程2
Thread t2 = new Thread(){
public void run() {
for(int i=25; i<50; i++){
map.put(String.valueOf(i), String.valueOf(i));
}
}
};
t1.start();
t2.start(); Thread.currentThread().sleep(1000);
//System.out.print(map.values());
for(int i=0; i<50; i++){
//如果key和value不同,说明在两个线程put的过程中出现异常。
if(!String.valueOf(i).equals(map.get(String.valueOf(i)))){
System.err.println(String.valueOf(i) + ":" + map.get(String.valueOf(i)));
}
}

 三、结束语

       这几个问题要好好深耕一下的话,估计1篇或者几篇是写不完的,我会慢慢写,请大家耐心等待吧,先进行一个预告,下一篇我会再把第一题发散一下:HashMap和ConcurrentHashMap对比,上面有不明白的地方可以找我,QQ群:438836709

面试(一)-HashMap的更多相关文章

  1. java面试之Hashmap

    在java面试中hashMap应该说一个必考的题目,而且HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,其中 HashMap 是 Map 接 ...

  2. 面试总结hashmap

    考点: 1.hashing的概念 2.HashMap中解决碰撞的方法 3.equals()和hashCode()的应用,以及它们在HashMap中的重要性 4.不可变对象的好处 5.HashMap多线 ...

  3. 面试中HashMap链表成环的问题你答出了吗

    HashMap作为老生常谈的问题,备受面试官的青睐,甚至成为了面试必问的问题.由于大量的针对HashMap的解析横空出世,面试官对HashMap的要求越来越高,就像面试官对JVM掌握要求越来越高一样, ...

  4. 面试笔记--HashMap扩容机制

    转载请注明出处 http://www.cnblogs.com/yanzige/p/8392142.html 扩容必须满足两个条件: 1. 存放新值的时候当前已有元素的个数必须大于等于阈值 2. 存放新 ...

  5. Android面试之HashMap的实现原理

    1.HashMap与HashTable的区别 HashMap允许key和value为null: HashMap是非同步的,线程不安全,也可以通过Collections.synchronizedMap( ...

  6. 深入解读大厂java面试必考基本功-HashMap集合

    课程简介 HashMap集合在企业开发中是必用的集合同时也是面试官面试率很高的集合,因为HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好offer,这是一个迈不过的 ...

  7. 一个HashMap能跟面试官扯上半个小时

    一个HashMap能跟面试官扯上半个小时 <安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 一个volatile ...

  8. HashMap原理阅读

    前言 还是需要从头阅读下HashMap的源码.目标在于更好的理解HashMap的用法,学习更精炼的编码规范,以及应对面试. 它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而 ...

  9. HashMap实现原理、核心概念、关键问题的总结

    简单罗列一下较为重要的点: 同步的问题 碰撞处理问题 rehash的过程 put和get的处理过程 HashMap基础: HashMap的理论基础:维基百科哈希表 JDK中HashMap的描述:Has ...

随机推荐

  1. WebStorm File Watchers配置将.less文件编译后的.css输出至指定目录

    Arguments:其实是命令行输入“lessc file.less file.css”后者指定路径 Output Paths to refresh:刷新changed后.css文件

  2. (后台)jxl.read.biff.BiffException: Unable to recognize OLE stream

    在excel中打开,另存成xls就可以.

  3. 第1章 HTTP协议基本介绍了解

    一.常见接口协议: HTTP     超文本传输协议 HTTPS   安全超文本传输协议 FTP       文件传输协议 TCP       网络控制协议 IP          互联网协议 UDP ...

  4. Android 官方DEMO BasicNetworking

    本示例演示如何使用Android API检查网络连接. Demo下载地址:https://github.com/googlesamples/android-BasicNetworking/#readm ...

  5. java中的泛型与反射

    在java开发中,大多数情况是多人开发,那么如何在没有API的情况下,快速的理解到别人开发的一些类的详细信息呢? 比如函数名,类名,返回参数等等 我们可以定义一个反射机制的类,通过此类来打印此你想要操 ...

  6. python 爬虫 requests+BeautifulSoup 爬取巨潮资讯公司概况代码实例

    第一次写一个算是比较完整的爬虫,自我感觉极差啊,代码low,效率差,也没有保存到本地文件或者数据库,强行使用了一波多线程导致数据顺序发生了变化... 贴在这里,引以为戒吧. # -*- coding: ...

  7. Python 提案

    学习Java 不可不知JSR,学习Python自然也得知道 PEP了 1- PEP简介 PEP是Python增强提案(Python Enhancement Proposal)的缩写.https://w ...

  8. jQuery -- 光阴似箭(一):初见 jQuery -- 基本用法,语法,选择器

    jQuery -- 知识点回顾篇(一):初见jQuery -- 基本用法,语法,选择器 1. 使用方法 jQuery 库位于一个 JavaScript 文件中,其中包含了所有的 jQuery 函数. ...

  9. June 5. 2018 Week 23rd Tuesday

    Learn to let go and be clear of where you really want to head for. 学会放手,同时也要弄清楚自己的真正所爱. From Kissing ...

  10. Python中进程线程协程小结

    进程与线程的概念 进程 程序仅仅只是一堆代码而已,而进程指的是程序的运行过程.需要强调的是:同一个程序执行两次,那也是两个进程. 进程:资源管理单位(容器). 线程:最小执行单位,管理线程的是进程. ...