年少不知编制香,错把技术当成宝。
共 13087字,需浏览 27分钟
·
2024-04-14 14:44
大家好,我是二哥呀。
周五晚上线下见了一名读者,和他的交集也有好多年了。21年就专门写过一篇关于他的帖子:去携程实习了,老读者应该有印象,当时阅读不低,内容也很干。
后来他又拿到一家互联网中厂的offer,薪资待遇给的不低;但抱着试一试的心态,又阴差阳错的考上了烟草局,于是就放弃了互联网转到了烟草局。
他现在的心态就和转码的时候完全不同,更加放松,对自己的人生也有了新的思考,关键是烟草局的工作实在是太舒服了,更关键的是薪资待遇也很不错。
具体我没办法透露,这个必须得保密,大家应该能理解,烟草局对这方面要求的比较严苛。
期间聊了很多,也很投机,他的一句话令我印象深刻:“二哥你真的是浑身散发着光芒却不刺眼,和你聊天真的太舒服了,下次我们还约。”
从他那里我也学到了很多,了解到了很多,后面在条件允许的情况下我再给大家仔细聊一下。
对于 25 届准备秋招或者 24 届还在春招中煎熬的同学来说,真的可以换一种活法了,如果自己对大厂的高薪,以及高强度的工作压力不是很心动,那不妨试试国企、银行。
国企和银行的面试难度真的比互联网大厂低太多了,我们仍然以《Java 面试指南-中国农业银行面经》中同学 1 的面试内容为例,来看看农行的面试官都喜欢问哪些问题。
看完这个面试题,你可能会感觉这也太简单了吧?
除了项目,仍然是围绕着二哥一直给大家强调的 Java 后端四大件展开,并且难度不大。内容较长,建议大家先收藏起来,面试的时候大概率会碰到,我会尽量用通俗易懂+手绘图的方式,让天下所有的面渣都能逆袭 😁
1、二哥的 Linux 速查备忘手册.pdf 下载 2、三分恶面渣逆袭PDF离线版:https://t.zsxq.com/04FuZrRVf 3、三分恶面渣逆袭在线版:https://javabetter.cn/sidebar/sanfene/nixi.html
农行面经
挑个项目讲一下,做了多久,做了啥
我主要做了两个项目,一个是社区项目技术派,一个是轮子项目 MYDB,我就以技术派为例来说说吧。
技术派是一个前后端分离的社区项目,包括前端 PC 和管理后台,主要用到的技术栈有 Spring Boot、MyBatis-Plus、MySQL、Redis、ElasticSearch、MongoDB、Docker、RabbitMQ 等。
这个项目一共有三名同学参与,我主要负责产品的调研、DB 的设计、项目骨架的搭建,以及后端接口的编写,包括登录认证、消息通知、文章模块、以及管理后台。
我先讲一个如何利用 Redis 来提升系统并发能力的知识点吧。
主要是在 MySQL 上游加一层 Redis 缓存,因为 Redis 支持集群、分片,单机就可以支持数十万 QPS,所以可以大大提高系统性能。
实现方式主要是将热点数据放入 Redis 缓存,比如文中的分类和标签,使用频率会非常高,因为这些数据不会经常变动,且后台配置完毕后,会实时存入缓存中,非常适合作为热点缓存,并对热点缓存设置失效时间,比如 30 分钟,可以作为 Redis 和 MySQL 不一致的兜底策略。
然后对于计数、排行榜等,也可以依托 Redis 的 Zset 来实现。
再讲一个通过 RabbitMQ 来实现消息异步解耦的知识点吧。
当用户订阅、点赞、评论时,会触发消息通知,如果消息是同步发送,一旦消息出现异常,就会影响主流程;或者当消息过多时,就会造成消息积压,进而影响服务器的性能,所以需要对消息进行异步解耦。
目前常用的消息队列有 Kafka、RocketMQ 和 RabbitMQ,Kafka 适用于海量数据的互联网服务的数据收集业务;RocketMQ 的吞吐量非常高,支持消息时序,对可靠性要求很高的场景会更合适,比如电商业务。
RabbitMQ 虽然虽然是用 erlang 开发的,不利于二次开发和维护,但是社区活跃度高,同时我们的系统对并发要求没有那么高,消息通知也可以无序,同时 RabbitMQ 也支持消息路由,宕机后的消息也能自动恢复,还提供了一个易用的用户界面,可以让用户监控和管理消息,所以就选择了 RabbitMQ。
Java的锁的优化
Java的锁优化,主要是指 synchronized 同步锁的优化。
在 JDK1.6 之前,synchronized 是直接调用 ObjectMonitor 的 enter 和 exit 实现的,这种锁也被称为重量级锁。这也是为什么很多声音说不要用 synchronized 的原因,有点“谈虎色变”的感觉。
从 JDK 1.6 开始,HotSpot 对 Java 中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,极大提升了 synchronized 的性能。
①、偏向锁:当一个线程首次获得锁时,JVM 会将锁标记为偏向这个线程,将锁的标志位设置为偏向模式,并且在对象头中记录下该线程的 ID。
之后,当相同的线程再次请求这个锁时,就无需进行额外的同步。如果另一个线程尝试获取这个锁,偏向模式会被撤销,并且锁会升级为轻量级锁。
②、轻量级锁:多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。
当一个线程尝试获取轻量级锁时,它会在自己的栈帧中创建一个锁记录(Lock Record),然后尝试使用 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针。
如果成功,该线程持有锁;如果失败,表示有其他线程竞争,锁会升级为重量级锁。
③、自旋锁:当线程尝试获取轻量级锁失败时,它会进行自旋,即循环检查锁是否可用,以避免立即进入阻塞状态。
自旋的次数不是固定的,而是根据之前在同一个锁上的自旋时间和锁的状态动态调整的。
④、锁粗化:如果 JVM 检测到一系列连续的锁操作实际上是在单一线程中完成的,则会将多个锁操作合并为一个更大范围的锁操作,这可以减少锁请求的次数。
锁粗化主要针对循环内连续加锁解锁的情况进行优化。
⑤、锁消除:JVM 的即时编译器(JIT)可以在运行时进行代码分析,如果发现某些锁操作不可能被多个线程同时访问,那么这些锁操作就会被完全消除。锁消除可以减少不必要的同步开销。
阻塞队列的实现方式
Java 中的队列主要通过 java.util.Queue 接口和 java.util.concurrent.BlockingQueue 两个接口来实现。
BlockingQueue 代表的是线程安全的队列,不仅可以由多个线程并发访问,还添加了等待/通知机制,以便在队列为空时阻塞获取元素的线程,直到队列变得可用,或者在队列满时阻塞插入元素的线程,直到队列变得可用。
阻塞队列(BlockingQueue)被广泛用于“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
BlockingQueue 接口的实现类有 ArrayBlockingQueue、DelayQueue、LinkedBlockingDeque、LinkedBlockingQueue、LinkedTransferQueue、PriorityBlockingQueue、SynchronousQueue 等。
阻塞指的是一种程序执行状态,其中某个线程在等待某个条件满足时暂停其执行(即阻塞),直到条件满足时恢复其执行。
就拿 ArrayBlockingQueue 来说,它是一个基于数组的有界阻塞队列,采用 ReentrantLock 锁来实现线程的互斥,而 ReentrantLock 底层采用的是 AQS 实现的队列同步,线程的阻塞调用 LockSupport.park 实现,唤醒调用 LockSupport.unpark 实现。
public void put(E e) throws InterruptedException {
checkNotNull(e);
// 使用ReentrantLock锁
final ReentrantLock lock = this.lock;
// 获取锁
lock.lockInterruptibly();
try {
// 如果队列已满,阻塞
while (count == items.length)
notFull.await();
// 插入元素
enqueue(e);
} finally {
// 释放锁
lock.unlock();
}
}
/**
* 插入元素
*/
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
// 插入元素后,通知消费者线程可以继续取元素
notEmpty.signal();
}
/**
* 获取元素
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 获取锁
lock.lockInterruptibly();
try {
// 如果队列为空,阻塞,等待生产者线程放入元素
while (count == 0)
notEmpty.await();
// 移除元素并返回
return dequeue();
} finally {
lock.unlock();
}
}
/**
* 移除元素并返回
*/
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
// 数组是循环队列,如果到达数组末尾,从头开始
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
// 移除元素后,通知生产者线程可以继续放入元素
notFull.signal();
return x;
}
实现线程的方式和区别
Java 中创建线程主要有三种方式,分别为继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。
第一种,继承 Thread 类,重写 run()
方法,调用 start()
方法启动线程。
class ThreadTask extends Thread {
public void run() {
System.out.println("看完二哥的 Java 进阶之路,上岸了!");
}
public static void main(String[] args) {
ThreadTask task = new ThreadTask();
task.start();
}
}
这种方法的缺点是,由于 Java 不支持多重继承,所以如果类已经继承了另一个类,就不能使用这种方法了。
第二种,实现 Runnable 接口,重写 run()
方法,然后创建 Thread 对象,将 Runnable 对象作为参数传递给 Thread 对象,调用 start()
方法启动线程。
class RunnableTask implements Runnable {
public void run() {
System.out.println("看完二哥的 Java 进阶之路,上岸了!");
}
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
Thread thread = new Thread(task);
thread.start();
}
}
这种方法的优点是可以避免 Java 的单继承限制,并且更符合面向对象的编程思想,因为 Runnable 接口将任务代码和线程控制的代码解耦了。
第三种,实现 Callable 接口,重写 call()
方法,然后创建 FutureTask 对象,参数为 Callable 对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,调用 start()
方法启动线程。
class CallableTask implements Callable<String> {
public String call() {
return "看完二哥的 Java 进阶之路,上岸了!";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTask task = new CallableTask();
FutureTask<String> futureTask = new FutureTask<>(task);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
这种方法的优点是可以获取线程的执行结果。
spring boot的自动装配
在 Spring 中,自动装配是指容器利用反射技术,根据 Bean 的类型、名称等自动注入所需的依赖。
在 Spring Boot 中,开启自动装配的注解是@EnableAutoConfiguration
。
Spring Boot 为了进一步简化,直接通过 @SpringBootApplication
注解一步搞定,这个注解包含了 @EnableAutoConfiguration
注解。
①、@EnableAutoConfiguration
只是一个简单的注解,但是它的背后却是一个非常复杂的自动装配机制,核心是AutoConfigurationImportSelector
类。
@AutoConfigurationPackage //将main同级的包下的所有组件注册到容器中
@Import({AutoConfigurationImportSelector.class}) //加载自动装配类 xxxAutoconfiguration
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
②、AutoConfigurationImportSelector
实现了ImportSelector
接口,这个接口的作用就是收集需要导入的配置类,配合@Import()
就将相应的类导入到 Spring 容器中。
③、获取注入类的方法是 selectImports()
,它实际调用的是getAutoConfigurationEntry()
,这个方法是获取自动装配类的关键。
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 检查自动配置是否启用。如果@ConditionalOnClass等条件注解使得自动配置不适用于当前环境,则返回一个空的配置条目。
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取启动类上的@EnableAutoConfiguration注解的属性,这可能包括对特定自动配置类的排除。
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 从spring.factories中获取所有候选的自动配置类。这是通过加载META-INF/spring.factories文件中对应的条目来实现的。
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 移除配置列表中的重复项,确保每个自动配置类只被考虑一次。
configurations = removeDuplicates(configurations);
// 根据注解属性解析出需要排除的自动配置类。
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查排除的类是否存在于候选配置中,如果存在,则抛出异常。
checkExcludedClasses(configurations, exclusions);
// 从候选配置中移除排除的类。
configurations.removeAll(exclusions);
// 应用过滤器进一步筛选自动配置类。过滤器可能基于条件注解如@ConditionalOnBean等来排除特定的配置类。
configurations = getConfigurationClassFilter().filter(configurations);
// 触发自动配置导入事件,允许监听器对自动配置过程进行干预。
fireAutoConfigurationImportEvents(configurations, exclusions);
// 创建并返回一个包含最终确定的自动配置类和排除的配置类的AutoConfigurationEntry对象。
return new AutoConfigurationEntry(configurations, exclusions);
}
Spring Boot 的自动装配原理依赖于 Spring 框架的依赖注入和条件注册,通过这种方式,Spring Boot 能够智能地配置 bean,并且只有当这些 bean 实际需要时才会被创建和配置。
参考链接
-
三分恶的面渣逆袭:https://javabetter.cn/sidebar/sanfene/nixi.html -
二哥的 Java 进阶之路:https://javabetter.cn
ending
一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 5000 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个编程学习指南 + Java 项目实战 + LeetCode 刷题的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。
两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远。
欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。