图解学习网站:https://xiaolincoding.com
大家好,我是小林。
之前「后端面经」系列已经分享过北京、深圳、上海等地方的小厂面经,有同学想看看广州的。
那么今天来分享一个广州小厂的Java后端面经,面试问了 40 分钟,除了拷打项目的问题,还问了一些技术面试题。
不算难,可惜同学一些问题没有回答好,最后还是挂了,其实面试官人很好,整个交流也比较舒服,是自己太菜了。
考察的范围:Java 基础、Java 集合、Java 并发、Spring、Linux 这些知识点。无算法,MySQL 、 Redis 、网络、系统也都没有问。
ArrayList在添加元素时,如果当前元素个数已经达到了内部数组的容量上限,就会触发扩容操作。ArrayList的扩容操作主要包括以下几个步骤:
ArrayList的扩容操作涉及到数组的复制和内存的重新分配,所以在频繁添加大量元素时,扩容操作可能会影响性能。为了减少扩容带来的性能损耗,可以在初始化ArrayList时预分配足够大的容量,避免频繁触发扩容操作。
之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。
// 新容量计算
int newCapacity = oldCapacity + (oldCapacity >> 1);
CopyOnWriteArrayList底层也是通过一个数组保存数据,使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到。
private transient volatile Object[] array;
在写入操作时,加了一把互斥锁ReentrantLock以保证线程安全。
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后就得到了一个新数组,新数组里的元素和旧数组的元素一样并且长度比旧数组多一个长度,然后将新加入的元素放置都在新数组最后一个位置后,用新数组的地址替换掉老数组的地址就能得到最新的数据了。
在我们执行替换地址操作之前,读取的是老数组的数据,数据是有效数据;执行替换地址操作之后,读取的是新数组的数据,同样也是有效数据,而且使用该方式能比读写都加锁要更加的效率。
现在我们来看读操作,读是没有加锁的,所以读是一直都能读
public E get(int index) {
return get(getArray(), index);
}
是 ParallelStream。
并行流(ParallelStream)就是将源数据分为多个子流对象进行多线程操作,然后将处理的结果再汇总为一个流对象,底层是使用通用的 fork/join 池来实现,即将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果
Stream串行流与并行流的主要区别:
对CPU密集型的任务来说,并行流使用ForkJoinPool线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用ParallelStream并不是很好的选择。
ParallelStream 底层使用了 ForkJoinPool 线程池。
ForkJoinPool 与 ThreadPoolExecutor 是完全不同的实现机制。ForkJoinPool 有四个参数可以设置:
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:
对于 io 密集型任务,线程数设置逻辑与 ThreadPoolExecutor 类似,队列容量无限大不可设置,在此基础上寻找合适的线程数即可。
N代表的是 CPU核数
在《Java并发编程实践》中,是这样来计算线程池的线程数目的:
这种计算方式,我们需要知道上面定义的几个数值,才能计算出来线程池需要设置的线程数。其中,CPU数量是确定的,CPU使用率是目标值也是确定的,W/C也是可以通过基准程序测试得出的。
对于计算密集型应用,假定等待时间趋近于0,是的CPU利用率达到100%,那么线程数就是CPU核心数,那这个+1意义何在呢?《Java并发编程实践》这么说:
计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。
所以 N+1 是一个经验值。
那么对于对于IO密集型应用,假定所有的操作时间几乎都是IO操作耗时,那么W/C的值就为1,那么对应的线程数确实为2N。
IOC:Inversion Of Control,即控制反转,是一种设计思想。在传统的 Java SE 程序设计中,我们直接在对象内部通过 new 的方式来创建对象,是程序主动创建依赖对象;
而在Spring程序设计中,IOC 是有专门的容器去控制对象。
所谓控制就是对象的创建、初始化、销毁。
总结:IOC 解决了繁琐的对象生命周期的操作,解耦了我们的代码。所谓反转:其实是反转的控制权,前面提到是由 Spring 来控制对象的生命周期,那么对象的控制就完全脱离了我们的控制,控制权交给了 Spring 。这个反转是指:我们由对象的控制者变成了 IOC 的被动控制者。
三级缓存主要是为了解决单例模式下的循环依赖的问题。
循环依赖指的是两个类中的属性相互依赖对方:例如 A 类中有 B 属性,B 类中有 A属性,从而形成了一个依赖闭环,如下图。
循环依赖问题在Spring中主要有三种情况:
只有【第三种方式】的循环依赖问题被 Spring 解决了,其他两种方式在遇到循环依赖问题时,Spring都会产生异常。
Spring 解决单例模式下的setter循环依赖问题的主要方式是通过三级缓存解决循环依赖。三级缓存指的是 Spring 在创建 Bean 的过程中,通过三级缓存(缓存的底层都是Map)来缓存正在创建的 Bean,以及已经创建完成的 Bean 实例。具体步骤如下:
通过三级缓存的机制,Spring 能够在处理循环依赖时,确保及时暴露正在创建的 Bean 对象,并能够正确地注入已经初始化的 Bean 实例,从而解决循环依赖问题,保证应用程序的正常运行。
不是,只有一级用了ConcurrentHashMap,二级和三级用的是HashMap。
Spring利用三级Bean缓存的方式解决Bean循环依赖的问题,三级缓存也就是DefaultSingletonBeanRegistry中的三个Map:
/** 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);
但是在 spring 2.7.10新版本开始,earlySingletonObjects(二级缓存)改成使用 ConcurrentHashMap。
/** 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 中查找日志文件中某个字符的长度,你可以使用一些工具和命令来实现。其中,grep 是一个强大的命令行工具,可以用于在文件中查找匹配指定模式的行。你也可以结合一些其他命令来完成这个任务,如 awk 或者 sed。以下是一个示例命令,用于查找日志文件中某个字符的长度:
grep "search_string" log_file | awk '{ print length }'
在这个命令中:
如果你想查找日志文件中单个字符的长度,你可以直接使用 grep 命令配合 wc 命令来实现:
grep "c" log_file | wc -m
这个命令会计算搜索到的字符 "c" 在日志文件中出现的次数。
image.png
在 Java 中,实现对象深拷贝的方法有以下几种主要方式:
实现 Cloneable 接口并重写 clone() 方法
这种方法要求对象及其所有引用类型字段都实现 Cloneable 接口,并且重写 clone() 方法。在 clone() 方法中,通过递归克隆引用类型字段来实现深拷贝。
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 接口。
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;
}
手动递归复制
针对特定对象结构,手动递归复制对象及其引用类型字段。适用于对象结构复杂度不高的情况。
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;
}
}