2023-12-08面试

1、自我介绍

面试官你好,我叫平明博,来自河南郑州,19年毕业,所学专业软件工程,之前任职于南京华苏科技,担任开发工程师一职,在职期间主要对省间现货相关项目进行研发,核心就是从多平台自动获取数据,根据不同业务逻辑,我们这边做可视化操作,负责的项目主要是使用微服务的设计思想和分布式部署,相关的框架有Spring Boot,MyBatis,常见中间件RedisEurekaRabbitMQ等,另外对Redis源码有一定了解;最后也做过代码重构的任务,因为项目有Sonar扫描,和Jenkins部署结合,定期扫描代码,保持项目代码整洁和质量,关于线上问题、性能调优,代码重构有自己的理解,如果有幸加入贵公司一起共事,我想我能第一时间上手并且产出结果,以上就是我的自我介绍,谢谢面试官。

衍生问题

1.1 Spring Boot

1.2 Redis

1.3 RabbitMQ

1.4 Sonar

1.5 性能优化都做过哪方面的处理。

1.6 Jenkins怎么用的

2、说一下项目逻辑,流程情况

技术层面

技术角度来说,采用是springcloud微服务架构,以springboot基础,数据层mybatis,数据库相关mysql,oracle,基于vue+elementui封装的前端框架,中间件有redisspring cloud各种组件服务调用,服务发现,服务注册,网关gateway,rabbitmq消息队列,为了保证高可用,Redis哨兵模式+集群,MySQL也是主从复制,读写分离,目前也是在云上部署。

业务层面

这个系统主要提供给供电所用户使用,根据各平台提供数据,计算各省出清分析与通道业务,推进相对复杂的电价预测与申报策略等功能开发工作,通过曲线图或者折线图可以看出来,当月,当日用电量预测分析,其中涉及模块数据采集,市场分析, 出清电价预测。

3、项目中担任的角色

前期 和开发,测试,产品,运营相关人员开会,进行业务梳理。

中期 根据需求分析会确定的点,结合需求文档进行开发工作,排期,何时测试,何时灰度验证,生产验证。

后期 部署上线等事宜,准备上线手册,测试验证业务场景,没问题。

4、项目中遇到问题该怎么解决

  • 一个是技术方面的话,如果是本地进行自测,debug来解决,解决不了查找相关知识点的资料,之后通过百度,谷歌查询有没有相关博客文章 最后实在解决不了请教同事,如果是线上问题,需要从服务器查看实时日志信息,通过日志来排查,搭配开源组件arthas
  • 一个是业务方面的话,我们开发之前都有需求讨论,因为这个需求讨论只能从大概方向来确定,一些场景可能无法考虑周全,开发过程中才会遇到或者测试时候才发现,然后跟项目经理沟通,确定合适的解决方案。

5、项目开发流程

我们项目是属于敏捷开发,每个月一般情况两次分支,上半月一次,下半月一次;

然后项目经理跟客户商讨需求细节,之后形成相应的书面文档,之后不同人员分发不同需求点,给几天时间熟悉需求,熟悉之后我们就开始进行需求分析,一般需求都是在原有基础上新增开发点或者根据客户特殊需求进行相应变更,大的方面没有什么问题,就开始讨论数据库表,字段,缓存技术上问题细节问题;其次,会有专门开发创建好新的分支,我们拉取最新的分支其次,按照需求研讨会暂定的需求点进行各自开发,一般是一周开发时间,开发相应功能之后,先自测,之后没啥问题

然后,项目开始上sit环境,提交各自模块代码,jenkins部署对应分支模块,开始sit环境配合前端联调对应功能

然后,uat,准生产测试人员来进行测试,出现技术问题,我们开发者根据测试人员禅道提供bug问题,进行修复

uat环境基本上不会有什么技术问题,逻辑问题;之后准生产验证就是业务整个衔接进行验证。

6、分布式事务

微服务中如果跨服务链路比较多,就会出现分布式事务,数据的一致性问题,就是一个回滚的问题,像一些架构,如果对性能要求不高,可以使用阿里的seata,因为seata采用就行主要解决它的分布式事务问题, 它里面是tcc的一个机制,就是两阶段提交,第一阶段就是说,类似于MySQL的binlog日志,它会获取它的一个原始值,对原始值做一个保留,然后在事务提交之前,它会获取全局锁的一个概念,因为seata里面有一个全局锁和局部锁的概念,因为需要获取全局锁,如果不获取全局锁,不会让它提交事务的,在二阶段提交事务时候,如果出现异常了,就利用它的一个就是一阶段保存的历史性然后回滚,但是这样一来有一个弊端,就是说seata的一个性能问题,因为公司会使用,但有的公司不一定使用它是获取全局锁的一个性能问题,因为全局锁它会从链路开始到结束,它这个全局锁会影响整个性能,为了保证数据一致性,还有解决分布式事务一个方案,代码作为补偿机制,通过消息队列mq异步补偿,如果说我们的服务之间,A服务调用B服务,B服务出现了宕机,或者error,我们就会通过消息队列补偿一个机制,这样性能比seata好一点,但是这样补偿不一定及时或者对数据一致性也会出现一些问题,因为是通过人为代码补偿的。

7、分布式锁如何使用的

8、线上遇到哪些问题,如何解决的

问题场景:服务是一个使用类似dubboRPC框架以及若干Spring全家桶组合起来的微服务架构,Java服务使用的是CMS的垃圾回收器,然收到一台实例(即一个Java应用)产生FullGC日志的报警,老年代内存已经不足而触发了FullGC,并且由于应用虽然STW,但是请求确还是在堆积,导致一直在持续FullGC,没有自愈;

然后逻辑上考虑出现fullgc场景,有人主动调用了System.gc(),基本上不会有人来调用这个方法,就算是有人调用了,我们服务启动时候开启了一个参数,这个参数可以将System.gc()转为CMS的并发gc,所以并不会触发FullGC.

老年代空间不足:对象出生于新生代,在挺过了一次次minorGC之后成功熬到了老年代,并且持续在老年代混吃等死,一直到大量的对象都这样在老年代混吃等死把老年代占满之后就会触发FullGC

元空间不足:代码中大量使用动态代理,生成了一大堆的代理类占用了方法区,如果是这个原因引起必然是所有服务都会报FullGC问题,然而其它机器的老年代内存很稳定,所以排查

为什么只有一个实例异常

只有单个服务出现了这样的问题,很有可能不是外部依赖的超时或者方法区空间不足造成,而是因为某个刚好落在这个服务上的超大请求占用了大量的内存并且耗时久,一直赖在老年代不走导致。

gc日志情况

第一次FullGC发生在2020-07-25 14:51:58,观察之前的日志可以发现历史上CMS并发回收一般都会将堆内存稳定在3608329K->1344447K,从3.6G左右回收到1.3G左右,但是从某个时间点开始开始回收效率变差了。

可以看到,这次回收从5G回收到3.5G,回收完之后还有这么多被占用!这个时间点2020-07-25 14:51:50左右一定发生了什么事情,导致老年代一直保留着一批老赖。

确定问题溯源

定位好时间段之后,接下来找到这个时间段附件的离谱的大请求,由于项目中通过aop全局配置处理日志情况,可以知道每个请求详细信息,通过日志找到一个一个请求,其中请求参数特别长,本来是修改了1000个文件夹的某个属性,但是业务逻辑是如果修改的是某个特殊属性时,会级联修改这些文件夹下的全部文件,实际上修改了1000个文件夹的请求,背后处理了1000个文件夹+几十万w个文件,而修改这些属性由于我们使用的框架的限制,100w个文件在修改前会查主属性表+所有辅属性表(内存根据主键id join),请求耗时90s。导致大量对象 长时间滞留在堆内存中挺过了一波波的minorGC和CMS GC干满老年代,最终触发了这个问题。

优化思路

  • 限制此类字段的修改,对于这样需要级联修改的情况时进行校验,不允许API用户传太多文件夹(1000个 --> 100个)
  • 微服务的思想,在该服务上层再做一个分发服务,对于这样级联修改的请求将1000个的修改拆成10个每次修改100个的请求去并发请求下面的机器,均摊压力
  • 异步化队列,修改文件夹本身属性后即立刻返回,后续级联修改的请求拆成n个放入队列,由其它服务订阅到请求后执行
  • 能有监控手段在应用FullGC时从注册中心踢掉,待FullGC自愈后再加入,而不是人工干预重启
  • 优化ORM框架,就算修改100w个文件的某个属性,也不需要查询出这些文件的全部属性,只查询出主表+需要修改的属性所在的表即可

9、 HashMap线程安全吗,原因,1.7与1.8区别

9.1 是否线程安全

不是

9.2 原因

简要介绍

在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题

详细说明

JDK 7 的 HashMap 解决冲突用的是拉链法,在拉链的时候用的是头插,每次在链表的头部插入新元素。resize() 的时候用的依然是头插,头插的话,如果某个下标中的链表在新的 table 中依然索引到同一个下标中,那么原链表的顺序会反转。因为链表是顺序访问的,那么每次访问一个节点,会把当前节点插到新 table 链表的头部,这样原链表的最后一个元素在 resize() 后,就变成新链表的头部了(如果它们索引到新 table 的同一个下标中)。这在并发的情况下可能产生环。(死循环)

当两个线程同时put元素时,因为两个都是在自己的工作内存中操作变量,所以可能情况下就是两个线程都认为自己是第一个put然后都触发了resize操作,且都认为oldTab为null,则必然有一个线程的赋值会丢失。(数据丢失)

1.8还是有数据覆盖的问题

9.3 区别

  1. 数组+链表改成了数组+链表或红黑树;
  2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
  3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
  4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

10 ConCurrentHashMap了解吗,如何保证线程安全,1.7与1.8区别

10.1 理解

concurrenthashmap分为两种情况来介绍,一个是jdk1.7和1.8各自情况不太一样,在jdk1.7它的底层是通过数组+链表实现的,然后使用了一种分段锁来保证线程安全,它是将数组分为16段,也就是说给每个segment配一把锁,然后读取每个segment时候,先获取锁,所以它最多有16个线程并发去操作;到了jdk1.8时候,它跟hashmap一样,引入红黑树这种数据结构,同时在并发处理方面不再使用分段锁而是采用cas+synchronize关键字来实现更为细粒度的锁,相当于是把锁的控制位于hash桶这个级别,然后写入键值对可以锁住hash桶这种链表头结点,这样的话不会影响其他hash桶写入,从而去提高并发处理能力。

10.2 区别

锁机构

JDK1.7中,ConcurrentHashMap基于Segment+HashEntry数组实现的。SegmentReentrantLock的子类,而其内部也维护了一个Entry数组,这个Entry数组和HashMap中的Entry数组是一样的。所以说Segment其实是一个锁,可以锁住一段哈希表结构,而ConcurrentHashMap中维护了一个Segment数组,所以是基于分段锁实现的。 而JDK1.8中,ConcurrentHashMap摒弃了Segment,而是采用synchronized+CAS+红黑树来实现的。锁的粒度也从段锁缩小为结点锁.

put执行流程

JDK1.7中,ConcurrentHashMap要进行两次定位,先对Segment进行定位,再对其内部的数组下标进行定位。定位之后会采用自旋锁+锁膨胀的机制进行加锁,也就是自旋获取锁,当自旋次数超过64时,会发生膨胀,直接陷入阻塞状态,等待唤醒。并且在整个put操作期间都持有锁。

而在JDK1.8中只需要一次定位,并且采用CAS+synchronized的机制。如果对应下标处没有结点,说明没有发生哈希冲突,此时直接通过CAS进行插入,若成功,直接返回。若失败,则使用synchronized进行加锁插入。

计算size方式

1.7:采用类似于乐观锁的机制,先是不加锁直接进行统计,最多执行三次,如果前后两次计算的结果一样,则直接返回。若超过了三次,则对每一个Segment进行加锁后再统计。

1.8:会维护一个baseCount属性用来记录结点数量,每次进行put操作之后都会CAS自增baseCount

10.3 项目中如何使用

11 ArrayList

底层结构

​ ArrayList是基于数组实现的,是一个动态数组,get和set效率高;其容量能自动增长,内存连续。

初始化过程

new一个ArrayList之后,当添加第一个元素,这个数组提供10个元素空间,当我们添加元素第十一位时候,

会有一个扩容操作,1.5扩容,15个元素的空间,用完之后,当添加第十六个元素时候,进行扩容操作15*1.5《=》

15 >> 1 + 15 = 22;

优缺点

优点

地址连续,get和set效率高

缺点

插入删除元素慢问题,

12 ArrayList与LinkedList区别

底层实现

ArrayList底层通过动态数组实现,实现RandomAccess接口,地址是连续的,故而get,set操作效率高

LinkedList底层通过双向链表,里面包含两个node类型节点,首节点,尾结点;每个节点通过引用记住上一个节点和下一个节点的地址,从而使得所有元素彼此连接起来,当添加,删除元素时候,修改彼此的引用即可。

读写操作

对于插入操作而言

头插法

LinkedList快一些,ArrayList需要大量的位移和复制操作,另外容量不够时候需要扩容;LinkedList直接定位元素在首位,修改引用即可插入。

尾插法

ArrayList快一些,ArrayList不需要大量位移和复制操作,LinkedList需要大量创建对象

中间插入

ArrayList定位时间很快,常量级别的时间复杂度,比较耗时就是数据迁移和容量不足时候扩容;

LinkedList插入元素不怎么耗时,主要是定位元素需要耗费O(n),以及元素实例化操作

获取元素

ArrayList获取元素,常量级别复杂度,O(1),底层是通过数组实现,获取元素很快

LinkedList获取元素,需要根据指定位置获取对应node节点,获取对应节点过程,这个过程需要遍历链表一半元素,这个遍历过程有点特殊,获取指定索引位置大于链表元素一般,从最后一个节点往前遍历;如果索引位置小于链表一半元素,从头遍历到后面

应用场景

1、两者均有自己的应用场景,如果不确定使用哪种集合,使用ArrayList

2、如果可以确定在首部增加,删除,获取元素,可以使用LinkedList,这个集合里面很对效率高的方法,addFirst,addLast,getFirst,getLast,removeFirst,removeLast,这些方法的时间复杂度都是O(1)常量级别

3、添加元素在尾部这种情况,ArrayList效率高一些,ArrayList不需要数据拷贝,而LinkedList需要创建大量node对象

4、对于这两种结构的集合,并不是说添加删除元素LinkedList就是很快,获取元素ArrayList效率高;需要具体问题具体分析

13、synchronize理解

synchronized经常用的,用来保证代码的原子性。

synchronized主要有三种用法:

修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

修饰静态方法:也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。

如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。

修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized(类.class) 表示进⼊同步代码前要获得 当前 class 的锁

14 synchronize升级过程

synchronized默认采用的是偏向锁,然后程序运行过程中始终是只有一个线程去获取,这个synchronized的这个锁,那么java对象中记录一个线程id,我们下次再获取这个synchronize的锁时候,只需要比较这个线程id就行了,在运行过程中如果出现第二个线程请求synchronized的锁时候,分两种情况,在没有发生并发竞争锁情况下,这个synchronized就会自动升级为轻量级锁,这个时候,第二个线程就会尝试自旋锁方式获取锁,很快便可以拿到锁,所以第二个线程也不会阻塞,但是如果出现两个线程竞争锁情况,这个synchronize就会升级为重量级锁,这个时候就是只有一个线程获取锁,那么另外一个线程就是阻塞状态,需要等待第一个线程释放锁之后,才能拿到锁。

15 说说synchronized和ReentrantLock的区别?

从概念上说明

  • 锁的实现: synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(一般是lock()和unlock()方法配合try/finally 语句块来完成。)
  • 性能: 在JDK1.6锁优化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6开始,增加了适应性自旋、锁消除等,两者性能就差不多了。
  • 功能特点: ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。
    • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
    • ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁

有哪些用处,有什么问题

16、CAS呢?CAS了解多少?

CAS叫做CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保证操作的原⼦性的。

CAS 指令包含 3 个参数:共享变量的内存地址 A、预期的值 B 和共享变量的新值 C。

只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的 。

17 CAS 有什么问题?如何解决?

CAS的经典三大问题:

CAS三大问题

ABA 问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

怎么解决ABA问题?

  • 加版本号

每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。参考乐观锁的版本号,这种做法可以给数据带上了一种实效性的检验。

Java提供了AtomicStampReference类,它的compareAndSet方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。

循环性能开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。

怎么解决循环性能开销问题?

在Java中,很多使用自旋CAS的地方,会有一个自旋次数的限制,超过一定次数,就停止自旋。

只能保证一个变量的原子操作

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

怎么解决只能保证一个变量的原子操作问题?

  • 可以考虑改用锁来保证操作的原子性
  • 可以考虑合并多个变量,将多个变量封装成一个对象,通过AtomicReference来保证原子性。

volatile有两个作用,保证可见性有序性

volatile怎么保证可见性的呢?

相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,它没有上下文切换的额外开销成本。

volatile可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。

volatile怎么保证有序性的呢?

重排序可以分为编译器重排序和处理器重排序,valatile保证有序性,就是通过分别限制这两种类型的重排序

工作中如何使用

1、CAS可以用于实现无锁计数器,允许多个线程同时对计数器进行递增或递减操作。
2、使用CAS,线程可以尝试原子地更新计数器的值。如果更新失败,则说明其他线程已经修改了计数器,可以重试操作直到更新成功。
3、无锁计数器避免了使用传统锁进行互斥访问的开销,提供了更高的并发性能。

18、volatile

当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据

多线程应用中被几个任务共享的变量应该加 volatile

*当读取一个变量时,编译器优化有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当内存变量或寄存器变量**因别的线程等而改变了值,**该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致*

加上volatile关键字区别

19 线程池

线程池: 简单理解,它就是一个管理线程的池子。

管理线程的池子

  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。

执行流程

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
  • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。

线程池执行流程

  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  2. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

线程池如何配置的,有哪些参数

之前我们有一个和第三方对接的需求,需要向第三方推送数据,引入了多线程来提升数据推送的效率,其中用到了线程池来管理线程。

线程池的参数如下:

  • corePoolSize:线程核心参数选择了CPU数×2

  • maximumPoolSize:最大线程数选择了和核心线程数相同

  • keepAliveTime:非核心闲置线程存活时间直接置为0

  • unit:非核心线程保持存活的时间选择了 TimeUnit.SECONDS 秒

  • workQueue:线程池等待队列,使用 LinkedBlockingQueue阻塞队列

同时还用了synchronized 来加锁,保证数据不会被重复推送

线程池拒绝策略

  • AbortPolicy :直接抛出异常,默认使用此策略
  • CallerRunsPolicy:用调用者所在的线程来执行任务
  • DiscardOldestPolicy:丢弃阻塞队列里最老的任务,也就是队列里靠前的任务
  • DiscardPolicy :当前任务直接丢弃

想实现自己的拒绝策略,实现RejectedExecutionHandler接口即可。

线程池有哪几种工作队列

  • ArrayBlockingQueue:ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
  • LinkedBlockingQueue:LinkedBlockingQueue(可设置容量队列)是基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
  • DelayQueue:DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列
  • SynchronousQueue:SynchronousQueue(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列

线程池怎么关闭知道吗?

shutdown 和shutdownnow简单来说区别如下:

  • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
  • shutdown()只是关闭了提交通道,而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。

线程池异常怎么处理知道吗

在使用线程池处理任务的时候,任务代码可能抛出RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

常见的异常处理方式:

线程池异常处理

20 线程通信方式

volatile

多个线程同时监听一个变量,当该变量发生变化的时候,线程能够感知并执行相应的业务。这是最简单的一种实现方式

synchronized,wait()/notify()/notifyAll()

wait和notify是Object的方法,因此任意类都可以调用。wait和notify必须要加到锁内,且必须持有同一把锁;执行顺序为:开始wait->开始notify->结束notify->结束wait。

notify与notifyall区别

notify()
唤醒正在等待此对象监视器的单个线程。 如果有多个线程在等待,则选择其中一个随机唤醒(由调度器决定),唤醒的线程享有公平竞争资源的权利
notifyAll()
唤醒正在等待此对象监视器的所有线程,唤醒的所有线程公平竞争资源

21、redis底层数据结构

redis是什么

Redis(Remote Dictionary Server) 是开源的高性能非关系型键值对数据库,可以存储键和五种不同类型的值之间的映射,键类型只能是字符串,值支持五种数据类型:字符串,列表,集合,散列表,有序集合;和传统的管系统数据库不同redis基于内存处理的,读写速度非常快,故而redis常常用来作为缓存,分布式锁,事务,持久化机制,多种集群方案。

有哪些数据结构

SDS,双向链表,压缩链表,hash表,整数集合,跳表

常用的结构

sds,hash

STRING字符串、整数或者浮点数对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 :一个键最大能存储512MB1、分布式锁:SETNX(Key, Value),释放锁:DEL(Key),2、复杂计数功能缓存(用户量,视频播放量)
LIST列表从两端压入或者弹出元素 对单个或者多个元素进行修剪, 只保留一个范围内的元素存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据,简单的消息队列的功能
SET无序集合添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集,做全局去重的功能,点赞,转发,收藏;
HASH包含键值对的无序散列表添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在结构化的数据,比如一个对象,单点登录
ZSET有序集合添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名去重但可以排序,如获取排名前几名的用户,做排行榜应用,取TOPN操作;延时任务;做范围查找。周榜,月榜,年榜

22、redis之SDS了解多少

c语言的缺陷

C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。

1 在 C 语言里,对字符串操作时,char * 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用“\0”表示,意思是指字符串的结束

举个例子,C 语言获取字符串长度的函数 strlen,就是通过字符数组中的每一个字符,并进行计数,等遇到字符为“\0”后,就会停止遍历,然后返回已经统计到的字符个数,即为字符串长度

C 语言的字符串用 “\0” 字符作为结尾标记有个缺陷。假设有个字符串中有个 “\0” 字符,这时在操作这个字符串时就会提早结束,比如 “xiao\0lin” 字符串,计算字符串长度的时候则会是 4

2 用 char* 字符串中的字符必须符合某种编码(比如ASCII)。这些限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据

3 C 语言标准库中字符串的操作函数是很不安全的,对程序员很不友好,稍微一不注意,就会导致缓冲区溢出。

举个例子,strcat 函数是可以将两个字符串拼接在一起。

c //将 src 字符串拼接到 dest 字符串后面 char *strcat(char *dest, const char* src);

C 语言的字符串是不会记录自身的缓冲区大小的,所以 strcat 函数假定程序员在执行这个函数时,已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容,而一旦这个假定不成立,就会发生缓冲区溢出将可能会造成程序运行终止,(*这是一个可以改进的地方*)。

而且,strcat 函数和 strlen 函数类似,时间复杂度也很高,也都需要先通过遍历字符串才能得到目标字符串的末尾。然后对于 strcat 函数来说,还要再遍历源字符串才能完成追加,对字符串的操作效率不高。

sds结构有哪些优化

结构中的每个成员变量分别介绍下:

  • len,SDS 所保存的字符串长度。这样获取字符串长度的时候,只需要返回这个变量值就行,时间复杂度只需要 O(1)。
  • alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算 出剩余的空间大小,然后用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区益处的问题。
  • flags,SDS 类型,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。
  • buf[],字节数组,用来保存实际数据。不需要用 “\0” 字符来标识字符串结尾了,而是直接将其作为二进制数据处理,可以用来保存图片等二进制数据。它即可以保存文本数据,也可以保存二进制数据,所以叫字节数组会更好点。

总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷。

优势
O(1)复杂度获取字符串长度

Redis 的 SDS 结构因为加入了 len 成员变量,那么获取字符串长度的时候,直接返回这个变量的值就行,所以复杂度只有 O(1)

二进制安全

因为 SDS 不需要用 “\0” 字符来标识字符串结尾了,而且 SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的

不会发生缓冲区溢出,自动扩容机制

C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,因为这些函数把缓冲区大小是否满足操作的工作交由开发者来保证,程序内部并不会判断缓冲区大小是否足够用,当发生了缓冲区溢出就有可能造成程序异常结束。

所以,Redis 的 SDS 结构里引入了 alloc 和 leb 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。

而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小,以满足修改所需的大小。

在扩展 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。

这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。

节省内存空间

SDS 结构中有个 flags 成员变量,表示的是 SDS 类型。

Redis 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

这 5 种类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同

之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少。

23、如何保证数据库与Redis缓存一致的

问题解决思路
先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致 先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了 方案一:写请求先删除缓存,再去更新数据库,(异步等待段时间)再删除缓存(成功表示有脏数据出现);这种方案读取快速,但会出现短时间的脏数据。 方案二:写请求先修改缓存为指定值,再去更新数据库,再更新缓存。读请求过来后,先读缓存,判断是指定值后进入循环状态,等待写请求更新缓存。如果循环超时就去数据库读取数据,更新缓存。这种方案保证了读写的一致性,但是读请求会等待写操作的完成,降低了吞吐量。

24、缓存雪崩,缓存穿透,缓存击穿

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决:redis高可用

  1. redis有可能挂掉,多增加几台redis实例,(一主多从或者多主多从),这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决:采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力;

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决:

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁

(1)一个“冷门”key,突然被大量用户请求访问。

(2)一个“热门”key,在缓存中时间恰好过期,这时有大量用户来进行访问。

25、如何解决redis并发竞争的key问题

方案一:分布式锁+时间戳
这种情况,主要是准备一个分布式锁,大家去抢锁,抢到锁就做set操作。

加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争。

分布式锁

  1. 加锁:SET lock_key $unique_id EX $expire_time NX
  2. 操作共享资源
  3. 释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁

问题

1、如何避免死锁

2、锁被别人释放怎么办

3、锁过期时间不好评估

时间戳

要求key的操作需要顺序执行,所以需要保存一个时间戳判断set顺序
方案二:
并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。

把Redis.set操作放在队列中使其串行化,必须的一个一个执行。

这种方式在一些高并发的场景中算是一种通用的解决方案。

26、延时队列如何实现,哪几种方案

项目中的流程监控,有几种节点,需要监控每一个节点是否超时。按传统的做法,肯定是通过定时任务,去扫描然后判断,但是定时任务有缺点:
1,数据量大会慢;2,时间不好控制,太短,怕一次处理不完,太长状态就会有延迟。所以就想到用延迟队列的方式去实现。

方案一:redis的zset实现延迟队列

生产者:可以看到生产者很简单,其实就是利用zset的特性,给一个zset添加元素而已,而时间就是它的score。

消费者:消费者的代码也不难,就是把已经过期的zset中的元素给删除掉,然后处理数据。

方案二:rabbitmq通过TTL+死信队列实现延迟队列

我们需要建立2个队列,一个用于发送消息,一个用于消息过期后的转发目标队列,生产者输出消息到Queue1,并且这个消息是设置有有效时间的,比如60s。消息会在Queue1中等待60s,如果没有消费者收掉的话,它就是被转发到Queue2,Queue2有消费者,收到,处理延迟任务。

27、 MySQL的慢sql优化一般如何来做?除此外还有什么方法优化?

1、通过相关指令开启慢查询日志

-- 查看是否开启了慢查询日志
show variables like 'slow_query_log';
-- 默认是OFF,不开启,可以手动开启
-- 方式一 set global slow_query_log=1;
--  修改配置文件my.cnf,加入下面一行命令 slow_query_log = ON

2、慢查询日志找到对应的SQL,分析SQL

-- 查询慢查询日志文件路径
show variables like '%slow_query_log_file%';
-- MySQL提供了分析慢查询日志的工具mysqldumpslow
mysqldumpslow -s t -t 10 /usr/local/mysql/data/localhost_slow.log

-- 例如 ,休眠20s
SELECT sleep(20); 
常用参数有 -s: 表示按何种方式排序:  c: 访问次数  l: 锁定时间  r: 返回记录  t: 查询时间  al: 平均锁定时间  ar: 平均返回记录数  at: 平均查询时间-t: 返回前面多少条的数据

3、where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高

看一下accurate_result = 1的记录数:

select count(*),accurate_result from stage_poi  group by accurate_result;
+----------+-----------------+
| count(*) | accurate_result |
+----------+-----------------+
|     1023 |              -1 |
|  2114655 |               0 |
|   972815 |               1 |
+----------+-----------------+

我们看到accurate_result这个字段的区分度非常低,整个表只有-1,0,1三个值,加上索引也无法锁定特别少量的数据。

4、explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询 )

索引是否应用,联合索引是否完全应用,扫描行数rows

explain select * from a;

explain select * from b;

5、了解业务应用场景,实时数据,历史数据,定期删除等因素,辅助我们更好的分析和优化查询语句

6、加索引时参照建索引的几大原则

最左匹配原则

区分度高的列作为索引,count(distinct name)/count(*),表示字段不重复的比例,比例越大扫描记录越少

索引失效情况

尽量扩展索引,不要新建索引

最佳左前缀原则——如果索引了多列,要遵守最左前缀原则。指的是查询要从索引的最左前列开始并且不跳过索引中的列。

前提条件:表中已添加复合索引(username,password,age)

该查询缺少username,查询条件复合索引最左侧username缺少,违反了最佳左前缀原则,导致索引失效,变为ALL,全表扫描

不在索引列上做任何操作(计算,函数,(自动或者手动)类型装换),会导致索引失效而导致全表扫描

7、mysqldumpslow

使用帮助

-s ORDER     what to sort by (al, at, ar, c, l, r, t), 'at' is default # 默认是at 平均查询时间
                al: average lock time
                ar: average rows sent
                at: average query time
                 c: count
                 l: lock time
                 r: rows sent
                 t: query time  # 查询时间排序
-r           reverse the sort order (largest last instead of first) # 反转排序顺序 
-t n  just show the top n queries # 仅仅显示前n行

实践

mysqldumpslow -s t -t 10 localhost-slow.log

总结

1 根据命令mysqldumpslow找到慢查询时间耗时比较长的SQL

2 explain查看执行计划,需要重点关注 type索引是否应用,联合索引是否完全应用,扫描行数rows

3 加索引时参照建索引的几大原则

3.1 最左匹配原则

3.2 区分度高的列作为索引,count(distinct name)/count(*),表示字段不重复的比例,比例越大扫描记录越少

3.3 索引失效情况

3.4 尽量扩展索引,不要新建索引

4 了解业务应用场景,实时数据,历史数据,定期删除等因素,辅助我们更好的分析和优化查询语句

28、 MySQL的索引结构说一下

MySQL采用B+树作为索引存储结构。

B+树是B树增强版, B 树是一种多路平衡树,用这种存储结构来存储大量数据,它的整个高度会相比二叉树来说,会矮很多。 而对于数据库来说,所有的数据必然都是存储在磁盘上的,而磁盘 IO 的效率实际上是很低的,特别是在随机磁盘 IO 的情况下效率更低。 所以树的高度能够决定磁盘 IO 的次数,磁盘 IO 次数越少,对于性能的提升就越大,这也是为什么采用 B 树作为索引存储结构的原因。

MysqlInnoDB 存储引擎里面,它用了一种增强的 B 树结构,也就是 B+树来作为索引和数据的存储结构。 相比较于 B 树结构,B+树做了几个方面的优化, B+树的所有数据都存储在叶子节点,非叶子节点只存储索引,叶子节点数据使用双向链表的方式进行关联;最重要的是,B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。

29、事务的ACID,其中把事务的隔离性详细解释一遍

Mysql 里面的事务,满足 ACID 特性,所以在我看来,Mysql 的事务实现原理, 就是 InnoDB 是如何保证 ACID 特性的。

首先,A 表示 Atomic 原子性,也就是需要保证多个 DML 操作是原子的,要么都成功,要么都失败。

那么,失败就意味着要对原本执行成功的数据进行回滚,所以 InnoDB 设计了一 个 UNDO_LOG 表,在事务执行的过程中,把修改之前的数据快照保存到 UNDO_LOG 里面,一旦出现错误,就直接从 UNDO_LOG 里面读取数据执行反 向操作就行了。

其次,C 表示一致性,表示数据的完整性约束没有被破坏,这个更多是依赖于业 务层面的保证,数据库本身也提供了一些,比如主键的唯一约束,字段长度和类 型的保证等等。

接着,I 表示事务的隔离性,也就是多个并行事务对同一个数据进行操作的时候,如何避免多个事务的干扰导致数据混乱的问题。

InnoDB 提供了四种隔离级别的实现

RU(未提交读)

RC(已提交读)

RR(可重复读)

Serializable(串行化)

InnoDB 默认的隔离级别是 RR(可重复读),然后使用了 MVCC 机制解决了脏读和不可重复读的问题,然后使用了行锁/表锁的方式解决了幻读的问题。

最后一个是 D,表示持久性,也就是只要事务提交成功,那对于这个数据的结果的影响一定是永久性的。

理论上来说,事务提交之后直接把数据持久化到磁盘就行了,但是因为随机磁盘 IO 的效率确实很低,所以 InnoDB 设计了 Buffer Pool 缓冲区来优化,也就是数据发生变更的时候先更新内存缓冲区,然后在合适的时机再持久化到磁盘。 那在持久化这个过程中,如果数据库宕机,就会导致数据丢失,也就无法满足持久性了,所以 InnoDB 引入了 Redo_LOG 文件,这个文件存储了数据被修改之后的值, 当我们通过事务对数据进行变更操作的时候,除了修改内存缓冲区里面的数据以外,还会把本次修改的值追加到 REDO_LOG 里面。

当提交事务的时候,直接把 REDO_LOG 日志刷到磁盘上持久化,一旦数据库出 现宕机,在 Mysql 重启在以后可以直接用 REDO_LOG 里面保存的重写日志读 取出来,再执行一遍从而保证持久性。

因此,在我看来,事务的实现原理的核心本质就是如何满足 ACID 的,在 InnoDB里面用到了 MVCC行锁表锁、UNDO_LOGREDO_LOG 等机制来保证

30、脏读、幻影读、不可重复读

脏读:指一个线程中的事务读取到了另外一个线程中未提交的数据。

脏读是读到了别的事务回滚前的脏数据。比如事务A执行过程中修改了数据X,在未提交前,事务B读取了X,而事务A却回滚了,这样事务B就形成了脏读。

不可重复读:指一个线程中的事务读取到了另外一个线程中提交的 update 的数据。

事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。

幻读:指一个线程中的事务读取到了另外一个线程中提交的 insert 的数据。

为什么一模一样的SQL语句,第一次查询是10条数据,第二次查询是12条数据?难道刚才出现了幻觉?导致我刚才幻读了?这就是幻读这个名词的由来

31、说说GC过程

新创建的对象会先被分配到到Eden区。JVM刚启动时,Eden区对象数量较少,两个Survivor区S0、S1几乎是空的。

随着时间的推移,Eden区的对象越来越多。当Eden区放不下时(占用空间达到容量阈值),新生代就会发生垃圾回收,我们称之为Minor GC或者Young GC。

发生GC时,第一步会通过可达性分析算法找到可达对象。如上图,蓝色为可达对象,其他紫色为不可达对象。第二步,被标示的可达对象会被转移到S0(此时S0是From Survivor),此时存活对象年龄加1,三个对象年龄都变为1。第三步,清除Eden区所有对象。

GC后各区域对象占用情况,如上图所示。

程序继续运行,Eden区再次达到容量阈值时,会再次发生GC。这时S0(From Survivor)已经有了对象。还是同样的步骤,通过可达性分析算法找到可达对象,然后再将EdenS0中的可达对象转移到S1(To Survivor),各存活对象年龄加1。最后将EdenS0中的所有对象清除。

GCS0区域被清空。如上图所示。S0S1发生了互换,S1变成了From Survivor,S0变成了To Survivor。

注意,To Survivor区永远都为空。这实际上是垃圾回收算法-复制算法在年轻代的实际应用。把年轻代分为Eden,S0,S1三个区域,每次垃圾回收时把可达对象复制到S0S1,然后再清除掉Eden和(S1S0)中的所有对象。由于每次GC时,新生代的可达对象非常少(绝大部分对象要被回收掉),一般不会超过新生代总体空间的10%,所以搜寻可达对象以及复制对象的成本都会非常低。而且这种复制的方式还能避免产生堆内存碎片,提高内存利用率。很多年轻代垃圾收集器都采用复制算法,如ParNew

在程序运行过程中,新生代GC会反复发生,长寿对象会在S0S1之间反复交换,年龄也会越来越大,当对象达到年龄上限时,会被晋升到老年代。这个年龄上限默认是15,可以通过参数-XX:MaxTenuringThreshold设置。如下图,有些年轻代对象年龄达到了上限15,被转移到了老年代。

其他晋升方式。新生代对象晋升到老年代,除了根据年龄正常晋升外。为了提高JVM的性能,JVM设计者还考虑了其他晋升方式。

大对象直接晋升。大对象会跨过年轻代直接分配到老年代。可以通过-XX:PretenureSizeThreshold参数设置对象大小。如果参数被设置成5MB,超过5MB的大对象会直接分配到老年代。这样做的目的,是为了避免大对象在Eden区及两个Survivor区之间大量的内存复制,大对象的内存复制耗时比普通对象要高很多。

注意:PretenureSizeThreshold参数只对Serial和ParNew两种回收器有效。

动态对象年龄判定。如果在Survivor空间中相同年龄对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象会直接进入老年代,而不用等到MaxTenuringThreshold中设置的年龄上限。上图,年龄为1的对象超过了Survivor空间的一半,所以这几个对象会直接进入老年代。

实际上,上面对动态对象年龄判定的描述并不精确。上图的场景也会导致相关对象晋升到老年代。年龄为1的对象加上年龄为2的对象超过了半数,这时包括年龄为2的对象以及年龄更大的对象都会被晋升到老年代。所以上图中年龄为2和3的对象都会被晋升到老年代。

老年代垃圾回收。随着年轻代对象的不断晋升,老年代的对象变得越来越多,达到容量阈值后老年代也会发生垃圾回收,我们称之为Major GC或者Full GC,Full GC并不是全局GC,它只发生在老年代。

虽然年轻代和老年代都会发生GC,但是每次GC的时间和成本却大不相同。由于老年代空间大小一般是年轻代的几倍,再加上老年代对象存活率很高,所以整个标记过程比较慢,GC成本也非常高。我们经常说的JVM调优,主要是为了尽量减少老年代Full GC的时间和频次。

老年代垃圾回收器,很少使用复制算法,主要为了避免大量对象的内存复制带来的时间和空间上的开销,一般采用标记清除、标记整理算法,就地标记回收。例如,老年代垃圾收集器CMS就采用了标记清除算法。对于标记清除算法带来的内存碎片问题,CMS提供了两个参数做碎片整理。

32、bean生命周期

第一个阶段:创建前准备

这个阶段主要作用,bean在开始加载之前,要从上下文和一些配置中去解析并且查找bean有关的扩展实现,比如说像"init-method" 容器在初始化bean时候会调用方法,destory-method 容器在销毁时候会调用方法,以及beanfactoryprocessor这类bean加载过程中,前置和后置一些处理扩展实现,这些类或者配置其实是spring 提供给开发者实现bean加载过程中的一些扩展,在很多spring集成的中间件比较常见,比如说像dubbo

第二个阶段:创建实例

这个阶段主要作用,通过反射去创建bean的实例对象,并且会扫描和解析bean定义声明的一些属性

第三个阶段: 依赖注入

如果被实例化的bean, 存在依赖其他bean对象一些情况,则需要对依赖的bean进行对象注入,比如常见的@autowired以及setter注入等,这样一些配置形式;同时在这个阶段会触发一些扩展的调用,比如说常见的扩展类beanpostProcessors用来实现bean初始化前后的扩展回调,比如beanfactoryaware

第四个阶段:容器缓存阶段

主要作用是把bean保存到容器中,以及spring的缓存中;到了这个阶段的bean,bean就可以被开发者使用了,这个阶段常用操作init-method,这个属性的一些方法,或者会被调用以及beanpostprocessors后置处理器方法,也会在这个阶段触发

第五个阶段:销毁实例阶段

当spring的应用上下文被关闭时候, 那么这个上下文所有的bean会被销毁,如果存在bean,实现了DisposableBean接口,或者配置了destory-method属性的方法会在这个阶段会被调用

33、SpringMVC流程

1)客户端向web服务器(如tomcat)发送一个http请求,web服务器对http请求进行解析,解析后的url地址如果匹配到DispatchServlet的映射路径(通过web.xml中的servlet-mapping配置),web容器就会将请求交给DispatchServlet处理

(2)DispatcherServlet接收到这个请求后,再对URL进行解析,得到请求资源标识符(URI)。然后调用相应方法得到的HandlerMapping对象,再根据URI,调用这个对象的相应方法获得Handler对象以及它对应的拦截器。(在这里只是获得了Handler对象,并不会操作它,在SpringMVC中,是通过HandlerAdapter对Handler进行调用、控制的)

(3)DispatcherServlet根据得到的Handler对象,选择一个合适的HandlerAdapter,创建其实例对象,执行拦截器中的preHandler()方法。

(4)在拦截器方法中,提取请求中的数据模型,填充Handler入参,所以所有准备工作都已做好,开始执行Handler(我们写的controller代码并不是能被直接执行,需要有刚才那些操作,才能转变为Handler被执行)。

(5)Handler执行完毕后返回一个ModelAndView对象给DispatcherServlet。

(6)这个ModleAndView只是一个逻辑视图,并不是真正的视图,DispatcherServlet通过ViewResolver视图解析器将逻辑视图转化为真正的视图(通俗理解为将视图名称补全,如加上路径前缀,加上.jsp后缀,能指向实际的视图)。

(7)DispatcherServlet通过Model将ModelAndView中得到的处数据解析后用于渲染视图。将得到的最终视图通过http响应返回客户端。

34、Spring Boot自动装配

26.1 如何理解,是什么

自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置;在我看来,Spring Boot 是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和业务无关的配置。

26.2 如何使用,怎么做

在 Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可以实现自动装配;@SpringBootApplication 是 一 个 复 合 注 解 , 真 正 实 现 自 动 装 配 的 注 解 是@EnableAutoConfiguration

26.3 如何实现,为什么

自动装配的实现主要依靠三个核心关键技术

引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration 配置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对象。

这个配置类是放在第三方的 jar 包里面,然后通过 Spring Boot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:/META-INF/spring.factories 文件中。

这样 Spring Boot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主要是用到了 Spring 里面的 SpringFactoriesLoader 来完成的。

Spring Boot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的ImportSelector 接口,实现对这些配置类的动态加载。

35、 Linux熟悉吗,用过哪些命令

排查日志会用到一些命令:`vim`,`grep`,`less`,`more`,`tail`

文件基本操作:find

线程操作:ps,top

磁盘操作:df,du

ps -ef | grep java

[root@localhost ~]# du -sh *
0       20221108.md
4.0K    ab.md
140K    arthas-boot.jar
36K     arthas-output
4.0K    catfile1.md
4.0K    catfile2.md
4.0K    catfile.md
4.0K    dir1
0       dir1_backup
12K     dir1_second
44K     dir2
4.0K    dir_abc
17M     highcpuproject-0.0.1-SNAPSHOT.jar
40K     logs
17M     springboot_oom_demo-0.0.1-SNAPSHOT.jar
4.0K    uniqfile_backup.txt
4.0K    uniqfile.txt

[root@localhost ~]# df -lh
文件系统                 容量  已用  可用 已用% 挂载点
devtmpfs                 475M     0  475M    0% /dev
tmpfs                    487M     0  487M    0% /dev/shm
tmpfs                    487M  7.6M  479M    2% /run
tmpfs                    487M     0  487M    0% /sys/fs/cgroup
/dev/mapper/centos-root   17G  4.1G   13G   24% /
/dev/sda1               1014M  156M  859M   16% /boot
tmpfs                     98M     0   98M    0% /run/user/0

[root@localhost ~]# ps -ef | grep redis
root        682      1  0 16:10 ?        00:00:01 /usr/local/redis/bin/redis-server 0.0.0.0:6379
root       2360   2181  0 16:25 pts/0    00:00:00 grep --color=auto redis                 

36、ThreadLocal

36.1 是什么

ThreadLocal,也就是线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

36.2 如何使用

场景1,ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

场景2,ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

36.3 底层如何实现的

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
  • 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
  • ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。

36.4 ThreadLocal 内存泄露是怎么回事?

我们先来分析一下使用ThreadLocal时的内存,我们都知道,在JVM中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。

所以呢,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。

ThreadLocal内存分配

ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。

“弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。”

那么现在问题就来了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题

那怎么解决内存泄漏问题呢?

很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。

ThreadLocal<String> localVariable = new ThreadLocal();
try {
    localVariable.set("鄙人三某”);
    ……
} finally {
    localVariable.remove();
}

那为什么key还要设计成弱引用?

key设计成弱引用同样是为了防止内存泄漏。

假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLoca的强引用就没有了,但是此时key还强引用指向ThreadLoca,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。

36.5 ThreadLocalMap的结构了解吗?

ThreadLocalMap虽然被叫做Map,其实它是没有实现Map接口的,但是结构还是和HashMap比较类似的,主要关注的是两个要素:元素数组散列方法

ThreadLocalMap结构示意图

  • 元素数组

    一个table数组,存储Entry类型的元素,Entry是ThreaLocal弱引用作为key,Object作为value的结构。

 private Entry[] table;
  • 散列方法

    散列方法就是怎么把对应的key映射到table数组的相应下标,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table数组长度减一&运算(相当于取余)。

int i = key.threadLocalHashCode & (table.length - 1);

这里的threadLocalHashCode计算有点东西,每创建一个ThreadLocal对象,它就会新增0x61c88647,这个值很特殊,它是斐波那契数 也叫 黄金分割数hash增量为 这个数字,带来的好处就是 hash 分布非常均匀

    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

36.6 ThreadLocalMap怎么解决Hash冲突的?

我们可能都知道HashMap使用了链表来解决冲突,也就是所谓的链地址法。

ThreadLocalMap没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。

ThreadLocalMap解决冲突

如上图所示,如果我们插入一个value=27的数据,通过 hash计算后应该落入第 4 个槽位中,而槽位 4 已经有了 Entry数据,而且Entry数据的key和当前不相等。此时就会线性向后查找,一直找到 Entry为 null的槽位才会停止查找,把元素放到空的槽中。

在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。

36.7 ThreadLocalMap扩容机制了解吗?

if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

再着看rehash()具体实现:这里会先去清理过期的Entry,然后还要根据条件判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4来决定是否需要扩容。

private void rehash() {
    //清理过期Entry
    expungeStaleEntries();

    //扩容
    if (size >= threshold - threshold / 4)
        resize();
}

//清理过期Entry
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

接着看看具体的resize()方法,扩容后的newTab的大小为老数组的两倍,然后遍历老的table数组,散列方法重新计算位置,开放地址解决冲突,然后放到新的newTab,遍历完成之后,oldTab中所有的entry数据都已经放入到newTab中了,然后table引用指向newTab

36.8 父子线程怎么共享数据?

父线程能用ThreadLocal来给子线程传值吗?毫无疑问,不能。那该怎么办?

这时候可以用到另外一个类——InheritableThreadLocal

使用起来很简单,在主线程的InheritableThreadLocal实例设置值,在子线程中就可以拿到了。

public class InheritableThreadLocalTest {
    
    public static void main(String[] args) {
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        // 主线程
        threadLocal.set("不擅技术");
        //子线程
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println("鄙人三某 ," + threadLocal.get());
            }
        };
        t.start();
    }
}

原理很简单,在Thread类里还有另外一个变量:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在Thread.init的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

37、idea解决git代码冲突问题

解决上图中的冲突方案如下:
Accept Yours:代表以自己的为准
Accept Theris:代表以更新下来的文件为准
Merge:代表手动合并
一般解决冲突我们都是选择 Merge

36、面试必备

简历,面经,知己知彼,录音设备

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/228343.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

nvidia安装出现7-zip crc error解决办法

解决办法&#xff1a;下载network版本&#xff0c;重新安装。&#xff08;选择自己需要的版本&#xff09; 网址&#xff1a;CUDA Toolkit 12.3 Update 1 Downloads | NVIDIA Developer 分析原因&#xff1a;local版本的安装包可能在下载过程中出现损坏。 本人尝试过全网说的…

crmeb本地开发配置代理

crmeb 是一个开源的商城系统&#xff0c; v5 版本是一个前后端分离的项目&#xff0c; 我们从git仓库中下载下来的是一个文件夹&#xff0c;其结构是这样的 我的系统没有使用docker &#xff0c;使用的是 laragon 的系统 所以首先我们要在 nginx 中配置 之后&#xff0c; 我们…

钒电解液回收提钒树脂

#钒电解液回收提钒树脂 钒是一种重要的战略金属具有硬度大、抗拉强度强、熔点高等优点主要应用于冶金、电池、核材料、航空航天及能源等领域。 钒电池全称全钒氧化还原液流电池具有环境友好、循环寿命长、能量效率较高等优点&#xff0c;钒电解液是钒电池的关键部分由钒离子和硫…

阿里云国际基于CentOS系统镜像快速部署Apache服务

阿里云轻量应用服务器提供了Windows Server系统镜像和主流的Linux系统镜像&#xff0c;您可以通过该类镜像创建纯净、安全、稳定的运行环境。本文以CentOS 7.6系统镜像为例&#xff0c;介绍如何快速配置Apache服务。 背景信息 注意&#xff0c;阿里云国际通过corebyt注册并充…

【小白专用】MySQL入门(详细总结)

3. 创建数据库 使用 create database 数据库名; 创建数据库。 create database MyDB_one; create database DBAliTest; 创建数据库成功后&#xff0c;数据库的数量变成了6个&#xff0c;多了刚才创建的 dbalitest 。 4. 创建数据库时设置字符编码 使用 create database 数据…

(六) python观察者设计模式

6.1行为型模式简介 观察者设计模式是最简单的行为型模式之一,所以我们先简单了解一下行为型模式 创建型模式的工作原理是基于对象的创建机制的。由于这些模式隔离了对象的创建细 节&#xff0c;所以使得代码能够与要创建的对象的类型相互独立。结构型模式用于设计对象和类的结…

echarts折线图的数据显示

一、 echarts让折线图的每个折点都显示y轴的数值 效果如下 // 在 series中添加 itemStyle : { normal: {label : {show: true}}}series: [{name: 买入汇率,data: BuyRate,type: line,itemStyle : { normal: {label : {show: true}}}},{name: 卖出汇率,data: SaleRate,type: lin…

仅需30秒完美复刻任何人的声音 - 最强AI音频11Labs

我的用词一直都挺克制的&#xff0c;基本不会用到“最强”这个字眼。 但是这一次的这个AI应用&#xff0c;是我认为在TTS&#xff08;文字转音频&#xff09;这个领域&#xff0c;当之无愧的“最强”。 ElevenLabs&#xff0c;简称11Labs。 仅需30秒到5分钟左右的极少的数据集…

Qt简介、工程文件分离、创建Qt工程、Qt的帮助文档

QT 简介 core&#xff1a;核心模块&#xff0c;非图形的接口类&#xff0c;为其它模块提供支持 gui&#xff1a;图形用户接口&#xff0c;qt5之前 widgets&#xff1a;图形界面相关的类模块 qt5之后的 database&#xff1a;数据库模块 network&#xff1a;网络模块 QT 特性 开…

土壤水分传感器土壤体积含水率含量监测仪器

产品概述 外型小巧轻便&#xff0c;便于携带和连接。 土壤水分传感器由电源模块、变送模块、漂零及温度补偿模块、数据处理模块等组成。传感器内置信号采样及放大、漂零及温度补偿功能&#xff0c;用户接口简洁、方便。 功能特点 ◆本传感器体积小巧化设计&#xff0c;测量…

Sam Altman当选“TIME时代周刊”2023年度最佳CEO!还有梅西、Taylor Swift当选...

TIME时代周刊昨日在官网公布了2023年最佳CEO—— Sam Altman当选! 此外&#xff0c;Taylor Swift当选年度最佳人物&#xff0c;梅西当选年度最佳运动员。 Sam Altman的当选可谓是实至名归&#xff01;没有谁能比火爆全球的ChatGPT背后&#xff0c;OpenAI的CEO更“成功”了。 …

手把手教你写 Compose 动画 -- 讲的不能再细的 AnimationSpec 动画规范

前面我们聊过 animateDpAsState 和 Animatable 两种动画 API 的用法&#xff0c;但只是简单了解了&#xff0c;这两个函数内部都有一个共同的核心参数&#xff1a;AnimationSpec。 Composable fun animateDpAsState(targetValue: Dp,animationSpec: AnimationSpec<Dp> …

代码随想录算法训练营第45天| 70. 爬楼梯 (进阶) 322. 零钱兑换 279.完全平方数

JAVA代码编写 70. 爬楼梯&#xff08;进阶版) 卡码网&#xff1a;57. 爬楼梯&#xff08;第八期模拟笔试&#xff09; 题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬至多m (1 < m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f…

卷王开启验证码后无法登陆问题解决

问题描述 使用 docker 部署&#xff0c;后台设置开启验证&#xff0c;重启服务器之后&#xff0c;docker重启&#xff0c;再次访问系统&#xff0c;验证码获取失败&#xff0c;导致无法进行验证&#xff0c;也就无法登陆系统。 如果不了解卷王的&#xff0c;可以去官网看下。…

【K8S】微服务不香了?单体化改造悄然兴起!!

微服务一直以来是服务治理的基本盘之一,落地到云原生上,往往是每个 K8s pods 部署一个服务,独立迭代、独立运维。 但是在快速部署的时候,有时候,我们可能需要一些宏服务的优势。有没有一种方法,能够 “既要又要” 呢?本文基于 tRPC-Go 服务,提出并最终实践了一种经验证…

医学图像数据处理流程以及遇到的问题

数据总目录&#xff1a; /home/bavon/datasets/wsi/hsil /home/bavon/datasets/wsi/lsil 1 规整文件命名以及xml拷贝 data_prepare.py 的 align_xml_svs 方法 if __name__ __main__: file_path "/home/bavon/datasets/wsi/lsil"# align_xml_svs(file_path) # b…

程序员的养生指南(生命诚可贵,一人永流传!珍惜生命,从你我做起)

作为程序员&#xff0c;我们经常需要长时间坐在电脑前工作&#xff0c;这对我们的身体健康造成了很大的影响。为了保持健康&#xff0c;我们需要采取一些养生措施来延寿。下面是我个人的一些养生经验和建议&#xff0c;希望能对大家有所帮助。 1、合理安排工作时间&#xff1a;…

Bert-vits2新版本V2.1英文模型本地训练以及中英文混合推理(mix)

中英文混合输出是文本转语音(TTS)项目中很常见的需求场景&#xff0c;尤其在技术文章或者技术视频领域里&#xff0c;其中文文本中一定会夹杂着海量的英文单词&#xff0c;我们当然不希望AI口播只会念中文&#xff0c;Bert-vits2老版本(2.0以下版本)并不支持英文训练和推理&…

多功能智能遥测终端机 5G/4G+北斗多信道 视频采集传输

计讯物联多功能智能遥测终端机&#xff0c;全网通5G/4G无线通信、弱信号地区北斗通信&#xff0c;多信道自动切换保障通信联通&#xff0c;丰富网络接口及行业应用接口&#xff0c;支持水利、环保、工业传感器、控制终端、智能终端接入&#xff0c;模拟量/数字量/信号量采集&am…

一文详解Java反射

文章目录 反射是什么&#xff1f;反射的作用所有方法汇总一、加载Class对象二、加载类的构造器对象三、加载类的成员变量四、加载类的成员方法 反射是什么&#xff1f; 反射就是&#xff1a;加载类&#xff0c;并允许以编程的方式解剖类中的某个成分&#xff08;成员变量&#…