理想今年的校招薪资已经开奖了,我觉得给的还是挺多的,和去年差不多。
后端:28K*16,北京,硕士 985 算法:40K*16,杭州,硕士 985 大模型:42K*16,北京,海归 985 嵌入式:32K*16,上海,硕士 985 安全工程师:32K*14,杭州,硕士 211
理想的五险一金中的公积金是交 12%,其他的就没什么福利了。
从上面的薪资分享可以看出,理想面试还是挺看重学历的,分享薪资的同学基本都是硕士 985 和 211,其中硕士 985 最多!
前段时间,星球里刚好有球友去面试了理想的 Java 后端岗位,可惜的是,一面面完就直接挂了。
接下来,分享一下这位球友的面经,大家感受一下难度如何。我会对其中涉及到的面试题添加详细的参考答案。
项目拷打
项目就拷打了几个问题,主要是缓存 Redis 相关的,难度较低。一面面试官更多的是在八股拷打,并发编程问了很多问题。有很多我都忘了,回答的不是很好。
作为求职者,我们可以从这些方案去准备项目经历的回答:
你对项目基本情况(比如项目背景、核心功能)以及整体设计(比如技术栈、系统架构)的了解(面试官可能会让你画系统的架构图、让你讲解某个模块或功能的数据库表设计) 你在这个项目中你担任了什么角色?负责了什么?有什么贡献?(具体说明你在项目中的职责和贡献) 你在这个项目中是否解决过什么问题?怎么解决的?收获了什么?(展现解决问题的能力) 你在这个项目用到了哪些技术?这些技术你吃透了没有?(举个例子,你的项目经历使用了 Seata 来做分布式事务,那 Seata 相关的问题你要提前准备一下吧,比如说 Seata 支持哪些配置中心、Seata 的事务分组是怎么做的、Seata 支持哪些事务模式,怎么选择?) 你在这个项目中犯过的错误,最后是怎么弥补的?(承认不足并改进才能走的更远) 从这个项目中你学会了那些东西?学会了那些新技术的使用?(总结你在这个项目中的收获)
谈谈你知道的 Java 锁
ReentrantLock
: 实现了Lock
接口,是一个可重入且独占式的锁,和synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。ReentrantReadWriteLock
:实现了ReadWriteLock
,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。StampedLock
:JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量Condition
。不同于一般的Lock
类,StampedLock
并不是直接实现Lock
或ReadWriteLock
接口,而是基于 CLH 锁 独立实现的(AQS 也是基于这玩意)。
什么叫可重入锁?
可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
JDK 提供的所有现成的 Lock
实现类,包括 synchronized
关键字锁都是可重入的。
在下面的代码中,method1()
和 method2()
都被 synchronized
关键字修饰,method1()
调用了method2()
。
public class SynchronizedDemo {
public synchronized void method1() {
System.out.println("方法1");
method2();
}
public synchronized void method2() {
System.out.println("方法2");
}
}
由于 synchronized
锁是可重入的,同一个线程在调用method1()
时可以直接获得当前对象的锁,执行 method2()
的时候可以再次获取这个对象的锁,不会产生死锁问题。假如synchronized
是不可重入锁的话,由于该对象的锁已被当前线程所持有且无法释放,这就导致线程在执行 method2()
时获取锁失败,会出现死锁问题。
公平锁和非公平锁有什么区别?
公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
synchronized 算重量级锁码?为什么?
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在 Java 早期版本中,synchronized
属于 重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock
来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
不过,在 Java 6 之后, synchronized
引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized
锁的效率提升了很多。因此, synchronized
还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized
。
关于偏向锁多补充一点:由于偏向锁增加了 JVM 的复杂性,同时也并没有为所有应用都带来性能提升。因此,在 JDK15 中,偏向锁被默认关闭(仍然可以使用 -XX:+UseBiasedLocking
启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)。
Spring Bean 生命周期
创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。 Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如 @Autowired
等注解注入的对象、@Value
注入的值、setter
方法或构造函数注入依赖和值、@Resource
注入的各种资源。Bean 初始化:
如果 Bean 实现了 BeanNameAware
接口,调用setBeanName()
方法,传入 Bean 的名字。如果 Bean 实现了 BeanClassLoaderAware
接口,调用setBeanClassLoader()
方法,传入ClassLoader
对象的实例。如果 Bean 实现了 BeanFactoryAware
接口,调用setBeanFactory()
方法,传入BeanFactory
对象的实例。与上面的类似,如果实现了其他 *.Aware
接口,就调用相应的方法。如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor
对象,执行postProcessBeforeInitialization()
方法如果 Bean 实现了 InitializingBean
接口,执行afterPropertiesSet()
方法。如果 Bean 在配置文件中的定义包含 init-method
属性,执行指定的方法。如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法。
如果 Bean 实现了 DisposableBean
接口,执行destroy()
方法。如果 Bean 在配置文件中的定义包含 destroy-method
属性,执行指定的 Bean 销毁方法。或者,也可以直接通过@PreDestroy
注解标记 Bean 销毁之前执行的方法。
AbstractAutowireCapableBeanFactory
的 doCreateBean()
方法中能看到依次执行了这 4 个阶段:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// 1. 创建 Bean 的实例
BeanWrapper instanceWrapper = null;
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
Object exposedObject = bean;
try {
// 2. Bean 属性赋值/填充
populateBean(beanName, mbd, instanceWrapper);
// 3. Bean 初始化
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
// 4. 销毁 Bean-注册回调接口
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
return exposedObject;
}
Aware
接口能让 Bean 能拿到 Spring 容器资源。
Spring 中提供的 Aware
接口主要有:
BeanNameAware
:注入当前 bean 对应 beanName;BeanClassLoaderAware
:注入加载当前 bean 的 ClassLoader;BeanFactoryAware
:注入当前BeanFactory
容器的引用。
BeanPostProcessor
接口是 Spring 为修改 Bean 提供的强大扩展点。
public interface BeanPostProcessor {
// 初始化前置处理
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
// 初始化后置处理
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
postProcessBeforeInitialization
:Bean 实例化、属性注入完成后,InitializingBean#afterPropertiesSet
方法以及自定义的init-method
方法之前执行;postProcessAfterInitialization
:类似于上面,不过是在InitializingBean#afterPropertiesSet
方法以及自定义的init-method
方法之后执行。
InitializingBean
和 init-method
是 Spring 为 Bean 初始化提供的扩展点。
public interface InitializingBean {
// 初始化逻辑
void afterPropertiesSet() throws Exception;
}
指定 init-method
方法,指定初始化方法:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="demo" class="com.chaycao.Demo" init-method="init()"/>
</beans>
如何记忆呢?
整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。 初始化这一步涉及到的步骤比较多,包含 Aware
接口的依赖注入、BeanPostProcessor
在初始化前后的处理以及InitializingBean
和init-method
的初始化操作。销毁这一步会注册相关销毁回调接口,最后通过 DisposableBean
和destory-method
进行销毁。
最后,再分享一张清晰的图解(图源:如何记忆 Spring Bean 的生命周期[1])。
Spring 事务超时指的是?
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition
中以 int 的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。
Spring 事务失效的原因列举几种
@Transactional
注解应用于private
、final
或者static
方法;同类内部方法调用; Bean 未被 Spring 管理; 错误使用传播机制; 业务方法本身捕获了异常; ......
详细介绍推荐阅读这篇文章:工作 6 年,@Transactional 注解用的一塌糊涂。
Redis 中的数据类型有哪些?
Redis 中比较常见的数据类型有下面这些:
5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
Redis 5 种基本数据类型其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
Redis 5 种基本数据类型对应的底层数据结构实现如下表所示:
String | List | Hash | Set | Zset |
---|---|---|---|---|
SDS | LinkedList/ZipList/QuickList | Dict、ZipList | Dict、Intset | ZipList、SkipList |
Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList。从 Redis 7.0 开始, ZipList 被 ListPack 取代。
除了上面提到的之外,还有一些其他的比如 Bloom filter(布隆过滤器)、Bitfield(位域)。
详细介绍可以参考这篇文章:Redis 八种常用数据类型常用命令和应用场景。
缓存和数据库一致性如何保证?
细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。
下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。
Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直接删除缓存 。
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案:
缓存失效时间变短(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 增加缓存更新重试机制(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。
相关文章推荐:缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹。
HTTP 4XX 状态码代表什么?
HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被成功处理。
GET 和 POST 有什么区别?
这个问题在知乎上被讨论的挺火热的,地址:GET 和 POST 到底有什么区别?[2]。
GET 和 POST 是 HTTP 协议中两种常用的请求方法,它们在不同的场景和目的下有不同的特点和用法。一般来说,可以从以下几个方面来区分二者(重点搞清两者在语义上的区别即可):
语义(主要区别):GET 通常用于获取或查询资源,而 POST 通常用于创建或修改资源。 幂等:GET 请求是幂等的,即多次重复执行不会改变资源的状态,而 POST 请求是不幂等的,即每次执行可能会产生不同的结果或影响资源的状态。 格式:GET 请求的参数通常放在 URL 中,形成查询字符串(querystring),而 POST 请求的参数通常放在请求体(body)中,可以有多种编码格式,如 application/x-www-form-urlencoded、multipart/form-data、application/json 等。GET 请求的 URL 长度受到浏览器和服务器的限制,而 POST 请求的 body 大小则没有明确的限制。不过,实际上 GET 请求也可以用 body 传输数据,只是并不推荐这样做,因为这样可能会导致一些兼容性或者语义上的问题。 缓存:由于 GET 请求是幂等的,它可以被浏览器或其他中间节点(如代理、网关)缓存起来,以提高性能和效率。而 POST 请求则不适合被缓存,因为它可能有副作用,每次执行可能需要实时的响应。 安全性:GET 请求和 POST 请求如果使用 HTTP 协议的话,那都不安全,因为 HTTP 协议本身是明文传输的,必须使用 HTTPS 协议来加密传输数据。另外,GET 请求相比 POST 请求更容易泄露敏感数据,因为 GET 请求的参数通常放在 URL 中。
再次提示,重点搞清两者在语义上的区别即可,实际使用过程中,也是通过语义来区分使用 GET 还是 POST。不过,也有一些项目所有的请求都用 POST,这个并不是固定的,项目组达成共识即可。
HTTP 基于 TCP 还是 UDP?
HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 基于 UDP 的 QUIC 协议 。
此变化解决了 HTTP/2 中存在的队头阻塞问题。队头阻塞是指在 HTTP/2.0 中,多个 HTTP 请求和响应共享一个 TCP 连接,如果其中一个请求或响应因为网络拥塞或丢包而被阻塞,那么后续的请求或响应也无法发送,导致整个连接的效率降低。这是由于 HTTP/2.0 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。HTTP/3.0 在一定程度上解决了队头阻塞问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。
除了解决队头阻塞问题,HTTP/3.0 还可以减少握手过程的延迟。在 HTTP/2.0 中,如果要建立一个安全的 HTTPS 连接,需要经过 TCP 三次握手和 TLS 握手:
TCP 三次握手:客户端和服务器交换 SYN 和 ACK 包,建立一个 TCP 连接。这个过程需要 1.5 个 RTT(round-trip time),即一个数据包从发送到接收的时间。 TLS 握手:客户端和服务器交换密钥和证书,建立一个 TLS 加密层。这个过程需要至少 1 个 RTT(TLS 1.3)或者 2 个 RTT(TLS 1.2)。
所以,HTTP/2.0 的连接建立就至少需要 2.5 个 RTT(TLS 1.3)或者 3.5 个 RTT(TLS 1.2)。而在 HTTP/3.0 中,使用的 QUIC 协议(TLS 1.3,TLS 1.3 除了支持 1 个 RTT 的握手,还支持 0 个 RTT 的握手)连接建立仅需 0-RTT 或者 1-RTT。这意味着 QUIC 在最佳情况下不需要任何的额外往返时间就可以建立新连接。
相关证明可以参考下面这两个链接:
https://zh.wikipedia.org/zh/HTTP/3 https://datatracker.ietf.org/doc/rfc9114/
如何记忆 Spring Bean 的生命周期: https://chaycao.github.io/2020/02/15/如何记忆Spring-Bean的生命周期.html
[2]GET 和 POST 到底有什么区别?: https://www.zhihu.com/question/28586791
推荐阅读:
推荐阅读: