2019年JAVA开发工程师面试题系列一

1、spring 是如何创建bean的?

在IoC容器中,bean的获取主要通过BeanFactory和ApplicationContext获取,这里ApplicationContext实际上是继承自BeanFactory的,两者的区别在于BeanFactory对bean的初始化主要是延迟初始化的方式,而ApplicationContext对bean的初始化是在容器启动时即将所有bean初始化完毕。

2、spring bean的作用域?

Spring IOC容器创建一个Bean实例时,可以为Bean指定实例的作用域,作用域包括singleton(单例模式)、prototype(原型模式)、request(HTTP请求)、session(会话)、global-session(全局会话)

单例(singleton):它是默认的选项,在整个应用中,Spring只为其生成一个Bean的实例。

原型(prototype):当每次注入,或者通过Spring IoC容器获取Bean时,Spring都会为它创建一个新的实例。

会话(session):在Web应用中使用,就是在会话过程中Spring只创建一个实例。

请求(request):在Web应用中使用的,就是在一次请求中Spring会创建一个实例,但是不同的请求会创建不同的实例。

全局会话(global-session):全局会话内有效,假如你在编写一个标准的基于Servlet的web应用,并且定义了一个或多个具有global session作用域的bean,系统会使用标准的HTTP Session作用域,并且不会引起任何错误。

3、spring事务的传播级别?

事务特性(4种):

原子性 (atomicity):强调事务的不可分割.

一致性 (consistency):事务的执行的前后数据的完整性保持一致.

隔离性 (isolation):一个事务执行的过程中,不应该受到其他事务的干扰

持久性(durability) :事务一旦结束,数据就持久到数据库

4、脏读、不可重复读、幻读

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

不可重复读 :是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。

虚幻读 :是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。.

5、设置事务隔离级别(5种)

DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.

未提交读(read uncommited) :脏读,不可重复读,虚读都有可能发生

已提交读 (read commited):避免脏读。但是不可重复读和虚读有可能发生

可重复读 (repeatable read) :避免脏读和不可重复读.但是虚读有可能发生.

串行化的 (serializable) :避免以上所有读问题.

Mysql 默认:可重复读

Oracle 默认:读已提交

read uncommited:是最低的事务隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。

read commited:保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。

repeatable read:这种事务隔离级别可以防止脏读,不可重复读。但是可能会出现幻象读。它除了保证一个事务不能被另外一个事务读取未提交的数据之外还避免了以下情况产生(不可重复读)。

serializable:这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读之外,还避免了幻象读(避免三种)。

6、事务的传播行为 注意是大写

* 保证同一个事务中

Propagation_required:PROPAGATION_REQUIRED 支持当前事务,如果不存在 就新建一个(默认)

Propagation_supports:PROPAGATION_SUPPORTS 支持当前事务,如果不存在,就不使用事务

Propagation_mandatory:PROPAGATION_MANDATORY 支持当前事务,如果不存在,抛出异常

* 保证没有在同一个事务中

Propagation_requires_new:PROPAGATION_REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务

Propagation_not_supported:PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务

Propagation_never:PROPAGATION_NEVER 以非事务方式运行,如果有事务存在,抛出异常

Propagation_nested:PROPAGATION_NESTED 如果当前事务存在,则嵌套事务执行

7、Transactional 注解

@Transactional 注解只能应用到 public 方法才有效。

如果不生效,加上@Transactional(rollbackFor = Exception.class)

原因是:当我们使用@Transaction 时默认为RuntimeException(也就是运行时异常)异常才会回滚。

8、spring boot注解?

@SpringBootApplication:申明让spring boot自动给程序进行必要的配置,这个配置等同于:

@Configuration ,@EnableAutoConfiguration 和 @ComponentScan 三个配置。

@ComponentScan:让spring Boot扫描到Configuration类并把它加入到程序上下文。

@Configuration :等同于spring的XML配置文件;使用Java代码可以检查类型安全。

@EnableAutoConfiguration :自动配置。

9、spring Boot是如何实现自动配置的?

1)@SpringBootApplication注解主配置类里边最主要的功能就是SpringBoot开启了一个@EnableAutoConfiguration注解的自动配置功能。

2)@EnableAutoConfiguration(开启自动配置)作用:它主要利用了一个EnableAutoConfigurationImportSelector选择器给Spring容器中来导入一些组件。

3)开启自动配置导入选择器调用、selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包。

4)这个spring.factories文件也是一组一组的key=value的形式,其中一个key是X类的全类名,而它的value是一个X的类名的列表,这些类名以逗号分隔.pring.factories文件,则是用来记录项目包外需要注册的bean类名。

5)这个@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(...)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。

10、索引

数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。采取的是空间换时间的概念。

MyISAM引擎和InnoDB引擎使用B+Tree作为索引结构

11、mysql索引?

1)普通索引,这是最基本的索引,它没有任何限制,比如上文中为title字段创建的索引就是一个普通索引,MyIASM中默认的BTREE类型的索引,也是我们大多数情况下用到的索引。

2)唯一索引,与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值(注意和主键不同)。如果是组合索引,则列值的组合必须唯一,创建方法和普通索引类似。

3)全文索引(FULLTEXT),对于较大的数据集,将你的资料输入一个没有FULLTEXT索引的表中,然后创建索引,其速度比把资料输入现有FULLTEXT索引的速度更为快。不过切记对于大容量的数据表,生成全文索引是一个非常消耗时间非常消耗硬盘空间的做法。

4)单列索引、多列索引,多个单列索引与单个多列索引的查询效果不同,因为执行查询时,MySQL只能使用一个索引,会从多个索引中选择一个限制最为严格的索引。

5)组合索引(最左前缀),平时用的SQL查询语句一般都有比较多的限制条件,所以为了进一步榨取MySQL的效率,就要考虑建立组合索引

12、什么是最左前缀原则?

 比如字段a,b,c建立复合索引。

 where a=1 and c=4 and b=10         可以利用到索引 (a,b,c),即使顺序乱也可以

 where a=1可以利用到索引 (a,b,c)

 where b=5无法利用索引 (a,b,c)

13、mysql的存储引擎

1)MyISAM 不支持事务,不支持外键,优势是访问速度快,对事务完整性没有要求,或者以select、insert为主的可以使用

2)InnoDB 支持事务,外键约束,自增,写的效率差一些,更占据空间,支持行级锁

3)Memory 使用内存中的内容来创建表,访问速度非常快,使用哈希索引。但是一旦服务关闭,表中的数据就会丢失。

4)Merge 是一组MyISAM表的组合,这些表必须结构完全相同,merge本身没有数据。对merge的查询、更新、删除实际是对MyISAM的修改。

14、存储引擎 MyISAM和InnoDB区别:

1)InnoDB支持事务,MyISAM不支持。

2)MyISAM适合查询以及插入为主的应用,InnoDB适合频繁修改以及涉及到安全性较高的应用。

3)InnoDB支持外键,MyISAM不支持。

4)从MySQL5.5.5以后,InnoDB是默认引擎。

5)MyISAM支持全文类型索引,而InnoDB不支持全文索引。

6)InnoDB中不保存表的总行数,select count(*) from table时,InnoDB需要扫描整个表计算有多少行,但MyISAM只需简单读出保存好的总行数即可。注:当count(*)语句包含where条件时MyISAM也需扫描整个表。

7)对于自增长的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中可以和其他字段一起建立联合索引。

8)清空整个表时,InnoDB是一行一行的删除,效率非常慢。MyISAM则会重建表。MyisAM使用delete语句删除后并不会立刻清理磁盘空间,需要定时清理,命令:OPTIMIZE table dept;

9)InnoDB支持行锁(某些情况下还是锁整表,如 update table set a=1 where user like ‘%lee%’)

10)Myisam创建表生成三个文件:.frm 数据表结构 、 .myd 数据文件 、 .myi 索引文件,Innodb只生成一个 .frm文件,数据存放在ibdata1.log

现在一般都选用InnoDB,主要是MyISAM的全表锁,读写串行问题,并发效率锁表,效率低,MyISAM对于读写密集型应用一般是不会去选用的。

应用场景:

MyISAM不支持事务处理等高级功能,但它提供高速存储和检索,以及全文搜索能力。如果应用中需要执行大量的SELECT查询,那么MyISAM是更好的选择。

InnoDB用于需要事务处理的应用程序,包括ACID事务支持。如果应用中需要执行大量的INSERT或UPDATE操作,则应该使用InnoDB,这样可以提高多用户并发操作的性能。

15、数据库查询优化

1)避免全部扫描,比如对null值进行筛选判读;使用!=或<>、like、or等等都将放弃索引全表扫描

2)考虑在where及order by涉及的列上建立索引

3)使用正向逻辑(not in,not exists)

4)数据库不擅长运算,把运算交给逻辑代码,非要有把运算放在右边

5)合理建表,使用合理的字段,善用非空、外键约束保证数据的完整性

6)索引并不是越多越好,一个表最好不要超过6个,多了影响增、删、改的性能。这个影响很大

7)多从业务逻辑方面考虑问题,合理使用中间件

8)对于数据量太大的数据分库分表,使用中间件比如mycat

252、分表分库

①:垂直分割(并不常用)

就是将一个表按照字段来分,每张表保证有相同的主键就好。一般来说,将常用字段和大字段分表来放。

优势:比没有分表来说,提高了查询速度,降低了查询结果所用内存;

劣势:没有解决大量记录的问题,对于单表来说随着记录增多,性能还是下降很快;

②: 水平分割(重要,实际应用中使用最多)

水平分割是企业最常用到的,水平拆分就是大表按照记录分为很多子表:

水平分的规则完全是自定义的,有以下几种参考设计:

1 hash、自增id取模:

对某个字段进行hash来确定创建几张表,并根据hash结果存入不同的表;

2 按时间

根据业务可以按照天、月、年来进行拆分;

3 按每个表的固定记录数

一般按照自增ID进行拆表,一张表的数据行到了指定的数量,就自动保存到下一张表中。比如规定一张表只能存1-1000个记录;

4 将老数据迁移到一张历史表

比如日志表,一般只查询3个月之内的数据,对于超过3个月的记录将之迁移到历史子表中;

16、    Sql执行顺序:

(1)FROM [left_table]

(2)ON <join_condition>

(3)<join_type> JOIN <right_table>

(4)WHERE <where_condition>

(5)GROUP BY <group_by_list>

(6)WITH <CUBE | RollUP>

(7)HAVING <having_condition>

(8)SELECT

(9)DISTINCT 

(10)ORDER BY <order_by_list>

(11)<Top Num> <select list>

GROUP BY表示分组,按某一个字段进行分组

HAVING是对于GROUP BY对象进行筛选

17、MySQL中exists和in的区别及使用场景

外层查询表小于子查询表,则用exists,外层查询表大于子查询表,则用in,如果外层和子查询表差不多,则爱用哪个用哪个

18、Redis支持的数据类型?

1)St ing字符串:格式: set key value

string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。string类型是Redis最基本的数据类型,一个键最大能存储512MB。

2)Hash(哈希)格式: hmset name  key1 value1 key2 value2

Redis hash 是一个键值(key=>value)对集合。Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

3)    lsit(列表)Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

4)set(集合)

5)zset(有序集合)

Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

19、什么是Redis持久化?Redis有哪几种持久化方式?优缺点是什么?

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 提供了两种持久化方式:RDB(默认) 和AOF

RDB:rdb是Redis DataBase缩写

功能核心函数rdbSave(生成RDB文件)和rdbLoad(从文件加载内存)两个函数

AOF:Aof是Append-only file缩写

每当执行服务器(定时)任务或者函数时flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作

aof写入保存:

WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件

SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。

比较:

1、aof文件比rdb更新频率高,优先使用aof还原数据。

2、aof比rdb更安全也更大

3、rdb性能比aof好

4、如果两个都配了优先加载AOF

20、redis缓存失效的解决方案?

1)首先redis是默认永不过期的,如果要手动设置时间,需要增量设置过期时间,避免redis中的缓存在同一时间失效。如果是系统级别的原因,比如宕机,采用主从复制。

2)缓存穿透的问题,通过设置分布式锁,取得锁的进程操作数据库并更新缓存,没取得锁的进程发现有锁就等待。

21、如何实现redis分布式锁?

1)加锁操作:jedis.set(key,value,"NX","EX",timeOut)。

key就是redis的key值作为锁的标识,value在这里作为客户端的标识,只有key-value都比配才有删除锁的权利【保证安全性】

NX:只有这个key不存才的时候才会进行操作,if not exists;

EX:设置key的过期时间为秒,具体时间由第5个参数决定

通过timeOut设置过期时间保证不会出现死锁【避免死锁】

2)解锁操作:unLock(String key,String value)

执行一个lua脚本,如果根据key拿到的value跟传入的value相同就执行del,否则就返回

3)重试机制:lockRetry(String key,String value,Long timeOut,Integer retry,Long sleepTime)

主要是用于其他进程,如果没有发现有锁就进入睡眠状态,设置睡眠时间,以及重试次数(循环次数)

22、如何组织表单提交!

jS做一个状态码false,当提交成功后状态码为true,提交前先验证这个状态码是否为false,否则就返回

23、微信小程序的四个组件

WXML,WXSS,javascript,json

24、消息中间件

消息中间件是程序相互通信的一种方式,消息队列是消息中间件的一种实现方式。

25、中间件消息丢失的解决方法?

1)消息没有收到,使用事务(有这个注解),接收到消息就返回一个状态,否则重复发送。性能会降低,类似于微信支付宝支付的异步通知。

2)MQ保存消息丢失,这种情况基本不存在。AMQ是一种文件存储形式,它具有写入速度快和容易恢复的特点。消息存储在一个个文件中,文件的默认大小为32M,如果一条消息的大小超过了32M,那么这个值必须设置大一点。当一个存储文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段,这个文件被删除。

如果要消息记录可以考虑持久化到数据库中

26、dubbo采用的是什么通信协议?

dubbo支持不同的通信协议

1)dubbo协议

dubbo://192.168.0.1:20188

默认就是走dubbo协议的,单一长连接,NIO异步通信,基于hessian作为序列化协议(默认)

适用的场景就是:传输数据量很小(每次请求在100kb以内),但是并发量很高

为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就100个连接。然后后面直接基于长连接NIO异步通信,可以支撑高并发请求。

否则如果上亿次请求每次都是短连接的话,服务提供者会扛不住。

而且因为走的是单一长连接,所以传输数据量太大的话,会导致并发能力降低。所以一般建议是传输数据量很小,支撑高并发访问。

2)rmi协议

走java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用

3)hessian协议

走hessian序列化协议,多个短连接,适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用

4)http协议

走json序列化

5)webservice

走SOAP文本序列化

27、dubbo的运行过程

1)服务提供者在启动时,向服务注册中心注册自己提供的服务

2)服务消费者在启动时,向注册中心订阅自己所需要的服务

3)注册中心返回服务提供者地址列表给服务消费者。如果有变更,服务注册中心将使用长连接推送变更数据给消费者

4)服务消费者,从服务提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用。如果调用失败,再选另一台调用

5) 服务消费者和服务提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次数据到监控中心

28、zookeeper的选举机制?

zookeeper提供了三种方式:

LeaderElection,AuthFastLeaderElection(授权快速领导人选举),FastLeaderElection

默认的算法是FastLeaderElection(快速选择领导),所以主要分析它的选举机制。

当启动初始化集群的时候,server1的myid为1,zxid为0   server2的myid为2,zxid同样是0,以此类推。此种情况下zxid都是为0。先比较zxid,再比较myid服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking(选举状态)。

服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的myid大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。

服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的myid最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。

服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的myid大,但之前服务器3已经胜出,所以服务器4只能成为小弟。

服务器5启动,后面的逻辑同服务器4成为小弟

当选举机器过半的时候,已经选举出leader后,后面的就跟随已经选出的leader,所以4和5跟随成为leader的server3

所以,在初始化的时候,一般到过半的机器数的时候谁的myid最大一般就是leader

运行期间

按照上述初始化的情况,server3成为了leader,在运行期间处于leader的server3挂了,那么非Observer服务器server1、server2、server4、server5会将自己的节点状态变为LOOKING状态

1、开始进行leader选举。现在选举同样是根据myid和zxid来进行

2、首先每个server都会给自己投一票竞选leader。假设server1的zxid为123,server2的zxid为124,server4的zxid为169,server5的zxid为188

3、同样先是比较zxid再比较,server1、server2、server4比较server4根据优先条件选举为leader。然后server5还是跟随server4,即使server5的zxid最大,但是当选举到server4的时候,机器数已经过半。不再进行选举,跟随已经选举的leader

zookeeper集群为保证数据的一致性所有的操作都是由leader完成,之后再由leader同步给follower。重点就在这儿,zookeeper并不会确保所有节点都同步完数据,只要有大多数节点(即n/2+1)同步成功即可。

咱们假设有一个写操作成功那么现在数据只存在于节点leader,之后leader再同步给其他follower。这时候宕掉3个机器,已经过半的机器无法进行投票选举,剩余2台不足过半,无法选举=无法提供任何服务。再启动一个机器恢复服务。所以宕掉的机器不要过半,过半就会导致无法正常服务

29、springMVC核心类

1)制器核心类:

org.springframework.web.servlet.DispatcherServlet  - 配置web.xml

2)加载配置文件核心类:

org.springframework.web.context.ContextLoaderListener – spring的配置文件

3)处理url影射核心类:

org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping-根据bean的名称请求一个bean. spring的配置文件- /abc

4)处理视图资源核心类:

org.springframework.web.servlet.view.ResourceBundleViewResolver

5)方法动态调用核心类

org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver

30、什么是双向链表?

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

31、Lock与synchronized有以下区别:

1)Lock是一个接口,而synchronized是关键字。

2)synchronized会自动释放锁,而Lock必须手动释放锁。

3)Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。

4)通过Lock可以知道线程有没有拿到锁,而synchronized不能。

5)Lock能提高多个线程读操作的效率。

6)synchronized能锁住类、方法和代码块,而Lock是块范围内的

32、HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

32、shiro登录的执行流程

创建 AuthenticationToken,然后调用 Subject.login 方法进行登录认证;

Subject 委托给 SecurityManager;

SecurityManager 委托给 Authenticator 接口;

Authenticator 接口调用 Realm 获取登录信息。

33、Shiro 的三大核心组件:

1、Subject :当前用户的操作

2、SecurityManager:用于管理所有的Subject

3、Realms:用于进行权限信息的验证,认证授权都在这里

33、身份认证Authentication 和 授权Authorization

在shiro的用户权限认证过程中其通过两个方法来实现:

1、Authentication:是验证用户身份的过程。重写doGetAuthenticationInfo()方法

2、Authorization:是授权访问控制,用于对用户进行的操作进行人证授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等,重写doGetAuthorizationInfo()

3、实现权限is or需要重写AuthorizationFilter中的isAccessAllowed(),循环遍历角色的权限数组,只要包含其一就返回true

其他组件:

除了以上几个组件外,Shiro还有几个其他组件:

1、SessionManager :Shiro为任何应用提供了一个会话编程范式。

2、CacheManager :对Shiro的其他组件提供缓存支持。

4、remenberMe:记住我

34、为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

35、 一对一、一对多的关联查询 ? 

association 一对一, 一对多  collection,多对多 discrimination

36、MyBatis实现一对一有几种方式?具体怎么操作的?

有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成;

嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置。

37、MyBatis实现一对多有几种方式,怎么操作的?

有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成;嵌套查询是先查一个表,根据这个表里面的 结果的外键id,去再另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置。

38、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。

它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。

当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。

 39、Mybatis的一级、二级缓存:

1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。

2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置<cache/> ;

3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。

40 、Hibernate的缓存机制?

一级缓存

  session级别的缓存,当我们使用了get load find Query等查询出来的数据,默认在session中就会有一份缓存数据,缓存数据就是从数据库将一些数据拷贝一份放到对应的地方.

  一级缓存不可卸载: (只要使用了session,肯定用到了session的缓存机制,是hibernate控制的,我们不能手动配置)

一级缓存的清理:

   close clear这两种方式会全部清理; evict方法是将指定的缓存清理掉

二级缓存

  sessionFactory级别的缓存,可以做到多个session共享此信息

sessionFactory缓存分类:

1. 内缓存: 预制的sql语句,对象和数据库的映射信息

2. 外缓存:存储的是我们允许使用二级缓存的对象

适合放在二级缓存中的数据:

1. 经常被修改的数据

2. 不是很想重要的数据,允许出现偶尔并发的数据

3. 不会被并发访问的数据

4. 参考数据

适合放到一级缓存中的数据:

1. 经常被修改的数据

2. 财务数据,绝对不允许出现并发

3. 与其它应用共享的数据

Hibernate的二级缓存策略的一般过程:

1. 条件查询的时候,

    String hql = “from 类名”;

这样的SQL语句查询数据库,一次获得所有的数据库.

2.把获得的所有数据对象根据ID放入到第二级缓存中

3.当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;查不到,如果配置了二级缓存,那么从二级缓存中查;查不到,再查询数据库,把结果按照ID放入到缓存

4.删除 更新 增加数据的时候,同时更新缓存

注: Hibernate的二级缓存策略,是针对于ID查询的缓存策略,对于条件查询则毫无作用.为此,Hibernate提供了针对条件查询的Query缓存

41、springboot启动流程

1.创建SpringApplication实例

1)  在SpringApplicaiton构造器中调用initialize(sources)方法。initialize方法中,将sources转换成list加到this.sources属性中。

2) 判断是否为web环境,在类路径下是否可以加载到Servlet和ConfigurableWebApplicationContext

3) 设置初始化器,从META-INF/spring.factories处读取配置文件中Key为:org.springframework.context.ApplicationContextInitializer的value,进行实例化操作 

4) 设置监听器,StopWatch主要是监控启动过程,统计启动时间,检测应用是否已经启动或者停止。

5) 推断应用入口类,通过寻找main方法找到启动主类。

2.执行SpringApplication.run()

1) 获取SpringApplicationRunListeners,(也是通过META-INF/spring.factories),默认加载的是EventPublishingRunListener。启动监听,调用RunListener.starting()方法。

2) 根据SpringApplicationRunListeners以及参数来准备环境,获取环境变量environment,将应用参数放入到环境变量持有对象中,监听器监听环境变量对象的变化(listener.environmentPrepared),打印Banner信息(SpringBootBanner)

3) 创建ApplicationContext(spring上下文AnnotationConfigEmbeddedWebApplicationContext)

4) 创建FailureAnalyzer, 用于触发从spring.factories加载的FailureAnalyzer和FailureAnalysisReporter实例

5) spring上下文前置处理prepareContext

6) spring上下文刷新refreshContext

7) spring上下文后置处理afterRefresh(ApplicationRunner,CommandLineRunner接口实现类的启动),返回上下文对象

42、java类加载过程

类加载的过程主要分为三个部分:加载;链接;初始化

而链接又可以细分为三个小部分:验证;准备;解析

1)加载

简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。

这里有两个重点:

字节码来源:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译

类加载器:一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。

2)链接

验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?

对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。

对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?

准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。

特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456

解析:将常量池内的符号引用替换为直接引用的过程。

两个重点:

符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。

直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

3)初始化

这个阶段主要是对类变量初始化,是执行类构造器的过程。

换句话说,只对static修饰的变量或语句进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

42、为什么会有自定义类加载器?

一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。

另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

42、什么是类加载器?

​Java类加载器是Java运行时环境的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。学习类加载器时,掌握Java的委派概念很重要。 

43、类加载器是干什么的?

类加载器它是在虚拟机中完成的,负责动态加载Java类到Java虚拟机的内存空间中,在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。

44、如何防止订单重复提交

         首先,前端js拦截,提交订单前先判断提交状态。只有未提交成功可以提交,提交成功后则禁止提交。后台,一个订单有唯一的编号,而且有新建、提交支付中,支付失败,未支付成功等状态。根据订单获取支付状态即可,失败可以继续提交支付,成功的返回结果即可。

45、如何实现分布式事务

1)2PC即两阶段提交协议,

是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit

phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。需要数据库支持X/A协议

1. 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)

2. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。

1.1 XA方案(2PC协议)

2PC的传统方案是在数据库层面实现的,如Oracle、MySQL都支持2PC协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织Open Group定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。主要实现思想是一个应用程序拥有2个数据源,把两个数据库的操作合并到一个事务。

1.2 Seata方案

Seata是由阿里中间件团队发起的开源项目 Fescar,后更名为Seata,它是一个是开源的分布式事务框架。传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案。

Seata的设计思想如下:

Seata的设计目标其一是对业务无侵入,Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。,通常分支事务本身就是一个关系数据库的本地事务。

Seata定义了3个组件来协议分布式事务的处理过程:

Transaction Coordinator (TC): 事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。

Transaction Manager (TM): 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令。

Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分支(本地)事务的提交和回滚。

seata客户端(RM、TM):spring-cloud-alibaba-seata-2.1.0.RELEASE

seata服务端(TC):seata-server-0.7.1

具体的执行流程如下:

1. 用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。

2. 用户服务的 RM 向 TC 注册 分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局

事务的管辖。

3. 用户服务执行分支事务,向用户表插入一条记录。

4. 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的RM 向 TC 注册分支事

务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。

5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。

6. 用户服务分支事务执行完毕。

7. TM 向 TC 发起针对 XID 的全局提交或回滚决议。

8. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

Seata实现2PC与传统2PC的差别:

架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而Seata的 RM 是以jar包的形式作为中间件层部署在应用程序这一侧的。

两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。

****小结****

传统2PC(基于数据库XA协议)和Seata实现2PC的两种2PC方案,由于Seata的0侵入性并且解决了传统2PC长期锁资源的问题,所以推荐采用Seata实现2PC。

Seata实现2PC要点:

1、全局事务开始使用 @GlobalTransactional标识 。

2、每个本地事务方案仍然使用@Transactional标识。

3、每个数据都需要创建undo_log表,此表是seata保证本地事务一致性的关键

 

2)分布式事务解决方案之TCC

TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel

操作若执行失败,TM会进行重试。

TCC分为三个阶段:

1. Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能真正构成一个完整的业务逻辑。

2. Confirm 阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。

3. Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。

4. TM事务管理器,TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公用组件,是为了考虑系统结构和软件复用。

TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录状态,由于Confirm 和cancel失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求多少次,其结果都相同。

2.1 Hmily

是一个高性能分布式事务TCC开源框架。基于Java语言来开发(JDK1.8),支持Dubbo,Spring Cloud等RPC框架进行分布式事务。但Seata的TCC模式对Spring Cloud并没有提供支持。它目前支持以下特性:

支持嵌套事务(Nested transaction support).

采用disruptor框架进行事务日志的异步读写,与RPC框架的性能毫无差别。

支持SpringBoot-starter 项目启动,使用简单。

RPC框架支持 : dubbo,motan,springcloud。

本地事务存储支持 : redis,mongodb,zookeeper,file,mysql。

事务日志序列化支持 :java,hessian,kryo,protostuff。

采用Aspect AOP 切面思想与Spring无缝集成,天然支持集群。

RPC事务恢复,超时异常恢复等。

Hmily利用AOP对参与分布式事务的本地方法与远程方法进行拦截处理,通过多方拦截,事务参与者能透明的调用到另一方的Try、Confirm、Cancel方法;传递事务上下文;并记录事务日志,酌情进行补偿,重试等。

Hmily不需要事务协调服务,但需要提供一个数据库(mysql/mongodb/zookeeper/redis/file)来进行日志存储。

Hmily实现的TCC服务与普通的服务一样,只需要暴露一个接口,也就是它的Try业务。Confirm/Cancel业务逻辑,只是因为全局事务提交/回滚的需要才提供的,因此Confirm/Cancel业务只需要被Hmily TCC事务框架发现即可,不需要被调用它的其他业务服务所感知。

TCC需要注意三种异常处理分别是空回滚、幂等、悬挂:

空回滚:在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。

出现原因是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。

解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。

幂等:通过前面介绍已经了解到,为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。

解决思路在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态。

悬挂:悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。

出现原因是在 RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。

解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,“分支事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try。

拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。

46、什么是强引用、软引用、弱引用、虚引用有什么区别?

1) 强引用,特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。

2))软引用,特点:软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

)弱引用,弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

应用场景:弱应用同样可用于内存敏感的缓存。

4) 虚引用:特点:虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

ReferenceQueue queue = new ReferenceQueue ();

PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。

应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

47、说一下"=="和equals方法究竟有什么区别?

== 可以作用于基本数据类型和引用数据类型

equals 只可以作用于引用数据类型

== 作用于基本数据类型比较的是基本数据类型的“值” 作用于引用数据类型比较的是地址

equals 目标对象没有重写equals()的方法的时候 比较的是对像的地址 重写了equals()比较 的是对象的内容

48、 Redis数据淘汰机制

在 redis 中,允许用户设置最大使用内存大小 server.maxmemory,在内存限定的情况下是很有用的。譬如,在一台 8G 机子上部署了 4 个 redis 服务点,每一个服务点分配 1.5G 的内存大小,减少内存紧张的情况,由此获取更为稳健的服务。

内存大小有限,需要保存有效的数据?

redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

Redis提供了以下几种数据淘汰策略:

1、 volatile-lru:从设置过期的数据集中淘汰最少使用的数据;

2、volatile-ttl:从设置过期的数据集中淘汰即将过期的数据(离过期时间最近);

3、volatile-random:从设置过期的数据集中随机选取数据淘汰;

4、allkeys-lru:从所有 数据集中选取使用最少的数据;

5、allkeys-random:从所有数据集中任意选取数据淘汰;

6、no-envicition:不进行淘汰;

49、数据库插入几百万数据怎么实现?

变多次提交为一次,获取一次连接,执行多次插入。在代码中使用循环插入数据,最后关闭连接。

像这样的批量插入操作能不使用代码操作就不使用,可以使用存储过程来实现。

50、 说一下Spring中的两大核心?

Spring是什么?

spring是J2EE应用程序框架,是轻量级的IoC和AOP的容器框架(相对于重量级的EJB),主要是针对javaBean的生命周期进行管理的轻量级容器,可以单独使用,也可以和Struts框架,ibatis框架等组合使用。

1、IOC(Inversion of Control )或DI(Dependency Injection)

       IOC控制权反转

          原来:我的Service需要调用DAO,Service就需要创建DAO

          Spring:Spring发现你Service依赖于dao,就给你注入.

核心原理:就是配置文件+反射(工厂也可以)+容器(map)

2、AOP:面向切面编程

核心原理:使用动态代理的设计模式在执行方法前后或出现异常做加入相关逻辑。

      我们主要使用AOP来做:

      1、事务处理

      2、权限判断

      3、日志

51、说说常见的集合有哪些吧?

答:Map接口和Collection接口是所有集合框架的父接口:

Collection接口的子接口包括:Set接口和List接口

Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等

Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

52、HashMap与HashTable的区别?

HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;

HashMap允许K/V都为null;后者K/V都不允许为null;

HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;

53、HashMap的put方法的具体流程?

下面先来分析一下源码

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

               boolean evict) {

    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;

    // 1.如果table为空或者长度为0,即没有元素,那么使用resize()方法扩容

    if ((tab = table) == null || (n = tab.length) == 0)

        n = (tab = resize()).length;

    // 2.计算插入存储的数组索引i,此处计算方法同 1.7 中的indexFor()方法

    // 如果数组为空,即不存在Hash冲突,则直接插入数组

    if ((p = tab[i = (n - 1) & hash]) == null)

        tab[i] = newNode(hash, key, value, null);

    // 3.插入时,如果发生Hash冲突,则依次往下判断

    else {

        HashMap.Node<K,V> e; K k;

        // a.判断table[i]的元素的key是否与需要插入的key一样,若相同则直接用新的value覆盖掉旧的value

        // 判断原则equals() - 所以需要当key的对象重写该方法

        if (p.hash == hash &&

                ((k = p.key) == key || (key != null && key.equals(k))))

            e = p;

        // b.继续判断:需要插入的数据结构是红黑树还是链表

        // 如果是红黑树,则直接在树中插入 or 更新键值对

        else if (p instanceof HashMap.TreeNode)

            e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        // 如果是链表,则在链表中插入 or 更新键值对

        else {

            // i .遍历table[i],判断key是否已存在:采用equals对比当前遍历结点的key与需要插入数据的key

            //    如果存在相同的,则直接覆盖

            // ii.遍历完毕后任务发现上述情况,则直接在链表尾部插入数据

            //    插入完成后判断链表长度是否 > 8:若是,则把链表转换成红黑树

            for (int binCount = 0; ; ++binCount) {

                if ((e = p.next) == null) {

                    p.next = newNode(hash, key, value, null);

                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

                        treeifyBin(tab, hash);

                    break;

                }

                if (e.hash == hash &&

                        ((k = e.key) == key || (key != null && key.equals(k))))

                    break;

                p = e;

            }

        }

        // 对于情况的后续操作:发现key已存在,直接用新value覆盖旧value&返回旧value

        if (e != null) { // existing mapping for key

            V oldValue = e.value;

            if (!onlyIfAbsent || oldValue == null)

                e.value = value;

            afterNodeAccess(e);

            return oldValue;

        }

    }

    ++modCount;

    // 插入成功后,判断实际存在的键值对数量size > 最大容量

    // 如果大于则进行扩容

    if (++size > threshold)

        resize();

    // 插入成功时会调用的方法(默认实现为空)

    afterNodeInsertion(evict);

    return null;

}

简单总结为:

1、put(key, value)中直接调用了内部的putVal方法,并且先对key进行了hash操作;

2、putVal方法中,先检查HashMap数据结构中的索引数组表是否位空,如果是的话则进行一次resize操作;

3、以HashMap索引数组表的长度减一与key的hash值进行与运算,得出在数组中的索引,如果索引指定的位置值为空,则新建一个k-v的新节点;

4、如果不满足的3的条件,则说明索引指定的数组位置的已经存在内容,这个时候称之碰撞出现;

5、在上面判断流程走完之后,计算HashMap全局的modCount值,以便对外部并发的迭代操作提供修改的Fail-fast判断提供依据,于此同时增加map中的记录数,并判断记录数是否触及容量扩充的阈值,触及则进行一轮resize操作;

6、在步骤4中出现碰撞情况时,从步骤7开始展开新一轮逻辑判断和处理;

7、判断key索引到的节点(暂且称作被碰撞节点)的hash、key是否和当前待插入节点(新节点)的一致,如果是一致的话,则先保存记录下该节点;如果新旧节点的内容不一致时,则再看被碰撞节点是否是树(TreeNode)类型,如果是树类型的话,则按照树的操作去追加新节点内容;如果被碰撞节点不是树类型,则说明当前发生的碰撞在链表中(此时链表尚未转为红黑树),此时进入一轮循环处理逻辑中;

8、循环中,先判断被碰撞节点的后继节点是否为空,为空则将新节点作为后继节点,作为后继节点之后并判断当前链表长度是否超过最大允许链表长度8,如果大于的话,需要进行一轮是否转树的操作;如果在一开始后继节点不为空,则先判断后继节点是否与新节点相同,相同的话就记录并跳出循环;如果两个条件判断都满足则继续循环,直至进入某一个条件判断然后跳出循环;

9、步骤8中转树的操作treeifyBin,如果map的索引表为空或者当前索引表长度还小于64(最大转红黑树的索引数组表长度),那么进行resize操作就行了;否则,如果被碰撞节点不为空,那么就顺着被碰撞节点这条树往后新增该新节点;

10、最后,回到那个被记住的被碰撞节点,如果它不为空,默认情况下,新节点的值将会替换被碰撞节点的值,同时返回被碰撞节点的值(V)。

54、HashMap的扩容操作是怎么实现的?

HashMap通过resize()方法进行扩容或者初始化的操作,下面是对源码进行的一些简单分析:

/**

 * 该函数有2中使用情况:1.初始化哈希表;2.当前数组容量过小,需要扩容

 */

final Node<K,V>[] resize() {

    Node<K,V>[] oldTab = table;// 扩容前的数组(当前数组)

    int oldCap = (oldTab == null) ? 0 : oldTab.length;// 扩容前的数组容量(数组长度)

    int oldThr = threshold;// 扩容前数组的阈值

    int newCap, newThr = 0;

 

    if (oldCap > 0) {

        // 针对情况2:若扩容前的数组容量超过最大值,则不再扩容

        if (oldCap >= MAXIMUM_CAPACITY) {

            threshold = Integer.MAX_VALUE;

            return oldTab;

        }

        // 针对情况2:若没有超过最大值,就扩容为原来的2倍(左移1位)

        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                oldCap >= DEFAULT_INITIAL_CAPACITY)

            newThr = oldThr << 1; // double threshold

    }

 

    // 针对情况1:初始化哈希表(采用指定或者使用默认值的方式)

    else if (oldThr > 0) // initial capacity was placed in threshold

        newCap = oldThr;

    else {               // zero initial threshold signifies using defaults

        newCap = DEFAULT_INITIAL_CAPACITY;

        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

    }

 

    // 计算新的resize上限

    if (newThr == 0) {

        float ft = (float)newCap * loadFactor;

        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                (int)ft : Integer.MAX_VALUE);

    }

    threshold = newThr;

    @SuppressWarnings({"rawtypes","unchecked"})

    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

    table = newTab;

    if (oldTab != null) {

        // 把每一个bucket都移动到新的bucket中去

        for (int j = 0; j < oldCap; ++j) {

            Node<K,V> e;

            if ((e = oldTab[j]) != null) {

                oldTab[j] = null;

                if (e.next == null)

                    newTab[e.hash & (newCap - 1)] = e;

                else if (e instanceof TreeNode)

                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                else { // preserve order

                    Node<K,V> loHead = null, loTail = null;

                    Node<K,V> hiHead = null, hiTail = null;

                    Node<K,V> next;

                    do {

                        next = e.next;

                        if ((e.hash & oldCap) == 0) {

                            if (loTail == null)

                                loHead = e;

                            else

                                loTail.next = e;

                            loTail = e;

                        }

                        else {

                            if (hiTail == null)

                                hiHead = e;

                            else

                                hiTail.next = e;

                            hiTail = e;

                        }

                    } while ((e = next) != null);

                    if (loTail != null) {

                        loTail.next = null;

                        newTab[j] = loHead;

                    }

                    if (hiTail != null) {

                        hiTail.next = null;

                        newTab[j + oldCap] = hiHead;

                    }

                }

            }

        }

    }

    return newTab;

}

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置。

0.75这个值成为负载因子,那么为什么负载因子为0.75呢?这是通过大量实验统计得出来的,如果过小,比如0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如1,那么当元素满的时候才进行扩容,会使get,put操作的碰撞几率增加。

可以看到HashMap不是无限扩容的,当达到了实现预定的MAXIMUM_CAPACITY,就不再进行扩容。

55、HashMap是怎么解决哈希冲突的?

我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;

什么是哈希?

Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。

什么是哈希冲突?

当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

HashMap的数据结构

在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:

这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

hash()函数

上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动

在JDK 1.8中的hash()函数如下:

static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)

}

这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);

JDK1.8新增红黑树

通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);

总结

简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;

2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;

3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

56、HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

那怎么解决呢?

HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;

在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

57、为什么数组长度要保证为2的幂次方呢?

只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;

如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。

那为什么是两次扰动呢?

答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

58、HashMap在JDK1.7和JDK1.8中有哪些不同?

不同

JDK 1.7

JDK 1.8

存储结构

数组 + 链表

数组 + 链表 + 红黑树

初始化方式

单独函数:inflateTable()

直接集成到了扩容函数resize()

hash值计算方式

扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算

扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算

存放数据的规则

无冲突时,存放数组;冲突时,存放链表

无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树

插入数据方式

头插法(先讲原位置的数据移到后1位,再插入数据到该位置)

尾插法(直接插入到链表尾部/红黑树)

扩容后存储位置的计算方式

全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))

按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

59、为什么HashMap中String、Integer这样的包装类适合作为K?

StringInteger等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况。内部已重写了equals()hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

面试官:如果我想要让自己的Object作为K应该怎么办呢?

重写hashCode()equals()方法

重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;

重写`equals()`方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值xx.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

60、ConcurrentHashMap和Hashtable的区别?

ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。

面试官:ConcurrentHashMap的具体实现知道吗?

答:在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:

1、该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;

2、Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:

1、如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))

        break;                   // no lock when adding to empty bin

}

2、如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;

if (fh >= 0) {

    binCount = 1;

    for (Node<K,V> e = f;; ++binCount) {

        K ek;

        if (e.hash == hash &&

            ((ek = e.key) == key ||

             (ek != null && key.equals(ek)))) {

            oldVal = e.val;

            if (!onlyIfAbsent)

                e.val = value;

            break;

        }

        Node<K,V> pred = e;

        if ((e = e.next) == null) {

            pred.next = new Node<K,V>(hash, key, value, null);

            break;

        }

    }

}

3、如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;

4、如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;

61、Java集合的快速失败机制 “fail-fast”?

是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

解决办法:

1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

2. 使用CopyOnWriteArrayList来替换ArrayList

62、ArrayList 和 Vector 的区别?

这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中的元素位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引来取出某个元素,并且其中的数据是允许重复的,这是与 HashSet 之类的集合的最大不同处,HashSet 之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。

ArrayList 与 Vector 的区别主要包括两个方面:

同步性:

Vector 是线程安全的,也就是说它的方法之间是线程同步(加了synchronized 关键字)的,而 ArrayList 是线程不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线程安全的问题,所以效率会高一些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。

数据增长:

ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个人超过了容量时,就需要增加 ArrayList 和 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要去的一定的平衡。Vector 在数据满时(加载因子1)增长为原来的两倍(扩容增量:原容量的 2 倍),而 ArrayList 在数据量达到容量的一半时(加载因子 0.5)增长为原容量的 (0.5 倍 + 1) 个空间。

63、ArrayList和LinkedList的区别?

LinkedList 实现了 List 和 Deque 接口,一般称为双向链表;ArrayList 实现了 List 接口,动态数组;

LinkedList 在插入和删除数据时效率更高,ArrayList 在查找某个 index 的数据时效率更高;

LinkedList 比 ArrayList 需要更多的内存;

面试官:Array 和 ArrayList 有什么区别?什么时候该应 Array 而不是 ArrayList 呢?

它们的区别是:

Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。

Array 大小是固定的,ArrayList 的大小是动态变化的。

ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。

对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

64、HashSet是如何保证数据不可重复的?

HashSet的底层其实就是HashMap,只不过我们HashSet是实现了Set接口并且把数据作为K值,而V值一直使用一个相同的虚值来保存,我们可以看到源码:

public boolean add(E e) {

    return map.put(e, PRESENT)==null;// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值

}

由于HashMap的K值本身就不允许重复,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V,那么在HashSet中执行这一句话始终会返回一个false,导致插入失败,这样就保证了数据的不可重复性;

65、BlockingQueue是什么?

Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

65、Map 的put和get

Map是以键值对来存储对象的,它的底层实际上是数组和链表

当使用put方法时,先查找出数组位置是否存在对象,通过key.hashcode对数组长度取余;存在,则把里面的链表拿出来,判断链表里面是否存在key值与传递过来的key值一样的对象,存在,则把传递过来的value取代链表key对应的value,不存在,则直接通过链表的add()方法加到链表后面;

当使用get方法时,先查找出数组位置是否存在对象,通过key.hashcode对数组长度取余;如果不存在,则返回为空,如果存在,则遍历链表,判断链表里面是否存在key值与传递过来的key值一样的对象,存在,则把key值对应的value取出返回,不存在,则返回为空;

66、项目 MySQL 的数据量和并发量有多大?

评注:此题为走向题,你的回答不同,后面问题走向就变了。

关于容量:单表行数超过 500 万行或者单表容量超过2GB,此时就要答分库分表的中间件了!那后面题目的走向就变为mycat、sharing-jdbc等分库分表中间件的底层原理了!

关于并发量:如果并发数过1200,此时就要答利用MQ或者redis等中间件,作为补偿措施,而不能直接操作数据库。那后面的题目走向就是redis、mq的原理了!

介于面试者还是一个应届生,我斗胆猜测面试者是这么答的

回答:数据量估计就三四百万吧,并发量就五六百左右!

67、你说下数据库的索引实现和非主键的二级索引

从数据结构角度:

B-Tree索引,数据结构就是一颗B+树。

Hash索引,Hash索引比较的是进行Hash运算之后的Hash值,所以它只能用于等值的过滤,不能用于基于范围的过滤。基本不用!

R-Tree索引,仅支持geometry数据类型,也基本不用!

至于非主键的二级索引,这个实际上问的就是非聚簇索引!非聚簇索引本身就是一颗B+树,其根节点指向聚簇索引的B+树,具体的请看这篇文章《MySQL(Innodb)索引的原理》

68、项目用的是 SpringBoot ,你能说下 Spring Boot 与 Spring 的区别吗?

•       Spring Boot可以建立独立的Spring应用程序;

•       内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作了。

•       无需再像Spring那样搞一堆繁琐的xml文件的配置;

•       可以自动配置Spring;

•       提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功能;

•       提供的POM可以简化Maven的配置

69、SpringBoot 的自动配置是怎么做的?

先答为什么需要自动配置?

顾名思义,自动配置的意义是利用这种模式代替了配置 XML 繁琐模式。以前使用 Spring MVC ,需要进行配置组件扫描、调度器、视图解析器等,使用 Spring Boot 自动配置后,只需要添加 MVC 组件即可自动配置所需要的 Bean。所有自动配置的实现都在 spring-boot-autoconfigure 依赖中,包括 Spring MVC 、Data 和其它框架的自动配置。

接着答spring-boot-autoconfigure 依赖的工作原理?

spring-boot-autoconfigure 依赖的工作原理很简单,通过 @EnableAutoConfiguration 核心注解初始化,并扫描 ClassPath 目录中自动配置类对应依赖。比如工程中有木有添加 Thymeleaf 的 Starter 组件依赖。如果有,就按按一定规则获取默认配置并自动初始化所需要的 Bean。

其实还能再继续答@EnableAutoConfiguration 注解的工作原理!不过篇幅太长,答到上面那个地步就够了!

70、MyBatis 定义的接口,怎么找到实现的?"

回答:一共五步

•       1. Mapper 接口在初始SqlSessionFactory 注册的。

•       2. Mapper 接口注册在了名为 MapperRegistry 类的 HashMap中, key = Mapper class value = 创建当前Mapper的工厂。

•       3. Mapper 注册之后,可以从SqlSession中get

•       4. SqlSession.getMapper 运用了 JDK动态代理,产生了目标Mapper接口的代理对象。

•       5. 动态代理的 代理类是 MapperProxy ,这里边最终完成了增删改查方法的调用。

71、Java 内存结构

JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;

方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

72、Minor GC 和 Full GC

堆内存是JVM中最大的一块由年轻代和老年代组成。

那么,从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。

Major GC 是清理老年代。

Full GC 是清理整个堆空间—包括年轻代和老年代。

73、垃圾回收算法

标记-清除算法、标记整理算法、复制算法、分代收集算法

74、垃圾回收器 G1

评注:上面的题目更深入的问法。JVM可以配置不同的回收器。比如Serial, Parallel和CMS几种垃圾回收器。以Serial Collector(串行回收器)为例,它在在年轻代是一个使用标记-复制算法的回收器。在老年代使用的是标记-清扫-整理算法。

另外,关于G1回收器可以问的点很多,此题作者没有描述清楚究竟问的是G1回收器的那个点,就满回答一下概念吧!

如果是我来问,我就直接给你场景,问你该用哪种回收器了。直接问回收器,那就比较容易了!

常用参数:

-XX:+UseSerialGC:在新生代和老年代使用串行收集器

-XX:+UseParNewGC:在新生代使用并行收集器

//自己查询吧,太多了!

回答:

G1 GC是Jdk7的新特性之一、Jdk7+版本都可以自主配置G1作为JVM GC选项。G1 将整个堆划分为一个个大小相等的小块(每一块称为一个region),每一块的内存是连续的,每个块也会充当 Eden、Survivor、Old三种角色,但是它们不是固定的,这使得内存使用更加地灵活。如下图所示

执行垃圾收集时,收集线程在标记阶段和应用程序线程并发执行,标记结束后,G1 也就知道哪些区块基本上是垃圾,存活对象极少,G1 会先从这些区块下手,因为从这些区块能很快释放得到很大的可用空间,这也是为什么 G1 被取名为 Garbage-First 的原因。

75、Spring RestTemplate 的具体实现

其实RestTemplate和sl4fj这种门面框架很像,本质就是在Http的网络请求中增加一个马甲,本身并没有自己的实现。对此有疑问的,可以看我的另一篇

《架构师必备,带你弄清混乱的JAVA日志体系!》

底层可以支持多种httpclient的http访问,上层为ClientHttpRequestFactory接口类,底层如下所示:

那么RestTemplate则封装了组装、发送 HTTP消息,以及解析响应的底层细节。

76、描述下网页一个 Http 请求,到后端的整个请求过程

利用DNS进行域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户

77、多线程的常用方法和接口类及线程池的机制

常用方法:

start,run,sleep,wait,notify,notifyAll,join,isAlive,currentThread,interrupt

常用接口类:

Runnable、Callable、Future、FutureTask

78、线程池的机制:

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,所以出现了池化技术!。

简单的线程池包括如下四个组成部分即可:

•       线程池管理器(ThreadPoolManager):用于创建并管理线程池

•       工作线程(WorkThread): 线程池中线程

•       任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行

•       任务队列:用于存放没有处理的任务。提供一种缓冲机制

79、死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

产生死锁的原因主要是:

•       (1) 因为系统资源不足。

•       (2) 进程运行推进的顺序不合适。

•       (3) 资源分配不当等。

80、有比较过 Http 和 RPC 吗?

只要是远程调用都可以叫RPC,和是不是通过http没什么关系。

那么,调用过程,也就是通信过程之间需要协议,可以是HTTP协议、dubbo协议等、其他协议等。

81、什么是降级

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。

降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

81、SpringBoot中starter原理

starter中简单来讲就是引入了一些相关依赖和一些初始化的配置。

为什么加了@Configuration注解还是要配置META-INF/spring.factories呢?因为springboot项目默认只会扫描本项目下的带@Configuration注解的类,如果自定义starter,不在本工程中,是无法加载的,所以要配置META-INF/spring.factories配置文件,这个由@EnableAutoConfiguration帮我们实现。

82、Java中volatile关键字

volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

(1)保证可见性,不保证原子性

a.当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;

b.这个写会操作会导致其他线程中的缓存无效。

1)volatile不适合复合操作

(2)禁止指令重排

83、并发编程的3个基本概念

1)原子性

定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

a. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。

b.所有引用reference的赋值操作

c.java.concurrent.Atomic.* 包中所有类的一切操作

(2)可见性

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

(3)有序性

定义:即程序执行的顺序按照代码的先后顺序执行。

Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。

84、什么是XA协议?

XA是X/Open组织为DTP(分布式事务处理)制定的标准协议。XA的目的是保证分布式事务的ACID特性,就像本地事务一样。

发布了130 篇原创文章 · 获赞 66 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/zhuzj12345/article/details/103078861