Interview
CopyOnWriteArrayList
了解写时复制机制、了解其适用场景、思考为什么没有ConcurrentArrayList
内部持有一个ReentrantLock的可冲入锁,在增加、删除是加锁,使用try finally来在finally的最后语句块当中解锁,在增加、删除时候复制出来一个新的数组,来完成增加、删除的操作,提高查询的效率,查询的时候查询原来的数组,操作完成后将新数组的引用,赋值给原来的引用。底层数组使用volatile transient修饰的数组,采用volatile transient修饰的原因,用transient修饰的数据不会被序列化,对于volatile,从java内存模型的角度,每个线程都有自己的工作内存,处理数据时候获取中内存数据的拷贝,volatile修饰之后,就禁止了这种拷贝,直接获取主内存,保证的指令可见性,而不是数据的可见性,还会禁止jvm底层对数据的重排序,使用场景,比如说CopyOnArrayList的成员变量,这种情况有多个线程并发同时访问的变量,而且既不是常量,也没有在synchronzed()的同步代码块中。CopyOnArrayList的使用场景:适用于读多写少的并发场景。为什么没有ConcurrentArrayList这样的数据结构?因为保证线程安全会有各种各样的手段,但是在保证线程安全的前提下,还要保证性能瓶颈,这就很难了,对于List这样的数据结构是做不到的,比如说在contains()这样的方法中,遍历的时候一定要锁住整个list的,采用分段锁也不合适,总不能数组有多少长度就用多少个分段锁吧,concurrentHahsMap的分段锁一旦确定后,即时数组扩容,也不会变了。即时线程安全的CopyOnArrayList也只规避了读操作的并发瓶颈。
ConcurrentHashMap
了解实现原理、扩容时做的优化、与HashTable对比。
concurrentHashMap是基于分段锁来实现的,默认是16,有一个参数叫做concurrentLevel,该值说明有多少个分段锁,segment继承自ReentrantLock,1.8采用cas来实现避免加锁提高性能,只有内存值与当前值一致,才会修改成功。Cas是乐观锁,synchronized等等都是悲观锁。Cas有一个问题,会产生ABA的问题,比如线程A B都从内存当中取了值,这是线程2修改了值,做了一些操作有把值修改回去了,这时候线程1cas还是成功的,但是线程2的操作就被忽略了,所以可以考虑加版本号,修改时除了比较当前值还要校验版本号。HashTable是直接在方法上的synchronized来保证安全的,并发是会有性能瓶颈的。扩容时候的优化可以考虑从避免rehash来回答。新建数组为原来长度的一倍,把原来的数据遍历放进来。
常见问题
- ConcurrentHashMap是如何在保证并发安全的同时提高性能?
使用细粒度的锁分段锁来实现线程安全、以及CAS来保证线程安全,不会有性能并发瓶颈。
- ConcurrentHashMap是如何让多线程同时参与扩容?
数组链表加红黑树,之前是头插,现在是尾插,链表的最后一个数据的next为null,并发扩容不会next不会产生死循环。优化角度避免重复hash。
- LinkedBlockingQueue、DelayQueue是如何实现的?
前者基于链表、后者基于优先级队列。
- CopyOnWriteArrayList是如何保证线程安全的?
新增、修改、删除,复制一个数组来完成操作,完成后指向原来的引用,读操作不会影响性能瓶颈。ReentrantLock可冲入锁。
ThreadLocal
了解ThreadLocal使用场景和内部实现
ThreadLocal的内部实现:ThreadLocal底层是有静态内部类ThreadLocalMap来实现的,ThreadLocal有一个静态内部类叫做ThreadLocalMap,ThreadLocalMap有一个静态内部类叫做Entry,entry是弱引用机制的,entry有一个属性是value,存储数据,key是当前threadLocal对象,Thread持有ThreadLocalMap引用。每个线程保存变量的副本。
使用场景:某次请求,同一个线程下数据的传递,否则只能靠方法的返回值,解耦操作。在某些框架上,如果不用threadLocal,会有耦合的问题。还可以透传全局上下文,通过设置Thread构造函数的最后一个参数initThreadLocal,设置为true。
缺点:线程池服用Thread对象,会有脏数据的问题,static 修饰会有内存溢出的问题,本来entry实现弱引用,想要通过GC来回收,value,static修饰方法区当中会有引用,导致无法回收。解决办法:remove()。
解决线上日志上那台服务器上去找?
ThreadLocal存储当前请求的traceID,根据id去找对应的日志文件。Value存IP。
ThreadPoolExecutor
了解线程池的工作原理以及几个重要参数的设置
1:核心线程数:如果为0,线程结束后所有线程都会被销毁。如果不为0,假设是n,会保留n个线程作为核心线程。
2:最大线程数:线程池中最多有当前数量的并发线程。
3:阻塞队列:LinkedBlockQueue,存储待执行的任务,使用锁来保证入队出队的原子性。
4:超时时间:多长时间回收空闲线程。核心线程默认是不会被回收的,有一个参数叫做允许核心线程超时设置为true,就可以回收了。
5:时间单位:时间单位
6:线程工厂:
7:拒绝策略:
l 丢弃任务,抛出异常(默认)
l 丢弃任务
l 丢弃最近最少使用的任务最久的
l 调用任务的run方法绕过线程池执行
线程池有几种类型:
l new FixedThreadPool()创建固定大小的线程池
l new singleThreadExecutor()创建单个线程池
l new ScheduledThreadPool()创建可调度的线程池
l new CheThreadPool()可伸缩的线程池
l new WorkStealingPool() jdk8新出的
原理:主要是execute()、addWork()这两个方法:
Execute执行任务,addWork()是用来创建线程,内部由32位的二进制数来表示,高三位表示线程池的状态,一共有五种状态,runnable:可以接受任务,shutdown:不能接受任务,可以执行正在处理任务stop:不能接受任务,停止正在执行的任务。Tyding:所有任务都已被终止Terminated:已经清理完现场。首先会判断线程数是否小于核心线程数,如果是,创建线程。否则判断线程池是否是runnable状态,如果是置于队列,不是从队列移除。
- 说一说往线程池里提交一个任务会发生什么?
Execute方法的执行流程,先判断工作线程数是否大于核心线程数,如果没有,那么就创建线程,否则判断线程是否是可运行状态,放到阻塞队列中。如果不是从阻塞队列中删除。在不满足可运行状态、或者缓存满了,达到了最大线程数,执行拒绝策略。
- 线程池的几个参数如何设置?
- 线程池的非核心线程什么时候会被释放?
达到空闲时间之后会被回收
任务执行完成之后,会设置空闲时间,到时间之后回收。
- 如何排查死锁?
引用
了解Java中的软引用、弱引用、虚引用的适用场景以及释放机制
强引用:不会被在能够被根节点可达的情况下,不会被回收。Object object = new Object();即时系统马上OOM了,也不会被回收。
软引用:有这样一个类,softRefferrence来定义,在系统OOM之前会被回收。适用于缓存,或者服务器计算的中间结果。
弱引用:YGC年轻代GC时会被回收,有WeakReferrence来调用,在引用的对象置为null时,可以主动断开连接,这是弱引用的使命。
虚引用:每一种引用都有一个对应的类来描述,定义之后就无法指向获取引用的对象。设置虚引用的目的是为了回收是获取一个系统通知。
常见问题
- 软引用什么时候会被释放
OOM
- 弱引用什么时候会被释放
YGC,当引用的对象设置为null,会自动断开引用
类加载
了解双亲委派机制
加载一个类时候,判断父类有没有加载,如果父类加载过了,就无需加载。如果没有加载,并且加载不了,就委托子类加载。
常见问题
- 双亲委派机制的作用?
避免重复加载类,在内存中有很多重复类的字节码,所以有父类加载优先加载,如果已经加载过了,子类就不能再加载了。防止内存中字节码爆炸。自定义的类加载器加载的特定位置的类,自定义类加载器加载特殊位置的类。
- Tomcat的classloader结构
- 如何自己实现一个classloader打破双亲委派
继承classloader,重写findclass方法,找到自定义目录下的文件,生成二进制流,调用defineClass()生成Class。什么时候需要自定义加载器?比如扩展加载源,比如数据库的驱动,系统自带的类加载肯定无法加载。
jvm
GC
垃圾回收基本原理、几种常见的垃圾回收器的特性、重点了解CMS(或G1)以及一些重要的参数
内存区域
能说清jvm的内存划分
Jvm的体系划分:
本地方法栈:
封装的需要调用的本地方法,比如System.currentMillions()
虚拟栈:
方法的执行就是栈帧入栈出栈的过程,正在执行的方法就是栈顶的栈帧。
操作变量表:类似于槽,存储标识地几个变量的值。存储方法的局部变量,参数,没有准备阶段,所以必须要马上初始化。
操作数栈:压栈入栈去做计算
动态链接:栈帧当中包含对常量池的引用。
方法出口:方法的返回值,包括正常返回,异常返回
堆:年轻代老年代。Eden survior1 surviior2 8:1:1。新创建的对象分配在Eden中,如果满了会触发YGC,把活着的对象放到未使用的输入survior中,每个对象都有一个计数器,每经历一次YGC,就加1,默认达到15次,就放到老年代。MaxTuringThreshold默认15次。如果一个对象elden区放不下,就会YGC,还放不下,就会放到老年代,还放不下,就会FULL GC,还放不下,就会报错。OOM。String字符串常量是在堆中。
程序计数器:CPU时间片切换到执行的不同线程,等在回到记录上一个线程执行的位置。
元数据区:方法信息、静态属性,常量,类信息、常量池
常见问题
- CMS GC回收分为哪几个阶段?分别做了什么事情?
Cms利用三种颜色来完成标记。黑:能够被GCroot直接找到的没有引用白色节点的。白色:需要回收的对象。灰色:能够被GCroot可达,但是没有扫描完成的。
Cms有四个阶段:
a) 初始标记:从GCroot出发,找到所有直接引用的节点。Stw
b) 并发标记:以上一阶段的节点为根节点,并发遍历标记。Stw
c) 重新标记:并发标记GCroot可能有引用关系的变化,所以需要重新标记。从根节点,GCroot遍历,重新标记。
d) 并发清理:并发清理所有对象。标记清理:会有空间碎片的问题。
回收的原则:浮动垃圾,之前有引用,现在没有引用了,发现不了,回收原则:可达的对象绝对不能回收,可以有垃圾没有回收。
- CMS有哪些重要参数?
1:老年代使用率达到某个阈值,开启fullGC。
2:执行了多少次的不压缩的full gc 来一次压缩的full gc。
- Concurrent Model Failure和ParNew promotion failed什么情况下会发生?
ConcurrentModelFilure:有大量对象从elden升级到老年代,老年代放不下,报的错误。解决办法:开启串行老年代收集器,回收整个老年代。
ParNew promotion failed:晋升担保失败:空间碎片的问题,来一次标记整理算法回收。
- CMS的优缺点?
缺点:有空间碎片化的问题,这是需要内存整理
优点:减少stw的次数。配置执行了多少次的full gc,采取做内存整理。
- 有做过哪些GC调优?
比如说:设置jvm的启动参数:XMS XMX最小堆内存与最大堆内存,设置成一样的大小,否则服务器会不断地收缩与扩容,增加系统的压力。调节elden区域surior区的交换次数参数,maxTurningThreshold,将年轻代的转为老年代。或者配置参数UseG1Gc启动新的G1垃圾回收器。
- 为什么要划分成年轻代和老年代?
对象的声明周期不同。
- 年轻代为什么被划分成eden、survivor区域?
采用复制算法回收,需要额外得存储区域做担保。
- 年轻代为什么采用的是复制算法?
需要频繁的做对象的创建于回收,对象声明周期较短。
- 老年代为什么采用的是标记清除、标记整理算法
老年代声明周期比较长,如果采用复制算法需要其他内存作担保,保留存活的对象。年轻代老年代分配比例是3:8。
- 什么情况下使用堆外内存?要注意些什么?
对外内存:java.nio.DirectorByteBuffer分配的内存是堆外内存,优点:提高io效率。缺点:堆外内存回收相对堆外内存来说耗时间。底层:unsafe.allocateMemory()来分配。每一个DirctorByteBuffer初始化都会有一个Cleaner对象调用cleaner()来回收。
- 堆外内存如何被回收?
- jvm内存区域划分是怎样的?
spring
bean的生命周期、循环依赖问题、spring cloud(如项目中有用过)、AOP的实现、spring事务传播
循环依赖问题:
比如说spring当中的自动装配,A依赖了B,B依赖了A,这个时候在实例化A类的这个对象时候,需要依赖B,A无法实例化,在实例化B的对象时候,依赖了A对象,B对象也无法完成实例化,需要在AutowiredBeanPostProcessor后置处理器中完成属性的注入。
Spring是这样处理的,他设计了一级缓存,已经实例化单例对象的缓存、二级缓存,正在创建中的对象的缓存、三级缓存,单例对象工厂的缓存,这样的map,在refresh()方法当中,倒数第二个方法实例化bean,首先实例化时候从一级缓存中获取,以及缓存存储已经实例化完成的单例对象,如果没有,从二级缓存中获取,二级缓存存储的是正在创建中的对象,二级缓存如果没有,有一个暴露对象的参数,true,可以从三级缓存中获取,这个时候是有的。就可以通过AutowiredBeanPostProcessor自动装配的后置处理器干扰bean的实例化过程。
- spring事务传播
当前没有事务,创建事务,有事务,就加入到这个事务中来等等。
或者就直接新建事务。
- spring中bean的生命周期是怎样的?
Spring当中完整的bean的声明周期,从构造函数初始化开始,实例化一个AnnotationConfigApplicationContext开始,接下来就可以从容器中获取bean,说明,在构造函数当中就已经完成了spring当中的bean的初始化,以及准备spring的环境。首先会调用this()函数,初始化defaultListableBeanFactory,在实例化一个reader以及scanner,实例化reader会添加5个beanpostProcessor以及一个beanFactoryPostProcessor。接下来会注册beanDefinition到beanFactory当中。Refresh()方法完成spring容器的准备,bean的实例化。初始化一个applicationcontextAware后置处理器,调用后置处理器,以及实例化bean。
- 属性注入和构造器注入哪种会有循环依赖的问题?
构造器注入会有循环依赖的问题。属性注入就是setter注入,因为构造函数马上就会创建对象,无法完成依赖注入。属性注入,可以利用到缓存。一级缓存、二级缓存、三级缓存。
l Spring如何实例化对象的?
通过反射,首先通过工厂方法来实例化对象,在通过构造函数,依据参数的类型去实例化,最后通过默认的构造函数,spring底层做了一个这样的判断。
redis
redis工作模型、redis持久化、redis过期淘汰机制、redis分布式集群的常见形式、分布式锁、缓存击穿、缓存雪崩、缓存一致性问题
@PostConstruct:缓存预热,在构造函数初始化之后执行
@PreDestroy:在bean销毁之后执行
常见问题
- redis性能为什么高?
基于内存数据库,数据结构简单,没有磁盘io,主要是io模型牛逼。
- 单线程的redis如何利用多核cpu机器?
- redis的缓存淘汰策略?
Redis的config配置文件中配置最大内存,超过之后,会有6种拒绝策略。
1:noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。 大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL )。
2:allkeys-lru: 所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
3:volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
4:allkeys-random: 所有key通用; 随机删除一部分 key。
5:volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。
6:volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。
- redis如何持久化数据?
RDB:将持久化的数据写到文件中。
AOF:记录执行的指令。
- redis有哪几种数据结构?
List 、set、 string、 hash、 sortedSet
- redis集群有哪几种形式?
哨兵模式:sentinel,监控master slave,如果master挂了,把slave升级为slave。
- 有海量key和value都比较小的数据,在redis中如何存储才更省内存?
- 如何保证redis和DB中的数据一致性?
查询如果缓存没有就查库,更新数据库,是缓存失效。
- 如何解决缓存穿透和缓存雪崩?
- 如何用redis实现分布式锁?
加锁:
解锁:
posted on 2019-05-20 21:15 心里向阳-无惧悲伤° 阅读(390) 评论(1) 收藏 举报
浙公网安备 33010602011771号