这里是有关设计模式,Linux,Git的一些八股,后续会补充场景题
1.设计模式
1.概述
- 创建型模式:创建型模式关注对象的创建过程,让对象的创建更灵活、更可控。 这类模式的特点是,不让用户依赖于对象的创建或排列方式,避免用户直接使用new运算符创建对象。
- 结构型模式:结构型模式关注类和对象的组合,形成更大的结构,提升系统的灵活性。 和类有关的结构型模式设计如何合理地使用继承机制;和对象有关的结构型模式涉及如何合理地使用对象组合机制。
- 行为型模式:行为型模式关注对象之间的通信和职责分配,让协作更清晰。
创建型模式
创建型模式关注对象的创建过程,让对象的创建更灵活、更可控。 这类模式的特点是,不让用户依赖于对象的创建或排列方式,避免用户直接使用new运算符创建对象。
单例模式:确保一个类只有一个实例,并提供一个全局访问点。
核心:保证整个程序里只有一个对象实例。
抽象工厂模式:提供一个创建一系列相关或依赖对象的接口,而无需指定它们具体的类。
核心:一次性创建一组相关的对象。
解释:像订购一个套餐,里面包含多个搭配好的东西。
工厂方法模式:定义一个创建对象的接口,但让子类决定实例化哪个类。
核心:让子类来决定创建哪种对象。
解释:就像工厂流水线,具体生产什么产品由“分厂”自己定。
建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
核心:分步骤把复杂对象拼装起来。
解释:就像搭积木,一步步把房子建好。
原型模式:通过复制现有对象来创建新对象。
结构型模式
结构型模式关注类和对象的组合,形成更大的结构,提升系统的灵活性。 和类有关的结构型模式设计如何合理地使用继承机制;和对象有关的结构型模式涉及如何合理地使用对象组合机制。
适配器模式:将一个类的接口转换成客户端期望的另一个接口。
核心:让不兼容的东西能一起用。
解释:就像电源转换器,把插头改成能用的形状。
桥接模式:将抽象部分与它的实现部分分离,使它们可以独立地变化。
核心:把抽象和实现分开,让它们独立变化。
解释:像手机和充电器,分开设计但能搭配使用。
组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。
解释:像文件夹和文件,层层嵌套但统一管理。
装饰者模式:动态地给一个对象添加一些额外的职责。
核心:动态给对象加点新功能。
解释:就像给蛋糕加奶油,随时装饰一下。
外观模式:为子系统中的一组接口提供一个统一的接口。
核心:给复杂系统提供一个简单入口。
解释:像遥控器,一个按钮控制一堆功能。
享元模式:运用共享技术有效地支持大量细粒度的对象。
核心:共享对象来省内存。
解释:就像公共自行车,大家轮着用同一辆。
代理模式:为其他对象提供一种代理以控制对这个对象的访问。
核心:通过中间人控制对对象的访问。
解释:像中介,帮你跟房东谈租房。
行为型模式
行为型模式关注对象之间的通信和职责分配,让协作更清晰。
责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
核心:把请求扔给一串处理者,谁能搞定谁上。
解释:像客服转接,问题层层传递。
命令模式: 将请求封装成一个对象,从而使你可用不同的请求对客户进行参数化。
核心:把请求打包成对象来处理。
解释:像点外卖,把订单写好再交给厨师。
解释器模式: 给定一个语言,定义它的文法的一种表示,并定义一个解释器。
核心:为特定语言定义解释规则。
解释:像翻译官,解读外语的意思。
迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
核心:挨个访问集合里的东西,不用管里面怎么存。
解释:像翻书,一页页看过去。
中介者模式: 用一个中介对象来封装一系列的对象交互。
核心:用一个中间人协调多个对象的关系。
解释:像群聊管理员,帮大家沟通。
备忘录模式: 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
核心:保存对象的状态,随时可以恢复。
解释:像游戏存档,随时读档重来。
观察者模式: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
核心:一个对象变了,其他跟着知道。
解释:像订阅公众号,有更新就通知你。
状态模式:允许一个对象在其内部状态改变时改变它的行为。
核心:状态变了,行为也跟着变。
解释:像红绿灯,颜色不同规则就不同。
策略模式: 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
核心:封装一堆方案,随时换着用。
解释:像导航选路线,走快路还是省钱路随便挑。
模板方法模式: 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
核心:定个大框架,细节留给别人填。
解释:像做菜的食谱,大步骤固定,调料你自己加。
访问者模式: 在不改变数据结构的前提下,定义作用于这些元素的新操作。
核心:不改结构也能加新功能。
解释:像请专家来检查设备,不用自己动手改。
2.单例模式
单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
单例模式通常用于管理全局共享资源,例如数据库连接池、日志对象等。
所有单例的实现都包含以下两个相同的步骤:
- 将默认构造函数设为私有, 防止其他对象使用单例类的
new运算符。 - 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。
实现单例模式的关键点
- 私有构造方法:确保外部代码不能通过构造器创建类的实例。
- 私有静态实例变量:持有类的唯一实例。
- 公有静态方法:提供全局访问点以获取实例,如果实例不存在,则在内部创建。
常见写法如下(重点掌握懒汉,饿汉,双重锁检查):
1、 饿汉式(线程安全)
饿汉式在类加载时就创建实例,因此线程安全,不会出现多次创建实例的问题。缺点是即使不需要该实例,类也会被加载。
1 | public class Singleton { |
2、 懒汉式(线程不安全)
在这种方式中,单例对象是在首次使用时被创建的。但由于没有同步处理,多个线程同时访问时可能会创建多个实例。
1 | public class Singleton { |
3、 懒汉式(线程安全)
通过 synchronized 关键字来保证多线程下的安全性,但性能较差,因为每次调用 getInstance() 时都会进行同步。
1 | public class Singleton { |
4、双重锁检查(DCL)
双重检查锁定是一种优化的懒汉式实现,它结合了懒加载和线程安全的优点,只有在实例为空时才会进入同步块,从而减少了锁的竞争。
懒加载 (lazy loading):使⽤的时候再创建对象
1 | public class Singleton { |
1. 为什么要双重检查?
| 检查次数 | 目的 | 性能影响 |
|---|---|---|
| 第一次检查 | 避免不必要的同步(实例已存在时) | 减少锁竞争 |
| 第二次检查 | 防止重复创建实例(当多个线程通过第一次检查时) | 保证单例 |
场景:多线程并发调用getInstance()
- 线程A首次调用getInstance()
- 第一次检查
instance == null:true(实例尚未创建) - 进入同步块(获取类锁)
- 第二次检查
instance == null:true - 执行
new DoubleCheckedSingleton() - 释放锁
- 返回新创建的实例
- 第一次检查
- 线程B在A创建实例期间调用getInstance()
- 第一次检查
instance == null:- 可能看到
null(未完全构造的对象) - 也可能看到非null(已构造完成)
- 可能看到
- 如果看到
null:- 尝试获取锁(此时线程A持有锁,线程B阻塞)
- 当线程A释放锁后:
- 线程B获得锁
- 第二次检查
instance == null:false(已被线程A创建) - 直接返回已存在的实例
- 第一次检查
- 线程C在实例创建完成后调用getInstance()
- 第一次检查
instance == null:false - 直接返回已存在的实例(不进入同步块)
- 第一次检查
2.为什么单例模式的双重检查锁需要用volatile修饰?
instance = new Singleton(); 这一行。这行代码在 JVM 中并不是一个原子操作,它实际上包含了三个关键步骤:
memory = allocate();// 步骤1:为 Singleton 对象分配内存空间ctorInstance(memory);// 步骤2:在分配的内存上初始化 Singleton 对象(调用构造函数,设置字段值)instance = memory;// 步骤3:将instance引用指向刚才分配的内存地址
由于步骤2(初始化对象)是一个可能比较耗时的操作,而步骤3(赋值引用)很快。为了优化性能,编译器或处理器完全可能进行指令重排序,将步骤3提到步骤2之前执行:
重排序后的执行顺序:
memory = allocate();// 步骤1:分配内存instance = memory;// 步骤3:引用指向内存(此时 instance 不为 null!)ctorInstance(memory);// 步骤2:初始化对象
在并发场景下:
- 线程A进入了同步块,开始创建对象。
- 由于指令重排序,线程A先执行了步骤1和3,使
instance引用指向了一块尚未初始化的内存。此时,instance已经不再是null。 - 就在此时,线程B执行
getInstance()。它进行第一次检查if (instance == null),发现instance不为null(因为线程A已经执行了步骤3),于是欣喜若狂地直接返回了这个 instance。 - 然而,线程B拿到的是一个半成品对象,因为线程A的步骤2(初始化)还没有执行!当线程B尝试使用这个单例对象时,就可能发生各种匪夷所思的错误,比如读取到
null值或默认值。
volatile 关键字在这里起到了两个至关重要的作用:
- 禁止指令重排序:
- 它会在
volatile写操作(即instance = new Singleton();)之前和之后插入特定的内存屏障。 - 这些屏障强制保证了步骤1、2、3的执行顺序不会被重排。具体来说,它确保了步骤2(初始化对象)一定会在步骤3(设置引用)之前完成。
- 这就从根本上杜绝了线程拿到半成品对象的可能性。
- 它会在
- 保证可见性:
- 当线程A完成对
instance的赋值后,会立即将其刷新到主内存。 - 当线程B第一次读取
instance时,会从主内存重新加载,确保看到的是线程A设置的最新值。
- 当线程A完成对
面试官:为什么单例模式的双重检查锁需要用 volatile 修饰?
你可以这样回答(面试范例):
“主要是为了防止指令重排序导致线程获取到尚未初始化完成的对象。
创建对象
instance = new Singleton();这行代码并不是原子性的,它分为三步:1. 分配内存、2. 初始化对象、3. 将引用指向内存地址。如果没有
volatile修饰,JVM 为了优化性能,可能会将步骤3和步骤2重排序。这样,就有可能在线程A执行创建时,刚执行完步骤1和3(此时instance已不为null),但尚未执行步骤2初始化对象。此时如果线程B进入方法,第一次检查发现instance非空,就会直接返回这个半成品对象,从而导致程序出错。使用
volatile关键字后,它插入的内存屏障会禁止这种重排序,强制保证‘初始化对象’的操作在‘设置引用’之前完成,这样就确保了其他线程读到的肯定是一个完全初始化好的对象。”
5、枚举单例
枚举单例是实现单例模式的最佳方式之一。它由 JVM 保证线程安全和单例,并且防止反序列化创建新的对象。
1 | public enum Singleton { |
6、静态内部类
利用 JVM 的类加载机制,通过静态内部类实现延迟加载。内部类只有在 getInstance() 被调用时才会加载。
1 | public class Singleton { |
总结
- 懒汉式(线程不安全):适用于多线程环境下,但不安全。
- 懒汉式(线程安全):通过
synchronized确保线程安全,但性能较差。 - 饿汉式:类加载时即创建实例,线程安全,但缺乏灵活性。
- 双重锁检查:性能较好,推荐使用。
- 枚举单例:推荐使用,简洁且线程安全。
3.工厂模式
Java 中的 工厂模式(Factory Pattern)是一种创建型设计模式,旨在通过定义一个接口来创建对象,但让子类决定实例化哪个类。工厂模式可以帮助减少客户端与具体产品类之间的耦合,提高代码的灵活性和扩展性。
1、简单工厂模式
简单工厂模式通过一个工厂类来根据提供的信息生成不同类型的对象。它不需要暴露创建对象的具体逻辑,只暴露一个工厂方法供客户端调用。
结构:
- 工厂类:负责创建实例。
- 产品接口:定义产品的公共接口。
- 具体产品:实现产品接口的具体类。
1 | // 产品接口 |
优缺点:
- 优点:客户端只需要知道工厂类和产品接口,无需关心具体实现。
- 缺点:工厂类一旦增加新的产品,需修改工厂类代码,违反开闭原则。
2、工厂方法模式
工厂方法模式通过在抽象类中定义一个工厂方法,让子类去实现这个方法,从而决定创建哪种产品。这种模式通过继承和多态来让子类决定创建的产品类型,避免了修改工厂类。
结构:
- 抽象工厂:声明工厂方法。
- 具体工厂:实现工厂方法,负责创建具体产品。
- 产品接口:定义产品的公共接口。
- 具体产品:实现产品接口的具体类.
1 | // 产品接口 |
优缺点:
- 优点:遵循了开闭原则,可以通过扩展子类来增加新产品,不需要修改原有代码。
- 缺点:需要创建大量的具体工厂类,如果产品种类过多,工厂类会急剧增加。
4.策略模式
在策略模式(Strategy Pattern)中一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
意图
将每个算法封装起来,使它们可以互换使用。
主要解决的问题
- 解决在多种相似算法存在时,使用条件语句(如if…else)导致的复杂性和难以维护的问题。
使用场景
- 当一个系统中有许多类,它们之间的区别仅在于它们的行为时。
实现方式
- 定义策略接口:所有策略类都将实现这个统一的接口。
- 创建具体策略类:每个策略类封装一个具体的算法或行为。
- 上下文类:包含一个策略对象的引用,并通过该引用调用策略。
关键代码
- 策略接口:规定了所有策略类必须实现的方法。
- 具体策略类:实现了策略接口,包含具体的算法实现。
优点
- 算法切换自由:可以在运行时根据需要切换算法。
- 避免多重条件判断:消除了复杂的条件语句。
- 扩展性好:新增算法只需新增一个策略类,无需修改现有代码。
缺点
- 策略类数量增多:每增加一个算法,就需要增加一个策略类。
- 所有策略类都需要暴露:策略类需要对外公开,以便可以被选择和使用。
使用建议
- 当系统中有多种算法或行为,且它们之间可以相互替换时,使用策略模式。
- 当系统需要动态选择算法时,策略模式是一个合适的选择。
注意事项
- 如果系统中策略类数量过多,考虑使用其他模式或设计技巧来解决类膨胀问题。
结构
策略模式包含以下几个核心角色:
- 环境(Context):维护一个对策略对象的引用,负责将客户端请求委派给具体的策略对象执行。环境类可以通过依赖注入、简单工厂等方式来获取具体策略对象。
- 抽象策略(Abstract Strategy):定义了策略对象的公共接口或抽象类,规定了具体策略类必须实现的方法。
- 具体策略(Concrete Strategy):实现了抽象策略定义的接口或抽象类,包含了具体的算法实现。
策略模式通过将算法与使用算法的代码解耦,提供了一种动态选择不同算法的方法。客户端代码不需要知道具体的算法细节,而是通过调用环境类来使用所选择的策略。
实现
1 | 1.创建一个接口。 |
2.Linux
1.Linux文件系统
在Linux中,“一切都是文件 ”
1.1.inode
硬盘以扇区为最小存储单位,操作系统以块为单位进行读写,块由多个扇区组成,inode存储了文件元信息(例如权限、大小、修改时间以及数据块位置) ,inode 的访问速度非常快
- inode:记录文件的属性信息,可以使用
stat命令查看 inode 信息。 - block:实际文件的内容,如果一个文件大于一个块时候,那么将占用多个 block,但是一个块只能存放一个文件。(因为数据是由 inode 指向的,如果有两个文件的数据存放在同一个块中,就会乱套了)
1.2.硬链接和软链接
在 Linux/类 Unix 系统上,文件链接(File Link)是一种特殊的文件类型,可以在文件系统中指向另一个文件。常见的文件链接类型有两种:
硬链接是多个目录项中的「索引节点」指向一个文件,也就是指向同一个 inode,但是 inode 是不可能跨越文件系统的,每个文件系统都有各自的 inode 数据结构和列表, 所以硬链接是不可用于跨文件系统的。由于多个目录项都是指向一个 inode, 那么只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。

软链接相当于重新创建一个文件,这个文件有独立的 inode, 但是这个文件的内容是另外一个文件的路径,所以访问软链接的时候, 实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的, 甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。

2.Linux常用命令
2.1.文件相关
ls:列出目录内容。cd:更改当前目录。cd ..回到上级目录,cd ~回到用户的主目录。rm:删除文件或目录。rm -r递归删除目录及其内容。mkdir:创建新目录。cat:查看文件内容。pwd:显示当前工作目录的完整路径。cp:复制文件或目录。mv:移动或重命名文件或目录。
文件权限详解
Linux 中的权限可以应用于三种类别的用户:
- 文件所有者(u)
- 与文件所有者同组的用户(g)
- 其他用户(o)
①、符号模式
符号模式使用字母来表示权限,如下:
- 读(r)
- 写(w)
- 执行(x)
- 所有(a)
例如:
chmod u+w file:给文件所有者添加写权限。chmod g-r file:移除组用户的读权限。chmod o+x file:给其他用户添加执行权限。chmod u=rwx,g=rx,o=r file:设置文件所有者具有读写执行权限,组用户具有读执行权限,其他用户具有读权限。
②、数字模式
数字模式使用三位八进制数来表示权限,数字是其各自权限值的总和:
- 读(r)= 4
- 写(w)= 2
- 执行(x)= 1
因此,权限模式可以是从 0(无权限)到 7(读写执行权限)的任何值。
- chmod 755 file:使得文件所有者有读写执行(7)权限,组用户和其他用户有读和执行(5)权限。
- chmod 644 file:使得文件所有者有读写(6)权限,而组用户和其他用户只有读(4)权限。
2.2.系统管理相关
ps:显示当前运行的进程。top:实时显示进程动态。kill:终止进程。kill -9 PID强制终止。慎用 kill -9:可能导致数据不一致,优先用
kill -15正常终止。chmod:更改文件或目录的权限。df:显示磁盘空间使用情况。df -h以易读格式显示。du:显示目录或文件的磁盘使用情况。
3.高频问题
如何查看 Java 进程?
ps -ef | grep java如何实时查看日志文件?
tail -f filename.log,-f表示循环读取,常用于查看递增的日志文件如何查看端口占用情况?
netstat -tunlp | grep 8080,起码记住netstat和grep如何杀死一个进程?
kill,-9是强制杀死,-15是正常终止如何查看服务器内存/CPU使用情况?
top如何查找文件?
find
高级题目
CPU飙升或100%问题怎么排查?
找到占用 CPU 最高的 Java 进程:
top找到占用 CPU 最高的线程:
top Hp 进程id,H参数表示要显示线程级别的信息,p则表示指定的pid,也就是进程id。保存线程堆栈信息:jstack 用于生成 Java 进程的线程快照(thread dump)。线程快照是一个关于 Java 进程中所有线程当前状态的快照,包括每个线程的堆栈信息。 将罪魁祸首的线程id转为16进制,然后在jstack输出的日志中查找该线程的信息
导致CPU飙到100的情况可能有哪些?
- 无限循环
- 内存不足
- 高流量
- 循环等待
3.Git
git clone <repository-url>:克隆远程仓库。git status:查看工作区和暂存区的状态。git add <file>:将文件添加到暂存区。git commit -m "message":提交暂存区的文件到本地仓库。git log:查看提交历史。git merge <branch-name>:合并指定分支到当前分支。git checkout <branch-name>:切换分支。git pull:拉取远程仓库的更新。
1.git merge和 git rebase的区别
- Rebase(变基)是将一个分支上的提交逐个地应用到另一个分支上,使得提交历史变得更加线性。 简而言之,rebase可以将提交按照时间顺序线性排列。
- Merge(合并)是将两个分支上的代码提交历史合并为一个新的提交。 在执行merge时,Git会创建一个新的合并提交,将两个分支的提交历史连接在一起。
3.场景
1.秒杀系统设计
什么是秒杀
通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动
业务特点
- 高并发:秒杀的特点就是这样时间极短、 瞬间用户量大。
- 库存量少:一般秒杀活动商品量很少,这就导致了只有极少量用户能成功购买到。
- 业务简单:流程比较简单,一般都是下订单、扣库存、支付订单
- 恶意请求,数据库压力大
解决方案
前端:页面资源静态化,按钮控制,使用答题校验码可以防止秒杀器的干扰,让更多用户有机会抢到
nginx:校验恶意请求,转发请求,负载均衡;动静分离,不走tomcat获取静态资源;gzip压缩,减少静态文件传输的体积,节省带宽,提高渲染速度
业务层:集群,多台机器处理,提高并发能力
redis:集群保证高可用,持久化数据;分布式锁(悲观锁);缓存热点数据(库存)
mq:削峰限流,MQ堆积订单,保护订单处理层的负载,Consumer根据自己的消费能力来取Task,实际上下游的压力就可控了。重点做好路由层和MQ的安全
数据库:读写分离,拆分事务提高并发度
秒杀系统设计小结
- 秒杀系统就是一个“三高”系统,即高并发、高性能和高可用的分布式系统
- 秒杀设计原则:前台请求尽量少,后台数据尽量少,调用链路尽量短,尽量不要有单点
- 秒杀高并发方法:访问拦截、分流、动静分离
- 秒杀数据方法:减库存策略、热点、异步、限流降级
- 访问拦截主要思路:通过CDN和缓存技术,尽量把访问拦截在离用户更近的层,尽可能地过滤掉无效请求。
- 分流主要思路:通过分布式集群技术,多台机器处理,提高并发能力。
2.分布式ID
一个分布式ID需要满足
- 全局唯一:ID 的全局唯一性肯定是首先要满足的!
- 高性能:分布式 ID 的生成速度要快,对本地资源消耗要小。
- 高可用:生成分布式 ID 的服务要保证可用性无限接近于 100%。
实现方案1——UUID