# 平安银行 Java 面试

大家好,我是小宇宙。
很多公司在面试流程之前,都会先安排行测题目,主要是职业性格、逻辑推理、价值观等非技术方面的测试。
很多同学觉得这不重要,就 5 分钟乱填就结束了,实际上这个还是蛮重要的,测评其实也是会挂人的,所以测评大家还是要认真做题,尽量往正能量、担当责任、高忠诚度、大公无私等方向去做选择,不要过于老实选择自己是怎样的就选怎样的。
看到有同学面试平安银行的时候,一面面试都回答出来的,面试官也反馈能力很不错,可惜职业人格测试没有通过,最后直接挂了,实在是可惜了。
平安银行开发岗的校招薪资,我收集了一些,总包在 25w~28w。
后端开发,总包 28w,硕士 985,深圳办公
运维开发,15.6w(13kx12)+9w(奖金)总总包,硕士 211,深圳办公
后端开发, 15.6w(13kx12)+9w (奖金)总包,硕士 211,深圳办公
前端开发, 15.6w(13kx12)+9w (奖金)总包,硕士 211,深圳办公
去年训练营 (opens new window)同学也有拿到了平安银行 offer,后面由于拿到 30w 年薪+互联网中厂 offer 之后就拒了。

也是建议同学们面试求职的时候,多积极投递和面试,拿到的 offer 也建议先接下来,最后去不去再说,有了保底 offer 之后,整个人的求职心得是会有变化的,会更加稳健,不会过度着急,这样的心态在面试表现中也会更自信一点,不要觉得接了 offer,最后不去有道德报复,公司简历池还是有很多优秀备选人才的,而我们作为求职者最终只能选择一家公司,其实是属于比较被动的一方,尽量多接 offer ,化被动为主动。
那这次来看看平安银行的一面面经,一面八股拷打为主,主要问了 10 多个技术问题,涉及的知识点主要有Java 并发、Spring、Redis、MySQL,都是比较基础的问题,整体难度不算大。

# 平安银行(一面)
# 1. synchronized的底层原理是什么?
synchronized是Java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,
使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。
实际上大部分时候我认为说到monitorenter就行了,但是为了更清楚的描述,还是再具体一点。
如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。
当多个线程进入同步代码块时,首先进入entryList
有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null

# 2. synchronized和reentrantlock的区别是什么?
synchronized 和 ReentrantLock 都是 Java 中提供的可重入锁:
用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。而 ReentrantLock 需要手动加锁和释放锁
锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
# 3. 线程的状态有哪些?
Java 线程(java.lang.Thread.State)共有 6 种状态:
java.lang.Thread.State
NEW(新建):线程对象已创建但尚未调用 start() 方法。
start()
RUNNABLE(可运行):调用 start() 后线程进入此状态,包含操作系统层面的"就绪"和"运行中"两种情形,Java 层面将两者合并为 RUNNABLE。
start()
BLOCKED(阻塞):线程等待进入 synchronized 同步代码块/方法时,因锁被其他线程持有而被阻塞。
synchronized
WAITING(无限期等待):调用 Object.wait()(无参)、Thread.join()(无参)或 LockSupport.park() 后进入,需要被其他线程显式唤醒。
Object.wait()
Thread.join()
LockSupport.park()
TIMED_WAITING(限期等待):调用带超时参数的 sleep(long)、wait(long)、join(long)、LockSupport.parkNanos/parkUntil 后进入,超时后会自动唤醒。
sleep(long)
wait(long)
join(long)
LockSupport.parkNanos/parkUntil
TERMINATED(终止):线程的 run() 方法执行完毕或因异常退出。
run()
# 4. sleep和wait的区别?
二者的区别主要体现在:语法使用不同、所属类不同、唤醒方式不同、释放锁不同和线程进入的状态不同。
语法不同:wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出的异常,而 sleep 可以单独使用,无需配合 synchronized 一起使用。
所属类不同:wait 方法属于 Object 类的方法,而 sleep 属于 Thread 类的方法
唤醒方式不同:sleep 方法必须要传递一个超时时间的参数,且过了超时时间之后,线程会自动唤醒。而 wait 方法可以不传递任何参数,不传递任何参数时表示永久休眠,直到另一个线程调用了 notify 或 notifyAll 之后,休眠的线程才能被唤醒。也就是说 sleep 方法具有主动唤醒功能,而不传递任何参数的 wait 方法只能被动的被唤醒。
锁释放资源不同:wait 方法会主动的释放锁,而 sleep 方法则不会
线程进入状态不同:调用 sleep 方法线程会进入 TIMED_WAITING 有时限等待状态,而调用无参数的 wait 方法,线程会进入 WAITING 无时限等待状态。
# 5. 讲讲java的线程池工作原理?
线程在正常执行或者异常中断时会被销毁,如果频繁的创建很多线程,不仅会消耗系统资源,还会降低系统的稳定性,一不小心把系统搞崩了。
使用线程池可以带来以下几个好处:
线程池内部的线程数是可控的,可以灵活的设置参数;
线程池内会保留部分线程,当提交新的任务可以直接运行;
方便内部线程资源的管理,调优和监控;
所以,线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗,线程池的工作原理如下图:

当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
# 6. 线程池的参数有哪些?
线程池的构造函数有7个参数:

corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
maximumPoolSize:线程池中最多可容纳的线程数量。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程且当前线程池的线程数量小于maximumPoolSize,就会创建新的线程来执行任务,否则就会将该任务加入到阻塞队列中,如果阻塞队列满了,并且当前线程数小于 maximumPoolSize,就会创建一个新的非核心线程来直接执行该新任务;如果当前线程数已达到 maximumPoolSize,就不会再创建新线程,而是执行拒绝策略。
keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
unit:就是keepAliveTime时间的单位。
workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
threadFactory:线程工厂。可以用来给线程取名字等等
handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,并且当前线程数小于 maximumPoolSize,就会创建一个新的非核心线程来直接执行该新任务;如果当前线程数已达到 maximumPoolSize,就不会再创建新线程,而是执行拒绝策略
# 7. 线程池的submit和execute有什么区别?
它们都用于向线程池提交任务,但在功能、返回值和异常处理等方面存在明显区别:
所属接口与方法签名:execute方法属于 Executor 接口,该接口是 Java 线程池框架的基础接口,execute 方法的作用是执行一个 Runnable 任务。submit方法属于 ExecutorService 接口,ExecutorService 继承自 Executor 接口并对其进行了扩展。submit 方法有三个重载形式,不仅可以接受 Runnable 任务,还能接受 Callable 任务,并且会返回一个 Future 对象用于获取任务的执行结果。
Executor
execute
Runnable
ExecutorService
ExecutorService
Executor
submit
Runnable
Callable
Future
返回值:execute 方法没有返回值,它仅仅是将 Runnable 任务提交给线程池执行,无法直接获取任务的执行结果。submit 方法会返回一个 Future 对象,通过这个 Future 对象,你可以在需要的时候获取任务的执行结果,或者检查任务是否完成、取消任务等,如果提交的是 Callable 任务,Future 对象可以获取 Callable 任务的返回值;如果提交的是 Runnable 任务,使用 submit(Runnable task, T result) 方法可以指定一个结果对象,在任务执行完成后可以通过 Future 对象获取该结果。
execute
Runnable
submit
Future
Future
Callable
Future
Callable
Runnable
submit(Runnable task, T result)
Future
异常处理 :execute 方法如果在 Runnable 任务中抛出未捕获的异常,该异常会直接抛给线程池,线程池会根据其异常处理策略进行处理,通常会导致执行该任务的线程终止。submit方法如果任务在执行过程中抛出异常,异常不会立即抛出,而是被封装在 Future 对象中,当调用 Future 对象的 get 方法时,会抛出 ExecutionException,其 getCause 方法可以获取实际的异常信息。
execute
Runnable
Future
Future
get
ExecutionException
getCause
# 8. Spring IOC的初始化过程是怎样的?
IOC容器的初始化分为三个过程实现:
第一个过程是Resource资源定位。这个Resouce指的是BeanDefinition的资源定位。这个过程就是容器找数据的过程,就像水桶装水需要先找到水一样。
第二个过程是BeanDefinition的载入过程。这个载入过程是把用户定义好的Bean表示成Ioc容器内部的数据结构,而这个容器内部的数据结构就是BeanDefition。
第三个过程是向IOC容器注册这些BeanDefinition的过程,这个过程就是将前面的BeanDefition保存到HashMap中的过程。
上面提到的过程一般是不包括Bean的依赖注入的实现。在Spring中,Bean的载入和依赖注入是两个独立的过程。依赖注入一般发生在应用第一次通过getBean向容器索取Bean的时候。
下面的一张图描述了这三个过程调用的主要方法,图中的四个过程其实描述的是上面的第二个过程和第三个过程:

# 9. beanFactory和ApplicationContext的区别?
BeanFactory是 Spring 框架的基础 Bean 容器,提供了基本的 Bean 管理功能;而ApplicationContext是BeanFactory的扩展,提供了更多高级特性,适用于大多数企业级应用开发,主要区别如下:
BeanFactory
ApplicationContext
BeanFactory
功能特性上的区别:BeanFactory:它是 Spring 框架中 Bean 容器的基础接口,提供了最基本的 Bean 管理功能,如获取 Bean、判断 Bean 是否存在等。它采用了懒加载的方式,只有在真正需要使用某个 Bean 时才会对其进行实例化。ApplicationContext:它继承自BeanFactory,是BeanFactory的子接口。除了具备BeanFactory的基本功能外,还提供了更多高级特性,如国际化支持、事件发布、资源加载等。它在容器启动时就会对所有单例 Bean 进行实例化和初始化。
BeanFactory
ApplicationContext
BeanFactory
BeanFactory
BeanFactory
事件处理上的区别:BeanFactory 本身不支持事件处理,需要手动注册ApplicationEventMulticaster才能实现事件发布和监听功能。ApplicationContext 内置了事件发布和监听机制,可以方便地实现事件驱动的编程模型。
BeanFactory
ApplicationEventMulticaster
ApplicationContext
使用场景的区别:BeanFactory:适用于资源受限的环境,如移动设备或嵌入式系统,因为它采用懒加载的方式,可以节省系统资源。ApplicationContext:适用于大多数企业级应用开发,因为它提供了丰富的功能,能够满足复杂应用的需求。
BeanFactory
ApplicationContext
# 10. Redis分布式锁的问题,锁过期应该怎么解决?
可以借助一个后台线程,在锁快要过期时自动延长锁的过期时间,保证在业务逻辑执行期间锁不会过期,Redisson等客户端库已内置此功能。
# 11. mysql 的 limit查询第1页和第200页有什么不同?
第1页:LIMIT 0, 20 只需扫描前20条记录,第200页:LIMIT 3980, 20 需要先扫描前3980条记录,再返回20条。
LIMIT 0, 20
LIMIT 3980, 20
这属于深分页的性能问题,可以游标分页法来优化:
-- 第一页
SELECT * FROM users ORDER BY id LIMIT 20;
-- 后续页 (记住上一页最后一条记录的id)
SELECT * FROM users WHERE id > last_seen_id ORDER BY id LIMIT 20;
-- 第一页
SELECT * FROM users ORDER BY id LIMIT 20;
-- 后续页 (记住上一页最后一条记录的id)
SELECT * FROM users WHERE id > last_seen_id ORDER BY id LIMIT 20;
对了,最新的互联网大厂后端面经都会在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。
