# 招银网络 Java 面试
大家好,我是小林。
这个月跟分享了很多互联网中大厂的校招薪资,有同学留言想看看银行方面薪资待遇。
这次我们来看招商银行网络科技 Java 后端开发的 25 届校招薪资:
- sp offer:18k x 12 + 9w(津贴+绩效)=年包 31w,同学学历背景硕士 985, 城市杭州
- sp offer:17.5k x 12 + 9w(津贴+绩效)= 年包 30w,同学学历背景硕士985, 城市深圳
- 普通 offer:15.5k x 12 + 9w(津贴+绩效)= 年包 27w,同学学历背景硕士双一流,城市杭州
公积金是按 12% 缴纳,整体年包在 27~30w。
招银网络科技是招商银行的软件中心,大部分开发都是在这边,上班时间会比银行多工作几个小时,深圳这边是早上8点半到下午5点半,中午两小时午休。工作日有时会有加班,这个也看项目情况,周末一般是正常双休,加班强度比互联网要低不少的。
看到拿到招银网络的 offer 同学的学历都还是比较优秀的,估计手上或多或少都有互联网中大厂的 offer,不少同学就把招银网络 offer 给鸽了,选择去了大厂。
比如,有位训练营同学找我 offer 咨询,他手上除了招银 offer 之外,还有很多不错的 offer。
那招银网络的面试难度如何?
互联网大厂的面试强度普遍是比较高的,动不动就是 1 个小时的,面下来整体人的感受是会精疲力尽的,不过一般也只有互联网大厂的强度会比较高,但是像国企和银行普遍是 20-30 分钟左右就结束一场面试,强度是直接降低了一半。
这次,来一起看看一位同学的**招银网络的 Java 后端面经,**一面主要是问八股了,八股+算法,面试时长大概 30 分钟,主要拷打了 Java 基础、JVM、操作系统、网络、MySQL、Redis 的内容。
大家看看难度如何?
# Java 基础
# 知道集合有哪些吗?
List是有序的Collection,使用此接口能够精确的控制每个元素的插入位置,用户能根据索引访问List中元素。常用的实现List的类有LinkedList,ArrayList,Vector,Stack。
- ArrayList是容量可变的非线程安全列表,其底层使用数组实现。当几何扩容时,会创建更大的数组,并把原数组复制到新数组。ArrayList支持对元素的快速随机访问,但插入与删除速度很慢。
- LinkedList本质是一个双向链表,与ArrayList相比,,其插入和删除速度更快,但随机访问速度更慢。
Set不允许存在重复的元素,与List不同,set中的元素是无序的。常用的实现有HashSet,LinkedHashSet和TreeSet。
- HashSet通过HashMap实现,HashMap的Key即HashSet存储的元素,所有Key都是用相同的Value,一个名为PRESENT的Object类型常量。使用Key保证元素唯一性,但不保证有序性。由于HashSet是HashMap实现的,因此线程不安全。
- LinkedHashSet继承自HashSet,通过LinkedHashMap实现,使用双向链表维护元素插入顺序。
- TreeSet通过TreeMap实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。
Map 是一个键值对集合,存储键、值和之间的映射。Key 无序,唯一;value 不要求有序,允许重复。Map 没有继承于 Collection 接口,从 Map 集合中检索元素时,只要给出键对象,就会返回对应的值对象。主要实现有TreeMap、HashMap、HashTable、LinkedHashMap、ConcurrentHashMap
- HashMap:JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突),JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- HashTable:数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap:红黑树(自平衡的排序二叉树)
- ConcurrentHashMap:Node数组+链表+红黑树实现,线程安全的(jdk1.8以前Segment锁,1.8以后volatile + CAS 或者 synchronized)
# arraylist和linkedlist的区别是什么?
ArrayList和LinkedList都是Java中常见的集合类,它们都实现了List接口。
- 底层数据结构不同:ArrayList使用数组实现,通过索引进行快速访问元素。LinkedList使用链表实现,通过节点之间的指针进行元素的访问和操作。
- 插入和删除操作的效率不同:ArrayList在尾部的插入和删除操作效率较高,但在中间或开头的插入和删除操作效率较低,需要移动元素。LinkedList在任意位置的插入和删除操作效率都比较高,因为只需要调整节点之间的指针。
- 随机访问的效率不同:ArrayList支持通过索引进行快速随机访问,时间复杂度为O(1)。LinkedList需要从头或尾开始遍历链表,时间复杂度为O(n)。
- 空间占用:ArrayList在创建时需要分配一段连续的内存空间,因此会占用较大的空间。LinkedList每个节点只需要存储元素和指针,因此相对较小。
- 使用场景:ArrayList适用于频繁随机访问和尾部的插入删除操作,而LinkedList适用于频繁的中间插入删除操作和不需要随机访问的场景。
- 线程安全:这两个集合都不是线程安全的,Vector是线程安全的
# JVM
# 为什么java有跨平台性?
Java虚拟机定义了一种Java内存模型(Java memory model, JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,简单理解也就是说Java虚拟机相当于是在源码和平台之间抽象了一层出来,专门处理一些平台之间访问的兼容问题,使得源码可以一次编译到处运行
。
Java源代码先是经过编译器进行编译,变成.class文件,由类加载器加载进内存运行,这种字节码文件与具体的平台和机器硬件无关。
Java 程序员只需在任意平台将 Java 代码编译成.class文件,就可以在任意平台下的 JVM 中运行,从而隔绝了平台和机器硬件的差异,实现了跨平台。
# 对象存储在哪个区?
对象主要存储在堆区中,不过 Java对象并不一定都是分配在堆内存上,如果JVM确定一个对象不会逃逸,它可以选择将这个对象分配在线程的栈上而不是堆上。栈是线程私有的内存区域,当方法执行结束时,栈上的数据会被自动销毁。
栈上分配依赖于逃逸分析和标量替换。
- 标量替换:就是将对象拆解为其成员变量。例如,如果一个对象包含整数、浮点数和其他基本数据类型的字段,那么这些字段将被单独分配到栈上。解决不会因为没有一大块连续空间导致对象内存不够分配的问题。
- 标量:标量即不可被进一步分解的量,JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等)。
- 聚合量:聚合量就是可以被进一步分解的量,通常是对象,JAVA中对象就是可以被进一步分解的聚合量,对象包含多个成员变量。
相关的JVM参数:
- 逃逸分析:-XX:+DoEscapeAnalysis(JDK7以后默认开启)
- 标量替换:-XX:+EliminateAllocations(JDK7以后默认开启)
通过逃逸分析和栈上分配,JVM可以减少垃圾回收的频率和开销。这有助于提高应用程序的性能,特别是在存在大量临时对象的情况下,因为这些对象可以更快地释放,而不会给垃圾回收器带来过大的压力。
# 垃圾清理的对象的分代介绍一下?
JVM 将堆空间分成了新生代和老生代,如下图所示:
通过上图, 可以看到新生代和老年代的对比,Minor GC 发生在新生代,而 Full GC 发生在老年代。新生代分为三个区,一个 Eden 区和两个 Survivor 区。
先来看下 Eden 区的作用,大部分新生成的对象都是在 Eden 区,Eden 区满了之后便没有内存给新对象使用,Eden 区便会 Minor GC 回收无用内存,剩下的存活对象便会转移到 Survivor 区。
那两个 Survivor 区的作用分别是什么呢?两者其实是对称分布的,一个是 From 区,一个是 To 区。从 Eden 区存活下来的对象首先会被复制到 From 区,当 From 区满时,此时还存活的对象会被转移到 To 区,经历了多次的 Minor GC 后,还存活的对象就会被复制到老年代,老年代的 GC 一般叫作 FullGC 或者 MajorGC。
我们对比下新生代垃圾回收和老年代垃圾回收的区别,如下表所示:
# 操作系统
# 线程和进程的区别?
- 本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
- 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
- 稳定性方面:进程中某个线程如果崩溃了,可能会导致整个进程都崩溃。而进程中的子进程崩溃,并不会影响其他进程。
- 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
- 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线
# 同一进程内线程不共享的内存知道吗?
每个线程的栈是独立的,不共享。
# 网络
# tcp 三次握手和四次挥手过程说一下
三次握手
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:
- 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态
- 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
- 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
TCP 四次挥手过程
具体过程:
- 客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个 FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态;
- 服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,服务端应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等候的其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
- 接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 read() 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个 FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
- 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
- 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
- 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;
# mysql
# 事务四大特性有哪些?
- 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
- 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
MySQL InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
- 持久性是通过 redo log (重做日志)来保证的;
- 原子性是通过 undo log(回滚日志) 来保证的;
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
- 一致性则是通过持久性+原子性+隔离性来保证;
# 事务并发会产生什么现象?
MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。
那么在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。
接下来,通过举例子给大家说明,这些问题是如何发生的。
脏读
如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
举个栗子。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取小林的余额数据,然后再执行更新操作,如果此时事务 A 还没有提交事务,而此时正好事务 B 也从数据库中读取小林的余额数据,那么事务 B 读取到的余额数据是刚才事务 A 更新后的数据,即使没有提交事务。
因为事务 A 是还没提交事务的,也就是它随时可能发生回滚操作,如果在上面这种情况事务 A 发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
举个栗子。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取小林的余额数据,然后继续执行代码逻辑处理,**在这过程中如果事务 B 更新了这条数据,并提交了事务,那么当事务 A 再次读取该数据时,就会发现前后两次读到的数据是不一致的,这种现象就被称为不可重复读。
幻读
在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
举个栗子。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库查询账户余额大于 100 万的记录,发现共有 5 条,然后事务 B 也按相同的搜索条件也是查询出了 5 条记录。
接下来,事务 A 插入了一条余额超过 100 万的账号,并提交了事务,此时数据库超过 100 万余额的账号个数就变为 6。
然后事务 B 再次查询账户余额大于 100 万的记录,此时查询到的记录数量有 6 条,发现和前一次读到的记录数量不一样了,就感觉发生了幻觉一样,这种现象就被称为幻读。
# 可重复读如何解决幻读?
当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。
按理论来说,只有到 可串行化 的最高隔离级别才能解决幻读问题,但是 MySql 在可重复读的隔离级别下就已经通过一些手段解决了幻读问题:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。无锁化,生成 ReadView 时版本链上已经提交的事务可见。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。每次都读最新记录,通过锁来控制并发。
这两个解决方案是很大程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是无法解决的。
# 如果要在一个表里面加入一个新的字段应该注意什么?
要注意这个表是否有被其他事务正在读写,如果有其他事务正在读写这张表,这时候在表增加字段可能会造成阻塞的问题。因为读写一张表的时候,加的是MDL读锁(表级锁),而且对一个表增加字段的时候会产生MDL写锁,这时候就发生了MDL读写锁冲突的问题。
# redis
# redis和mysql数据库如何保证一致性?
对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。对于写数据,我会选择更新 db 后,再删除缓存。
缓存是通过牺牲强一致性来提高性能的。这是由CAP理论决定的。缓存系统适用的场景就是非强一致性的场景,它属于CAP中的AP。所以,如果需要数据库和缓存数据保持强一致,就不适合使用缓存。
所以使用缓存提升性能,就是会有数据更新的延迟。这需要我们在设计时结合业务仔细思考是否适合用缓存。然后缓存一定要设置过期时间,这个时间太短、或者太长都不好:
- 太短的话请求可能会比较多的落到数据库上,这也意味着失去了缓存的优势。
- 太长的话缓存中的脏数据会使系统长时间处于一个延迟的状态,而且系统中长时间没有人访问的数据一直存在内存中不过期,浪费内存。
但是,通过一些方案优化处理,是可以最终一致性的。
针对删除缓存异常的情况,可以使用 2 个方案避免:
- 删除缓存重试策略(消息队列)
- 订阅 binlog,再删除缓存(Canal+消息队列)
消息队列方案
我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
- 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
- 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
举个例子,来说明重试机制的过程。
重试删除缓存机制还可以,就是会造成好多业务代码入侵。
订阅 MySQL binlog,再操作缓存
「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
下图是 Canal 的工作原理:
将binlog日志采集发送到MQ队列里面,然后编写一个简单的缓存删除消息者订阅binlog日志,根据更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存一致性。
# 算法
- 找出一些数中第二大数
对了,最新的互联网大厂后端面经都会在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。