文章目录
一、自我介绍
个人背景、项目经历、实习经历。
二、Java后台基础
2.1 HashMap相关
2.1.1 HashMap的存储结构
HashMap采用了数组和链表的数据结构,在查询修改上继承了数组的线性查找和链表的寻址修改,它存储的内容是键值对(key-value)映射。
2.1.2 数组和链表分别的作用
- 数组中存放的是对象,数组部分进行的操作主要是散列。
根据hash算法实现快速存储第一步,确定存储在数组的哪个位置。每一个单位中存储的内容为:key+value+指针,其中指针主要用来进行链表操作。 - 链表存在主要是为了解决经过hash计算后会有重复值这一问题。
当多次散列后,hash计算结果值一致,需要存放在数组的同一部分,就要使用链表结构。当一个非空数组中需要存入新的对象,就需要把原来对象中末尾的指针指向新的对象,每次有新的对象key值经过hash计算后得到相同的值,都会重复以上步骤,“往下塞”,使每个对象都能够被访问到。
2.1.3 HashMap判断key相同
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
先判断hash是否一致,然后再判断传入key和当前集合中是否有相同的key。如果key相同,则新值替换旧值。其中在判断中使用了==
和equals
。
2.1.4 HashMap扩容以及元素的移动
- HashMap默认的负载因子大小为
0.75
,当一个map填满了75%数组的时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小16
的两倍的数组,来重新调整map的大小,并将原来的对象放入新的数组中。 - 假设扩容前的map大小为2的N次方,扩容后map中的元素只有两种情况:
元素hash值第N+1位为0:不需要进行位置调整。
元素hash值第N+1位为1:调整至原位置+原数组容量位置。
2.1.5 HashMap的hash冲突解决办法
hash冲突:如果存在相同的hashCode值,那么它们索引位置相同。如果此时key不相同,就产生hash冲突。
- 开放定址法:通过一个探测算法,当某一个槽位已经被占据的情况下,继续查找下一个可以使用槽位。
- 链地址法:将所有hash为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中。
2.1.6 HashMap为什么引入红黑树及优势
在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中。
红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(logn)。加快检索速率。
2.1.7 为什么不用二叉查找树
选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(跟原来链表结构一样会造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可以通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了使查找数据更快,解决链表查询深度的问题。红黑树属于自平衡的二叉查找树,虽然为了保持“平衡”是需要付出代价,但是该代价所损耗的资源要比遍历线性链表少很多。
2.2 线程安全
2.2.1 怎么做到线程安全
- 锁和同步,sychronized关键字。
- volatile关键字。
- ReentrantLock可重入锁。
2.2.2 介绍sychronized
Synchronized的作用主要有三个:
- 原子性:确保线程互斥的访问同步代码;
- 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
- 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;
从语法上讲,Synchronized可以把任何一个非null对象作为"锁",锁有个专门的名字:对象监视器(Object Monitor)。Synchronized总共有三种用法:
- 当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);
- 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
- 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例。
2.2.3 介绍volatile
volatile用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。
2.2.4 介绍ReentrantLock
ReentrantLock是Lock接口的一个实现类。可重入的意思是一个线程拥有锁之后,可以再次获取锁。
2.2.5 Java高并发的实现
- 初级技巧 - 乐观锁
乐观锁使用的场景是,读不会冲突,写会冲突。同时读的频率远大于写。
悲观锁认为所有代码执行都会有并发问题,所以将所有代码块都用sychronized锁住;乐观锁认为在读的时候不会产生冲突为题,在写时添加锁。所以解决的应用场景是读远大于写时的场景。 - 中级技巧 - String.intern()
类String维护了一个字符串池。当调用intern方法时,如果池已经包含一个等于此String对象的字符串(该对象由equals(Object)方法确定),则返回池中的字符串。可见,当String 相同时,总返回同一个对象,因此就实现了对同一用户加锁。由于锁的颗粒度局限于具体用户,使得系统获得最大程度的并发。比较类似行锁和数据库表锁的概念,行锁的并发能力比表锁高很多。 - 高级技巧 - 类ConcurrentHashMap
可以借鉴ConcurrentHashMap的方式,将需要加锁的对象分为多个bucket,每个bucket加一个锁。
三、项目
3.1 印象最深的项目
四、算法题(口述)
4.1 二叉树镜像
4.1.1 递归
public static boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
return in(root.left, root.right);
}
//递归
public static boolean in(TreeNode l1, TreeNode l2) {
if (l1 == null && l2 == null) {
return true;
}
if (l1 == null || l2 == null) {
return false;
}
return l1.val == l2.val && in(l1.left, l2.right) && in(l1.right, l2.left);
}
4.1.2 迭代
public static boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
q.add(root);
while (!q.isEmpty()) {
TreeNode t1 = q.poll();
TreeNode t2 = q.poll();
if (t1 == null && t2 == null) {
continue;
}
if (t1 == null || t2 == null) {
return false;
}
if (t1.val != t2.val) {
return false;
}
q.add(t1.left);
q.add(t2.right);
q.add(t1.right);
q.add(t2.left);
}
return true;
}