一、JAVA八股
1、Java实现线程的三种方式
(1) 继承 Thread
类:
创建一个新类,该类继承自Thread
类,并重写run
方法。然后创建该类的实例,并调用它的start
方法来启动线程。
public class MyThread extends Thread {
public void run() {
System.out.println("Thread using Thread class");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
(2) 实现 Runnable
接口:
创建一个新类,该类实现Runnable
接口,并重写run
方法。然后创建该类的实例,并将它传递给一个Thread
对象,然后调用Thread
对象的start
方法来启动线程。
public class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread using Runnable interface");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
(3) 实现 Callable
接口:
创建一个新类,该类实现Callable
接口,并重写call
方法。然后可以使用FutureTask
类来包装Callable
对象,并将FutureTask
对象传递给一个Thread
对象来启动线程。这种方式的优点是可以获取线程的返回值和异常。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
public String call() throws Exception {
return "Thread using Callable interface";
}
}
public class Main {
public static void main(String[] args) {
MyCallable callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
String result = futureTask.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2、Java线程池
(1)线程池参数:
(2)线程池执行流程
3、JAVA的设计模式
-
创建型设计模式:
- 单例模式(Singleton Pattern)
- 工厂方法模式(Factory Method Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 建造者模式(Builder Pattern)
- 原型模式(Prototype Pattern)
-
结构型设计模式:
- 适配器模式(Adapter Pattern)
- 桥接模式(Bridge Pattern)
- 组合模式(Composite Pattern)
- 装饰器模式(Decorator Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
-
行为型设计模式:
- 责任链模式(Chain of Responsibility Pattern)
- 命令模式(Command Pattern)
- 解释器模式(Interpreter Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 备忘录模式(Memento Pattern)
- 观察者模式(Observer Pattern)
- 状态模式(State Pattern)
- 策略模式(Strategy Pattern)
- 模板方法模式(Template Method Pattern)
- 访问者模式(Visitor Pattern)
4、单例模式为什么是线程安全的
(1)饿汉式(Eager Initialization):在这种实现中,实例是在类加载时创建的,这确保了线程安全性。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
(2) 双重检查锁定(Double-Checked Locking):在这种实现中,我们使用synchronized块和两次null检查来确保线程安全性。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
(3)静态内部类(Static Inner Class):在这种实现中,我们使用一个静态内部类来持有单例的实例,这利用了类加载机制来保证线程安全性。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
(4)枚举(Enum):使用枚举是创建单例的最简单和最安全的方法,它本身就是线程安全的,并且能防止反序列化创建新的对象。
public enum Singleton {
INSTANCE;
public void doSomething() {
// ...
}
}
5、单例模式和工厂模式实现
(1)单例模式(懒汉式,线程安全)
public class Singleton {
private static volatile Singleton instance;
// 私有化构造器,防止外部实例化
private Singleton() {}
// 提供一个全局的访问点
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
运用场景:
- 配置管理:在一个系统中,配置信息是唯一的,我们可以使用单例模式来保证配置对象的唯一性,避免频繁地创建和销毁。
- 连接池:例如数据库连接池,线程池等,它们管理的资源是有限的,通过单例模式来确保整个应用中有一个统一的资源访问入口。
- 日志记录器:日志记录器通常也会是单例的,以保证日志的连贯性和性能。
- 缓存系统:在一个应用中,缓存对象通常也是唯一的,可以使用单例模式来保证缓存系统的一致性。
- Spring框架中的Bean:在Spring框架中,Bean默认是单例的,这样可以节省资源和提高效率。
(2) 工厂模式
public interface Product {
void create();
}
public class ProductA implements Product {
@Override
public void create() {
System.out.println("Product A created");
}
}
public class ProductB implements Product {
@Override
public void create() {
System.out.println("Product B created");
}
}
public class ProductFactory {
public static Product createProduct(String type) {
if ("A".equals(type)) {
return new ProductA();
} else if ("B".equals(type)) {
return new ProductB();
} else {
throw new IllegalArgumentException("Unknown product type");
}
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Product productA = ProductFactory.createProduct("A");
productA.create();
Product productB = ProductFactory.createProduct("B");
productB.create();
}
}
运用场景:
-
创建库和框架:当创建一个库或框架时,你可能希望提供一种方法给使用者创建某个接口的实例,但不想让他们知道具体的实现类。
-
UI库:许多UI库使用工厂模式来创建控件。例如,在一个跨平台的UI库中,你可能有一个
Button
接口和多个具体的实现类(如WindowsButton
,MacButton
等)。使用工厂模式可以根据运行的操作系统创建正确的按钮类型。 -
支持多种支付方法:例如,如果你正在开发一个电商平台,你可能有一个
PaymentProcessor
接口和多个具体的实现类(如CreditCardProcessor
,PaypalProcessor
等)。使用工厂模式可以根据用户选择创建正确的支付处理器。 -
加载和注册插件或驱动程序:应用程序可能使用工厂模式动态地加载和注册插件或驱动程序。
-
数据库访问:应用程序可能需要与多种数据库进行交互。使用工厂模式,可以为不同的数据库创建适当的数据库连接和查询对象
6、Java的几个特性,说说你对Java多态怎么理解
- 面向对象:Java是一个面向对象的编程语言,它支持类和对象的概念,以及继承、封装和多态这些面向对象的基本特性。
- 平台无关:Java程序(字节码)可以在任何装有Java虚拟机(JVM)的设备上运行,无论该设备的操作系统是什么。
- 强类型:在Java中,每个变量都必须有一个预先定义的数据类型,这有助于提高代码的健壮性和可预测性。
- 多线程支持:Java内置了多线程支持,允许你开发高效并发的应用程序。
- 内存管理:Java有一个垃圾收集系统,它自动回收不再使用的内存,这有助于防止内存泄漏。
Java多态实现:
- 方法重载(Overloading) - 编译时多态: 在同一个类中,你可以有多个方法共享同一个名称,但是有不同的参数列表(不同类型或不同数量的参数)。这称为方法重载,它是编译时多态的一种形式。
- 方法重写(Overriding) - 运行时多态: 子类可以提供一个特定实现来重写父类的方法。这是运行时多态的一种形式,因为具体调用哪个方法是在运行时决定的。
7、GC垃圾回收算法,jvm GC如何清除一个对象,判断是否清除该对象的方法、
(1) GC垃圾回收算法
-
标记-清除算法(Mark-Sweep)
- 标记阶段:标记所有从根节点可达的对象。
- 清除阶段:清除所有未被标记的对象。
-
标记-整理算法(Mark-Compact)
- 标记阶段:标记所有从根节点可达的对象。
- 整理阶段:将所有活动对象移动到内存的一端,然后清除剩余的内存空间。
-
分代收集算法(Generational Collection)
- 将内存分为几个代(如年轻代和老年代)并根据对象的年龄对其进行不同的垃圾收集策略。
-
引用计数算法(Reference Counting)
- 这种算法在每个对象中维护一个引用计数,当计数为零时,对象被视为垃圾。
(2) 判断是否应清除一个对象
-
可达性分析
- 从一组根对象开始,如果一个对象不能通过任何引用链从根对象到达,它就是不可达的,因此可以被回收。
-
引用类型
- Java 提供了几种不同的引用类型(强引用、软引用、弱引用和虚引用),它们对垃圾收集的影响也是不同的。
- 强引用:只要对象有强引用指向它,它就不会被回收。
- 软引用:对象只有软引用时,它可以被GC回收,但通常是在内存不足的时候才会被回收。
- 弱引用:对象只有弱引用时,它会更容易被GC回收。
- 虚引用:对象有虚引用时,基本不影响其生命周期,虚引用主要用于跟踪对象被GC回收的活动。
-
循环引用
- 即使对象之间存在循环引用,但如果整个引用链不可达,那么这些对象仍然可以被视为垃圾。
-
finalize 方法
- 对象提供了
finalize
方法来进行清理操作。这个方法会在对象被GC回收前被调用一次。但是要注意,这个方法不推荐使用,因为它可以拖延对象的回收时间,并且有可能导致对象复活。
- 对象提供了
8、JVM内存
-
方法区(Method Area)
- 类信息存储:这里存储了每一个加载到JVM中的类的信息,包括类的元数据、静态成员变量、常量池等。
- 运行时常量池:这部分内存包含了编译时期生成的各种字面量和符号引用,这些内容会在类加载后进入方法区的运行时常量池中。
-
堆区(Heap)
- 对象存储:这是JVM管理的最大一块内存区域,几乎所有的对象实例以及数组都在这里分配内存。
- 垃圾收集区:这个区域是垃圾收集器工作的重点区域。
- 年轻代(Young Generation):新创建的对象首先分配在年轻代,这个区域分为一个Eden区和两个Survivor区(S0和S1)。
- 老年代(Old Generation或Tenured Generation):对象在年轻代经过一定次数的垃圾收集后(如果还存活),会被移到老年代。
-
栈区(Stack)
- 局部变量表:用于存储方法中的局部变量。
- 操作数栈:一个基于栈的计算区域,用于存储方法执行过程中的中间结果。
- 动态链接和返回地址:存储了方法执行的动态链接信息和返回地址信息。
-
程序计数器(Program Counter Register)
- 当前线程执行字节码的行号指示器:每个线程都有一个程序计数器,用于记录当前线程正在执行的字节码指令的地址。
-
本地方法栈(Native Method Stack)
- 本地方法调用:与栈区相似,但是它是为本地方法(使用native关键字声明的方法)服务的
9、Java内存泄漏怎么造成的,怎么排查
在Java中,内存泄漏通常是由于以下几种原因造成的:
- 对象的长时间引用:一些对象被长时间持有引用,导致它们不能被垃圾回收器回收。
- 集合类的不当使用:比如往集合类(如HashMap)中添加对象但不移除它们。
- 监听器未被正确移除:注册了一些监听器或回调函数但没有正确地注销它们。
- 静态字段的滥用:静态字段的生命周期与类的生命周期相同,如果静态字段持有对象的引用,那么这个对象就不会被回收,直到程序结束。
- 线程泄漏:创建了大量线程但没有正确地关闭它们,这也会造成内存泄漏。
- 内部类和外部类之间的引用:非静态内部类会持有外部类的引用,如果不当使用,可能会导致内存泄漏。
排查Java内存泄漏,通常可以使用以下几种方法:
-
使用内存分析工具:
这些工具可以帮助你监视Java应用程序的内存使用情况,找出内存泄漏的来源。
- VisualVM
- Eclipse Memory Analyzer (MAT)
- JProfiler
-
查看Heap Dump: 通过获取Heap Dump来分析哪些对象占用了大量的内存和它们的引用链。可以使用
jmap
工具或者内存分析工具来获取Heap Dump。 -
分析GC日志: 通过分析GC日志来查看对象的分配和回收情况。你可以使用
-XX:+PrintGCDetails
和-XX:+PrintGCTimeStamps
等JVM选项来启用GC日志。 -
代码审查: 手动检查代码中可能存在内存泄漏的地方,特别是注意那些长时间持有对象引用的地方。
-
创建测试用例: 创建特定的测试用例来复现潜在的内存泄漏情况,然后使用内存分析工具来分析内存使用情况。
-
使用监控系统: 在生产环境中使用监控系统来监控Java应用程序的内存使用情况,这样可以及时发现内存泄漏问题。
10、Java怎么实现线程同步
(1)synchronized 关键字
public void method() {
synchronized(this) {
// ... 同步代码块
}
}
(2)CountDownLatch
可以用来实现一个或多个线程等待其他线程完成操作。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
private final CountDownLatch latch = new CountDownLatch(1);
public void method() throws InterruptedException {
latch.await();
// ... 受保护的代码块
}
public void release() {
latch.countDown();
}
}
11、Java有哪些集合,分别作用于哪些场景
(1)List 接口:
ArrayList
- 场景:动态数组实现,适用于随机访问元素的场景。
- 特点:支持快速随机访问,但插入和删除元素(尤其是在列表中间)可能较慢。
LinkedList
- 场景:双向链表实现,适用于频繁插入和删除元素的场景。
- 特点:插入和删除元素快速,但随机访问元素较慢
(2)Set 接口
HashSet
- 场景:无序集合,适用于存储不允许重复的元素的场景。
- 特点:提供快速的添加、删除和查找操作,但不保证元素的顺序。
TreeSet
- 场景:有序集合,适用于需要按某种顺序存储元素的场景。
- 特点:元素按照自然顺序或自定义比较器的顺序进行排序。
(3)Queue 接口
LinkedList
- 场景:可以作为队列使用,适用于FIFO(先进先出)场景。
- 特点:实现了 Queue 接口,可以作为双端队列使用。
PriorityQueue
- 场景:优先队列,适用于需要根据元素的自然顺序或自定义比较器排序的场景。
- 特点:元素可以按照优先级进行排序和检索。
Map 接口
HashMap
- 场景:无序映射,适用于需要快速查找键-值对的场景。
- 特点:提供快速的添加和查找操作,但不保证键的顺序。
TreeMap
- 场景:有序映射,适用于需要根据键的自然顺序或自定义比较器排序的场景。
- 特点:键按照自然顺序或自定义比较器的顺序进行排序。
LinkedHashMap
- 场景:保持插入顺序的映射,适用于需要记录键-值对插入顺序的场景。
- 特点:保持键的插入顺序。
12、Java有哪些引用类型,Java里一个对象没有被引用了会被丢弃吗?什么时候丢弃?
(1) 强引用(Strong Reference):常见的对象引用就是强引用,即创建一个对象并赋值给一个变量。
Object obj = new Object();
(2) 软引用(Soft Reference)软引用可以让对象在内存不足时被垃圾收集器回收。
SoftReference<Object> softReference = new SoftReference<>(new Object());
(3) 弱引用(Weak Reference):弱引用比软引用更“弱”,垃圾收集器会更积极地回收弱引用指向的对象。
WeakReference<Object> weakReference = new WeakReference<>(new Object());
(4)虚引用(Phantom Reference):虚引用是最“弱”的一种引用类型,它不能用来访问对象,只能用来跟踪对象被垃圾收集器回收的状态。
PhantomReference<Object> phantomReference = new PhantomReference<>(new Object(), new ReferenceQueue<>());
在Java中,一个对象在没有任何引用指向它时(或者只有弱引用或虚引用指向它时),它就成为垃圾收集的候选对象。具体什么时候被回收是由垃圾收集器决定的,这取决于垃圾收集器的实现和当前的垃圾收集策略。
13、Java异常了解吗?说说平时遇到的异常?说出6种常见的java异常。
-
NullPointerException:这是一个运行时异常,通常发生当你试图访问一个 null 对象的成员时。
-
ArrayIndexOutOfBoundsException:这也是一个运行时异常,发生于尝试访问数组的一个不存在的索引时。
-
ClassCastException:这是一个运行时异常,发生于尝试将一个对象强制转换为不兼容的类型时。
-
IOException:这是一个检查异常,通常发生在 I/O 操作失败或被中断时。需要用 try-catch 语句或者 throws 关键字来处理。
-
FileNotFoundException:这是 IOException 的一个子类,是一个检查异常,通常发生在尝试访问一个不存在的文件时。
-
NumberFormatException:这是一个运行时异常,发生在尝试将一个字符串转换为数字,但字符串的格式不正确时
14、数字转换成字符串用哪个函数,要是出错会出现什么异常
如果在尝试将一个不合适的字符串转换为数字(而不是数字转换为字符串)时,你会遇到 NumberFormatException
。这通常发生在使用 Integer.parseInt()
或Double.parseDouble()
等方法时,如果传递的字符串不能转换为有效的数字,则会抛出此异常。
15、Java反射机制,优点缺点,动态代理的好处和坏处
(1)Java反射机制优点缺点
优点:
- 灵活性:允许运行时动态创建对象和调用方法,使得代码更加灵活。
- 框架开发:便于创建灵活且可扩展的框架。
- 解耦:有助于减少代码之间的依赖关系,使系统更易于维护。
缺点:
- 性能开销:反射操作相比直接的方法调用有更高的时间和空间开销。
- 安全性:由于可以动态地调用任何方法,可能引发安全问题。
- 代码复杂性:使用反射可能使代码更加复杂和难以理解。
(2) 动态代理的好处和坏处
好处:
- 解耦:能够在不修改目标对象代码的情况下对目标对象进行功能增强。
- 灵活性:可以动态创建代理类和代理对象,以及动态处理代理方法。
坏处:
- 性能开销:动态代理涉及到反射,这会增加系统的运行时开销。
- 代码复杂性:实现动态代理通常涉及到复杂的代码逻辑。
16、说说静态变量和非静态变量的区别,用类名调用静态变量和用实例对象调用静态变量有区别吗?非静态方法里可以有静态变量吗?static能不能被重写?
(1) 静态变量和非静态变量的区别
-
静态变量(Static Variables):
- 属于类,而不是任何单一的实例。
- 由所有实例共享。
- 可以通过类名直接访问。
-
非静态变量(Instance Variables):
- 每个类的实例都有其自己的一套非静态变量。
- 只能通过类的实例访问。
(2) 类名调用和实例对象调用静态变量的区别
- 使用类名来调用静态变量是推荐的做法,因为它明确指出该变量是一个静态变量。
- 通过实例对象也可以访问静态变量,但这可能会导致代码读者的混淆,因为它看起来像是在访问一个实例变量。
(3) 在非静态方法中使用静态变量
- 是可以的,非静态方法可以访问静态变量。
(4) static 方法是否可以被重写:
- static 方法不能被重写,因为它们是属于类的,而不是属于任何单一的实例。
17、反射能破坏private,那还要private干嘛?
- 使用 private 是为了封装和保护类的内部实现,它可以防止类的使用者直接访问和修改内部状态,这有助于保持类的稳定性和可维护性。
- 虽然反射可以破坏 private 的访问控制,但这通常是在特殊情况下才会使用的技术,比如框架开发或测试。
18、局部变量不赋值不能通过编译,为啥?
局部变量不赋值不能通过编译是因为Java要保证在任何情况下,使用局部变量时它都已经被初始化了。Java编译器会检查局部变量在它被使用之前是否已经被赋值了一个初始值,如果没有,就会报编译错误。这是为了避免未初始化的局部变量导致程序的不确定行为。
19、list和set区别(重复/不重复)
20、stringbuffer 和 stringbuilder
21、Hashmap底层(数组+链表+红黑树)
22、数组和链表存储的使用场景,为什么
二、数据库八股
1、数据库sql怎么优化
-
索引优化
- 创建适当的索引:根据查询条件和JOIN操作创建适当的索引。
- 避免全表扫描:尽量避免全表扫描,使用索引来加速查询。
- 索引维护:定期维护和重建索引,以保持其效率。
-
查询结构优化
- 简化查询:尽量避免复杂的查询,可以通过分解查询来简化。
- 避免使用子查询:尽量避免使用子查询,可以使用JOIN或临时表替代。
- 使用预编译语句:使用预编译语句可以避免SQL注入攻击,并提高SQL执行效率。
-
SQL写法优化
- 避免使用*选择所有列:只选择需要的列,而不是选择所有列。
- 使用批量插入和更新:使用批量插入和更新可以减少数据库的IO操作。
-
数据库设计优化
- 数据库分区:根据数据的访问模式和大小,可以考虑数据库分区来提高查询效率。
- 数据归档和分库分表:对于大数据量的情况,可以考虑数据归档和分库分表来提高查询效率。
-
优化事务
- 减少锁的范围:减少锁的范围可以减少锁冲突,提高系统的并发性能。
- 合理设置事务隔离级别:根据业务需求,合理设置事务隔离级别,避免不必要的锁冲突。
2、数据库中索引B+树
3、什么情况下会出现幻读
幻读(Phantom Read)是指在一个事务内多次查询时,由于其他事务的插入或删除操作,导致后一次查询结果中包含前一次查询中没有的行,或者少了某些行。幻读主要是因为其他事务插入或删除了记录所引起的。
比如:
- 事务A第一次读取了10行数据。
- 此时事务B插入了几行数据或删除了几行数据并提交了事务。
- 事务A再次读取相同的数据,发现行数与第一次不一样,此时出现了幻读。
幻读可以通过使用更高的事务隔离级别(如可重复读或串行化)来避免。
4、多表查询了解吗,使用过哪些
多表查询是在数据库中同时查询多个表的数据。这种查询通常使用以下几种技术:
- 连接(Join):使用JOIN关键字将多个表连接在一起进行查询。常见的有内连接(INNER JOIN)、左外连接(LEFT JOIN)、右外连接(RIGHT JOIN)和全外连接(FULL OUTER JOIN)。
- 子查询:在一个查询的WHERE或SELECT段中嵌套另一个查询。
- 并集(Union):使用UNION或UNION ALL操作符将多个SELECT查询的结果合并成一个结果集。
- 交叉查询(Cross Join):产生笛卡尔乘积,即每个表的每行与另一个表的每行组合。
5、从数据库中查询一个数据和查询10个数据,有什么差别
- 性能:查询10个数据通常会比查询1个数据稍慢,因为需要处理更多的数据。
- 资源使用:查询10个数据可能会占用更多的数据库资源(如内存和CPU)。
- 数据传输:从数据库服务器到应用服务器的数据传输量会增加。
- 索引效率:如果数据库有合适的索引,查询一个数据可能会更快,因为可以直接定位到数据所在的位置。查询10个数据可能涉及更多的索引扫描或全表扫描。
6、读未提交是异常吗,数据库如何解决读未提交
-
读未提交(Read Uncommitted)不是异常,而是事务的一种隔离级别。在这种隔离级别下,一个事务可以读取到另一个事务未提交的数据。
-
如何解决:
- 提高隔离级别:通过将事务隔离级别设置为读已提交(Read Committed)、可重复读(Repeatable Read)或串行化(Serializable)可以避免读未提交的现象。
- 使用锁:数据库管理系统可以通过使用各种类型的锁(如共享锁和排他锁)来控制对数据的访问,从而避免读未提交的情况。
三、计算机网咯八股
1、TCP三次握手四次挥手
(1)三次握手的步骤如下:
客户端向服务器发送一个SYN包,表示请求建立连接。
服务器接收到客户端发来的SYN包后,对该包进行确认后结束LISTEN阶段,并返回一段TCP报文,表示确认客户端的报文序号有效,服务器能正常接收客户端发送的数据,并同意创建新连接。
客户端接收到服务器发来的TCP报文后,再向服务器发送一段确认报文,表示客户端已经准备好发送数据。
(2)四次挥手的步骤如下:
客户端向服务器发送一个FIN包,表示请求断开连接。
服务器接收到客户端发来的FIN包后,对该包进行确认后进入CLOSE_WAIT状态。
服务器向客户端发送一个ACK包,表示已经准备好断开连接。
客户端接收到服务器发来的ACK包后,进入TIME_WAIT状态,并向服务器发送一个FIN包,表示已经准备好断开连接。
由TCP的三次握手和四次断开可以看出,TCP使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据正式传输前就有了交互, 为数据正式传输打下了可靠的基础。
2、IOS七层和TCP四层协议
(1)OSI 七层模型是一个标准化的网络协议族层次划分,每一层都有特定的功能和责任。从上到下,这些层次是:
-
应用层(Application Layer)
- 负责提供网络服务与最终用户的接口。
- 常见协议:HTTP, HTTPS, FTP, SMTP, POP3, IMAP等。
-
表示层(Presentation Layer)
- 负责数据格式转换、数据加密等。
- 例子:ASCII, UTF-8, JPEG, MPEG等。
-
会话层(Session Layer)
- 负责建立、管理和终止会话。
- 功能包括:对话控制、同步等。
-
传输层(Transport Layer)
- 负责端对端的通信和流控制。
- 常见协议:TCP, UDP。
-
网络层(Network Layer)
- 负责数据包的路由和转发。
- 常见协议:IP, ICMP, OSPF, BGP等。
-
数据链路层(Data Link Layer)
- 负责在相邻网络节点间的数据传输。
- 分为两个子层:逻辑链路控制(LLC)和媒体访问控制(MAC)。
- 常见协议:ARP, PPP, Ethernet等。
-
物理层(Physical Layer)
- 负责比特流在物理媒介(如电缆、光纤)上的传输。
- 包括定义物理媒体的特性、比特编码等。
(2)TCP/IP 模型是实际使用最为广泛的网络协议族结构模型,它简化了 OSI 模型层次划分,主要包括以下几层:
-
应用层(Application Layer)
- 对应 OSI 的应用层、表示层和会话层。
- 负责提供网络服务与最终用户的接口。
- 常见协议:HTTP, SMTP, FTP等。
-
传输层(Transport Layer)
- 对应 OSI 的传输层。
- 负责提供端到端的通信服务。
- 常见协议:TCP, UDP。
-
网络层(Network Layer)
- 对应 OSI 的网络层。
- 负责路由和转发数据包。
- 常见协议:IP, ICMP。
-
链路层(Link Layer)
- 对应 OSI 的数据链路层和物理层。
- 负责在相邻网络节点间的数据传输。
- 常见协议:Ethernet, PPP, ARP等.
3、TCP报文结构,HTTP结构
(1)TCP (Transmission Control Protocol) 是一个面向连接的、可靠的、基于字节流的传输层通信协议。其报文段结构主要包括以下字段:
- 源端口号(Source Port):16位,表示数据包的发送方的端口号。
- 目的端口号(Destination Port):16位,表示数据包的接收方的端口号。
- 序号(Sequence Number):32位,用于标识从TCP源端到目的端的数据流中发送的数据字节流的编号。
- 确认号(Acknowledgment Number):32位,如果ACK标志位为1,则该字段包含接收方期望接收的下一个序列号,即期望接收序列号+1。
- 数据偏移(Data Offset):4位,表示TCP报文段的头部长度。
- 保留(Reserved):6位,保留为未来使用。
- 控制位(Flags):9位,其中包含了如SYN、ACK、FIN等标志位,用于控制TCP的状态转换。
- 窗口大小(Window Size):16位,表示接收方的缓冲区大小。
- 校验和(Checksum):16位,用于检查报文段的完整性和正确性。
- 紧急指针(Urgent Pointer):16位,仅当URG标志位为1时有效,表示紧急数据的末尾。
- 选项(Options):可变长度,包含一些可选字段,如最大段大小(MSS)、时间戳等。
- 数据部分(Data):可变长度,包含实际传输的数据。
(2) HTTP (HyperText Transfer Protocol) 是一个基于TCP/IP协议的应用层协议。它用于传输超文本,如网页。HTTP报文可以分为请求报文和响应报文,结构如下:
-
请求行/状态行(Request Line/Status Line):
- 请求报文:包含HTTP方法(GET, POST等)、请求URI和HTTP版本。
- 响应报文:包含HTTP版本、状态码和状态描述。
-
头部字段(Headers):
- 包含一系列的键值对,用于描述报文的属性,如
Content-Type
、User-Agent
、Accept
等。
- 包含一系列的键值对,用于描述报文的属性,如
-
空行(Empty Line):
- 头部和主体之间的一个空行,用于分隔头部和主体。
-
主体(Body):
- 请求报文:可包含要发送的数据,比如POST请求中的表单数据。
- 响应报文:包含服务器返回的数据,比如请求的HTML页面。
4、设计一个应用层协议
-
协议名称
- 让我们命名这个协议为“SimpleAppProtocol”(简称 SAP)。
-
协议版本
-
消息结构:每条消息将由以下几部分组成:
-
Header(头部):
-
Version
(2 bytes):协议版本。 -
Type
(1 byte):消息类型(例如,请求或响应)。 -
Command
(1 byte):命令代码。 -
Length
(4 bytes):消息体的长度。
-
-
Body(消息体):
- 携带具体的数据或参数,其结构依赖于命令类型。
-
Footer(尾部):
-
Checksum
(2 bytes):校验和,用于检查消息的完整性。
-
-
Header(头部):
-
命令集
-
错误处理
-
安全性:
- 认证:在登录命令中包含用户名和密码进行用户认证。
- 加密:可考虑为重要的数据传输提供加密选项。
- 防止重放攻击:可以通过在消息中包含一个时间戳和/或唯一标识符来防止重放攻击
四、其他八股
1、给你10个tomcat你如何判断自己用哪个
(1)你可以通过查看访问的URL中的端口号来判断你正在使用哪个Tomcat实例
(2)每个Tomcat实例都会有其自己的日志文件,通常位于logs
目录中。你可以通过查看特定Tomcat实例的日志来确定你正在使用哪个实例。
(3)如果每个Tomcat实例都有其自己的管理控制台,你可以登录到管理控制台来查看和管理每个Tomcat实例
(4)每个Tomcat实例都有其自己的配置文件,通常位于conf
目录中。你可以查看server.xml
文件中的<Server>
元素的port
属性来确定每个Tomcat实例的端口号。
(5)如果可能,你可以在每个Tomcat实例的某个应用程序页面上添加一个唯一的标识,例如一个特殊的HTML注释或者一个隐藏的元素,这样你可以通过查看页面源代码来确定你正在使用哪个Tomcat实例。
2、负载均衡算法
(1)RandomLoadBalance:根据权重随机选择(对加权随机算法的实现)。这是Dubbo默认采用的一种负载均衡策略。
(2)LeastActiveLoadBalance:最小活跃数负载均衡。
Dubbo 就认为谁的活跃数越少,谁的处理速度就越快,性能也越好,这样的话,我就优先把请求给活跃数少的服务提供者处理。
(3)ConsistentHashLoadBalance:一致性Hash负载均衡策略。
ConsistentHashLoadBalance 中没有权重的概念,具体是哪个服务提供者处理请求是由你的请求的参数决定的,也就是说相同参数的请求总是发到同一个服务提供者。另外,Dubbo 为了避免数据倾斜问题(节点不够分散,大量请求落到同一节点),还引入了虚拟节点的概念。通过虚拟节点可以让节点更加分散,有效均衡各个节点的请求量。
(4)RoundRobinLoadBalance:加权轮询负载均衡。
轮询就是把请求依次分配给每个服务提供者。加权轮询就是在轮询的基础上,让更多的请求落到权重更大的服务提供者上。
3、什么是无向图和有向图,什么时候会用到,举例
(1)无向图
- 社交网络:在Facebook或LinkedIn中,如果用户A和用户B是朋友或联系人,则存在一个无向边,表示这种双向的友谊或联系关系。
- 物联网:在物联网网络中,无向图可以用来表示各种物理设备之间的双向通信。
(2)有向图
- 网页链接:在万维网中,网页之间的链接可以看作是有向图,其中每个网页是一个顶点,每个链接是一个有向边,表示从一个页面可以链接到另一个页面。
- 交通网络:在交通网络中,有向图可以用来表示单行道,从A点到B点和从B点到A点可能是两条不同的路线或者一方不通行。
4、在浏览器填入地址后会发生什么操作
- 用户在浏览器地址栏输入URL “ https://www.baidu.com/ ”。
- 浏览器查找当前URL的DNS缓存记录。
- DNS解析URL对应的IP。
- 根据IP建立TCP连接(三次握手)。
- HTTP发起请求。
- 服务器处理请求并返回HTTP报文。
- 浏览器解析渲染页面
5、生产者和消费者,多生产者同步
为了确保多个生产者和消费者能够正确地共享数据,你需要使用同步机制来控制对共享数据的访问。在Java中,你可以使用以下的同步机制:
- synchronized 关键字
- ReentrantLock
五、算法
1、冒泡排序、快速排序、归并排序
(1)冒泡排序
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
(2)快速排序
def quick_sort(arr):
if len(arr) <= 1:
return arr
else:
pivot = arr[len(arr)//2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
(3)归并排序
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
2、输出 2 到 N 之间的全部素数
综合起来,这个表达式将检查i
是否可以被2到√i之间的任何数字整除。如果没有任何这样的数字(即,i
不能被2到√i之间的任何数字整除),那么all
函数将返回True
,表示i
是一个素数。如果i
可以被2到√i之间的任何数字整除,all
函数将返回False
,表示i
不是一个素数。
def find_primes(n):
primes = []
for i in range(2, n+1):
is_prime = all(i % j != 0 for j in range(2, int(i ** 0.5) + 1))
if is_prime:
primes.append(i)
return primes
3、双向链表插入一个节点
class Node:
def __init__(self, data):
self.data = data
self.next = None
self.prev = None
class DoublyLinkedList:
def __init__(self):
self.head = None
def insert_at_beginning(self, data):
new_node = Node(data)
new_node.next = self.head
if self.head:
self.head.prev = new_node
self.head = new_node
def insert_at_end(self, data):
new_node = Node(data)
if not self.head:
self.head = new_node
return
temp = self.head
while temp.next:
temp = temp.next
temp.next = new_node
new_node.prev = temp
def insert_after_node(self, target_data, data):
new_node = Node(data)
temp = self.head
while temp:
if temp.data == target_data:
break
temp = temp.next
if temp:
new_node.next = temp.next
new_node.prev = temp
if temp.next:
temp.next.prev = new_node
temp.next = new_node
else:
print(f"Node with data {target_data} not found")
4、链表找环
class Node:
def __init__(self, data):
self.data = data
self.next = None
def detect_cycle(head):
if not head or not head.next:
return False
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
# 示例代码:
# 创建一个有环链表
head = Node(1)
head.next = Node(2)
head.next.next = Node(3)
head.next.next.next = Node(4)
head.next.next.next.next = head.next
# 检测环
print(detect_cycle(head)) # 输出: True
5、链表找环入口,不用双指针怎么做
方法步骤:
- 遍历链表,同时将访问过的节点存储到哈希集合中。
- 当我们再次访问到已存在于哈希集合中的节点时,这个节点就是环的入口。
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
def detectCycle(head):
seen = set()
current = head
while current:
if current in seen:
return current
seen.add(current)
current = current.next
return None
6、字符串反转
def reverse_string(s):
return s[::-1]
def reverse_string(s):
stack = list(s)
result = []
while stack:
result.append(stack.pop())
return ''.join(result)
7、Java在不引用第三个变量前提下使两个整数类型交换值
(1) 使用加法和减法
public class SwapWithoutThirdVariable {
public static void main(String[] args) {
int a = 5;
int b = 3;
a = a + b; // 此时 a 是 a 和 b 的和
b = a - b; // 减去 b (原始的 b),得到原始的 a
a = a - b; // 减去新的 b (原始的 a),得到原始的 b
System.out.println("a: " + a); // 输出: a: 3
System.out.println("b: " + b); // 输出: b: 5
}
}
(2) 使用俺位异或运算(XOR):如果两个比较位不同,则结果位将为1。如果两者相同,则结果为0。文章来源:https://uudwc.com/A/XkgPr
public class SwapWithoutThirdVariable {
public static void main(String[] args) {
int a = 5;
int b = 3;
a = a ^ b; // XOR 运算
b = a ^ b; // XOR 运算
a = a ^ b; // XOR 运算
System.out.println("a: " + a); // 输出: a: 3
System.out.println("b: " + b); // 输出: b: 5
}
}
8、求四个数绝对值的最小值,有没有办法优化之类的
min_abs_value = min(min(abs(a), abs(b)), min(abs(c), abs(d)))
9、如果有两个方法,AB方法分别有上下两部分,要求B方法下部分结束后,才能执行A上部分的方法,如何做?
初始化 信号量S = 0;
方法A {
上部分 {
P(S); // 会阻塞直到S变成1
// 执行A方法的上部分代码
}
下部分 {
// 执行A方法的下部分代码
}
}
方法B {
上部分 {
// 执行B方法的上部分代码
}
下部分 {
// 执行B方法的下部分代码
V(S); // 使S变成1,这样A方法的P操作将不再阻塞
}
}
文章来源地址https://uudwc.com/A/XkgPr