【金三银四】刷刷八股吧,准备新的一周的到来

前言

八股整理题目来源:https://learn.skyofit.com/archives/66

1、Java中==和equals有什么区别?

在Java中,==equals() 方法都是用来比较对象的,但它们在功能和用途上有显著差异:

  1. == 运算符

    • 对于基本数据类型(如 intcharboolean 等),== 比较的是它们的值是否相等。
    • 对于引用类型(如对象),== 比较的是对象的引用地址是否相同,也就是说,它判断的是两个引用是否指向内存中的同一个对象实例。
  2. equals() 方法

    • equals() 是从 Object 类继承而来的方法,所有类都默认拥有这个方法。
    • 默认情况下,equals() 在引用类型上与 == 行为相同,即比较对象的引用地址是否相同。
    • 但是,像 StringIntegerDate 等类对 equals() 方法进行了重写,使得它按照类的业务逻辑来比较对象的内容是否相等,而不是比较引用地址。
    • 如果要在自定义类中正确地比较对象内容,也需要覆盖 equals() 方法,并遵守其约定(如自反性、对称性、传递性、一致性),这样才能准确地比较两个对象的内在状态是否相等。

总结起来,如果你想比较两个基本类型的值,可以使用 ==;而对于对象,尤其是需要比较其内容是否相等时,通常需要使用重写过的 equals() 方法。如果不确定自定义类的 equals() 方法是否重写过,最好查阅类的文档或源码以确认其比较逻辑。

2、String, StringBuffer, StringBuilder区别

String, StringBuffer, StringBuilder 都是Java中用于处理文本字符串的类,它们的主要区别在于可变性和线程安全性:

  1. String

    • String 类表示不可变的字符序列,一旦创建了 String 对象,其内容就无法修改。每次对 String 进行拼接、替换等操作都会创建一个新的 String 对象,这可能导致较大的内存开销。
    • 由于 String 是不可变的,所以在多线程环境下不需要进行额外的同步操作,因此是线程安全的。
  2. StringBuffer

    • StringBuffer 类表示可变的字符序列,它允许进行字符串的修改操作,如添加、删除、替换等,而不会每次都创建新的对象。
    • StringBuffer 是线程安全的,因此在多线程环境下,多个线程可以共享一个 StringBuffer 对象进行操作,而不会出现并发问题。
  3. StringBuilder

    • StringBuilder 类同样表示可变的字符序列,其功能与 StringBuffer 几乎完全相同,也是为了进行字符串的修改操作而设计的。
    • StringBuffer 的主要区别在于 StringBuilder 是非线程安全的,因此在单线程环境下,其操作效率往往高于 StringBuffer,因为不必付出线程同步的额外开销。

结论:

  • 当需要频繁修改字符串并且程序处于单线程环境时,优先选用 StringBuilder,因为它没有线程安全的开销,性能更高。
  • 当需要修改字符串且程序处于多线程环境时,应使用 StringBuffer,因为它能保证线程安全。
  • 如果字符串内容不需要修改,或者只是简单地读取和显示,使用 String 类最为合适。

3、项目中对泛型的使用

在项目中,泛型(Generics)的使用是为了提高代码的类型安全性和可重用性,它允许在编译时就可以检测到非法类型的操作,避免运行时的ClassCastException。以下是一些泛型在项目中常见的应用场景:

  1. 集合类的使用

    • List<T>Set<T>Map<K, V> 等集合框架中的类都广泛使用了泛型。例如,你可以创建一个只允许存储特定类型的列表,如 List<String>,这样就确保了集合中只能添加字符串类型的元素,编译器会在编译阶段检查类型。
    List<String> names = new ArrayList<>();
    names.add("Alice"); // 合法操作
    names.add(123); // 编译错误,不能添加非字符串类型
    
  2. 泛型方法

    • 泛型方法允许定义一个方法,它能够接受和返回任意类型的参数,只需在方法签名中指定泛型参数。
    public <T> T findFirst(List<T> list) {
        if (!list.isEmpty()) {
            return list.get(0);
        }
        return null;
    }
    
  3. 泛型类

    • 泛型类允许定义一个类,其内部的成员变量和方法可以使用泛型参数。例如,一个通用的容器类可以使用泛型来容纳不同类型的对象。
    public class Box<T> {
        private T content;
    
        public void setContent(T content) {
            this.content = content;
        }
    
        public T getContent() {
            return content;
        }
    }
    
    Box<Integer> integerBox = new Box<>();
    integerBox.setContent(10); // 可以存储整数
    
  4. 约束泛型类型

    • 可以通过泛型界用来约束泛型类型,例如限制类型必须实现某个接口或继承自某个类。
    public class Pair<K extends Comparable<K>, V> {
        private K key;
        private V value;
    
        // ...
    }
    
    Pair<String, Integer> stringIntPair = new Pair<>("apple", 5);
    
  5. 类型擦除与桥接方法

    • 在Java中,尽管泛型提供了编译时的类型检查,但运行时Java会执行类型擦除,所有泛型参数会被替换为他们的上限(如果是受限的)或Object(如果不受限)。因此,有时还需要提供桥接方法来处理原始类型和泛型类型之间的转换。

总的来说,泛型在项目中被广泛应用,目的是为了让代码更简洁、更具扩展性,并确保类型安全。在实际开发中,泛型可以帮助开发者写出更高质量、更容易维护的代码。

4、运行时数据区是怎样的?线程安全(即线程私有)的有哪些?

Java虚拟机(JVM)的运行时数据区(Runtime Data Areas)主要包括以下几个部分:

  1. 程序计数器(Program Counter Register)

    • 每个线程都有一个独立的程序计数器,用于记录当前正在执行的字节码指令地址。它是线程私有的。
  2. 虚拟机栈(Java Virtual Machine Stacks)

    • 每个线程也有一个私有的虚拟机栈,栈中的每个栈帧(Stack Frame)对应一次方法调用。栈帧中存放局部变量表、操作数栈、动态链接和方法出口等信息,这些都是线程私有的。
  3. 本地方法栈(Native Method Stacks)

    • 与虚拟机栈类似,本地方法栈服务于native方法的调用,也是线程私有的。
  4. Java堆(Heap)

    • Java堆是所有线程共享的一块内存区域,主要存放对象实例和数组。对象的创建和大部分对象的内存分配都在这里进行。虽然堆是共享的,但对象本身是线程间共享的数据,线程对其访问需要通过对象引用进行,因此对象内部的状态更新必须保证线程安全。
  5. 方法区(Method Area)/元空间(Metaspace)

    • 在Java 7及以前,方法区(Method Area)是线程共享的区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。而在Java 8及以后,HotSpot JVM废弃了永久代(PermGen),改为使用元空间(Metaspace),同样为线程共享区域。
  6. 运行时常量池(Runtime Constant Pool)

    • 运行时常量池是方法区的一部分,它存放了类的符号引用和字面量,属于线程共享区域。

线程私有的区域包括:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的区域包括:

  • Java堆
  • 方法区/元空间
  • 运行时常量池

在多线程编程中,线程私有的数据区域不需要考虑线程安全问题,因为每个线程都有自己独立的栈空间。而线程共享的区域则需要通过适当的同步机制(如synchronized、volatile关键字,Lock等)来确保线程安全。

5、对象实例、类信息、常量、静态变量分别在运行时数据区的哪个位置?

在Java虚拟机(JVM)的运行时数据区中,对象实例、类信息、常量和静态变量的存储位置如下:

  1. 对象实例

    • 对象实例存储在Java堆(Heap)中。每当通过new关键字或者其他反射机制创建一个新的对象时,JVM会在堆中为该对象分配内存空间。
  2. 类信息

    • 类信息,包括类的结构(如类名、方法表、字段表等)、方法的字节码、方法的符号引用等,存储在方法区(Method Area)中。在Java 8及其以后版本的HotSpot JVM中,这部分数据存储在元空间(Metaspace)中。
  3. 常量

    • 常量,包括编译器可知的字面量和符号引用,同样存储在方法区(或元空间)中的运行时常量池(Runtime Constant Pool)里。
  4. 静态变量

    • 静态变量分为两种情况:
      • 静态变量的引用(指针)存储在方法区(或元空间)中,与类信息一起,静态变量的实际值(如果它是一个对象引用的话)存储在Java堆中。
      • 如果静态变量是基本类型(如int、double等),其实际值也会存储在方法区(或元空间)中,而不是Java堆。

总之,Java堆主要用于存储对象实例,而方法区(或元空间)主要存储类信息、常量和静态变量的引用(或实际值)。对于静态变量,其引用(或者说声明)与类信息一同存储在方法区,而其实际内容(如果为对象)存储在堆中。

6、Java类加载流程?初始化流程?

Java类加载流程

  1. 加载(Loading)

    • 通过类的全限定名(包括包名)获取二进制字节流。
    • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  2. 验证(Verification)

    • 确保被加载类的信息符合JVM规范,没有安全方面的问题。
    • 包括文件格式验证、元数据验证、字节码验证和符号引用验证。
  3. 准备(Preparation)

    • 为类变量(即静态变量)分配内存并设置初始零值(注意:这里的初始零值并不包括final变量的初始化赋值,final变量在初始化阶段才会被赋予正确的初始值)。
    • 这些内存都在方法区内进行分配。
  4. 解析(Resolution)

    • 将常量池内的符号引用转换为直接引用,如将字符串常量池中的引用解析为实际内存地址。
  5. 初始化(Initialization)

    • 执行类构造器<clinit>()方法的过程,此方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。
    • 初始化是类加载过程的最后一步,此时会执行静态初始化块中的代码和给静态变量赋值的代码。

类初始化流程

类初始化是在类加载过程中的第五步,具体流程如下:

  1. 如果类还没有被初始化,则先触发其直接父类的初始化
  2. 如果类中有初始化语句(包括静态变量初始化和静态代码块)
    • 按照代码顺序依次执行静态变量初始化表达式。
    • 执行静态初始化块中的代码。
  3. 执行类构造器<clinit>()方法
    • <clinit>方法是由编译器合成的,方法体中包含了上述的静态变量初始化和静态代码块的内容。
    • <clinit>方法不同于实例构造器(即构造函数),它是线程安全的,JVM保证在多线程环境下只会被初始化一次。

类初始化发生在第一次主动使用类的时候,如创建类的实例、调用类的静态方法或访问类的静态字段(除了final常量,因为final常量在编译时就被赋值了,无需等到运行时初始化阶段)。

7、JVM双亲委派模型

JVM的双亲委派模型是一种类加载器之间的协作工作模式,用于确定类加载请求的处理方式。在Java中,类加载器负责加载类文件(.class)到JVM中执行。按照双亲委派模型的设计原则,当一个类加载器收到类加载请求时,它首先不是自己去尝试加载,而是把请求转发给它的父类加载器。整个加载请求的委派过程会一直沿着类加载器的层级向上,直到达到最顶层的启动类加载器(Bootstrap ClassLoader)为止。

以下是双亲委派模型的具体步骤:

  1. 当应用程序首次请求加载某个类型时,发起请求的类加载器会先检查该类型是否已经被加载过。
  2. 若尚未加载,则该类加载器会把加载请求提交给它的父加载器进行加载。
  3. 如果父加载器也未加载过该类,那么继续向上委托至祖父类加载器,直至到达引导类加载器。
  4. 引导类加载器负责加载Java平台核心类库,如rt.jar等。若这些类库包含请求加载的类,则加载;否则,加载请求将回退至下一级类加载器。
  5. 如经过逐级委派,最终仍无法找到并加载相应的类,则最初的发起请求的类加载器才会尝试自己去加载对应的类。

这一模型的优势在于:

  • 防止类的重复加载:确保所有加载请求都由同一个加载器或者其祖先加载器加载,从而避免了不同加载器加载同名类而造成的混乱。
  • 保护系统的安全性:类加载器的层级关系使得系统核心类库得以隔离,防止用户自定义类覆盖标准库中的类,增强了系统的安全性。

不过,值得注意的是,虽然双亲委派模型是Java中默认且推荐的类加载策略,但并不是强制性的,开发者可以通过重写类加载器的部分方法实现自定义加载逻辑,从而“破坏”这一模型。

8、反射是什么?如何获取对象?如何通过反射破坏单例

反射是什么?
反射是Java和其他一些编程语言中的一项功能,它允许运行中的Java程序对自身进行检查和操作,包括但不限于获取类、接口、字段、方法等的详细信息,以及在运行时创建和操作对象、调用方法、访问和修改字段等。通过反射,程序可以在不知道具体类型信息的情况下,动态地创建和操控对象,大大增强了程序的灵活性和适应性。

如何获取对象?
在Java中,通过反射获取对象主要涉及以下步骤:

  1. 获取Class对象:

    Class<?> clazz = Class.forName("全限定类名");
    

    或者,如果已经有了类的实例,可以直接调用其getClass()方法:

    YourClass obj = new YourClass();
    Class<?> clazz = obj.getClass();
    
  2. 通过Class对象创建对象:

    Constructor<?> constructor = clazz.getDeclaredConstructor(参数类型...);
    Object instance = constructor.newInstance(参数值...);
    

如何通过反射破坏单例?
单例模式通常保证在全局范围内只有一个实例,通过构造函数私有化来防止外部直接创建新的实例。然而,通过Java反射API,我们可以绕过这些限制,创建多个实例,从而破坏单例模式。

例如,对于经典的懒汉式单例类,其构造函数通常被设为私有,但通过反射可以强行调用构造函数创建新实例:

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;
    }
}

// 使用反射破坏单例
public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance(); // 单例对象
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true); // 解除构造方法的访问限制
        Singleton instance2 = constructor.newInstance(); // 通过反射创建第二个实例
        System.out.println(instance1 == instance2); // 输出false,证明创建了两个不同的实例
    }
}

这段代码中,通过反射获取了Singleton类的私有构造函数,并通过setAccessible(true)方法解除访问限制,然后调用newInstance()方法创建了第二个实例,这破坏了单例模式的原则。在实际开发中,为了防止此类情况,可以在单例类的构造函数中加入额外的检查逻辑,当检测到通过反射创建实例时抛出异常。

9、双重检测锁?是哪双重?为什么要双重?volatile的意义呢?

双重检测锁(Double-Checked Locking,DCL)是一种在多线程环境下,用来实现高效且线程安全的单例模式的技巧。双重检测体现在两个层次的检查上:

  1. 第一重检查:在方法外部,首先检查单例实例是否已经被创建(通常使用一个静态变量来存储实例)。
  2. 第二重检查:在创建实例的同步块内部,再次检查实例是否已经被创建。只有在实例未被创建的情况下,才会真正执行实例的创建过程。

之所以需要双重检测,是因为在多线程环境下,如果不加控制,多个线程可能会同时检测到单例还未创建,从而并发地执行实例创建过程,违反了单例模式的初衷。双重检测锁的目的就是在保证线程安全的同时,尽可能减少使用同步的次数,从而提高性能。

volatile关键字的意义
在Java中,volatile关键字用于修饰共享变量,确保多线程环境下该变量的可见性和有序性。

  • 可见性:一个线程修改了volatile变量的值,其他线程可以立即得知这个修改。在双重检测锁的场景中,当线程A成功创建了单例实例后,其他线程通过volatile修饰的变量可以立即看到这个实例的存在,从而避免了创建多个实例的问题。

  • 有序性:禁止指令重排序,确保了初始化单例实例的操作不会被编译器或者CPU优化而导致其他线程看到“半初始化”的对象。

双重检测锁的代码示例(Java):

public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在这个示例中,volatile关键字用于确保当instance被初始化为Singleton实例时,多个线程都能够看到最新值,避免了内存可见性问题。然而,仅仅依靠volatile还不足以保证双重检测锁的正确性,在Java 5及其以后版本中,由于引入了JSR-133内存模型的改进,volatile加上双重检测锁可以正确实现单例模式。但在早期版本的Java中,单纯使用volatile可能无法完全解决并发问题,需要配合其他措施,比如final关键字或者使用AtomicReference等工具类。

用if判断第一重的原因

在双重检测锁(Double-Checked Locking, DCL)模式中,第一重if判断的作用是为了提高并发环境下的性能和效率。这是因为,对于一个高并发访问的单例类而言,理想情况下希望在多线程环境下只初始化一次单例实例,而在之后的访问中直接返回已创建的实例,而不必每次都需要同步。

第一重if判断位于对外公开的getInstance()方法的非同步代码块中,它的目的是先做一个初步的、低成本的检查,看看是否已经有人创建了单例实例。如果实例已经存在,那么直接返回这个实例,避免了不必要的同步操作,降低了锁的使用频率,提高了程序的并发性能。

在并发环境下,如果有多个线程同时调用getInstance()方法,如果没有第一重if判断,每个线程都将进入同步代码块,这会造成不必要的同步开销。有了第一重if判断,只有在实例尚未创建的情况下,线程才会进入同步代码块,进一步检查并创建实例。

因此,双重检测锁的第一重if判断是实现既保证线程安全又兼顾性能的关键所在。

10、死锁产生的条件?

死锁(Deadlock)是指在多线程环境下,两个或多个线程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力干涉,这些线程都无法向前推进,形成一种永久阻塞的状态。死锁产生的必要条件通常有以下四点:

  1. 互斥条件(Mutual Exclusion)
    一个资源在任意时刻只能被一个线程占用。如果有其他线程请求该资源,请求线程必须等待,直到资源被释放。

  2. 请求保持(Hold and Wait)
    已经持有至少一个资源的线程还在等待获取其他的资源,而它又不释放已持有的资源。

  3. 不可剥夺条件(No Preemption)
    已经被线程占用的资源不能被其他线程强行剥夺,必须由占有资源的线程自行释放。

  4. 循环等待条件(Circular Wait)
    存在一个线程等待序列,线程A等待线程B释放资源,线程B等待线程C释放资源,……线程Z等待线程A释放资源,形成一个闭环等待链。也就是说,系统中存在一个线程集合 {A, B, …, Z},其中每个线程都在等待下一个线程所持有的资源。

以上四个条件必须同时满足时,系统中才会出现死锁。为了预防和解决死锁问题,通常需要破坏上述条件中的至少一个。

11、索引原理;为什么采用B+树?为什么不用哈希索引,哈希索引比B+树快么索引原理:

索引是一种数据库管理系统中用于提高查询效率的数据结构。在关系型数据库中,索引的工作原理类似于书籍的索引页,它为数据库表中的某一列或多列建立一种快速访问路径,以便在执行查询时更快地定位到需要的数据行。索引通常以某种数据结构存储,这个结构关联着数据表的主键和指向数据行的指针。

为什么采用B+树作为索引结构
B+树是一种平衡的、自平衡的树形数据结构,特别适合于磁盘等外部存储设备的索引结构。它有以下特点使它在数据库索引中广受欢迎:

  1. 高度平衡:B+树的高度较低,有利于减少磁盘I/O次数。在数据库查询中,磁盘I/O往往是性能瓶颈,通过保持树的高度较小,可以在O(log n)的时间复杂度内定位到数据。

  2. 范围查询友好:B+树的所有叶子节点形成了一个有序链表,可以进行高效的范围查询和顺序扫描。对于SQL中的范围查询和排序操作,B+树的这种特性尤为实用。

  3. 全节点存储数据:在B+树中,所有的叶子节点存储了实际的数据记录,而非叶节点仅存储索引,这样可以减少数据在内存和磁盘之间的传输。

  4. 缓存友好:B+树的节点大小通常设计得适合磁盘块的大小,使得一次I/O可以加载更多索引,提高索引的命中率和查询效率。

相比之下,哈希索引虽然在查找单个键值时具有较快的速度(O(1)时间复杂度),但它有以下局限性:

  1. 范围查询和排序困难:哈希索引基于哈希函数,键值映射到固定的位置,不适合连续的范围查询和排序操作,因为哈希表中键的物理顺序与键值的大小无关。

  2. 哈希冲突:虽然哈希索引可以通过链地址法、开放寻址法等方式处理哈希冲突,但过多的冲突会导致性能下降,尤其是在并发环境下。

  3. 不支持部分索引键匹配:哈希索引只能根据完整的索引键进行查找,不支持前缀匹配等查询。

综上所述,虽然哈希索引在某些场景下查询速度很快,但由于其无法很好地支持范围查询、排序操作以及不适合关系型数据库普遍存在的复杂查询需求,故在主流的关系型数据库管理系统中,B+树被广泛用于实现二级索引。而在一些特定场合,如MySQL的Memory存储引擎,以及其他一些需要高速查找单一键值的场景下,哈希索引也可以作为一种辅助索引策略。

12、分布式-生成数据库全局唯一ID的方案

在分布式系统中生成全局唯一ID是一个常见的问题,下面列举几种常用的生成全局唯一ID的方案:

  1. UUID (Universally Unique Identifier):

    • UUID由一组32位的十六进制数构成,可以保证在全球范围内的唯一性。它可以使用多种算法生成,其中Version 1基于时间戳和MAC地址,Version 4基于随机数生成,确保了全局唯一性。然而,UUID较长且无序,可能对存储和索引效率有一定影响。
  2. Twitter的Snowflake算法:

    • Snowflake算法结合了时间戳、工作机器ID和序列号来生成64位的全局唯一ID。它的结构是0 - 0000000000 0000000000 0000000000 0000000000 - 00000 - 000000000000 - 000000000000,每个部分代表不同的含义:
      • 最高位为未使用标志位
      • 接下来的41位是毫秒级时间戳(精确到毫秒)
      • 然后是10位的机器标识符,可以部署在1024个节点
      • 再之后的12位是序列号,每个节点每毫秒可以生成4096个ID
    • 这种方案生成的ID有序且可反解,非常适合大数据量下的分布式环境。
  3. MongoDB的ObjectId:

    • MongoDB的ObjectId也是基于时间戳、机器标识、进程标识符及计数器生成的12字节唯一ID。其设计类似Snowflake算法,但是长度较短且包含的信息略有不同。
  4. Zookeeper或者Etcd的自增ID:

    • 利用分布式协调服务,比如Zookeeper或Etcd,在分布式环境中维护一个全局自增序列。当需要生成新的ID时,向协调服务请求一个新的ID,并保证原子性和全局唯一性。
  5. 雪花算法变种:

    • 很多公司会根据Snowflake算法进行定制,例如增加数据中心ID字段,适应更大规模的分布式环境。
  6. 数据库自增序列(分布式):

    • 使用数据库提供的序列功能,并通过分布式事务或者分布式锁机制保证全局唯一性。例如MySQL的InnoDB表可以在多个节点共享同一个序列,但这种方式在高并发场景下可能会遇到性能瓶颈。
  7. Redis生成:

    • 利用Redis的原子操作incr或incrby来实现全局自增ID,但同样需要注意集群环境下如何保证全局唯一性,可能需要结合key的命名规则或lua脚本实现。

选择哪种方案取决于具体的应用场景、性能要求、持久化需求以及系统的扩展性等因素。

13、Redis数据类型及其使用场景

Redis 数据类型及其使用场景如下:

  1. String(字符串)

    • 存储简单的键值对,如用户信息、文章内容、Session数据等。
    • 实现分布式锁,因为Redis的SETNX(设置并判断是否已存在)命令可以原子地设置一个只有不存在时才能设置成功的字符串。
    • 作为计数器,使用INCR、DECR命令对数值进行原子递增或递减操作,适用于统计PV(页面浏览量)、UV(独立访客数)等实时统计数据。
    • 作为缓存层,存储热点数据,提高数据访问速度。
  2. Hash(哈希)

    • 存储对象,适合存储结构化的数据,如用户资料、产品详情等,通过field-value的方式组织数据,便于对单个属性进行增删改查。
    • 用于实现购物车、用户状态管理等场景,可以方便地修改单个字段而不必更新整个数据结构。
  3. List(列表)

    • 有序且可重复的数据集合,支持左推(LPUSH)、右推(RPUSH)、弹出(LPOP、RPOP)等操作。
    • 应用场景包括消息队列(先进先出FIFO特性)、文章最新发表列表、微博时间线等。
  4. Set(集合)

    • 无序且不重复的数据集合,支持添加(SADD)、移除(SREM)、成员是否存在(SISMEMBER)、并集、交集、差集运算等。
    • 常用于标签系统、共同好友计算、唯一ID生成(如用户注册时生成唯一邀请码)等场景。
  5. Sorted Set(有序集合)

    • 类似Set,但每个成员都有一个分数与其关联,集合中的元素按照此分数进行排序。
    • 适用于排行榜、带权重的消息、地理位置索引等,支持ZRANK、ZREVRANK、ZRANGE、ZREVRANGE等操作,可以根据分数进行范围查询。

综上所述,Redis的各种数据类型为开发者提供了强大的灵活性,使得它不仅可以作为缓存系统,还能够承担起复杂的数据结构存储和实时计算任务。

14、SpringMVC流程

Spring MVC(Model-View-Controller)的工作流程可以概括为以下步骤:

  1. 用户请求到达

    • 用户通过浏览器向服务器发送HTTP请求,请求首先到达服务器的Web容器(如Tomcat)。
  2. DispatcherServlet接收请求

    • Web容器将请求分发给Spring MVC的核心控制器——DispatcherServlet。
    • DispatcherServlet根据web.xml中配置的URL映射规则,匹配到相应的处理器映射器(HandlerMapping)。
  3. 处理器映射器寻找处理器

    • HandlerMapping根据请求的URL和配置的映射规则查找合适的处理器(Handler,通常是Controller类的方法)。
  4. 调用处理器适配器执行处理器

    • 找到处理器后,DispatcherServlet将请求转交给处理器适配器(HandlerAdapter)。
    • HandlerAdapter会调用实际的处理器(Controller)来处理请求,并执行相应的业务逻辑。
  5. Controller处理请求

    • Controller执行业务逻辑,可能访问数据库、调用Service层方法等,处理完成后,它会返回一个ModelAndView对象或者直接返回数据(在注解驱动中,可以使用ResponseBody、RestController等)。
  6. 视图解析器处理视图

    • ModelAndView对象中包含了模型数据(Model)和视图名称(View Name)。
    • 视图解析器(ViewResolver)根据视图名称解析出实际的视图(如JSP、Thymeleaf模板、Velocity模板等)。
  7. 渲染视图

    • 视图负责渲染模型数据到HTTP响应中,生成HTML等内容。
    • 渲染完毕后,视图将渲染后的结果返回给DispatcherServlet。
  8. 响应客户端

    • DispatcherServlet将渲染好的响应内容封装成HTTP响应,通过Web容器发送回客户端(浏览器)。

以上就是Spring MVC的基本工作流程,它体现了MVC模式中请求的分发、处理和响应的全过程。在实际开发中,Spring MVC提供了丰富的扩展点,可以根据需要进行定制和扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值