前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >输了!广州某小厂一面,也凉了

输了!广州某小厂一面,也凉了

作者头像
小林coding
发布2024-05-28 12:29:09
1550
发布2024-05-28 12:29:09
举报
文章被收录于专栏:小林coding小林coding

图解学习网站:https://xiaolincoding.com

大家好,我是小林。

之前「后端面经」系列已经分享过北京、深圳、上海等地方的小厂面经,有同学想看看广州的。

那么今天来分享一个广州小厂的Java后端面经,面试问了 40 分钟,除了拷打项目的问题,还问了一些技术面试题。

不算难,可惜同学一些问题没有回答好,最后还是挂了,其实面试官人很好,整个交流也比较舒服,是自己太菜了。

考察的范围:Java 基础、Java 集合、Java 并发、Spring、Linux 这些知识点。无算法,MySQL 、 Redis 、网络、系统也都没有问。

面试八股

Arraylist和LinkedList区别

  • 底层数据结构:ArrayList使用数组作为底层数据结构,而LinkedList使用双向链表作为底层数据结构
  • 随机访问性能:ArrayList支持通过索引直接访问元素,因为底层数组的连续存储特性,所以时间复杂度为O(1)。而LinkedList需要从头或尾部开始遍历链表,时间复杂度为O(n)。
  • 插入和删除操作:ArrayList在尾部插入和删除元素的时间复杂度为O(1),因为它只需要调整数组的长度即可。但在中间或头部插入和删除元素时,需要将后续元素进行移动,时间复杂度为O(n)。而LinkedList在任意位置插入和删除元素的时间复杂度为O(1),因为只需要调整节点的指针即可。
  • 内存占用:ArrayList在每个元素中都存储了实际的数据,而LinkedList在每个节点中存储了数据和前后节点的指针。因此,相同数量的元素情况下,LinkedList通常比ArrayList占用更多的内存空间。

ArrayList的扩容机制说一下

ArrayList在添加元素时,如果当前元素个数已经达到了内部数组的容量上限,就会触发扩容操作。ArrayList的扩容操作主要包括以下几个步骤:

  • 计算新的容量:一般情况下,新的容量会扩大为原容量的1.5倍(在JDK 10之后,扩容策略做了调整),然后检查是否超过了最大容量限制。
  • 创建新的数组:根据计算得到的新容量,创建一个新的更大的数组。
  • 将元素复制:将原来数组中的元素逐个复制到新数组中。
  • 更新引用:将ArrayList内部指向原数组的引用指向新数组。
  • 完成扩容:扩容完成后,可以继续添加新元素。

ArrayList的扩容操作涉及到数组的复制和内存的重新分配,所以在频繁添加大量元素时,扩容操作可能会影响性能。为了减少扩容带来的性能损耗,可以在初始化ArrayList时预分配足够大的容量,避免频繁触发扩容操作。

之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。

代码语言:javascript
复制
// 新容量计算
int newCapacity = oldCapacity + (oldCapacity >> 1);

线程安全的 List, CopyonWriteArraylist是如何实现线程安全的

CopyOnWriteArrayList底层也是通过一个数组保存数据,使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到。

代码语言:javascript
复制
private transient volatile Object[] array;

在写入操作时,加了一把互斥锁ReentrantLock以保证线程安全。

代码语言:javascript
复制
public boolean add(E e) {
    //获取锁
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
        //获取到当前List集合保存数据的数组
        Object[] elements = getArray();
        //获取该数组的长度(这是一个伏笔,同时len也是新数组的最后一个元素的索引值)
        int len = elements.length;
        //将当前数组拷贝一份的同时,让其长度加1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //将加入的元素放在新数组最后一位,len不是旧数组长度吗,为什么现在用它当成新数组的最后一个元素的下标?建议自行画图推演,就很容易理解。
        newElements[len] = e;
        //替换引用,将数组的引用指向给新数组的地址
        setArray(newElements);
        return true;
    } finally {
        //释放锁
        lock.unlock();
    }
}

看到源码可以知道写入新元素时,首先会先将原来的数组拷贝一份并且让原来数组的长度+1后就得到了一个新数组,新数组里的元素和旧数组的元素一样并且长度比旧数组多一个长度,然后将新加入的元素放置都在新数组最后一个位置后,用新数组的地址替换掉老数组的地址就能得到最新的数据了。

在我们执行替换地址操作之前,读取的是老数组的数据,数据是有效数据;执行替换地址操作之后,读取的是新数组的数据,同样也是有效数据,而且使用该方式能比读写都加锁要更加的效率。

现在我们来看读操作,读是没有加锁的,所以读是一直都能读

代码语言:javascript
复制
public E get(int index) {
    return get(getArray(), index);
}

Stream流的并行API是什么?

是 ParallelStream。

并行流(ParallelStream)就是将源数据分为多个子流对象进行多线程操作,然后将处理的结果再汇总为一个流对象,底层是使用通用的 fork/join 池来实现,即将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果

Stream串行流与并行流的主要区别:

对CPU密集型的任务来说,并行流使用ForkJoinPool线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用ParallelStream并不是很好的选择。

底层使用线程池的核心线程数

ParallelStream 底层使用了 ForkJoinPool 线程池。

ForkJoinPool 与 ThreadPoolExecutor 是完全不同的实现机制。ForkJoinPool 有四个参数可以设置:

代码语言:javascript
复制
public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

但其实关键参数也只有一个线程数 parallelism(并行度,默认为CPU数,最小为1),其它参数影响不大。ForkJoinPool 机制特殊,每个线程都有自己独立的队列,并且没有核心线程和最大线程的概念。ForkJoinPool 线程数的设置很简单,分 cpu 密集型任务和 io 密集型任务两种情况考虑。对于 cpu 密集型任务,建议都用静态的 commonPool(线程数为 CPU 核心数 - 1,JDK 已经设置好了)原因是,如果 new 出多各 ForkJoinPool:

  • 浪费内存:ForkJoinPool 内存占用与线程数成正比,而且还挺大,可以计算,一个队列一个 ForkJoinTask 数组,数组初始容量就有 2^13,存储的是 ForkJoinTask 引用,一个对象引用占用 4 字节,那么一个队列占用 2^13 * 4 + 8 Byte 约等于 32 KB,如果设置了 16 线程,那么会有 32 个队列(大于等于线程数的最小 2 的 n 次幂乘 2),也就是 1M,多一个 16 线程的 ForkJoinPool 仅队列自身,一个任务都没有就多占用 1M 内存,再加上拆分出一堆子任务占用内存更多;
  • 影响性能:多占用内存,不仅没有性能提升,反而可能有性能损耗,cpu 密集型任务线程数最多也就 cpu 核心数,再多除了增加线程上下文切换次数没什么意义。

对于 io 密集型任务,线程数设置逻辑与 ThreadPoolExecutor 类似,队列容量无限大不可设置,在此基础上寻找合适的线程数即可。

CPU密集型和IO密集型 N+ 1 2N 具体是如何计算的?

N代表的是 CPU核数

  • CPU密集型:corePoolSize = CPU核数 + 1
  • IO密集型:corePoolSize = CPU核数 * 2

在《Java并发编程实践》中,是这样来计算线程池的线程数目的:

这种计算方式,我们需要知道上面定义的几个数值,才能计算出来线程池需要设置的线程数。其中,CPU数量是确定的,CPU使用率是目标值也是确定的,W/C也是可以通过基准程序测试得出的。

对于计算密集型应用,假定等待时间趋近于0,是的CPU利用率达到100%,那么线程数就是CPU核心数,那这个+1意义何在呢?《Java并发编程实践》这么说:

计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。

所以 N+1 是一个经验值。

那么对于对于IO密集型应用,假定所有的操作时间几乎都是IO操作耗时,那么W/C的值就为1,那么对应的线程数确实为2N。

说一下你对 spring的IOC 的理解

IOC:Inversion Of Control,即控制反转,是一种设计思想。在传统的 Java SE 程序设计中,我们直接在对象内部通过 new 的方式来创建对象,是程序主动创建依赖对象;

而在Spring程序设计中,IOC 是有专门的容器去控制对象。

所谓控制就是对象的创建、初始化、销毁。

  • 创建对象:原来是 new 一个,现在是由 Spring 容器创建。
  • 初始化对象:原来是对象自己通过构造器或者 setter 方法给依赖的对象赋值,现在是由 Spring 容器自动注入。
  • 销毁对象:原来是直接给对象赋值 null 或做一些销毁操作,现在是 Spring 容器管理生命周期负责销毁对象。

总结:IOC 解决了繁琐的对象生命周期的操作,解耦了我们的代码。所谓反转:其实是反转的控制权,前面提到是由 Spring 来控制对象的生命周期,那么对象的控制就完全脱离了我们的控制,控制权交给了 Spring 。这个反转是指:我们由对象的控制者变成了 IOC 的被动控制者。

spring 三级缓存是什么?

三级缓存主要是为了解决单例模式下的循环依赖的问题。

循环依赖指的是两个类中的属性相互依赖对方:例如 A 类中有 B 属性,B 类中有 A属性,从而形成了一个依赖闭环,如下图。

循环依赖问题在Spring中主要有三种情况:

  • 第一种:通过构造方法进行依赖注入时产生的循环依赖问题。
  • 第二种:通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
  • 第三种:通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

只有【第三种方式】的循环依赖问题被 Spring 解决了,其他两种方式在遇到循环依赖问题时,Spring都会产生异常。

Spring 解决单例模式下的setter循环依赖问题的主要方式是通过三级缓存解决循环依赖。三级缓存指的是 Spring 在创建 Bean 的过程中,通过三级缓存(缓存的底层都是Map)来缓存正在创建的 Bean,以及已经创建完成的 Bean 实例。具体步骤如下:

  • 实例化 Bean:Spring 在实例化 Bean 时,会先创建一个空的 Bean 对象,并将其放入一级缓存中。
  • 属性赋值:Spring 开始对 Bean 进行属性赋值,如果发现循环依赖,会将当前 Bean 对象提前暴露给后续需要依赖的 Bean(通过提前暴露的方式解决循环依赖)。
  • 初始化 Bean:完成属性赋值后,Spring 将 Bean 进行初始化,并将其放入二级缓存中。
  • 注入依赖:Spring 继续对 Bean 进行依赖注入,如果发现循环依赖,会从二级缓存中获取已经完成初始化的 Bean 实例。

通过三级缓存的机制,Spring 能够在处理循环依赖时,确保及时暴露正在创建的 Bean 对象,并能够正确地注入已经初始化的 Bean 实例,从而解决循环依赖问题,保证应用程序的正常运行。

三级缓存都是CurrentHashMap吗?

不是,只有一级用了ConcurrentHashMap,二级和三级用的是HashMap。

Spring利用三级Bean缓存的方式解决Bean循环依赖的问题,三级缓存也就是DefaultSingletonBeanRegistry中的三个Map:

代码语言:javascript
复制
/** Cache of singleton objects: bean name to bean instance. 一级缓存*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
 
/** Cache of early singleton objects: bean name to bean instance. 二级缓存*/
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
 
/** Cache of singleton factories: bean name to ObjectFactory. 三级缓存*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
  • singletonObjects:一级缓存,存放完整的Bean;
  • earlySingletonObjects:二级缓存,存放早期Bean,即还没有设置属性的Bean;
  • singletonFactories:三级缓存,存放ObjectFactory对象,即Bean的生产工厂。

但是在 spring 2.7.10新版本开始,earlySingletonObjects(二级缓存)改成使用 ConcurrentHashMap。

代码语言:javascript
复制
/** Cache of singleton objects: bean name to bean instance. 一级缓存 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of early singleton objects: bean name to bean instance. 二级缓存 */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

/** Cache of singleton factories: bean name to ObjectFactory. 三级缓存 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

Linux的常用命令了解过哪些?

  • 文件相关(mv mkdir cd ls)
  • 进程相关( ps top netstate )
  • 权限相关(chmod chown useradd groupadd)
  • 网络相关(netstat ip addr)
  • 测试相关(测试网络连通性:ping 测试端口连通性:telnet)

查找日志中某个字符的长度?

要在 Linux 中查找日志文件中某个字符的长度,你可以使用一些工具和命令来实现。其中,grep 是一个强大的命令行工具,可以用于在文件中查找匹配指定模式的行。你也可以结合一些其他命令来完成这个任务,如 awk 或者 sed。以下是一个示例命令,用于查找日志文件中某个字符的长度:

代码语言:javascript
复制
grep "search_string" log_file | awk '{ print length }'

在这个命令中:

  1. grep "search_string" log_file 会找到日志文件中包含 "search_string" 的所有行。
  2. awk '{ print length }' 会输出每一行的长度,即搜索到的 "search_string" 的长度。

如果你想查找日志文件中单个字符的长度,你可以直接使用 grep 命令配合 wc 命令来实现:

代码语言:javascript
复制
grep "c" log_file | wc -m

这个命令会计算搜索到的字符 "c" 在日志文件中出现的次数。

深拷贝和浅拷贝区别是什么?

image.png

  • 浅拷贝是指只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。换句话说,浅拷贝只是创建一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是将引用复制到新对象中,两个对象指向的是同一个引用对象。
  • 深拷贝是指在复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。换句话说,深拷贝会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对象。

实现深拷贝的三种方法是什么?

在 Java 中,实现对象深拷贝的方法有以下几种主要方式:

实现 Cloneable 接口并重写 clone() 方法

这种方法要求对象及其所有引用类型字段都实现 Cloneable 接口,并且重写 clone() 方法。在 clone() 方法中,通过递归克隆引用类型字段来实现深拷贝。

代码语言:javascript
复制
class MyClass implements Cloneable {
    private String field1;
    private NestedClass nestedObject;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        MyClass cloned = (MyClass) super.clone();
        cloned.nestedObject = (NestedClass) nestedObject.clone(); // 深拷贝内部的引用对象
        return cloned;
    }
}

class NestedClass implements Cloneable {
    private int nestedField;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

使用序列化和反序列化

通过将对象序列化为字节流,再从字节流反序列化为对象来实现深拷贝。要求对象及其所有引用类型字段都实现 Serializable 接口。

代码语言:javascript
复制
import java.io.*;

class MyClass implements Serializable {
    private String field1;
    private NestedClass nestedObject;
    
    public MyClass deepCopy() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            oos.flush();
            oos.close();
            
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (MyClass) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

class NestedClass implements Serializable {
    private int nestedField;
}

手动递归复制

针对特定对象结构,手动递归复制对象及其引用类型字段。适用于对象结构复杂度不高的情况。

代码语言:javascript
复制
class MyClass {
    private String field1;
    private NestedClass nestedObject;

    public MyClass deepCopy() {
        MyClass copy = new MyClass();
        copy.setField1(this.field1);
        copy.setNestedObject(this.nestedObject.deepCopy());
        return copy;
    }
}

class NestedClass {
    private int nestedField;

    public NestedClass deepCopy() {
        NestedClass copy = new NestedClass();
        copy.setNestedField(this.nestedField);
        return copy;
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-05-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小林coding 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 面试八股
    • Arraylist和LinkedList区别
      • ArrayList的扩容机制说一下
        • 线程安全的 List, CopyonWriteArraylist是如何实现线程安全的
          • Stream流的并行API是什么?
            • 底层使用线程池的核心线程数
              • CPU密集型和IO密集型 N+ 1 2N 具体是如何计算的?
                • 说一下你对 spring的IOC 的理解
                  • spring 三级缓存是什么?
                    • 三级缓存都是CurrentHashMap吗?
                      • Linux的常用命令了解过哪些?
                        • 查找日志中某个字符的长度?
                          • 深拷贝和浅拷贝区别是什么?
                            • 实现深拷贝的三种方法是什么?
                            相关产品与服务
                            容器服务
                            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档