面经
1.hashmap底层实现说一下
答:Node数组,Node的数据结构是一个链表或者是一个红黑树。
hashmap的hash方法:使用传入key的hashcode与它的hashcode像左移16位的值异或,如果key值为null,则返回0;
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
get方法,使用hash(key)&tab.length-1,如果不是null,如果是链表结构,则从头向后遍历,找到hash值和key值都相等的节点,返回。如果是红黑树结构,则去红黑树中查找。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
put方法,先判断是否需要扩容。然后使用hash(key)&tab.length-1,如果当前值是null,则创建新的节点。如果不是null,判断他们的hash值和key值是否相同,相同的话将旧key的值修改为新值,不同的话,如果是链表结构,则给链表后面增加节点。如果是红黑树结构,则给红黑树增加新节点。如果链表节点增加完长度大于8了,则转换为红黑树。
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.hashMap是线程安全的吗?
不是,线程安全的有hashTable和ConcurrentHashMap。hashTable get,put等方法上都加了synchronized关键字,全表锁。ConcurrentHashMap只锁当前节点
ConcurrrentHashMap1.7采用的是分段锁,Segment实现了ReentrantLock
3.说一下java的锁,以及redis、zookeeper的锁
java的锁主要是jdk的关键字,synchronized和Lock接口
synchronized
synchronized主要利用对象头MarkWord的存储锁的类型,以及monitor实现。可以加在方法上,或者主动去修饰对象或者类。加在普通方法上和对象上锁的是当前对象。加在静态方法上和修饰类,锁的是这个类所有的对象。
synchronized在1.6以后进行了锁的优化,对象上的锁可以进行升级,无锁->偏向锁->轻量级锁->重量级锁。偏向锁对象头存储了锁的状态以及当前持锁的线程的id,如果有线程想要获取锁,则去做CAS操作,获取偏向锁,如果当前对象的锁被其他线程持有,则去看这个线程是否存活,如果不存活,则将锁降级为无锁,再由我们的线程去获取偏向锁。如果需要,则升级为轻量级锁,轻量级锁会暂停线程1,把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间,用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址。线程2获取锁的时候,也把锁对象的对象头MarkWord复制一份到线程2的栈帧中,然后执行CAS操作,去更新对象头中的内容,由于线程1以及替换了,所以线程2回去自旋获取锁,如果一直没获取到锁,发生了锁的膨胀,默认阈值是10,如果又有其他线程来竞争锁,将锁升级为重量级锁,则会将其他未获取到锁的线程全部阻塞,重量级锁由monitor实现。
ReentrantLock
Lock的话,是依赖java API的,是一个接口,主要的实现由ReentrantLock,ReentrantLock的实现是基于AQS,aqs全名AbstractQueueSychronized,主要思想对空闲资源,如果有线程尝试获取,则将该线程置为有效线程。如果还有其他线程也竞争该资源,那么就需要一套线程阻塞,等待,唤醒机制。这个机制AQS是利用CHL队列实现的。CHL是一个双向的对象,AQS将暂时获取不到锁的线程封装成节点放入CHL队列中进行调度。具体实现是使用一个int值,作为竞争资源的同步标记,然后子类通过实现资源获取,资源释放等方法,并且同步的修改同步标记即可。具体线程的等待唤醒AQS已经做了。支持共享,以及独占资源。支持公平和非公平锁,公平按队列顺序执行获取锁即可,非公平则无视队列顺序去抢锁。
异同
1.都是可重入锁
2.sync程序结束,或异常会自动释放锁,lock不会
3.lock可以中断当前等待,去执行其他事情,sync不会
4.lock支持公平锁&非公平锁,sync只支持非公平锁
5.lock支持获取锁的状态,sync不支持
6.sync使用object的方法进行锁的调度,lock可以使用condition
redis分布式锁
- Redis2.6.12以上版本,可以用set获取锁。set可以实现setnx和expire,这个是原子操作。
- Lua删除锁。Lua是原子操作。
- 让获取锁的线程开启一个守护线程,给线程还没执行完,又快要过期的锁续航。大概是这样的,
- 线程A还没执行完,守护线程每当快过期时,延时expire时间。当线程A执行完,显示关闭守护线程。如果中间宕机,锁超过超时,守护线程也不在了,自动释放锁。
- 随机值,防止误删锁
- 分布式可重入锁redisson提供了自动续期 启动watch dog线程
4.类加载机制说一下
加载、验证、准备、解析、初始化、使用和卸载
1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
类加载,双亲委派
验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
准备:给类变量设置初值
解析:解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。
初始化:类方法块,静态方法块,成员变量初始化
整个类加载过程中,除了在加载阶段用户应用程序可以自定义类加载器参与之外,其余所有的动作完全由虚拟机主导和控制。到了初始化才开始执行类中定义的Java程序代码(亦及字节码),但这里的执行代码只是个开端,它仅限于<clinit>()方法。类加载过程中主要是将Class文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操作,在加载完成后才真正开始。
5.线索池说一下,为什么核心线程池慢了要加入缓冲队列?
核心线程数,最大线程数,缓冲队列类型,线程池工厂,超时时间,时间单位,拒绝策略
创建新线程执行->核心线程数满->加入缓冲队列->缓冲队列满->创建新线程->最大线程数满->拒绝策略
加入缓冲队列的目的:
- 线程池创建线程需要获取mainlock这个全局锁,会影响并发效率,所以使用阻塞队列把第一步创建核心线程与第三步创建最大线程隔离开来,起一个缓冲的作用。
- 引入阻塞队列,是为了在执行execute()方法时,尽可能的避免获取全局锁。
拒绝策略默认abortPolicy,报错;callerUserpolicy使用当前线程执行任务;discardPolicy提交上来的任务之间丢弃;discartOldestPolicy最早任务丢弃
6.JVM说一下
线程私有
程序计数器,可以用来控制程序执行顺序
java虚拟机栈,本地方法栈,用来存储方法运行时数据,方法执行结束出栈
方法区:存储类的定义数据,还有静态变量等
堆:存储对象,分为新生代和老年代,新生代分edan区和survive from和survive to区。XX:MaxTenuringThreshold
元空间:直接内存,受本机可用内存限制,不会出现oom
垃圾收集器
serial 新生代複製算法,老年代标记整理 串行
parnew serial的多线程模式
parallel Scavenge 关注 吞吐量
CMS 并发收集器,获取最短停顿时间为目标,注重用户体验 垃圾收集和用户程序并行
标记清除算法,老年代
初始标记->并发标记->重新标记->并发清除 初始标记和重新标记会SAW,初始标记,暂停其他线程,记录下直接与root相连的对象,很快。并发标记会记录引用更新
G1 维护了一个优先队列,根据允许的收集时间,优先选择回收价值最大的Region
判断对象是否死亡
引用计数法
可达性分析(重点)
使用OopMap记录并枚举根节点
HotSpot首先需要枚举所有的GC Roots根节点,虚拟机栈的空间不大,遍历一次的时间或许可以接受,但是方法区的空间很可能就有数百兆,遍历一次需要很久。更加关键的是,当我们遍历所有GC Roots根节点时,我们需要暂停所有用户线程,因为我们需要一个此时此刻的”虚拟机快照”,如果我们不暂停用户线程,那么虚拟机仍处于运行状态,我们无法确保能够正确遍历所有的根节点。所以此时的时间开销过大更是我们不能接受的。
基于这种情况,HotSpot实现了一种叫做OopMap的数据结构,这种数据结构在类加载完成时把对象内的偏移量是什么类型计算出,并且存放下位置,当需要遍历根结点时访问所有OopMap即可。
用安全点Safepoint约束根节点
如果将每个符合GC Roots条件的对象都存放进入OopMap中,那么OopMap也会变得很大,而且其中很多对象很可能会发生一些变化,这些变化使得维护这个映射表很困难。实际上,HotSpot并没有为每一个对象都创建OopMap,只在特定的位置上创建了这些信息,这些位置称为安全点(Safepoints)。
为了保证虚拟机中安全点的个数不算太多也不是太少,主要决定安全点是否被建立的因素是时间。当进行了耗时的操作时,比如方法调用、循环跳转等时会产生安全点。此外,HotSpot虚拟机在安全点的基础上还增加了安全区域的概念,安全区域是安全点的扩展。在一段安全区域中能够实现安全点不能达成的效果。
可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
System Class(系统类,例如Java.util.*),
Thread Block(一个对象存活在一个阻塞的线程中) ,
Thread(线程),正在运行的线程
Busy Monitor (调用了wait()或notify()或已同步的所有内容。例如,通过调用synchronized(Object)或进入 synchronized 同步方法。静态方法表示类,非静态方法表示对象。
7.对象生成说一下
类加载检查,分配内存,初始化0值,设置对象头,执行init方法
分配内存:指针碰撞,空闲列表
访问方式:句柄,直接指针
8.spring ioc aop说一下
ioc控制反转,实现方式DI,依赖注入。
-
1.定位并加载配置文件
2.解析配置文件中的bean节点,一个bean节点对应一个BeanDefinition对象(这个对象会保存我们在Bean节点内配置的所有内容,比如id,全限定类名,依赖值等等)
3.根据上一步的BeanDefinition集合生成(BeanDefinition对象内包含生成这个对象所需要的所有参数)所有非懒加载的单例对象,其余的会在使用的时候再实例化对应的对象。
4.依赖注入
5.后置处理
spring boot启动过程
1.先说@SpringBootApplication,
自动装配,
2.说实例化SpringBootApplication
根据配置读取ApplicationContactInitializer,ApplicationListenerInitalizer
3.run方法
·获取监听器和参数配置。·打印Banner信息。·创建并初始化容器。·监听器发送通知。2.创建环境变量
3.加载ioc容器,
·获取监听器和参数配置。·打印Banner信息。·创建并初始化容器。·监听器发送通知。
9.redis高可用
1.主从,会全量同步,优点非阻塞,可以减轻主节点压力。缺点无容错,无法水平扩容
2.哨兵:哨兵进程一秒一次,ping,监视主从节点
3.集群:每个节点分配卡槽 214次方
10.动态代理
JDK动态代理
我我们自己实现一个代理类的Hander,实现invacationHander接口
然后调用Proxy.createNewProxyInstance(class loader,class实现的接口,还有hander)
这个方法里首先,获取类的代理对象,如果缓存里面有从缓存里面拿,如果没有,通过proxyClassFactory去创建,也是根据反射
生成代理类的字节码文件
cglib动态代理
特点
- 原理是对指定的目标生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
注意:jdk的动态代理只可以为接口去完成操作,而cglib它可以为没有实现接口的类去做代理,也可以为实现接口的类去做代理。