分享一下这两周刷面经遇到的一些八股与算法

8.5
HTTP协议的缓存策略有哪些?

一、强缓存(无需与服务器通信,直接复用本地缓存)

强缓存是指客户端在缓存有效期内,直接从本地缓存读取资源,不向服务器发送请求。其有效性由响应头中的字段控制,优先级高于协商缓存。

  1. 核心响应头字段
  • Expires(HTTP/1.0)
    定义:服务器返回的资源过期时间(绝对时间,格式为GMT,如Expires: Wed, 21 Oct 2025 07:28:00 GMT)。
    工作原理:客户端会对比本地时间与Expires,若未过期则直接使用缓存;若过期则触发后续缓存策略(如协商缓存)。
    缺点:依赖客户端本地时间,若客户端时间与服务器时间不同步(如手动修改系统时间),可能导致缓存失效或过期资源被错误使用。

  • Cache-Control(HTTP/1.1,优先级高于 Expires)
    通过多个指令精确控制缓存行为,格式为Cache-Control: 指令1, 指令2。常见指令包括:

    max-age = 秒数:资源从被请求到过期的相对时间(如max-age=3600表示 1 小时内有效)。客户端无需依赖本地时间,直接根据请求时间计算有效期,更可靠。

    s-maxage = 秒数:仅作用于共享缓存(如 CDN、代理服务器),优先级高于max-age。用于控制中间节点的缓存时长(如s-maxage=86400表示 CDN 缓存 1 天)。

    public:资源可被任何缓存存储(包括客户端和共享缓存)。默认情况下,带Authorization的请求资源为private,需显式声明public才允许共享缓存存储。

    private:资源仅能被用户代理缓存(如浏览器),禁止共享缓存存储(如敏感用户数据)。

    no-cache:不使用强缓存,每次请求需与服务器协商(触发协商缓存),但允许缓存资源(只是使用前必须验证)。

    no-store:完全禁止缓存(包括本地和中间节点),每次请求必须从服务器获取完整资源(如支付信息、实时数据)。

    must-revalidate:缓存过期后,必须向服务器验证有效性,不允许使用过期缓存(即使客户端离线)。

强缓存工作流程

客户端首次请求资源,服务器返回资源及Cache-Control/Expires头。

客户端将资源存入本地缓存,并记录有效期。

再次请求时,若资源仍在有效期内,直接从缓存读取(状态码为 200 OK,来自缓存);若过期,则进入协商缓存流程。

二、协商缓存(需与服务器通信,验证缓存有效性)

当强缓存过期后,客户端需携带缓存标识向服务器请求,由服务器判断缓存是否可用。若可用,返回 304 Not Modified(不返回资源体,仅更新缓存有效期);若不可用,返回 200 OK 及新资源。

核心标识与对应头字段

协商缓存通过 “请求头(客户端发送)” 和 “响应头(服务器返回)” 配对实现,分为两类:

  • Last-Modified 与 If-Modified-Since(基于文件修改时间)

    • Last-Modified(响应头):服务器返回资源的最后修改时间(如Last-Modified: Tue, 12 Sep 2023 08:00:00 GMT)。
    • If-Modified-Since(请求头):客户端再次请求时,将Last-Modified的值作为该头的值发送给服务器,用于验证资源是否在该时间后被修改。

    工作原理:
    服务器对比If-Modified-Since与资源当前修改时间:

    • 若资源未修改(时间一致),返回 304 Not Modified,客户端复用缓存。
    • 若资源已修改(时间更新),返回 200 OK 及新资源,并更新Last-Modified

    缺点:

    • 精度限制:仅能精确到秒,若资源在 1 秒内多次修改,无法识别。
    • 误判风险:若资源内容未变但修改时间被更新(如手动编辑后未改内容),服务器会误判为 “已修改”,导致无效的资源传输。
  • ETag 与 If-None-Match(基于文件内容哈希,优先级高于 Last-Modified)

    • ETag(响应头):服务器根据资源内容生成的唯一标识(如哈希值,格式为ETag: “abc123”)。内容不变则 ETag 不变,内容修改则 ETag 更新。

      • 弱验证 ETag:以W/前缀标识(如W/"abc123"),表示允许内容轻微修改(如空格、注释变化)时仍视为有效(适用于非核心内容)。
    • If-None-Match(请求头):客户端再次请求时,将ETag的值作为该头的值发送给服务器,用于验证资源内容是否变化。

    工作原理:
    服务器对比If-None-Match与资源当前 ETag:

    • 若 ETag 一致(内容未变),返回 304 Not Modified。
    • 若 ETag 不一致(内容已变),返回 200 OK 及新资源,并更新ETag

    优势:

    • 精度更高:直接基于内容判断,不受修改时间影响,解决了Last-Modified的误判问题。
    • 灵活性:支持弱验证,适合允许轻微内容变化的场景。

协商缓存工作流程

  1. 强缓存过期后,客户端请求资源时,携带If-Modified-Since(或If-None-Match)头。
  2. 服务器验证标识:
    • 缓存有效:返回 304 Not Modified,客户端复用缓存并更新有效期。
    • 缓存无效:返回 200 OK 及新资源,客户端更新缓存及标识。
介绍一下几种 IO 模型
  • BIO:应用程序发起 IO 后,全程阻塞至数据准备完成并拷贝到用户空间。
  • NIO:IO 操作立即返回,数据未就绪时返回错误,需应用程序轮询检查。
  • AIO::应用程序发起后不阻塞,内核完成数据准备和拷贝,完成后通知应用。
  • IO多路复用:通过系统调用监控多个 IO 通道,数据就绪时通知应用程序处理。
  • 信号驱动IO:注册信号处理函数,数据就绪时内核发信号通知,无需主动轮询 / 阻塞。
说一说NIO的实现原理

一、核心组件与底层映射

Java NIO 的核心组件与操作系统底层 IO 模型存在明确的映射关系,这是理解其原理的关键:

  1. Channel(通道)
    • 对应操作系统中的文件描述符(FD),是 Java 程序与底层 IO 设备(网络套接字、文件)的连接点。
    • 常见实现:SocketChannel(网络客户端通道)、ServerSocketChannel(网络服务端通道)、FileChannel(文件通道)等。
    • 核心特性:支持非阻塞模式(通过configureBlocking(false)开启),这是 NIO 的基础。在非阻塞模式下,read()write()accept()等操作会立即返回,不会阻塞线程。
  2. Buffer(缓冲区)
    • 对应内存中的字节数组,是 Java 程序与 Channel 之间的数据传输载体。
    • 工作原理:数据必须先读入 Buffer(从 Channel 到 Buffer),或从 Buffer 写出(从 Buffer 到 Channel),避免了 BIO 中直接操作流的低效性。
    • 核心机制:通过positionlimitcapacity三个指针控制数据读写,支持flip()(切换读写模式)、clear()(清空缓冲区)等操作,优化数据处理效率。
  3. Selector(选择器)
    • 对应操作系统的IO 多路复用器(如 Linux 的epoll、Windows 的IOCP),是 NIO 实现高并发的核心。
    • 作用:一个 Selector 可以同时监控多个 Channel 的事件(如连接请求、数据可读、数据可写),实现 “单线程管理多通道”。

二、非阻塞 IO 的工作流程(以网络通信为例)

Java NIO 的非阻塞特性通过 “通道非阻塞化 + 选择器事件驱动” 实现,具体流程如下:

  1. 初始化非阻塞通道与选择器

    运行

    // 创建服务器通道并设置为非阻塞
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.configureBlocking(false);
    serverChannel.bind(new InetSocketAddress(8080));
    
    // 创建选择器
    Selector selector = Selector.open();
    
    // 将服务器通道注册到选择器,关注“接收连接”事件(OP_ACCEPT)
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    
    • 底层:configureBlocking(false)会调用操作系统的fcntl系统调用,为通道对应的 FD 设置O_NONBLOCK标志,使其进入非阻塞模式。
  2. 选择器等待事件就绪

    运行

    while (true) {
        // 阻塞等待事件发生(无事件时线程休眠,不消耗CPU)
        int readyChannels = selector.select();
        if (readyChannels == 0) continue;
    
        // 获取所有就绪事件的SelectionKey集合
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        // ...处理事件
    }
    
    • 底层:selector.select()会调用操作系统的多路复用接口(如epoll_wait),阻塞等待内核通知 “注册的事件已就绪”,避免了应用程序主动轮询的 CPU 消耗。
  3. 事件驱动处理 IO 操作
    当选择器检测到事件就绪(如客户端连接、数据到达),会通过SelectionKey通知应用程序,程序只需处理就绪的通道:

    运行

    Iterator<SelectionKey> iterator = selectedKeys.iterator();
    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
    
        // 处理“接收连接”事件
        if (key.isAcceptable()) {
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            // 非阻塞接收连接(立即返回,若无可接收连接则返回null)
            SocketChannel clientChannel = server.accept();
            if (clientChannel != null) {
                clientChannel.configureBlocking(false);
                // 注册客户端通道到选择器,关注“读数据”事件(OP_READ)
                clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
            }
        }
        // 处理“读数据”事件
        else if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            // 非阻塞读取数据(立即返回,若无可读数据则返回0)
            int bytesRead = client.read(buffer);
            if (bytesRead > 0) {
                // 处理读取到的数据
                buffer.flip();
                // ...
            }
        }
    
        iterator.remove(); // 移除已处理的事件,避免重复处理
    }
    
    • 核心:仅当通道有事件发生时才进行 IO 操作,且所有操作(accept()read())均为非阻塞,线程无需等待,大幅提升 CPU 利用率。
介绍一下Java中的序列化与反序列化

在 Java 中,序列化(Serialization) 是指将对象的状态信息转换为可存储或传输的字节序列的过程;反序列化(Deserialization) 则是将字节序列恢复为原来的对象的过程。这一机制主要用于对象的持久化存储(如保存到文件)或网络传输(如远程服务调用)。

一、核心作用

  1. 对象持久化:将内存中的对象状态保存到磁盘文件中,程序重启后可通过反序列化恢复对象。
  2. 网络传输:在分布式系统中,对象需通过网络传输时,需先序列化为字节流,接收方再反序列化为对象(如 RPC 框架、Socket 通信)。

二、实现方式

Java 通过以下两个接口和相关 API 实现序列化:

  1. Serializable 接口

    • 一个标记接口(无任何方法),用于标识类的对象可以被序列化。
    • 若类未实现Serializable,尝试序列化其对象时会抛出NotSerializableException

    运行

    import java.io.Serializable;
    
    // 实现Serializable接口,标识该类可序列化
    public class User implements Serializable {
        private String name;
        private int age;
        //  transient修饰的字段不会被序列化
        private transient String password; 
    
        // 构造方法、getter、setter省略
    }
    
    • transient 关键字:修饰的字段在序列化时会被忽略,反序列化时该字段会被设为默认值(如null0)。
  2. 序列化 / 反序列化工具类

    • ObjectOutputStream:负责将对象序列化为字节流(核心方法writeObject())。
    • ObjectInputStream:负责将字节流反序列化为对象(核心方法readObject())。
算法题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

 private static ListNode removeDupliacates(ListNode head) {
        if (head == null) {
            return null;
        }
        // 创建哑节点
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode pre = dummy;
        ListNode cur = head;

        while (cur != null) {
            // 跳过重复的节点,找到当前值最后一个重复的位置
            while (cur.next != null && cur.val == cur.next.val) {
                cur = cur.next;
            }
            if (pre.next == cur) {
                // 说明当前节点值只出现了一次,pre 后移
                pre = pre.next;
            } else {
                // 说明当前节点值有重复,跳过这些重复节点
                pre.next = cur.next;
            }
            cur = cur.next;
        }
        return dummy.next;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

package hot100;

public class 最大子数组和 {
    public static void main(String[] args) {
        int[] nums=new int[]{-2,1,-3,4,-1,2,1,-5,4};
        // 使用Kadane算法优化
        int max = nums[0];  // 全局最大值
        int currentSum = nums[0];  // 当前子数组和
        
        for (int i = 1; i < nums.length; i++) {
            // 如果当前子数组和为负数,则重新开始计算
            currentSum = Math.max(nums[i], currentSum + nums[i]);
            // 更新全局最大值
            max = Math.max(max, currentSum);
        }
        
        System.out.println(max);
    }
}
8.6
Serializable接口为什么需要定义serialVersionUID常量?

保证版本兼容性

当一个类实现了 Serializable 接口后,Java 序列化机制会根据类的结构(包括类名、字段类型、字段名称等)自动生成一个默认的 serialVersionUID。但是,如果类的结构发生了变化(例如添加、删除字段,修改字段类型等),重新编译后,自动生成的 serialVersionUID 也会改变。

在反序列化时,Java 会比较序列化对象中的 serialVersionUID 和当前类的 serialVersionUID。如果两者不匹配,就会抛出 InvalidClassException 异常,导致反序列化失败。通过显式地定义 serialVersionUID,可以在类的结构发生一些不影响序列化和反序列化逻辑的小变化时,保证版本兼容性,使得旧版本序列化的对象依然可以被新版本的类成功反序列化。例如:

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    // 省略构造函数、getter和setter方法
}

假设在后续版本中,为 Person 类添加了一个新字段 String address,只要 serialVersionUID 保持不变,之前序列化的 Person 对象仍然可以被正确反序列化。

增强控制能力

显式定义 serialVersionUID 可以让开发者对序列化和反序列化过程有更强的控制。开发者可以根据实际需求,在类的结构发生变化时,手动修改 serialVersionUID 的值,从而主动决定哪些旧版本的对象不应该再被反序列化,或者在反序列化时执行特定的兼容逻辑。

避免潜在的不一致问题

自动生成的 serialVersionUID 依赖于类的具体结构,不同的编译器或者 Java 运行时环境在计算默认 serialVersionUID 时可能存在细微差异。显式定义 serialVersionUID 可以消除这种潜在的不一致性,确保在不同环境下,序列化和反序列化的行为是一致的。

说一说synchronized的实现原理

一、synchronized 的基本实现:基于 “对象监视器”(Monitor)

synchronized 的同步本质是通过 对象监视器(Monitor) 实现的。在 Java 中,每个对象都天生携带一个 Monitor(可理解为一把 “锁”),当线程需要进入同步代码时,必须先获取该对象的 Monitor 所有权;退出同步代码时,则释放 Monitor 所有权。

Monitor 的底层结构(HotSpot 虚拟机)

Monitor 在 HotSpot 中由 ObjectMonitor 结构体实现,核心属性包括:

  • _owner:指向当前持有 Monitor 的线程(初始为 null)。
  • _EntryList:阻塞队列,存放等待获取 Monitor 的线程(未获取到锁的线程会进入此队列)。
  • _WaitSet:等待队列,存放调用 wait() 方法后释放锁的线程。
  • _recursions:记录当前线程重入锁的次数(支持可重入性)。

二、synchronized 的两种使用形式及对应实现

synchronized 可修饰 方法代码块,二者实现方式略有不同,但核心都是获取对象的 Monitor。

  1. 同步方法(修饰方法)

当修饰普通方法时,同步对象是 当前实例对象(this);当修饰静态方法时,同步对象是 类的 Class 对象

public class SynchronizedExample {
    // synchronized修饰普通方法,锁是当前实例对象(this)
    public synchronized void method1() {
        // 方法体
    }
    
    public synchronized void method2() {
        // 方法体
    }
}

// 使用示例
SynchronizedExample obj1 = new SynchronizedExample();
SynchronizedExample obj2 = new SynchronizedExample();

// 线程1执行obj1的method1
// 线程2可以同时执行obj2的method1(因为锁是不同的实例)
// 但线程2不能同时执行obj1的method2(因为锁是同一个实例obj1)

实现原理:
JVM 在编译时,会为同步方法添加一个 ACC_SYNCHRONIZED 标志(在方法表中)。当线程调用该方法时,JVM 会检查此标志:

  • 若存在,线程会先尝试获取该方法所属对象的 Monitor。
  • 获取成功后,执行方法体;执行完毕后释放 Monitor。
  • 若未获取到,线程会进入该对象 Monitor 的 _EntryList 队列阻塞等待。
  1. 同步代码块(修饰代码块)

同步代码块通过显式指定同步对象(如 synchronized(obj) { ... }),锁定的是 obj 对应的 Monitor。

实现原理:
编译时,JVM 会在同步代码块的 入口出口 分别插入字节码指令 monitorentermonitorexit

  • monitorenter:线程尝试获取同步对象的 Monitor。若 Monitor 的 _owner 为 null,则当前线程成为 _owner,并将 _recursions 设为 1;若当前线程已持有 Monitor(重入),则 _recursions 加 1;若被其他线程持有,则当前线程进入 _EntryList 阻塞。
  • monitorexit:线程释放 Monitor。将 _recursions 减 1,当 _recursions 为 0 时,将 _owner 设为 null,唤醒 _EntryList 中等待的线程重新竞争锁。

(注:通常会生成两个 monitorexit 指令,一个处理正常退出,一个处理异常退出,确保锁一定会被释放。)

三、锁的优化:从重量级锁到多态锁(JDK 1.6+)

早期 JVM 中,synchronized 是 “重量级锁”,依赖操作系统的 互斥量(Mutex) 实现,线程阻塞 / 唤醒需要在用户态和内核态之间切换,开销极大。JDK 1.6 为优化性能,引入了 偏向锁轻量级锁重量级锁 的升级机制,根据竞争强度动态切换锁的类型。

  1. 偏向锁(Biased Locking):无竞争场景优化

适用场景:单线程重复获取同一把锁(无竞争)。

原理:当锁首次被线程获取时,JVM 会在对象头的 Mark Word 中记录该线程 ID(“偏向” 该线程),后续该线程再次获取锁时,无需 CAS 操作,直接通过比对线程 ID 即可获取锁,几乎无开销。

撤销:若有其他线程尝试获取锁,偏向锁会被撤销(需暂停持有锁的线程),升级为轻量级锁。

  1. 轻量级锁:短时间竞争场景优化

适用场景:多线程交替获取锁(竞争不激烈)。

原理:线程获取锁时,会在自己的栈帧中创建一个 “锁记录(Lock Record)”,并通过 CAS 操作将对象头的 Mark Word 复制到锁记录中,同时将 Mark Word 更新为指向锁记录的指针(表示锁被当前线程持有)。

膨胀:若 CAS 失败(说明有线程竞争),JVM 会自旋几次尝试获取锁;若自旋失败,轻量级锁膨胀为重量级锁。

  1. 重量级锁:激烈竞争场景

适用场景:多线程同时竞争锁(竞争激烈)。

原理:基于操作系统的互斥量(Mutex)实现,此时对象头的 Mark Word 指向 Monitor。未获取到锁的线程会进入 _EntryList 阻塞,不再自旋(避免 CPU 空耗),等待持有锁的线程释放后被唤醒。

四、synchronized 的核心特性

  1. 可重入性:同一线程可多次获取同一把锁(通过 _recursions 计数实现),避免自己阻塞自己。
  2. 可见性:释放锁时,JVM 会将线程工作内存中的数据刷新到主内存;获取锁时,会从主内存加载最新数据,保证线程间数据可见。
  3. 排他性:同一时间只有一个线程能持有 Monitor(重量级锁下),或通过 CAS 保证交替执行(轻量级锁)。
HTTPS协议非对称加密的过程?

1. 非对称加密的核心角色

  • 公钥(Public Key):公开传播,用于加密数据或验证签名。
  • 私钥(Private Key):仅服务器持有,用于解密公钥加密的数据或生成签名。
  • 特性:公钥加密的数据只能用对应的私钥解密,私钥加密的数据(签名)只能用对应的公钥验证。

2.HTTPS 中非对称加密的完整流程

(1)客户端发起 HTTPS 请求

客户端(如浏览器)向服务器发送Client Hello消息,包含:

  • 支持的 TLS 版本(如 TLS 1.3)
  • 支持的加密套件(如ECDHE-RSA-AES256-GCM-SHA384
  • 随机数(用于后续生成会话密钥)

(2)服务器返回证书与公钥

服务器收到请求后,返回Server Hello消息,包含:

  • 选定的 TLS 版本和加密套件
  • 服务器生成的随机数
  • 服务器证书(由 CA 机构签发,包含服务器公钥、域名、有效期等信息)

(3)客户端验证证书合法性

客户端验证证书有效性:

  • 检查证书是否由可信 CA 机构签发(操作系统 / 浏览器内置 CA 根证书)
  • 验证证书签名:用 CA 的公钥解密证书中的签名,与证书内容的哈希值比对,确认证书未被篡改
  • 检查证书域名是否与请求域名一致、是否在有效期内

(4)客户端生成会话密钥并加密传输

验证通过后,客户端:

  • 生成一个预主密钥(Pre-Master Secret)(随机数)
  • 用服务器证书中的公钥加密该预主密钥
  • 将加密后的预主密钥发送给服务器(Client Key Exchange消息)

(5)服务器解密获取预主密钥

服务器收到加密的预主密钥后,用自己的私钥解密,得到预主密钥。

(6)双方生成对称会话密钥

客户端和服务器分别使用:

  • 客户端随机数 + 服务器随机数 + 预主密钥
  • 通过约定的密钥派生算法(如 HKDF)生成相同的对称会话密钥

(7)后续通信使用对称加密

从此刻起,客户端与服务器的所有通信均使用对称加密(如 AES),而非对称加密的使命完成。

3.非对称加密的核心作用

  • 身份认证:通过证书和签名确保服务器身份真实(防止中间人伪装)。
  • 安全密钥交换:用公钥加密预主密钥,确保只有服务器(持有私钥)能解密,避免密钥在传输中被窃取。

为什么不全程使用非对称加密?

非对称加密算法(如 RSA、ECC)的计算复杂度远高于对称加密(如 AES),全程使用会导致通信效率极低。因此 HTTPS 仅在密钥交换阶段使用非对称加密,后续数据传输使用更高效的对称加密,兼顾安全性和性能。

静态库和动态库如何制作及使用,区别是什么

静态库是将函数和代码编译成一个单独的文件,可以通过链接器静态地将其链接到程序中。当程序被编译时,静态库的代码被复制到最终的可执行文件中,这意味着程序不需要外部库的存在。这样的好处是,程序更容易分发和部署,因为它只需要一个可执行文件。静态库的缺点是它们增加了可执行文件的大小,并且如果多个程序使用相同的静态库,那么这些程序会重复使用相同的代码,浪费磁盘空间。

动态库是将函数和代码编译成一个单独的文件,但是在程序运行时才加载。当程序被编译时,它只包含对动态库的引用,而不是实际的代码。在程序运行时,操作系统会动态地加载动态库,并将其映射到进程的地址空间中。这样,多个程序可以共享相同的动态库,因为它们只需要加载一次。动态库的缺点是,程序依赖于外部库的存在,因此在分发和部署时需要确保库的可用性。

下面是制作和使用静态库和动态库的基本步骤:

制作静态库:

  1. 编写函数和代码并编译成目标文件(.o文件)
  2. 使用静态库工具(如ar)将目标文件打包成静态库(.a文件)

使用静态库:

  1. 在编译时将静态库链接到程序中,例如使用gcc编译器时,使用-l参数指定静态库的名称。
  2. 在程序中包含头文件,以便可以调用库中的函数。

制作动态库:

  1. 编写函数和代码并编译成共享目标文件(.so文件)。
  2. 在编译时使用共享目标文件的位置和名称,例如使用gcc编译器时,使用-L和-l参数分别指定库文件的路径和名称。

使用动态库:

  1. 在程序中包含头文件,以便可以调用库中的函数。
  2. 在程序运行时,动态库会被自动加载,无需手动链接。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    public int search (int[] nums, int target) {
        // write code here
        int n=nums.length;
        int low=0,hight=n-1;
        while(low<=hight){
            int mid=low+(hight-low)/2;
            if(nums[mid]==target){
                return mid;
            }
            if(nums[0]<=nums[mid]){
                if(nums[0]<=target&&target<nums[mid]){
                    hight=mid-1;
                }else{
                    low=mid+1;
                }
            }else{
                if(nums[mid]<target&&target<=nums[n-1]){
                    low=mid+1;
                }else{
                    hight=mid-1;
                }
            }
        }
        return -1;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public static int mysqrt(int x) {
        if (x == 0) {
            return 0;
        }
        int left = 1;
        int right = x;
        while (left <= right) {
            // 防止 (left + right) 溢出,改用 left + (right - left) / 2
            int mid = left + (right - left) / 2;
            // 避免 mid * mid 溢出,改用 x / mid 比较
            if (mid == x / mid) {
                return mid;
            } else if (mid < x / mid) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        // 循环结束时,right 是最接近且小于等于平方根的整数
        return right;
    }
8.7
介绍一下Java中的IO流
  1. 按流向划分

    • 输入流(Input Stream):从外部设备(如文件、网络)读取数据到程序中,使用read()方法。
    • 输出流(Output Stream):从程序将数据写入到外部设备,使用write()方法。
  2. 按操作数据单位划分

    • 字节流:以字节(8 位)为单位处理数据,适用于所有类型文件(如文本、图片、音频等)。

      • 输入字节流:InputStream(抽象类)
      • 输出字节流:OutputStream(抽象类)
    • 字符流:以字符(16 位)为单位处理数据,仅适用于文本文件(如.txt、.java

      等)。

      • 输入字符流:Reader(抽象类)
      • 输出字符流:Writer(抽象类)
  3. 按流的角色划分

    • 节点流:直接连接数据源或目标设备的流(如FileInputStreamFileReader)。
    • 处理流:对节点流进行包装,增强功能(如BufferedInputStreamObjectInputStream)。
DNS(域名系统)是什么?

DNS(域名系统,Domain Name System)是互联网中用于将人类可读的域名(如www.example.com)转换为计算机可识别的 IP 地址(如192.168.1.1)的分布式数据库系统。

简单来说,它的核心作用是 “翻译”:因为计算机之间通信依赖 IP 地址(类似设备的网络身份证),但人类记忆一串数字(如203.0.113.5)远不如记忆有意义的域名(如example.com)方便。DNS 就像互联网的 “通讯录”,帮助我们通过易记的域名找到对应的服务器 IP。

DNS 的工作原理(简化流程):

  1. 当你在浏览器输入www.google.com时,电脑会先查询本地 DNS 缓存(如操作系统或路由器缓存),若有记录直接返回 IP。
  2. 若本地无缓存,会向你的 ISP(网络服务提供商)的 DNS 服务器查询。
  3. 若 ISP 的 DNS 服务器也无记录,会逐级向上查询根域名服务器(全球共 13 组)、顶级域名服务器(如.com.cn对应的服务器)、权威域名服务器(管理具体域名的服务器),最终获取 IP 地址并返回。
  4. 浏览器使用获取到的 IP 地址与目标服务器建立连接,加载网页内容。

DNS 的重要性:

  • 简化网络访问:无需记忆复杂的 IP 地址。
  • 支持负载均衡:同一域名可对应多个 IP,实现流量分配。
  • 提供灵活性:域名可以绑定新的 IP,而用户无需感知变化(如服务器迁移时)。
Redis有哪些数据类型
  1. String(字符串)扩展,sds
  2. Hash(哈希)哈希过程,渐进式哈希
  3. List(列表)跳表,压缩列表
  4. Set(集合)
  5. Sorted Set(有序集合)
说一说虚拟地址空间有哪些部分

在现代操作系统中,进程的虚拟地址空间(Virtual Address Space)通常划分为以下几个主要部分(以 32 位系统为例,从低地址到高地址排列):

  1. 代码段(Text Segment)
    存储程序的可执行代码(机器指令),通常为只读,防止意外修改。
  2. 数据段(Data Segment)
    存储已初始化的全局变量和静态变量,程序运行期间一直存在。
  3. BSS 段(Block Started by Symbol)
    存储未初始化的全局变量和静态变量,程序启动时会被初始化为 0,占用地址空间但不占用磁盘空间。
  4. 堆(Heap)
    用于动态内存分配(如 C 中的malloc、C++ 中的new),地址从低到高增长,需手动管理释放。
  5. 内存映射区(Memory Mapping Segment)
    映射文件内容或共享内存到虚拟地址,例如动态链接库(.so/.dll)、内存映射文件等。
  6. 栈(Stack)
    存储函数调用的局部变量、返回地址、参数等,地址从高到低增长,由操作系统自动管理(进栈 / 出栈)。
  7. 内核空间(Kernel Space)
    位于地址空间高位(如 32 位系统通常为最高 1GB),存放内核代码和数据,用户进程无法直接访问,需通过系统调用间接交互。

从 Java 角度看,虚拟地址空间的划分与操作系统层面基本一致,但结合 Java 虚拟机(JVM)的内存模型,会有更具体的映射关系。以下是 Java 程序运行时涉及的虚拟地址空间关键部分:

  1. JVM 方法区(Method Area)
    对应操作系统虚拟地址空间的数据段 / 内存映射区,存储类信息(结构、方法、字段)、常量池、静态变量等。在 HotSpot 中,JDK 8 及以上用 “元空间(Metaspace)” 实现,直接使用本地内存(操作系统虚拟地址空间的一部分),不再受 JVM 堆大小限制。
  2. JVM 堆(Heap)
    对应操作系统虚拟地址空间的堆区,是 Java 中最大的内存区域,存储对象实例和数组。由所有线程共享,垃圾回收器主要针对此区域工作(如新生代、老年代的划分)。
  3. JVM 虚拟机栈(VM Stack)
    对应操作系统虚拟地址空间的栈区,每个线程私有,存储栈帧(局部变量表、操作数栈、方法返回地址等)。方法调用时创建栈帧,调用结束后销毁,遵循 “先进后出” 原则。
  4. 本地方法栈(Native Method Stack)
    类似虚拟机栈,但为 Native 方法(如通过 JNI 调用的 C/C++ 代码)服务,同样对应操作系统的栈区
  5. 程序计数器(Program Counter Register)
    占用极小的内存空间,记录当前线程执行的字节码指令地址,本质上是虚拟地址空间中的一个指针,确保线程切换后能恢复执行位置。
  6. 直接内存(Direct Memory)
    不属于 JVM 内存模型,但 Java 可通过ByteBuffer.allocateDirect()申请,直接对应操作系统虚拟地址空间的堆 / 内存映射区,用于提升 IO 效率(避免 JVM 堆与 native 堆之间的数据拷贝)。
  7. 操作系统内核空间
    JVM 作为用户态进程,无法直接访问,需通过系统调用(如 IO 操作、线程调度)与内核交互,这部分对应虚拟地址空间的高位内核区。
算法题

二分查找升序有重复元素的数组,返回第一个目标元素的下标

    public static int findFirst(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int result = -1;
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                result = mid; // 记录当前位置
                right = mid - 1; // 继续向左查找更早出现的位置
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    }
  • 数字位置调换,考虑溢出,前置0

123->321

    public static int reverse(int x) {
        int result = 0;
        
        while (x != 0) {
            // 取出最后一位数字(处理负数时也适用)
            int lastDigit = x % 10;
            x = x / 10;
            
            // 检查溢出:
            // 1. 如果结果已经大于Integer.MAX_VALUE/10,乘以10后必溢出
            // 2. 如果结果等于Integer.MAX_VALUE/10,且最后一位大于7(因为2^31-1=2147483647),则溢出
            if (result > Integer.MAX_VALUE / 10 || (result == Integer.MAX_VALUE / 10 && lastDigit > 7)) {
                return 0;
            }
            
            // 检查负数溢出:
            // Integer.MIN_VALUE=-2147483648,最后一位是8
            if (result < Integer.MIN_VALUE / 10 || (result == Integer.MIN_VALUE / 10 && lastDigit < -8)) {
                return 0;
            }
            
            // 累加结果(自动处理前置0,因为0乘以10加0还是0)
            result = result * 10 + lastDigit;
        }
        
        return result;
    }
8.8
TCP协议的流量控制和拥塞控制

一、流量控制(Flow Control)

流量控制是点对点的控制机制,用于解决 “发送方发送速度过快,接收方来不及处理” 的问题,防止接收方缓冲区溢出,确保接收方能够有序处理接收到的数据。其核心思想是:限制发送方的发送速率,使其不超过接收方的接收能力

  1. 核心依据:接收窗口(rwnd,Receiver Window)

接收方通过 TCP 报文头部的 “窗口字段”(Window Size)告知发送方自己当前的接收能力,这个值被称为 “接收窗口(rwnd)”。

  • rwnd 的大小由接收方的缓冲区剩余空间决定:若接收方缓冲区剩余空间为 500 字节,则 rwnd=500,表示发送方可继续发送不超过 500 字节的数据。
  • 发送方的 “发送窗口”(实际发送数据的上限)必须小于等于接收方告知的 rwnd,以此保证接收方不会被 “淹没”。
  1. 实现机制:滑动窗口协议

TCP 通过 “滑动窗口协议” 实现流量控制,其核心是动态调整发送窗口的大小:

发送窗口内的数据是 “已发送但未确认” 或 “可发送但未发送” 的数据,窗口大小由 rwnd 决定(发送窗口 ≤ rwnd)。

当接收方处理完部分数据后,会释放缓冲区空间,并通过确认报文(ACK)更新 rwnd 的值;发送方收到新的 rwnd 后,调整发送窗口大小,继续发送数据。

例如:

  • 初始时接收方 rwnd=1000 字节,发送方发送 800 字节后,发送窗口剩余 200 字节。
  • 接收方处理完 500 字节,缓冲区剩余空间变为 500+200=700 字节,通过 ACK 告知 rwnd=1000。
  • 发送方收到后,发送窗口扩大至 1000 字节,可继续发送更多数据。
  1. 特殊情况:窗口关闭与探测报文

若接收方缓冲区已满(rwnd=0),发送方会停止发送数据。但为了避免 “接收方缓冲区已释放空间,发送方却不知道” 的死锁问题,发送方会定期发送零窗口探测报文(携带 1 字节数据):

  • 接收方收到探测报文后,若缓冲区已释放空间,会在回复中更新 rwnd(如 rwnd=500);
  • 发送方收到新的 rwnd 后,恢复数据发送。

二、拥塞控制(Congestion Control)

拥塞控制是全局性的控制机制,用于解决 “网络中数据量过大,超过网络承载能力” 的问题(即 “网络拥塞”)。网络拥塞会导致时延增加、丢包率上升、吞吐量下降,拥塞控制的核心思想是:限制所有发送方的总发送速率,避免网络因过载而崩溃

  1. 拥塞的表现与原因
  • 表现:数据包传输时延骤增、丢包率上升(如路由器缓冲区溢出导致丢包)。
  • 原因:网络中所有发送方的总数据量超过了链路带宽、路由器处理能力等网络资源的上限。
  1. 核心依据:拥塞窗口(cwnd,Congestion Window)

TCP 引入 “拥塞窗口(cwnd)” 来表示发送方在 “不引发网络拥塞” 的前提下可发送的数据量。cwnd 的大小由网络拥塞程度动态调整,与接收方的 rwnd 无关(发送窗口的实际大小是 min (rwnd, cwnd),即取接收能力和网络承载能力的最小值)。

  1. 拥塞控制算法:慢开始、拥塞避免、快重传、快恢复

TCP 通过四个阶段的算法动态调整 cwnd,以适应网络拥塞状态:

阶段触发条件算法逻辑
慢开始连接建立初期或网络恢复后cwnd 从 1 个报文段开始,每经过一个往返时间(RTT)就加倍(指数增长),直到 cwnd 达到 “慢开始门限(ssthresh)”。
拥塞避免cwnd ≥ ssthresh 或从快恢复进入cwnd 不再指数增长,而是每经过一个 RTT 增加 1 个报文段(线性增长),避免快速引发拥塞。
快重传收到 3 个重复的确认报文(ACK)无需等待超时,立即重传丢失的报文段(说明网络未严重拥塞,只是个别包丢失)。
快恢复执行快重传后1. 将 ssthresh 设为当前 cwnd 的一半;2. 将 cwnd 设为 ssthresh(而非重置为 1);3. 直接进入拥塞避免阶段。

此外,若因 “超时” 检测到丢包(说明网络严重拥塞),则会执行:1. ssthresh = 当前 cwnd / 2;2. cwnd 重置为 1;3. 重新从慢开始阶段启动。

TCP协议是怎么保证有效传输的?

一、可靠传输的核心机制

  1. 面向连接(三次握手建立连接)

TCP 是 “面向连接” 的协议,在数据传输前必须通过三次握手建立逻辑连接,确保双方收发能力正常:

  • 客户端发送SYN报文,请求建立连接;
  • 服务器回复SYN+ACK报文,确认客户端请求并告知自己已准备好;
  • 客户端发送ACK报文,确认服务器的响应。
    三次握手后,双方确认彼此的收发缓冲区、初始序列号等信息,为后续传输奠定基础。
  1. 序列号与确认机制(确保不丢失、不重复)
  • 序列号:每个 TCP 报文段都包含 “序列号”(Sequence Number),标识该段数据在整个数据流中的位置(以字节为单位)。例如,发送方第一个报文段序列号为 100,携带 50 字节数据,则下一个报文段序列号为 150。
  • 确认机制:接收方收到数据后,会返回 “确认报文(ACK)”,其中的 “确认号”(Acknowledgment Number)表示期望接收的下一字节位置。例如,接收方收到序列号 100~149 的 50 字节数据后,会返回确认号 150,告知发送方 “已正确接收至 149 字节,下次请从 150 开始发送”。

通过序列号和确认号,发送方可判断数据是否丢失,接收方可检测重复数据(如收到序列号小于确认号的报文则直接丢弃)。

  1. 超时重传(应对数据丢失)

若发送方在一定时间内(超时重传时间 RTO)未收到对某段数据的确认,会认为数据已丢失并重新发送该数据
RTO 并非固定值,TCP 会根据网络往返时间(RTT)动态调整(如网络延迟大则增大 RTO,避免不必要的重传)。

  1. 流量控制(避免接收方缓冲区溢出)

发送方可能因速率过快导致接收方缓冲区溢出,TCP 通过滑动窗口机制实现流量控制:

  • 接收方在 ACK 报文中通过 “窗口字段” 告知发送方自己的 “接收窗口(rwnd)”(即缓冲区剩余空间)。
  • 发送方的 “发送窗口”(可发送但未确认的数据量上限)不得超过 rwnd,确保接收方有足够空间处理数据。

二、高效传输的关键机制

  1. 拥塞控制(避免网络过载)

网络拥塞(如路由器缓冲区满导致丢包)会严重影响传输效率,TCP 通过拥塞窗口(cwnd) 动态调整发送速率:

  • cwnd 表示在不引发拥塞的前提下,发送方可发送的最大数据量,由网络状况决定。
  • 结合 “慢开始”“拥塞避免”“快重传”“快恢复” 等算法(详见前文),cwnd 会根据丢包情况(网络拥塞信号)动态收缩或增长,平衡吞吐量与网络稳定性。

发送方实际的发送窗口为min(rwnd, cwnd),即同时受接收方能力和网络拥塞状态限制。

  1. 滑动窗口与批量发送(提升传输效率)

TCP 的滑动窗口机制不仅用于流量控制,还能实现批量发送以提高效率:

  • 窗口内的数据可连续发送,无需等待每段数据的单独确认(只需收到对窗口内最后一段数据的确认,即可认为窗口内所有数据已被接收)。
  • 例如,窗口大小为 1000 字节时,发送方可一次性发送多段数据(总大小≤1000 字节),只需等待最终的确认即可滑动窗口继续发送后续数据。
  1. 数据分片与重组(适配 MTU)

网络层(如 IP 协议)对单次传输的数据包大小有限制(最大传输单元 MTU)。TCP 会根据 MTU 将应用层数据分片为合适大小的报文段,确保能被 IP 层有效传输;接收方则在 TCP 层将分片重组为完整数据,再交付给应用层。

  1. 拥塞窗口与慢启动优化

连接建立初期,TCP 通过 “慢启动” 机制(cwnd 从 1 个报文段开始,指数增长)逐步增加发送速率,避免突然发送大量数据导致网络拥塞,待达到一定阈值后进入 “拥塞避免” 阶段(线性增长),实现效率与稳定性的平衡。

三、其他辅助机制

  • 校验和:每个 TCP 报文段包含校验和字段,接收方通过校验和检测数据在传输中是否被篡改或损坏,若校验失败则丢弃该报文段(触发发送方重传)。
  • 紧急指针:支持 “带外数据” 传输,允许发送方标记紧急数据,接收方可优先处理(如中断请求)。
  • 四次挥手断开连接:确保双方数据都已传输完毕再释放连接,避免数据残留。
什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程?

一、孤儿进程(Orphan Process)

当一个子进程的父进程先于子进程结束时,这个子进程就会成为孤儿进程。

特点:

  • 孤儿进程不会被立即销毁,而是会被操作系统的init 进程(或 systemd 等进程管理器,进程 ID 为 1) 接管。
  • init 进程会成为孤儿进程的新父进程,并负责在孤儿进程结束后回收其资源(避免资源泄漏)。

示例:

父进程创建子进程后,父进程提前退出,子进程继续运行,此时子进程就成为孤儿进程,由 init 进程接管。

影响:

孤儿进程本身是正常运行的进程,只是父进程已不存在,由系统进程接管后会被正常回收,通常不会造成问题

二、僵尸进程(Zombie Process)

当一个子进程已经结束运行(终止),但父进程未及时读取其退出状态时,子进程的进程控制块(PCB)会残留在内存中,成为僵尸进程。

特点:

  • 僵尸进程已经终止,不再执行任何代码,但其进程 ID(PID)和退出状态等信息仍保存在内核中(占用少量内存)。
  • 僵尸进程无法被kill命令清除(因为它已经终止,没有运行的代码)。

危害:

  • 系统的 PID 数量是有限的,若大量僵尸进程积累,会耗尽可用的 PID 资源,导致新进程无法创建。
  • 占用内核内存资源,虽然单个僵尸进程占用资源很少,但积累过多会影响系统性能。

示例:

父进程创建子进程后,子进程完成任务并退出,但父进程未调用wait()waitpid()等系统调用来获取子进程的退出状态,子进程的 PCB 就会残留,成为僵尸进程。

如何解决僵尸进程?

1.父进程主动调用wait()waitpid()

父进程通过wait()waitpid()系统调用阻塞等待子进程结束,并获取其退出状态,从而回收子进程资源。

2.利用信号机制(SIGCHLD)

当子进程退出时,内核会向父进程发送SIGCHLD信号。父进程可以注册该信号的处理函数,在函数中调用waitpid()回收子进程资源,避免阻塞父进程的正常运行。

3.父进程退出,僵尸进程被 init 进程接管

若父进程无法及时回收子进程(如程序设计缺陷),可让父进程先退出,此时僵尸进程会成为孤儿进程,被 init 进程接管,init 进程会自动回收其资源。

4. 使用进程组或会话管理

通过将子进程放入独立的进程组或会话,当父进程退出时,子进程被 init 接管,避免僵尸进程积累。

说说常见信号有哪些,表示什么含义
信号编号信号名称含义与触发场景默认处理行为
1SIGHUP终端挂起或控制进程退出(如关闭终端、父进程退出)终止进程
2SIGINT键盘中断(用户按下 Ctrl+C终止进程
3SIGQUIT键盘退出(用户按下 Ctrl+\),比 SIGINT 更强烈终止进程并生成核心转储文件(core dump)
4SIGILL非法指令(进程执行了无效的机器指令,如错误的代码段)终止进程并生成 core dump
5SIGTRAP调试陷阱(用于调试器捕获进程状态,如断点触发)终止进程并生成 core dump
6SIGABRT进程主动调用 abort() 函数触发(通常表示程序异常)终止进程并生成 core dump
7SIGBUS总线错误(访问无效的内存地址,如硬件层面的内存对齐错误)终止进程并生成 core dump
8SIGFPE浮点异常(如除零操作、浮点运算溢出)终止进程并生成 core dump
9SIGKILL强制终止信号(“必杀信号”)立即终止进程(不可被捕获或忽略
10SIGUSR1用户自定义信号 1(可由程序员在程序中定义处理逻辑)终止进程
11SIGSEGV段错误(访问未分配的内存、越界访问等,最常见的内存错误)终止进程并生成 core dump
12SIGUSR2用户自定义信号 2(同 SIGUSR1,用于用户自定义场景)终止进程
13SIGPIPE管道破裂(向已关闭的管道 / 套接字写入数据,如客户端断开连接后服务器仍发送数据)终止进程
14SIGALRM闹钟信号(由 alarm() 函数设置的定时器到期触发)终止进程
15SIGTERM终止请求(默认的 “优雅终止” 信号,kill 命令默认发送此信号)终止进程(可被捕获或忽略,用于优雅退出)
说一说你对SQL注入的理解

一、SQL 注入的原理

Web 应用程序通常需要通过 SQL 语句与数据库交互(如查询用户信息、提交表单数据等)。正常情况下,用户输入应作为 “数据” 被嵌入 SQL 语句中;但如果开发者未对输入进行严格过滤或转义,恶意用户可构造特殊输入,将其伪装成 “SQL 命令”,使数据库将这些输入解析为 SQL 语句的一部分并执行。

示例
假设某登录功能的 SQL 查询逻辑为:

SELECT * FROM users WHERE username = '用户输入的用户名' AND password = '用户输入的密码';
  • 正常输入:用户名alice、密码123,生成的 SQL 为:
    SELECT * FROM users WHERE username = 'alice' AND password = '123';(正常查询)。
  • 恶意输入:用户名输入' OR '1'='1,密码任意,生成的 SQL 为:
    SELECT * FROM users WHERE username = '' OR '1'='1' -- AND password = 'xxx';
    由于'1'='1恒为真,–会注释掉后面的sql语句,该语句会返回数据库中所有用户的信息,导致登录绕过。

二、防御 SQL 注入的方法

  1. 使用参数化查询(预编译语句)
    这是最有效的防御方式。参数化查询将 SQL 语句的结构与用户输入的 “数据” 分离,数据库会将输入视为纯数据,而非 SQL 命令的一部分。
    示例(Java JDBC):

    // 错误:直接拼接字符串
    String sql = "SELECT * FROM users WHERE username = '" + username + "'";
    
    // 正确:参数化查询
    String sql = "SELECT * FROM users WHERE username = ?";
    PreparedStatement pstmt = conn.prepareStatement(sql);
    pstmt.setString(1, username); // 输入作为参数传入,自动转义
    
  2. 严格过滤和转义用户输入
    对用户输入的特殊字符(如单引号'、分号;、注释符--等)进行转义或过滤,确保其无法改变 SQL 语句的结构。不同数据库的转义规则不同(如 MySQL 用\'转义单引号),需针对性处理。

  3. 限制数据库账号权限
    应用程序连接数据库时,应使用权限最小的账号(如仅授予SELECTINSERT等必要权限,禁止DROPEXEC等高危操作),即使发生注入,也能降低危害范围。

  4. 使用 ORM 框架
    多数 ORM(对象关系映射)框架(如 Hibernate、MyBatis)默认采用参数化查询,可减少手动拼接 SQL 的风险,但需注意避免在框架中使用 “原生 SQL 拼接” 功能。

请描述Spring Boot自动装配的过程

Spring Boot自动装配的过程大致为:通过@SpringBootApplication注解开启自动装配,该注解包含的@EnableAutoConfiguration会触发SpringFactoriesLoader加载类路径下META-INF/spring.factories文件中注册的自动配置类,这些类通过@Conditional系列注解根据类路径下是否存在特定依赖、是否有特定Bean等条件决定是否生效,最终将符合条件的Bean定义加载到Spring容器中,实现自动配置。

你对MySQL的慢查询优化有了解吗

1. 识别慢查询

首先需准确定位慢查询 SQL:

  • 开启慢查询日志(slow_query_log = 1),通过long_query_time设置阈值(默认 10 秒,可根据业务调整为 1-2 秒),记录执行超时的 SQL;
  • 实时查看:用show processlist查看当前运行的 SQL,关注Time字段(执行时间)和State字段(当前状态,如Sending data可能表示扫描行数多);
  • 工具分析:通过pt-query-digest(Percona Toolkit)或 MySQL Workbench 的 Performance Schema,统计慢查询的频率、耗时分布,锁定高频低效 SQL。

2.分析慢查询原因

常见原因包括:

  • 索引问题:无索引、索引失效(如使用函数 / 表达式操作索引列、隐式类型转换、like '%xxx'左模糊匹配、or连接非索引列等)、索引设计不合理(如选择性低的列建索引);
  • SQL 写法问题select *查询冗余字段、子查询嵌套过深、join表过多或关联条件无索引、order by/group by未使用索引导致文件排序;
  • 表结构问题:表数据量过大(未分表)、字段类型不合理(如用varchar存数字)、表碎片过多;
  • 数据库配置:缓冲池(innodb_buffer_pool_size)过小导致频繁磁盘 IO、连接数不足等。

3.针对性优化措施

  • 索引优化:为过滤条件、排序 / 分组字段、join 关联字段创建合适索引(如联合索引需遵循最左前缀原则);删除冗余 / 无效索引;避免索引失效场景(如where substr(name,1,3)='abc'改为where name like 'abc%')。
  • SQL 改写:避免select *,只查必要字段;将子查询改为join(减少临时表生成);限制join表数量(建议不超过 3-4 张);order by/group by尽量使用索引排序,避免using filesort
  • 表结构优化:大表分表(水平分表按时间 / ID,垂直分表拆分冷热字段);使用合适字段类型(如用int存状态而非varchar);定期optimize table清理碎片。
  • 配置调优:调大innodb_buffer_pool_size(建议占可用内存的 50%-70%),减少磁盘 IO;调整sort_buffer_size/join_buffer_size优化排序和连接性能。
  • 架构层面:引入缓存(如 Redis)减轻热点查询压力;读写分离(主库写、从库读)分散负载;对超大型表考虑分库或使用时序数据库等专用存储。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    public List<String> topKFrequent(String[] words, int k) {
        // 1. 统计每个单词的出现频率
        Map<String, Integer> countMap = new HashMap<>();
        for (String word : words) {
            countMap.put(word, countMap.getOrDefault(word, 0) + 1);
        }
        
        // 2. 将所有单词放入列表
        List<String> candidates = new ArrayList<>(countMap.keySet());
        
        // 3. 自定义排序规则
        Collections.sort(candidates, (a, b) -> {
            int countA = countMap.get(a);
            int countB = countMap.get(b);
            // 频率不同时,频率高的排在前面
            if (countA != countB) {
                return countB - countA;
            } else {
                // 频率相同时,字典序小的排在前面
                return a.compareTo(b);
            }
        });
        
        // 4. 取前k个元素
        return candidates.subList(0, k);
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

 public static int[][] findNearerSmallerElements(int[] nums) {
        int n = nums.length;
        int[][] result = new int[n][2];
        Stack<Integer> stack = new Stack<>();

        // 找左边最近更小的元素
        for (int i = 0; i < n; i++) {
            while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
                stack.pop();
            }
            result[i][0] = stack.isEmpty()? -1 : stack.peek();
            stack.push(i);
        }

        stack.clear();

        // 找右边最近更小的元素
        for (int i = n - 1; i >= 0; i--) {
            while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
                stack.pop();
            }
            result[i][1] = stack.isEmpty()? -1 : stack.peek();
            stack.push(i);
        }

        return result;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

private int checkBalance(TreeNode node) {
        if (node == null) {
            // 空树的高度为 0
            return 0;
        }

        // 递归检查左子树是否平衡,并获取左子树的高度
        int leftHeight = checkBalance(node.left);
        if (leftHeight == -1) {
            return -1;
        }

        // 递归检查右子树是否平衡,并获取右子树的高度
        int rightHeight = checkBalance(node.right);
        if (rightHeight == -1) {
            return -1;
        }

        // 检查当前节点的左右子树高度差是否超过 1
        if (Math.abs(leftHeight - rightHeight) > 1) {
            return -1;
        }

        // 返回当前节点为根的二叉树的高度
        return Math.max(leftHeight, rightHeight) + 1;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

 private TreeNode buildBST(int[] nums, int left, int right) {
        // 递归终止条件,区间无效时返回 null
        if (left > right) {
            return null;
        }
        // 取区间中间位置的元素作为根节点(选左中位数,也可选右中位数,影响树的形态但都平衡)
        int mid = left + (right - left) / 2;
        TreeNode root = new TreeNode(nums[mid]);
        // 递归构建左子树,区间为 [left, mid - 1]
        root.left = buildBST(nums, left, mid - 1);
        // 递归构建右子树,区间为 [mid + 1, right]
        root.right = buildBST(nums, mid + 1, right);
        return root;
    }
8.9
如何打开一个大文件
  1. 核心原则:避免一次性加载全量数据

大文件(如几十 GB 的日志、数据文件)的特点是体积远超 Java 虚拟机(JVM)的内存容量。若直接使用File类的方法将文件内容全部读入byte[]String,会瞬间占用大量内存,触发OutOfMemoryError。因此,Java 处理大文件的核心是按需读取,即每次只加载文件的一小部分到内存。

  1. 核心类与机制:基于 “流” 的读取

Java 的 I/O 体系中,输入流(InputStream) 是处理大文件的基础,其设计初衷就是 “逐字节 / 逐块” 读取数据,而非一次性加载。

  • 字节流 vs 字符流
    • 字节流(如FileInputStream):直接处理原始字节,适用于任意类型文件(二进制文件、文本文件)。
    • 字符流(如FileReader):专为文本文件设计,会自动处理字符编码(需注意编码一致性),本质上是字节流的包装,内部通过缓冲区处理字符转换。
  • 缓冲机制
    无论是BufferedInputStream还是BufferedReader,都通过内置缓冲区减少磁盘 IO 次数(内存读写远快于磁盘)。例如,BufferedReaderreadLine()方法会一次读取一行数据到缓冲区,避免逐字节读取的低效。
  1. 处理方式:按需求分块读取
  • 逐行读取:对于文本文件(如日志、CSV),可使用BufferedReaderreadLine()方法,每次读取一行并处理,处理完后释放该行长的内存,再读取下一行,内存占用始终保持在单行数据的量级。
  • 固定大小分块读取:对于二进制文件(如视频、大型压缩包),可通过InputStreamread(byte[] buffer)方法,指定缓冲区大小(如 8KB、1MB),每次读取固定字节数到缓冲区,处理完后重复读取,直到文件末尾。
  • 随机访问:若需跳转到文件特定位置读取(如大文件的中间部分),可使用RandomAccessFile类,通过seek(long pos)方法定位到目标位置,再进行局部读取,避免读取无关内容。
介绍一下数据库分页

数据库分页是处理大量数据查询时的一种优化技术,用于将查询结果按固定大小拆分,每次只返回一部分数据(一页),而非一次性加载全部结果。

核心目的

  1. 减少数据传输量:避免一次性返回数万甚至数百万条记录,降低网络带宽消耗。
  2. 降低内存占用:客户端和服务器无需加载全部数据,减少内存压力。
  3. 提升响应速度:小批量数据处理更快,用户无需等待全部数据加载完成。

注意事项

偏移量过大的性能问题
当页码过大(如第 10000 页),LIMIT 100000, 10 这类查询需要扫描大量前置数据,效率低下。解决方案:

  • 基于游标(Cursor)的分页(不支持跳页,仅支持上下页)

    :以上一页最后一条记录的标识(如id)作为下一页的起点,避免计算偏移量。例如:

    -- 上一页最后一条id为100,查询下一页10条
    SELECT * FROM users WHERE id > 100 ORDER BY id LIMIT 10;
    
内存溢出问题该如何解决
  1. 堆内存溢出(Java heap space)
  • 若为内存泄漏(对象无用但未回收)
    • 找到泄漏点:通过堆分析工具定位持有对象的长期引用(如静态List未清空、单例中缓存未过期、监听器未移除)。
    • 修复引用关系:及时移除无用引用(如list.clear())、使用弱引用(WeakHashMap)缓存临时对象、避免静态集合无限制存储数据。
  • 若为正常内存需求(无泄漏,但对象确实多)
    • 优化对象创建:减少临时对象(如循环中复用对象)、使用基本类型(int替代Integer)、避免创建过大对象(如超大byte[]可分片处理)。
    • 调整堆内存参数:根据应用需求增大堆大小(-Xms初始堆、-Xmx最大堆,建议两者设为相同避免动态调整),例如:-Xms2G -Xmx2G(堆大小固定为 2GB)。
    • 优化 GC:选择适合的垃圾回收器(如大堆用 G1、ZGC),通过-XX:MaxGCPauseMillis控制 GC 停顿时间。
  1. 栈溢出(StackOverflowError)
  • 递归过深:重构递归逻辑为循环(如用while替代递归),或增加递归终止条件,减少调用层级。
  • 多线程栈溢出:减少单个线程的栈大小(-Xss参数,如-Xss256k,默认 1MB),或控制线程数量(使用线程池限制最大线程数)。
  1. 元空间 / 永久代溢出
  • 减少类加载:避免频繁动态生成类(如 CGLib 代理可缓存生成的类)、清理无用的类加载器(如自定义类加载器使用后需释放引用)。
  • 调整元空间大小:通过-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m增大元空间(JDK 8+),或-XX:PermSize=256m -XX:MaxPermSize=512m(JDK 7 及之前)。
  1. 直接内存溢出
  • 释放直接内存ByteBuffer的直接内存需通过Cleaner机制回收,确保不再使用的ByteBuffer被及时置为null,并触发 GC(可调用System.gc(),但不推荐频繁使用)。
  • 调整直接内存上限:通过-XX:MaxDirectMemorySize=512m增大直接内存限制(默认与堆最大值相同)。
常见的HTTP协议响应头有哪些?

一、通用响应头

适用于所有 HTTP 请求 / 响应场景,描述基本传输信息。

  • Date
    服务器生成响应的时间,格式为 GMT(格林尼治标准时间)。
    示例:Date: Sun, 10 Aug 2025 12:00:00 GMT
  • Server
    服务器的软件信息(可选,可能被隐藏以增强安全性)。
    示例:Server: Nginx/1.21.0Server: Apache
  • Connection
    指示连接状态,如是否保持长连接。
    常见值:Connection: keep-alive(保持连接)、Connection: close(关闭连接)。

二、内容相关响应头

描述响应体的类型、编码、长度等信息。

  • Content-Type
    响应体的 MIME 类型(数据格式),可能包含字符编码。
    示例:
    • 文本:Content-Type: text/html; charset=UTF-8
    • JSON:Content-Type: application/json
    • 图片:Content-Type: image/jpeg
  • Content-Length
    响应体的字节长度(仅用于非分块传输)。
    示例:Content-Length: 1024
  • Content-Encoding
    响应体的压缩编码方式(客户端需解码)。
    常见值:gzipdeflatebr(Brotli)。
    示例:Content-Encoding: gzip
  • Content-Language
    响应内容的自然语言(供客户端选择展示)。
    示例:Content-Language: zh-CNContent-Language: en-US

三、缓存控制响应头

控制客户端、代理服务器如何缓存响应内容。

  • Cache-Control
    最核心的缓存控制头,支持多个指令组合。
    常见指令:
    • public:允许任何缓存(客户端、代理)存储。
    • private:仅客户端可缓存,代理不可。
    • max-age=3600:缓存有效时间(秒),优先级高于Expires
    • no-cache:需验证(如 ETag)后再使用缓存。
    • no-store:禁止缓存(敏感数据)。
      示例:Cache-Control: public, max-age=86400
  • Expires
    缓存过期的具体时间(GMT),优先级低于Cache-Control: max-age
    示例:Expires: Mon, 11 Aug 2025 12:00:00 GMT
  • ETag
    响应体的唯一标识(类似指纹),用于验证缓存有效性。
    客户端下次请求时可通过If-None-Match携带此值,服务器判断是否返回 304(未修改)。
    示例:ETag: "abc123"
  • Last-Modified
    资源最后修改时间(GMT),类似 ETag 的作用,客户端通过If-Modified-Since验证。
    示例:Last-Modified: Sun, 10 Aug 2025 10:00:00 GMT

四、跨域资源共享(CORS)响应头

当客户端跨域请求时,服务器通过这些头允许或限制访问。

  • Access-Control-Allow-Origin
    允许访问的源(域名),*表示允许所有源(不建议用于带认证的请求)。
    示例:Access-Control-Allow-Origin: https://example.com
  • Access-Control-Allow-Methods
    允许的 HTTP 方法(跨域预检请求时返回)。
    示例:Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  • Access-Control-Allow-Headers
    允许的请求头(跨域预检请求时返回)。
    示例:Access-Control-Allow-Headers: Content-Type, Authorization
  • Access-Control-Allow-Credentials
    指示是否允许客户端携带认证信息(如 Cookie),需与Access-Control-Allow-Origin配合(不能为*)。
    示例:Access-Control-Allow-Credentials: true

五、重定向响应头

用于指示客户端跳转至新 URL(配合 3xx 状态码使用)。

  • Location
    重定向的目标 URL,常见于 301(永久重定向)、302(临时重定向)、307(临时重定向,方法不变)等状态。
    示例:Location: https://new-example.com/page

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode p1 = headA, p2 = headB;
        while (p1 != p2) {
            // p1 遍历完 headA 后,切换到 headB 继续遍历
            p1 = p1 == null ? headB : p1.next;
            // p2 遍历完 headB 后,切换到 headA 继续遍历
            p2 = p2 == null ? headA : p2.next;
        }
        return p1;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public class SortAlgorithms {
    // 冒泡排序
    public static int[] bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    // 交换元素
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        return arr;
    }
}

public class SortAlgorithms {
    // 快速排序
    public static int[] quickSort(int[] arr) {
        quickSortHelper(arr, 0, arr.length - 1);
        return arr;
    }

    private static void quickSortHelper(int[] arr, int left, int right) {
        if (left < right) {
            int pivotIndex = partition(arr, left, right);
            quickSortHelper(arr, left, pivotIndex - 1);
            quickSortHelper(arr, pivotIndex + 1, right);
        }
    }

    private static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int i = left - 1;
        for (int j = left; j < right; j++) {
            if (arr[j] <= pivot) {
                i++;
                // 交换元素
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
        // 交换基准元素和 i+1 位置的元素
        int temp = arr[i + 1];
        arr[i + 1] = arr[right];
        arr[right] = temp;
        return i + 1;
    }
}

public class SortAlgorithms {
    // 希尔排序
    public static int[] shellSort(int[] arr) {
        int n = arr.length;
        for (int gap = n / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < n; i++) {
                int temp = arr[i];
                int j;
                for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                    arr[j] = arr[j - gap];
                }
                arr[j] = temp;
            }
        }
        return arr;
    }
}

public class SortAlgorithms {
    // 堆排序
    public static int[] heapSort(int[] arr) {
        int n = arr.length;
        // 构建最大堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }
        // 从最后一个元素开始,逐个将堆顶元素与末尾元素交换,并调整堆结构
        for (int i = n - 1; i > 0; i--) {
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            heapify(arr, i, 0);
        }
        return arr;
    }

    private static void heapify(int[] arr, int n, int i) {
        int largest = i;
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        if (left < n && arr[left] > arr[largest]) {
            largest = left;
        }
        if (right < n && arr[right] > arr[largest]) {
            largest = right;
        }
        if (largest != i) {
            int temp = arr[i];
            arr[i] = arr[largest];
            arr[largest] = temp;
            heapify(arr, n, largest);
        }
    }
}

public class SortAlgorithms {
    // 桶排序
    public static int[] bucketSort(int[] arr) {
        if (arr.length == 0) {
            return arr;
        }
        int minVal = arr[0];
        int maxVal = arr[0];
        for (int num : arr) {
            minVal = Math.min(minVal, num);
            maxVal = Math.max(maxVal, num);
        }
        int bucketCount = (maxVal - minVal) / arr.length + 1;
        List<List<Integer>> buckets = new ArrayList<>(bucketCount);
        for (int i = 0; i < bucketCount; i++) {
            buckets.add(new ArrayList<>());
        }
        for (int num : arr) {
            int bucketIndex = (num - minVal) / arr.length;
            buckets.get(bucketIndex).add(num);
        }
        int index = 0;
        for (List<Integer> bucket : buckets) {
            Collections.sort(bucket);
            for (int num : bucket) {
                arr[index++] = num;
            }
        }
        return arr;
    }
}
8.10
说一说 select 的原理以及缺点

一、Java 中 Select 的原理

Java NIO 的 select 机制本质上是对操作系统底层 I/O 多路复用(如 Linux 的 select/poll/epoll、Windows 的 IOCP 等)的封装,其核心流程如下:

  1. 核心组件
  • Selector:选择器,负责监控多个通道的就绪状态;
  • SelectableChannel:可被选择器监控的通道(如 SocketChannelServerSocketChannel),必须设置为非阻塞模式(configureBlocking(false))才能注册到选择器;
  • SelectionKey:通道注册到选择器时返回的键,包含通道、选择器、感兴趣的事件(interest set)和就绪事件(ready set)等信息。
  1. 工作流程

  2. 创建选择器:通过 Selector.open() 创建一个 Selector 实例;

  3. 注册通道到选择器
    可选择的通道(如 SocketChannel)通过 register(Selector sel, int ops) 方法注册到选择器,其中 ops 表示感兴趣的事件(通过 SelectionKey 的常量定义):

    • SelectionKey.OP_READ:通道可读(如接收数据);
    • SelectionKey.OP_WRITE:通道可写(如发送缓冲区空闲);
    • SelectionKey.OP_CONNECT:客户端连接成功;
    • SelectionKey.OP_ACCEPT:服务器接收新连接。
      注册后返回 SelectionKey,用于后续关联通道和选择器。
  4. 调用 select() 阻塞等待就绪事件
    选择器通过 select() 方法阻塞等待,直到至少一个注册的通道就绪(或被唤醒)。底层会调用操作系统的 I/O 多路复用接口(如 epoll_wait),由内核监控通道状态。

  5. 处理就绪事件
    select() 返回后,通过 selectedKeys() 获取所有就绪的 SelectionKey 集合,遍历集合并通过 readyOps() 判断具体就绪事件(如 key.isReadable()),然后进行相应的 I/O 操作(如读取数据)。

二、Java 中 Select 的缺点

尽管 Selector 解决了传统 BIO(阻塞 I/O)中 “一连接一线程” 的资源浪费问题,但仍存在以下局限性:

  1. 空轮询问题(JDK 早期 bug)

在某些 JDK 版本(如 1.4.x-1.6.x)中,select() 可能会在没有任何通道就绪的情况下返回,导致线程陷入无限空轮询,占用 100% CPU。
这是由于底层操作系统(如 Linux)的 epoll 机制在特定场景下的信号处理缺陷,JDK 后续版本通过引入 “轮询次数限制 + 重建选择器” 的策略修复了该问题,但仍需开发者在代码中额外处理(如检测空轮询并重建 Selector)。

  1. 通道注册与注销的开销
  • 通道注册到选择器时,需要在用户态与内核态之间传递数据(如文件描述符、事件掩码),存在一定开销;
  • 注销通道(或取消 SelectionKey)后,选择器需要清理内部数据结构,频繁的注册 / 注销会降低效率。
  1. 单个线程处理所有事件的瓶颈

虽然 Selector 允许单线程处理多通道,但当并发量极高(如十万级连接)时,单线程处理所有就绪事件会成为瓶颈 ——I/O 操作(即使是非阻塞)和事件分发的累计耗时可能导致响应延迟。
解决方式通常是引入多线程池,将就绪事件分配给工作线程处理,但会增加代码复杂度。

  1. 对大文件传输的优化有限

Selector 更适合处理 “小数据、高并发” 的场景(如网络通信)。对于大文件传输等 “大数据、低并发” 场景,其优势不明显,甚至可能因频繁的事件通知和用户态 / 内核态切换导致效率低于传统 BIO。

  1. 编程模型复杂

相比 BIO 的同步阻塞模型,NIO 的 Selector 机制需要开发者手动管理通道注册、事件轮询和就绪事件处理,涉及 SelectionKey 状态维护、非阻塞 I/O 异常处理等细节,容易出现 bugs(如漏处理事件、重复注册通道)。

HTTP(超文本传输协议)是什么?

HTTP(超文本传输协议)是互联网中客户端与服务器间的核心通信协议,基于请求 - 响应模式工作,运行在应用层且多依赖 TCP/IP 传输数据,其核心是实现超文本(包括文本、图片等资源及链接)的传输。

TCP协议三次握手和四次挥手的过程

一、三次握手(建立连接)

三次握手的目的是让客户端和服务器确认彼此的发送和接收能力,协商初始序列号,并建立可靠连接。过程如下:

  1. 第一次握手(客户端 → 服务器)
    客户端主动发起连接,发送一个 SYN(同步)报文段:
    • 标志位:SYN = 1(表示请求建立连接);
    • 序号:seq = x(客户端生成的初始随机序列号)。
      此时客户端进入 SYN_SENT 状态,等待服务器响应。
  2. 第二次握手(服务器 → 客户端)
    服务器收到 SYN 后,确认可建立连接,返回 SYN + ACK(同步 + 确认)报文段:
    • 标志位:SYN = 1ACK = 1(表示同意连接,并确认收到客户端的请求);
    • 序号:seq = y(服务器生成的初始随机序列号);
    • 确认号:ack = x + 1(表示已收到客户端的 seq = x,下一次期望接收 x + 1)。
      此时服务器进入 SYN_RCVD 状态。
  3. 第三次握手(客户端 → 服务器)
    客户端收到 SYN + ACK 后,发送一个 ACK(确认)报文段:
    • 标志位:ACK = 1
    • 序号:seq = x + 1(按客户端序列号递增);
    • 确认号:ack = y + 1(表示已收到服务器的 seq = y,下一次期望接收 y + 1)。
      客户端发送后进入 ESTABLISHED(连接建立)状态;服务器收到 ACK 后也进入 ESTABLISHED 状态,双方开始传输数据。

二、四次挥手(终止连接)

TCP 是全双工通信(双方可同时发送数据),关闭连接时需双方分别确认 “不再发送数据”,因此需要四次交互:

  1. 第一次挥手(主动关闭方 → 被动关闭方)
    假设客户端主动关闭连接,发送 FIN(结束)报文段:
    • 标志位:FIN = 1(表示客户端不再发送数据);
    • 序号:seq = u(客户端当前的序列号)。
      客户端进入 FIN_WAIT_1 状态,等待服务器确认。
  2. 第二次挥手(被动关闭方 → 主动关闭方)
    服务器收到 FIN 后,返回 ACK 报文段确认:
    • 标志位:ACK = 1
    • 序号:seq = v(服务器当前的序列号);
    • 确认号:ack = u + 1(表示已收到客户端的终止请求)。
      此时服务器进入 CLOSE_WAIT 状态,客户端收到后进入 FIN_WAIT_2 状态。
      (注:此时服务器可能仍有数据需发送给客户端,客户端需继续接收)
  3. 第三次挥手(被动关闭方 → 主动关闭方)
    服务器完成所有数据发送后,主动发送 FIN 报文段:
    • 标志位:FIN = 1ACK = 1(表示服务器也不再发送数据);
    • 序号:seq = w(服务器当前的序列号,可能因发送数据而更新);
    • 确认号:ack = u + 1(与第二次挥手一致)。
      服务器进入 LAST_ACK 状态,等待客户端确认。
  4. 第四次挥手(主动关闭方 → 被动关闭方)
    客户端收到服务器的 FIN 后,发送 ACK 报文段确认:
    • 标志位:ACK = 1
    • 序号:seq = u + 1
    • 确认号:ack = w + 1(表示已收到服务器的终止请求)。
      客户端进入 TIME_WAIT 状态(等待 2MSL 时间,确保服务器收到确认,避免残留报文影响新连接),服务器收到 ACK 后进入 CLOSED 状态。客户端等待超时后,也进入 CLOSED 状态,连接彻底关闭。
请你说说MySQL的ACID特性分别是怎么实现的?
  1. 原子性(Atomicity):要么全执行,要么全不执行

原子性确保事务中的所有操作是一个不可分割的整体,要么全部成功提交,要么在发生错误时全部回滚。
实现核心:undo 日志(回滚日志)

  • undo 日志记录了事务执行前的数据状态(即 “反向操作”),例如插入操作对应删除记录的日志,更新操作对应恢复原值的日志。
  • 当事务执行失败(如异常、手动回滚)时,MySQL 通过 undo 日志逆向执行操作,将数据恢复到事务开始前的状态,保证 “要么全做,要么全不做”。
  • 事务提交后,undo 日志会被标记为可删除,不再用于回滚。
  1. 一致性(Consistency):事务前后数据状态合法一致性指事务执行前后,数据库中的数据必须满足预设的约束(如主键唯一、外键关联、业务规则等),始终处于 “合法状态”。
    实现核心:多机制协同保障
  • 原子性、隔离性、持久性是一致性的基础:原子性确保错误时回滚到合法状态,隔离性避免并发干扰,持久性确保提交后状态稳定。
  • 数据库内置约束:主键、外键、CHECK 约束、唯一索引等,会在事务执行中自动校验(如插入重复主键会直接失败)。
  • 应用层逻辑:业务代码需保证事务内操作符合业务规则(如转账时 “转出金额不能大于余额”),这是一致性的上层保障。
  1. 隔离性(Isolation):并发事务互不干扰

隔离性指多个事务并发执行时,每个事务的操作应与其他事务隔离开,避免 “脏读”“不可重复读”“幻读” 等问题。
实现核心:锁机制 + MVCC(多版本并发控制)

  • 锁机制:
    • 行锁(InnoDB 基于索引实现):对修改的行加锁,避免并发写冲突(如两个事务同时更新同一行)。
    • 表锁:对整个表加锁(如 MyISAM 默认表锁),适用于全表操作,但并发度低。
    • 意向锁、间隙锁等:辅助行锁实现更精细的隔离,防止幻读(如间隙锁锁定索引区间,避免插入新行)。
  • MVCC:
    通过 undo 日志保存数据的多个版本,配合 “Read View”(读视图)控制事务可见的数据版本。
    例如,读已提交(RC)级别下,事务每次查询都会生成新的 Read View,只能看到已提交的版本;可重复读(RR)级别下,事务开始时生成一次 Read View,保证多次查询看到相同版本,避免不可重复读。
  • 隔离级别:MySQL 通过锁和 MVCC 的组合实现 4 种隔离级别(读未提交、读已提交、可重复读、串行化),级别越高,并发度越低,一致性越强。
  1. 持久性(Durability):事务提交后数据永久保存

持久性指事务提交后,其修改的数据应永久保存,即使发生数据库崩溃、断电等故障也不会丢失。
实现核心:redo 日志 + 双写缓冲区(Double Write Buffer)

  • redo 日志:记录数据页的物理修改(如 “某页某偏移量的值从 A 改为 B”),事务执行时先写入 redo 日志缓冲区,提交时通过 “force log at commit” 机制将日志刷入磁盘(确保日志先于数据持久化)。
    若数据库崩溃,重启后可通过 redo 日志重做已提交的事务,恢复数据。
  • 双写缓冲区:解决 “部分写页” 问题(如写入数据页时断电,导致页数据不完整)。InnoDB 先将数据页写入双写缓冲区(连续磁盘空间),再刷入数据文件,确保即使写入失败,也能从双写缓冲区恢复完整页数据。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        // 先对数组进行排序,方便后续剪枝去重
        Arrays.sort(nums);
        boolean[] used = new boolean[nums.length];
        backtrack(nums, used, new ArrayList<>(), result);
        return result;
    }

    private void backtrack(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result) {
        if (path.size() == nums.length) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            // 剪枝条件:当前元素和前一个元素相同,且前一个元素未被使用过(说明前一个元素在本次回溯中是同一层被跳过的,会导致重复排列)
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }
            if (!used[i]) {
                used[i] = true;
                path.add(nums[i]);
                backtrack(nums, used, path, result);
                used[i] = false;
                path.remove(path.size() - 1);
            }
        }
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

 public static void rotate(int[] arr, int M) {
        int n = arr.length;
        if (n == 0 || M == 0) return; // 边界:空数组或移动0位,直接返回
        
        // 1. 优化 M:取模避免重复移动(M 可能大于 n)
        M = M % n;
        if (M == 0) return; // M 是 n 的倍数时,无需移动
        
        // 2. 三次反转
        reverse(arr, 0, n - 1);    // 整体反转
        reverse(arr, 0, M - 1);    // 反转前 M 位
        reverse(arr, M, n - 1);    // 反转剩余 n-M 位
    }
    
    // 辅助方法:反转数组指定区间 [start, end]
    private static void reverse(int[] arr, int start, int end) {
        while (start < end) {
            // 交换 start 和 end 位置的元素
            int temp = arr[start];
            arr[start] = arr[end];
            arr[end] = temp;
            start++;
            end--;
        }
    }
8.11
HTTPS协议中间人攻击是什么?

HTTPS 协议的中间人攻击(Man-in-the-Middle Attack,简称 MITM) 是指攻击者通过技术手段,秘密介入客户端与服务器之间的 HTTPS 加密通信,伪装成 “可信第三方” 同时与客户端和服务器建立连接,从而窃取、篡改甚至伪造通信数据的攻击方式。

一、攻击核心原理

HTTPS 的安全基础是SSL/TLS 加密协议数字证书验证机制:客户端与服务器通过 SSL/TLS 协商加密算法、交换会话密钥,后续数据通过密钥加密传输;同时,服务器需向客户端出示由权威 CA(证书颁发机构)签发的数字证书,证明自身身份,客户端通过验证证书有效性确保通信对象真实。

而中间人攻击的核心是打破 “身份验证” 和 “密钥机密性”
攻击者通过欺骗手段,让客户端误认为攻击者是服务器,同时让服务器误认为攻击者是客户端,从而在客户端与服务器之间构建 “双向伪装” 的通信链路。具体来说:

  1. 攻击者拦截客户端向服务器发起的 HTTPS 连接请求;
  2. 攻击者伪装成 “服务器”,向客户端发送伪造的数字证书,并与客户端协商加密会话(客户端误以为在和真实服务器通信);
  3. 同时,攻击者伪装成 “客户端”,使用真实服务器的证书(或再次伪造)与服务器建立加密会话(服务器误以为在和真实客户端通信);
  4. 客户端与服务器之间的加密数据会先发送给攻击者,攻击者解密后获取内容(或篡改),再用另一端的密钥重新加密后转发,最终客户端和服务器均无法察觉中间人的存在。

二、攻击完整流程(以浏览器访问网站为例)

  1. 客户端发起请求:用户在浏览器输入https://example.com,向服务器发起 HTTPS 连接请求。
  2. 攻击者拦截并伪装:
    • 攻击者拦截请求,向客户端返回伪造的服务器证书(例如,伪造example.com的证书,或使用自签证书);
    • 客户端若未严格验证证书(如用户手动忽略浏览器警告),会与攻击者建立 SSL/TLS 连接,生成客户端与攻击者之间的会话密钥。
  3. 攻击者与服务器建立连接:
    • 攻击者同时以 “客户端” 身份向真实服务器发起 HTTPS 请求,获取服务器的真实证书并建立连接,生成攻击者与服务器之间的会话密钥。
  4. 数据中转与窃取:
    • 客户端发送的加密数据(用客户端 - 攻击者的密钥加密)被攻击者解密,攻击者可查看或修改内容;
    • 攻击者用攻击者 - 服务器的密钥重新加密数据,转发给服务器;
    • 服务器的响应数据则以相反流程经过攻击者,最终客户端和服务器均认为通信是 “直接且加密” 的,但数据已被攻击者掌控。

三、攻击成功的关键条件

中间人攻击能成功,本质是绕过了 HTTPS 的证书验证机制,常见场景包括:

  • 客户端主动忽略证书警告:浏览器检测到证书无效(如伪造、过期、签名错误)时会弹出警告,但用户可能点击 “继续访问”,导致攻击者证书被信任。
  • 恶意根证书被植入:攻击者通过病毒、恶意软件等在客户端设备(如电脑、手机)中安装自己的 “根证书”(即攻击者自建的 CA 证书)。由于客户端会信任系统中已安装的根证书,攻击者用该根证书签发的伪造服务器证书会被客户端视为 “有效”,直接绕过验证。
  • SSL/TLS 协议漏洞:若客户端或服务器使用存在漏洞的 SSL/TLS 版本(如 SSLv3、TLS 1.0)或弱加密算法(如 RC4),攻击者可能通过破解加密密钥直接获取数据(例如 “心脏滴血” 漏洞曾被用于窃取会话密钥)。

四、HTTPS 如何防范中间人攻击?

HTTPS 的核心防御机制是证书链验证,具体包括:

  1. 验证证书颁发者:服务器证书必须由客户端信任的 CA(如 Let’s Encrypt、DigiCert 等)签发,客户端会检查证书的 “签名链” 是否追溯到系统中预存的可信根证书。
  2. 验证证书有效性:检查证书是否过期、是否被 CA 吊销(通过 CRL 或 OCSP 协议查询)、证书中的域名是否与访问域名一致(防止 “域名劫持”)。
  3. 拒绝自签证书:未由权威 CA 签发的自签证书会被客户端直接标记为 “无效”(除非用户手动信任)。
说一说你对MVCC的了解

详见Mysql面试总结。

说一说ConcurrentHashMap的实现原理

详见Java集合面试总结。

UDP(用户数据报协议)是什么?

UDP(User Datagram Protocol,用户数据报协议)是 TCP/IP 协议族中一种无连接、不可靠、面向数据报的传输层协议,与 TCP(传输控制协议)共同承担数据传输的核心功能,但设计理念和适用场景截然不同。

一、UDP 的核心特性

UDP 的设计追求简单、高效和低延迟,牺牲了部分可靠性,具体特性如下:

  1. 无连接性
    • 通信前无需建立连接(如 TCP 的三次握手),也无需断开连接(四次挥手)。客户端直接向目标服务器发送数据报,服务器收到后无需确认 “已准备好接收”。
    • 类比:如同发送邮件,无需提前告知收件人 “我要发邮件了”,直接投递即可。
  2. 不可靠传输
    • 不保证数据的完整性:数据在传输中可能丢失、重复或乱序,UDP 自身不提供重传机制。
    • 不提供流量控制拥塞控制:发送方不会根据网络状况调整发送速率,可能导致网络拥塞时数据丢失更严重。
    • 仅提供最基本的校验和:用于检测数据在传输中是否被篡改,若校验失败则直接丢弃数据,不通知发送方。
  3. 面向数据报
    • 数据以 “数据报” 为单位传输,每个数据报包含完整的源端口、目标端口、长度、校验和以及数据部分。
    • 发送方一次发送一个数据报,接收方一次接收一个完整数据报,不会像 TCP 那样将数据拆分为多个片段再重组(UDP 也可能分片,但由 IP 层处理,而非 UDP 自身)。
  4. 高效低延迟
    • 由于省去了连接建立 / 断开、确认、重传等机制,UDP 的协议开销极小,数据传输速度快,延迟低。
    • 头部结构简单(仅 8 字节),远小于 TCP 的 20 字节(最小)头部。

二、UDP 的头部结构

UDP 数据报的头部固定为 8 字节,包含 4 个字段:

字段长度(字节)作用
源端口号2标识发送端的端口(可选,若为 0 则表示不需要对方回复)
目标端口号2标识接收端的端口,用于将数据交付给正确的应用程序
数据报长度2整个 UDP 数据报的总长度(头部 + 数据),最大值为 65535 字节(含头部 8 字节)
校验和2用于校验数据报在传输中是否被损坏(可选,部分场景可能不启用)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public int countK(int[] nums, int k) {
        // 寻找第一个等于 k 的位置
        int first = findFirst(nums, k);
        // 寻找最后一个等于 k 的位置
        int last = findLast(nums, k);
        if (first == -1 || last == -1) {
            return 0;
        }
        return last - first + 1;
    }

    // 二分查找第一个等于 k 的位置
    private int findFirst(int[] nums, int k) {
        int left = 0, right = nums.length - 1;
        int result = -1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == k) {
                result = mid;
                right = mid - 1;
            } else if (nums[mid] < k) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    }

    // 二分查找最后一个等于 k 的位置
    private int findLast(int[] nums, int k) {
        int left = 0, right = nums.length - 1;
        int result = -1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == k) {
                result = mid;
                left = mid + 1;
            } else if (nums[mid] < k) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

 public static List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> results = new ArrayList<>();
        // 排序,以便去重和按非递减顺序生成组合
        Arrays.sort(candidates);
        // 回溯搜索
        backtrack(candidates, target, 0, new ArrayList<>(), results);
        return results;
    }

    /**
     * 回溯搜索函数
     * @param candidates  候选数组(已排序)
     * @param target      目标和
     * @param start       开始搜索的索引
     * @param combination 当前组合
     * @param results     所有满足条件的组合
     */
    private static void backtrack(int[] candidates, int target, int start, List<Integer> combination, List<List<Integer>> results) {
        // 如果当前组合的和等于目标值,加入结果集
        if (target == 0) {
            results.add(new ArrayList<>(combination));
            return;
        }

        // 遍历候选数
        for (int i = start; i < candidates.length; i++) {
            // 跳过重复元素,避免生成重复组合
            if (i > start && candidates[i] == candidates[i - 1]) {
                continue;
            }
            // 当前数字
            int num = candidates[i];
            // 如果当前数字大于目标值,后续数字也不可能满足(因为数组已排序)
            if (num > target) {
                break;
            }
            // 选择当前数字
            combination.add(num);
            // 递归搜索,注意 i + 1(每个数字只能使用一次)
            backtrack(candidates, target - num, i + 1, combination, results);
            // 回溯,移除当前数字
            combination.remove(combination.size() - 1);
        }
    }
8.12
结合OSI模型和TCP/IP模型的五层协议体系结构
五层协议模型OSI 七层模型TCP/IP 四层模型
应用层应用层、表示层、会话层应用层
运输层运输层运输层
网络层网络层网际层(IP 层)
数据链路层数据链路层网络接口层
物理层物理层(包含在网络接口层)
说一说线程的生命周期

1. 新建状态(New)

  • 定义:当线程对象被创建(如通过 new Thread())但尚未调用 start() 方法时,线程处于新建状态。
  • 特点:此时线程已分配必要的内存资源,但尚未进入操作系统的线程调度队列,不参与 CPU 调度。

2. 就绪状态(Runnable)

  • 定义:调用 start() 方法后,线程进入就绪状态(也称为 “可运行状态”)。
  • 特点:
    • 线程已加入操作系统的调度队列,等待 CPU 分配时间片。
    • 具备运行条件,但尚未获得 CPU 资源。
  • 转换触发:
    • 新建状态调用 start() 后进入就绪状态。
    • 运行状态的线程因时间片用完(被操作系统抢占)回到就绪状态。
    • 阻塞状态 / 等待状态的线程被唤醒后,先进入就绪状态,再等待调度。

3. 运行状态(Running)

  • 定义:就绪状态的线程获得 CPU 时间片后,进入运行状态,执行 run() 方法中的代码。
  • 特点:同一时刻,一个 CPU 核心只能有一个线程处于运行状态(多核心 CPU 可并行运行多个线程)。
  • 转换触发:
    • 就绪状态的线程被操作系统调度选中,获得 CPU 资源。
    • 运行状态的线程因时间片用完,回到就绪状态。
    • 运行中调用 sleep()wait() 等方法,或尝试获取同步锁失败时,进入阻塞 / 等待状态。

4. 阻塞 / 等待状态(Blocked/Waiting/Timed Waiting)

线程暂时停止执行,放弃 CPU 资源,等待特定条件满足后才能重新进入就绪状态。根据触发原因,可细分为:

  • 阻塞状态(Blocked)
    • 因竞争同步锁(如 synchronized 代码块)失败而阻塞,等待其他线程释放锁。
  • 等待状态(Waiting)
    • 调用 wait()join() 等无超时参数的方法,线程进入无限期等待,需被其他线程显式唤醒(如 notify())。
  • 超时等待状态(Timed Waiting)
    • 调用 sleep(ms)wait(ms) 等带超时参数的方法,线程等待指定时间后自动唤醒,或被提前唤醒。
  • 共同特点:此时线程不参与 CPU 调度,直到等待的条件满足(如锁释放、超时时间到、被唤醒)。

5. 终止状态(Terminated)

  • 定义:线程的 run() 方法执行完毕,或因异常终止,进入终止状态。
  • 特点:线程生命周期结束,无法再转换到其他状态,也不能通过 start() 方法重新启动(否则会抛出异常)。
  • 终止原因:
    • run() 方法正常执行完毕。
    • 线程中抛出未捕获的异常,导致线程终止。
    • 调用 stop() 方法(已废弃,可能导致资源泄露,不推荐使用)。
表跟表是怎么关联的

在关系型数据库中,表与表之间的关联(关系)是通过主键(Primary Key)外键(Foreign Key) 来建立的,目的是实现数据的结构化存储、减少冗余,并保证数据一致性。常见的表关联类型有以下几种:

1. 一对一关联(One-to-One)

  • 定义:两个表中的记录一一对应,即 A 表中的一条记录仅对应 B 表中的一条记录,反之亦然。
  • 实现方式:
    • 在其中一个表中添加外键,关联另一个表的主键,且该外键设置为唯一(UNIQUE)。
    • 例如:用户表(user)身份证表(id_card),一个用户仅对应一张身份证,一张身份证仅属于一个用户。

2. 一对多关联(One-to-Many)

  • 定义:A 表中的一条记录可对应 B 表中的多条记录,但 B 表中的一条记录仅对应 A 表中的一条记录。这是最常见的关联类型。
  • 实现方式:
    • 在 “多” 的一方(B 表)中添加外键,关联 “一” 的一方(A 表)的主键。
    • 例如:部门表(department)员工表(employee),一个部门可包含多名员工,一名员工仅属于一个部门。

3. 多对多关联(Many-to-Many)

  • 定义:A 表中的一条记录可对应 B 表中的多条记录,同时 B 表中的一条记录也可对应 A 表中的多条记录。
  • 实现方式:
    • 需要创建一个中间表(关联表),该表中包含两个外键,分别关联 A 表和 B 表的主键,两个外键组合作为中间表的主键(或唯一约束)。
    • 例如:学生表(student)课程表(course),一个学生可选多门课程,一门课程可被多名学生选择。
介绍一下信号量

信号量的核心概念

信号量本质上是一个整数变量,通常记为 S,其值反映了当前可用资源的数量。它支持两种核心操作:

  • P 操作(Proberen,荷兰语 “尝试”):也称为 wait()down()
    • 执行时,先将 S 减 1(S = S - 1)。
    • S ≥ 0:当前进程 / 线程可继续执行(表示成功获取资源)。
    • S < 0:当前进程 / 线程被阻塞,并放入该信号量的等待队列中(表示资源已被占用)。
  • V 操作(Verhogen,荷兰语 “增加”):也称为 signal()up()
    • 执行时,先将 S 加 1(S = S + 1)。
    • S > 0:当前进程 / 线程继续执行(表示释放资源后仍有剩余)。
    • S ≤ 0:从该信号量的等待队列中唤醒一个阻塞的进程 / 线程,使其继续执行(表示有进程正在等待资源)。

信号量的分类

根据初始值和用途,信号量可分为两类:

  1. 二进制信号量(Binary Semaphore)
  • 特点:取值只能是 01,本质上是一种 “互斥锁(Mutex)”。
  • 用途:实现对临界资源的互斥访问(同一时间只允许一个进程 / 线程访问)。
  • 示例:
    • 初始值 S = 1(资源可用)。
    • 进程访问资源前执行 P(S)S 变为 0,进程可访问,后续进程阻塞。
    • 进程释放资源后执行 V(S)S 变为 1,允许其他进程访问。
  1. 计数信号量(Counting Semaphore)
  • 特点:取值可以是任意非负整数,表示可用资源的数量。
  • 用途:控制对有限数量共享资源的并发访问(如连接池、线程池、缓冲区等)。
  • 示例:
    • 若系统有 3 个打印机,初始值 S = 3
    • 每个进程申请打印机时执行 P(S)S 减 1,当 S = 0 时,后续进程需等待。
    • 进程释放打印机时执行 V(S)S 加 1,唤醒等待队列中的进程。

信号量的作用

  1. 实现互斥:通过二进制信号量,确保同一时间只有一个进程 / 线程进入临界区(如修改共享变量),避免数据竞争。
  2. 实现同步:通过计数信号量,协调多个进程 / 线程的执行顺序。例如:
    • 进程 A 生成数据,进程 B 处理数据,可通过信号量确保 B 只能在 A 生成数据后执行。
    • 初始时,信号量 S = 0(无数据)。A 生成数据后执行 V(S)S = 1),B 执行 P(S)S = 0)后开始处理。

信号量的实现与注意事项

  • 原子性PV 操作必须是原子操作(不可中断),否则可能导致信号量状态不一致(如多个进程同时执行 P 操作时,S 的值可能错误)。现代操作系统通过硬件指令(如 Test-and-Set)保证其原子性。
  • 死锁风险:若多个进程 / 线程相互等待对方持有的资源,可能导致死锁。例如,进程 A 持有信号量 S1 并等待 S2,进程 B 持有 S2 并等待 S1,此时两者都会永久阻塞。
  • 与互斥锁的区别
    • 互斥锁(Mutex)只能由持有锁的进程 / 线程释放,而信号量可由任意进程 / 线程释放(更灵活,但也易出错)。
    • 信号量可用于多个资源的并发控制,互斥锁仅用于单个资源的独占访问。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  private boolean isMirror(TreeNode left, TreeNode right) {
        // 左右子树都为 null,对称
        if (left == null && right == null) {
            return true;
        }
        // 只有一个子树为 null,不对称
        if (left == null || right == null) {
            return false;
        }
        // 比较当前节点的值,以及对称位置的子树
        return (left.val == right.val) && isMirror(left.left, right.right) && isMirror(left.right, right.left);
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

 public int countNodesSimple(TreeNode head) {
        if (head == null) {
            return 0;
        }
        // 根节点算1个,加上左子树节点数和右子树节点数
        return 1 + countNodesSimple(head.left) + countNodesSimple(head.right);
    }
8.13
IP协议的首部结构

IP 协议(以 IPv4 为例)的首部结构由固定部分(20 字节)和可选部分(0~40 字节)组成,共包含 12 个字段,按 32 位(4 字节)一行排列,具体结构如下:

  1. 版本(Version):4 位,标识 IP 协议版本,IPv4 固定为 0100(十进制 4)。
  2. 首部长度(IHL):4 位,表示首部总长度(单位为 32 位字),最小值 5(20 字节,仅固定部分),最大值 15(60 字节,含可选部分)。
  3. 服务类型(TOS):8 位,用于指示服务质量(如延迟、吞吐量优先级),已扩展为区分服务(DS)字段。
  4. 总长度:16 位,指整个 IP 数据报(首部 + 数据)的总字节数,最大值 65535 字节。
  5. 标识(Identification):16 位,用于分片重组,同一数据报的分片标识相同。
  6. 标志(Flags):3 位,前两位有效:DF(1 表示不允许分片)、MF(1 表示后续还有分片)。
  7. 片偏移:13 位,标识当前分片在原数据报中的偏移量(单位 8 字节),用于重组。
  8. 生存时间(TTL):8 位,数据包最大存活跳数(每过一个路由器减 1,为 0 则丢弃),防止环路。
  9. 协议:8 位,指示上层协议(如 TCP=6、UDP=17、ICMP=1)。
  10. 首部校验和:16 位,仅校验 IP 首部,用于检测传输中的错误。
  11. 源 IP 地址:32 位,发送方的 IPv4 地址。
  12. 目的 IP 地址:32 位,接收方的 IPv4 地址。
  13. 选项(Options):0~40 字节(可选),用于特殊控制(如记录路由),需为 4 字节整数倍。
说一说你对Spring IoC的理解

一、什么是 “控制反转”?

“控制反转” 是相对于 “传统程序设计” 而言的:

  • 传统方式:开发者在代码中主动控制对象的创建和依赖关系。例如,在类 A 中需要使用类 B 时,开发者会直接在 A 中通过new B()创建 B 的实例,即 “我要什么,我自己创建”。
  • IoC 方式:对象的创建、依赖关系的维护等 “控制权” 被转移给第三方容器(Spring IoC 容器)。开发者只需定义 “需要什么”,容器会在运行时自动创建对象并注入依赖,即 “我要什么,容器给我什么”。

简言之,IoC 反转的是 “对象的创建权和依赖管理的控制权”—— 从开发者手中转移到了容器手中。

二、Spring IoC 容器:IoC 思想的载体

Spring IoC 的具体实现依赖于IoC 容器,它是一个负责管理对象(Bean)的生命周期(创建、初始化、依赖注入、销毁等)的核心组件。

  1. 容器的核心功能
  • Bean 的定义:通过 XML 配置、注解(如@Component)或 Java 配置类(@Configuration)定义需要容器管理的对象(称为 “Bean”)。
  • Bean 的创建:容器根据定义,在需要时通过反射机制实例化 Bean(开发者无需手动new)。
  • 依赖注入(DI):容器自动将 Bean 所需的依赖对象(其他 Bean)注入到当前 Bean 中(如通过构造器、setter 方法)。
  • Bean 的生命周期管理:容器负责 Bean 的初始化(如@PostConstruct)和销毁(如@PreDestroy)。
  1. 核心容器接口

Spring 提供了两个主要的容器接口,体现了容器的功能演进:

  • BeanFactory:最基础的容器接口,提供了 Bean 的创建、获取等核心功能,采用 “懒加载”(获取 Bean 时才创建)。
  • ApplicationContext:BeanFactory 的子接口,扩展了更多功能(如国际化、事件发布、AOP 集成等),采用 “预加载”(容器启动时就创建所有 Bean),是实际开发中最常用的容器类型(如ClassPathXmlApplicationContextAnnotationConfigApplicationContext)。

三、依赖注入(DI):IoC 的实现方式

IoC 是思想,依赖注入(Dependency Injection,DI)是实现 IoC 的具体手段—— 即容器通过一定方式(构造器、setter 方法等)将依赖对象 “注入” 到目标 Bean 中。

常见的注入方式:

  1. 构造器注入:通过 Bean 的构造方法传递依赖,容器在创建 Bean 时调用对应构造器并传入依赖。

    @Service
    public class UserService {
        private final UserDao userDao;
        
        // 构造器注入(推荐,确保依赖不可变)
        public UserService(UserDao userDao) { 
            this.userDao = userDao; 
        }
    }
    
  2. Setter 方法注入:通过 Bean 的 setter 方法设置依赖,容器在创建 Bean 后调用 setter 传入依赖。

    @Service
    public class UserService {
        private UserDao userDao;
        
        // Setter注入
        @Autowired
        public void setUserDao(UserDao userDao) { 
            this.userDao = userDao; 
        }
    }
    
  3. 字段注入:直接在字段上使用@Autowired注解,容器通过反射直接为字段赋值(简单但不推荐,不利于测试和依赖可见性)。

四、IoC 的核心优势

  1. 降低耦合度
    传统方式中,类与类之间通过new直接依赖(硬编码),修改一个类可能导致多个类需要调整;而 IoC 中,依赖关系由容器管理,类之间仅通过接口或抽象类关联,实现 “面向接口编程”,耦合度大幅降低。
  2. 提高代码复用性
    容器管理的 Bean 可以被多个组件共享,无需重复创建。
  3. 便于测试
    依赖由容器注入,测试时可轻松替换为 Mock 对象(如通过 Spring Test 框架),无需修改业务代码。
  4. 增强系统扩展性
    当需要替换某个依赖的实现类时,只需修改配置(如换一个@Service的实现),无需修改使用该依赖的类,符合 “开闭原则”。
  5. 简化开发
    开发者无需关注对象创建和依赖维护的细节,只需专注于业务逻辑,减少重复代码。
说一说你对双亲委派模型的理解

一.双亲委派模型的规则
当一个类加载器需要加载某个类时,它不会先自己尝试加载,而是优先委托给其父类加载器去加载,只有当父类加载器无法加载(即父类加载器的搜索范围内不存在该类)时,才由自己尝试加载。
这里的 “双亲” 并非指继承关系,而是类加载器之间的一种委派层次关系(通常通过 getParent() 方法关联)。

二、工作流程(以加载 com.example.MyClass 为例)

  1. 应用程序类加载器收到加载请求,首先委托给父类加载器(扩展类加载器)。
  2. 扩展类加载器收到请求,继续委托给父类加载器(启动类加载器)。
  3. 启动类加载器在自己的搜索范围(核心类库)中查找 com.example.MyClass,若找不到,返回 “无法加载”。
  4. 扩展类加载器接收到父类的 “无法加载” 信号,在自己的搜索范围(扩展目录)中查找,若找不到,返回 “无法加载”。
  5. 应用程序类加载器接收到信号,在 classpath 下查找并加载 com.example.MyClass,若仍找不到,则抛出 ClassNotFoundException

三、核心作用

  1. 保证类的唯一性
    避免同一个类被不同类加载器重复加载。例如,java.lang.String 只会被启动类加载器加载,无论哪个类加载器请求加载,最终都会委派到启动类加载器,确保所有地方使用的 String 类是同一个类(否则会导致类强转失败等问题)。
  2. 防止核心类被篡改
    假设开发者自定义了一个 java.lang.String 类,如果没有双亲委派,应用程序类加载器可能会加载这个自定义类,从而替换核心类,引发安全风险。而双亲委派模型下,自定义类会被委派给启动类加载器,而启动类加载器只会加载核心库中的 String,从而防止核心类被恶意替换。
进程调度算法有哪些?
  1. 先来先服务(FCFS)
    按进程到达就绪队列的先后顺序调度,先到先执行,直到完成或阻塞。
    优点:简单公平;缺点:对短作业不利,长作业会导致短作业等待时间过长(“convoy effect”)。
  2. 短作业优先(SJF)
    优先调度估计运行时间最短的进程。
    优点:能有效减少平均等待时间;缺点:需要预先知道作业运行时间,对长作业可能产生 “饥饿”(长期得不到调度)。
  3. 高响应比优先(HRRN)
    综合考虑进程等待时间和估计运行时间,响应比 =(等待时间 + 运行时间)/ 运行时间,优先调度响应比高的进程。
    兼顾短作业和长作业,避免 “饥饿”。
  4. 时间片轮转(RR)
    为每个进程分配固定时间片(如 10ms),按顺序轮流执行,时间片用完则切换到下一个进程。
    优点:公平性好,响应快,适用于分时系统;缺点:时间片大小影响效率(过短会增加切换开销,过长退化为 FCFS)。
  5. 优先级调度
    为进程分配优先级,优先调度高优先级进程(优先级可静态分配或动态调整)。
    优点:能优先处理紧急任务;缺点:低优先级进程可能 “饥饿”。
  6. 多级反馈队列调度
    多个优先级队列,进程初始进入低优先级队列,按 RR 调度;若时间片内未完成,升级到高优先级队列。
    兼顾短作业(快速完成)、长作业(逐渐提升优先级)和 I/O 密集型进程(频繁阻塞后优先级高),是综合性能较好的算法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

   public static int trailingZeroes(int n) {
        int count = 0;
        while (n > 0) {
            n /= 5;
            count += n;
        }
        return count;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

 public static List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> result = new ArrayList<>();
        if (matrix == null || matrix.length == 0) {
            return result;
        }

        int m = matrix.length, n = matrix[0].length;
        int l = 0, r = n - 1, t = 0, b = m - 1;

        while (l <= r && t <= b) {
            // 从左到右遍历上边界
            for (int i = l; i <= r; i++) {
                result.add(matrix[t][i]);
            }
            // 上边界向下移动,如果超过下边界则退出
            if (++t > b) break;

            // 从上到下遍历右边界
            for (int i = t; i <= b; i++) {
                result.add(matrix[i][r]);
            }
            // 右边界向左移动,如果小于左边界则退出
            if (--r < l) break;

            // 从右到左遍历下边界
            if (t <= b) {
                for (int i = r; i >= l; i--) {
                    result.add(matrix[b][i]);
                }
                // 下边界向上移动
                --b;
            }

            // 从下到上遍历左边界
            if (l <= r) {
                for (int i = b; i >= t; i--) {
                    result.add(matrix[i][l]);
                }
                // 左边界向右移动,如果超过右边界则退出
                if (++l > r) break;
            }
        }

        return result;
    }
8.14
如何判断MySQL中的索引有没有生效

核心方法:使用 EXPLAIN 分析执行计划

在 SQL 语句前加上 EXPLAIN 关键字,执行后会生成一行或多行信息,通过观察其中的关键字段(如 typekeykey_len 等),即可判断索引是否生效。

示例:

假设有一张 user 表,结构如下:

CREATE TABLE user (
  id INT PRIMARY KEY,
  name VARCHAR(50),
  age INT,
  INDEX idx_name (name) -- 为 name 字段创建索引
);

执行带 EXPLAIN 的查询:

EXPLAIN SELECT * FROM user WHERE name = '张三';

关键字段解析(判断索引是否生效)

执行 EXPLAIN 后,重点关注以下字段:

  1. type
    表示连接类型,反映查询使用索引的效率,从优到劣常见值为:
    system > const > eq_ref > ref > range > index > ALL
    • 生效标志typeconstrefrange 等(非 ALL),说明使用了索引。
    • 失效标志typeALL,表示全表扫描(未使用索引)。
  2. key
    表示实际使用的索引名称。
    • 生效标志key 显示索引名(如 idx_name),说明索引被使用。
    • 失效标志keyNULL,表示未使用任何索引。
  3. key_len
    表示使用的索引长度(字节数),用于判断索引是否被完全使用(多列索引时尤其重要)。
    • 生效标志key_len 大于 0,且长度符合索引定义(如 varchar(50) 索引的 key_len 可能为 152,因包含字符集和长度标识)。
  4. rows
    表示 MySQL 估计需要扫描的行数(非精确值)。
    • 参考意义:使用索引时,rows 通常远小于表的总记录数;全表扫描时,rows 接近表的总记录数。
  5. Extra
    包含额外执行信息,常见与索引相关的值:
    • Using index:使用了覆盖索引(查询的字段都在索引中,无需回表),效率极高。
    • Using where; Using index:使用索引过滤条件,且覆盖索引。
    • 失效相关Using filesort(需额外排序,未用到索引排序)、Using temporary(使用临时表,未用到索引)、Using where(全表扫描后过滤,未用索引)。

索引生效 / 失效的示例对比

  1. 索引生效的情况
EXPLAIN SELECT * FROM user WHERE name = '张三';
  • type = ref(使用了索引)
  • key = idx_name(实际使用 name 索引)
  • key_len = 152(索引长度有效)
  1. 索引失效的情况(例如对索引字段做函数操作)
EXPLAIN SELECT * FROM user WHERE SUBSTR(name, 1, 1) = '张'; -- 对 name 做了函数处理
  • type = ALL(全表扫描)
  • key = NULL(未使用索引)
  • Extra = Using where(全表扫描后过滤)
说一说HashMap的扩容机制
  1. 扩容的触发条件

HashMap 扩容的核心阈值公式为:
阈值(threshold)= 数组容量(capacity)× 负载因子(loadFactor)

  • 容量(capacity):底层桶数组的长度,默认初始值为 16(必须是 2 的幂,如 16、32、64…);
  • 负载因子(loadFactor):默认 0.75,是平衡空间与时间效率的阈值(值越高,空间利用率高但冲突概率大,反之则相反);
  • 触发时机:当 HashMap 中存储的键值对数量(size超过当前阈值时,触发扩容。
  1. 扩容的核心步骤(以 JDK 1.8 为例)

步骤 1:计算新容量和新阈值

  • 新容量 = 原容量 × 2(始终保持 2 的幂,这是通过位运算 newCap = oldCap << 1 实现的);
  • 新阈值 = 新容量 × 负载因子(若原阈值是通过构造函数手动指定的,会特殊处理)。

步骤 2:创建新的桶数组

  • 初始化一个长度为新容量的新数组(newTab),作为扩容后的底层存储结构。

步骤 3:迁移旧数组中的元素到新数组

遍历旧数组(oldTab)中的每个桶,将桶内的元素(链表或红黑树)迁移到新数组,核心是重新计算元素在新数组中的索引

  • 索引计算逻辑
    原索引通过 hash & (oldCap - 1) 计算,扩容后新索引为 hash & (newCap - 1)
    由于新容量是原容量的 2 倍,newCap - 1oldCap - 1 多了一个高位的 1(如 16-1=15 是 1111,32-1=31 是 11111),因此新索引只有两种可能:
    • 与原索引相同(当 hash 的新增高位为 0 时);
    • 原索引 + 原容量(当 hash 的新增高位为 1 时)。
      这种设计避免了复杂的哈希值重新计算,仅通过高位判断即可确定新位置。
  • 元素迁移细节
    • 若桶内是链表:遍历链表,按新索引将元素拆分为两个子链表(分别对应 “原索引” 和 “原索引 + 原容量”),直接放入新数组的对应位置;
    • 若桶内是红黑树:先尝试将树拆分为两个子链表(若子链表长度 <= 6,则退化为普通链表),再分别放入新数组的对应位置。

步骤 4:替换数组并更新参数

  • 将 HashMap 的底层数组引用从旧数组(oldTab)切换到新数组(newTab);
  • 更新容量(capacity)为新容量,更新阈值(threshold)为新阈值。
  1. 特殊场景的扩容处理
  • 初始扩容:当第一次调用 put 方法时,若数组未初始化(table == null),会先触发初始化,容量为默认 16 或构造函数指定的初始值(需调整为最接近的 2 的幂);
  • 容量上限:当容量达到最大阈值(1 << 30)时,不再扩容,仅将阈值设为 Integer.MAX_VALUE,允许元素继续插入(此时冲突概率会增加);
  • 并发问题:HashMap 是非线程安全的,并发扩容可能导致链表成环(JDK 1.7 及之前),JDK 1.8 虽优化了迁移逻辑,但仍不建议在并发场景使用(需用 ConcurrentHashMap)。
  1. 扩容的核心目的
  • 降低哈希冲突:通过增大数组容量,使元素在桶中的分布更稀疏,减少单个桶内的元素数量(链表 / 红黑树长度);
  • 维持高效访问:保证 getput 等操作的时间复杂度接近 O (1)(若不扩容,桶内元素过多会导致查询退化为 O (n) 或 O (log n))。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

 public static List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        // 先对数组进行排序
        Arrays.sort(nums);
        int n = nums.length;
        // 遍历数组,固定第一个数
        for (int i = 0; i < n - 2; i++) {
            // 去重:如果当前数和前一个数相同,跳过,避免重复三元组
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int left = i + 1;
            int right = n - 1;
            // 双指针向中间靠拢
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    // 找到满足条件的三元组,加入结果集
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    // 去重:跳过左指针和右指针重复的元素
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }
                    // 继续移动指针寻找其他可能的组合
                    left++;
                    right--;
                } else if (sum < 0) {
                    // 和小于 0,左指针右移增大和
                    left++;
                } else {
                    // 和大于 0,右指针左移减小和
                    right--;
                }
            }
        }
        return result;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public int maxProfit(int[] prices) {
        int n = prices.length;
        if (n == 0) {
            return 0;
        }
        // 第一次买入后的现金(负数)
        int buy1 = -prices[0]; 
        // 第一次卖出后的现金
        int sell1 = 0; 
        // 第二次买入后的现金(初始为极小值,因为要先完成第一次卖出才能第二次买入 )
        int buy2 = Integer.MIN_VALUE; 
        // 第二次卖出后的现金
        int sell2 = 0; 

        for (int i = 1; i < n; i++) {
            // 新的第一次买入状态 = 之前第一次买入状态 或 当天新买入(现金为 -prices[i])
            int newBuy1 = Math.max(buy1, -prices[i]); 
            // 新的第一次卖出状态 = 之前第一次卖出状态 或 之前第一次买入后当天卖出(buy1 + prices[i])
            int newSell1 = Math.max(sell1, buy1 + prices[i]); 
            // 新的第二次买入状态 = 之前第二次买入状态 或 第一次卖出后当天买入(sell1 - prices[i])
            int newBuy2 = Math.max(buy2, sell1 - prices[i]); 
            // 新的第二次卖出状态 = 之前第二次卖出状态 或 之前第二次买入后当天卖出(buy2 + prices[i])
            int newSell2 = Math.max(sell2, buy2 + prices[i]); 

            // 更新状态为新计算的值,用于下一轮循环
            buy1 = newBuy1;
            sell1 = newSell1;
            buy2 = newBuy2;
            sell2 = newSell2;
        }

        // 最大收益是第二次卖出后的现金和第一次卖出后的现金中的较大值(可能只进行一次交易收益更高 )
        return Math.max(sell2, sell1); 
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值