面试题集锦-有无到有(2020版完结)

mybatis一对多和多对一配置

一对多使用:collection

多对一使用:assocation 标签

分布式事物的实现方式

引入协调者(Cooradinator)和参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。

1.XA事物(2pc)提交,两阶段提交

第一阶段确定是否可以提交事务(提交事务请求),同时将对应的数据写入uodo日志中。返回一个状态值

第二阶段根据协调者返回的状态值,进行对应的事物提交或者事物回滚。

问题

同步阻塞,单点故障等

2.3pc

三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

相比2pc引入两个变化

1.增加了超时时间的属性。

2.在第一阶段和第二阶段中插入一个准备阶段

3.最大努力通知(本地消息表)

本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。

4.TCC最终一致性 (Try-Confirm-Cancel)补偿模式

5.可靠消息最终一致(常用)

直接使用mq

分布式锁的实现方式

  1. 基于数据库实现分布式锁;

  2. 基于缓存(Redis等)实现分布式锁;

setnx

  1. 基于Zookeeper实现分布式锁(临时节点);

4.Memcached(add命令)

分布式锁需要具备哪些条件?

互斥性:在任意一个时刻,只有一个客户端持有锁。

无死锁:即便持有锁的客户端崩溃或者其他意外事件,锁仍然可以被获取。

容错:只要大部分Redis节点都活着,客户端就可以获取和释放锁

事物的隔离级别

1.读未提交(Read Uncommitted)

2.读提交(Read Committed)

3.可重复读(Repeated Read)

4.串行化(Serializable)

什么是脏读、幻读、不可重复读

脏读

是指当一个事务正在访问数据,并且对数据进行了修改。而这种修改还没有提交到数据库中,这时,另外一个事务也访问了这个数据,然后使用了这个数据。

不可重复读

通俗的讲,一个事务范围内,多次查询某个数据,却得到不同的结果。

幻读

是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到了表中的全部数据行。

同时,第二个事务也修改了这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好像发生了幻觉一样。

sping 的ioc和di

IOC: 控制反转,将类的对象的创建交给Spring类管理创建.

DI: 依赖注入,将类里面的属性在创建类的过程中给属性赋值.

DI和IOC的关系: DI不能单独存在,DI需要在IOC的基础上来完成.

rabbitmq的几种工作模式

1.简单模式:一个生产者,一个消费者

2.work模式:一个生产者,多个消费者,每个消费者获取到的消息唯一。

3.订阅模式:一个生产者发送的消息会被多个消费者获取。

4.路由模式:发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key

5.topic模式:将路由键和某模式进行匹配,此时队列需要绑定在一个模式上,“#”匹配一个词或多个词,“*”只匹配一个词。

6.rpc模式

rabbitmq如何实现同时给多个消费者发送

订阅模式或者topic模式

rabbitmq如何实现延时消费

可以用消息超时,加入死信队列,进而从另外一个队列中实现消费,达到延迟消费的目的;

具体操作:需要给原队列设置死信队列,然后消费方监听,选择设置的死信队列进行消费。

如何确保消息正确地发送至RabbitMQ

将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。

一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。

如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。

发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。

当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息

如何保证消费者消息不丢失

1.生产者丢失消息
a.使用事物方式,但是会存在同步阻塞问题
b.使用confirm模式,异步非阻塞

2.rabbitmq弄丢消息
设置消息持久化到磁盘。

设置持久化有两个步骤:
a.创建queue的时候将其设置为持久化的,这样就可以保证rabbitmq持久化queue的元数据,但是不会持久化queue里面的数据。

b.发送消息的时候讲消息的deliveryMode设置为2,这样消息就会被设为持久化

3.消费者丢失消息
使用rabbitmq提供的ack机制

关闭自动ack,使用收到ack模式

dubbo的原理和层级架构

1.服务接口层(Service):与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。

2.配置层(Config):对外配置接口,以ServiceConfig和ReferenceConfig为中心,可以直接new配置类,也可以通过Spring解析配置生成配置类。

3.服务代理层(Proxy):服务接口透明代理,生成服务的客户端Stub和服务器端Skeleton,以ServiceProxy为中心,扩展接口为ProxyFactory。

4.服务注册层(Registry):封装服务地址的注册与发现,以服务URL为中心,扩展接口为RegistryFactory、Registry和RegistryService。可能没有服务注册中心,此时服务提供方直接暴露服务。

5.集群层(Cluster):封装多个提供者的路由及负载均衡,并桥接注册中心,以Invoker为中心,扩展接口为Cluster、Directory、Router和LoadBalance。将多个服务提供方组合为一个服务提供方,实现对服务消费方来透明,只需要与一个服务提供方进行交互。

6.监控层(Monitor):RPC调用次数和调用时间监控,以Statistics为中心,扩展接口为MonitorFactory、Monitor和MonitorService。

7.远程调用层(Protocol):封将RPC调用,以Invocation和Result为中心,扩展接口为Protocol、Invoker和Exporter。Protocol是服务域,它是Invoker暴露和引用的主功能入口,它负责Invoker的生命周期管理。Invoker是实体域,它是Dubbo的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起invoke调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。

8.信息交换层(Exchange):封装请求响应模式,同步转异步,以Request和Response为中心,扩展接口为Exchanger、ExchangeChannel、ExchangeClient和ExchangeServer。

9.网络传输层(Transport):抽象mina和netty为统一接口,以Message为中心,扩展接口为Channel、Transporter、Client、Server和Codec。

10.数据序列化层(Serialize):可复用的一些工具,扩展接口为Serialization、 ObjectInput、ObjectOutput和ThreadPool。

dubbo服务发现的流程

dubbo服务提供者暴露服务的主过程:

首先 ServiceConfig 类拿到对外提供服务的实际类 ref(如:HelloWorldImpl),

然后通过 ProxyFactory 类的 getInvoker 方法使用 ref 生成一个 AbstractProxyInvoker 实例,

到这一步就完成具体服务到 Invoker 的转化,

接下来就是 Invoker 转换到 Exporter 的过程.

redis的集群

1.主从模式

具备减少单库读写压力的特性

2.Sentinel模式

sentinel模式基本可以满足一般生产的需求,具备高可用性

3.Cluster模式

cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。

Redis集群详解

搜索引擎

ES是什么

Elasticsearch是高度可伸缩的开源全文搜索和分析引擎。它允许我们快速实时地存储、搜索、分析大数据。

Elasticsearch使用Lucene作为内部引擎,但是在你使用它做全文搜索时,只需要使用统一开发好的API即可,而不需要了解其背后复杂的Lucene的运行原理。

它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。

不过,Elasticsearch不仅仅是Lucene和全文搜索,我们还能这样去描述它:

1.分布式的实时文件存储,每个字段都被索引并可被搜索

2.分布式的实时分析搜索引擎

3.可以扩展到上百台服务器,处理PB级结构化或非结构化数据

ES特点

ES中,每一个字段都会默认被建立索引。也就是说,每一个字段都会有一个反向索引以便快速搜索

ES可以在同一个查询中使用所有的反向索引,以惊人的速度返回查询结果。

ES是面向文档型数据库,这意味着它存储的是整个对象或者文档,它不但会存储它们,还会为他们建立索引。

ES使用JSON作为文档序列化的格式

ES存储搜索引擎入门

什么是缓存击穿 缓存穿透,缓存雪崩,以及解决方案

缓存穿透

是指缓存和数据库中都没有的数据,而用户不断发起请求,请求直接打在了数据库上。解决方案: 对空值设置缓存

缓存击穿

是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

缓存击穿的话,设置热点数据永远不过期。或者加上互斥锁就能搞定了。

缓存雪崩

是指在某一个时间段,缓存集中过期失效。

雪崩其实也可以做到提前预防,那就是用到过期时间的key,时间全部错开,此外,有些数据可以做永久保存的话那就直接保存好了,这样就不会造成大面积的key失效了。

Spring boot bean的自动装配实现

1.激活自动装配

比如 使用@EnableAutoConfiguration

2.实现自动装配

通过定义 AutoConfiguration注解

3.配置自动装配

在META-INF/spring.factories文件中进行配置

ArrayList和LinkedList的区别

1.ArrayListArray和LinkedList数据结构的不同。ArrayList是基于数组实现的,LinkedList是基于双链表实现的。
2.对于随机访问get和set,ArrayList优于LinkedList,因为ArrayList可以随机定位,而LinkedList要移动指针一步一步的移动到节点处。

3.如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;

4.如果应用程序有更多的插入或者删除操作,较少的数据读取,LinkedList对象要优于ArrayList对象;

5.注意ArrayList的插入,删除操作也不一定比LinkedList慢,如果在List靠近末尾的地方插入,
那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,
反而耗费较多时间,这时ArrayList就比LinkedList要快。

hashmap的原理和ConcurrentHashMap的区别

HashMap和ConcurrentHashMap的知识总结

1.ConcurrentHashMap对整个桶数组进行了分bai段du,而HashMap则没有

2.ConcurrentHashMap在每一个分段上都用锁进行保护,从而让锁的粒dao度更精细一些,并发性能更好,而HashMap没有锁机制,不是线程安全的

hashmap和TreeMap的区别

1、HashMap是通过hashcode()对其内容进行快速查找的;HashMap中的元素是没有顺序的;

TreeMap中所有的元素都是有某一固定顺序的,如果需要得到一个有序的结果,就应该使用TreeMap;

2、HashMap和TreeMap都不是线程安全的;

3、HashMap继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;

TreeMap继承SortedMap类;它保持键的有序顺序;

4、HashMap:基于hash表实现的;使用HashMap要求添加的键类明确定义了hashcode() 和equals() (可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子;

TreeMap:基于红黑树实现的;TreeMap就没有调优选项,因为红黑树总是处于平衡的状态;

5、HashMap:适用于Map插入,删除,定位元素;

TreeMap:适用于按自然顺序或自定义顺序遍历键(key);

hashmap和hashtable的区别

相同点:

实现原理相同,功能相同,底层都是哈希表结构,查询速度快,在很多情况下可以互用

不同点:

1.Hashtable是早期提供的接口,HashMap是新版JDK提供的接口。

2.Hashtable继承Dictionary类,HashMap实现Map接口。

3.Hashtable线程安全,HashMap线程非安全。

4.Hashtable不允许null值,HashMap允许null值。

5.Hashtable初始值大小是11,扩容是长度的2倍+1;HashMap初始化大小是16,扩容是原来的两倍。

6.哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

list和set的区别

1:List:可重复,有序,数组或者链表存储

2:Set:不可重复,无序,使用map存储

List 和 Map 区别?

一个是存储单列数据的集合,另一个是存储键和值这样的双列数据的集合,

List 中存储的数据是有顺序,并且允许重复;

Map 中存储的数据是没有顺序的,其 key 是不能重复的,它的值是可以有重复的.

List 的实现类有 ArrayList,Vector 和 LinkedList

ArrayList 和 Vector 内部是线性动态数组结构,在查询效率上会高很多,Vector 是线程安全的,相比 ArrayList 线程不安全的,性能会稍慢一些。

LinkedList:是双向链表的数据结构存储数据,在做查询时会按照序号索引数据进行前向或后向遍历,查询效率偏低,但插入数据时只需要记录本项的前后项即可,所以插入速度较快。

Set 的实现类有 HashSet 和 TreeSet

HashSet:内部是由哈希表(实际上是一个 HashMap 实例)支持的。它不保证 set 元素的迭代顺序。

TreeSet:TreeSet 使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序。

Map 接口有三个实现类:Hashtable,HashMap,TreeMap,LinkedHashMap

Hashtable:内部存储的键值对是无序的是按照哈希算法进行排序,与 HashMap 最大的区别就是线程安全。键或者值不能为 null,为 null 就会抛出空指针异常。

TreeMap:基于红黑树 (red-black tree) 数据结构实现,按 key 排序,默认的排序方式是升序。

LinkedHashMap:有序的 Map 集合实现类,相当于一个栈,先 put 进去的最后出来,先进后出。

接口幂等性的实现

1.同步锁(单线程,集群可能会失效)

2.分布式锁如redis(实现复杂)

2.业务字段加唯一约束(简单)

3.令牌表+唯一约束(简单推荐)

4.mysql的insert ignore或者on duplicate key update(简单)

5.共享锁+普通索引(简单)

6.利用MQ或者Redis扩展(排队)

7.其他方案如多版本控制MVCC 乐观锁 悲观锁 状态机等。。。

上传和下载

分为xls和xlsx的,分别对应的HSSFWorkbook和XSSFWorkbook工作簿

如何防止xss注入

XSS 攻击可分为存储型、反射型和 DOM 型三种

反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。

DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。

1.改成纯前端渲染,把代码和数据分隔开。

2.对 HTML 做充分转义。

nginx反向代理后如何获取ip地址

可以通过配置获取

每个location中增加配置:

proxy_set_header Host $http_host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_set_header X-Forwarded-Proto $scheme;

解释以下上面的配置,以上配置是在Nginx反向代理的时候,添加一些请求Header。

  1. Host包含客户端真实的域名和端口号;

  2. X-Forwarded-Proto表示客户端真实的协议(http还是https);

  3. X-Real-IP表示客户端真实的IP;

  4. X-Forwarded-For这个Header和X-Real-IP类似,但它在多层代理时会包含真实客户端及中间每个代理服务器的IP。

ASP.NET可以通过取header中X-Real-IP得到真实ip:RequestContext.HttpContext.Request.Headers.Get(“X-Real-IP”)

在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),但是如果存在nginx中间层是获取不了的。

public String getRemortIP(HttpServletRequest request) {  
    if (request.getHeader("x-forwarded-for") == null) {  
        return request.getRemoteAddr();  
    }  
    return request.getHeader("x-forwarded-for");  
}

事物的传播机制

1.propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。

2.propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。

3.propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。

4.propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。

5.propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

6.propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。

7.propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作

什么是索引

1.索引在搜索引擎优化简单解释

指已经被收录且参与关键词排名的页面。

2.索引的通俗解释

索引就像是图书的目录,根据目录中的页码快速找到所需内容。

3.索引在百度百科中的解释

在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。

索引设计的原则

1.适合索引的列是出现在where子句中的列,或者连接子句中指定的列;

2.基数较小的类,索引效果较差,没有必要在此列建立索引;

3.使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间;

4.不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

建索引的几大原则

1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

2.=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

3.尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。

4.索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)。

5.尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

慢查询的优化

1.先运行看看是否真的很慢,注意设置SQL_NO_CACHE

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

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

4.order by limit 形式的sql语句让排序的表优先查

5.了解业务方使用场景

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

7.观察结果,不符合预期继续从0分析

索引失效的情况

1.如果条件中有or,即使其中有条件带索引也不会使用

2.对于多列索引,不是使用的第一部分,则不会使用索引,最左匹配原则

3.like查询是以%开头

4.存在索引列的数据类型隐形转换,则用不上索引,比如列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引

5.where 子句里对索引列上有数学运算,用不上索引

6.where 子句里对有索引列使用函数,用不上索引

7.如果mysql估计使用全表扫描要比使用索引快,则不使用索引

es如何实现分词

es中的分词器由三部分组成

1、character filter:作用:先对要进行分析的文本进行一下过滤,比如html文档,去除其中的标签,比如等等;

2、tokenizer:作用:对文本进行分词,把要进行分析的文本根据所指定的规则,按照其规则把文本拆分为单词,,只可以指定一个;

3、tokenizer filter:作用:把分好的词条(也即将tokenizer分好的词)进一步进行过滤,根据指定的filter把其识别的没用的词条给删除,或者增加(比如增加某些词的同义词)、修改(比如将testing、tested这种词同意为test)词条,可以指定多个。

分词使用场景

1.创建或者更新文档时候,会对相应的文档进行分词处理

2.查询时,会对查询语句进行分词
注意,index时所用的分词器和查询时所用的分词器可不一样,可通过"analyzer"指定index时分词器,

通过"search_analyzer"指定查询时分词器,但建议是设置成一样的,不然不利于查询(”put test_index{“mappings”:{“doc”:{“properties”:{“title”:{“type”:“text”,“analyzer”:“whitespace”,“search_analyzer”:“standard”}}}}})

dubbo服务发现是如何实现的

在配置解析读取装配成bean之后, 初始化, 根据配置协议, 找到注册中心(如Zookeeper)注册, 找到对应服务Protocol(如DubboProtocol)暴露服务.

客户端发起远程调用的大致思路是:

将配置转化为子节码生成的代理实例存到spring上下文中, 调用时, 通过代理实例, 找到相关协议方法发起远程调用.

dubbo zk 注册中心采用是事件通知与客户端拉取方式。

服务第一次订阅的时候将会拉取对应目录下全量数据,然后在订阅的节点注册一个 watcher。

一旦目录节点下发生任何数据变化,zk 将会通过 watcher 通知客户端。客户端接到通知,将会重新拉取该目录下全量数据,并重新注册 watcher。

利用这个模式,dubbo 服务就可以就做到服务的动态发现。

dubbo如何实现服务降级的

dubbo中服务降级分成两个:屏蔽(mock=force)、容错(mock=fail)

mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。

mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。

普通情况是直接调用,容错的情况是调用失败后,返回一个设置的值.而屏蔽就很暴力了,直接连调用都不调用,就直接返回一个之前设置的值.

jvm的结构

1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。

2.执行引擎:负责执行class文件中包含的字节码指令

3.内存区(也叫运行时数据区)

4.本地方法接口:主要是调用C或C++实现的本地方法及返回结果。

jvm内存区分为

1.方法区(Method Area):用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。

2.java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域(后面解释)。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。

3.java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。

4.程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。

5.本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

jvm的垃圾回收算法有哪些?

1.标记-清除算法

从算法的名称上可以看出,这个算法分为两部分,标记和清除。首先标记出所有需要被回收的对象,然后在标记完成后统一回收掉所有被标记的对象。

这个算法简单,但是有两个缺点:一是标记和清除的效率不是很高;二是标记和清除后会产生很多的内存碎片,导致可用的内存空间不连续,当分配大对象的时候,没有足够的空间时不得不提前触发一次垃圾回收。

2.复制算法

这个算法将可用的内存空间分为大小相等的两块,每次只是用其中的一块,当这一块被用完的时候,就将还存活的对象复制到另一块中,然后把原已使用过的那一块内存空间一次回收掉。这个算法常用于新生代的垃圾回收。

复制算法解决了标记-清除算法的效率问题,以空间换时间,但是当存活对象非常多的时候,复制操作效率将会变低,而且每次只能使用一半的内存空间,利用率不高。

3.标记-整理算法

这个算法分为三部分:一是标记出所有需要被回收的对象;二是把所有存活的对象都向一端移动;三是把所有存活对象边界以外的内存空间都回收掉。

标记-整理算法解决了复制算法多复制效率低、空间利用率低的问题,同时也解决了内存碎片的问题。

4.分代收集算法

根据对象生存周期的不同将内存空间划分为不同的块,然后对不同的块使用不同的回收算法。一般把Java堆分为新生代和老年代,新生代中对象的存活周期短,只有少量存活的对象,所以可以使用复制算法,而老年代中对象存活时间长,而且对象比较多,所以可以采用标记-清除和标记-整理算法。

jvm1.8之后又什么变动

1.移除方法区

JDK 1.7及之前方法区存放的数据有类信息(类名,修饰符,字段描述,方法描述等),常量,静态变量,即时编译后的class文件。

方法区中还包含有常量池:常量池中主要有字面量和符号引用

字面量:文本字符串,声明为final的常量值;

符号引用:包括了三种常量,分别是:类和接口的全限定名,字段的名称和描述符,方法的名称和修饰符。

为什么移除方法区?

1.它在启动时固定大小,很难进行调优,并且FullGC时会移动类元信息

2.类及方法的信息等比较难确定大小,因此对永久代的大小指定比较困难

3.在某些场景下,如果动态加载类过多,容易造成Perm区的OOM。

4.字符串存在方法区中,容易出现性能问题和内存溢出

5.永久代(在GC时用永久代实现方法区)GC垃圾回收效率偏低(回收目标主要是常量池和类型的卸载)

6.永久代的垃圾收集和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集

2.MetaSpace元空间 取而代之

JDK 1.8将方法区中的字符串常量移至堆内存,其他内容如类信息、静态变量、其他常量(如整形常量),即时编译后的class文件等都移动到元空间内。

元空间(MetaSpace)不在堆内存上,而是直接占用的本地内存。因此元空间的大小仅受本地内存限制。也可通过参数来设定元空间的大小

-XX:MetaSpaceSize  初始元空间大小,达到该值就会触发垃圾收集器进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低

该值;如果释放了很少的空间,那么在不超过MaxMetaSpaceSize时,适当提高该值。

-XX:MaxMetaSpaceSize  最大元空间大小,默认没有限制

元空间的特点:

1.每个加载器有专门的存储空间。

2.不会单独回收某个类。

3.元空间里的对象的位置是固定的。

4.如果发现某个加载器不再存活了,会把相关的空间整个回收。

抽象类和接口的区别

1.抽象类可以有构造方法,接口中不能有构造方法。

2.抽象类中可以有普通成员变量,接口中没有普通成员变量

3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。

  1. 抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然

eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。

  1. 抽象类中可以包含静态方法,接口中不能包含静态方法

  2. 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

  3. 一个类可以实现多个接口,但只能继承一个抽象类。

20200801

Spring boot的启动流程

1.初始化SpringApplication

2.运行SpringApplication

3.配置并准备环境

4.创建Spring容器上下文

5.配置Spring容器上下文

6.Spring容器创建之后回调方法postProcessApplicationContext

7.初始化器开始工作

8.Spring容器创建完成之后会调用afterRefresh方法

Spring Mvc 的流程

1.用户发送请求至前端控制器DispatcherServlet;

2.DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;

3.处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;

4.DispatcherServlet 调用 HandlerAdapter处理器适配器;

5.HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);

6.Handler执行完成返回ModelAndView;

7.HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;(ModelAndView包含“模型数据”和“视图名称”)

8.DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;(根据“视图名称”,解析出用户真正能看到的物理视图)

9.ViewResolver解析后返回具体View到DispatcherServlet;

10.DispatcherServlet对View进行渲染视图(即 将模型数据填充至视图中)

11.DispatcherServlet响应用户

Spring Mvc 中的关键点

1.dispatcherservlet

主要请求的分发响应作用

2.HandlerMapping

更具请求路径找到handler,handler实际是业务controller#方法

主要的方法:getHandler()

3.HandlerAdapter

调用handler得到modelAndView,既是拿到数据和viewname

主要方法:support() ; handle()

4.ViewResolver

更具viewName找到html等静态文件,将数据model替换静态文件中的变量,既渲染,最终输出html到页面

Spring boot 循环依赖如何解决

1.进行业务上的解耦

2.注入bean时,在互相依赖的两个bean上加上@Lazy注解

3.spring 使用了三级缓存避免了循环依赖

Spring 的三级缓存

	//一级缓存
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
  	//二级缓存
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
   	//三级缓存
    private final Map<String, Object> earlySingletonObjects = new HashMap(16);

Spring在启动过程中,使用到了三个map,称为三级缓存。

解决循环依赖的时候会优先查询二级缓存,二级缓存中没有则查三级缓存,有则升级二级缓存为三级缓存

Spring 循环依赖及三级缓存

mybatis的二级缓存

MyBatis的一级缓存指的是在一个Session域内,session为关闭的时候执行的查询会根据SQL为key被缓存(跟mysql缓存一样,修改任何参数的值都会导致缓存失效)

二级缓存就是global caching,它超出session范围之外,可以被所有sqlSession共享,它的实现机制和mysql的缓存一样,开启它只需要在mybatis的配置文件开启settings里的

<setting name="cacheEnabled" value="true"/>

Spring的spi机制,以及Spring中哪儿使用的spi

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

为某个接口寻找服务实现的机制

SPI约定

当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。

该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

通过这个约定,就不需要把服务放在代码中了,通过模块被装配的时候就可以发现服务类了。

spring的中的使用

数据库的配置等

Sql优化、大查询优化

1.合理使用索引

2.避免或简化排序

3.消除对大型表行数据的顺序存取

4.避免相关子查询

5.避免困难的正规表达式

6.使用临时表加速查询

7.用排序来取代非顺序存取

Mysql千万级大数据量查询优化

explain如何看

table:显示这一行的数据是关于哪张表的

type:这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、indexhe和all

possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从where语句中选择一个合适的语句

key: 实际使用的索引。如果为null,则没有使用索引。很少的情况下,mysql会选择优化不足的索引。这种情况下,可以在select语句中使用use index(indexname)来强制使用一个索引或者用ignore index(indexname)来强制mysql忽略索引

key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好

ref:显示索引的哪一列被使用了,如果可能的话,是一个常数

rows:mysql认为必须检查的用来返回请求数据的行数

extra:关于mysql如何解析查询的额外信息。

如何防止超卖、以及发生超卖的后续处理

1.使用数据库版本号作为乐观锁

2.使用 redis 的队列来实现。将要促销的商品数量以队列的方式存入 redis 中,每当用户抢到一件促销商品则从队列中删除一个数据,确保商品不会超卖。

rabbitMq发生消息堆积如何处理

1.紧急临时扩容,更快的速度去消费数据

修复Consumer不消费问题,使其恢复正常消费,根据业务需要看是否要暂停

临时topic队列扩容,并提高消费者能力,但是如果增加Consumer数量,但是堆积的topic里面的message queue数量固定,过多的consumer不能分配到message queue

编写临时处理分发程序,从旧topic快速读取到临时新topic中,新topic的queue数量扩容多倍,然后再启动更多consumer进行在临时新的topic里消费

直到堆积的消息处理完成,再还原到正常的机器数量

2.先修复consumer的问题,确保其恢复消费速度,然后将现有cnosumer都停掉

新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量

然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,

消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue

接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据

这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据

等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息

rabbitMq发生数据丢失如何处理

类似如何保证消息不丢失的处理方式

增加:重试机制

spring.rabbitmq.listener.simple.retry.max-attempts=5  最大重试次数
spring.rabbitmq.listener.simple.retry.enabled=true 是否开启消费者重试(为false时关闭消费者重试,这时消费端代码异常会一直重复收到消息)
spring.rabbitmq.listener.simple.retry.initial-interval=5000 重试间隔时间(单位毫秒)
spring.rabbitmq.listener.simple.default-requeue-rejected=false 重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)

如果设置了重试模式,那么在出现异常时没有捕获异常会进行重试,如果捕获了异常不会重试。

synchronized的锁的范围

1.修饰方法

Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,

修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数

2.修饰一个代码块

在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

但是线程可以访问同一个类中非synchronized修饰的方法或代码块

3.修饰一个静态的方法

静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。

4.修饰一个类

给class加锁和上例的给静态方法加锁是一样的,所有对象公用一把锁

总结

  1. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;

如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。

  1. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。

  2. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

synchronized的升级过程

通过关键字synchronized 可以对实例对象,实例方法,类、类方法进行加锁,锁的生命周期/范围,对应着加锁的对象/类/方法的生命周期/范围。

synchronized可分为无锁(逃逸分析锁解除)、偏向锁、轻量级锁、重量级锁,锁可以升级但是不可以降级

各种级别的锁对应线程场景

无锁:无线程个数约束,没有synchronized、lock修饰/做同步控制

偏向锁:只有一个线程访问

轻量级锁:2个线程交替访问

重量级锁:多线程并发访


1.无锁–>偏向锁–>轻量级锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,

只需测试Mark Word里线程ID是否为当前线程。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要判断偏向锁的标识。

如果标识被设置为0(表示当前是无锁状态),则使用CAS竞争锁;如果标识设置成1(表示当前是偏向锁状态),则尝试使用CAS将对象头的偏向锁指向当前线程,触发偏向锁的撤销。

偏向锁只有在竞争出现才会释放锁。

当其他线程尝试竞争偏向锁时,程序到达全局安全点后(没有正在执行的代码),它会查看Java对象头中记录的线程是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程可以竞争将其设置为偏向锁;

如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

2.轻量级锁–>重量级锁

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的MarkWord复制到锁记录中,即Displaced Mark Word。

然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。

如果成功,当前线程获得锁。如果失败,表示其他线程在竞争锁,当前线程使用自旋来获取锁。当自旋次数达到一定次数时,锁就会升级为重量级锁。

轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回到对象头,如果成功,表示没有竞争发生。如果失败,表示当前锁存在竞争,锁已经被升级为重量级锁,则会释放锁并唤醒等待的线程。

lock的使用

1.使用 ReentrantLock 类

lock():获取锁

unlock():释放锁

效果和 synchronized 关键字一样。

2.使用 Condition 实现等待/通知

3.实现生产者/消费者模式:一对一交替打印

4.公平锁与非公平锁

公平锁:线程获取锁的顺序是按照线程加载的顺序来分配的(先来先得 FIFO)

非公平锁:一种获取锁的抢占机制,是随机获得锁的

new ReentrantLock(true); // 公平锁
new ReentrantLock(false); // 非公平锁(默认这种,空构造函数)

线程池的创建方式、线程池的参数

1.通过Executors工厂方法创建

2.通过new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue《Runnable》 workQueue)自定义创建

1.newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

2.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

3.newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

4.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程池的参数

1.corePoolSize:核心线程数

核心线程会一直存活,即使没有任务需要执行

当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理

设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

2.queueCapacity:任务队列容量(阻塞队列)

当核心线程数达到最大时,新任务会放在队列中排队等待执行

3.maxPoolSize:最大线程数

当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务

当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

4.keepAliveTime:线程空闲时间

当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize

如果allowCoreThreadTimeout=true,则会直到线程数量=0

5.allowCoreThreadTimeout:允许核心线程超时

6.rejectedExecutionHandler:任务拒绝处理器

两种情况会拒绝处理任务:

当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务

当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务

线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常

ThreadPoolExecutor类有几个内部实现类来处理这类情况:

AbortPolicy 丢弃任务,抛运行时异常

CallerRunsPolicy 执行任务

DiscardPolicy 忽视,什么都不会发生

DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务

实现RejectedExecutionHandler接口,可自定义处理器

线程池执行流程

当线程数小于核心线程数时,创建线程。

当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

当线程数大于等于核心线程数,且任务队列已满

若线程数小于最大线程数,创建线程

若线程数等于最大线程数,抛出异常,拒绝任务

无界线程池会造成什么问题

LinkedBlockingQueue默认的最大任务数量是Integer.MAX_VALUE,非常大,可以理解为无限大吧;

但是存在这种情况,当每个线程获取到一个任务后,执行时间比较长,导致workQueue里积压的任务越来越多,机器的内存使用不停的飙升,最后也会导致OOM

统一的异常处理,不使用trycatch

使用@ControllerAdvice与@ExceptionHandler

全局异常处理
使用 @ControllerAdvice 实现全局异常处理,只需要定义类,添加该注解即可定义方式如下:

@ControllerAdvice
public class MyGlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ModelAndView customException(Exception e) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("message", e.getMessage());
        mv.setViewName("myerror");
        return mv;
    }
}

20200803

sql视图

1.视图可以简化用户的操作
  
视图机制使用户可以将注意力集中在所关心的数据上

2.视图使用户能以多种角度看待同一数据

视图使用户能以多种角度看待同一数据,当许多不同种类的用户共享同一数据库时,这种灵活性很重要

3.视图对重构数据库提供了一定程度的逻辑独立性

4.视图能够对机密数据提供安全保护

有了视图机制,就可以在设计数据可应用系统时,对不同的用户定义不同的视图,使机密数据不出现在不应看到这些数据的用户视图上

5.适当的利用视图可以更清晰的表达查询

6.多表的复杂的联合查询可以考虑使用视图,能大大提高查询性能。

但是视图不能修改

shiro框架

统一参数校验实现方式

spring boot 使用@Validated

布隆过滤器原理和实现

原理

+布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k

以上图为例,具体的操作流程:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。

对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。

查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。

如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。

反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。

可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。

虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。

布隆过滤器添加元素

将要添加的元素给k个哈希函数

得到对应于位数组上的k个位置

将这k个位置设为1

布隆过滤器查询元素

将要查询的元素给k个哈希函数

得到对应于位数组上的k个位置

如果k个位置有一个为0,则肯定不在集合中

如果k个位置全部为1,则可能在集合中

io、nio、aio区别和实践

1.IO 阻塞IO

每个请求开启一个线程

线程开启,如果当前线程没有数据可读,线程阻塞在read

2.NIO 同步阻塞IO

一个线程并发处理多个写读

空闲线程处理其他通道IO操作

3.AIO NIO2,异步阻塞IO

应用操作之后直接返回,不阻塞,后台处理完,操作系统通知相应线程进行后续操作。AIO应用不广泛。

aop的原理

思想

不去动原来的代码,而是基于原来代码产生代理对象,通过代理的方法,去包装原来的方法,就完成了对以前方法的增强。换句话说,AOP的底层原理就是动态代理的实现。

原理

1.将复杂的需求分解出不同的方面,将公共功能集中解决。

2.采用代理机制组装起来运行,在不改变原程序的基础上对代码段进行增强处理,增加新的功能

总结

所谓面向切面编程,是一种通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态添加功能的技术。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
java面试真的很多,下面我来回答一个有关多线程的问。 在Java中实现多线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。这两种方式有何区别? 继承Thread类的方式是直接定义一个类继承Thread,并重写它的run()方法。然后创建该类的对象,并调用对象的start()方法来启动线程。这种方式简单直接,但因为Java是单继承的,所以如果某个类已经继承了其他类,就不能再直接继承Thread类实现多线程。 实现Runnable接口的方式是定义一个类实现Runnable接口,并实现其唯一的抽象方法run()。然后创建Thread类的对象,将实现了Runnable的对象作为参数传递给Thread类的构造方法。最后调用Thread对象的start()方法来启动线程。这种方式灵活性更大,因为Java允许一个类实现多个接口,所以即使某个类已经继承了其他类,仍然可以通过实现Runnable接口来实现多线程。 另一个区别在于资源共享的问。继承Thread类的方式,不管是数据还是方法,都是线程自己拥有的,不存在共享的情况。而实现Runnable接口的方式,多个线程可以共享同一个对象的数据和方法,因为多个线程共同操作的是同一个Runnable对象。 总结来说,继承Thread类方式简单直接,但只能通过单继承来实现多线程;实现Runnable接口方式更灵活,可以解决单继承的限制,并且多个线程可以共享同一个Runnable对象的数据和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值