Java
- ArrayList Vector LinkedList Set区别
- ArrayList: 底层数组实现,通过索引访问,o(1)查询时间复杂度,插入删除性能不佳,线程不安全,需要扩容时扩大为原来的0.5倍
- Vector: 底层数组实现,也是List的实现类,线程安全,集合需要扩容时扩展为原来的1倍大小
- LinkedList: 底层采用链表实现,插入和删除效率高,只移动指针即可,但是查询效率为o(n),适合高频插入删除场景,线程不安全
- HashSet: 无需存储集合,底层使用HashMap存储,key存储Set中的值,value为null, 所以无法通过下标访问
- fail-fast快速失败 fail-safe安全失败
fail-fast: 指的是java.util.* 下面的非线程安全的集合在多线程并发修改(或一边遍历一边修改)时出现ConcurrentModifyException的
问题,快速失败抛出异常。
扩展:对于非线程安全的集合在每次获取时会判断一个modCount变量值,该值会在每次集合修改时自增,
开始迭代时会获取一次当时的modCount值,如果迭代过程中发现不一样就会抛出异常。
fail-safe: 指的是java.util.concurrent.* 下面的安全集合在多线程并发修改时不会出现ConcurrentModifyException问题
- HashMap数据结构
HashMap底层数据结构采用数组+链表的形式,通过对key进行hashCode运算,将得到的值与size取模放到指定的数组位置
,如果存在哈希碰撞问题,将会在出现重叠的位置形成一个链表,查找时,如果某一个key对应的值是一个链表会在进行key的equals比较
,equals相同的返回。
jdk1.8为了优化搜索性能,当链表的元素大于8个时,会转换为红黑树,红黑树是一个平衡树,查找时间复杂度为o(logn).
HashMap在创建时可选参数:初始化容量、loadFactor负载因子(默认0.75),如果明确知道存储的元素个数,最好直接指定初始化容量
,loadFactor负载因子在HashMap内存储的元素个数 >= loadFactor * 容量 时会进行rehash, 此时会影响性能
,所以推荐初始化容量大小为:实际元素个数/loadFactor。
-
常见的集合类型接口
List Set Map Collection
-
HashSet TreeSet的区别
HashSet 底层采用HashMap实现,实际数据存储在HashMap的key中,无序并且不可重复,可以存在null值(HashMap特性)
TreeSet 底层采用TreeMap实现,实际存储使用TreeSet中的key中,有序并且comparable,不能存在null值(TreeMap特性)
- LinkedHashMap底层实现
LinkedHashMap底层采用数组+双向链表形式,继承HashMap后重新实现了Entry,Entry保存在数组中也是通过hash值计算存储位置
,不过Entry除了kv值外还增加了before/after两个指针,分别指向Entry的前一个和后一个,在put时除了放入到数组的指定位置
,还会将Entry使用指针连接。LinkedHashMap额外提供了accessOrder参数,用来标识是否按照访问顺序构建双链表,默认按照插入顺序
,如果按照访问顺序,则每次get后会将被访问到的元素放到链表头,因为链表的插入更新为只需移动指针所以性能很好。
(该属性可以实现LRUMap,删除最近最少使用的元素,只需继承LinkedHashMap并重写removeEldestEntry方法返回true即可)
继承自HashMap所以允许null值。
此外HashMap中存在未实现的方法afterNodeInsertion afterNodeAccess afterNodeRemoval在LinkedHashMap中有实现
,LinkedHashMap中进行链表的操作用来实现accessOrder功能。
- 集合接口类为什么不实现Cloneable和Serializable接口
接口设计的原则就是单一职责,对于集合类接口最主要的职责就是实现集合特性,序列化和克隆功能留给子类实现
,根据子类需要选择是否要进行序列化或者克隆操作,以方便进行对象的传输和复制。
集合的子类如HashMap、HashSet、ArrayList等都有实现浅拷贝和序列化接口。
- 什么是迭代器 (Iterator)
实现了Iterable接口的类需要通过iterator()方法可以返回一个Iterator迭代器,Collection集合接口继承了Iterable
,所以所有子类都可以迭代,子类实现Iterator接口,可以对元素进行next访问。
- Iterator ListIterator的区别
ListIterator继承自Iterator添加了一些List访问方法。
Iterator只能向前next遍历,ListIterator可以前后移动。
Iterator可以迭代Set/List集合,ListIterator只适用于List集合。
- ArrayList为什么在不使用Iterator对集合循环时,边循环边修改会出现ConcurrentModifyException,而用了Iterator迭代修改就不会
对于各个集合子类实现的Iterator来说,在使用Iterator进行循环并删除元素时,在Iterator.remove方法内部对元素操作后
,将expectedModCount更新为modCount,所以再次获取是检查两个值是否相等时不会出现异常。
但是在ArrayList.remove中没有exceptedModCount更新操作, 多以在get时checkModification就会失败。
- Comparable Comparator接口
Comparable 接口:可比较接口,如果希望对象本身支持比较,可实现该接口,同时实现compareTo(obj)方法
,用于当前对象与其他对象作比较,如Arrays.sort(comparableList), Collections.sort(..)
Comparator 接口:比较器接口用于比较两个对象,如果这两对象本身不可比较或者不想使用对象本身的比较逻辑时
,则此时可以使用该接口实现自定义的两个对象比较逻辑
,如: collections.sort(list, (a,b) -> {...return 0/1/-1;}) lambda表达式对list进行排序并自定义比较逻辑
- Collection 和 Collections 的区别
Collection是集合顶级接口类,如List、Set继承该接口定义集合的基本操作
Collections是集合工具类,提供了一些集合的操作静态方法,如: binarySearch/sort/emptyXXX/UnmodifiableXXX/synchronizedXXX...
- heap堆 stack栈区别
堆在jvm里用于分配和存储实际对象数据,栈为运行时数据区,除了存储基本类型数据外,还存储指向堆内存的引用。
堆中1字节对齐,栈中4个字节对齐,所以基本类型除了long/double占用两个位8个字节,其他类型都占用4个字节。
堆线程共享,栈线程私有
- Java类加载过程
加载 -> 链接(校验-准备-解析) -> 初始化 -> 使用 -> 卸载
其中链接阶段还细分为3个小阶段:
1. 校验: 检查字节码是否符合java规范,例如:字节码caffebaby开头、字节码版本是否兼容、语义合法性分析。
2. 准备:为_类变量_分配存储空间。
此时final static的字段会赋值为设置值,static类型的字段赋默认值(如int类型赋值0)
实例变量不会在此阶段进行内存分配,实例变量在运行期间直接分配在堆上
3. 解析:将常量池中的符号引用转换为直接引用
主要针对以下7类符号引用进行转换:类/接口、字段、类方法、接口方法、方法类型、方法句柄、调用限定符(.)
初始化阶段:static类型的字段赋值为设置的值, 并使用类的构造方法初始化实例对象
- Jvm加载class文件的原理机制(类加载器、双亲委派)
Jvm内置类加载器主要有3种:
1. Bootstrap ClassLoader 该加载器使用c++编写的加载器,负责加载lib/rt.jar resources.jar charset.jar等
,负责加载Jvm虚拟机运行环境.没有parent
java9 以后:加载lib/modules内的模块文件,如:java.base、java.net等基础模块
2. Platform(Ext) ClassLoader 负责加载lib/ext目录下的jar包
java9 以后Ext ClassLoader改名为Platform ClassLoader:
加载lib/modules内的javase标准实现库的模块文件,如:java.sql、java.charsets等
3. System ClassLoader 负责加载用户应用路径的class或者jar包,又叫系统类加载器、应用类加载器
Ext(Platform) ClassLoader加载的所有类对System ClassLoader可见,因为Ext(Platform) ClassLoader是System ClassLoader
的超类,遵循双亲委派模式,在加载一个class文件时,会有以下过程:
System ClassLoader -> Ext(Platform) ClassLoader -> Bootstrap ClassLoader
当系统类加载器加载class文件时,会先委托给Platform类型的类加载器加载,Platform类加载器会委托给Bootstrap ClassLoader
加载器加载,如果他们都找不到要加载的class文件,则最后交由系统类加载器通过loadClassData加载类二进制文件。
- 类加载器是否线程安全
类加载器是线程安全的,在ClassLoader类的方法中如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) { // 加锁
...
return c;
}
}
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
// 同名类加载时会为该类创建一个对象锁,如果已经存在了则返回之前创建的
// 多线程下同一个类加载器,同名类只能有一个线程加载
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
- Java内存划分
Java中内存主要分为两个区域,一个是堆,一个是栈,堆内存用于存储对象分配,多线程共享,栈线程独有。
堆内存又可以划分为:Young(Eden/Survivor(s0|s1)) 、Old(又叫tenured终身代)、方法区(又叫永久代,java8开始被元空间)
Young年轻代:年轻代主要分为Eden、SurvivorFrom、SurvivorTo,年轻代的对象再进行回收时会将无法回收的对象存储到幸存区(survivor)
, 每执行一次gc, from/to的幸存区空间就会角色互换。
(Eden与Survivor的比值可以通过-XX:SurvivorRatio=8设置,eden:survivor=8:1:1, 两个survivor大小相同)
Old老年代:老年代主要用于存储长期存在的对象,当一个对象被在年轻代创建后,经过若干次gc还是没有回收的情况下,会进入老年代
,默认15(-XX:MaxTenuringThreshold=15),一次gc相当于对象年龄+1。
此外老年代还有担保的作用,当一个对象特别大在新生代无法创建时,会直接创建在老年代.
(老年代内存:新生代内存默认2:1,新生代与老年代的内存比值可以通过-XX:NewRatio=2设置,表示old:young=2:1)
元空间(方法区/永久代): 主要用于存储类class元信息和常量。如:字节码、常量池(静态变量、常量final)、类字段和方法等
,full-gc时会进行内存回收。
java8开始为每个类加载器分配一块空间,不同的类加载器使用不同的空间。当类加载器不再使用时gc会直接清除掉那块内存。
(java8以后本部分空间大部分直接在本地内存中分配, 默认大小没限制取决于物理内存,-XX:MaxMetaspaceSize调整大小)
- Java8中为什么使用元数据区代替永久代(方法区)
方法区主要用于存储类的元信息,如:字节码、常量池(java7放到堆里了)、类字段方法等信息,该部分信息存储java8前可以通过MaxPermSize指定大小,
java8以后参数废弃。由于该区域占用内存相对固定,所以很难调优,java8中的元数据区默认可以自动调节大小,大部分内存直接在物理内存
中分配,并且对于每个加载器都分配了独有的内存块,将类加载器和类元信息生命周期绑定到了一起,当类加载器不再使用时,直接将类加载
器的内存块全部清除,不再对类进行单独回收,省去了该区域的gc扫描,所以提升full-gc的性能。
如果持续的出现元空间垃圾回收,说明可能类加载器存在内存泄漏或者大小指定不合适。
- GC 作用及算法
GC Garbage Collector 垃圾回收器,由于物理内存有限,应用运行过程中会不断创建对象,内存会不断增大,所以需要将无用的对象占用
的内存释放,以便新对象的产生。垃圾回收器的作用就是对于经过一系列判断逻辑认定的无效对象进行回收,来回收内存。
对象存活分析:
引用计数器:jdk1.2前使用的垃圾回收算法,根据对象间的引用关系确定是否可以回收,如果对象被其他对象引用则引用计数器+1
,如果取消引用则-1,此方法最大的弊端就是无法处理循环引用问题(A<->B两个对象互相引用,但实际已不再使用)。
可达性分析:从根对象开始根据引用关系搜索查找所有可找到的对象即为存活对象,其他所有无法从根对象到达的对象为游离对象
,游离对象即标记为可回收对象。
根对象:可以是虚拟机栈中对象引用、本地方法栈中的对象引用、元空间中的常量、静态变量
回收算法演进:
标记-清除算法:将标记为可回收的对象存储空间直接清除,需要遍历所有对象速度慢,多次回收会出现可用内存不连续的情况
,导致产生内存碎片问题
标记-复制算法:开辟两块相同的内存,程序运行时只是用其中一块,当进行垃圾回收时,将存活对象复制到另一块内存中,清除原内存块
,此方法避免了内存碎片的产生,但是由于运行期间只能使用一半的内存导致内存浪费,并且当存活对象较多时复制效率低。
标记-整理(压缩)算法:将存活的对象在内存中移动到内存的一头,移动完毕后,将其他内存直接清理掉,该方式能避免内存碎片和浪费
,但是移动对象成本高导致效率比较低。
每种回收算法都有优缺点,为了尽可能的减少内存浪费和内存碎片问题,当前jvm的内存回收根据以上算法进行了内存划分设计和融合改进,
最终设计了分代回收算法。
分代回收算法:
java对象存活分为短期存活(局部变量)和长期存活(常量、全局对象等), 所以按照对象存活时间,将内存区域划分为年轻代和老年代。
年轻代:用于存放新创建的对象和短期存活的对象,发生在该区域的gc被称为minor-gc/young-gc
老年代:用于存放长期存活的对象和年轻代无法放下的大对象, 发生在该区域的gc被称为major-gc/full-gc
默认年轻代:老年代=1:2,老年代内存是年轻代的2倍,除了存放长期存活对象外还用于对新生代内存做担保。
担保:就是对于创建的大对象无法放入年轻代时会直接放到老年代创建,如果都放不下会引发minor-gc,major-gc。
年轻代又分为Eden和两个Survivor,默认比例为:8:1:1,采用标记-复制(Serial/ParNew/ParallelScavenge/G1)算法。
因为根据sun研究发现,90%的对象都是朝生夕灭,所以每次存活的对象大约为10%左右,所以只需要预留10%的空间进行存活对象复制操作即可。
预留的空间占年轻代内存的10%,既能减少内存浪费,又能避免年轻代出现内存碎片。
两个幸存区(s0,s1)存放无法回收的对象,两个幸存区同一时刻只会使用其中一个区域。
一次minor-gc过程为:将eden区和s1区存活对象复制到s0, 如果发现对象年龄>15或者s0区域放不下,放入老年代
,同时清空eden和s1内存区域。下一次minor-gc时由于s1已经清空,所以s1与s0空间角色互换其他操作相同。
老年代由于主要存放长期存活的对象,因此内存回收率不高,所以该区域一般使用标记-清除(cms)、标记-整理(SerialOld/ParallelOld)算法
minor-gc触发条件:
1. Eden区域内存不足
major-gc/full-gc触发条件:
1. 当手动调用System.gc()时,jvm会选择一个时机执行gc, 可能不是立即执行
2. 老年代空间不足
3. 元空间(方法区)空间不足
4. minor-gc时升级到老年代对象大于老年代剩余内存时,或者新创建的对象eden放不下老年代也放不下时候。(其实还是老年代空间不足)
- Java 中会存在内存泄漏吗
Java理论上语义层面不存在内存泄漏问题,这一点主要是jvm垃圾回收器自动回收的结果。但是编程缺陷或者本地方法操作有可能会引起内存泄漏。
例如:
1. ThreadLocal本地变量如果只进行set不进行remove操作会引起内存溢出,内存只增不减,最终outOfMemory
2. 集合类对象使用不当,导致集合内元素不断增加而产生的内存溢出
3. 线程池方法newCachedThreadPool创建的线程池可能因为线程数过多产生内存溢出
,newFixed/SingleThreadPool创建的线程池可能因为队列过长导致内存溢出
4. 因调用本地方法使用不当,导致本地方法资源不能释放导致的内存泄漏
- 内存泄漏 内存溢出的区别
内存溢出:out of memory, 当程序申请内存,但没有足够的空间就会出现内存溢出。
内存泄漏:memory leak, 只程序申请的内存使用后,无法释放,导致的内存泄漏。
实际上申请了内存用完没释放的都叫内存泄露,内存泄漏最终会导致内存溢出out of memory。
- 深拷贝和浅拷贝
深拷贝:拷贝对象的实际数据,拷贝完成后,新的对象操作完全不影响原对象的数据
,java中深拷贝方法:
1. 手动创建新对象,将原对象所有数据取出赋给新对象
2. 通过序列化工具ObjectInput/OutputStream将对象序列化后再读出,读取的新对象也是深拷贝
浅拷贝:默认Object的所有子类实现的clone方法都是实现的浅拷贝,除了基本数据类型外,拷贝的是对象引用,引用指向的数据区为同一块。
- System.gc() 和 Runtime.gc() 会做什么,有什么区别
System.gc()最终调用的Runtime.getRuntime().gc(),会触发虚拟机执行gc内存回收,但是并不保证会执行,也不保证何时执行
,可以在jvm启动参数中配置禁用程序调用gc,不推荐该操作方式。
- finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?
在对象被标记为可回收对象时,执行gc后会调用finalize()方法,主要用于资源回收操作,但是该方法不推荐覆写。
原因:不推荐覆写主要是在jvm垃圾回收时,如果发现对象覆写了该方法,会将该对象放到一个队列中,由单独的线程进行finalize调用,
但是何时调用不一定,所以调用前这一段时间,该对象虽然已不被引用但是仍要占用内存。
- 如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存
不会,gc时机取决于内存是否充足,如果不发生gc,即使引用置为null,原对象数据在堆中也不会被释放
- 什么是分布式垃圾回收(DGC)?它是如何工作的?
java对象不止有本地引用还可以有远程引用(rmi协议的远程过程调用),对象既没有本地引用也没有远程引用才为可回收对象。
服务器端的一个远程对象有3个地方会引用:
1. 本地引用
2. rmiregistry注册表中注册的对象引用
3. 客户端调用时,在客户端的存根stub
远程对象引用方式:
1. 客户端获取一个远程对象存根,发出lease租约通知,告诉服务器端该对象被引用了
2. 客户端定期发送lease通知告诉服务器端,远程对象还在引用
3. 如果租期到了,未收到客户端续租请求,则认定对象不再被客户端引用
- JVM 的永久代中会发生垃圾回收么?
会,当永久代内存不足时,会进行内存回收,java8去掉了永久代,使用了元空间,该部分空间取决于物理内大小。
该区域进行的gc为full-gc。
- Synchronized 原理是什么?
synchronized是java同步锁关键字,主要是利用系统互斥锁机制来实现多线程同步问题,属于非公平锁。
java编译后的class文件中可以看到被synchronized关键字修饰的代码块或者方法,会有monitorenter、monitorexit指令,这两条指令是
获取监视器锁和释放监视器锁的指令,当一个线程获取到监视器锁时,其他线程只能阻塞等待,当一个对象被一个线程获取到锁以后,对象的
锁计数器就会+1,释放-1,同一个线程可以反复获取锁,获取几次就要释放几次,所以synchronized是可重入的。
synchronized关键字还保证了多线程可见性,被synchronized关键字修饰的变量在解锁前必须同步会主内存。
jdk1.6版本对synchronized锁进行了优化,因为该锁是通过系统内核控制的互斥锁,资源消耗大,并且内核态用户态来回切换也损耗性能
,因此为了提升性能减少资源消耗,出现了偏向锁、轻量级锁、自旋锁概念,只有在竞争激烈情况下才会升级为重量级锁(系统级)。
- 为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?
悲观锁:总是认为资源存在竞争,对资源进行操作时会首先加上排它锁,独占资源使用。
synchronized悲观锁主要是使用系统mutex互斥锁实现,对资源独占。
乐观锁:认为资源基本不存在竞争,直接使用资源并记录资源版本,如果需要修改,则先判断版本是否发生变化,如果没变化直接更改。
,乐观锁的实现原理多数采用基于版本的锁控制。
CAS是系统原语操作指令,该指令能保证原子性,是一种乐观锁的实现,通过compare and swap进行加锁判定,检测是否加锁成功。
- 跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?对比下 Synchronized 和 ReentrantLock 的异同
ReentrantLock: java语言层面的cas加锁机制,是在用户态通过cas原语加锁,不需要切换到内核态,资源消耗低,速度快。
1. 可重入
2. cas原语实现加锁
3. 可中断
4. 可实现公平非公平锁
5. 需要手动释放
Synchronized: 系统级互斥锁,在内核态,加锁需要在用户态和内核态之间切换
1. 可重入
2. 系统互斥锁
3. 不可中断
4. 非公平锁
5. 自动释放(异常或同步块结束)
- java 中的锁
java中有偏向锁、轻量级锁、自旋锁、重量级锁的概念,这些概念是使用synchronized关键字锁升级的过程。
java对象的头部有markword字段用于存储哈希、偏向锁id、锁标识(轻量级锁使用)等字段
偏向锁:当只有一个线程时,遇到同步块,会使用cas操作将偏向锁id指向自己的线程id。
轻量级锁:当存在两个线程使用该同步块时,如果其中一个线程在设置偏向锁过程中失败,则会撤销偏向锁,尝试使用cas设置锁标识字段
,此时锁升级为轻量级锁。
自旋锁:在设置锁标识字段时,如果发现cas操作失败,此时会进行短暂的自旋操作(等待消耗cpu),自旋结束后再次尝试获取锁。
重量级锁:如果自旋结束后,获取锁依然失败,则升级为重量级锁,进入wait set等待队列。
此外juc(java.util.concurrent)包中的同步锁实现,都是采用cas原语实现虚拟机层面的加锁机制,避免了系统锁过重问题。
如ReentrantLock、ReadWriteLock、CyclicBarrier、CountDownlatch、Semaphore等内部都通过内部类实现AbstractQueueSynchronizer
(AQS)类来实现同步操作。
代码层面上的锁优化主要集中在:
1. 锁粗化:例如synchronized关键字加锁的代码块如果位于循环体内,尽量应该扩大到循环体外,这就是锁粗化。
2. 锁细化:细化的规则是尽量缩小加锁范围,使线程能尽快释放锁,减少锁占用时间
3. 锁消除:如果代码不需要加锁不需要加锁,比如使用了StringBuffer拼接字符串,就不要再对该部分字符串拼接加锁了
- 为什么juca(java.util.concurrent.atomic)包下的XXXAdder做累加功能要比AtomicXXX要好?(比如AtomicLong LongAdder)
AtomicLong采用cas做自增自减操作,虽然能实现多线程同步,但是竞争激烈情况下,大量线程同时调用xxxIn/Decrement等方法时
,会有很多线程调用失败,同一时刻只有一个线程能通过cas设置成功。
LongAdder也是采用cas原语做的增减操作,但是在此基础上,引入了copy on write的机制,使多线程并发情况下性能远高于AtomicLong。
如果没有竞争现象会直接使用cas此时与AtomicLong效果一样。
copy on write机制:线程在获取到LongAdder对象进行加减操作时实际上会创建一个cell对象,对cell进行加减操作
,此时就相当于单线程独享的一个状态,所以操作不会失败而且速度很快。最后将所有cell进行一次longAccumlate累加操作。
此外在CopyOnWriteArrayList中也使用copy on write机制实现多线程并发操作安全性。
- AQS 框架是什么
AQS AbstractQueueSynchronized,java提供的锁同步抽象类,通过继承该类可以实现自己的加锁逻辑。java本身提供了较多实现
,比如:ReentrantLock、ReadWriteLock、Semaphore、CountdownLatch等,他们内部都是通过内部类继承AQS实现加锁和释放逻辑。
AQS 内包含一个基于链表实现的等待队列(CLH队列FIFO),用于存储所有阻塞的线程,还包含一个state变量用于标识加锁状态。
扩展:
CLH锁是一种自旋锁,能确保无饥饿性,提供先来先服务的公平性。所谓的自旋是指:当线程试图去拿已经被其它线程占有的锁时
,当前线程不会进入阻塞态,而是进入一个死循环去自旋的获取锁,获取到锁之后退出死循环。
同时CLH锁也是一种基于链表的可扩展,高性能,公平的自旋锁,申请线程只在本地变量上自旋轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
CLH队列是一个隐形的链表,空间复杂度低。
CLH锁包含mypre和mynode节点,mynode节点为true表示需要获取锁,然后mypre节点指向前一个线程的mynode结点,之后
,该线程执行自旋判断mypre指向的结点是否为false,直到mypre指向的节点为false则退出自旋,获取到锁,使用完毕
,unlock时将自己的mynode节点设置为false,此时指向该线程mynode节点的线程便可以获取到锁并执行后续的线程都采用该逻辑。
- ReadWriteLock 和 StampedLock
ReadWriteLock: 基于AQS实现的读写锁,使用cas进行读写锁获取和释放,内部AQS维护被阻塞的CLH线程读写队列
,但是在竞争激烈情况下,cas操作可能大部分是无效的,会导致cpu占用高的问题。
StampedLock:是对读写锁的一种优化实现,可以实现读写锁相互转换,内部不是采用AQS框架实现的锁处理逻辑,不支持锁重入,对于写锁的
重入会导致死锁等不确定性问题。该锁基于乐观锁的版本控制实现,每次获取锁都会得到一个stamp, stamp为0表示获取失败
,否则获取成功,释放锁需要传入得到的stamp,分别支持读锁、写锁、乐观读锁,前两种与RRW读写锁一样
,乐观读锁在进行读取时不会阻塞写锁获取,可能会导致数据不一致问题。
内部采用类似CLH队列的方式维护着因获取锁而阻塞的线程队列,与CLH区别在于,如果新进入的线程获取读锁
,并且链表尾部也是读锁会直接使用cowait引用追加到该读线程节点后面,唤醒时同时唤醒,
如果最后一个节点是写锁线程,则连接到该写线程链表后面,按照顺序一个个唤醒(如下图)
ReentrantReadWriteLock:RRW 中写锁可以降级为读锁,读锁无法升级为写锁
参照:https://segmentfault.com/a/1190000015808032?utm_source=tag-newest
- 如何让 Java 的线程彼此同步?你了解过哪些同步器?
线程同步方式:
1. 多线程读,单线程写情况可以使用volatile关键字
2. 多线程读写可以使用AQS同步锁或者synchronized关键字,AQS同步器如:ReentrantLock、ReadWriteLock、Semaphore
- CyclicBarrier 和 CountDownLatch 对比下
CyclicBarrier:
1. 可实现同步多线程等待,内部ReentrantLock实现同步机制
2. 可重置reset重用
CountDownLatch:
1. 可实现多线程同步等待,内部自己实现AQS来实现state变量操作
2. 不可重用
- Java 中的线程池是如何实现的
java中线程池维护一个线程池集合和一个任务队列,线程池启动后会创建core个线程(根据需要可以提前启动也可以根据任务到来现启动)
, 创建的线程通过不断获取任务队列中的任务,并调用任务的run方法执行任务,新提交的任务会被放入到任务队列等待执行,当指定任务队列
大小后,任务数量超过队列最大值,则新提交的任务会再次创建线程直到活动线程数=max配置的最大线程数。到达最大线程数后不在创建,
如果此时任务再次超过队列最大长度,则根据设置的执行策略,对任务进行抛弃、直接执行、丢弃最老的等操作,支持自定义策略。
线程池预启动:preStartAllCoreThread
- Java 默认实现的线程池
Java中提供了Executors工具类,实现默认线程池的创建;
newCachedThreadPool: 缓存线程池,适合于大量执行时间短的异步任务,默认6秒过期,任务到达时如果有空闲线程直接使用空闲线程
,否则创建新线程,最多可以创建Integer.MAX_VALUE,使用SynchronousQueue(该队列特性,队列内不超过一个元素).
newFixedThreadPool: 固定大小的线程池,该线程池拥有固定大小的线程数,并且空闲线程不会过期,新任务提交有空闲线程则执行
,否则提交到队列中,使用无界队列LinkedBlockingQueue.
newSingleThreadExecutor: 一个线程的线程池,能保证任务顺序执行,并且如果因为任务异常导致线程异常终止,会有新的线程启动代替
,使用无界队列LinkedBlockingQueue.
newScheduledThreadPool: 定时执行的线程池,周期性的执行某个任务,如果任务因异常导致线程终止,会有新线程启动代替。
- JMM Java Memory Model 什么是Java内存模型,线程间如何通讯?
Java内存模型分为:工作内存(CPU寄存器、高速缓存L1-L2-L3)、主内存(RAM内存)两块。
逻辑上与JVM内存对应关系:
工作内存:对应JVM虚拟机栈、程序计数器、本地方法栈,线程独享,操作的数据是主内存中的数据拷贝,外部不可见
,对于数据的修改操作最终要通过save指令传回到主内存,因此多线程对于相同数据修改会造成数据不一致问题。
主内存:对应JVM中虚拟机堆内存,分配的对象最终都存储在主内存,工作内存需要操作某个数据时,通过load指令加载到工作内存。
线程间的通讯通常有两种方式:
1. 共享内存
2. 消息传递
Java中的保证数据可见性:
1. volatile
被该关键字修饰的变量,要求修改后马上同步到主内存,并且任何线程使用时需从主内存中读取,不能直接使用,只能保证数据可见性。
2. synchronized
同步锁, 对象加锁成功,其他持有的线程必须清空本地工作内存该对象的数据,使用时重新加载,能保证数据的原子、一致、可见性。
3. final
无this溢出时多线程安全, this溢出: 构造方法中final常量还未完成赋值,对象构造函数就返回了(异步初始化)。
Java线程中共享堆内存,可以通过volatile变量保证数据可见性,但是不能保证数据原子性,只适用于单写多读的情况
,此外同步关键字synchronized可以实现数据lock和unlock,被加锁的数据能保证加锁成功时,其他线程持有的该对象数据拷贝会被清除
,使用被加锁的对象需要重新从主内存中load。
- volatile 是否能保证多线程安全
volatile语义要求所有被该关键字修饰的变量每次读取操作必须从主内存中读取,进能保证数据可见性,对于多线程写存在线程安全问题。
多线程写操作无法保证原子性、一致性。
- ThreadLocal 是怎么解决并发安全的?
ThreadLocal用于线程执行期内的数据共享,ThreadLocal本身内部持有一个引用指向Thread对象内变量
,Thread类中存在一个ThreadLocal.ThreadLocalMap的变量,最终数据存储在当前线程内,所以多线程互不影响。
对于ThreadLocal的使用需注意以下几点:
1. 推荐将ThreadLocal定义为静态常量。因为实际上ThreadLocal数据存取操作最终都是对当前线程的操作。
(ThreadLocal.get()方法内其实最终是获取当前线程内的ThreadLocalMap,然后通过ThreadLocalMap获取或者设置值,就相当于调用
一个类的静态方法,只要没有使用全局变量,则每个线程的调用都是线程安全的,所以ThreadLocal声明为静态全局类型即可)
2. 线程退出时需要及时通过remove()方法删除掉线程数据。虽然线程数据最终实际存储在线程内,但是忘记remove会导致内存占用不能及时释放。
ThreadLocal.ThreadLocalMap中的的Entry实现为WeakReference,只有在内存不足时才会被回收
- Java中的引用类型
强引用(StrongReference): 平时通过变量声明赋值形式的变量引用都为强引用,强引用的变量作用域内任何时候都不会自动清除
,即使出现outOfMemeory
弱引用(WeakReference): 弱引用通过WeakReference<T> 持有的变量引用为弱引用,弱引用在内存不足时会被gc回收。
软引用(SoftReference): 软引用通过SoftReference<T> 持有的变量引用为软引用,软引用在发生gc是就会被回收。
虚引用(PhantomReference): 虚引用通过PhantomReference<T> 持有的变量为虚引用,虚引用任何时候获取都返回null
,主要用于垃圾回收标记对象引用。
- 2pc两阶段提交、3pc三阶段提交、tcc(try-confirm-cancel)事务补偿
2pc : 2phase commit protocol 强一致、中心化原子协议
中心化主要是体现在需要一个协调者,由协调者负责与数据库通讯主要分为以下过程:
1. 事务发起方发送事务请求给协调者
2. 协调者发送prepare请求给事务参与的资源持有者(数据库)
3. 数据库收到prepare请求,开启本地事务并执行,但是不提交,执行完毕ack响应给协调者
4. 协调者收到ack响应后,如果都正常,发出提交请求给所有参与者,否则发出混滚请求给所有参与者
5. 如果协调者发送prepare请求后长时间无法收到响应,则发出abort请求放弃事务
存在问题:
1. 性能低,prepare后资源一直被占用,过程漫长,性能较差
2. 单点故障,如果协调者出现异常,进行中的事务无法正常结束,导致数据库处于中间状态并长期阻塞
3. 一致性问题,如果第二阶段发送提交/回滚请求时,存在某个资源持有方为正常接收或处理导致数据不一致
3pc : 3phase commit protocol 在2阶段提交基础上增加了canCommit、协调者与参与者timeout机制
1. 事务发起方发送事务请求给协调者
2. 协调者发送canCommit请求给所有参与者
3. 参与者检查自身是否正常,是否能进行事务,通知协调者是否一切正常
4. 协调者如果收到所有响应都正常则发送preCommit请求,否则发送abort放弃
5. 参与者接收到preCommit请求, 执行本地事务,但是不提交,响应执行结果给协调者
6. 协调者如果收到所有响应都正常则发送doCommit请求,否则发送abort请求
7. 如果协调者发送某次请求后一直无响应,到达超时时间后发出abort请求
如果参与者开始事务后长时间无提交请求,到达超时时间后自动回滚释放资源
存在问题:
1. 只是在2pc基础上对参与者做了超时设置并增加了一次参与者健康度检查canCommit,减少了资源长时间不释放的情况,
无法解决数据一致性问题,如果doCommit操作最终参与者无法都完成,依然存在数据一致性问题
tcc : 事务补偿机制,2pc/3pc都是数据库层面的事务保证,而tcc是应用层面的事务保证,强调最终一致性
1. try阶段,事务发起方分别调用参与方的try接口对资源进行预留
(就是对操作的资源进行冻结, 如:库存-1,则其他应用只能操作(当前库存量-1)的数量)
2. confirm阶段,事务发起方分别调用参与方的confirm接口
(就是对冻结的资源进行实际操作,如:冻结的库存1,对库存进行-1操作)
3. cancel阶段,如果try阶段失败,分别调用参与方cancel接口
(就是对资源进行释放,如:冻结的库存清零)
存在问题:
事务补偿机制相比2pc/3pc吞吐量大大提升,但是对业务代码侵入严重,需要由业务代码保证数据一致性,
业务代码需要实现两套接口,分别用于正向事务操作和反向回滚操作。此外还需要保证接口的幂等性。
- BIO、NIO和AIO的区别
BIO: Blocking IO 同步阻塞io, 每个io操作由一个线程负责,并且读写操作也在线程内阻塞完成。
NIO: New/NoBlocking IO 同步非阻塞io, 由一个线程负责轮询所有io操作(java中selector),一旦有io操作
,将io交给另外一个线程执行。jdk1.4引入
AIO: Asynchronized IO 异步非阻塞io, 不需要线程轮询,读写事件由系统完全负责,当出现读写事件时
,由系统通知线程处理读写操作。 jdk1.7引入
- NIO的组成
NIO主要有:Channel、Buffer、Selector
Channel & Buffer : bio是基于流(Stream)和字节(byte)的(面向流),而NIO是基于管道(channel)和缓冲区(buffer)的(面向缓冲)。
Stream和channel的最大区别在于Stream是单向的,要么读(inputStream)要么写(outputStream)
,而channel可以进行读写操作,经缓冲区发送和接收.
Selector: 多路复用选择器, 用于监听通道channel上的事件(读、写、打开关闭), 单线程监听多事件。
- Netty的线程模型
Reactor模式是一种设计模式,主要是针对多客户端连接处理的一种高效解决方式,Reactor模式主要涉及3个部分:
1. 处理连接请求的acceptor,用于接收客户端连接, 并将建连后的连接注册到事件分发器上
2. 负责事件(读/写)分发的dispatch,用于将读写事件分发给业务处理器(dispatch通过selector轮询事件)
3. 负责处理读写事件的具体业务处理器handler
Netty的线程模型设计基于Reactor模式实现,核心通过EventLoopGroup实现boss和work两个线程组,boss线程池负责accept接收请求并将
建立的请求封装为NioSocketChannel交给work线程池处理, worker线程池通过handler对channel进行读写操作。
Netty当前支持3中Reactor线程模型
1. 单线程Reactor模型:boss和worker线程使用同一个EventLoopGroup,accept(OP_ACCEPT)和读写事件都由同一个reactor线程处理
,selector到事件后交由handler处理。(客户端连接的感受也是异步非阻塞的)
总的来说就是由一个线程内的一个selector选择器负责事件轮询和处理,所有操作都在一个线程完成。
2. 多线程Reactor模型:boss由一个线程负责selector轮询accept和读写事件,但是将轮到的事件交由worker线程池处理
,读写业务逻辑最终由线程池异步处理,适用于大部分高并发场景,但是由于事件轮询处理依然是单线程的
,所以selector在高并发连接请求情况下会出现瓶颈。相比单线程模型就是将事件处理放到了线程池中。
3. 多Reactor模型(主从Reactor模型):boss使用EventLoopGroup(多EventLoop)作为主Reactor线程池
,多个reactor线程(selector)轮询连接(tcp握手、认证等)和读写请求(每个selector处理一部分连接)
,将轮询到的读写(OP_READ/OP_WRITE)事件,交给worker线程池,然后由单独io线程池继续处理。
EventLoopGroup内有n个EventLoop(线程),作为boss线程池时,用于selector轮询socket事件(OP_CONNECT、OP_ACCEPT、OP_READ
、OP_WRITE),作为worker线程池时,用于处理boss线程分派下来的io读写事件。
- TCP 粘包/拆包的原因及解决方法
由于TCP网络传输以字节流形式传输,所以避免不了传输过程中的粘包和拆包问题,对于该类问题一般采用应用层解决
假设当客户端连续发送2次数据,服务端接收数据时,可能存在如下情况:
1. 客户端发送了2次,服务端接收了2次数据,并且每次接收的都是完整的客户端数据,此时没有粘包拆包问题
2. 客户端发送了2次,服务端接收了一次,一次接收了两条数据,此种情况为粘包(两次数据粘到了一起)
3. 客户端发送了2次,服务端接收了两次,但是第一次接收了客户端第一次发送的所有数据和第二次发送的一部分数据,
第二次接收了客户端第二次发送的剩下部分数据,此种情况叫拆包(第二次数据被拆开了,当然第一次接收的也有粘包)
这类情况一般是由于客户端发送缓存区与发送数据大小不一致导致(或者接收方接收缓存读取不及时)
,当发送数据>发送缓存时,会导致拆包,发送数据<发送缓存,会导致粘包。发送缓存默认1500bit大小。
解决方法,应用层区分:
1. 发送请求头指定发送数据大小,服务端读取时读取指定大小的数据
2. 发送固定长度的数据(不足时用特殊字符补位),服务器端读取定长数据
3. 发送数据指定分隔符,服务器端读取到一次分隔符时,认为一次数据接收完成
- 了解哪几种序列化协议?如何选择序列化协议?
1. Java的序列化方式,通过实现Serializable/Externalizable接口, 使用ObjectInput/OutputStream实现序列化和反序列化
2. xml 将对象转化为xml结构化形式的数据,用于序列化传输
3. json 相比xml跟为简洁的数据表示方式,常用于网络序列化传输
4. thrift 是一种rpc框架,实现了自己的序列化方案,可以使用文本或二进制,节约带宽可用二进制传输
5. avro hadoop中的子项目,支持json和二进制形式,性能可与protobuf媲美
6. protobuf 谷歌开源的序列化工具,需要通过工具根据.proto描述文件生成序列化类,完成序列化,内存小性能高
对于序列化协议使用,根据不同场景选择:通常对于数据传输性能和带宽要求不高的服务可以使用xml
,对于终端接口类少量数据传输有带宽和速度要求的可以使用json序列化,对于大量数据或者高性能场景的数据传输可以采用二进制序列化协议。
- Netty的零拷贝实现?
对于java中流读写操作,通常需要在用户态和内核态之间通过syscall切换,而且还在读取时需要先从内核态拷贝到用户态,写出时需要从
用户态写入到内核态。用户态与内核态之间的来回切换和复制操作降低了应用性能。
java上的零拷贝是使用mmap系统调用将用户空间内的一段内存直接映射到内核空间,相当于直接操作内核空间
,避免了数据拷贝和用户态内核态之间的来回切换。(如kafka、rocketmq等都使用了0拷贝技术,FileChannel.transferTo方法操作直接内存)
netty中采用管道channel和缓冲区读数据进行读写操作,零拷贝的实现主要得益于缓冲区,与上面的系统层面mmap不同,netty中使用
UnPooled工具类操作ByteBuf全部在Java层面也就是用户态操作,ByteBuf使用的是直接内存进行读写操作,避免直接内存和jvm内存间的拷贝
(如ByteBuf实现直接内存操作可以直接封装byte数组,CompositeByteBuf实现多个ByteBuf合并也不需要拷贝)。
此外netty中的FileRegion通过java的FileChanel.transferTo实现0拷贝,可以直接将数据通过FileRegion发送出去,不需要先拷贝到
buf内在发送。
零拷贝是Netty高性能的原因之一。
- Netty的高性能表现在哪些方面?
1. 基于reactor模式实现的EventLoopGroup,非阻塞式NIO多路复用selector
2. 零拷贝技术
3. 内存池缓冲区重用,ByteBuf支持重用
4. 无锁串行化,基于reactor,对于每个channel使用一个单独的io线程处理读写
Spring
- Spring框架及主要组成
Spring框架是一款轻量级开发框架,通过ioc/aop特性大大降低了开发复杂度。使开发人员更多的精力放到业务实现上。
主要组成:
spring-core: 框架核心接口和实现ioc特性,其他模块都依赖于该模块。
spring-aop: 实现aop特性,支持切面编程,常见的如spring对于事务的支持。
spring-beans: bean管理,bean工厂和装配
spring-context: 上下文管理,spring的context,是spring bean容器特性的实现,可以访问容器中的bean资源
spring-orm: 提供orm接口定义规范,可以集成三方orm实现,如:mybatis、hibernate、jpa等
spring-web: 提供了web编程支持,是springmvc容器的实现
- IOC / DI 控制反转 / 依赖注入
IOC: 控制反转, 对象的创建由开发人员转移到spring容器来实现,减少了开发量,降低了耦合,更利于后期扩展和维护。
DI: 依赖注入,spring启动扫描bean时会自动将对应类型的bean注入到代码中,代码块可以直接使用。
- BeanFactory 与 ApplicationContext的区别
ApplicationContext是BeanFactory的实现,是spring的核心容器,BeanFactory是spring容器接口的定义,用于管理spring中的bean,
ApplicationContext作为spring上下文,除了实现了bean的管理,还实现了事件发布、资源访问、国际化等接口。
BeanFactory属于懒加载,只有在get获取bean时才加载bean,该方式启动速度相比ApplicationContext快,但是启动时无法检查bean是否存在问题。
ApplicationContext启动时就会加载所有bean,所以启动速度会慢一些,可以启动阶段发现bean配置问题。
- Spring 几中配置方式
spring支持配置方式:
1. xml配置式,此方式在spring2前广泛使用,通过xml配置容器bean、事务、通知等
2. 注解配置,spring2(jdk1.5+)开始支持bean的注解,减少了配置量
3. java编程配置,spring3开始,推荐采用java编程配置式,使用@Configuration注解配置类,编程实现bean的配置。
- Spring bean生命周期
1. 实例化阶段 -> createBeanInstance() 创建bean, 如果实现了InstantiationAwareBeanPostProcessor会在创建前后执行postProcessBefore..
和postProcessAfterInstantiation() 方法
2. bean属性赋值 -> populateBean() 设置bean的属性,如bean实现了XXXAware等接口会将对应对象set到bean属性上。
3. 初始化 -> initializeBean() 初始化bean调用bean的init-method方法初始化,如果实现了BeanPostProcessor还会再bean初始化前后
执行postBeforeInitialization/postAfterInitialization方法
4. 销毁 -> close() 容器关闭时销毁bean, 调用bean的destroy-method方法释放资源。
此外,常见的生命周期接口如:InitializingBean(初始化)、DisposableBean(销毁)
- Spring bean的作用域
Bean的作用域:
singleton: 单例模式,所有线程共用一个bean,每次获取都是同一个bean,无状态的bean使用该作用域。当bean的方法处理逻辑不涉及到
bean对象中的实力变量时(无状态),bean是多线程安全的。
prototype: 原型模式,每次从容器获取bean时,都会创建一个新的bean,有状态的bean使用该作用域
request: web环境下,每次请求都会创建一个新的bean, 只存在于WebApplicationContext上下文
session: web环境下,同一个session会话使用一个bean, 只存在于WebApplicationContext上下文
globalSession: web环境下,全局session中,一个bean对应一个web实例,如portletContext,只存在于WebApplicationContext上下文
Spring Bean的作用域默认为Singleton
portlet: java的web组件,由portlet容器管理,生产动态内容,portals使用portlet实现可插拔用户接口组件,作为系统表示层。
多个portlet生成页面片段组成一个portal页面展示出来。portal是作为网关web站点
- Spring容器 SpringMVC容器
spring中分为spring容器和springmvc容器,spring容器是springmvc的父容器,spring容器在spring启动时创建
,并将service/dao层的bean加入到容器中,springmvc容器在DispatcherServlet初始化时创建,是Controller的容器,
子容器中的bean可以使用父容器中的bean,反过来不可以。
如:controller是springmvc中的bean,该bean中可以使用spring容器中的service bean,但是不能反过来。
- Spring 的inner bean是什么
Spring中inner bean类似于匿名类对象,可以在xml中配置property属性或者构造函数时,在内部通过<bean>标签配置一个对象最为参数
传递进去,这种形式为inner bean, 对于inner bean不需要配置id和name.
例如:
<bean id="beanA" class="com.abc.BeanA">
<property name="beanB">
<bean class="com.abc.BeanB">
<property name="name" value="tom" />
</bean>
</property>
</bean>
- Spring中注入collection对象方法、注入java.util.Properties的方法
Spring的xml配置中支持list、set标签注入集合对象,通过map标签注入HashMap对象:
<list>
<value>hello</value> // 除了string类型还支持通过ref标签:<ref bean="beanA" /> 用其他bean
</list>
<set>
<value>hello</value> // 除了string类型还支持通过ref标签:<ref bean="beanA" /> 用其他bean
</set>
<map>
<entry key="1" value-ref="bean1" />
<entry key="2" value-ref="bean2" />
</map>
java.util.Properties对象注入方式:
<property name="a">
<props>
<prop key="p1">hello</prop>
<prop key="p2">world</prop>
</props>
</property>
- 什么是Spring的自动装配,如何开启基于注解的自动装配?
Spring中对于使用@Autowired注解属性,在容器启动时,会自动将对应类型的bean注入到属性中。
@Autowired支持的装配方式:
1. byName: 根据bean名称自动装入
2. byType: 根据bean类型自动装入,多个相同类型的bean时需要使用@Qulifier或者@Primary指定使用哪一个
,还有一种方式将属性名定义给要使用的那个bean名称相同即可(比如User user1, 有两个bean分别user1,user2,则匹配user1)。
3. constructor: 把与bean构造器入参相同的其他bean装入到该bean的构造参数中
4. autodetect: 先使用constructor方式,如果失败则使用byType, spring3开始废弃
5. none: 不自动装配
使用@Autowired 注解属性,将自动开启自动装配
- Spring有哪几种注入方式?
1. 构造方法:通过构造方法constructor-arg参数注入bean依赖(xml), 编码方式时,实例化bean会检查构造参数类型是否有对应的bean
2. setter方法:通过配置bean的<property>属性注入依赖bean
3. 通过注解:@Autowired自动注入依赖bean
- Spring中有哪些不同类型的事件
ContextStartedEvent:容器启动事件(start方法)
ContextRefreshedEvent: 容器刷新时的事件(onRefresh方法)
ContextStoppedEvent: 容器停止事件(stop方法)
ContextClosedEvent: 容器关闭事件(close方法)
- FileSystemResource ClassPathResource区别
FileSystemResource需要从相对路径或者绝对路径中资源文件(在spring-config.xml中配置绝对或者相对路径)
ClassPathResource从classpath路径下搜索并加载资源文件
- BeanFactory 和 FactoryBean的区别
BeanFactory: 是Spring的核心容器,ApplicationContext容器功能就是继承自该接口,用于bean管理是ioc容器的接口定义。
此外还有其他实现如:DefaultListenableBeanFactory、XmlBeanFactory等
FactoryBean: 抽象工厂bean,用于生产其他类型的bean,提供工厂bean的定义方法: getObject、getObjectType、isSingleton。
- 静态工厂 与 抽象工厂
静态工厂:是通过静态方法产生特定对象的工厂类,如Calendar.getInstance()
抽象工厂:具有一类共同方法工厂的抽象,子类通过实现抽象工厂来生产特定的对象
,如:显示屏抽象工厂类,子类可以是电视机工厂类、电子广告牌工厂类
- 动态代理 与 静态代理 区别
静态代理:代理类与被代理类继承自同一个接口或者抽象类,代理类只能代理实现了相同接口或者抽象类的类对象。
动态代理:在运行时动态生成代理类,代理类不需要实现被代理类的接口或者方法,更灵活一些。比如:jdk的代理、cglib的代理都是动态代理
两种代理方式最大的区别就是:静态代理需要实现被代理对象所实现的接口或者抽象类,而动态代理不需要,静态代理class文件是编译期间
生成,动态代理在运行期间才会生成。
(也就是说静态代理是一个普通的类看得见摸得着,动态代理只有运行时才会动态生成class文件默认只存在于内存中)
- Spring中使用了哪些设计模式
1. 单例模式:Spring的bean默认实例化方式为singleton,单例,所有线程共用一个bean
2. 工厂模式:如SqlSessionFactory、DateTimeFormatterFactory、LogFactory、ProxyFactory...
3. 模板模式:如JdbcTemplate、RedisTemplate、RestTemplate等
4. 建造器模式:RedisTemplateBuilder、RestTemplateBuilder、BeanDefinitionBuilder、RequestBuilder...
5. 责任链模式:Inteceptor接口(拦截器链)、HandlerExecutorChain(获取HanderAdapter)、AOP(切面链)...
6. 代理模式:Spring中aop的实现(Spring事务)、bean实例实际上是bean的代理类
7. 组合模式:CompositePropertySource、ViewResolverCompsite..
8. 适配器模式:HandlerAdapter、WebMvcConfigureAdapter...
此外还有迭代器模式、装饰器模式、构造器模式、原型模式(prototype)等
SpringBoot
- 什么是SpringBoot
SpringBoot是在Spring基础上做的自动化封装,免去了复杂的配置,并且自动管理Spring组件间的版本关系,避免了手动配置Spring时出现的冲突问题,
通过SpringApplication.run方法可以直接运行,实际上在运行run方法时,SpringBoot做了自动扫描、自动装配、自动加载位于Spring.factories
文件中的组件并启动内置web容器等操作,实现了0配置运行。大大降低了配置复杂度。
- SpringBoot的核心配置文件有哪些?
application.properties/yml SpringBoot相关的配置都可以在该文件中直接配置覆盖默认值,如端口号、profile、path等
log日志文件配置:简单的日志配置也可以直接放在application.yml中,但是一般推荐单独配置log日志配置文件,如log4j.xml
logback-spring.xml等
- SpringBoot的核心注解
@SpringBootApplication : 复合注解=@EnableAutoConfiguration + @Configuration + @ComponentScan
@EnableAutoConfiguration:启用自动配置,自动加载Spring.factories内的自动配置组件
@Configuration : 标注该类可以作为Spring容器Bean的定义类
@ComponentScan :组件扫描,指定扫描哪些包,用于扫描bean或者配置类
@EnableScheduling : 启用定时任务
@ConfigurationProperites :将配置文件与java对象绑定,加载配置文件内容赋值给注解对象bean
- SpringBoot自动配置原理
SpringBoot在启动时,会通过@EnableAutoConfiguration注解通过@Import注入importSelector的实现类AutoConfigurationImportSelector
,通过AutoConfigurationImportSelector扫描spring-boot-autoconfigure/META-INF/spring.factories文件加载自动配置类
,然后执行自动配置类完成对指定组件的加载。期间做exclude的判断,剔除掉排除的自动配置类。
- SpringBoot需要独立容器运行吗?
不需要,默认情况下,SpringBoot内嵌tomcat容器,通过bean形式管理容器组件,可以自定义替换默认内嵌容器,如替换为jetty容器,
如果不需要内嵌容器,可以将内嵌容器排除掉,将SpringBoot打包为war放到外部容器中。
- SpringBoot运行方式有哪几种?
1. ide中直接运行main方法
2. mvn命令 mvn spring-boot:run
3. 打包为jar,通过java -jar xxx.jar运行
- SpringBoot中的监视器是什么
SpringBoot可以通过引入actuator组件,可以实现对SpringBoot中的bean、配置、restful接口、内存等信息的监控。
- SpringBoot2.x有哪些新特性
1. 实现了对java8-9的支持,并且最低需要java8版本
2. 提供webflux响应式编程:异步事件驱动
3. 支持quartz集成
简化了一些配置,更多去官网上看new Futures
- Spring如何捕获全局异常
使用@ControllerAdvice注解捕获controller全局异常
- RequestMapping与GetMapping区别
GetMapping是SpringBoot提供的get方法注解简化版,直接通过该注解实现get请求处理。
RequestMapping是Spring的注解,可以实现GET/POST/PUT/DELETE等请求方法的处理
Spring Cloud
- Spring Cloud是什么
SpringCloud是基于SpringBoot实现的服务治理,SpringCloud包含许多组件,通过组件间的注册管理,实现应用的微服务化、去中心化。
SpringCloud常见组件包含:
spring-cloud-netflix-eureka 服务注册中心,所有服务需要注册到注册中心,服务间调用通过服务名称查找,此外还可以使用nacos
spring-cloud-gateway/spring-cloud-netflix-zuul 网关服务,对外提供统一入口,通常实现限流/认证授权等操作
spring-cloud-netflix-feign(robbin) 服务间接口调用服务,用于将一类业务接口集成到一个地址上对内部服务间暴漏
spring-cloud-netflix-hystrix 熔断服务,用于接口限流降级,防止服务因流量过高而不可用, 此外还有阿里的sentinel
spring-cloud-config 配置服务,负责配置统一管理,还可以使用阿里nacos
spring-cloud-bus 服务间消息总线,用于服务间消息事件,一般需要借助mq实现, 例如配置更新
spring-cloud-sleuth/zipkin 链路追踪服务,通过接入该模块实现请求跨服务链路追踪,需要引入依赖(低侵入),可以使用skywalking无侵入
其他还有消息类组件如kafak、安全类组件如oauth2等
SpringCloud优势在于微服务化、去中心化能够较好的实现集群扩展和容灾,在服务数量不是特别巨大的情况下,能够较好的实现服务的治理
,但是服务数庞大子项目过多时,项目间的调用关系复杂存在协议不统一等问题。(因此又有了ServiceMesh服务网格的概念)
- 什么是SpringCloud服务注册与发现
SpringCloud中的服务通过http将自身注册到注册中心(eureka/nacos), 首先将注册中心的服务信息拉取到本地缓存起来
,每隔一定的间隔上报一次自己的状态,定期更新本地缓存的服务信息,服务间的调用通过服务名称调用,在本地缓存中查找服务名对应的地址
,直接通过实际地址调用对需要的服务获取数据。
- 什么是hystrix?
hystrix是netflix实现的断路器,用于在服务压力过大或者调用失败时实现断路,以保护后端服务不被打垮,此外还可以通过callback定义
对接口进行降级处理,使接口返回信息更友好。
- 什么是feign
feign是用于后端服务间调用的网关服务,一般将一类业务接口集成到feign服务中,feign服务相当于对一类后端服务的代理,
本身集成了robbin/hystrix可以实现被代理接口的负载均衡和熔断降级操作。
MyBatis
- Mybatis核心组件
Configuration: Mybatis基本配置
SqlSessionFactory: SqlSession的创建工厂
SqlSession: 一次crud会话持有的连接,相当于connection
Executor: sql执行器,负责sql语句生成和查询缓存
TypeHandler: java对象与结果集映射处理器
-
Statement/Parameter/ResultSetHandler: 语句、参数、结果集处理器
MappedStatement: 具体的sql语句封装类
SqlSource: 根据参数动态生成sql,并封装为SqlBound
SqlBound: 持有动态sql及参数信息
- MyBatis 与 Hibernate 有哪些异同
1. Mybatis和Hibernate都是jdbc底层封装,通过orm映射实现对象与库表关系映射
2. Mybatis采用原生sql编写,灵活性大,简单易用,可移植性差;
Hibernate采用jpa实现crud,复杂sql支持不好,学习复杂度高,可以移植性好
3. Hibernate开发效率高,代码量较Mybatis少
-
{}和${}的区别
#{} 是预编译占位符,通过PreparedStatement设置参数,防止sql注入
${} 是字符串替换占位符,直接将原生字符串替换带,一般不推荐使用
- 表字段与对象字段不一致时如何处理
1. sql语句通过as 转换为对象字段
2. 使用resultMap映射
- Dao接口的工作原理
1. 对于标注了@Mapper的接口类,会以接口全限名生成一个命名空间,就是xml配置式的namespace, 接口中的每个方法会生成一个MappedStatement
, Mybatis维护一个列表,key为命名空间+方法名,value为MappedStatement。
2. Mybatis初始化后,会为每个接口类通过jdk动态代理生成一个代理类MapperProxy,该代理类拦截方法执行,并在invoke方法中执行具体
的crud操作。
- Mybatis 是如何进行分页的?分页插件的原理是什么?
Mybatis本身提供了RowBound实现分页功能,该分页是内存分页,将所有数据加载到内存,然后取其中的部分数据,数据量大有问题,不推荐使用。
一般采用分页插件实现分页,如:SqlHelper。如果自定义分页实现可以实现Mybatis的Interceptor类,拦截查询请求,拼接参数进行分页。
- 如何执行批量插入
sql: insert into names (name) values (#{value})
java参数:List<String> names = Arrays.asList("a","B","c")
- 如何获取自动生成的(主)键
通过设置useGeneratedKeys、keyProperty属性,在执行插入操作后,直接通过对象.getId()获取主键
- mapper中传参
传递过个参数可以使用#{0}、#{1}按照顺序获取。但是推荐采用@Param注解指定参数名称,通过#{name}获取。
- 动态Sql执行原理及常用标签
动态sql是在进行xml标签解析式根据标签类型动态生成sql语句。
常用标签:trim where set foreach if choose when otherwise bind
bind: 可以将OGNL表达式的值绑定到一个变量中, 后续直接引用,如:
<select ...>
<bind name="bindName" value="'%'+name+'%'"/>
SELECT * FROM a where name like #{bindName}
</select>
- Xml 映射文件中常见标签
insert/update/select/delete/resultMap/sql/include/parameterMap..
- Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复
如果没有填写namespace,则只能有一个xml没有namespace,因为MappedStatement是通过Map<namespace+methodName, MappedStatement>
映射的;
如果都有填写namespace,则可以两个namespace相同,一般namespace除了用于区分实体接口,还用于二级缓存处理,同一个namespace下的缓存
对应一份。
- 为什么说 Mybatis 是半自动ORM 映射工具
Mybatis通过手动编写sql来进行对象关系映射,所以称为半自动orm映射。
Hibernate会自动处理对相关系映射,不需要sql,所以为全自动orm映射。
- 一对一,一对多的关联查询,有几种方式
1. 使用association标签关联其他sql
2. 联合查询、子查询,然后通过resultMap映射实体
- Mybatis持延迟加载及原理
在进行association关联查询时,支持通过配置lazyLoaddingEnable属性,实现懒加载。
实现原理:通过jdk动态代理拦截get关联属性的方法,当调用该方式时,发出sql执行查询操作。
- Mybatis 的一级、二级缓存
一级缓存:SqlSession级,同一次会话缓存到HashMap中,进行重复查询时直接使用缓存数据,默认开启
二级缓存:作用域为namespace,同一个namespace下缓存有效,默认不开启,默认采用HashMap内存缓存,可以使用第三方实现如:ehcache
,使用二级缓存时,对象要实现序列化接口
缓存更新机制:当缓存作用域(一级SqlSession/二级Namespace)内出现CUD操作时,作用域内的select查询缓存会被清除。
- 什么是MyBatis的接口绑定
1. 通过@Mapper注解实现的接口类内通过@Select/@Update等注解方法绑定查询语句
2. 通过xml配置,并将namespace指定为接口全名,复杂的sql可以使用xml配置(但是注解也支持SqlProvider书写复杂sql)
- Mybatis 的插件运行原理
Mybatis支持对4种接口方法实现拦截:ParameterHandler、ResultSetHandler、StatementHandler、Executor。
使用jdk动态代理为拦截接口类(Mapper)生成代理对象,通过代理对象方法执行前后执行拦截器。
实现方式:实现Interceptor接口并重写intercept()方法(需要将插件配置到配置文件中)
网络
- 网络 7 层架构
物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
四层架构:网络接口层、网络层、传输层、应用层
- TCP/IP 原理
TCP(transmission control protocol传输控制协议)/IP(Internet Protocol)是一个协议簇:
TCP:位于传输层,提供可靠的数据传输,是在IP协议基础上设计的,用于消息校验(如次序、完整性等),TCP只指定源端口和目的端口
,通过与IP协议结合,完成网络间主机的路由,到达主机后由TCP协议负责将数据传输到指定端口。
IP:位于网络层,在底层通讯基础上提供主机到主机的连接,IP是尽力传送但是不可靠的协议, 无法对消息进行验证,也无法保证消息的顺序
,只能路由到网络间主机,配合TCP协议端口完成应用层数据的定位
应用层用户数据向下到达传输层会被TCP协议包装为 TCP 数据段,当TCP数据段在网络层传输时,会被包装成IP数据包
,IP数据包通过网络接口按帧传输到目的地址,在目标主机上对数据进行解包操作。
TCP数据段首部包含:源端口号、目的端口号、seq发送序列号等
IP数据包首部包含:版本(ipv4/6)、源地址、目标地址、ttl、首部长度、总长度、分片和重装信息等
tcp协议栈见下图:
TCP 三次握手/四次挥手
三次握手:TCP连接需要经过3次握手后才能传输数据,具体过程如下:
1. client发送SYN请求(带一个seq=x序列号),进入SYN_SENT状态
2. server接收到SYN请求,发送SYN请求响应ACK=1(ack=x+1,seq=y),进入SYN_RCVD状态
3. client接收到服务端的SYN后进入ESTABLISHED状态,发送ACK响应(ack=y+1)
4. server端进入ESTABLISHED状态
5. clent-server间数据传输
四次挥手:TCP断开时需要经过四次挥手,具体过程如下:
1. client端发送FIN完成请求(携带seq=m序列),进入FIN_WAIT1状态
2. server端响应ACK(ack=m+1), 进入CLOSE_WAIT状态(此时服务端可能还没发送完数据,所以可能不会马上发送FIN请求)
3. client端接收到请求后,进入FIN_WAIT2状态
4. server端发送FIN请求(携带seq=n序列), 进入LAST_ACK状态(服务端发送完数据了可以结束)
5. client端接收到服务端的FIN完成请求,响应ACK请求(ack=n+1),进入TIME_WAIT状态,等待2MSL(Max Segment Lifetime)
6. server端收到client端回应,进入CLOSE状态(如果未收到还会重新发送FIN请求给客户端)
2MSL的作用:
客户端最后一次响应后等待2MSL主要有两点考虑:
1. 防止回应未能到达server端,网络中ip数据包在其ttl时间后自动过期丢弃,如果被丢弃,则server端会重试,此时client如果关闭
,server端会不正常关闭;client等到2MSL时,收到server端重试后会再次发送响应请求。
2. client端等到2MSL继续占用端口,防止网络中存在还未到达的数据包, 如果不等待,其他连接使用该端口后可能会收到上一个连接的数据
,等待2MSL能保证网络中没到达的数据已过期。
- HTTP 原理
HTTP协议是超文本传输协议 HyperText Text Transfer Protocol, 基于TCP/IP协议设计的应用层协议,是一种无状态协议,采用C/S模式
对数据进行传输。
Http协议包含:请求地址、请求头、请求体,响应头、响应体
一次数据传输流程如下:
客户端将请求数据封装为Http数据包 -> TCP数据包 -> IP数据包 -> 传输数据帧 -> 字节二进制流 -> 物理网卡发送
- HTTPS
HTTPS是在Http协议之上加上了SSL(TLS)层,实现对数据的加密传输。
加密传输主要有两种:
对称加密:密钥只有一个,双方都是用一个密钥进行加密解密操作,速度快安全性低,常见加密算法:AES、DES、3DES、RC5等
非对称加密:客户端持有公钥,服务器端持有私钥,公钥加密私钥解密,速度慢安全性高,常见的加密算法:RSA、DSA等
传输过程如下:
1. 客户端发送连接服务端443端口的请求,并携带自己可以实现的算法列表和其他信息
2. 服务器端回应本次会话(连接建立后)通信需要使用的算法,然后发送证书给客户端(证书中包含域名、有效期、颁发机构、公司、公钥等信息)
3. 客户端收到证书,判断证书有效期、域名是否为请求域名、颁发机构等信息,有效则使用使用公钥加密一段随机数发给服务器端
4. 服务器端收到后使用私钥解密,得到随机串,后续的通讯过程采用该随机串使用之前协定的加密算法加密数据,通讯过程为对称加密
随机串是客户端和服务端3次握手期间生成的,每次生成一个随机数,最后将3次随机数拼接作为通信密钥(1次随机可能不够随机),后期通信
使用该随机串作为密钥加密解密数据,该方式主要是解决非对称加密速度慢的问题,所以使用对称加密,因为对称加密密钥是在握手期间使用
公钥私钥非对称加密获得的,所以对称密钥是安全的。
- RPC HTTP的区别
1. RPC 效率高于 http,http每次请求需要3次握手,需携带请求头
2. rpc复杂,自定义传输协议和调用协议,涉及到服务治理
3. rpc采用长连接,http1.1开始通过connection:keep-alive也可以实现长连接
4. rpc多用于大型分布式项目后端互通调用,http多用于web站点
Redis
- redis特点
redis是基于内存存储的非关系型数据库(c语言编写),采用k-v存储结构,性能强大,常用作高速缓存、数据存储。
单线程无锁化操作,避免了线程切换开销,支持原子性操作。支持pub/sub模式、数据过期。
可以将数据通过rdb、aof方式保存到磁盘,防止数据丢失,提高数据安全性
- redis支持的数据类型
支持5中数据类型:string、list、hash、set、zset
redis中所有类型的数据都是redisObject对象结构,该结构包含:
type(数据类型string\list..)
encoding(编码方式, redis中每种数据结构至少有两种编码方式)
refCount(被引用数)
lru(最近一次被访问的时间)
ptr(指向实际值的指针)
string: redis中的最基本类型sds(Simple dynamic string),是redis自己实现的string数据结构,没有采用c的string
,在redis中任何数据存储都需要设置一个key,类型也是redis的sds类型,sds结构,sds相比c中的string最大的好处就是
保存了字符段长度、总长度,在sds中有一个char数组,使得在字符串超过长度时可以自动扩容。
基本编码方式:int、embstr、raw
int: 整数类型时使用
embstr: 长度小于39字节时
raw: 不符合以上情况使用改编码,当string类型改变时,转换为该编码类型,无论改变后如何
list: 列表类型数据,可以当作链表、队列、栈使用(最大元素个数Integer.MAX_VALUE),基本编码方式:ziplist、linkedlist。
ziplist: 为节省内存开发的编码方式, 连续的内存存储数据,当数据元素个数少,并且总大小小于一定值时使用该编码方式
linkedlist: 双端链表,可以保存不同的数据类型,链表head和tail分别指向链表的头和尾,链表中的每个节点都是redisObject类型
hash: k-v型数据结构,用于存储键值对,时间复杂度o(1),基本编码方式:ziplist、hashtable
ziplist: 同list数据类型中的ziplist编码方式
hashtable: 由1个dict结构,2个dictht,1个dictEntry指针数组组成。
dict: dict对象结构就是redis中hash数据类型的结构
dictht:在dict对象中有一个表头指针数组指向dictht,存储两个指针,分别指向ht[0],ht[1],哈希表头的作用就是存储
哈希表的元信息包含指向dictEntry数组的指针、元素个数、已用的slot等信息,ht[0]和ht[1]默认情况下,
使用ht[0],ht[1]对象的dictEntry数组指针指向null,当ht[0]中元素个数达到最大值或者哈希碰撞严重时,
需要进行rehash操作(类似于java中HashMap的rehash操作),会直接在ht[1]上生成一个新的dictEntry数组,
大小是ht[0]中dictEntry数组的两倍,然后执行rehash操作将ht[0]中的元素hash到ht[1]中的dictEntry中,
rehash结束,将ht[0]的dictEntry指向ht[1]的dictEntry,ht[1]的dictEntry指向null。
(由于存在dictEntry数组十分巨大的情况,针对该情况直接rehash可能会影响其他操作,所以对于大的dictEntry
,使用渐进hash方式,就是每次对hash表操作时,仅rehash一部分元素,直到全部rehash完成)
dictEntry:指针数组中存储的就是指向dictEntry对象指针,dictEntry对象就相当于HashMap.Entry,当存在哈希碰撞时
,将相同hash值的dictEntry与已存在的dictEntry生成一个链表(与Java中hash碰撞处理方式相同)
set: 集合类型数据存储,无序不能通过索引访问,不能有重复元素(最大元素个数Integer.MAX_VALUE),支持交集、并集、差集操作。
基本编码方式:intset、hashtable
intset: 当元素类型为整数、元素数量较少时使用intset编码方式,内部采用数组实现,集中存储节省空间
hashtable: 同hash类型中的hashtable编码方式
zset: 有序集合、不能重复,通过score分数来实现集合内元素的排序操作,基本编码方式:ziplist、skiplist
ziplist: 同list类型的压缩列表编码方式,主要为节省内存,小数据量使用
skiplist: 跳跃表,类似于平衡树的数据结构,搜索效率高,时间复杂度在o(logn)到o(n)之间,主要有skiplist、skiplistnode组成
,skiplist为索引指针节点存储跳跃表信息,skiplistnode是具体的跳跃表节点。
(根据score将元素节点换分为若干段,每个分界值作为上层索引,最多16层,查找时效率类似于平衡树,形状也像平衡树)
- Redis是单进程单线程的?
redis在未开启rdb、aof数据持久化情况下,是单进程单线程的,当开启rdb或者aof时,redis会fork一个子进程用于数据刷盘操作。
- redis虚拟内存
当redis内存配置较小时,可能会使用虚拟内存,数据会被swap到磁盘,将大大影响redis性能。
通过info命令,查看mem_fragmentation_ration判断是否使用了虚拟内存:该值=used_memory_rss/used_memory
used_memory_rss :redis进程占用内存 + jmalloc分配内存(数据存储),不包含swap的虚拟内存
used_memory : redis分配的总内存包含jemalloc分配 + swap虚拟内存
该比值通常应该>1,如果使用了虚拟内存,则该比值<1,则说明内存不足,使用了虚拟内存,需要增大redis内存。
理想情况下应该在1.03左右,如果过大说明存在内存碎片,需要重启redis清理碎片,重启后,redis会重新规划内存。
关于内存碎片:
redis中分配对象有三种类型,small、large、huge
small: 存储小对象,最小8个字节,以8的倍数取值,最大到512个字节
large:存储大对象,超过512字节,小于4kb
huge: 存储巨型对象,大于4kb,小于4mb
如果一些key对应的值频繁更新,并且更新前后内存占用变化大,时间长了就会产生内存碎片。
- Redis锁
redis可以实现分布式锁,来解决分布式应用资源加锁问题,类似的解决方案还可以使用zookeeper实现。
redis中比较健壮的锁实现,可以使用第三方redison.
核心原理通过setnx实现加锁 ,如果设置成功,则获取锁,否则获取失败,此外加锁的key需要设置一个过期时间,方式锁删除失败
导致其他服务无法获取到锁
- 读写分离模型
redis支持master-slave模式,通过客户端redis库可以将读请求转发到slave节点,将写请求转发到master节点,完成读写分离操作,
(阿里云中提供了redis-proxy实现后端redis代理实现读写分离操作,twiter的Twemproxy开源项目)。
读写分离集群通常有以下两种方式:
1. 星型结构,master为中心节点,其他slave从中心节点同步数据,slave越多master压力会越大,带宽会越高
,但是数据一致性良好,并且任意一个slave故障不影响其他slave.
2. 链式结构,1-2个slave直接从master节点获取数据,其他slave从前两个slave上同步数据,master压力小
,但是数据一致性一般,如果一级slave异常容易导致所有从该slave同步数据的其他slave数据不可用。
- redis-分片
当数据量过大时,需要将数据分散到不同的redis实例上,以缓解单台redis的压力和数据量问题。
当前的分片方式,一般采用客户端分片,常用方式为:将key进行crc32运算,得到的数值与redis实例数取余,余数就是要落到的第几个redis
实例,对于偏压问题,可以采用一致性hash方式,根据实际redis实例数,生成一定数量的虚节点,使数据尽量平均分散到不同的redis实例。
分片可以借助代理实现也可以自行实现分片逻辑。
分片也会带来如下问题:
1. 跨redis的集合无法做并集、交集、差集操作
2. 跨redis不支持事务
3. 数据备份变得复杂
4. 添加删除redis实例,数据重新分配问题(所以最好预先做好分片,避免后期频繁扩展带来的数据分片问题)
- Redis的回收策略
当redis内存不足时,会根据内存回收策略堆内存进行回收。
当前支持的内存回收策略redis.conf中:maxmemory-policy
noeviction: 默认,不回收,达到最大内存时,执行指令报错
allkeys-lru: 所有的key,把最近最少使用的淘汰掉,适用于热点数据
allkeys-random:所有的key随机淘汰,适用于访问概率相似的
volatile-random: 设置过期的key中随机淘汰
volatile-lru: 设置过期的key中最近最少使用的淘汰掉
volatile-ttl: 设置了过期的key,从中淘汰将要过期的key
- redis相比memcached有哪些优势?
redis支持类型更多,支持数据持久化,存储小数据时性能高于memcached. memcached支持多核。
- redis常见性能问题和解决方案
1. 碎片问题,当redis运行一段时间后,如果redis中长期存在的key更新前后大小差距较大,容易产生碎片问题
解决方式:数据备份,重启redis,redis重启过程中会进行内存重新划分,redis4.0以上可以使用新增指令处理内存碎片
2. 可用最大文件句柄数,默认情况下linux系统文件句柄数为1024,如果使用默认值,socket连接过多会导致句柄数过少的问题
解决方式:增大句柄数 ulimit -n 65535
3. save/bgsave命令,会导致刷盘操作,如果数据过大,会导致其他命令被阻塞
4. 单点故障问题,redis的主从复制存在单点故障问题
解决方式:使用sentinel监控集群实例状态,以便及时提升slave
5. 主从复制性能问题:主从复制,在第一次同步时,master会dump全量文件,slave同步该文件到本地,后续增量同步,如果文件过大会导致
master上的执行命令被阻塞或者中断,并且网络抖动导致slave与master差距过大时,也会进行全量同步操作。
6. 不要使用星状集群,推荐使用链式集群,形状集群容易造成master压力过大问题。
- MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
配置maxmemory-policy使用allkeys-lru数据淘汰策略,淘汰最近最少使用的数据。
并估算20w条数据占用的最大内存,配置maxmemory为估算占用内存
- redis适用场景
1. 非关系型数据存储,点赞、收藏
2. 缓存
3. 秒杀预热、限时有效
4. 排行榜
5. 分布式锁
6. 队列、发布订阅消息
Zookeeper
- Zookeeper 提供了什么
文件系统
通知机制(watcher)
- Zookeeper的文件系统
zk的存储类似于一个树状的文件系统,并且zk的目录也可以存储数据,每一级的目录和文件都是一个节点,zk支持对节点进行事件监听,
可以对节点或者节点及子节点进行监听,修改操作时,触发事件通知。
- Zookeeper的通知机制
zk的通知机制就是对某些节点状态变化的事件监听,如:节点创建、删除、更新等操作,都会触发通知机制,客户端需要注册一个watcher,
server端出现节点更新时,发送通知给客户端,并将注册的watcher移除掉,不能保证通知一定能被客户端收到,如果客户端与server间出现
网络抖动导致通知丢失,则客户端无法得知,所以如果要保证数据可靠性,除了watcher监听外,还需要定期拉取更新本地缓存值。
- Zookeeper 能做什么
1. 分布式锁(临时节点;创建同名节点,只会有一个创建成功)
2. 配置存储(配置监听;使用watcher机制,动态更新配置)
3. 集群管理(临时节点;掉线自动剔除)
4. 服务选主(有序节点;每次选择序号最小的最为master)
5. 全局id(有序节点)
6. 队列管理(有序节点,自动追加编号,按照编号可以实现节点FIFO效果)
- Zookeeper工作原理
zk是一个分布式协调组件,能保证数据的一致性和分区容错(CP),由于崩溃恢复期存在一个选主过程(期间无法提供服务),所以不能保证可用性
,zk的核心功能是基于paxos算法实现的崩溃恢复和广播协议,也叫做ZAB协议。
崩溃恢复模式:崩溃恢复在leader不可用时,进入崩溃恢复阶段,该阶段follower间通过发送包含(sid,txid)的投票选取新的leader节点
,sid为本服务节点的serverid, txid为事务id(txid为64位数字,高32为epoch,低32为自增数),在接收到对方的投票后,
本地进行比对,txid大的获胜,并将txid大的发出去作为下一轮投票结果,如果txid相同,则sid大的获胜,直到出现某个节点
的得票数超过一半,则该节点升级为leader,txid中的epoch加1(奔溃的leader恢复将自动成为follower,因为txid小与当前leader的)
广播模式:在leader接收到写操作时,会将数据包装为一个proposal提议发送给其他follower,follower接收到以后将proposal写入本地日志
,并响应ACK发给leader,当leader接收到超过半数的ACK后,将proposal写入本地日志,并发出commit请求,其他follower接收到
以后进行commit操作。
新增的follower会将本身的txid发给leader,leader根据txid确定同步位置,然后进行follower的同步操作,同步结束后,follower状态
变为uptodate,接收客户端连接请求。
- znode节点的类型
1. 持久化节点persist:客户端断开连接后依然存在
2. 持久化有序节点persist sequential: 客户端断开后依然存在,并且节点序号自增
3. 临时节点ephemeral:客户端断开连接自动删除
4. 临时有序节点ephemeral sequential: 客户端断开连接后依然存在,并且节点序号自增
- Zookeeper的server角色
- leader: 主节点,负责客户端所有写操作
- follower: 从节点,从主节点同步数据到本地,负责写转发和读请求,参与投票
- observer: observer节点主要作用是负责集群读性能扩展但不影响写性能,除了不参与proposal投票, 其他与follower一样
observer除了扩展读性能,一般还用于跨数据中心部署,将leader和follower部署在一个数据中心,observer部署到其他数据中心
,这样在其他数据中心的客户端可以直接读取observer,减少网络延时的影响。
follower增多,能增加读性能,但是会导致集群数据同步变慢(每次写入需收到半数以上投票),也就是写操作性能下降
,observer就是为此中场景设计的,可以扩展读性能,但是又不会对写性能有较大影响,observer接收leader发过来的inform通知,
inform通知包含commit的proposal(投票), 通过inform通知学习提交的数据,observer和follower都被称为learner。
leader会发送proposal给follower节点,不发给observer节点,commit信息会发送给follower和observer节点,但是commit信息只有
zxid,所以observer无法通过commit信息获取到数据,而发给observer的inform通知包含已提交的proposal数据,observer从中获取数据。
这也就是inform通知存在的原因。
- Zookeeper是如何保证顺序一致性的
顺序一致性由leader和follower共同保证,leader负责所有的写操作,当follower收到写请求时会将请求放入请求队列发送给leader.
对于每次写请求,leader的txid就会自增1,然后将proposal发送给follower(利用tcp传输队列,能保证发送顺序性)
,follower接收到后会写入本地pendingtxn队列和txnLog中,然后响应ack给leader,当ack过半后,leader发送commit给follower
,follower得到commit后检查commit中的txid是否和队列中的txid相同, 相同则持久化,否则退出,重新与leader同步.
也就是借助于txid,保证了数据被更新后,客户端只能看到更新后的数据。
存在如下情况:
客户端连接到server上txid是最新的,但是由于网络缘故导致连接断开,当客户端重新连接到新的server时,该server可能
不是上次更新半数通过的那部分server,此时会不会获取到旧数据?
不会,客户端会记录与server通信过程中最大的txid,当服务器端发现自己的txid小于客户端连接的持有的txid时,server会
关闭当前session,此时客户端会重新连接到其他server。
如果客户端连接到的server不是超过半数那部分server,并且客户端一直连接未曾断开,则获取的数据是否会出现不一致?
会,zk保证数据单调一致性,就是同一个server读到的数据只会和上次一样或者比上次的新,对于此种情况,客户端txid可能与
server相同,读到的数据可能不是最新的,只有等到该server同步结束后,客户端才能读到txid大于自己的数据。但是永远不会读到比自己旧的。
- 关于最终一致性和顺序一致性
数据一致性一般划分为:强一致性、顺序一致性、弱一致性(最终一致性)
顺序一致性一定会产生最终一致性的结果,但是最终一致性不一定是顺序一致性。
zookeeper是顺序一致性,所以也能保证最终一致性
zk的写操作角度看是属于线性一致性,从读写角度看是属于顺序一致性。
最终一致性:数据最终能达到一致,强调结果一致性,过程不是有序的
线性一致性:写操作最终都由leader处理,到达顺序与数据写入顺序相同,数据写入成功后,txid会自增
顺序一致性:数据写入成功后,所有提交的follower的txid与leader相同,不能保证所有的客户端连接的server都是txid最新的
,但是能保证,所有客户端读到的数据的txid>=自己的txid,数据是按照txid的顺序每次获取都是当前连接txid最大的。
单调一致性:对于一次写入,如果客户端连接的正好不是投票的那半数follower,则可能txid落后于leader,此时如果客户端读取可能读到
的数据与leader不同,但是读到的数据不会是比自己持有的数据更老的数据,这就是单调一致性。对于最新的数据只有follower
与txid同步后才会被读到。
- Zookeeper的server状态
LOOKING: 当选主时,server处于搜寻状态,查找leader节点
LEADING: 作为leader节点时的状态
FOLLOWING: leader选举结束后,follower节点的节点状态
- 什么是分布式通知和协调
zk的通知机制允许在节点上注册watcher,当节点发生变化时,主动通知注册监听的客户端。
- 为什么会有leader?
1. 所有更新操作都有leader处理,减少计算量,其他节点直接同步处理结果即可
2. 降低复杂度,如果无leader节点,每个节点都处理读写操作,则节点间数据同步没有统一标准,同步过程更复杂
(wisper算法:闲话算法,无leader,节点间通过乱序交换数据,最终达到数据一致)
- zookeeper负载均衡
zk集群扩容可以通过增加follower节点或者observer节点,增加读请求处理能力,负载均衡在客户端实现,客户端通过连接不同的server,
实现请求的负载。
Kafka
- Kafka概念
kafka起源于linkedIn, 由scala和java编写,分布式消息订阅和发布系统,使用zookeeper维护集群状态
有以下特点:
1. 海量数据的堆积能力(取决于partition的个数,理论上partition越多,堆积能力越强,推荐<=12个)
2. partition的顺序性,每个partition只能有一个消费者,能保证单个partition的顺序消费(topic顺序需要设置topic一个partition)
3. 消息可回溯(消费者可以指定offset偏移量对消息进行重复消费)
4. 高可用,分区容灾(集群内多个broker,每个broker是某个partition的leader,其他partition的follower)
5. 顺序读写(消息顺序写入磁盘上的partition日志,生产和消费都为顺序读写,性能高)
6. 0.11开始支持事务和生产者幂等发布,保证消息可靠性
分区不宜过多原因:
1. 每个分区目录都有若干个log/index文件,每个分区的broker都要维护当前操作的log和index文件句柄,分区越多,broker需要维护的越多
2. 生产者和消费者都会为每个分区缓存消息,如果分区过多导致生产消费端内存占用增大
3. 降低可用性,当分区过多时,如果主分区崩溃,则其他follower分区过多导致选举过程缓慢
- Kafka的数据存储设计
Kafka数据存储采用每个partition一个文件夹,文件夹命名方式为:topicName-partitionIndex,有几个partition就会有几个文件夹,
partition的索引从0开始,每个partition文件夹内有若干个segment组成,每个segment默认大小为1gb,当达到1gb时,会生成下一个
segment文件,segment成对出现,以.log和.index结尾,.log为消息数据实际存储位置,.index为消息索引文件,采用稀疏索引,默认
每隔4kb生成一条索引,.index索引文件大小默认为10mb, segment文件命名方式为:19位长度的数字+log/index后缀文件,数字部分为
上一个segment文件的最后一条消息的偏移量,第一个segment文件偏移量0,文件名为19个0.log/index。
topic分区存储能提高写入和读取速度,分区增加可以增加读写性能。
segment小文件存储可以高效的实现文件压缩或者过期日志清理,默认1gb大小为通过mmap直接映射到内核,操作直接内存
稀疏索引能大大节省空间,避免每条消息创建索引(每隔一段生成一条索引),搜索效率类似于二叉搜索,时间复杂度o(logn)
数据文件类别:
.log文件:消息内容实际存储位置
.index文件:消息索引文件,稀疏索引,存储消息偏移量
.timeindex文件:消息时间索引文件
.txnindx文件:事务文件(0.11+)
- 生产者 & 消费者的设计
生产者设计:
1. 批量发送,kafka0.8后使用发送缓冲区,发送缓冲区缓存发送批次,默认一个批次大小16kb,有单独的sender线程负责按批次发送
,大大提高了发送性能(但是存在数据丢失问题,推荐异步发送方式获取发送结果)
2. 自定义数据结构CopyOnWriteMap, 发送缓冲区中使用该结构存储数据,key为partition,value是一个队列Deque,该数据结构保证了
数据安全提高了发送效率,实现参照了java中的CopyOnWriteArrayList
3. 内存池,生产者内存池默认大小32MB, 16kb一个批次,如果每次内存使用完毕重新申请则会导致频繁gc,kafka采用内存(BytePool)池形式,
当一个批次发送完毕将该批次16kb内存清空,放回内存池,继续使用,避免了频繁gc.
(内存池中存储的是序列化后的消息,使用ByteBuffer存储数据)
发送消息方式:
1. 直接发送,不关心是否成功,允许少量消息丢失
2. 同步发送,发送完成返回future,通过get阻塞获取发送结果
3. 异步发送,通过注册一个回调函数来异步接收发送结果
消费者设计:
1. 消费者组:处于同一个消费组的消费者对于同一条消息只能有一个消费者可以消费到
2. 消费者偏移量offset,每个消费组维持一个offset偏移量,持久化在kafka的队列里,__consumers_offsets的topic配置了日志压缩
能保证保存的消费者组与topic-partition偏移量一直是最新的(日志压缩会删除掉key相同旧的消息)
3. reblance再平衡。发生再平衡的条件:consumer数量变化、partition的数量变化
kafka提供了一个协调者负责再平衡分区分配,协调者为:当前消费者组的消费偏移量所在的分区leader对应的broker为协调者。
协调者将选择一个consumer担任leader角色,并把consumer和分区对应信息发给该leader,该leader负责进行分区,分区结束后
,将分区结果发给协调者,协调者将分区结果响应给所有消费者,完成分区。
消费组本身维护一个generation属性,当需要再平衡时,协调者将消费组generation+1,此时其他消费者发过来的心跳请求会被响应
illegalGeneration,消费者接到该响应后将本地offset提交到broker,broker检查提交的generation<=当前的则将偏移量保存到topic
,否则认为不合法,消费者端可能存在错误。
Markdown : https://gitee.com/t0mZ/notebook/tree/master/interview-skill