前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >年少不知编制香,错把技术当成宝。

年少不知编制香,错把技术当成宝。

作者头像
沉默王二
发布2024-04-19 15:24:06
640
发布2024-04-19 15:24:06
举报
文章被收录于专栏:沉默王二沉默王二

大家好,我是二哥呀。

周五晚上线下见了一名读者,和他的交集也有好多年了。21年就专门写过一篇关于他的帖,老读者应该有印象,当时阅读不低,内容也很干。

后来他又拿到一家互联网中厂的offer,薪资待遇给的不低;但抱着试一试的心态,又阴差阳错的考上了烟草局,于是就放弃了互联网转到了烟草局。

他现在的心态就和转码的时候完全不同,更加放松,对自己的人生也有了新的思考,关键是烟草局的工作实在是太舒服了,更关键的是薪资待遇也很不错。

具体我没办法透露,这个必须得保密,大家应该能理解,烟草局对这方面要求的比较严苛。

期间聊了很多,也很投机,他的一句话令我印象深刻:“二哥你真的是浑身散发着光芒却不刺眼,和你聊天真的太舒服了,下次我们还约。”

从他那里我也学到了很多,了解到了很多,后面在条件允许的情况下我再给大家仔细聊一下。

对于 25 届准备秋招或者 24 届还在春招中煎熬的同学来说,真的可以换一种活法了,如果自己对大厂的高薪,以及高强度的工作压力不是很心动,那不妨试试国企、银行。

国企和银行的面试难度真的比互联网大厂低太多了,我们仍然以《Java 面试指南-中国农业银行面经》中同学 1 的面试内容为例,来看看农行的面试官都喜欢问哪些问题。

看完这个面试题,你可能会感觉这也太简单了吧?

除了项目,仍然是围绕着二哥一直给大家强调的 Java 后端四大件展开,并且难度不大。内容较长,建议大家先收藏起来,面试的时候大概率会碰到,我会尽量用通俗易懂+手绘图的方式,让天下所有的面渣都能逆袭 ?

农行面经

挑个项目讲一下,做了多久,做了啥

我主要做了两个项目,一个是社区项目技术派,一个是轮子项目 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 同步锁的优化。

三分恶面渣逆袭:Mark Word变化

在 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 实现。

代码语言:javascript
复制
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()方法启动线程。

代码语言:javascript
复制
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() 方法启动线程。

代码语言:javascript
复制
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() 方法启动线程。

代码语言:javascript
复制
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

二哥的 Java 进阶之路

Spring Boot 为了进一步简化,直接通过 @SpringBootApplication 注解一步搞定,这个注解包含了 @EnableAutoConfiguration 注解。

二哥的 Java 进阶之路

①、@EnableAutoConfiguration 只是一个简单的注解,但是它的背后却是一个非常复杂的自动装配机制,核心是AutoConfigurationImportSelector 类。

代码语言:javascript
复制
@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 容器中。

二哥的 Java 进阶之路

③、获取注入类的方法是 selectImports(),它实际调用的是getAutoConfigurationEntry(),这个方法是获取自动装配类的关键。

代码语言:javascript
复制
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 实际需要时才会被创建和配置。

三分恶面渣逆袭:SpringBoot自动配置原理

参考链接

  • 三分恶的面渣逆袭:https://javabetter.cn/sidebar/sanfene/nixi.html
  • 二哥的 Java 进阶之路:https://javabetter.cn

ending

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-14,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 沉默王二 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 农行面经
    • 挑个项目讲一下,做了多久,做了啥
      • Java的锁的优化
        • 阻塞队列的实现方式
          • 实现线程的方式和区别
            • spring boot的自动装配
            • 参考链接
            • ending
            相关产品与服务
            云数据库 Redis
            腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com