Duan Yao's Blog

思无涯,行无疆,言无忌,行无羁

操作系统基础

用户态-内核态: 1. 系统调用 2. 中断 3. 异常

进程与线程

线程同步的方式: 1. 互斥锁 2. 信号量 3. 读写锁 4. 屏障 5. 事件,通知

操作系统进程状态 1. 创建 2. 就绪 3. 等待 4. 运行 5. 终止 注意,Java中等待有两种,而把就绪与运行合并为 runnable 了

进程间通信方式 1. 管道/匿名管道:在磁盘中 2. 有名管道 3. 消息队列:通信数据先进先出,但是可以随机读取 4. 信号 用于通知事件 5. 信号量,计数器,用于同步 6. 共享内存 7. 套接字:CS架构中

进程调度算法: 1. 先来先服务 2. 短作业优先 3. 时间片轮转 4. 优先级调度 5. 多级反馈队列调度算法

僵尸进程:子进程终止,但是父进程没有调用获取紫禁城信息,紫禁城的PCB仍然存在。 孤儿进程:父进程死了,但是子进程还在运行,一般是因为父进程意外终止,没能及时调用 wait 或者 waitpid 导致的。操作系统会将孤儿进程的父进程设置为 init,进程号1,统一回收。

死锁

死锁: 1. 互斥 2. 占有并等待 3. 非抢占 4. 循环等待

预防: - 破坏互斥,一般不可行 - 破坏非抢占:剥夺式调度算法,一般用于贮存或者处理器,会导致资源利用率下降 - 破坏 占有等待:静态分配,一次性分配好 - 破环循环等待:层次分配,只能申请更高层次的资源。

避免: 银行家算法: 每个进程维护 需求矩阵M,已经分配矩阵A,还需要矩阵N 每次尝试将资源分配给某个进程,之后执行安全性算法,判断分配后是否在安全性状态。是否存在一个安全序列

检测 进程资源分配图:方框表示资源,进程采用圆圈。 每次找到及不阻塞又不独立的进程,消除,归还资源,重复,检查是否能消除所有的额边。

解除 重启系统 撤销所有死锁进程 逐个撤销 抢占资源

内存

外部碎片:没分配,分配不出去。 内部碎片:已经分配,但是无法使用

连续内存 非连续内存管理 1. 页式 2. 段式 3. 段页式 减少外部碎片,同时拥有段式共享与保护的特性

虚拟内存,逻辑化的物理内存

程序采用虚拟内存的虚拟地址

CPU给出逻辑地址,通过MMU进行欸村地址转换为物理地址。

多级页表:页表开销太大,每次没必要存入所有的页表。

页缺失: 硬件页缺失:物理内存中没有对应的物理页,也就是还没从磁盘里面读进来。 软件性:物理内存有,但是虚拟也与页表的映射没建立。 如果直接访问无效地址,那就是无效缺页错误。

页面置换算法: 最佳置换:现实情况不太可能 先进先出,比较差。频繁换出,Belady现象。随着分配的页面数增加,但是缺页率反而上升的情况。 最近最久未使用 最少使用 时钟页面置换:最近没用的

因为 LRU 存储使用频率高的,FIFO 存储后来的页

文件系统

硬链接:inode节点 软链接:文件路径、快捷方式

磁盘调度算法: 先来先服务 最短寻道时间优先 扫描 村换扫面 LOOK CLOOK

三种机制缓解并发

我在项目中同时使用了Redis 缓存、Nginx 反向代理和 Thymeleaf 模板 来保证较高的并发。

这几种机制对于一个请求的相应顺序:

当客户端向服务器请求一个页面时,依次经过 Nginx 反向代理Redis 缓存Spring Boot 服务器Thymeleaf 模板引擎进行响应。以下是详细的顺序和流程:

  1. Nginx 反向代理
    • 初始入口:客户端的请求首先到达 Nginx。Nginx 检查请求类型和 URL 路径。
    • 静态资源处理:如果请求的是静态资源(如 CSS、JS、图片),Nginx 直接从其缓存中提供响应,避免请求进入服务器。
    • 反向代理转发:对于动态页面请求(如博客帖子页面),Nginx 将请求转发给后端的 Spring Boot 服务器。
  2. Redis 缓存
    • 缓存检查:Spring Boot 服务器接收到请求后,会先查找 Redis 缓存中是否已有该页面的数据。
    • 命中缓存:如果缓存中有页面数据,Spring Boot 服务器会直接从 Redis 获取页面内容,并立即返回给 Nginx,从而快速响应客户端请求。
    • 未命中缓存:若 Redis 中没有找到页面数据,Spring Boot 服务器将进入下一步,由 Thymeleaf 模板引擎来渲染页面。
  3. Thymeleaf 模板渲染
    • 数据准备:Spring Boot 从数据库或其他服务中获取页面所需的数据。
    • 模板渲染:将获取到的数据传递给 Thymeleaf 模板引擎进行动态渲染,将数据填充到 HTML 模板中,生成完整的 HTML 页面。
    • 缓存更新:渲染完成的页面数据可以被缓存到 Redis,以便下次请求直接从缓存获取,减少渲染时间。
  4. 返回响应
    • 发送至 Nginx:生成的响应页面返回到 Nginx。
    • 响应客户端:Nginx 将最终的 HTML 页面发送给客户端,从而完成请求-响应流程。

具体的顺序示意图

  1. 客户端Nginx(反向代理或静态资源返回)
  2. NginxSpring Boot(若为动态页面请求)
  3. Spring BootRedis 缓存(查找缓存数据)
  4. Redis(若缓存命中直接返回)或 Thymeleaf 模板渲染(未命中)
  5. 渲染完成后的 HTMLNginx
  6. Nginx客户端

流程总结

通过这一流程,Redis 缓存显著减少了对数据库的访问次数,Nginx 反向代理则分担了静态资源的请求,Thymeleaf 动态渲染负责生成未缓存的页面。这种协同处理方式使服务器响应更高效,同时降低了数据库和服务器的压力。

测试

我是如何测试网站的并发数量的?

采用 并发压力测试工具:Linux之压测工具Siege安装和使用_siege压测-CSDN博客

性能测试还可以使用 JMeter

安全性

Nginx 限流方式(速率限流、并发限流及黑白名单配置)_nginx 限流类型-CSDN博客

Redis基础

Redis 是基于C语言开发的开源NoSQL数据库。

  1. 基于内存
  2. 时间处理模型:单线程事件循环、IO多路复用
  3. 多种优化后的数据结构
  4. 通信协议简单、解析高效

为什么

为什么不直接用Redis?因为内存成本太高且Redis的持久化有数据丢失的风险。

为什么使用Redis? 1. 访问速度快 2. 高并发,一般4核8G的数据上能够达到4k,使用Redis之后能够达到5w+,这还只是单机,集群会更高。

缓存读写策略

旁路缓存模式 Cache Aside

适合读请求比较多的情况。

写:

  • 先更新db
  • 然后直接删除 cache

读:

  • 从 cache中读数据,返回
  • cache中读不到数据,从db中读取数据返回
  • 再把数据放回到缓存里面

问题:为什么不先删除cache,然后更新db?因为如果反过来,那么先删除后读会出现数据不一致。再问:现有策略不会吗?会,但是概率小,因为cache比数据库快得多。

缺陷:

  1. 首次请求数据没有在cache中。解决:热点数据提前存入
  2. 写操作比较频繁时影响缓存命中率。分情况,是否短暂允许数据库与缓存数据不一致。

读写穿透 写直达 Read/Write Through Pattern

把 cache 作为主要数据存储。cache 服务负责将数据读取与写入db,比较少见,因为Redis 没有提供将cache 写入db 的功能。

异步缓存写入 Write Behind Pattern

Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。

但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。

很明显,这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了。

这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。

Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

应用

  1. 缓存
  2. 分布式锁
  3. 限流
  4. 消息队列 不建议
  5. 延时队列
  6. 分布式 Session
  7. 复杂业务场景

还有很多,主要是对上面应用场景的详细解释

Redis 数据类型

5 种基本数据类型:String、List、Set、Hash、Zset(有序集合)

3 种特殊类型:HyperLogLog(基数统计)、Bitmap(位图)、Geospatial(地理位置)

还有一些比如布隆过滤器,位域

如果是完整的对象,存储建议使用哈希,整个的读写操作多的。

如果对于部分信息需要经常变动,建议使用Hash

JAVA IO 设计模式

装饰器模式

通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。

适配器模式

适配器(Adapter Pattern)模式 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。适配器模式中存在被适配的对象或者类称为 适配者(Adaptee) ,作用于适配者的对象或者类称为适配器(Adapter) 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。

工厂模式

工厂模式用于创建对象

观察者模式

NIO 中的文件目录监听服务使用到了观察者模式。

线程与进程

进程:程序的一次执行过程,资源分配的基本单位。

线程:CPU 调度的基本单位。拥有自己的 程序计数器、虚拟机栈、本地方法栈。而对于 堆、字符串常量池 是线程共享的

  • 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
  • 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。

创建

一般说有三种或者多种创建线程的方式,实际本质上只有一种 new Thread().start() 大家都说Java有三种创建线程的方式!并发编程中的惊天骗局!

Java Runnable 与 Callable 的区别是什么?

RunnableCallable 是 Java 中用于并发编程的两个接口,它们都可以被线程执行,但有以下几个关键区别:

1. 返回结果

  • Runnable: 不返回结果。Runnablerun() 方法是 void,所以它不能返回任何结果。

    1
    2
    3
    4
    @FunctionalInterface
    public interface Runnable {
    void run();
    }
  • Callable: 可以返回结果。Callablecall() 方法返回一个泛型结果,可以用 Future 获取返回值。

    1
    2
    3
    4
    @FunctionalInterface
    public interface Callable<V> {
    V call() throws Exception;
    }

2. 异常处理

  • Runnable: 不能抛出检查异常(Checked Exception),只能通过 try-catch 语句捕获和处理异常。

  • Callable: 可以抛出检查异常,call() 方法声明了 throws Exception,允许抛出异常以便调用者处理。

3. 使用场景

  • Runnable: 适用于不需要返回结果的任务,例如在后台运行一个任务或执行一些更新操作。

  • Callable: 适用于需要返回结果或可能抛出异常的任务,比如计算结果或获取数据。

4. 提交到线程池的处理方式

  • Runnable: 可以通过 ExecutorService.submit(Runnable) 提交,返回一个 Future<?> 对象,但 Future.get() 的结果始终为 null

  • Callable: 通过 ExecutorService.submit(Callable<V>) 提交,返回一个带结果的 Future<V>,可以通过 Future.get() 获取 Callable 返回的结果。

示例代码

1
2
3
4
5
6
7
8
9
10
// 使用 Runnable
Runnable runnableTask = () -> System.out.println("Runnable task running");
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(runnableTask); // 提交任务,无返回值

// 使用 Callable
Callable<String> callableTask = () -> "Callable task result";
Future<String> future = executor.submit(callableTask); // 提交任务,有返回值
System.out.println("Callable result: " + future.get());
executor.shutdown();

总结

  • Runnable:不返回结果、不能抛出检查异常,适用于简单任务。
  • Callable:可返回结果、可以抛出检查异常,适合需要返回值或异常处理的任务。

状态

关于状态,JVM有六种

  1. NEW:创建出来但是还没有调用 start
  2. RUNNABLE:可运行,实际上包含了 运行态与就绪态
  3. BLOCKED: 进入临界区等待锁的释放
  4. WAITING:无限期等待,不能自动恢复,只能被其他线程唤醒
  5. TIME_WAITING:有限期,能自动恢复
  6. TERMINATED:终止

在 Java 中,JVM 对线程状态的区分通过 Thread.State 枚举,其中 BLOCKEDWAITINGTIMED_WAITING 是三种不同的等待状态:

1. BLOCKED(阻塞状态)

  • 含义:线程尝试获取一个由其他线程持有的锁(synchronized 块或方法)时进入此状态。线程在进入临界区前会处于 BLOCKED 状态,直到该锁释放并获取成功后,线程才可以继续执行。
  • 触发条件:一个线程在进入 synchronized 块或方法时,发现锁已被其他线程持有时,进入 BLOCKED 状态。
  • 恢复条件:一旦锁被释放,BLOCKED 状态的线程有机会获取锁并继续执行。

2. WAITING(等待状态)

  • 含义:线程在等待其他线程的通知或中断,不会自动返回。它只能被显式唤醒(例如,通过另一个线程调用 notify()notifyAll() 方法)。
  • 触发条件:以下方法会让线程进入 WAITING 状态:
    • Object.wait() 方法(不带超时参数)
    • Thread.join() 方法(不带超时参数)
    • LockSupport.park() 方法
  • 恢复条件:必须由其他线程调用相应的唤醒方法,如 notify()notifyAll(),或在 join() 时等待的线程结束。

3. TIMED_WAITING(超时等待状态)

  • 含义:线程在等待一段时间后将自动返回,不需要被其他线程显式唤醒。该状态适合有超时时间要求的任务或等待。
  • 触发条件:以下方法会让线程进入 TIMED_WAITING 状态:
    • Thread.sleep(long millis) 方法
    • Object.wait(long timeout) 方法(带超时参数)
    • Thread.join(long millis) 方法(带超时参数)
    • LockSupport.parkNanos(long nanos) 方法
    • LockSupport.parkUntil(long deadline) 方法
  • 恢复条件:时间到达后自动返回到 RUNNABLE 状态,或在到期前被其他线程唤醒(例如通过中断)。

状态转换总结

  • BLOCKED:线程等待锁释放。
  • WAITING:线程等待其他线程的通知或中断,无限期等待。
  • TIMED_WAITING:线程在等待超时后自动返回,无需显式唤醒。

sleep wait

Thread.sleep()object.wait()

共同点:都能暂停线程

不同:

  1. 前者没有释放锁,后者释放锁
  2. 前者是 Thread 的静态本地方法,后者是 Object 类的本地方法。

多线程

并发与并行

并发:同一时间段内

并行:同一时刻

同步与异步

同步:调用后被迫原地等待

异步:调用后继续执行,不用等待

CPU密集型与IO密集型

单核CPU上使用多线程不一定快。

  • CPU 密集型:太多线程会导致频繁的线程上下文切换,增加了开销,降低了效率。
  • IO 密集型:多线程可以利用等待 IO 的空闲时间,提高效率。

死锁

线程持有并等待资源,被无限期阻塞,程序无法正常终止。

死锁的四个条件:

  1. 互斥条件:一个萝卜一个坑,一个资源一个线程
  2. 请求并保持条件:吃着碗里的想着锅里的
  3. 不剥夺条件:谁也不能抢走我碗里的
  4. 循环等待条件:我想要你碗里的,你想要我碗里的

预防

  1. 破坏请求保持:一次性申请资源
  2. 破坏不剥夺:主动释放对方手里的
  3. 破坏循环等待:按顺序申请,相反顺序释放。

避免

资源分配时借助算法堆资源分配进行估算,使其进入安全状态。银行家算法

安全状态:存在一种资源分配顺序,使得系统按序分配后,所有线程都可以顺利满足。

检测

一些Java 工具

消除

虚拟线程

就是JDK中使用多个线程对应于一个操作系统线程。

JDK 的线程更加轻量化。

能够在 IO 密集场景下,提高处理速度。减少县城资源的额创建以及上下文的切换。

JMM JVM内存模型

SPI 是什么?服务提供者的接口,是 Java (调用者)给服务提供者(被调用者)指定的接口。

SPI与API的区别:

  • SPI:服务调用者制定接口,被调用者去实现接口
  • API:被调用者制定并实现了接口,服务调用者去适配这种接口。

提供了一种服务发现机制,允许在程序外部动态指定具体实现。这与控制反转(IoC)的思想相似,将组件装配的控制权移交给了程序之外。

也解决了双亲委派模型带来的限制。

缺点:

  • 需要遍历
  • 有并发问题

什么是反射

运行时分析类以及执行类中方法的能力。

通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。

优缺点

优点:灵活,框架,运行时分析操作类

缺点: 安全问题,无视泛型的参数的安全检查(编译时),性能问题

应用场景

  1. 开发通用框架
  2. 动态代理
    1. JDK 代理
    2. GGLIB 代理
  3. 自定义注解
  4. 访问私有成员

实际使用

获取 class 对象

  1. <className>.class

  2. Class.forName("<path>")

  3. <objectName>.getClass()

  4. ClassLoader.loadClass("<path>")

1. 知道具体类的情况下可以使用:

1
Class alunbarClass = TargetObject.class;

但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化

2. 通过 Class.forName()传入类的全路径获取:

1
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");

3. 通过对象实例instance.getClass()获取:

1
2
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();

4. 通过类加载器xxxClassLoader.loadClass()传入类路径获取:

1
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");

通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行。

反射的基本操作

  1. 你得先有一个需要反射得到的类
  2. 获取类对象并创建实例
  3. 获取其中的所有方法
  4. 获取指定方法并且掉哟个(invoke
  5. 获取指定参数并对参数进行修改
  6. 调用 private 方法,需要先取消安全检查

要复习的内容: 1. 数据结构 2. 操作系统 3. 计算机网络 4. Java 面经 5. 项目相关的知识点 6. 了解最新的技术文档、当下技术、软件开发趋势 7. 简单算法的手撕 8. 华为的三道题目必须非常熟练