0%

Interview汇总

Interview Tech XMind

CS

32位和64位系统底层的本质区别是什么?

Java

2020年2月面试题100+大全(合适各级Java人员)

基础

通过new String(“test”) 与 “test” 与运行时常量池的关系

  • String a = “aaa”;(保存在常量池中的)
  • String b = new String(“aaa”);(new创建的对象会存放在堆内存中)

集合

ArrayList vs LinkedList vs SynchronizedList vs CopyOnWriteArrayList

ArrayList

ArrayList底层使用的是数组实现,也就是基于顺序表的原理,是线程不安全的。
默认初始化的时候,其内部的数组是一个静态的空数组:

1
2
3
4
5
6
7
8
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

也可以在初始化的时候指定默认容量,或者指定默认要承接的集合数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

当数组为空或者数组长度不够进行扩容的长度增长因子一般是1.5(但是要注意极端情况下,扩容到大于Integer.MAX_VALUE-8,会默认增长到ArrayList的最大长度Integer.MAX_VALUE,超过最大长度将抛出OutOfMemoryError异常):

1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList因为其扩容因子是1.5,其内部的数组会预留出一定的空间,所以从资源利用的角度来讲,这存在一定程度的浪费。
正因为是基于顺序表原来实现,ArrayList元素的物理存储地址是连续的,在其内部数组中间插入或者删除元素的话,其后面的所有元素都要进行移位,所以性能开销会相对较大。

LinkedList

LinkedList底层使用的是双链表来实现的,它默认初始化之后有着头尾两个空结点,同样也是线程不安全的。
可见空双链表插入第一个结点时,判断尾结点是否为空,如果为空是在first头结点插入,否则是插入到尾部结点之后。

LinkedList得益于双链表的数据结构优势,在对单个元素的插入和删除操作上,一般性能开销会相对较小,但是在单个元素的访问上,会表现较差。

快速失败迭代器(fail-fast Iterator)

ArrayList、LinkedList等很多常用集合的Iterator实现中,都是通过快速失败检查机制来检查数据是否在外部不被期望的情况下修改了,常见的快速失败检查都是通过如下代码:

1
2
3
4
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

异常时通过判断expectedModCount是否等于modCount,如果不相等就抛出异常。

  • expectedModCount:被期望的修改次数,在Iterator初始化的时候被赋值,int expectedModCount = modCount;,在调用迭代器的remove方法时会被更新。
  • modCount:真实的修改次数,每次调用add(),remove()方法(非迭代器的方法)会导致modCount+1。

迭代器在调用next()remove()都会做ConcurrentModificationException的异常检测。

所以,当集合是使用迭代器Iterator来遍历的时候,删除集合的元素记得用迭代器的remove()方法。

SynchronizedList(线程安全)

1
2
3
// 线程安全List的创建方式
List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>());
List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());

线程安全实现原理:

1
final Object mutex;     // Object on which to synchronize

其内部具有线程安全问题的方法都被互斥量加了锁:

1
2
3
4
5
6
7
8
9
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}

CopyOnWriteArrayList

CopyOnWriteArrayList和ArrayList类似,其内部也是基于数组实现的,但是还额外多出了一个可重入锁:

1
2
3
4
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

因为加了transient修饰,所以其数据不支持序列化。
另外其内部存储数据的数组是通过volatile修饰的,避免了线程安全的问题。
插入和删除操作也加了锁机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}

SynchronizedList是通过对读写方法使用synchronized修饰来实现同步的,即便只是多个线程在读数据,也不能进行,如果是读比较多的场景下,会性能不高,所以适合读写均匀的情况。
而CopyOnWriteArrayList是读写分离的,只对写操作加锁,但是每次写操作(添加和删除元素等)时都会复制出一个新数组,完成修改后,然后将新数组设置到旧数组的引用上,所以在写比较多的情况下,会有很大的性能开销,所以适合读比较多的应用场景。

数据结构

B+树和二叉树的区别

红黑树和跳表

Spring

一个接口太多事务操作是否要使用批量事务?

SpringBoot

SpringBoot VS Spring

SpringBoot自动装配原理

数据库

OLAP和OLTP

冗余字段

微服务架构加上前后端分离的情况下。后端接口很多时候会出现太多联表查询的情况。其实可以数据库存储空间大小允许的情况下,留出一些关键字段的冗余字段来代替联表查询。甚至可以将一些数据拆分出来查询封装成比较合适的数据JSON结构,然后可以将需要连表查询的逻辑放到前端去计算拼接,这样一来可以减少服务器和数据库的压力,同时前端做这部分的计算能力其实也是绰绰有余了,毕竟目前相对来说这是一个用户端性能过剩的时代。

尽可能多的冗余字段也可以为一定业务场景带来便捷的查询,没必要一味得遵循数据库三方式。

索引

必要时,时间字段也要上索引,索引一般是建立在字段值重复性比较少的,很多时候,时间字段基本不会重复,而且查询排序经常用到。

redis

redis如何清理过期key

redis内存不足时的策略

缓存更新的套路

https://coolshell.cn/articles/17416.html[](https://coolshell.cn/articles/17416.html “”)

Cache Aside Pattern

Read/Write Through Pattern

Write Behind Caching Pattern

微服务

Ribbon或者Nacos负载均衡有多少种实现机制?

轮询调用

注册中心

注册中心的作用是什么?

注册中心的实现机制

  • 基于心跳包在注册中心与各个微服务之间保持通讯

Eureka VS Nacos

Combo

后端架构

每一个单一表的对应Service类里面有且只能有一个Mapper,这也遵守了类的单一责任原则。

微服务架构的好处

  • 解耦:系统之间业务的解耦,代码的维护和扩展比较方面;
  • 影响面会更小:降低系统的依赖风险,微服务之后,如果某个单一模块业务代码出现问题,其影响可以控制在相对来说较小的范围;
  • 三高:高并发、高流量、高可用性;扩容的时候可以针对某些处理业务比较繁忙的微服务模块进行水平扩容。

序列化

类似DO、DTO、VO这种有可能要做传输的类一定不要忘记实现Serializable,和唯一标识化serialVersionUID。

COW(写时复制/Copy-on-write)

Copy-on-write在对数据进行修改的时候,不会直接在原来的数据位置上进行操作,而是重新找个位置修改,这样的好处是一旦系统故障,能保证数据的完整性,容易恢复。

  • 比如说:要修改数据块A的内容,先把A读出来,写到B块里面去。如果这时候断电了,原来A的内容还在!

各种问题模拟回答

描述一下什么是IO/NIO,以及它的原理和各种主流技术之间的关系?

IO是Input/Output的简写,然后NIO是New IO的简写,简单来说IO就是我的代码程序去跟计算机的硬件资源做读写交互的一个过程,比如与内存、CPU和硬盘之间的输入和输出交互,与Java相关最常见的就是Socket通讯、文件流的读写、输入流输出流这一些,在Linux系统上可以通过strace命令去跟踪IO流的整一个详细的过程,早期的IO其实存在着一些明显弊端,对计算机的硬件资源利用不够充分,比较浪费CPU和内存资源,所以后面通过技术的改进才有个NIO。
NIO最关键的地方就是基于Linux的epoll对IO做了一个很大的改进,epoll更充分发挥硬件资源,尽量不浪费CPU和内存资源,现在主流的Redis、Nginx和消息队列Kafka这些其实底层都是基于NIO在计算机的一些端口去做网络请求的监听的。