数据结构:
排序算法有哪些,哪些稳定,哪些不稳定
排序算法有冒泡排序、简单选择排序、插入排序、快速排序、归并排序、希尔排序、堆排序 其中堆排序、希尔排序、简单选择排序、快速排序不稳定,剩下的都稳定【快些选一堆朋友吧】
快排的整个流程思路
它的思想是分治的思想,首先选择一个基准,一般选择左边第一个数,然后选择最左和最右的两个下标i,j,就可以开始循环比较了,假如要自然排序的结果话,每当i都小于j的时候,j向左遍历,找到第一个小于基准数的数字,填到基准数的位置上,i向右遍历找到第一个大于基准数的数字填到刚才j的位置上,以此循环往复直到i不小于j,即等于或大于。这样一次排序就结束了,对于0到i-1和i+1到length-1两个数组再次治理。
红黑树的底层实现,如何变色,如何旋转
红黑树的底层实现是由节点组成,其中包含5个属性,分别是键、值、颜色、左子节点指针,右子节点指针。
它有如下几个特性:
- 节点有红色和黑色两种颜色
- 叶子节点都是黑色
- 两个红色节点不能连续
- 所有链路上的黑色节点数量相同
- 根节点是黑色
变色用于解决上面的两个红色节点不能连续、根节点为黑色的两个条件。
旋转分为左旋转和右旋转
- 左旋转中,根节点成为右孩子的左孩子,右孩子的左孩子变为根节点的右孩子,左孩子依旧为根节点的左孩子。
- 右旋转中,根节点成为左孩子的右孩子,左孩子的右孩子变为根节点的左孩子,右孩子依旧为根节点的右孩子。
接口与抽象类的区别:
相同点:
- 不能实例化
- 都可以包含抽象方法
不同点:
- 可以实现多个接口,但是只能实现一个抽象类
- 接口用于规范方法行为,抽象类多数用于继承,代码复用
- 接口中的成员变量只能用public static final修饰,不能被修改且需要有初始值,而抽象类中默认default可以被定义与修改
重写与重载:
重写:(两同两大一小)
- 方法名相同、参数相同、方法修饰符比父类大,抛出异常的类型大于等于父类,返回类型要小于等于父类
- 父类用private\final\static修饰的方法不可以被重写,但是static可以再次声明
- 构造方法不能被重写
重载: 方法名相同,返回类型,修饰符,参数不同。
深拷贝、浅拷贝、引用拷贝:
浅拷贝是创建一个新对象,然后指向被复制对象的引用。深拷贝是完全复制了整个对象,引用拷贝就是两个不同的引用指向了同一个对象
容器:
用过什么容器,hashmap底层原理、优化?
用过HashMap、ArrayList、LinkedList、Deque、HashSet、ConcurrentHashMap等
-
ArrayList底层由Object[]实现,初始化时赋值的是一个空数组,当放入第一元素时初始容量变为10,每次扩容为原数组的1.5倍,可以存储null值,但是不建议。
-
LinkedList是由双向链表实现,但是因为每次在列表中间插入时需要先定位到指定位置,所以时间复杂度也为O(n),所以相对来说平时使用ArrayList多一些。
-
HashMap/HashTable/ConcurrentHashMap:
1)HashMap底层是数组和链表结合在一起使用的,初始容量为16,扩容因子为0.75,每次扩容2倍,也就是链表散列,通过对key的hashcode进行扰动得到hash值,经过(length - 1) & hash进行判断当前元素存放的位置,如果当前位置存在数据的话,则判断是否一致,一致则覆盖,如果不一致则使用拉链法进行解决冲突。在jdk1.8之后引入了红黑树,在链表超过了阈值(默认为8)后,链表自动转化为红黑树,但是转化前会判断如果数组长度小于64,则会优先考虑扩容。
2)对于HashMap在多线程场景下不安全的问题,jdk1.7之前在扩容时会出现不安全问题,当一个桶位【Entry 数组】有多个元素需要扩容时,多线线程会同时操作链表,在头插法的情况下可能会形成死循环。在jdk1.8之后采用了尾插法避免,但是不建议使用,会有数据覆盖的风险,建议使用ConcurrentHashMap。;
3)使用ConcurrentHashMap:ConcurrentHashMap在jdk1.7前使用segment[默认16个]+entry数组+链表,原理是使用了分割分段,只对segment进行加锁,降低了线程之间的加锁冲突,提高了并发率。jdk1.8之后使用node/treeNode数组+链表/红黑树,阈值为8,并发控制使用synchronized+CAS操作,看起来就像是线程安全的hashMap。而HashTable只用了Synchronized加锁,效率非常低下。
4)对于ConcurrentHashMap不能放入null值是因为有二义性,无法判断是get的是没有值还是值就为null,所以对多线程操作会有影响,而单线程不会。可以使用putIfAbsent、compute、computeIfAbsent、merge等进行实现原子操作
- Deque是一个双端队列,在队列的两段都可以插入删除,根据失败返回结果的不同分为两类,一类是抛出异常add/remove/get,一类是抛出特殊值offer/poll/peek。也有push/pop用于模拟栈
针对容器使用时需要注意的地方:
- 集合判断空用isEmpty(),不是size == 0的方式,这样会保证复杂度为O(n),因为size对于ConcurrentHashMap会产生复杂度不是O(n);
- 使用toMap转换map时需要注意value为null的情况,因为toMap会调用merge方法,merge会首先对值进行判断非null
- 对Map进行remove操作,不要再forEach中进行才做,最好使用Iterator,并发情况是对其进行加锁
- 集合去重,不要用contains,直接转化为set,因为两者的contains实现方式不同,set为O(1),list为O(n)
- 集合转数组,必须使用toArray,传入类型一致长度为0的空数组,因为JVM优化,new String[0]作为参数效果最好,只是起到一个模板的效果
- 数组转集合,使用Array.asList不能使用集合相关方法,add/remove/clear会出现UnsupportedOperationException异常,因为只是返回一个内部类,没有重写add这些方法。所以需要new ArrayList<>(Array.asList(xxxx)),或者使用Stream的Arrays.stream(xxxx).collect(Collectors.toList());
List,Map,Set和Queue的区别
- List: 一个维护有序的集合,允许有重复值的,可以通过索引查找和操作元素,通常的实现类有ArrayList、LinkedList、Vector
- Set:一个不维护顺序的集合,不允许有重复的值,有性能比较好的去重,通常实现类有HashSet\TreeSet\LinkedHashSet
- Queue:一个队列,符合FIFO,支持头与尾的直接操作,通常的实现类是ArrrayDeque、PriorityQueue等
- Map:使用k-v存储元素,k不能重复,一个k映射一个value值,提供了高效的查找和关联功能,通常的实现类有HashMap、TreeMap、LinkedHashMap
- 都允许有null值,但是HashTable和ConcurrentHashMap都不能有null值
StringBuilder和StringBuffer的区别
StringBuilder是线程不安全的,StringBuffer是线程安全,在JDK9前,String、StringBuffer、StringBuilder都是使用char[]实现,在JDK9后,使用byte[]实现,因为byte占用的空间更少。
HashMap和HashTable的区别
- HashMap是线程不安全的,HashTable是线程安全的使用synchronized锁实现。
- HashTable不支持null,HashMap支持。
- HashTable初始容量为11,扩容方式为2n+1,HashMap的初始容量为16,扩容方式为达到扩容因子的数量后(0.75),扩容为2倍,容量为2^n可以满足一个公式:(n - 1) & hash = hash % n
- HashMap有链表转红黑树的机制,而HashTable没有
线程池的优缺点
优点:管理性好,响应快,降低系统资源 缺点:线程池需要初始化,增加了代码的复杂度,不适合所有场景,有时也需要手工的调整线程的粒度以及使用更高级的调度方式
Synchronized和Volatile的区别
- Volatile用于让变量在多个线程下可见,Synchronized用于线程同步。
- Volatile无法保证操作的原子性,Synchronized可以保证。
- Volatile比较轻,效率比Synchronized好
- Volatile作用于变量,Synchronized作用于方法和代码块
volatile的使用场景
可重入锁寄就用到了volatile,而且ConcurrentHashMap中实现线程安全的方式就用CAS+volatile+Synchronized。
具体的业务场景中:
- 简单的布尔标识,只有true,false,比如初始化+请求停机
- 简单的读写锁,并且读的操作比写频繁的多,这样读的不用synchronized,直接请求到数值
SpringBoot的优势
他是一个用于开发的脚手架,自动配置,结构简单,运行只需要maven打包就可以不用配置tomcat那些,需要添加某些外部功能只需要在maven中导入依赖
分布式限流算法
- 计数器算法,请求一次+1,调用结束-1,但是可能在前一段时间的结尾+后一段时间的开头组成的新时间内的请求数超过阈值,适合用于发送短信,60s后重试
- 滑动窗口算法,在滑动前的时间+滑动后的时间组成的新时间还是会超过阈值。spring的熔断机制使用的是这个
- 领牌桶算法,在一段时间内往领牌桶扔一定数量的领牌,每次请求从领牌桶中拿一个领牌,无法速度恒定,适用于突峰流量,redission限流使用的就是这个
- 漏桶算法,不管请求多少,服务器请求的速度是恒定的,放到队列中可以实现请求速度的恒定,但是不适合用于解决突峰流量。适用于基于MQ实现的消费者模型
IO模型
了解IO模型吗,BIO和NIO的区别?
IO模型有:1. BIO:Blocking IO、2. NIO:Non-Blocking/New IO、3. AIO:Asynchronous IO
其中BIO在发出read请求要一直阻塞到read返回数据,NIO在发出select请求后不阻塞直到内核发出ready请求后开始阻塞从发送read请求到read返回,AIO发送read后会直接返回,等内核拷贝完数据后再返回一次,是一个异步的。
BIO和NIO的区别在于:首先BIO是同步阻塞的,NIO是IO多路复用的,效率大大提升。并且由于NIO采用了Selector、Channel、Buffer,也与BIO有所不同。Channel是全双工的,既可以支持写又可以支持读,而BIO是单向的。NIO中是面向Buffer的,而BIO是面向字节的。Selector可以监听多个Channel,不断轮询在selector上注册的Channel,而BIO不行。总之,NIO与BIO的区别在于提供了非阻塞、面向缓冲、基于通道的IO。
JVM
jvm内存模型
类加载器、运行时数据区、执行引擎、本地接口、本地库;
在jdk1.7,运行时数据区中,有线程共享的堆(字符串常量池)与方法区(运行时常量池),线程私有的程序计数器,本地方法栈,虚拟机栈,本地内存中有直接内存
在jdk1.8,本地方法区改为元空间放到本地内存中,本地内存中还有个直接内存。元空间中存储运行时常量池。
元空间
在jdk1.7之前,方法区被视为永久代存放在堆中,在jdk1.8之后,永久代被取消,方法区存在元空间中,而元空间是存在本地内存中的。而永久代被取消,方法区放到元空间是为了:一是避免每次GC收集还要扫描永久代的,二是避免不知道如何设置永久代大小参数去避免OOM,三是元空间的使用空间是由系统的实际可用空间来控制,四是HotSpot代码中就没有永久代的这个概念。
垃圾回收机制,怎么判断是否回收?
有4个回收机制
标记-清除,他的过程大概是每次创建对象的时候给一个标记位,赋值为0,在标记时候对可达或客户可以引用的对象标记位赋值为1,在扫描清除时会判断哪些是0,是0则清除,但是他有两个缺点,第一个缺点是会产生零碎不连续的的空间造成空间问题,第二个缺点是标记和清除的效率不高造成效率问题。标记-复制算法,他将内存分为相同大小的两块内存,每次使用其中的一块,如果这一块使用后,将其存活的复制到另一块中,再把使用的空间都清除掉,但是他有两个缺点,第一个缺点是对于存活对象多复制性能变差,不适合老年代代,第二个缺点是一个块内存只能使用1/2,利用率低标记-整理算法,他每次发生回收时,将所有存活的对象,向一端移动,然后清除端边界以外的内存,但是多了整理这一步效率也不高,适合老年代不太需要整理的分代收集算法,对于新生代使用标记-复制,对于老年代使用标记-清除和标记-整理
判断回收的方式:
引用计数法,每次被引用则标记位加1,但是难以解决那些循环引用的情况可达性分析,以GC ROOTS为根,向下搜索,其中的路径称为引用链,如果一个对象无法达到GC ROOTS时候则说明需要被回收了,一般选择虚拟机栈的对象、本地方法栈的对象作为GC ROOTS。但是对象可以被回收不一定代表着一定会回收,在JDK9前,需要经过两次判断过程。第一次为在可达性分析中第一次被标记并且判断是否覆盖Finalize方法,如果没有被覆盖或者finalize已经被调用过则视为没有必要执行。如果需要执行则对象被放入到一个队列中二次标记,除非这个对象跟任何对象都不可达,则直接被回收
1.7默认回收器,相比较之前版本回收器优势?
jdk1.7默认使用的是G1收集器,他有如下4个特点:
- 可以实现真正的并行与并发,
利用多个CPU降低Stop the world的时间。 宏观上使用标记-整理,实际上使用标记-复制,解决了CMS的收集器中空间分散零碎的问题- 可
预测的停顿,可以由用户提供一个可以接受的垃圾消除时间,让实际上的垃圾消除的时间在这个范围内 - 分代收集,保留了分代的概念
他的流程是:初始标记-》并发标记-》最终标记-》筛选收回
他的上一代收集器是CMS收集器,他的流程为:初始标记-》并发标记-》重新标记-》并发清除,优势为hotSpot第一款真正意义上的并发收集器,并发收集低停顿,缺点为使用的标记-清除算法,会产生不连续的零碎空间啊,无法处理浮动垃圾,对CPU资源敏感。
jvm调优经验
final、finally、finalize
- final用于修饰变量、方法、类,被修饰的类不能被继承、被修饰的方法不能被重写,被修饰的变量不能被更改。
- fianlly用于try-catch-finally,无论是否捕获或处理异样都会执行里面的语句,不过如果虚拟机停止工作,就不会被执行,通常用于资源被正确释放
- finalize在GC收集的可达性分析中,在JDK9之前,如果当前对象没有引用链达到GC ROOT的话,则需要判断是否覆盖了finalize方法,如果执行过了或没有覆盖,则直接删除,如果没有执行则放到队列中进行二次标记,如果对象跟任何一个结点都没有引用链的话则直接删除,不过在JDK9之后就几乎没有用到finalize方法了,因为finalize调用时机具有不确定性