9.6每日一面

美团 后端开发工程师 一面

1.Spring AOP的底层原理

1.Spring AOP的功能实现使用了代理设计模式,增强和通知使用了适配器模式。
2.Spring AOP能够将业务无关却未业务模块共同调用的逻辑封装起来,减少重复代码,它是基于动态代理的,如果要代理的对象是实现了某个接口的,会使用JDK Proxy去创建代理对象,而对于没有实现接口的对象,就会去使用Cglib来创建代理对象。
3.步骤:在IOC容器启动的时候,会通过@EnableAspectJAutoProxy注解注入AnnotationAwareAspectJAutoProxyCreator对象,它会在所有bean创建时进行拦截,对需要增强的对象,他会对切面进行一次包装的处理,在包装的过程中,会去创建一个代理对象,而在创建它前会先根据切入点表达式对增强器一一匹配,最终拿到所有的增强器。创建代理对象过程中,会先创建一个代理工厂,获取到所有的增强器(通知方法),将这些增强器和目标类注入代理工厂,再用代理工厂创建对象。代理工厂会选择使用JDK的Proxy或者Cglib来获取代理对象。然后目标方法执行时,代理对象会先拦截目标方法的执行,然后获取它对应的拦截器链,遍历它的全部增强器,将增强器转为拦截器后也加入拦截器链,然后依次执行拦截器链中的拦截器invoke方法直到执行到真正的目标方法。

2.HashMap底层数据结构

1.JDK1.8以前,HashMap的底层实现是使用数组加链表的方式实现,在JDK1.8后,当数组长度大于等于64,链表长度大于(默认)8时,会将链表转为红黑树。

3.HashMap的扩容实现方式

先明确:容量(默认16),负载因子(默认0.75),阈值(默认16*0.75=12)
1.当元素数量超过阈值,便会进行扩容,每次扩容都是之前容量的两倍,这个容量有上限,是小于1<<30的。
2.JDK8前,如果不是第一次扩容(第一次put会初始化数组,算一次扩容,使其容量变为不小于指定容量的2的次方)新容量是旧的两倍,阈值是新容量*负载因子,然后遍历旧数组中的每一个桶,对其中元素重新计算hash(也可能不计算),对应到新数组中的位置,用头插法插入。
3.JDK1.8后,如果不是第一次扩容(第一次put会初始化数组,算一次扩容,使其容量变为不小于指定容量的2的次方)新容量是旧的两倍,阈值是新容量*负载因子,然后遍历旧数组,无需重计算hash值,只要判断其首节点目前的最高位hash值是0还是1,是0就仍在原位置,是1就在原位置+旧数组长度的位置。

4.ConcurrentHashMap如何实现线程安全

1.在JDK1.7中,他底层使用分段锁对整个桶数组进行了分割分段(Segment,分段数组),每把锁只锁其中一部分数据,这样多线程访问不同数据段的时候,就不会有锁竞争
2.在JDK1.8时,摒弃Segment的概念(但是还有,不过简化了属性,为了兼容旧版本),直接使用Node数组+链表+红黑树的结构实现,并发控制使用synchronized和CAS来实现,它只锁定当前链表或红黑树的首节点。

5.ConcurrentHashMap的size()方法加锁吗,如何实现的?

1.JDK1.7中,首先第一种方案,他会使用不加锁的方案去尝试最多三次计算size,比较前后两次的size,如果一致就认位计算结果准确,否则采取第二种方案,将每个Segment加锁,然后计算
2.JDK1.8中,它提供了baseCount、counterCells两个辅助变量和一个CounterCell辅助内部类。size方法它先通过sumCount方法计算,如果超过int的最大值,就返回int的最大值,但是还是推荐使用mappingCount方法,他会返回long。sumCount有两个重要的属性baseCount和counterCells,如果counterCells不为空,总大小就是baseCount与遍历counterCells的value值累加获取。baseCount是没有线程争用的时候使用的计数变量,他是volatile long型,在put方法结束后调用addCount方法更新它,如果在并发状态下,如果CAS修改baseCount失败,就会使用CounterCell类,创建一个对象,它的volatile的value属性值为1,并发时利用CAS修改baseCount失败后,就会利用CAS操作修改CountCell的值,如果这个CAS也失败,在fullAddCount方法中就会死循环操作直到成功

6.线程池的参数

1.阿里开发手册强制要求只能使用ThreadPoolExecutor类实现线程池
2.它的参数有:
    int corePoolSize:线程池的核心线程数量
    int maximumPoolSize:线程池最大线程数
    long keepAliveTime:当前线程数大于核心线程数时,多余的线程存活的最大时间
    TimeUnit unit:时间单位
    BlockingQueue<Runnable> workQueue:任务队列,用于存储等待执行任务的队列
    ThreadFactory threadFactory:线程工厂,用于创建线程
    RejectedExecutionHandler handler:拒绝策略,当提交的任务过多而不能及时处理,使用定制策略来处理任务
3.任务队列:
    一.ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
    二.LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
    三.PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
    四.DelayQueue:一个使用优先级队列实现的无界阻塞队列。用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。(淘宝订单业务:下单之后如果三十分钟之内没有付款就自动取消订单)
    五.SynchronousQueue:一个不存储元素的阻塞队列。你设置的最大线程数没有意义,在超过核心线程数的时候,就会执行拒绝策略了。
    六.LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    七.LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
4.拒绝策略:实现RejectedExecutionHandler接口
    一.ThreadPoolExecutor.AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;
    二.ThreadPoolExecutor.CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;
    三.ThreadPoolExecutor.DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;
    四.ThreadPoolExecutor.DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;

7.线程池大小如何设置(大小如何合理设置)

1.通过设置线程池的corePoolSize和maximumPoolSize来设置线程池的大小
2.如果是CPU密集型的任务,建议尽量使用较小的线程池,一般是CPU核心数+1,因为开过多的线程,在CPU密集型任务中会大量增加线程上下文切换次数,导致额外的开销,而这个+1是为了即使当CPU密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。
3.如果是IO密集型,可以使用较多的线程,一般为CPU核心数*2,因为CPU使用率不高,等待IO时切换线程执行任务,可以充分使用CPU

8.IO密集型的线程池大小为2*CPU核心数是如何计算的

这个我不太懂
1.由于IO密集型线程需要等待IO结果,所以需要更多线程进行充分利用CPU。那么IO时间越长,相应也就该多设置线程数(即线程数和IO时间成正比)
2.由于CPU密集型需要CPU的大量支持,越多的线程只能带来上下文切换的浪费。那么当CPU计算时间越长的时候,相应也就该设置更少的线程(即线程数和CPU时间成反比)
3.我们假设CPU计算和IO时间相同,那么(CPU时间+IO时间)/CPU时间=2,即每一个CPU都可以开2个线程,则总线程数就是2*CPU核心数。

9.synchronized的锁优化

1.在JDK1.6,Java对synchronized关键字在JVM层面有了较大的优化,之前的synchronized属于重量级锁,效率低下。同时JDK1.6对锁的实现引入了大量的优化,引入了自旋锁、适应性自旋锁、偏向锁、轻量级锁、锁消除、锁粗化等技术减少锁操作的开销。锁主要存在四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们随着竞争而逐渐升级,锁是只能升级不能降级的。
2.synchronized同步语句块在JVM中是使用monitorenter和monitorexit指令。对方法的修饰是ACC_SYNCHRONIZED标识。他们都是对对象监视器monitor的获取

10.常用的垃圾收集器有哪些

1.Serial收集器:单线程收集器,会在垃圾收集的时候,暂停其他线程(“Stop The World”)直达结束。新生代使用标记-复制算法,老年代使用标记整理算法。
2.ParNew收集器:其实就是Serial收集器的多线程版本,除了使用多线程进行GC外和Serial完全一致。
3.Parallel Scavenge收集器:这是JDK1.8的默认收集器,它有多条垃圾收集线程,但用户线程仍处于等待状态,它关注吞吐量:CPU中用于运行用户代码的时间和CPU总消耗时间的比值。它提供了很多参数供用户找到最合适的停顿时间或最大吞吐量。
    -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间
    -XX:GCTimeRatio:直接设置吞吐量大小
    -XX:+UseAdaptiveSizePolicy:是参数开关,打开后,不需要手动指定新生代的大小和Eden与Survivor区的比例等细节参数
4.Serial Old收集器:Serial收集器的老年代版本,它用于JDK1.5及以前与Parallel Scavenge收集器配合使用以及CMS收集器的后备方案
5.Parallel Old收集器:Parallel Scavenge的老年代版本,使用多线程和标记整理算法
6.CMS收集器:是一种以获取最短回收停顿时间为目标的收集器,注重用户体验,他是HotSpot虚拟机上第一款真正意义上的并发收集器,第一次实现了用户线程和垃圾收集线程同时工作,它使用标记清除算法(会导致大量碎片的产生),分为4个步骤:
    初始标记:暂停所有其他线程,记录下直接与root相连的对象,速度快
    并发标记:开启GC和用户线程,用一个闭包结构区记录可达对象,但因为用户线程可能会不断更新引用域,所以无法保证实时性,这个算法会跟踪记录这些发生引用更新的地方
    重新标记:它也会暂停其他线程,是为了修正并发标记期间因为用户线程继续运行而导致标记变动的那一部分对象的标记记录
    并发清除:开启用户线程与GC开始对标记区域做清除
7.G1收集器:面向服务器的垃圾收集器,主要针对配备多处理器与大容量内存的机器。它具有并行与并发的,可分代收集的,具有空间整合的(整体上标记整理,局部标记复制),同时具有可预测停顿的特点。G1收集器维护了一个优先列表,每次允许根据允许的收集时间,优先回收价值最大的Region。(这也是它名字的由来)
8.ZGC收集器:也采用标记复制算法,会出现更少的Stop The World

11.如果线上系统发生OOM了,要如何排查

1.使用top命令,能够实时的显示系统中各个进程的资源占用情况
2.查看系统日志关于OOM的错误记录,dmesg命令可以用来查看开机之后的系统日志,其中可以捕捉到一些系统资源与进程的变化信息。
3.OOM一般就是内存泄漏或者加载了过多的class,创建过多的对象,给JVM分配的内存不够了,我们可以使用jstat -gcutil pid 间隔时间 查询次数,来查看当前GC的状态,观察堆的大小和垃圾回收状况。再用jmap -histo:live pid,来统计存活对象的分布情况
4.利用Java dump分析问题,jmap -dumo:format=b,file=文件名 [pid],转储文件为hprof离线可视化分析
5.使用MAT进行分析,查看出问题的东西,然后再去容器日志中查看具体问题

12.MySQL的事务隔离等级

1.读未提交(READ-UNCOMMITTED):允许读取尚未提交的数据,可能导致脏读、不可重复读、幻读
2.读已提交(READ-COMMITTED):允许读取并发事务已提交的数据,可以阻止脏读,但仍有不可重复读,幻读
3.可重复读(REPEATABLE-READ):对同一字段的多次读取结果都一致,除非数据本身是被本身事务自己所修改,但仍有幻读的发生
4.可串行化(SERIALIZABLE):最高的隔离级别,完全服从ACID,所有事务依次逐个执行。

13.可重复读解决了哪些问题

1.可重复读解决了脏读与不可重复读

14.脏读、不可重复读、幻读

1.脏读:读取到了其他事务未提交的数据。一个事务再访问数据并对其进行了修改,而这个修改还未提交到数据库,然而另一个事务就已经能访问到这个被修改的数据了,导致脏读。
2.不可重复读:同一次事务中,对一个数据的两次读取获得的结果不同,一个事务读取了一个数据,然后有另一个事务修改了这个数据的值,并且提交了事务,然后第一个事务再次对这个数据进行读取的时候,发现和上次读取到的结果不一致,导致了不可重复读
3.幻读:一次事务中,第一次进行读取了几行的数据,接着另一个并发事务插入了一些数据,在第一个事务的之后的查询中,就会发现多出了几条数据。一个实际的场景:事务A希望插入一个小明进学生表,首先它查询学生表是否有小明,发现没有,此时事务B插入了一些数据,正好包括小明,事务A在执行插入的时候,发现小明又已经存在了。

15.什么是 聚集索引、非聚集索引

1.聚集索引也称聚簇索引,它是物理存储按照索引排序,聚集索引是一种索引的组织形势,索引的键值逻辑顺序决定了表数据行的物理存储顺序,一个表中聚集索引只有一个,且索引的叶子节点就是对应的数据节点,如果索引列的数据被修改,那么对应的索引也会被修改,开销较大,所以不应频繁修改聚集索引。
2.非聚集索引,他的索引中索引的逻辑顺序和磁盘上数据行的物理存储顺序不同,一个表中可以有多个非聚集索引

16.慢查询优化,会考虑哪些优化

1.索引失效的情况:
    第一种是你使用了LIKE关键字进行查询,并且匹配字符串的第一个字符为"%",这样索引是不会起作用的,只有"%"不在第一个位置才行
    第二种是你使用了多列索引,并且你的查询语句用的不是多列索引的第一个字段,这样索引就不会生效,导致性能很差
2.将字段比较多的表,如果有部分字段的使用频率低,可以将其分离成新表
3.如果有经常联合查询的表,可以建立中间表提高查询效率
4.将较大的查询分解为多个小的查询
5.对于分页操作的偏移量过大的时候(如limit 10000,10),这样MySQL会查询10010条数据并只返回最后10条,代价极大,我们可以采用延迟关联技术,通过覆盖索引获取主键,然后通过这些主键关联原表获得所需的行

17.redis分布式锁

1.通过Redis做分布式锁是一种比较常见的方案,通常情况下,我们都基于Redisson实现分布式锁
2.Redis作为一个公共可访问的地方,适用于作为分布式缓存
3.我们抢占锁并设置锁的过期时间和唯一编号,如果设置失败,在一小段时间的等待后再去抢占锁,如果设置成功,再去执行业务,然后在业务结束释放锁的时候,要查询锁编号是否和自己设置的编号一致,如果不一致就不释放这把锁,因为这把锁可能是自己锁超时后其他人抢占并设置的锁。

18.缓存穿透、缓存击穿、缓存雪崩以及他们的解决方案

1.缓存穿透:大量不存在的key被请求了,导致请求直接落在了数据库上,根本没经过缓存这一层,导致数据库的压力过大宕机。解决方案:第一是做好参数校验,对于明显不合法的参数请求直接抛出异常,二是缓存无效key,但他并不能从根本上解决缓存穿透问题,要尽量让无效key的过期时间设置的短一点,否则容易被攻击,三是使用布隆过滤器,我们把所有可能存在的请求都存放在布隆过滤器中,请求到达时,先判断是否存在于布隆过滤器,不存在直接返回错误信息,存在再去进行缓存和数据库的请求
2.缓存击穿:一个热点缓存在某个时间点过期,但是恰好整个时间有大量对这个key的请求发过来,这个时候大量的请求都会落到数据库上,导致数据库压力过大宕机。解决方案:第一使用互斥锁,在缓存失效的时候,不是立即去加载数据库而是先获取锁,再去数据库取数据并更新到缓存,其他未获取锁的等待一段时间后再去走缓存与数据库来获取数据。二是可以通过设置永不失效来解决。
3.缓存雪崩:缓存在同一时刻大量失效,导致接下来的大量请求直接落在数据库上,导致数据库压力过大宕机。解决方案:第一采用Redis集群,防止单机出现问题导致整个缓存服务失效。二是限流,防止同一时刻处理大量的请求。三是设置不同的缓存失效时间,避免同一时刻大量失效,或者设置永不失效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值