10-9
面试
项目介绍
股票的实时价格如何获取
难点 如何解决
集合类简单介绍 对哪个熟悉一些 介绍一下
Arraylist 扩容时候 resize()方法
当向容器中添加元素的时候,会判断当前容器中的元素个数,如果大于等于当前数组的长度*加载因子的时候,需要自动扩容。
什么时候resize()
/**
* HashMap 添加节点
*
* @param hash 当前key生成的hashcode
* @param key 要添加到 HashMap 的key
* @param value 要添加到 HashMap 的value
* @param bucketIndex 桶,也就是这个要添加 HashMap 里的这个数据对应到数组的位置下标
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//size:The number of key-value mappings contained in this map.
//threshold:The next size value at which to resize (capacity * load factor)
//数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值
// 2.底层数组的bucketIndex坐标处不等于null
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//扩容之后,数组长度变了
hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢?
bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。
}
createEntry(hash, key, value, bucketIndex);
}
/**
* 这地方就是链表出现的地方,有2种情况
* 1,原来的桶bucketIndex处是没值的,那么就不会有链表出来啦
* 2,原来这地方有值,那么根据Entry的构造函数,把新传进来的key-value mapping放在数组上,原来的就挂在这个新来的next属性上了
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K, V> e = table[bucketIndex];
table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e);
size++;
}
resize():(JDK1.7)
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);//修改阈值
}
transfer()函数将原来Entry数组的元素拷贝到新的Entry数组中
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了旧的Entry数组
int newCapacity = newTable.length;
for (int j = 0;j < src.length;j++){ //遍历旧的Entry数组
Entry<K,V> e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null; //释放旧的Entry数组的对象引用 (for循环后,旧的Entry数组不再引用任何对象)
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash,newCapacity); //重新计算每个元素在数组中的位置
e.next = newTable[i];//标记
newTable[i] = e;//将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
}
static int indexFor(int h,int length) {
return h & (length - 1);
}
ArrayList的put()方法 以及 添加节点时候 新节点放哪里 尾部还是头部
注意:在添加的时候:新加入的放在链表的头部,最早先加入的放在链尾。
Resize线程不安全
1.put操作时导致线程不安全
两个线程A和B,线程A将一个key-value插入到HashMap中,首先计算记录所要落搭配的桶的索引坐标,然后获取到这个桶中的链表头节点,此时线程A的时间片用完了,线程B调度得以执行,线程B将记录插入到桶中。假设线程A插入的记录算出来的桶索引和线程B插入的记录的桶索引一样,那么当线程B成功插入后,线程A再次被调度运行时,线程A持有过期的链表头,它将记录进行插入,就将线程B插入的记录进行了覆盖,导致线程B插入的记录消失。
2. hashmap的get操作可能因为resize扩容而引起死循环
我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3,需要resize桶到4,原来的记录分别为:[3,A],[7,B],[5,C],在原来的map里面,我们发现这三个entry都落到了第二个桶里面。
假设线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [3,A], next = [7,B]。线程thread2被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,B]的next为[3,A]。此时线程thread1重新被调度运行,此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,A]迁移到新的数组上,然后再处理[7,B],而[7,B]被链接到了[3,A]的后面,处理完[7,B]之后,就需要处理[7,B]的next了啊,而通过thread2的resize之后,[7,B]的next变为了[3,A],此时,[3,A]和[7,B]形成了环形链表,在get的时候,如果get的key的桶索引和[3,A]和[7,B]一样,那么就会陷入死循环。
如果在取链表的时候从头开始取(现在是从尾部开始取)的话,则可以保证节点之间的顺序,那样就不存在这样的问题了。
Lock和synchronized 介绍以及区别
Synchronized 演进过程
Java内存模型
Volatile
线程安全的集合
异常 error和exception
网络模型 tcp和udp区别 三次握手 四次挥手 三次挥手可以吗
Hashset底层
堆和栈区别
堆:堆是jvm的一块内存区域。存储的是数组和对象(其实数组就是对象),new的东西都是在堆中,堆不会随时释放空间,但是会有垃圾回收机制。(栈中存放的是单个变量,变量释放后,就没有了)
栈:栈是一块内存区域,存储的是局部变量(在方法中定义的都是局部变量,for循环中定义的也是局部变量)。先加载函数才能对局部变量的定义,因此方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。因此栈内存更新速度很快。
区别:
- 栈内存存储的是局部变量,而堆内存存储的是实体
- 栈内存的更新速度快于堆内存,因为局部变量的生命周期很短
- 栈内存存放的变量在生命周期一结束就会被释放,而堆内存存放的对象会被垃圾回收机制不定时地回收。
队列和栈区别
创建线程方式 thread和runnable区别
线程池参数 解释
synchronized修饰静态方法和修饰非静态方法有什么区别
算法题
单例模式 如何改进
二叉树 层序遍历