1Java 基础
1、HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化。
jdk1.7实现:
源码是采用Node数组方式实现,node即链表节点,故jdk1.7中hashMap是采用数组+链表方式实现,hashMap在进行初始化时,数组长度默认为16,当对key进行hash运算取数组下标发现该下标已存在时,会针对此key重新生成一个node节点然后追加到数组下标已存在对应的节点后面
jdk1.8实现:
原理大部分和1.7类似,最大的区别在于设置了链表默认长度,当链表长度大于8时,会转化为红黑树存储节点数据,以提升其查找效率
2、HaspMap扩容是怎样扩容的,为什么都是2的N次幂的大小
空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数组。
有参构造函数:根据参数确定容量、负载因子、阈值等。
第一次put时会初始化数组,其容量变为不小于指定容量的2的幂数。然后根据负载因子确定阈值。
如果不是第一次扩容,则新容量=旧容量*2,新阈值=新容量*负载因子
- table.length = 2^n,是为了能利用位运算(&)来求 key 的下标,而 h&(length-1) 是为了充分利用 table 的空间,并减少 key 的碰撞
- 加载因子太小, table 需要不断的扩容,影响 put 效率;太大会导致碰撞越来越多,链表越来越长(转红黑树),影响效率;0.75 是一个比较理想的中间值
- table.length = 2^n、hash 方法获取 key 的 h、加载因子 0.75、数组 + 链表(或红黑树),一环扣一环,保证了 key 在 table 中的均匀分配,充分利用了空间,也保证了操作效率环环相扣的,而不是心血来潮的随意处理;缺了一环,其他的环就无意义了!
- 网上有个 put 方法的流程图画的挺好,我就偷懒了
3、HashMap,HashTable,ConcurrentHashMap的区别。
hashMap:内部采用数组+链表(1.7)或数据+链表+红黑树实现(1.8),线程不安全
HashTabel:内部结构数组+链表,线程安全,对里面的所有存取值方法采用了synchronize加锁方式
ConcurrentHashMap:
jdk1.7:采用分段锁机制,即基本结构为ReetrantLock+segement+数组+链表,其大概思想为将底层数组进行分段加锁,相比于hashTable,锁粒度相对较小,hashTable锁全局数据,ConcurrentHashMap则锁部分数据,需要2次hash运算才能定位到该元素,第一次定位segement,第二次定位segement里的链表节点头部,segement继承ReetrantLock进行加锁,并发度为segement个数,segement扩容时对其他segement不造成影响,其get方法无需加锁,采用volatile保证对其他线程可见
jdk1.8:采用Synchronized+CAS+数组+链表+红黑树,相比于jdk1.7锁粒度更小,只锁链表头部节点,节点内部(node)的val和next都是volatile保证其可见性,替换,赋值都采用CAS
因为其锁链表头部节点,不影响其他元素读写,所以锁粒度更小,并发时效率也更高,扩容时阻塞所有读写操作,并发扩容,读操作无需加锁,采用volatile修饰node的val和next是保证读写线程对该变量可见,数组用volatile修饰,是保证扩容时被读线程感知
4、极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。
ConcurrentHashMap,锁粒度更小,写并发效率更高
5、HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。
多线程put时可能会导致get无限循环,具体表现为CPU使用率100%; 原因:在向HashMap put元素时,会检查HashMap的容量是否足够,如果不足,则会新建一个比原来容量大两倍的Hash表,然后把数组从老的Hash表中迁移到新的Hash表中,迁移的过程就是一个rehash()的过程,多个线程同时操作就有可能会形成循环链表,所以在使用get()时,就会出现Infinite Loop的情况
写覆盖即多线程put时可能导致元素丢失 原因:当多个线程同时执行addEntry(hash,key ,value,i)时,如果产生哈希碰撞,导致两个线程得到同样的bucketIndex去存储,就可能会发生元素覆盖丢失的情况
6、java中四种修饰符的限制范围。
懒得写
7、Object类中的方法。
equals、hashCode等
8、接口和抽象类的区别,注意JDK8的接口可以有实现。
9、动态代理的两种方式,以及区别。
jdk动态代理:
Proxy:Proxy是所有动态代理的父类;它提供了一个静态的方法创建代理的Class对象来配置生成代理类Class文件的方法与参数,主要就是通过Proxy.newProxyInstance(类加载器,类实现的接口,InvocationHandler实现类),返回Object类型,通过接口类型强转换即可使用代理类;
JDK的动态代理通过
Interface proxy = Proxy.newProxyInstance(ClassLoader loader,Class<?> interface,InvocationHandler h);
InvacationHandler:每个动态代理实例都有一个关联的InvocationHandler;被代理类的代理方法被调用时,方法将被转发到InvocationalHandler的invoke方法执行
cglib动态代理:
Enchancer:来指定要代理的目标对象;实际处理逻辑的对象;最终通过create()方法得到代理对象,对这个对象的非final()方法的调用都会转发给代理对象;
bMethodInterceptor:动态代理的方法调用都会转发到intercept()上进行增强
1、JDK动态代理具体实现原理:
通过实现InvocationHandler接口创建自己的调用处理器;
通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理;
通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型;
通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入;
JDK动态代理是面向接口的代理模式,如果被代理目标没有接口那么Spring也无能为力,Spring通过Java的反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。
2、CGLib动态代理:
利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
3、两者对比:
JDK动态代理是面向接口的。
CGLib动态代理是通过字节码底层继承要代理类来实现,因此如果被代理类被final关键字所修饰,会失败。
4、使用注意:
如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);
如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。
10、Java序列化的方式。
Java原生序列化
只要让类实现 Serializable 接口就行,序列化具体的实现是由ObjectOutputStream和ObjectInputStream来实现的
JSON序列化
JSON 可能是我们最熟悉的一种序列化格式了,JSON 是典型的 Key-Value 方式,没有数据类型,是一种文本型序列化框架,JSON 的具体格式和特性,网上相关的资料非常多,这里就不再介绍了。他在应用上还是很广泛的,无论是前台 Web 用 Ajax 调用、用磁盘存储文本类型的数据,还是基于 HTTP 协议的 RPC 框架通信,都会选择 JSON 格式
11、传值和传引用的区别,Java是怎么样的,有没有传值引用。
前者传的是真实内存里的值,后者传的是另外一个对象的地址,对于值引用,赋值运算符会直接改变变量的值,原来的值被覆盖掉,对于传引用,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变
12、一个ArrayList在循环过程中删除,会不会出问题,为什么。
肯定会啦,
异常:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
list执行的remove方法:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
// 示例中调用的是此处的 fastRemove
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
// 此处 modCount + 1
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
改变了modCount值,导致判断出现异常
Iterator中的remove方法:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
删除元素时expectedModCount也要做相应变更
2JVM
1、JVM的内存结构。
1.虚拟机栈,创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中的入栈到出栈的过程。
2.堆,存放对象实例及数组,垃圾回收器的主要活动营地,Java 堆划分为新生代和老年代两个大模块,在新生代中,我们又可以进一步分为 Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1),Survivor 空间有一个为空,用于发生 GC 时存放存活对象,老年代存放的是经过多次 Minor GC 仍然存活的对象或者是一些大对象,FGC 就是发生在老年代
3.本地方法栈,本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是 Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务
4.方法区:方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它存储了每个类的结构信息,例如运行时常量池、字段、方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
5.程序计数器,线程私有的,字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
2、JVM方法栈的工作过程,方法栈和本地方法栈有什么区别。
见上
3、JVM的栈中引用如何和堆中的对象产生关联。
在java虚拟机栈中创建的栈针保存堆中对象引用地址
4、可以了解一下逃逸分析技术。
1、全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
对象是一个静态变量
对象是一个已经发生逃逸的对象
对象作为当前方法的返回值
2、参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的
3、没有逃逸
即方法中的对象没有发生逃逸
栈上分配
当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能
5、GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势。
引用计数法、标记-清除、标记-压缩、标记-复制算法
少量对象存活,适合复制算法
大量对象存活,适合标记清理或者标记压缩
CMS:初始标记(STW)、并发标记、重新标志(STW)、并发清除
G1:初始标记、Root Region 扫描、并发标记、最终标记、筛选回收
CMS老年代使用标记-清除回收策略,因此会有内存碎片问题。当碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多空间但就已经不能保存对象了。不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了-XX:UseCMSCompactAtFullCollection开关参数,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程
G1优势:
1、G1在压缩空间方面有优势。
2、G1通过将内存空间分成区域(Region)的方式避免内存碎片的问题。
3、Eden、Survivor、Old区不再固定,在内存使用效率上来说更灵活。
4、G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间,避免应用雪崩现象。
5、G1在回收内存后会马上同时做合并空闲内存的工作,而CMS默认是在STW(stop the world)的时候做。
6、G1会在young GC中使用,而CMS只能在老年代中使用。
G1适合的场景:
1、服务端多喝CPU、JVM内存占用较大的应用。
2、应用在运行过程中会产生大量的内存碎片,需要经常压缩空间。
3、想要更可控、可预期的GC停顿周期;防止高并发下应用的雪崩现象
如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC。
6、标记清除和标记整理算法的理解以及优缺点。
标记清除:每次清除的时候都需要停机、存在内存空间太强片化问题
标记-复制:(Copying)算法是为了解决标记-清除算法,的效率和收集的时间空间不连续等问题。主要的实现是将空间分为两份,将存活的对象移到另外一份,标记完后,将原来的空间清除,这样的话空间是连续的,并且效率较高,特点:空间连续无碎片化、清除高效;缺点:压缩一半空间,垃圾清楚的时候一半空间不可用。对存活对象较多的老年代下,交率较差
标记-整理:由于复制算法的高效性是建立在存活对象少,垃圾对象多的前提下的,对于新生代来说比较适合,但是针对老年代来说,很多对象是一直存活的,所以就不能用复制算法,这样会导致每次回收的垃圾很少,会造成大量的复制。所以标记-整理算法主要是针对老年代来设计的。其原理主要是:分为两个阶段,第一个阶段与标记-清理算法一样,先从根节点标记哪些是被对象引用的,第二阶段将所有存活的对象压缩移动到内存的另一端,按顺序排放,最后清除所有边界以外的空间
分代收集算法(Generational Collection)
背景:由于每个收集的算法都没办法符合所有的场景,就好比每个对象所在的内存阶段不一样,被回收的概率也不一样,比如在新生代,基本可以说90%以上的都会被回收,而到老年代接近一半以上的对象则是一半存活的,所以针对这两种不同的场景,回收的策略肯定有所不一样,所以引发而出的就是分代收集算法,根据新生代和老年代不同的场景而用不同的算法,比如新生代用复制算法,而老年代则用标记-整理算法
7、eden survivor区的比例,为什么是这个比例,eden survivor的工作过程。
8:1:1,年轻代中有一个eden区,2个survivor区,新生代可用内存为整个新生代内存空间的90%,剩余10%即剩余的一个survivor区用于内存分配担保策略,以防止第一个survivor区不足以容纳一次minorGC之后存活的对象
工作过程:发生垃圾收集时,将Eden和survivor仍然存活的对象一次性复制到另外一块survivor空间上,然后直接清理掉Ede和已用过的那块survivor空间
8、JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型。
引用计数算法:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1,当引用失效时,计数器值减一,任何时刻计数器为0的对象就是不可能在被使用的
可达性分析算法:
可达性分析算法的思路就是通过一系列的“GC Roots”,也就是根对象作为起始节点集合,从根节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连。
9、强软弱虚引用的区别以及GC对他们执行怎样的操作。
10、Java是否可以GC直接内存。
能,
JVM在堆内只保存堆外内存的引用,用DirectByteBuffer对象来表示。
每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象。
这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。
当DirectByteBuffer对象在某次YGC中被回收,只有Cleaner对象知道堆外内存的地址。
当下一次FGC执行时,Cleaner对象会将自身Cleaner链表上删除,并触发clean方法清理堆外内存。
此时,堆外内存将被回收,Cleaner对象也将在下次YGC时被回收。
如果JVM一直没有执行FGC的话,无法触发Cleaner对象执行clean方法,从而堆外内存也一直得不到释放。
其实,在ByteBuffer.allocateDirect方式中,会主动调用System.gc()强制执行FGC
11、Java类加载的过程。
加载
简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
这里有两个重点:
- 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
- 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
注:为什么会有自定义类加载器?
- 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
- 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
验证
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
准备
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456
解析
将常量池内的符号引用替换为直接引用的过程。
两个重点:
- 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
- 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
12、双亲委派模型的过程以及优势。
Java的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。
这种双亲委派模式的好处,一个可以避免类的重复加载,另外也避免了java的核心API被篡改
13、常用的JVM调优参数。
14、dump文件的分析。
jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具,类加载,内存,垃圾收集
jiinfo:Java配置信息工具,实时查看和调整虚拟机各项参数
jmap:java内存映像工具,生成堆转储快照
jhat:虚拟机堆转储快照分析工具,于jmap搭配使用,分析堆转储快照
jstack:java堆栈跟踪工具,生成当前时刻的线程快照
15、Java有没有主动触发GC的方式(没有)。
有,比如System.GC,但不会立刻触发GC
3数据结构与算法
1、B+树
2、快速排序,堆排序,插入排序(八大排序算法)
3、一致性Hash算法,一致性Hash算法的应用
4多线程
1、Java实现多线程有哪几种方式。
2、Callable和Future的了解。
3、线程池的参数有哪些,在线程池创建一个线程的过程。
corePoolSize:线程池的基本线程数。这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads()或者 prestartCoreThread()方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建 corePoolSize 个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。
maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime:线程活动保持时间。线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
unit:参数 keepAliveTime 的时间单位,有 7 种取值。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
workQueue:任务队列。用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
threadFactory:创建线程的工厂。可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
handler:饱和策略。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务时抛出异常。以下是 JDK1.5 提供的四种策略。
AbortPolicy:直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
当然也可以根据应用场景需要来实现 RejectedExecutionHandler 接口自定义策略。如记录日志或持久化不能处理的任务
4、volitile关键字的作用,原理。
5、synchronized关键字的用法,优缺点。
synchronized关键字的作用、原理以及锁优化 - 知乎
6、Lock接口有哪些实现类,使用场景是什么。
7、可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment)。
8、悲观锁,乐观锁,优缺点,CAS有什么缺陷,该如何解决。
CAS的实现方式:
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。当多个线程同时尝试使用CAS更新一个变量时,任何时候只有一个线程可以更新成功,若更新失败,线程会重新进入循环再次进行尝试
ABA问题
例如说:
一. 线程1查询值是否为A
二. 线程2查询值是否为A
三. 线程2使用CAS将值更新为B
四. 线程2查询值是否为B
五. 线程2使用CAS将值更新为A
六. 线程1使用CAS将值更新为C
线程一线程二交替执行。第二步到第五步,线程二将值由A更新为B由更新为A,但线程一并没有察觉,因此线程一还是可以继续执行。我们称这种现象为ABA问题
Synchronized的优化:
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
偏向锁:
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
轻量级锁:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
重量级锁:
重量级锁是依赖对象内部的monitor锁来实现。当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,需要从用户态转换到内核态,而转换状态是需要消耗很多时间。
9、ABC三个线程如何保证顺序执行。
10、线程的状态都有哪些。
11、sleep和wait的区别。
12、notify和notifyall的区别。
13、ThreadLocal的了解,实现原理。
5分布式
1、分布式事务的控制。分布式锁如何设计。
2、分布式session如何设计。
3、dubbo的组件有哪些,各有什么作用。
4、zookeeper的负载均衡算法有哪些。
5、dubbo是如何利用接口就可以通信的。
6框架相关
1、SpringMVC的Controller是如何将参数和前端传来的数据一一对应的。
1、用户发送请求到核心控制器(DispatcherServlet)
2、核心控制器根据请求路径通过处理器映射器找到对应的方法(也就是对应的RequestMapping)
3、处理器适配器执行找到的方法,处理业务,返回视图(ModelAndView)
4、通过视图解析器处理返回的视图,返回真正的视图对象
5、对视图页面进行渲染,渲染后响应给用户
2、Mybatis如何找到指定的Mapper的,如何完成查询的。
3、Quartz是如何完成定时任务的。自定义注解的实现。
4、Spring使用了哪些设计模式。Spring的IOC有什么优势。
业务代码与对象创建解耦
5、Spring bean生命周期。
https://blog.csdn.net/xgy258/article/details/125505743
6、一些较新的东西JDK8的新特性,流的概念及优势,为什么有这种优势。
7、区块链了解如何设计双11交易总额面板,要做到高并发高可用
8.、spring IOC与AOP
IOC(工厂模式):控制反转也叫依赖注入,IOC利用java反射机制
所谓控制反转是指:将类的创建和依赖关系写在Spring配置文件里,由配置文件注入,实现了松耦合
AOP(代理模式):面向切面编程。(Aspect-Oriented Programming)
AOP可以说是对OOP的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。
实现AOP的技术,主要分为两大类:
1.一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
2.二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理
spring事务传播机制
REQUIRED(Spring默认的事务传播类型)
如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
SUPPORTS
当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
MANDATORY
当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常
REQUIRES_NEW
创建一个新事务,如果存在当前事务,则挂起该事务
NOT_SUPPORTED
始终以非事务方式执行,如果当前存在事务,则挂起当前事务
NEVER
不使用事务,如果当前事务存在,则抛出异常
NESTED
如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
redis面试题
Redis支持的数据类型
string、list、hash、set、zset
Redis主从同步
1.初次同步(全量复制)
(1)从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;具体判断过程需要在讲述了部分复制原理后再介绍。
(2)主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令
(3)主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态
(4)主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态
(5)如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态
2.增量同步(部分复制)
1. 复制偏移量
执行复制的双方,主从节点,分别会维护一个复制偏移量offset: 主节点每次向从节点同步了N字节数据后,将修改自己的复制偏移量offset+N 从节点每次从主节点同步了N字节数据后,将修改自己的复制偏移量offset+N
offset用于判断主从节点的数据库状态是否一致: 如果二者offset相同,则一致; 如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据
2. 复制积压缓冲区:
主节点内部维护了一个固定长度的、先进先出(FIFO)队列 作为复制积压缓冲区,其默认大小为1MB 在主节点进行命令传播时,不仅会将写命令同步到从节点,还会将写命令写入复制积压缓冲区。
由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。因此,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制
- 如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
- 如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制
redis架构
1.主从
- 无法保证高可用
- 没有解决 master 写的压力
2.哨兵
- 主从模式,切换需要时间丢数据
- 没有解决 master 写的压力
3.客户端集群
- 增加了新的 proxy,需要维护其高可用。
- failover 逻辑需要自己实现,其本身不能支持故障的自动转移可扩展性差,进行扩缩容都需要手动干预
4.服务端集群-redis cluster
- 无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
- 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
- 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本 -实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。
缺点:
- 资源隔离性较差,容易出现相互影响的情况。
- 数据通过异步复制,不保证数据的强一致性
使用过Redis分布式锁么,它是怎么实现的?
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的
使用过Redis做异步队列么,你是怎么用的?有什么缺点?
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
缺点:
- 在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等
什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?
缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
如何避免?
- 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
- 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免?
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
为什么Redis 单线程却能支撑高并发
- 纯内存操作
- 核心是基于非阻塞的 IO 多路复用机制
- 单线程反而避免了多线程的频繁上下文切换问题
Redis常见性能问题和解决方案:
1).Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。
2).Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
3).Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
4).Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内
(5) big key造成内存倾斜问题,解决方案是尽量分拆redis key,如list,set这种集合数据
(6)热点key访问导致CPU负载过高,解决方案是尽量将热点key缓存到本地
Redis的内存淘汰策略,
volatile-lru:从设置过期时间的数据集(server.db[i].expires)中挑选出最近最少使用的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。
2. volatile-ttl:除了淘汰机制采用LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。
3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。
4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。
5. allkeys-random:从数据集(server.db[i].dict)中选择任意数据淘汰。
6. no-enviction:禁止驱逐数据,也就是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失,这也是系统默认的一种淘汰策略
过期策略:
- 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
- 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略
Redis最适合的场景
会话缓存(Session Cache)
最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件
排行榜/计数器
Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”。
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORES
Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。发布/订阅
kafka面试题
聊聊Kafka的特点
- 可靠性:Kafka是分布式的、可分区的、数据可备份的、高度容错的
- 可扩展性:在无需停机的情况下实现轻松扩展
- 消息持久性:Kafka支持将消息持久化到本地磁盘
- 高性能:Kafka的消息发布订阅具有很高的吞吐量,即便存储了TB级的消息,它依然能保持稳定的性能
Kafka与Zookeeper是什么关系?
Kafka的数据会存储在zookeeper上。包括broker和消费者consumer的信息 其中,
broker信息:包含各个broker的服务器信息、Topic信息
消费者信息:主要存储每个消费者消费的topic的offset的值
1)图中,除了包含前面说到的生产者Producer、Kafka集群以及消费者Consumer三个角色之外,还包含了用于存储信息的注册中心-Zookeeper
2)生产者:很明显,它是消息的生产者,用于发送消息的客户端
3)消费者:消息的消费者,用于消费消息的客户端。
4)消费者组:kafka的消费者角色,还有消费者组的概念,也就是说每个消费者组中可以包含多个consumer。其中,Kafka规定,消费者组中的消费者不能同时消费topic中的同一分区
比如说,图中,消费者组中包含Consumer A 和Consumer B两个,对于有两个分区的topic A,Consumer A消费了partition0,这时Consumer B就不能消费partition0的消息了,它只能消费partition1中的消息
延伸出消息如何保证顺序?
因为队列的先进先出的特点,保证了消息在发送的时候是有序的,而在同一个分区中,它是被一个消费者所消费的,那么它就也可以在一个分区中,保证消费消息时的顺序性。而在一个有两个及两个以上的topic内的话,就不能保证消息的顺序性了
因此,想要保证消息的顺序性,只在新建topic时,指定一个分区即可
5)Kafka集群:消息存储转发的地方,一般是集群的方式存在,而每个集群节点称为一个broker
6)Zookeeper:用于存储broker信息和消费者信息
7)broker:即Kafka集群的一台机器,可包含多个Topic
8)Topic : 主题,可以理解为一个队列
9)Partation: 队列Topic的分区,一个Topic可以分为多个分区,用于高并发场景的负载功能;实际上Topic只是一个逻辑概念,真正存在的是分区
10)Offset:即队列中当前读取消息的位置。顺便说一下,kafka的存储文件都是按照offset.kafka来命名,使用offset做名字,就方便查找。例如你想找位于2035的位置,只要找到2035.kafka的文件即可
mysql面试题
1.mysql优化:
scheme优化
选择更小的数据类型,占用更少磁盘,内存和CPU缓存
简单就好,如IP用整形存储,可减少CPU周期
尽量避免NULL
选择合适的范式标准,范式与反范式搭配使用
设计缓存表与汇总表
索引优化:
1)适合索引的列是出现在where子句中的列,或者连接子句中指定的列;
2)基数较小的类,索引效果较差,没有必要在此列建立索引;
3)使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间;
4)不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可
水平扩容-分库分表
垂直扩容-拆分服务
mysql深度分页问题:
2.mysql事务原理(mvvc)
3.mysql事务
一、事务的基本要素(ACID)
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
二、事务的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读
三、MySQL事务隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
dubbo面试题
1.dubbo服务发布与消费过程
服务发布:
获取注册中心,协议等信息
获取服务接口,方法,参数以及参数类型信息
将以上信息形成url注册到注册中心,同时注册中心还会记录接口与服务以及版本等对应 信息
启动netty暴露服务
服务消费:
根据该服务接口创建一个代理对象
该代理对象将接口,方法,版本,负载均衡策略,集群容错策略等信息封装为invocation,然后将请求发送给netty进行处理
netty接收请求,将请求转到NettyHandler->AbstractServer->AbstractEndpoint->
AbstractPeer->ChannelHandler处理,而dubboProtocol在创建nettyServer时传得参数是
ExchangeHandlerAdapter,故业务处理实际指得是
ExchangeHandlerAdapter.reply方法内容
将消费端传得信息解析位Invocation,最终执行invoker.invoke方法
通过该方法,可以根据具体负载均衡策略,接口,方法,参数信息,版本号获取最终具体服务实例,服务本地都会缓存一份与注册中心相对应的服务地址以及服务实例列表
2.dubbo泛化调用
3.dubbo超时时间优先级设置
4.dubbo集群容错机制,负载均衡
5.dubbo SPI
高并发解决方案:
解决高并发的几种方法_qq_41444863的博客-CSDN博客_高并发三种解决方法
elasticsearch原理
倒排索引:聊聊 Elasticsearch 的倒排索引 - 知乎
如何保证幂等性,zk为啥是CP而不能保证可用性,zk脑裂及解决方案,zk选举机制,分布式事务解决方案
词频作用:排序
ES健康状态判定:聊一聊Elasticsearch的健康状态_Thinkgamer博客-CSDN博客
幂等性,zk为啥是CP而不是AP,ZK选举机制以及脑裂问题解决,分布式事务的解决方案