当前位置: 首页 > article >正文

0101java面经

1.Java 中有哪些垃圾回收算法?

  1. 标记 - 清除算法(Mark - Sweep)
    • 基本原理:标记 - 清除算法是最基础的垃圾回收算法之一。它分为两个阶段,首先是标记阶段,从根对象(如栈帧中的局部变量、静态变量等引用的对象)开始,通过遍历对象图,标记所有可达的对象。然后是清除阶段,遍历整个堆内存,将未被标记的对象回收,即将这些对象占用的内存空间释放掉,以便后续重新分配给新的对象使用。
    • 优点与缺点:优点是实现简单,不需要移动对象,对于存活对象较多的情况比较高效。缺点也很明显,一是标记和清除过程效率相对较低,特别是在对象数量较多的情况下;二是清除后会产生内存碎片,因为回收的对象可能分布在内存的各个位置,导致后续分配大对象时可能找不到足够连续的内存空间。
  2. 复制算法(Copying)
    • 工作过程:复制算法将内存空间划分为两个大小相等的区域,通常称为 From 区和 To 区。在垃圾回收时,首先将 From 区中存活的对象复制到 To 区,然后清空 From 区。下一次垃圾回收时,两个区域的角色互换,即原来的 To 区变为 From 区,原来的 From 区变为 To 区。这样,每次回收后,存活的对象都被集中在一个区域,而另一个区域则可以被完全清空用于后续分配新对象。
    • 适用场景与局限性:这种算法的优点是实现简单,并且不会产生内存碎片,因为每次回收后存活对象都被紧凑地放置在一个区域。它适用于新生代垃圾回收,因为新生代中大部分对象都是 “朝生暮死” 的,即存活时间较短,复制算法可以高效地处理这种情况。然而,它的局限性在于它需要占用双倍的内存空间来实现这种复制操作,因为有一半的内存空间在任何时候都是闲置的,这在内存资源紧张的情况下可能是一个问题。
  3. 标记 - 整理算法(Mark - Compact)
    • 算法步骤:标记 - 整理算法结合了标记 - 清除算法和复制算法的一些特点。首先是标记阶段,与标记 - 清除算法一样,从根对象开始标记所有可达的对象。然后是整理阶段,将所有存活的对象向一端移动,使得存活对象在内存中是连续的,最后将边界以外的内存空间全部释放,这样就不会产生内存碎片,而且也不需要像复制算法那样占用双倍的内存空间。
    • 优势与应用场景:标记 - 整理算法的主要优势是它既解决了标记 - 清除算法产生内存碎片的问题,又避免了复制算法需要双倍内存空间的缺点。它适用于老年代垃圾回收,因为老年代中的对象存活时间较长,而且通常对内存的连续性要求较高,标记 - 整理算法可以很好地满足这种需求。
  4. 分代收集算法(Generational Collection)
    • 基于对象生命周期的策略:分代收集算法是一种综合考虑了对象生命周期特点的垃圾回收策略。它根据对象的存活时间将堆内存划分为不同的代,通常包括新生代和老年代。新生代又可细分为 Eden 区和 Survivor 区(一般有两个 Survivor 区)。新创建的对象首先被分配到 Eden 区,当 Eden 区满时,触发一次 Minor GC(新生代垃圾回收),采用复制算法将存活的对象复制到 Survivor 区,经过多次 Minor GC 后仍然存活的对象会被晋升到老年代。老年代的垃圾回收(Major GC 或 Full GC)频率相对较低,一般采用标记 - 整理算法或者标记 - 清除算法。
    • 优势与实践中的应用效果:分代收集算法的优势在于它能够根据不同代中对象的特点,采用最适合的垃圾回收算法,从而提高垃圾回收的效率。在实际应用中,大量的研究和实践表明,大部分对象的生命周期较短,在新生代就可以被回收,只有少数对象会进入老年代。通过这种分代收集的方式,可以大大减少垃圾回收的总时间,提高应用程序的性能。

2,常见的数据结构有哪些?

    • 定义与存储方式:数组是一种线性数据结构,它由一组连续的内存单元组成,用于存储相同类型的数据元素。例如,在 Java 中可以这样定义一个整数数组:int[] array = new int[5];,这个数组可以存储 5 个整数。数组的元素在内存中是顺序存储的,通过索引(从 0 开始)可以快速访问数组中的任意元素。例如,array[0]表示数组中的第一个元素。
    • 操作特点与应用场景:数组的优点是访问元素速度快,时间复杂度为,因为可以通过计算偏移量直接定位元素。但它的缺点是插入和删除元素效率较低,在中间插入或删除一个元素时,需要移动后面的元素,时间复杂度为。数组常用于存储数据量固定且需要快速访问元素的场景,如存储学生成绩、图像的像素数据等。
  1. 链表(Linked List)

    • 链表的构成与存储原理:链表是一种动态的数据结构,它由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针(单链表)。例如,一个简单的单链表节点结构可以用 Java 表示为:
    class ListNode {
        int data;
        ListNode next;
        ListNode(int data) {
            this.data = data;
            this.next = null;
        }
    }
    

    链表的节点在内存中不是连续存储的,而是通过指针链接在一起。

    • 不同类型链表(单链表、双链表、循环链表)及特点
      • 单链表:每个节点只有一个指向下一节点的指针,只能从表头开始单向遍历。它的插入和删除操作相对简单,只需要修改节点之间的指针即可,时间复杂度为(在已知插入或删除位置的前驱节点时),但查找一个节点需要从头开始遍历,时间复杂度为。
      • 双链表:每个节点有两个指针,一个指向前一个节点,一个指向后一个节点。双链表可以双向遍历,在某些操作上更加灵活,如删除一个节点时,可以直接通过前驱和后继节点的指针进行操作,不需要像单链表那样先找到前驱节点。
      • 循环链表:可以是单循环链表或双循环链表,它的最后一个节点的指针指向头节点(单循环链表)或头节点的前驱节点(双循环链表),形成一个环形结构。循环链表适用于需要循环处理数据的场景,如操作系统中的进程调度队列。
    • 应用场景举例:链表适用于需要频繁插入和删除元素的场景,如实现栈、队列(特别是链式队列),或者在一些动态数据管理的场景中,如文本编辑器中存储文本行的结构。
  2. 栈(Stack)

    • 栈的操作规则与特点:栈是一种特殊的线性数据结构,它遵循后进先出(LIFO - Last In First Out)的原则。就像一个弹夹,最后压入的子弹最先被射出。栈主要有两个操作:入栈(push)和出栈(pop)。入栈操作将元素添加到栈顶,出栈操作则将栈顶元素移除。栈可以通过数组或链表来实现。
    • 应用场景介绍:栈在很多算法和程序中有广泛的应用。例如,在函数调用过程中,系统会使用栈来保存函数的调用信息(包括局部变量、返回地址等),这就是程序运行时的栈帧。在表达式求值(如后缀表达式求值)、语法检查(如括号匹配检查)等场景中,栈也发挥着重要的作用。
  3. 队列(Queue)

    • 队列的操作顺序与性质:队列是另一种线性数据结构,它遵循先进先出(FIFO - First In First Out)的原则。想象一个排队买票的场景,先排队的人先买到票离开。队列主要有两个操作:入队(enqueue)和出队(dequeue)。入队操作将元素添加到队尾,出队操作将队头元素移除。

    • 不同类型队列(普通队列、循环队列、优先级队列)及用途

      • 普通队列:可以通过数组或链表来实现,如用数组实现时,需要两个指针(头指针和尾指针)来管理队列的操作。它用于简单的排队场景,如任务调度(先来先服务的任务队列)。
      • 循环队列:是一种特殊的队列,它将队列的存储空间视为一个环形。当队尾指针到达数组末尾时,下一个元素可以从数组开头存放,这样可以更有效地利用存储空间。循环队列常用于生产者 - 消费者模型等场景,在操作系统、网络通信等领域有广泛应用。
      • 优先级队列:在优先级队列中,元素按照优先级进行排序。入队时,根据元素的优先级插入到合适的位置;出队时,总是先取出优先级最高的元素。优先级队列可以通过堆等数据结构来实现,常用于任务调度(根据任务的优先级进行调度)、操作系统中的进程调度(高优先级进程先执行)等场景。
  4. 树(Tree)

    • 树的基本概念与结构组成:树是一种非线性的数据结构,它由节点(Node)和边(Edge)组成。树有一个根节点(Root),根节点没有父节点,其他节点都有一个父节点,并且可以有多个子节点。例如,一个简单的二叉树节点结构可以用 Java 表示为:
    class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int val) {
            this.val = val;
            this.left = null;
            this.right = null;
        }
    }
    
    • 二叉树、平衡二叉树、红黑树等常见类型及特点
      • 二叉树:每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树有多种遍历方式,如前序遍历(根节点 - 左子树 - 右子树)、中序遍历(左子树 - 根节点 - 右子树)和后序遍历(左子树 - 根节点 - 右子树)。二叉树在表达式树、哈夫曼树等场景中有应用。
      • 平衡二叉树(AVL 树):它是一种特殊的二叉树,左右子树的高度差绝对值不超过 1。平衡二叉树可以保证树的高度在对数级别,从而保证了插入、删除和查找操作的时间复杂度都为。它通过在插入和删除操作后进行旋转操作来保持平衡。
      • 红黑树:红黑树是一种自平衡二叉查找树,它在每个节点上增加了一个颜色属性(红色或黑色),并且通过一系列规则来保证树的平衡。红黑树的插入、删除和查找操作的时间复杂度也为,它在 Java 中的TreeMapTreeSet等集合类中有应用,用于高效地存储和查找数据。
    • 树在数据存储和算法中的应用举例:树在数据存储方面可以用于组织层次结构的数据,如文件系统的目录结构。在算法中,树结构常用于搜索算法(如深度优先搜索、广度优先搜索)、排序算法(如堆排序)和数据压缩(如哈夫曼编码)等场景。
  5. 图(Graph)

    • 图的定义与组成要素:图是一种更复杂的非线性数据结构,它由顶点(Vertex)和边(Edge)组成。边可以是有向的(有向图),表示从一个顶点指向另一个顶点的关系;也可以是无向的(无向图),表示两个顶点之间的双向关系。图可以用邻接矩阵或邻接表来表示。例如,一个简单的无向图的邻接矩阵表示可以用 Java 二维数组实现:
    class Graph {
        int[][] adjacencyMatrix;
        int numVertices;
        Graph(int numVertices) {
            this.numVertices = numVertices;
            this.adjacencyMatrix = new int[numVertices][numVertices];
        }
    }
    
    • 有向图、无向图、加权图等类型及应用场景
      • 有向图:有向图中的边具有方向,例如在社交网络中,关注关系可以用有向图表示,A 关注 B 并不意味着 B 关注 A。有向图用于表示单向的关系和流程,如网络拓扑结构中的数据流向、任务依赖关系等。
      • 无向图:无向图中的边没有方向,例如城市之间的公路连接(假设公路是双向通车的)可以用无向图表示。无向图用于表示对称的关系,如朋友关系、电路中的元件连接等。
      • 加权图:在加权图中,边具有一个权重值,这个权重可以表示距离、成本、时间等。例如,在地图导航中,城市之间的道路长度可以用加权图表示,权重就是道路的长度。加权图用于需要考虑边的成本或距离等因素的场景,如最短路径问题(如 Dijkstra 算法、Floyd - Warshall 算法)、最小生成树问题(如 Prim 算法、Kruskal 算法)。

3,HashMap 的原理

  1. 数据结构基础
    • 哈希表(Hash Table)概念:HashMap 是基于哈希表实现的。哈希表是一种数据结构,它可以通过一个哈希函数(Hash Function)将键(Key)映射到一个特定的位置,这个位置称为桶(Bucket)。理想情况下,哈希函数能够将不同的键均匀地分布到各个桶中,这样在查找、插入和删除操作时,可以通过计算键的哈希值快速定位到对应的桶,从而提高操作效率。
    • 数组与链表(或红黑树)的组合结构:在 Java 的 HashMap 中,内部使用了一个数组来存储数据,这个数组的每个元素可以看作是一个桶。当有新的键值对要插入时,首先计算键的哈希值,然后通过哈希值确定在数组中的位置(即桶的位置)。如果多个键的哈希值计算后对应到同一个桶,就会在这个桶上形成一个链表(在 Java 8 之前,一直是这种结构;Java 8 后,如果链表长度达到一定阈值(默认为 8),链表会转换为红黑树,以提高查找效率)。
  2. 哈希函数与哈希冲突解决
    • 哈希函数的设计与作用:HashMap 中的哈希函数用于计算键的哈希值,从而确定键值对在数组中的存储位置。在 Java 中,hashCode()方法用于获取对象的哈希码,然后通过一系列位运算来得到最终的哈希值。例如,对于一个简单的自定义类作为键,需要重写hashCode()方法来提供一个合理的哈希码计算方式。一个良好的哈希函数应该尽量减少哈希冲突,并且计算速度要快。
    • 哈希冲突的处理方式(链表法 / 红黑树法)
      • 链表法:当两个不同的键计算出相同的哈希值时,就会发生哈希冲突。在这种情况下,Java 的 HashMap 会将新的键值对插入到对应桶的链表中。在查找元素时,先通过哈希值定位到桶,然后在链表中逐个比较键是否相等来找到目标元素。这种方式在哈希冲突较少的情况下效率较高,但如果链表过长,查找效率会下降,因为需要遍历链表中的每个元素。
      • 红黑树法(Java 8+):为了优化链表过长导致的查找效率问题,Java 8 引入了红黑树。当链表长度达到一定阈值(默认为 8)时,链表会转换为红黑树。红黑树是一种自平衡二叉查找树,它的查找、插入和删除操作的时间复杂度都是,相比链表的(在最坏情况下)有了很大的提升。当链表长度小于等于 6 时,红黑树又会转换回链表,以节省空间和维护成本。
  3. 重要属性与方法实现细节
    • 负载因子(Load Factor)与容量(Capacity)的概念及作用
      • 容量:是指 HashMap 中桶的数量,也就是内部数组的长度。初始容量默认为 16,并且总是 2 的幂次方。这个设计是为了方便在计算哈希值和定位桶时进行位运算,提高效率。例如,在计算哈希值确定桶的位置时,可以使用hash & (capacity - 1)这种位运算来代替取模运算,因为当容量是 2 的幂次方时,这两种运算结果相同,但位运算速度更快。
      • 负载因子:负载因子用于衡量 HashMap 的填满程度,它等于 HashMap 中元素的数量(键值对的数量)除以容量。默认负载因子是 0.75。当元素数量超过负载因子乘以容量时,HashMap 会进行扩容操作,以避免哈希冲突过于频繁。例如,如果当前容量是 16,负载因子是 0.75,当元素数量达到 12 时,HashMap 会自动扩容,新的容量通常是原来容量的 2 倍(即 32)。
    • put 方法的执行过程
      • 当调用put(key, value)方法时,首先会对键key调用hashCode()方法获取哈希码,然后经过一些位运算得到哈希值,通过哈希值确定在数组中的存储位置(桶的位置)。
      • 如果该位置为空,直接将键值对存储在这个位置。
      • 如果该位置已经有元素(发生哈希冲突),则会判断该位置的元素是链表节点还是红黑树节点。如果是链表节点,就遍历链表,比较键是否相等。如果找到相同的键,则更新对应的值;如果没有找到,则将新的键值对插入到链表的末尾。如果是红黑树节点,则在红黑树中进行插入操作,根据红黑树的规则来确定插入位置。
    • get 方法的操作逻辑
      • 调用get(key)方法时,同样先计算键key的哈希值,通过哈希值定位到桶的位置。
      • 如果该位置为空,则返回null,表示没有找到对应的键值对。
      • 如果该位置有元素,会判断是链表节点还是红黑树节点。如果是链表节点,就遍历链表,比较键是否相等,找到相等的键就返回对应的的值;如果是红黑树节点,则在红黑树中进行查找操作,根据红黑树的查找规则来找到对应的键值对。

4,哈希表的负载因子为什么是0.75?

  1. 空间利用率与哈希冲突的平衡
    • 空间利用率方面:负载因子是哈希表中元素个数与哈希表容量的比值。如果负载因子设置得过高,比如接近 1,那么哈希表会被填充得很满。虽然这样可以充分利用空间,但会导致哈希冲突的概率大大增加。因为随着元素增多,不同的键被映射到同一个桶的可能性就会变高。
    • 哈希冲突方面:哈希冲突是指不同的键通过哈希函数计算后得到相同的哈希值,从而被分配到同一个桶中。当哈希冲突过多时,在桶中查找、插入和删除元素的效率会降低。例如,在使用链表来处理哈希冲突的情况下,当链表过长,查找一个元素可能需要遍历很长的链表,时间复杂度会从理想的(没有冲突时直接定位桶)变为(为链表长度)。
    • 0.75 的平衡作用:将负载因子设置为 0.75 是一种折中的选择。它既保证了哈希表有一定的空间利用率,不会浪费太多空间,又能在一定程度上控制哈希冲突的概率。在这个负载因子下,哈希表大约有 75% 的空间被利用,还有 25% 的空间用于减少哈希冲突,使得哈希表在大多数情况下能够保持较好的性能。
  2. 性能优化与扩容机制的关联
    • 扩容触发条件:当哈希表中的元素个数超过负载因子乘以容量时,就会触发扩容操作。例如,对于初始容量为 16 的哈希表,当元素个数达到时,就会进行扩容。扩容通常是将容量扩大为原来的 2 倍,这样可以重新分配元素,减少哈希冲突。
    • 性能优化考虑:扩容操作是一个相对比较耗时的操作,因为需要重新计算所有元素的哈希值,并将它们重新分配到新的桶中。如果负载因子过高,扩容操作会过于频繁,这会消耗大量的时间和资源。而 0.75 的负载因子可以使得扩容操作不会过于频繁,同时也能及时调整哈希表的容量,以适应元素数量的增长,保证哈希表的性能不会因为哈希冲突过多而下降得太快。
  3. 实验与实践验证
    • 理论与实际的结合:在实际应用中,通过大量的实验和实践经验发现,0.75 这个负载因子在综合考虑空间利用率和哈希冲突方面表现良好。不同的应用场景可能对哈希表的性能有不同的要求,但在一般情况下,0.75 能够适应大多数场景。例如,在 Java 的 HashMap 实现中,采用 0.75 的负载因子可以在各种数据量和数据分布情况下,使得哈希表的查找、插入和删除操作能够保持相对稳定的性能。
    • 性能测试示例:可以通过编写性能测试代码来验证不同负载因子对哈希表性能的影响。比如,创建多个具有不同负载因子的哈希表,分别向它们中插入大量元素,并记录插入、查找操作的时间。会发现负载因子为 0.75 左右的哈希表在插入一定数量元素后,其性能下降的速度相对较慢,而负载因子过高(如 0.9 以上)的哈希表在元素数量增加后,性能下降得更快,主要体现在查找操作的时间变长。

5你使用过Java的反射机制吗?如何应用反射?

  1. 反射机制概述

    • 定义与概念:Java 反射机制是指在运行时动态地获取类的信息以及操作类或对象的功能。它允许程序在运行时加载、探知和使用编译期间完全未知的类。通过反射,可以获取类的各种信息,如类的属性、方法、构造函数等,并且能够创建对象、调用方法、访问和修改属性,就好像在代码中直接使用这些类一样,即使在编译时并不知道这些类的具体定义。
  2. 获取类对象的方法

    • 通过类的字面量(.class)获取:如果在编译时已经知道类的名称,可以使用类的字面量来获取类对象。例如,对于java.util.Date类,可以这样获取它的类对象:Class<Date> dateClass = Date.class;。这种方法简单直接,适用于已知类名的情况。
    • 通过对象的getClass()方法获取:每个 Java 对象都有一个getClass()方法,它返回该对象所属类的类对象。例如,假设有一个Date对象date,可以通过date.getClass()获取Date类的类对象。这种方法在已经有对象实例的情况下很有用。
    • 通过Class.forName()方法获取:这是一种更灵活的方法,它允许通过类的全限定名(包括包名)在运行时加载类并获取类对象。例如,要获取com.example.User类的类对象,可以使用Class.forName("com.example.User");。这种方法在不知道类的具体信息,只知道类的全限定名时非常有用,比如在插件式架构或者动态加载类的场景中。
  3. 反射的应用场景

    • 动态创建对象

      • 使用构造函数创建对象:通过反射获取类的构造函数,然后使用构造函数来创建对象。例如,对于一个有参数的构造函数的类User(假设User类有一个String类型的name参数),可以这样创建对象:
      Class<User> userClass = User.class;
      Constructor<User> constructor = userClass.getConstructor(String.class);
      User user = constructor.newInstance("John");
      

      首先通过getConstructor方法获取指定参数类型的构造函数,然后使用newInstance方法传入实际参数来创建对象。

    • 动态调用方法

      • 调用普通方法:获取类对象后,可以进一步获取类中的方法,然后调用这些方法。例如,对于User类中有一个sayHello()方法,可以这样调用:
      Class<User> userClass = User.class;
      User user = userClass.newInstance();
      Method method = userClass.getMethod("sayHello");
      method.invoke(user);
      

      先创建User对象,然后通过getMethod获取sayHello方法,最后使用invoke方法在user对象上调用该方法。

      • 调用私有方法:反射也可以用于调用私有方法。不过需要先通过setAccessible(true)来绕过访问权限检查。例如,假设User类中有一个私有方法privateMethod,可以这样调用:
      Class<User> userClass = User.class;
      User user = userClass.newInstance();
      Method privateMethod = userClass.getDeclaredMethod("privateMethod");
      privateMethod.setAccessible(true);
      privateMethod.invoke(user);
      
    • 访问和修改属性

      • 访问和修改公共属性:可以获取类的属性并访问或修改它们的值。例如,对于User类中有一个公共属性age,可以这样操作:
      Class<User> userClass = User.class;
      User user = userClass.newInstance();
      Field field = userClass.getField("age");
      field.set(user, 30);
      System.out.println(field.get(user));
      

      通过getField获取属性,然后使用set方法设置属性的值,使用get方法获取属性的值。

      • 访问和修改私有属性:与调用私有方法类似,访问和修改私有属性也需要先通过setAccessible(true)来绕过访问权限检查。例如,假设User类中有一个私有属性privateField,可以这样操作:
      Class<User> userClass = User.class;
      User user = userClass.newInstance();
      Field privateField = userClass.getDeclaredField("privateField");
      privateField.setAccessible(true);
      privateField.set(user, "private value");
      System.out.println(privateField.get(user));
      

6,SpringBoot如何实现控制反转?

  1. 控制反转(IoC)与依赖注入(DI)概念

    • 控制反转(IoC)定义:控制反转是一种设计思想,它将对象的创建和依赖关系的管理从程序代码中转移到外部容器(在 Spring Boot 中是 IoC 容器)。传统的程序设计中,对象的创建和对象之间的依赖关系是由程序员在代码中直接控制的。例如,在一个没有使用 IoC 的应用中,一个服务类可能会在自身内部直接实例化它所依赖的其他类。而在 IoC 的模式下,对象的创建和依赖关系的配置由容器来负责,程序代码只需要从容器中获取所需的对象并使用即可。
    • 依赖注入(DI)是 IoC 的一种实现方式:依赖注入是指当一个对象(被注入对象)需要依赖其他对象(依赖对象)时,由外部容器将依赖对象注入到被注入对象中。在 Spring Boot 中,主要有三种依赖注入方式:构造函数注入、Setter 方法注入和字段注入。通过这些方式,将对象之间的依赖关系在容器中进行配置,使得对象在运行时能够获取到它所需要的依赖对象。
  2. Spring Boot 的 IoC 容器(ApplicationContext)

    • 容器的创建与启动过程:在 Spring Boot 应用启动时,会创建一个ApplicationContext(应用上下文),它是 Spring Boot 的 IoC 容器。这个过程是通过SpringApplication.run()方法来启动的。在启动过程中,容器会扫描指定的包路径下的所有类,寻找带有特定注解(如@Component@Service@Repository@Controller等)的类,这些被注解标记的类会被视为 Spring 管理的组件,容器会为它们创建对象实例,并管理它们之间的关系。
    • 组件扫描机制:Spring Boot 默认会扫描启动类所在的包及其子包下的所有类。例如,如果启动类Application位于com.example包下,那么com.example及其子包下所有带有 Spring 注解的类都会被扫描到。可以通过@ComponentScan注解来指定扫描的包路径,这样可以扩大或缩小扫描范围。在扫描过程中,容器会根据类上的注解来判断组件的类型(如服务类、数据访问类、控制器类等),并为它们进行相应的配置。
  3. 依赖注入的实现方式在 Spring Boot 中的应用

    • 构造函数注入

      • 原理与优势:构造函数注入是通过在类的构造函数中声明依赖对象作为参数来实现的。当容器创建这个类的对象时,会自动将依赖对象注入到构造函数中。这种方式的优点是能够保证对象在创建时就拥有完整的依赖关系,对象的状态是完整的,并且可以很方便地将依赖关系进行显式声明,提高代码的可读性和可维护性。
      • 示例代码:假设我们有一个UserService类依赖于UserRepository类,使用构造函数注入可以这样写:
      @Service
      public class UserService {
          private final UserRepository userRepository;
          public UserService(UserRepository userRepository) {
              this.userRepository = userRepository;
          }
          // 其他业务方法
      }
      
    • Setter 方法注入

      • 适用场景与操作方式:Setter 方法注入是通过为依赖对象提供 Setter 方法来实现的。容器在创建对象后,会通过调用 Setter 方法将依赖对象注入进去。这种方式适用于依赖关系不是必需的情况,或者在对象创建后需要动态地改变依赖对象。例如,在一个配置类中,可能需要在某些条件下重新设置依赖对象。
      • 示例代码:对于上述的UserServiceUserRepository关系,使用 Setter 方法注入可以写成:
      @Service
      public class UserService {
          private UserRepository userRepository;
          public void setUserRepository(UserRepository userRepository) {
              this.userRepository = userRepository;
          }
          // 其他业务方法
      }
      
    • 字段注入(不推荐在生产环境使用)

      • 简单介绍与潜在问题:字段注入是直接在类的字段上使用@Autowired等注解来实现依赖注入。这种方式是最简洁的,例如:
      @Service
      public class UserService {
          @Autowired
          private UserRepository userRepository;
          // 其他业务方法
      }
      

      但是,这种方式存在一些潜在问题,如对象之间的依赖关系不够直观,不利于单元测试(因为无法通过构造函数或者 Setter 方法来方便地注入模拟对象),所以在生产环境中一般不推荐使用

7,Spring是如何管理bean的?

  1. Bean 的定义

    • 配置方式

      • 基于 XML 配置:在早期的 Spring 应用中,经常使用 XML 文件来定义 Bean。在 XML 文件中,可以通过<bean>标签来定义一个 Bean。例如,定义一个简单的UserService Bean 可以这样写:
      <bean id="userService" class="com.example.service.UserService">
          <!-- 可以在这里配置属性和构造函数参数 -->
      </bean>
      

      其中,id属性用于唯一标识这个 Bean,class属性指定了 Bean 的类型。可以通过property标签来配置 Bean 的属性,通过constructor - arg标签来配置构造函数参数。

      • 基于注解配置(推荐):在现代 Spring 应用中,更常用的是基于注解的方式来定义 Bean。例如,通过@Component@Service@Repository、@Controller` 等注解可以将一个类标记为 Spring 管理的 Bean。这些注解的功能基本相同,只是在语义上有所区别,用于区分不同层次的组件(如服务层、数据访问层、控制层等)。例如:
      @Service
      public class UserService {
          // 业务逻辑代码
      }
      

      这个UserService类被标记为一个服务层的 Bean,Spring 在扫描类路径时会发现这个注解,并将这个类作为一个 Bean 进行管理。

    • Bean 的作用域(Scope)定义

      :Spring 中的 Bean 有多种作用域,如

      singleton
      

      (单例)、

      prototype
      

      (原型)、

      request
      

      (请求)、

      session
      

      (会话)等。

      • 单例作用域(Singleton):这是默认的作用域。在这种作用域下,整个应用程序中只有一个 Bean 实例。例如,对于一个DataSource Bean(用于数据库连接),通常将其设置为单例,因为数据库连接池一般只需要一个实例来管理连接。可以在 XML 配置中通过scope="singleton"或者在类上使用@Scope("singleton")注解来设置。
      • 原型作用域(Prototype):每次从容器中获取这个 Bean 时,都会创建一个新的实例。例如,对于一个UserForm Bean(用于接收用户输入的表单数据),可能希望每次使用时都是一个新的对象,就可以将其设置为原型作用域。在 XML 配置中使用scope="prototype"或者在类上使用@Scope("prototype")注解来设置。
  2. Bean 的创建与初始化

    • 创建时机

      • 懒加载(Lazy - Initialization):默认情况下,单例 Bean 在容器启动时就会被创建。但可以通过设置懒加载来改变这种方式。在 XML 配置中,可以使用lazy - init="true"属性来实现懒加载,对于基于注解的配置,可以使用@Lazy注解。懒加载的 Bean 只有在第一次被使用时才会被创建,这样可以减少容器启动时间和资源占用。
      • 非懒加载情况:对于非懒加载的单例 Bean,在容器启动过程中,Spring 会扫描配置文件或者带有注解的类,当发现一个 Bean 定义后,就会根据定义创建 Bean 实例。例如,当发现一个@Service注解标记的类后,会使用默认的无参构造函数(如果有)来创建这个类的实例。
    • 初始化方法调用

      • 通过init - method属性(XML 配置):可以在 XML 配置的<bean>标签中指定init - method属性,这个属性的值是一个方法名。在 Bean 创建后,会自动调用这个方法来进行初始化操作。例如,对于一个UserService Bean,可能有一个init方法用于加载一些缓存数据,在 XML 中可以这样配置:
      <bean id="userService" class="com.example.service.UserService" init - method="init">
          <!-- 其他配置 -->
      </bean>
      
      • 通过@PostConstruct注解(注解配置):在基于注解的配置中,更常用的是@PostConstruct注解。在 Bean 的实例化和属性注入完成后,被@PostConstruct注解标记的方法会被自动调用。例如:
      @Service
      public class UserService {
          @PostConstruct
          public void init() {
              // 加载缓存数据等初始化操作
          }
          // 其他业务逻辑
      }
      
  3. Bean 的依赖注入(Dependency Injection)

    • 构造函数注入(Constructor Injection)

      • 配置方式与示例:在 XML 配置中,可以通过constructor - arg标签来为构造函数参数注入依赖。例如,假设有一个UserService类依赖于UserRepository,并且UserService的构造函数接收UserRepository作为参数,在 XML 中可以这样配置:
      <bean id="userRepository" class="com.example.repository.UserRepository"/>
      <bean id="userService" class="com.example.service.UserService">
          <constructor - arg ref="userRepository"/>
      </bean>
      

      在基于注解的配置中,通过在构造函数参数上添加@Autowired注解来实现自动注入。例如:

      @Service
      public class UserService {
          private final UserRepository userRepository;
          @Autowired
          public UserService(UserRepository userRepository) {
              this.userRepository = userRepository;
          }
          // 业务逻辑代码
      }
      
    • Setter 方法注入(Setter Injection)

      • 配置过程说明:在 XML 配置中,使用property标签来为 Bean 的属性进行 Setter 方法注入。例如,对于一个UserService Bean 有一个userRepository属性,在 XML 中可以这样注入:
      <bean id="userRepository" class="com.example.repository.UserRepository"/>
      <bean id="userService" class="com.example.service.UserService">
          <property name="userRepository" ref="userRepository"/>
      </bean>
      

      在基于注解的配置中,在属性上添加@Autowired注解来实现 Setter 方法注入。例如:

      @Service
      public class UserService {
          @Autowired
          private UserRepository userRepository;
          // 业务逻辑代码
      }
      
    • 字段注入(不推荐在生产环境使用)

      • 简单介绍与缺点:直接在 Bean 的字段上添加@Autowired注解来实现注入。例如:
      @Service
      public class UserService {
          @Autowired
          private UserRepository userRepository;
          // 业务逻辑代码
          // 这种方式虽然简单,但不利于单元测试和代码的可维护性
      }
      

      这种方式的缺点是使得依赖关系不那么直观,并且在单元测试时,很难对这个 Bean 进行隔离测试,因为无法方便地替换依赖对象。

  4. Bean 的生命周期管理

    • 销毁方法调用

      • 通过destroy - method属性(XML 配置):在 XML 配置的<bean>标签中,可以指定destroy - method属性来定义 Bean 销毁时要调用的方法。这个属性通常用于释放资源,如关闭数据库连接、释放文件句柄等。例如,对于一个管理数据库连接的 Bean,可能有一个close方法用于关闭连接,在 XML 中可以这样配置:
      <bean id="dataSource" class="com.example.datasource.DataSource" destroy - method="close">
          <!-- 其他配置 -->
      }
      
      • 通过@PreDestroy注解(注解配置):在基于注解的配置中,使用@PreDestroy注解来标记在 Bean 销毁时要调用的方法。例如:
      @Service
      public class DataSourceBean {
          @PreDestroy
          public void close() {
              // 关闭数据库连接等操作
          }
          // 其他业务逻辑
      }
      
    • 整个生命周期流程总结:一个 Bean 的生命周期包括创建(根据配置方式,通过构造函数或者工厂方法等)、属性注入(依赖注入)、初始化(调用init - method或者@PostConstruct标记的方法)、使用(在应用程序中被调用执行业务逻辑)和销毁(调用destroy - method或者@PreDestroy标记的方法)。Spring 通过对这些过程的精细管理,使得 Bean 能够在合适的时机完成相应的操作,并且能够有效地管理 Bean 之间的依赖关系和资源。

8,解释HashMap的底层原理是什么?

  1. 数据结构基础
    • 哈希表结构:HashMap 的底层是基于哈希表实现的。哈希表是一种能够通过特定的哈希函数将键(key)映射到存储位置(桶,bucket)的数据结构。其目的是在理想情况下,能够快速地根据键来定位、插入和删除值,时间复杂度接近 。
    • 数组与链表 / 红黑树的组合:在 Java 的 HashMap 中,它内部维护了一个数组,这个数组的每个元素可以看作是一个桶(bucket)。当有新的键值对(key - value)要插入时,首先会计算键的哈希值,通过哈希值确定其在数组中的位置。如果不同的键计算出的哈希值相同(哈希冲突),那么在这个位置上会形成一个链表(在 Java 8 之前一直是这种结构)。从 Java 8 开始,如果链表的长度达到一定阈值(默认为 8),链表会转换为红黑树,以提高查找效率。
  2. 哈希函数与哈希冲突解决
    • 哈希函数的设计与作用:HashMap 中的哈希函数用于计算键的哈希值,以确定键值对在数组中的存储位置。在 Java 中,对象的hashCode()方法用于获取一个初始的哈希码,HashMap 会进一步对这个哈希码进行处理,通常是通过位运算来得到最终的哈希值。例如,计算索引位置的公式可能是(table.length - 1) & hash,这里的table.length是数组的长度,hash是经过处理后的哈希值。这个公式能够快速地计算出键值对应在数组中的位置,并且在数组长度为 2 的幂次方时,等价于取模运算,但效率更高。
    • 哈希冲突的处理(链表法 / 红黑树法)
      • 链表法:当两个不同的键计算出相同的哈希值时,就会发生哈希冲突。在这种情况下,新的键值对会被插入到对应桶的链表中。在查找元素时,先通过哈希值定位到桶,然后在链表中逐个比较键是否相等来找到目标元素。这种方式在哈希冲突较少的情况下效率较高,但如果链表过长,查找效率会下降,因为需要遍历链表中的每个元素,最坏情况下时间复杂度为 ,其中 是链表的长度。
      • 红黑树法(Java 8+):为了优化链表过长导致的查找效率问题,Java 8 引入了红黑树。当链表长度达到一定阈值(默认为 8)时,链表会转换为红黑树。红黑树是一种自平衡二叉查找树,它的查找、插入和删除操作的时间复杂度都是 ,相比链表的最坏情况有了很大的提升。当链表长度小于等于 6 时,红黑树又会转换回链表,以节省空间和维护成本。
  3. 重要属性与方法实现细节
    • 负载因子(Load Factor)与容量(Capacity)的概念及作用
      • 容量:是指 HashMap 中桶的数量,也就是内部数组的长度。初始容量默认为 16,并且为了方便计算哈希值和定位桶,容量总是 2 的幂次方。这样在计算哈希值确定桶的位置时,可以使用位运算(table.length - 1) & hash来代替取模运算,提高计算效率。
      • 负载因子:负载因子用于衡量 HashMap 的填满程度,它等于 HashMap 中元素的数量(键值对的数量)除以容量。默认负载因子是 0.75。当元素数量超过负载因子乘以容量时,HashMap 会进行扩容操作。例如,初始容量为 16,负载因子为 0.75,当元素数量达到 12 时,就会触发扩容。扩容通常是将容量扩大为原来的 2 倍,这样可以重新分配元素,减少哈希冲突。
    • put 方法的执行过程
      • 当调用put(key, value)方法时,首先会对键key调用hashCode()方法获取哈希码,然后经过一些位运算得到哈希值,通过哈希值确定在数组中的存储位置(桶的位置)。
      • 如果该位置为空,直接将键值对存储在这个位置。
      • 如果该位置已经有元素(发生哈希冲突),则会判断该位置的元素是链表节点还是红黑树节点。如果是链表节点,就遍历链表,比较键是否相等。如果找到相同的键,则更新对应的值;如果没有找到,则将新的键值对插入到链表的末尾。如果是红黑树节点,则在红黑树中进行插入操作,根据红黑树的规则来确定插入位置。
    • get 方法的操作逻辑
      • 调用get(key)方法时,同样先计算键key的哈希值,通过哈希值定位到桶的位置。
      • 如果该位置为空,则返回null,表示没有找到对应的键值对。
      • 如果该位置有元素,会判断是链表节点还是红黑树节点。如果是链表节点,就遍历链表,比较键是否相等,找到相等的键就返回对应的的值;如果是红黑树节点,则在红黑树中进行查找操作,根据红黑树的查找规则来找到对应的键值对。

9,线程池有哪些核心参数?

  1. 核心线程数(corePoolSize)

    • 定义与作用:核心线程数是线程池中始终保持存活的线程数量。这些线程即使在空闲状态下也不会被销毁,它们会一直等待任务到来。例如,在一个处理网络请求的线程池中,设置一个合适的核心线程数可以确保有足够的线程来及时处理频繁的请求,避免因为线程创建和销毁的开销而影响性能。
    • 应用场景示例:对于一个简单的 Web 服务器,假设平均每秒会有 10 个请求,每个请求的处理时间较短。如果设置核心线程数为 5,那么这 5 个线程可以同时处理 5 个请求,其他请求可能需要等待线程空闲后才能处理。如果设置核心线程数过小,可能会导致请求排队等待时间过长;如果设置过大,可能会浪费系统资源。
  2. 最大线程数(maximumPoolSize)

    • 含义与使用场景:最大线程数是线程池允许创建的最大线程数量。当任务队列已满,并且当前线程数小于最大线程数时,线程池会创建新的线程来处理任务。例如,在处理突发的高负载任务时,最大线程数可以提供额外的处理能力。但需要注意的是,创建过多的线程可能会导致系统资源耗尽,如 CPU 过度切换、内存不足等问题。
    • 与核心线程数的关系:最大线程数应该大于等于核心线程数。在正常情况下,线程池中的线程数量会在核心线程数和最大线程数之间动态变化。当任务负载较低时,线程池中的线程数量可能会保持在核心线程数;当任务负载增加,任务队列已满时,线程池会根据需要创建新的线程,直到达到最大线程数。
  3. 线程存活时间(keepAliveTime)

    • 时间单位与作用:线程存活时间是指当线程池中的线程数量超过核心线程数时,多余的线程在空闲状态下能够存活的时间。这个参数需要结合时间单位(TimeUnit)一起使用,如秒、毫秒等。例如,设置存活时间为 60 秒,表示当线程池中的线程数量超过核心线程数并且这些多余的线程空闲了 60 秒后,它们将会被销毁,以释放系统资源。
    • 动态调整线程数量的意义:通过设置合理的存活时间,可以使线程池根据任务负载的变化动态地调整线程数量。在任务负载高峰期,线程池可以拥有较多的线程来快速处理任务;在任务负载低谷期,多余的线程可以在空闲一段时间后被销毁,避免占用过多的系统资源。
  4. 任务队列(workQueue)

    • 不同类型的任务队列

      任务队列用于存储等待执行的任务。常见的任务队列有阻塞队列(BlockingQueue)的几种实现类型,如

      ArrayBlockingQueue
      

      (有界数组阻塞队列)、

      LinkedBlockingQueue
      

      (无界链表阻塞队列)、

      SynchronousQueue
      

      (同步队列)等。

      • ArrayBlockingQueue:是一个有界队列,它在初始化时需要指定队列的大小。当线程池使用这种队列时,如果任务队列已满,并且线程池中的线程数量已经达到最大线程数,那么新的任务将会被拒绝。这种队列适用于对任务数量有明确限制的场景,以避免任务过多导致系统资源耗尽。
      • LinkedBlockingQueue:是一个无界队列(理论上可以无限添加任务,但受系统资源限制)。当线程池使用这种队列时,除非系统资源耗尽,否则新的任务一般不会被拒绝。这种队列适用于任务数量不确定,但希望尽可能地接受和处理任务的场景,不过需要注意可能会导致任务堆积过多的问题。
      • SynchronousQueue:是一种特殊的队列,它没有存储能力。当一个任务被放入这种队列时,必须有一个线程正在等待接收这个任务才能成功放入,否则会被拒绝。这种队列适用于要求任务能够被立即处理,不允许任务在队列中等待的场景。
    • 任务队列在任务调度中的角色:任务队列是线程池任务调度的重要组成部分。当线程池中的线程有空闲时,会从任务队列中获取任务并执行。它起到了缓冲任务的作用,使得线程池能够更高效地利用线程资源,避免线程频繁地创建和销毁。

  5. 线程工厂(threadFactory)

    • 自定义线程创建的作用:线程工厂用于创建新的线程。通过自定义线程工厂,可以对新创建的线程进行一些定制化操作,如设置线程名称、设置线程的优先级、设置线程为守护线程等。例如,可以为线程池中的线程设置一个有意义的名称,方便在调试和监控时识别不同的线程。
    • 实现方式与应用示例:可以通过实现ThreadFactory接口来创建自定义的线程工厂。例如:
    import java.util.concurrent.ThreadFactory;
    import java.util.concurrent.atomic.AtomicInteger;
    class CustomThreadFactory implements ThreadFactory {
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String prefix;
        public CustomThreadFactory(String prefix) {
            this.prefix = prefix;
        }
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, prefix + "-" + threadNumber.getAndIncrement());
            return t;
        }
    }
    

    这个自定义的线程工厂可以为创建的线程设置一个带有前缀的名称,方便区分不同用途的线程池。

  6. 拒绝策略(RejectedExecutionHandler)

    • 多种拒绝策略及适用场景

      :当线程池无法接收新的任务时(如任务队列已满并且线程池中的线程数量已经达到最大线程数),就会采用拒绝策略来处理新的任务。常见的拒绝策略有:

      • AbortPolicy(默认策略):直接抛出RejectedExecutionException异常,这种策略比较简单直接,适用于不允许丢失任务并且希望在任务无法处理时能够及时发现并处理异常的场景。
      • CallerRunsPolicy:由调用executesubmit方法的线程来执行这个任务。这种策略可以在一定程度上减轻线程池的压力,并且不会丢失任务,适用于对任务的实时性要求不是特别高的场景。
      • DiscardPolicy:直接丢弃新的任务,不做任何处理。这种策略适用于允许丢失部分不重要的任务,以保证线程池和系统的稳定运行的场景。
      • DiscardOldestPolicy:丢弃任务队列中最旧的任务(即最早进入队列的任务),然后将新的任务添加到队列中。这种策略适用于希望能够及时处理新的任务,并且可以接受丢失部分旧任务的场景。

10任务提交到线程池后,线程数是如何变化的?

  1. 初始状态与核心线程创建阶段
    • 初始线程数为 0:当线程池刚刚创建时,线程池中没有线程。此时,如果有任务提交到线程池,线程池会检查当前线程数是否小于核心线程数(corePoolSize)。
    • 创建核心线程处理任务:如果当前线程数小于核心线程数,线程池会创建新的线程来执行任务。例如,假设核心线程数为 3,第一个任务提交时,线程池会创建一个线程来执行这个任务;当第二个任务提交时,线程池会再创建一个线程(此时线程池中有 2 个线程),以此类推,直到线程池中的线程数达到核心线程数。这些核心线程在创建后不会因为空闲而被销毁,它们会一直等待任务到来。
  2. 任务队列缓冲阶段
    • 任务队列未满时:当线程池中的线程数达到核心线程数后,如果再有任务提交,线程池会将任务放入任务队列(workQueue)中。只要任务队列还有空间,新提交的任务就会被添加到队列中等待执行。例如,使用LinkedBlockingQueue作为任务队列(它是一个无界队列,在资源允许的情况下可以容纳很多任务),线程池会不断地将任务放入队列,而不会立即创建新的线程。
    • 线程池中的线程从任务队列取任务执行:线程池中的核心线程在执行完当前任务后,会从任务队列中获取下一个任务来执行。这样,通过任务队列的缓冲作用,线程池可以更高效地利用线程资源,避免频繁地创建和销毁线程。
  3. 最大线程数扩展阶段
    • 任务队列已满且未达到最大线程数时:当任务队列已满,并且当前线程数小于最大线程数(maximumPoolSize)时,线程池会创建新的线程来执行任务。例如,假设核心线程数为 3,最大线程数为 5,任务队列已满。此时,如果再有任务提交,线程池会创建新的线程,直到线程池中的线程数达到最大线程数。这些新创建的线程在空闲一段时间后(由 keepAliveTime 参数决定),如果仍然没有任务可执行,就会被销毁。
    • 创建新线程的边界条件与限制:线程池不会无限制地创建新线程。它会根据最大线程数这个限制来控制线程数量。一旦线程池中的线程数达到最大线程数,并且任务队列也已满,就会进入拒绝策略阶段。
  4. 拒绝策略阶段(线程池无法接收新任务)
    • 多种拒绝策略的触发情况:当线程池无法接收新的任务时(即任务队列已满且线程数达到最大线程数),会根据预先设置的拒绝策略(RejectedExecutionHandler)来处理新的任务。例如,采用默认的AbortPolicy拒绝策略时,会直接抛出RejectedExecutionException异常;如果是CallerRunsPolicy,则由提交任务的线程来执行这个任务;DiscardPolicy会直接丢弃新的任务;DiscardOldestPolicy会丢弃任务队列中最旧的任务,然后将新的任务添加到队列中。

11,为什么线程池要先使用阻塞队列,而不是直接增加线程?

  1. 资源管理与性能优化方面的考虑
    • 减少线程创建和销毁的开销:创建和销毁线程是有一定成本的。当创建一个线程时,需要为其分配系统资源,如栈空间、CPU 时间片等。而且线程销毁时也需要回收这些资源。如果每次有新任务就直接创建新线程,当任务频繁到来时,会频繁地进行线程创建和销毁操作,这会消耗大量的系统资源和时间。而使用阻塞队列可以将任务缓存起来,让线程池中的线程能够重复利用,从队列中获取任务并执行,避免了不必要的线程创建和销毁,从而提高系统的性能。
    • 更好地控制线程数量:直接增加线程可能会导致线程数量过多,从而引发一系列问题。例如,过多的线程会导致 CPU 频繁地进行线程上下文切换,这会占用大量的 CPU 时间,降低系统的整体性能。而且,每个线程都需要占用一定的内存资源,过多的线程可能会导致内存资源紧张。通过使用阻塞队列,可以先将任务存储起来,根据线程池的配置(如核心线程数、最大线程数)来合理地控制线程数量,使得线程数量在一个合适的范围内,既能有效地处理任务,又不会对系统资源造成过大的压力。
  2. 任务调度和负载均衡的便利性
    • 任务缓冲和顺序执行:阻塞队列可以作为任务的缓冲区域。当任务到来的速度不均匀时,例如,可能会出现短时间内大量任务涌入,然后又一段时间没有任务的情况。阻塞队列可以先将这些任务缓存起来,按照一定的顺序(如先进先出)让线程来执行。这样可以保证任务的有序执行,避免任务丢失或者混乱。而且,对于一些有依赖关系的任务,通过阻塞队列可以方便地实现顺序调度。
    • 实现负载均衡:在多线程环境下,通过阻塞队列可以更好地实现负载均衡。不同的线程可以从队列中获取任务,根据线程的处理能力和任务的性质,自动地分配任务。例如,一些处理速度快的线程可以从队列中获取更多的任务来执行,而不会出现某个线程过度忙碌而其他线程闲置的情况,提高了系统处理任务的整体效率。
  3. 系统稳定性和可靠性的保障
    • 避免系统过载:如果没有阻塞队列,直接增加线程来处理任务,在高负载情况下可能会导致系统资源耗尽。例如,当任务量突然增大,可能会无限制地创建线程,最终导致系统内存不足或者 CPU 崩溃。而阻塞队列可以作为一种流量控制机制,当任务队列已满时,可以通过线程池的拒绝策略来合理地处理无法接收的任务,从而保护系统的稳定性。
    • 故障隔离和恢复:在分布式系统或者复杂的应用场景中,使用阻塞队列可以提供一定程度的故障隔离。如果某个线程出现故障,其他线程可以继续从队列中获取任务执行,不会因为个别线程的问题而导致整个系统崩溃。而且,在系统进行维护或者升级时,通过阻塞队列可以暂停任务的接收和处理,完成维护后再继续从队列中获取任务,方便系统的恢复。

12,SQL 事务有哪些特性?

  1. 原子性(Atomicity)
    • 定义与概念:事务的原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部执行,要么全部不执行。就好像一个原子,它是构成物质的最小单位,不能再被分割。例如,在银行转账业务中,从一个账户扣款和向另一个账户收款这两个操作必须作为一个整体来执行,不能出现只完成了扣款而没有完成收款,或者反之的情况。
    • 实现机制与示例:在数据库管理系统中,通过日志(Log)和回滚(Rollback)机制来保证原子性。当事务开始执行时,数据库会记录下这个事务的所有操作信息到日志文件中。如果在事务执行过程中出现了错误或者事务需要被撤销,数据库可以根据日志文件中的信息将已经执行的操作进行回滚,使得事务的所有操作好像都没有发生过一样。例如,在使用 SQL 的BEGIN TRANSACTIONCOMMITROLLBACK语句时,如果在BEGIN TRANSACTIONCOMMIT之间的操作出现错误,可以使用ROLLBACK语句将事务回滚到事务开始前的状态。
  2. 一致性(Consistency)
    • 含义及重要性:事务的一致性是指事务执行前后,数据库的完整性约束没有被破坏。完整性约束包括实体完整性(如主键约束,确保表中的每一行都有唯一的主键)、参照完整性(如外键约束,确保表之间的关联关系正确)和用户自定义的完整性(如某个字段的取值范围)。例如,在一个学生选课系统中,学生表和课程表之间通过外键关联,当在选课表中插入一条选课记录时,必须保证学生表中有对应的学生记录,课程表中有对应的课程记录,这样才能保证数据库的一致性。
    • 数据库如何保证一致性:数据库通过在事务执行过程中对完整性约束的检查来保证一致性。在执行每一个操作时,数据库会自动检查是否违反了各种完整性约束。如果违反了约束,事务会被回滚,以确保数据库状态的合法和一致。例如,当试图插入一条违反主键约束的记录时,数据库会拒绝这个插入操作,并且如果这个操作是在一个事务中,可能会导致整个事务被回滚。
  3. 隔离性(Isolation)
    • 隔离级别的概念与种类:事务的隔离性是指多个事务并发执行时,一个事务的执行不能被其他事务干扰。SQL 标准定义了四种隔离级别,从低到高分别是READ UNCOMMITTED(读取未提交数据)、READ COMMITTED(读取已提交数据)、REPEATABLE READ(可重复读)和SERIALIZABLE(串行化)。不同的隔离级别对并发事务之间的相互影响有不同的限制。例如,在READ UNCOMMITTED隔离级别下,一个事务可以读取到另一个未提交事务的数据,这可能会导致脏读(Dirty Read)问题;而在SERIALIZABLE隔离级别下,事务会以串行的方式执行,完全避免了并发问题,但会牺牲一定的性能。
    • 不同隔离级别下的并发问题与解决方式
      • 脏读(Dirty Read):在READ UNCOMMITTED隔离级别下,一个事务可以读取到另一个事务未提交的数据。例如,事务 A 修改了一条记录但还未提交,事务 B 就读取了这条记录,之后事务 A 回滚了修改,那么事务 B 读取的数据就是脏数据。解决这个问题可以将隔离级别提升到READ COMMITTED或更高,这样事务 B 只能读取事务 A 已提交的数据。
      • 不可重复读(Non - repeatable Read):在READ COMMITTED隔离级别下,一个事务在两次读取同一数据时,可能会因为另一个事务在这期间修改并提交了这个数据而得到不同的结果。例如,事务 A 先读取了一条记录的值,然后事务 B 修改并提交了这条记录,事务 A 再次读取时就会得到不同的值。可以通过将隔离级别提升到REPEATABLE READ来解决这个问题,在这个隔离级别下,事务 A 在整个事务期间读取同一数据会得到相同的结果。
      • 幻读(Phantom Read):在REPEATABLE READ隔离级别下,一个事务在按照某个条件进行范围查询时,可能会因为另一个事务插入或删除了满足这个条件的记录而得到不同的结果集。例如,事务 A 按照某个条件查询了一批记录,然后事务 B 插入了一条满足这个条件的记录,事务 A 再次按照相同条件查询时,就会发现多了一条记录,好像出现了 “幻影” 一样。SERIALIZABLE隔离级别可以解决幻读问题,但会严重影响性能。在一些数据库中,通过使用间隙锁(Gap Lock)等技术在REPEATABLE READ隔离级别下也可以解决幻读问题。
  4. 持久性(Durability)
    • 定义与意义:事务的持久性是指一旦事务提交,它对数据库中数据的改变就是永久性的,即使数据库系统出现故障(如断电、系统崩溃等),这些修改的数据也不会丢失。例如,在电商系统中,当用户成功下单后,订单数据被持久化到数据库中,即使服务器出现故障,这些订单数据也应该能够恢复,以保证业务的正常进行。
    • 数据库的持久化机制:数据库通过多种方式来保证持久性。一种常见的方式是使用日志和数据文件的同步机制。在事务提交时,数据库会先将事务的修改操作记录到日志文件中,并且将日志文件中的信息同步到磁盘上的数据文件中。有些数据库还会使用缓存技术,先将数据修改存储在缓存中,在合适的时候(如定期或者达到一定条件时)将缓存中的数据同步到磁盘上,以确保数据的持久性。

13,这些特性是如何实现的?

  1. 原子性的实现机制

    • 日志(Log)机制:数据库在事务开始时会记录事务的操作日志。这些日志包含了事务对数据库的各种修改操作,如插入、更新、删除等记录的信息。例如,对于一个更新语句UPDATE table SET column = value WHERE condition,日志中会记录更新前的数据状态、更新后的目标数据状态以及更新的条件等内容。当事务执行过程中出现故障(如系统崩溃、断电等),数据库可以根据这些日志来恢复事务的操作,要么将事务完整地执行完成(通过重新执行日志中的操作),要么将事务回滚(撤销已经执行的操作)。
    • 回滚(Rollback)操作与 undo 日志:在数据库内部,为了实现回滚功能,会使用 undo 日志。undo 日志记录了如何撤销一个已经执行的操作。例如,对于一个插入操作,undo 日志会记录插入的记录内容,以便在需要回滚时能够删除这条记录;对于一个更新操作,undo 日志会记录更新前的数据值,这样在回滚时可以将数据恢复到更新前的状态。当执行ROLLBACK命令或者事务因为异常需要回滚时,数据库会根据 undo 日志来反向操作,从而保证事务的原子性。
  2. 一致性的实现方式

    • 完整性约束检查

      :数据库在执行事务中的每个操作时,都会检查各种完整性约束。

      • 实体完整性约束(如主键约束):在插入或更新记录时,数据库会检查主键是否唯一。例如,在一个关系型数据库中,当插入一条记录到带有主键的表中时,数据库会自动验证插入的主键值在表中是否已经存在。如果存在,就会拒绝插入操作,以保证实体完整性。
      • 参照完整性约束(如外键约束):当对带有外键的表进行插入、更新或删除操作时,数据库会检查外键关系是否正确。例如,在一个订单系统中,订单表中的用户 ID 是外键关联到用户表的主键。当插入一个订单记录时,数据库会检查用户表中是否存在对应的用户 ID,否则会拒绝插入操作,保证参照完整性。
      • 用户自定义完整性约束:除了内置的完整性约束,数据库还允许用户定义自己的完整性约束。例如,用户可以定义一个字段的取值范围,在进行操作时,数据库会检查数据是否满足这个范围。如果不满足,事务可能会被回滚。
    • 事务的隔离性保证一致性:合适的隔离级别可以防止并发事务之间的干扰,从而保证数据库的一致性。例如,在READ COMMITTED隔离级别下,一个事务只能读取另一个已提交事务的数据,避免了脏读问题,使得事务读取的数据是合法和一致的。更高的隔离级别如REPEATABLE READSERIALIZABLE进一步限制了并发事务的相互影响,确保事务在执行过程中看到的数据是符合一致性要求的。

  3. 隔离性的实现技术

    • 锁机制(Locking)
      • 共享锁(Shared Lock)和排他锁(Exclusive Lock):共享锁用于对数据进行读取操作,多个事务可以同时对同一数据加共享锁,例如在READ COMMITTED隔离级别下,事务在读取数据时会加共享锁。排他锁用于对数据进行写操作,一个数据对象加上排他锁后,其他事务既不能对其加共享锁也不能加排他锁。例如,当一个事务对一条记录进行更新操作时,会对这条记录加排他锁,防止其他事务同时对这条记录进行读写操作。
      • 行级锁(Row - level Lock)、表级锁(Table - level Lock)和间隙锁(Gap Lock):行级锁是对表中的具体行进行锁定,这样可以在高并发场景下,只限制对被操作行的访问,而不影响其他行的并发操作。表级锁是对整个表进行锁定,当一个事务对一个表加表级锁时,其他事务不能对该表进行读写操作,这种锁粒度较大,会影响并发性能,但在某些情况下(如批量操作)可能会使用。间隙锁主要用于解决幻读问题,它锁定的不是具体的行,而是行与行之间的间隙,防止其他事务在这个间隙中插入新的记录。
    • 多版本并发控制(MVCC,Multi - Version Concurrency Control):MVCC 是一种并发控制的技术,它通过为每个数据对象维护多个版本来实现隔离性。在 MVCC 机制下,每个事务在开始时会获取一个事务版本号。当一个事务对数据进行读取操作时,它会读取在这个事务版本号之前最后一次提交的数据版本。当一个事务对数据进行更新操作时,它会创建一个新的数据版本,并将这个版本标记为未提交。其他事务在读取时不会看到这个未提交的版本,只有当更新事务提交后,其他事务才会看到新的版本。这样,MVCC 可以在不使用锁或者减少使用锁的情况下,实现不同隔离级别的并发控制,提高系统的并发性能。
  4. 持久性的实现措施

    • 日志文件与数据文件的同步

      :数据库在事务提交时,会将事务的修改操作记录到日志文件中。然后,会将日志文件中的修改同步到磁盘上的数据文件中。这个同步过程可以通过多种方式实现。

      • 预写式日志(Write - Ahead Logging,WAL):这是一种常见的持久化机制。在这种机制下,事务的修改首先被记录到日志文件中,并且在日志文件成功写入磁盘后,才会将修改应用到数据文件中。这样,即使在数据文件还没有来得及更新时发生了故障,数据库也可以根据日志文件来恢复数据,保证事务的持久性。
      • 数据缓存与刷盘机制:数据库通常会使用缓存来提高性能。在内存中有数据缓存,事务的修改首先会在缓存中进行。然后,数据库会根据一定的策略(如定时、缓存满了或者事务提交时)将缓存中的数据刷盘(写入磁盘)。为了保证持久性,数据库会在事务提交时确保缓存中的数据已经安全地写入磁盘,例如通过强制刷盘操作或者等待缓存中的数据成功写入磁盘后才认为事务提交成功

14,什么是mvcc

  1. MVCC 的定义与基本概念

    • 定义:MVCC(Multi - Version Concurrency Control)即多版本并发控制,是一种数据库并发控制的技术。它的核心思想是为每个数据对象维护多个版本,不同的事务在访问数据时可以看到不同的版本,从而实现高效的并发访问控制,同时在一定程度上保证事务的隔离性。
    • 版本的产生与维护:在 MVCC 机制下,当一个事务对数据进行更新操作时,数据库不会直接覆盖原来的数据,而是会创建一个新的数据版本。这个新版本会记录更新后的内容,并且会与更新事务相关联。例如,在一个支持 MVCC 的数据库中,一条记录可能会同时存在多个版本,每个版本对应不同的事务阶段。
  2. MVCC 在事务隔离中的作用

    • 解决读写冲突问题:在并发事务环境下,读写冲突是一个常见的问题。MVCC 通过提供数据的多个版本来解决这个问题。例如,在READ COMMITTED隔离级别下,一个事务在读取数据时可以获取到在其开始时刻已经提交的最新版本的数据。这样,即使另一个事务正在对同一数据进行更新操作,读取事务也不会被阻塞,因为它可以读取到旧的版本,避免了读写冲突导致的等待。

    • 实现不同隔离级别

      MVCC 可以用于实现不同的事务隔离级别。

      • READ COMMITTED 隔离级别实现方式:在这种隔离级别下,事务每次读取数据时都会获取到最新已提交的版本。当一个事务开始读取数据后,如果有其他事务对同一数据进行更新并提交,后续该读取事务再次读取这个数据时,会获取到更新后的版本。这种方式可以避免脏读问题,因为读取事务只能看到已提交的数据版本。
      • REPEATABLE READ 隔离级别实现方式:在这个隔离级别下,事务在开始时会确定一个版本号或者时间戳。在整个事务过程中,事务读取的数据版本都是基于这个初始的版本号或者时间戳。即使其他事务对同一数据进行更新并提交,该事务在重复读取数据时,仍然会看到其初始版本的数据,从而实现了可重复读的特性,避免了不可重复读的问题。
  3. MVCC 的工作原理与实现细节

    • 版本链(Version Chain)的概念与构建:在 MVCC 中,数据对象的多个版本通过版本链的方式进行组织。每个版本都包含一个指向前一个版本的指针,形成一个链表结构。例如,对于一个数据表中的某条记录,每次更新操作都会生成一个新的版本,并将新的版本插入到版本链中。最新的版本在链的头部,最旧的版本在链的尾部。当事务需要读取数据时,可以根据事务的隔离级别和其他条件沿着版本链查找合适的版本。
    • 事务版本号(Transaction Version Number)与可见性规则:每个事务在开始时会被分配一个事务版本号。在读取数据时,事务会根据版本号和可见性规则来确定能够看到的数据版本。例如,在一个简单的 MVCC 实现中,可见性规则可能是:事务只能看到在其事务版本号之前最后一次提交的版本。如果一个数据版本的创建事务版本号大于当前读取事务的版本号,那么这个版本对于当前事务是不可见的。
    • 垃圾回收(Garbage Collection)机制:随着数据版本的不断更新,旧的版本可能会变得不再需要。MVCC 会有一个垃圾回收机制来回收这些旧的版本,以节省存储空间。通常,当一个版本不再被任何事务引用时,就可以被回收。例如,当所有可能需要访问某个旧版本的事务都已经结束(提交或者回滚),这个旧版本就可以被安全地删除。

15,深拷贝与浅拷贝的区别是什么?如何实现深拷贝与浅拷贝?

  1. 深拷贝与浅拷贝的概念区别

    • 浅拷贝(Shallow Copy)
      • 定义:浅拷贝是指在对象复制时,只复制对象的基本数据类型成员变量的值,而对于引用类型的成员变量,只是复制引用(即内存地址),而不复制引用对象本身。这意味着原始对象和拷贝对象的引用类型成员变量指向相同的内存地址,它们共享同一份引用对象。
      • 示例:假设有一个类Person,其中包含一个String类型的姓名(基本数据类型)和一个Address类型的地址(引用类型)。当对Person对象进行浅拷贝时,新对象的姓名会是原始对象姓名的副本,但新对象和原始对象的地址引用将指向同一个Address对象。
    • 深拷贝(Deep Copy)
      • 定义:深拷贝是指在对象复制时,不仅复制对象的基本数据类型成员变量的值,还会递归地复制引用类型成员变量所指向的对象,使得原始对象和拷贝对象完全独立,它们的引用类型成员变量也指向不同的对象。
      • 示例:对于上述Person类进行深拷贝时,新对象的姓名是原始对象姓名的副本,并且新对象的地址是原始Address对象的一个全新副本,这两个Address对象在内存中是相互独立的,修改其中一个对象的地址信息不会影响另一个对象。
  2. 浅拷贝的实现方式

    • 使用赋值语句(对于基本数据类型)和引用复制(对于引用类型):在 Java 中,如果只是简单地使用赋值语句来复制一个对象,对于基本数据类型成员变量,会进行值的复制;对于引用类型成员变量,实际上是复制引用。例如,假设有一个简单的Student类,包含一个int类型的学号和一个String类型的姓名:
    class Student {
        int studentId;
        String studentName;
        public Student(int studentId, String studentName) {
            this.studentId = studentId;
            this.studentName = studentName;
        }
    }
    public class ShallowCopyExample {
        public static void main(String[] args) {
            Student student1 = new Student(1, "Alice");
            Student student2 = student1;  // 浅拷贝,student2和student1指向相同的对象
            System.out.println(student1.studentId);  // 输出1
            System.out.println(student2.studentId);  // 输出1
            student2.studentId = 2;
            System.out.println(student1.studentId);  // 输出2,因为student1和student2指向相同的对象
        }
    }
    
    • 实现Cloneable接口(Java 特定方式)并调用clone()方法(部分浅拷贝情况):在 Java 中,类可以实现Cloneable接口来表明它可以被克隆。当调用clone()方法时,默认情况下是浅拷贝。例如,对于一个包含引用类型成员变量的类:
    class Book {
        String title;
        Author author;
        public Book(String title, Author author) {
            this.title = title;
            this.author = author;
        }
    }
    class Author {
        String name;
        public Author(String name) {
            this.name = name;
        }
    }
    class BookCloneable implements Cloneable {
        Book book;
        public BookCloneable(Book book) {
            this.book = book;
        }
        @Override
        public BookCloneable clone() throws CloneNotSupportedException {
            return (BookCloneable) super.clone();
        }
    }
    public class ShallowCloneExample {
        public static void main(String[] args) throws CloneNotSupportedException {
            Author author = new Author("John");
            Book book = new Book("Java Basics", author);
            BookCloneable bookCloneable1 = new BookCloneable(book);
            BookCloneable bookCloneable2 = bookCloneable1.clone();
            // bookCloneable1和bookCloneable2的book.author指向相同的Author对象
            System.out.println(bookCloneable1.book.author.name);  // 输出John
            System.out.println(bookCloneable2.book.author.name);  // 输出John
            bookCloneable2.book.author.name = "Mike";
            System.out.println(bookCloneable1.book.author.name);  // 输出Mike,因为共享Author对象
        }
    }
    
  3. 深拷贝的实现方式

    • 递归复制引用类型成员变量(手动实现):对于包含引用类型成员变量的对象,要实现深拷贝,可以通过手动创建新的引用对象,并将原始对象引用类型成员变量的内容复制到新对象中。例如,继续以Book类为例,手动实现深拷贝:
    class BookDeepCopy {
        String title;
        Author author;
        public BookDeepCopy(String title, Author author) {
            this.title = title;
            this.author = new Author(author.name);  // 深拷贝Author对象
        }
    }
    public class DeepCopyManualExample {
        public static void main(String[] args) {
            Author author = new Author("John");
            BookDeepCopy book1 = new BookDeepCopy("Java Basics", author);
            BookDeepCopy book2 = new BookDeepCopy(book1.title, book1.author);
            System.out.println(book1.author.name);  // 输出John
            System.out.println(book2.author.name);  // 输出John
            book2.author.name = "Mike";
            System.out.println(book1.author.name);  // 输出John,因为book1和book2的Author对象是独立的
        }
    }
    
    • 使用序列化和反序列化(Java 特定方式):在 Java 中,另一种实现深拷贝的方式是通过序列化和反序列化。对象需要实现Serializable接口。例如:
    import java.io.*;
    class SerializableBook {
        String title;
        Author author;
        public SerializableBook(String title, Author author) {
            this.title = title;
            this.author = author;
        }
    }
    public class DeepCopySerializationExample {
        public static void serialize(SerializableBook book, String fileName) throws IOException {
            try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
                oos.writeObject(book);
            }
    

16,如何编写分页查询的SQL语句?

  1. 基于 MySQL 的分页查询

    • 基本语法与原理:在 MySQL 中,常用LIMIT关键字来实现分页查询。LIMIT后面可以跟两个参数,第一个参数表示偏移量(从第几行开始查询),第二个参数表示要查询的行数。例如,LIMIT 0, 10表示从第一行(偏移量为 0)开始,查询 10 行数据。这是基于索引从 0 开始计数的。
    • 示例代码:假设我们有一个名为employees的表,其中包含员工信息,要查询第一页,每页显示 10 条记录,可以使用以下 SQL 语句:
    SELECT * FROM employees LIMIT 0, 10;
    

    如果要查询第二页,可以这样写:

    SELECT * FROM employees LIMIT 10, 10;
    

    一般情况下,如果要查询第n页,每页显示m条记录,偏移量可以通过(n - 1) * m来计算。所以,查询第n页的 SQL 语句可以写成:

    SELECT * FROM employees LIMIT (n - 1) * m, m;
    

17,注解的原理是什么

  1. 注解的定义与元数据概念
    • 定义:注解(Annotation)是 Java 等编程语言中的一种语法元素,它提供了一种将元数据(Metadata)与程序元素(如类、方法、字段等)相关联的方式。元数据是关于数据的数据,在这里可以理解为是对程序元素的额外描述信息。例如,@Override注解用于标记一个方法是重写父类中的方法,它本身不改变方法的功能,但为编译器和开发工具等提供了重要的信息。
  2. Java 反射机制与注解读取
    • 反射机制基础:Java 的反射机制是注解能够发挥作用的重要基础。反射允许程序在运行时动态地获取类的信息,包括类的方法、字段、构造函数等。通过反射,可以获取到被注解标记的程序元素。例如,在一个类上使用了@MyAnnotation注解,在运行时可以通过反射获取这个类的Class对象,然后再获取该类上的注解信息。
    • 读取注解信息的步骤
      • 获取Class对象:首先,使用Class.forName()(通过类的全限定名)或者类的字面量(如MyClass.class)等方式获取类的Class对象。例如,Class<?> myClassObj = MyClass.class;
      • 获取注解对象:通过Class对象的getAnnotation()方法(如果注解是直接标记在类上,并且只有一个该类型的注解)或者getAnnotations()getDeclaredAnnotations()方法(获取所有注解或只获取直接声明在该类上的注解)来获取注解对象。例如,如果有一个自定义注解@MyAnnotation标记在MyClass类上,可以这样获取注解对象:MyAnnotation myAnnotation = myClassObj.getAnnotation(MyAnnotation.class);
      • 获取注解中的属性值:一旦获取了注解对象,就可以通过注解对象的方法(通常是一些以get开头的方法,这些方法是在定义注解时确定的)来获取注解中的属性值。例如,如果@MyAnnotation有一个属性value,可以通过myAnnotation.value()来获取这个属性的值。
  3. 注解处理器(Annotation Processor)的作用
    • 编译期处理:在 Java 编译过程中,注解处理器可以对注解进行处理。它是一个特殊的工具,能够在编译阶段扫描源代码中的注解,并根据注解的信息生成额外的代码或者执行一些检查操作。例如,在使用 Java Persistence API(JPA)时,@Entity注解标记的类会被注解处理器识别,处理器会根据这个注解以及类中的其他相关注解(如@Id@Column等)生成数据库表映射相关的代码或者进行一些合法性检查。
    • 运行时处理与自定义处理逻辑:除了编译期处理,注解也可以在运行时进行处理。通过自定义的代码逻辑,在程序运行时读取注解信息并根据这些信息执行相应的操作。例如,在一个基于 Spring 框架的应用中,@Service@Controller等注解标记的类会在 Spring 容器启动时被扫描到,Spring 会根据这些注解创建对应的 Bean 对象,并管理它们的生命周期,这是通过 Spring 内部的运行时注解处理机制实现的。

http://www.kler.cn/a/462995.html

相关文章:

  • 迟来的前端面试经验
  • C# _ 数字分隔符的使用
  • Node.js 中 http 模块的深度剖析与实战应用
  • 前端路由 Hash 和 History 模式原理对比区别
  • python生成、操作svg图片
  • 设计模式 创建型 单例模式(Singleton Pattern)与 常见技术框架应用 解析
  • Neo4j GDS 2.0 安装与配置
  • logback日志框架源码分析
  • Ceph 手动部署(CentOS9)
  • 深入探索 Spring Boot:开启高效开发之旅
  • java实现一个kmp算法
  • 路由算法之RIP、OSPF、BGP( The Ruting Agorithm of RIP OSPF BGP)
  • 小程序租赁系统开发的优势与应用探索
  • canvas+fabric实现时间刻度尺(二)
  • 集合(List、Set、Map)ArrayList、LinkedList、Vector、HashSet、LinkedHashSet、HashMap
  • [JAVA]MyLogger
  • 音视频入门基础:MPEG2-PS专题(4)——FFmpeg源码中,判断某文件是否为PS文件的实现
  • Web安全 - 使用 Nginx + Lua 防御 NoSQL 注入攻击
  • 【TensorFlow】Keras介绍与入门
  • redis zset底层实现
  • react相关报错--持续更新中
  • Android studio 将项目打包apk
  • 新年算法题:矩阵对称性检测
  • Linux 内核学习(3) --- 内核中断机制
  • 单片机-- 51-keil使用查看空间占用
  • C++ 设计模式:状态模式(State Pattern)