Java 面试知识点

172 阅读10分钟

集合框架

jdk-1.8.0_211

List

允许重复元素(包括 null)、支持顺序存储的集合

ArrayList

基于数组实现、可自动扩容非线程安全List

原理

  • 主要属性
    • transient Object[] elementData;元素数组
    • private int size;元素数量
  • 自动扩容
    1. 每次添加元素时,判断 elementData 长度是否足够
      ensureCapacityInternal(size + 1);
    2. 若长度不够,则执行扩容
      if (minCapacity - elementData.length > 0) grow(minCapacity);
    3. 非临界情况下,新数组的容量为原数组容量的 1.5 倍
      int newCapacity = oldCapacity + (oldCapacity >> 1);
    4. 复制原数组中的元素,并替换原数组
      elementData = Arrays.copyOf(elementData, newCapacity);

优点

  • 实现了 RandomAccess 接口,即支持快速随机访问 (for 循环比 Iterator 快)
  • 不考虑扩容的情况下,添加元素效率非常高(非插入元素)

缺点

  • 保存元素的数组 (elementData) 可能会占用额外的空闲空间
  • 扩容时,需要全量复制,所以对于所需容量可预测的场景,最好初始化时指定容量大小
  • 指定位置插入/删除元素时,需要整体移动元素 (向后/向前),效率较低

适用场景

  • 随机访问较多的场景
  • 所需容量可预测的场景
  • 不需要或很少在指定位置插入/删除元素的场景

LinkedList

基于双向链表实现、非线程安全List

原理

  • 主要属性
    • transient int size = 0; 元素数量
    • transient Node<E> first; 头节点
    • transient Node<E> last; 尾节点
  • 查找指定位置的元素
    如果索引小于 size / 2,从头节点开始遍历,否则从尾节点开始遍历

优点

  • 无需扩容
  • 实现了 Deque 接口,可当作队列、栈来使用
    • 队列: offer - 入队,poll - 出队
    • 栈: push - 入栈,pop - 出栈
  • 指定位置插入/删除元素时,只需要修改节点指针即可,无需移动元素

缺点

  • 不支持快速随机访问
  • 每个节点都需要浪费一定的空间来存放前后节点的指针

适用场景

  • 随机访问较少的场景
  • 频繁插入/删除的场景
  • 需要使用队列、栈的场景

CopyOnWriteArrayList

基于数组 + 读写分离思想实现、线程安全List

原理

  • 主要属性
    • final transient ReentrantLock lock = new ReentrantLock(); 写锁
    • private transient volatile Object[] array; 元素数组
  • 写入时复制
    1. 添加、修改、删除元素时,先获取写锁
    2. 将元素数组复制成新的数组,在新的数组上执行操作
    3. 用新数组替换原数组
    4. 释放锁

优点

  • 实现了 RandomAccess 接口,即支持快速随机访问 (for 循环比 Iterator 快)
  • 支持了线程安全的情况下,保证了读的效率

缺点

  • 无法保证读取的数据一定是最新的(弱一致性
  • 每次添加、修改、删除元素时,都需要全量拷贝数组(写性能低

适用场景

  • 需要保证线程安全,且对数据一致性要求不是很高的场景
  • 多读少写的场景

Set

不允许重复元素的集合

Java 中的 Set 一般都是基于对应的 Map 实现,如 HashSet 是基于 HashMap 实现,通过将元素作为 Mapkey 来实现元素不重复。并且可以通过 Collections.newSetFromMap 将一个 Map 转换为 Set

HashSet

基于 HashMap 实现,内部维护一个 HashMap 对象

LinkedHashSet

保存了插入顺序的 HashSet,原理参考 LinkedHashMap

继承自 HashSet,初始化时调用特殊的构造函数

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
  map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

TreeSet

基于 TreeMap 实现,内部维护一个 NavigableMap 对象

CopyOnWriteArraySet

原理、优缺点和适用场景与 CopyOnWriteArrayList 相同,区别是,CopyOnWriteArraySet 不允许重复元素,在添加、插入元素时需要判断元素是否存在

Map

HashMap

基于数组 + 链表/红黑树实现,使用链地址法解决 hash 冲突、key、value允许为空不保证顺序非线程安全的键值对集合

原理

  • 主要属性
    • transient Node<K,V>[] table; Hash 数组
    • transient Set<Map.Entry<K,V>> entrySet; 键值对缓存,方便 keySetvalues 方法
    • transient int size; 键值对数量
    • int threshold; 扩容的阈值,一般等于 capacity * loadFactor
    • final float loadFactor; 扩容的加载因子,默认 0.75f
  • hash 算法
    • hash 扰动
      static final int hash(Object key) {
        int h;
        // 低 16 位与高 16 异或,减少 hash 碰撞的概率
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
      
    • 位置计算
      i = (n - 1) & hash
      高效取余操作,前提 `n` 是 `2` 的次幂  
      若 n = 16; hash = 21
      n - 1   00001111
       hash   00010101 & 
      index   00000101 = 5
      
  • 链表树化
    • 键值对的数量小于 MIN_TREEIFY_CAPACITY(64) 的情况下,会进行扩容,而不是树化
    • 链表长度不小于 TREEIFY_THRESHOLD(8) 时,树化为红黑树
    • 红黑树节点数量不大于 UNTREEIFY_THRESHOLD(6) 时,退化为链表
  • 自动扩容
    • 键值对的数量大于 threshold 时,执行扩容
    • 创建新的 Hash 数组,非临界情况下,新数组的长度为原数组长度的两倍
      newCap = oldCap << 1;
    • 将原数组中的键值对复制到新数组中,如果 (e.hash & oldCap) == 0 则分配到低位(原位置),否则分配到高位(原位置+原容量)
      无需 rehash 的分配算法
      若 oldCap = 16, newCap = 32, hash1 = 5, hash2 = 21,原位置都为 5
         oldCap 00010000     | 0010000
          hash1 00000101 &   | 0010101
                00000000 = 0 | 0010000 != 0
                低位 5       | 高位 5 + 16 = 21
      

优点

存取效率相对都较高,在不需要排序的情况下,都推荐使用

缺点

无重大缺陷

适用场景

大部分存储键值对的场景

LinkedHashMap

保存了插入顺序的 HashMap,继承自 HashMap,通过在节点中 beforeafter 两个指针保存插入顺序

transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;

static class Entry<K,V> extends HashMap.Node<K,V> {
  Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
      super(hash, key, value, next);
    }
  }

TreeMap

基于红黑树实现,自动排序非线程安全Map

原理

  • 主要属性
    • private final Comparator<? super K> comparator; 排序比较器,若为 null,则按 key 自然排序
    • private transient Entry<K,V> root; 红黑树的根节点
    • private transient int size = 0; 键值对数量

优点

  • 可指定元素排序方式(Comparator
  • 可按 key 自然排序 (Comparable),此时 key 不允许为 null

缺点

性能较 HashMap 差,在不需要排序的场景,建议使用 HashMap

适用场景

存储需要排序的键值对的场景

ConcurrentHashMap

原理

优点

缺点

适用场景

Queue

并发编程

网络编程

JVM

MySQL

常用框架

Spring

Mybatis

Netty

中间件

Zookeeper

Redis

Kafka

高并发解决方案

分布式解决方案

系统架构

RPC 架构设计

RPC 框架示意图

RPC 主要角色

  • 服务消费者:远程方法的调用方,即客户端,一个服务既可以是消费者也可以时提供者
  • 服务提供者:远程方法的提供方,即服务端,一个服务既可以是消费者也可以时提供者
  • 注册中心:保存服务提供者的服务地址等信息
  • 监控运维(可选) :监控接口的响应时间、统计请求数量等,及时发现系统问题并发出告警通知

RPC-3.png RPC 调用过程:

  1. 客户端处理过程中调用 Client Stub (本地方法),传递参数
  2. Client Stub 将类信息、方法信息、参数序列化为消息,通过 Socket 发送给服务端
  3. 服务端收到数据包后,Server Stub 将消息反序列化为类信息、方法信息、参数
  4. Server Stub 调用对应的本地方法,并将执行结果返回给客户端

注册中心

目前较成熟的注册中心有 Zookeeper,Nacos,Consul,Eureka 等,其主要比较如下:

ZookeeperNacosConsulEureka
一致性协议 CPCP + APCPAP
雪崩保护  无     有     无    有    
多数据中心 不支持   支持    支持   支持   
自动注销实例支持    支持    支持   支持   

负载均衡算法

随机、轮询、hash 和一致性 hash

通信框架

Netty 是一个高性能事件驱动型的非阻塞的 IO(NIO) 框架,非常适合作为 RPC 框架的通信框架

通信协议

TPC 通信过程中,会根据 TCP 缓存区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。所以需要对发送的数据包封装到一种通信协议里

业界的主流协议的解决方案可以归纳如下

  1. 消息定长,例如每个报文的大小固定为固定长度 100 字节,如果不够用空格补足
  2. 在包尾以特殊结束符分割
  3. 将消息分为消息头和消息体,消息头中包含消息总长度(或者消息体长度)的字段

很明显,1、2 两种方法都有些局限性,一般采用方案 3,具体协议设计如下

+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
|  Byte  |        |        |        |        |        |        |                ...                |
+---------------------------------------------------------------------------------------------------
|  magic | version|  type  |         content length            |         content byte[]            |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
  • 第一个字节是魔法数,比如定义为 0X35
  • 第二个字节代表协议版本号,以便对协议进行扩展,使用不同的协议解析器
  • 第三个字节是请求类型,如 0 代表请求,1代表响应
  • 第四到七个字节表示消息长度,即此四个字节后面该长度的内容是消息体

序列化协议

4 种序列化协议,JavaSerializerProtobufHessianKryo 优缺点比较

优点缺点
JavaSerializer使用方便,可序列化所有类速度慢,体积大
Protobuf速度快需静态编译
Hessian默认支持跨语言较慢
Kryo速度快,序列化后体积小跨语言支持较复杂

秒杀系统

需求分析

  • 定时开始
  • 限量
  • 限购

流程图

flowchart LR
U(用户);
Q1{是否开始?};Q2{已抢完?};Q3{库存锁定\n成功?};Q4{按时支付?}
P1[商品购买页];P2[点击购买按钮];P3[创建订单];P4[支付倒计时];P5[释放库存];P6[扣减库存];
F1{{展示倒计时\n抢购按钮置灰}};F2{{秒杀结束}};F3{{下单失败}};F4{{购买失败}};F5{{购买成功}};
	
U --访问--> P1 --> Q1 --> |是| Q2 --> |是| P2 --> P3 --> Q3 --> |是| P4 --> Q4 --> |是| P6 --> F5
Q1 --> |否| F1
Q2 --> |否| F2
Q3 --> |否| F3
Q4 --> |否| P5 --> F4

系统要求

  • 瞬时大流量高并发 - 服务器、数据库等能承载的 QPS 有限,如数据库一般是单机 1000QPS,需要根据业务预估并发量
  • 有限库存,不能超卖、少卖 - 库存是有限的,需要精确的保证不超卖,不少卖
  • 黄牛恶意请求 - 使用脚本模拟用户购买,模拟十几万个请求去抢购
  • 固定时间开启 - 时间到了才能购买
  • 严格限购 - 一个用户只能购买一件

单体架构设计

                   |-----------------|
                   |  |-----------|  |
                   |  | 秒杀模块  |  |
                   |  |-----------|  |
                   |                 |
                   |  |-----------|  |
                   |  | 商品信息和 |  |
                   |  | 库存模块  |  |
  网关             |  |-----------|  |             |---------|
 Gateway --------- |                 | ----------- |  数据库  |
                   |  |-----------|  |             |---------|
                   |  | 订单模块  |  |
                   |  |-----------|  |
                   |                 |
                   |  |-----------|  |
                   |  | 支付模块  |  |
                   |  |-----------|  |
                   |-----------------|
  • 前后端耦合,服务压力较大 -- 前后端分离
  • 各功能模块耦合严重
    • 系统复杂,一个模块的升级需要整个服务都升级
    • 扩展性差,难以针对某个模块单独扩展
    • 开发协作困难,各个部门的人都使用同一个代码块
    • 级联故障,一个模块的故障可能会导致整个系统不可用
    • 只能使用同一种技术和语言
  • 数据库崩溃导致整个服务崩溃

微服务架构设计

                   |-----------|             |------------|
            |----- | 秒杀服务  | ----------- |  秒杀数据库 |
            |      |-----------|             |------------|
            |                   
            |      |-----------|             |-----------|
            |----- | 商品信息和 |             | 商品与库存 |
            |      | 库存服务  | ----------- | 数据库     |
  网关      |      |-----------|             |-----------|
 Gateway ---|                   
            |      |-----------|             |------------|
            |----- | 订单服务  | ----------- |  订单数据库 |
            |      |-----------|             |------------|
            |                   
            |      |-----------|             |------------|
            |----- | 支付服务  | ----------- |  支付数据库 |
                   |-----------|             |------------|

表结构设计

  商品信息表(t_commodity)                           商品库存表(t_stock)
 |--------------------------------------------|  |----------------------------|
 | 商品 ID | 商品名称   | 商品描述 | 商品价格 |  | 库存 ID | 商品 ID  | 库存  |
 |  id     |  name      |  desc    |  price   |  |  id     |  comm_id | stock |
 |  189    |  IPhone 11 | xxxxxxxx |  5999    |  |  1      |  189     | 10000 |
 |--------------------------------------------|  |----------------------------|
  订单表(t_order)                         秒杀表(t_sekill)
 |-----------------------------------|  |--------------------------------------------------------|
 | 订单 ID | 商品 ID  | 用户 ID      |  | 秒杀 ID | 秒杀名称    | 商品 ID  | 秒杀价格  | 秒杀数量 |
 |  id     |  comm_id |  customer_id |  |  id     |  name       |  comm_id |  price   |  stock   |
 |  189    |  189     |  9           |  |  28     | 618秒杀活动 |  189     |  4000    |  100     |
 |-----------------------------------|  |--------------------------------------------------------|

库存扣减时,使用 TRANSACTION + FOR UPDATE 保证原子性

START TRANSACTION;
SELECT stock FROM `t_stock` WHERE `comm_id` = 189 FOR UPDATE;
UPDATE `t_stock` SET stock = stock - 1 WHERE `comm_id` = 189;
COMMIT;

架构优化

前端优化

  • 使用前后端分离架构,前后端部署到不同的服务器,分担服务器压力。必要的情况下,可以使用 CDN 缓存静态资源
  • 用户点击购买后,短时间内按钮置灰或多次点击后,弹出验证码,防止恶意刷单

后端优化

  • 网关拦截恶意 IP 地址和用户 ID

    • 黑名单
    • 布隆过滤器
  • 使用 Nginx 做负载均衡分散瞬时压力

    • 加权随机
    • 加权轮询
    • 最小连接数
    • 一致性哈希
  • 使用 Redis 缓存分担数据库的压力

     SET sekill_stock:sekill_id:comm_id 500 // 缓存预热,这里的数量可以稍大于真实数量
                                            // 可能需要考虑少卖的问题
     DECR seckill_stock:28:189 // 扣减库存,这里并不是真的扣减库存,最终是否扣减,需要从数据库来判断
                               // 当返回值小于 0 时,表示没有库存,此时不需要再请求数据库
                               // 或者使用 lua 脚本,先查询再减库存,保证原子性
    
    // 限购方案
    SADD sekill_customer:sekill_id:comm_id
    SISMEMBER sekill_customer:sekill_id:comm_id customer_id
    
  • Redis 性能依然有限

    • Redis 库存小于 0 时,后续请求直接拒绝
    • 服务限流,计数器、漏桶、令牌桶、消息队列
    • 服务降级,跳转繁忙页
  • 商品信息等短时间内变化不大的信息也可以缓存到 Redis

  • 考虑缓存雪崩、缓存穿透、缓存击穿等问题

  • 使用分布式事务保证数据一致性

    • 三阶段提交
    • TCC
    • Seata
  • 使用服务熔断防止服务雪崩

    • Netflix Hystrix
    • Alibaba Sentinel

大数据

Hadoop

HBase

Hive

Flink