Fork me on GitHub

interview(1)

网易考拉 java 凉面面经

1. 同步 异步 非阻塞 阻塞 bio nio aio

这题问的是网络 IO 模型。

以 IO 的读数据(read)举例,会经历两个阶段:
1)等待数据准备。
2)将数据从内核拷贝到进程中。

IO 模型主要分为五种:

  1. 阻塞 IO
    1. 第一阶段,进程发起 recvfrom,Kernel 开始准备数据(同时用户进程被 block)。
    2. 数据准备好了之后,进入第二阶段,Kernel 开始负责拷贝数据到用户内存(完成之前,用户进程被 block)。
    3. Kernel 返回一个 ok 告知用户进程拷贝完毕。
  2. 非阻塞 IO
    1. 第一阶段,进程发起轮询 recvfrom(并立即返回),询问数据是否准备好。
    2. 第二阶段,当 recvfrom 时发现数据已准备好后,不再立即返回,而是将用户进程 block,同时 Kernel 开始拷贝数据。
    3. Kernel 返回一个 ok 告知用户进程拷贝完毕。
    4. 【墙裂推荐阅读】:深入理解Java NIO
  3. IO 复用
    1. 第一阶段,进程发起 select,Kernel 开始准备数据(同时用户进程被 block,其实是进程等待多个 socket,一旦有一个 socket 准备好了,就可以变成就绪态完成业务,业务结束重新进入阻塞态)。
    2. 第二阶段,当数据准备好后,select 就会返回用户进程,之后,用户进程发起 recvfrom,等待 Kernel 拷贝数据(同时用户进程被 block)。
    3. Kernel 返回一个 ok 告知用户进程拷贝完毕。
    4. 跟阻塞 IO很像,但 select 的优势是能处理更多的连接,参考解释:IO多路复用 到底是阻塞还是非阻塞
  4. 信号驱动 IO
    1. 第一阶段,进程发出 sigaction 信号并立即返回,Kernel 开始准备数据(用户进程不被 block)。
    2. 第二阶段,Kernel 发出 sigio 信号告知进程数据已准备完毕,用户进程发出 recvfrom 给 Kernel(同时对自身 block)。等待 Kernel 拷贝数据。
    3. Kernel 返回一个 ok 告知用户进程拷贝完毕。
  5. 异步 IO
    1. 第一阶段:用户进程发起 aio_read 并立即返回,Kernel 开始准备数据(进程不被 block)。
    2. 第二阶段:Kernel 在数据准备完成后,会立即开始拷贝数据。
    3. 等一切完成后,Kernel 发出一个 signal 告知进程 read 完毕。

推荐阅读:5种网络 IO 模型
github 代码实例:iomode

2. java异常机制类图手写

ExceptionTree.png

RuntimeException == UncheckedException

3. spring事务传播机制

spring 定义了七种事务传播行为

PROPAGATION_REQUIRED – 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS – 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY – 支持当前事务,如果当前没有事务,就抛出异常。(没有事务直接报错)
PROPAGATION_REQUIRES_NEW – 新建事务,如果当前存在事务,把当前事务挂起。(无论什么时候都新建事务)
PROPAGATION_NOT_SUPPORTED – 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER – 以非事务方式执行,如果当前存在事务,则抛出异常。(若有事务直接报错)
PROPAGATION_NESTED – 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

1
2
3
4
5
6
7
8
9
10
ServiceA {
void methodA() {
ServiceB.methodB(); // 运行到此处了
}
}

ServiceB {
void methodB() {
}
}

举例理解:以上事务传播级别都是指ServiceB#methodB(),先看 A 有没有事务,再看 B 的级别是否需要起事务。加深理解:

B的事务传播级别 A 有事务 A 无事务
required 支持A 新建事务
supports 支持A 非事务方式执行
mandatory 支持A 抛异常
required_new 事务A挂起,再新建事务 新建事务
not_supported 事务A挂起,非事务方式执行 非事务方式执行
never 抛异常 非事务方式执行
nested 嵌套事务A内执行 新建事务

解释:

  • 创建一个新事务:新事务跟原来事务没有任何关系;
  • 嵌套事务:A、B 两个事务有父子关系,父事务提交或回滚时,子事务也会提交或回滚;

PS:nested 方式并不是专门为方法嵌套使用的,默认的 required 方式足够满足我们的需要了。

事务的一些小功能:

推荐阅读:深入理解事务–Spring 事务的传播机制

4. 数据库有 2000w 条记录,复制到另一个服务器内,保证无重复,速度快

2000w 记录大概有 2G(估算)
方案:

  1. 定点停机迁移方案。
  2. MySQL 的 binlog方案。解析主服务器的 binlog 日志,把数据写入从服务器中;
  3. 触发器方案。创建触发器,在数据写入时,同时写入新的服务器中。
  4. memcached 协议方案。在数据写入时,同时让 Memcached 服务器接收,然后解析 json 到新的数据库。
  5. 中间件方案。

Tips:

  1. 先 select into outfile 然后再 load data infile 。
  2. 考虑先把索引删除,迁移后再重新建立。
  3. 考虑先把引擎改成 MyISAM。因为在数据量比较大的情况MyISAM的插入速度比Innodb高,这里也是当数据导入完成后再将存储引擎修改为InnoDB。
  4. 导出语句的 insert 语句写成多值形式。
  5. 如果发送的SQL语句太长,以致超过了max_allowed_packet的大小,可以合适地修改这个值。
  6. 增加bulk_insert_buffer_size。

推荐阅读:MySQL-大批量数据如何快速的数据迁移
MySQL数据库的无缝迁移问题:binlog方案、触发器方案
mysql 中如何提高大表之间复制效率

5. redis 事务实现,集群实现

以 MULTI 开始一个事务,然后将多个命令入队到事务中,最后由 EXEC 命令触发事务,一并执行事务中的所有命令。

Tips: redis 命令是原子性的,但是 Redis 事务并不是原子性的。

推荐阅读:Redis 事务-Runoob.com,Redis之事务实现,Redis集群搭建与简单使用

墙裂推荐阅读:Redis(十一):Redis 的事务功能详解

Redis 集群方案:

  1. 客户端分片:在客户端进行路由选择,把对 Key 的访问转发到不同的 Redis 实例中,最后把返回结果汇集。
  2. 代理层:Redis 客户端把请求发送给代理,由代理按照路由规则发送正确的 Redis 实例,并将结果汇集返回给客户端。
  3. P2P 模式:使用 Hash Slot 进行数据拆分。Redis 客户端发送请求到某个 Redis 实例,如果数据不在此实例时,实例返回重定向指定给客户端,客户端进行对目标实例进行访问请求。
  4. 使用云服务器上的集群服务。

redis cluster在设计的时候,就考虑到了去中心化,去中间件,也就是说,集群中的每个节点都是平等的关系,都是对等的,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据

Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384。所以我们在测试的时候看到set 和 get 的时候,直接跳转到了7000端口的节点。

Redis 集群会把数据存在一个 master 节点,然后在这个 master 和其对应的salve 之间进行数据同步。当读取数据时,也根据一致性哈希算法到对应的 master 节点获取数据。只有当一个master 挂掉之后,才会启动一个对应的 salve 节点,充当 master 。

需要注意的是:必须要3个或以上的主节点,否则在创建集群时会失败,并且当存活的主节点数小于总节点数的一半时,整个集群就无法提供服务了。

6. 数据库哈希索引、B+树索引,原理,优缺点

推荐阅读:MySQL B+树索引和哈希索引的区别
原理:
哈希索引:采用一定的哈希算法,把键值换算成新的哈希值,只需要一次哈希算法即可立刻定位到相应的位置。
B+ 树索引:使用平衡的多叉树,非叶节点做索引,关键字都放在叶子节点中,叶子节点间通过双向指针快速左右移动,效率非常高。

区别:

  1. 哈希索引适用于等值查询,一般一次算法就能找到键值,比 B+ 树更好快。
  2. 哈希索引不适用于范围查询(包括部分模糊查询等)。
  3. 哈希索引不适用于联合索引最左规则(最左规则:多列联合查询时,把最常用的放在最左能加快索引速度)。
  4. 如果有大量重复键值的情况,哈希索引效率极低。
  5. B+ 树可以很好地利用局部性原理(3 层 B+ 树可以表示上百万的数据,只需要三次 IO)。

7. java线程池有哪些参数,分别有什么作用

线程池正是为了解决多线程效率低的问题而产生的,它使得线程可以被复用。
线程池参数有七个:
(lee 记忆:两个数、两个时间、一个队列一个工厂、一个拒绝策略)

  1. corePollSize:核心线程数。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。通常不会被回收,除非主动设置。
  2. maximumPoolSize:最大线程数。表明线程中最多能够创建的线程数量。
  3. keepAliveTime:空闲的线程保留的时间。
  4. TimeUnit:时间单位。
  5. BlockingQueue:阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue可选。
  6. ThreadFactory:线程工厂,用来创建线程
  7. RejectedExecutionHandler:队列已满,而且任务量大于最大线程的异常处理策略(lee 注:可理解为拒绝策略)。有以下取值
  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

8. 重入锁有哪些,底层实现方式

可重入锁(ReentrantLock):一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加,并不会互斥(即允许重入)。像 synchronized 和 ReentrantLock 都是可重入锁。 推荐阅读:轻松学习 java 可重入锁(ReentrantLock)
实现代码参考:可重入锁 ReentrantLock 的底层原理实现?

扩展知识:
可重入锁分为:公平锁和非公平锁两类。

公平锁保证等待时间最长的线程将优先获得锁,而非公平锁并不会保证多个线程获得锁的顺序,但是非公平锁的并发性能表现更好,ReentrantLock默认使用非公平锁。

以非公平锁举例,大意是指:

  1. 首先在 lock()方法中用 CAS 进行抢占锁(或设置 state 变量),如果抢占失败,会调用 acquire()方法。
  2. 方法中继续调用tryAcquire()方法再次去获取锁,先判断是否有锁,再判断这个锁是不是本线程持有,若是则修改 state 值。
  3. tryAcquire()方法失败,则将线程置于列尾进行排队。

除了可重入锁外,还有中断锁(Lock)、读写锁(ReadWriteLock)、偏向锁、自旋锁等。

  • 偏向锁:适用于无竞争的情况下,偏向于第一个访问锁的线程,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行。一旦出现竞争,就撤掉偏向锁,升级为轻量级锁。

锁的 3 种优化:、

  1. 自适应自旋锁:自旋的次数不固定,如果上次自旋成功了,那么下一次的自旋次数会增加。
  2. 锁消除:JVM 如果检测到不可能存在共享数据竞争时,JVM 会对这些同步锁进行锁消除(依据是逃逸分析的数据支持)。比如,StringBuffer 的 append()方法、Vector 的 add() 方法,在出现某些代码时,JVM 会大胆地进行锁消除。
  3. 锁粗化:如果一系列连续加锁的操作,也会造成效率降低。锁粗化就是将上面的加锁解锁操作连接在一起,形成范围更大的锁。以上操作由 JVM 检测完成。

推荐阅读:JAVA 并发各种锁

9. 数据库的 on where having 区别

on、where、having 都是条件筛选数据。
区别:

  1. on 通过限制条件筛选后得到中间表,然后中间表返回得到查询结果。
  2. where 是得到中间表后,再通过限制条件筛选出结果。相比下,on 的中间表数据集小,效率更高。
  3. having 必须跟 GroupBy 一起出现,where 不一定。where 在聚集函数计算前筛选,having 在之后筛选。相比之前,where 更快一些。

10. 一个try finally 块返回值是哪个

  1. 首先,return 放在 try-finally 块中是个不好的习惯。
  2. 先执行 try 中的 return,然而并不会结束程序,而是继续执行 finally(但 return 的东西不会改变)。
  3. 然后执行 finally 代码,如果 finally 中有 return(提前结束程序,会被 IDE 警告块不正常完成),就会覆盖掉 try 里的 return(即失效了)。

11. 值传递 索引传递

  • 值传递:方法调用时,实参把它的值传递给对应的形参,而方法内部仅能拿到一个副本,对副本的修改不会改变原实参的值。比如基本数据类型传值。
  • 引用传递:方法调用时,实参的地址传递给对应的形参,如果方法内部对地址中的内容进行了修改,那个原实参的值也会相应发生改变。
  • 但对于一些非基本数据类型,比如 String、Integer、Double等 immutable 的类型需要特殊处理,可以当做值传递来处理。

Ps:原始类型包装类(primitive wrappers)(Integer,Long, Short, Double, Float, Character, Byte, Boolean)也都是不可变的。

有关不可修改对象可参考:Java中mutable对象和immutable对象的区别

12. RPC框架

RPC 是指远程过程调用,通过网络来表达调用的语义和传达调用的数据,并得到返回的结果。
目的是本地调用远程的方法时,隐藏底层的通讯细节,达到功能与服务实现解耦的效果。
RPC 框架一般有:rpcx、grpc、go std rpc、thrift、dubbo等
推荐阅读:流行的 rpc 框架性能测试对比

13. 多线程实现方式

  1. 继承 Thread,覆写 run()方法,再调用 start()方法。
  2. 实现 Runnable 接口,然后创建 Thread 实例,再调用 thread.start()方法。
  3. 实现 Callable 接口,覆写 call()方法,通过 FutureTask 包装器创建 Thread 实例,再调用 thread.start()方法。
    推荐阅读:JAVA多线程实现

// todo 补充一下 Future 类的用法,在多线程合并请求时也用得上:

14. 反射的几种情况

深度好文:深入解析 Java 反射(1) - 基础

-------------The End-------------