(必收藏)Java常见面试题十五道: 线程、并发、HashMap、JVM、异常、Spring、mysql

1. 线程安全本质是什么,Java 如何保证线程安全,callable,runnable 有什么区别,线程不正常终止会发生什么,线程占用的空间具体是哪,是寄存器还是内存还是什么

线程安全本质是多线程并发访问共享资源时,不会出现不可预期的结果,也就是说多个线程同时对同一份数据进行读写操作时,不会导致数据的错误、丢失或不一致。

在 Java 中,可以通过以下几种方式来保证线程安全:

  1. 使用 synchronized 关键字来实现同步访问,即对某个对象加锁,在同一时间只允许一个线程访问被加锁的代码块或方法。
  2. 使用 ReentrantLock 来控制多个线程对共享资源的访问。
  3. 使用 AtomicInteger、ConcurrentHashMap、CopyOnWriteArrayList 等并发容器,来解决并发场景下的数据安全问题。
  4. 使用 volatile 关键字来保证变量在线程之间的可见性。

Callable 和 Runnable 都是用来实现多线程的接口,主要区别在于:

  1. Callable 接口定义了一个 call() 方法来返回计算结果,而 Runnable 接口的 run() 方法则没有返回值。
  2. Callable 接口的 call() 方法声明了一个受检查的异常,需要通过 try-catch 来捕获异常,而 Runnable 接口的 run() 方法则没有声明异常。

当线程不正常终止时,会发生一些难以预料的情况。比如,如果子线程死锁或者因为异常退出,那么主线程可能无法正确地获取子线程的结果,从而导致程序出现未知错误。

线程占用的空间主要包括两个方面:

  1. 程序计数器:每个线程都拥有自己的程序计数器,它用于记录下一条指令的地址,当 CPU 切换至其他线程时,会将当前线程的程序计数器的状态保存到线程栈中。
  2. 线程栈:每个线程都拥有自己的线程栈,用于存储方法调用的局部变量、参数、返回值等信息。当一个线程被创建时,其线程栈也会被创建。线程栈中的内容在进入一个新的方法时被压入栈中,在退出该方法时弹出.

2. Java 的线程和 Linux 的线程有什么区别,为什么需要 Java 的线程

Java 的线程和 Linux 的线程在实现层面上有着本质不同。Linux 线程是由操作系统内核管理的轻量级进程,它们的调度和切换都是由操作系统内核完成的。而 Java 线程则是由 JVM 虚拟机创建和管理的重量级线程,每个线程都需要占用较多内存,因此不能创建过多的 Java 线程。不过,JVM 实现了一个称为“用户级线程”的机制,即将多个 Java 线程映射到少量的系统线程上,这些线程被称为“内核级线程”。

从使用角度来看,Java 线程提供了更高层次的抽象和更易用的接口,可以帮助开发者更容易地进行多线程编程,并且提供了丰富的 API 支持,如锁、同步器、线程池等,可以帮助开发者更容易地编写高效、可靠的并发程序。而 Linux 线程则适用于需要大量线程且资源受限的低端设备上。因此,在选择线程模型时应该根据具体的应用场景进行选择,以达到最优的效果。

3. volatile 具体实现原理,内存重排序都会发生在哪,为什么要内存重排序

  • *volatile 是 Java 中的关键字之一,用于保证多线程环境下变量的可见性和禁止指令重排序。

在多线程程序中,如果一个线程修改了共享变量的值,但是其他线程并没有立即感知到这个变化,这就会导致线程间数据不一致的问题。而使用 volatile 关键字声明的变量,可以确保每次对该变量的读操作都从主存中读取最新的值,每次对该变量的写操作都能立即同步到主存中,从而保证了该变量在多线程之间的可见性。

此外,volatile 也可以禁止 CPU 对代码的指令进行重排序,确保代码执行顺序与程序员编写的顺序一致,从而保证了程序的正确性和可靠性。

需要注意的是,虽然 volatile 可以确保变量的可见性和禁止指令重排序,但是它并不能保证原子性。如果需要保证原子性,需要使用 synchronized 或者 Lock 等同步机制。

  • volatile 关键字主要有两个作用:
  1. 禁止指令重排序:当一个变量被声明为 volatile 后,它的赋值操作就会被编译器编译成一条内存屏障指令(Memory Barrier),这个指令会阻止该指令后面的指令和之前的指令的重排序。
  2. 保证可见性:当一个线程修改了一个 volatile 变量的值,这个值会立即写入主内存,其他线程读取该变量时也会从主内存中读取该变量的最新值。

具体实现原理是,volatile 变量被修改后会立即写入主内存,并且每次读取时都会从主内存中重新获取最新的值,而不是从线程的本地缓存中获取。因此,在多线程环境下,使用 volatile 变量可以保证线程安全。

内存重排序是指在编译器或者运行时对指令序列进行优化的过程,通过重新排序指令可以提高程序的执行效率。但是,内存重排序可能会破坏程序的正确性,导致程序出现异常的行为。

内存重排序主要有两种类型:编译器重排序和处理器重排序。编译器重排序是指编译器在生成目标代码时对指令进行重排序,处理器重排序是指处理器在执行指令时对指令进行重排序。

内存重排序的原因是为了提高程序的执行效率。但是,在多线程环境下,内存重排序可能会导致竞态条件(Race Condition)的问题,因此需要使用同步机制(如 synchronized 和 volatile 等)来保证程序的正确性。除此之外,还可以使用各种锁和原子变量等机制避免内存重排序引起的问题。

4. 使用过哪些 Java 并发包

Java 并发包主要包括以下几个:

  1. java.util.concurrent:这个包是 Java 并发编程的核心包,提供了线程池、阻塞队列、原子变量、锁、信号量等并发工具类,可以方便地实现高效的多线程程序。
  2. java.util.concurrent.atomic:这个包提供了一组原子操作类,用于实现无锁并发编程。其中包括 AtomicBoolean、AtomicInteger、AtomicLong 等原子变量类。
  3. java.util.concurrent.locks:这个包提供了一组基于锁的并发编程工具类,包括 ReentrantLock、ReadWriteLock、Condition 等,可以用于实现精细化的线程同步控制。
  4. java.util.concurrent.atomic 和 java.util.concurrent.locks 包下的工具类都是通过底层的 CAS(Compare and Swap)算法来实现的,可以保证原子性和可见性。

除此之外,Java 并发包还有:

  1. java.util.concurrent.Executor:这个包定义了 Executor 框架,提供了线程池的实现方式,以及提交任务的方式。
  2. java.util.concurrent.ScheduledExecutorService:这个包提供了一组定时任务的支持,可以用于实现周期性执行任务的功能。
  3. java.util.concurrent.ConcurrentHashMap:这个包提供了一种线程安全的哈希表实现方式,可以用于实现高效的并发编程。
  4. java.util.concurrent.BlockingQueue:这个包提供了一组阻塞队列的实现,可以用于实现生产者消费者模式等功能。

总之,Java 并发包提供了丰富的并发编程工具类和框架,可以方便地编写高效的多线程程序。

5. 简述 BIO,NIO 的具体使用及原理

BIO 的具体使用:

  1. 使用 Socket 类创建一个连接对象,然后通过输入流和输出流来进行数据传输。在读取数据时,该方法会阻塞直到有数据可读。
  2. 如果需要同时处理多个客户端连接,则需要使用多线程或者线程池来实现,并且每个线程或者线程池中都运行着一个独立的任务,用来处理与对应客户端连接的 I/O 操作。

BIO 的原理:

  1. 当客户端发起连接请求时,服务端通过 ServerSocket 类的 accept() 方法来接受连接,该方法是一个阻塞式调用。
  2. 如果有新的连接请求,该方法将返回一个表示该连接的 Socket 对象,并以此完成握手过程。
  3. 通过 InputStream 和 OutputStream 对象进行数据读写,在读取数据时,如果没有数据可读,则会一直阻塞,直到有数据可读为止。

NIO 的具体使用:

  1. 使用 Selector 类管理一个或多个通道,然后通过 Channel 类来进行数据读写。
  2. Selector 对象可以监听多个通道上的事件,当一个通道中有可读数据时,Selector 就会返回该通道的 SelectionKey,程序通过这个 SelectionKey 可以获取该通道上的数据。

NIO 的原理:

  1. NIO 是基于事件驱动的模型,通过 Selector 监听多个通道上的事件,当一个已注册的通道有数据可读时,Selector 就会返回该通道的 SelectionKey。
  2. 通过 Channel 类来进行 I/O 操作,在读取数据时,如果没有数据可读,则不会阻塞线程,而是直接返回。因此,NIO 可以实现非阻塞式的 I/O 操作。

BIO 是阻塞式的 I/O 操作,也就是说在进行 I/O 操作时会一直阻塞,直到读取/写入出数据为止。在这种模型中,客户端连接请求需要在服务端单独启动一个线程来处理,因此无法实现高并发的处理能力。

NIO 则是一种非阻塞式的 I/O 操作,它可以在执行 I/O 操作时立即返回,而不必一直等待。因此,NIO 提供了异步的 I/O 操作方式。使用 NIO,可以通过 Selector 监听多个通道上的事件,当一个已注册的通道有数据可读时,Selector 就会返回该通道的 SelectionKey,程序通过这个 SelectionKey 可以获取该通道上的数据。

6. concurrentHashMap1.8 和之前版本有什么区别,HashMap 的具体实现,红黑树和平衡二叉树的区别,为什么不用 B+ 树

ConcurrentHashMap 是 Java 中线程安全的哈希表实现,它在 1.8 版本中进行了一些优化和改进。

  1. Segment 数组被废弃:在之前的版本中 ConcurrentHashMap 内部维护了一个 Segment 数组,每个 Segment 维护一个 HashEntry 数组,不同的线程对于不同的 Segment 进行操作,因此在多线程下操作不同的 Segment 可以保证并发安全。但是这种方式存在一定的内存浪费,而且会导致并发性能瓶颈。1.8 版本中废弃了这种方式,采用了 CAS 和 synchronized 等方式来保证线程安全。
  2. 加入了红黑树:在 1.8 版本中,ConcurrentHashMap 对其哈希桶(bucket)中元素的数量进行了限制,当某个桶中的元素数量达到某个阈值时,ConcurrentHashMap 会将该桶中的链表转换成红黑树,这样可以提高查找、插入和删除操作的效率。

HashMap 的具体实现:HashMap 是基于哈希表实现的,即通过哈希函数将存储的 key 映射到数组的索引位置,如果出现哈希冲突,则使用链表将相同哈希值的元素串联在一起。当元素数量较大时,这种实现方式可能导致链表过长,影响查找、插入和删除的效率。在 1.8 版本中,当某个桶中的元素数量达到某个阈值时,链表会自动转换成红黑树,这样可以提高操作的效率。

红黑树和平衡二叉树的区别:红黑树是一种自平衡二叉查找树,它是一种特殊的平衡二叉树,其中每个节点要么是红色,要么是黑色,同时满足以下几个条件:

  1. 根节点是黑色的;
  2. 每个叶子节点是黑色的空节点(NIL 节点);
  3. 如果一个节点是红色的,则它的子节点必须是黑色的;
  4. 从任意一个节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。

红黑树和平衡二叉树都是为了保证二叉查找树的时间复杂度为 O(log n)。不同之处在于,红黑树对平衡性的要求相对较低,因此可以在插入和删除操作时保持对数级别的时间复杂度,并且红黑树相对于平衡二叉树来说,它的旋转次数更少,所以性能更好。

为什么不用 B+ 树:B+ 树是一种多路平衡查找树,比起 B 树来说它具有更高的查找效率和更少的磁盘 I/O 次数。但是,B+ 树的适用场景主要是数据库索引,而哈希表主要适用于内存中的数据结构,因此在内存中使用 B+ 树并不比哈希表更优秀。而且,红黑树和哈希表的实现更加简单,易于理解和编写,所以在 Java 中,HashMap 和 ConcurrentHashMap 采用了哈希表的实现方式。

为什么内存中使用 B+ 树并不比哈希表更优秀

B+ 树在磁盘的存储上有优势,这是因为 B+ 树采用了一定的预读策略来加速磁盘 I/O,可以更好地适应磁盘读写的特性,减少磁盘寻址的次数。但是,在内存中使用 B+ 树可能会面临以下问题:

  1. 内存中的数据访问通常比较快,并且不需要考虑磁盘 I/O 的特性。因此,B+ 树对于内存中的数据结构而言可能过于复杂,因为大量的查找和插入操作都可以在 O(1) 的时间内完成。
  2. 在实际的编程中,B+ 树的实现可能会比哈希表的实现更加复杂,尤其是在处理节点分裂和合并等操作时。而哈希表的实现相对简单,易于理解和调试。
  3. 哈希表支持快速的插入、删除和查找操作,几乎可以在常数时间内完成,而 B+ 树则需要遍历树结构才能找到目标节点,导致其操作复杂度可能比哈希表高一些。

7. Java 的 GC 整体过程,垃圾收集算法,流程,垃圾收集器,强引用,弱引用,虚引用等概念等,如果我要设置一个内存缓冲区,让垃圾收集器不对其进行操作怎么办

Java 的 GC(Garbage Collection,垃圾回收)是 Java 自动内存管理机制的重要组成部分。其主要目的是检测和回收那些不再被程序所引用的对象,从而释放这些对象所占用的内存空间,以便让 JVM(Java Virtual Machine,Java 虚拟机)在运行时更加高效地分配内存。Java 的 GC 过程可以分为以下几个阶段:

  1. 标记:从根对象开始扫描整个对象图,标记所有被引用的对象。
  2. 可达性分析:与标记阶段类似,找到所有存活的对象。
  3. 清除:清除所有未被标记的对象。
  4. 压缩(可选):将存活对象向一端移动,以便更好地利用空间。

Java 的垃圾收集算法主要有两种:标记-清除算法和复制算法。标记-清除算法的缺点是会产生大量的内存碎片,而复制算法则需要额外的空间来完成复制操作。因此,在实际应用中,针对不同情况可以选择适合的 GC 算法和垃圾收集器。

Java 的常见垃圾收集器有 SerialGC、ParallelGC、CMS、G1 等。SerialGC 是一种单线程的收集器,适用于小型的应用程序和客户端环境;ParallelGC 利用多线程来进行 GC,适用于大型数据中心和服务器环境;CMS 具有较短的停顿时间,适用于互联网应用等低延迟场景;G1 是一种分代的垃圾收集器,可在分区、并行处理和停顿时间之间进行折衷。不同的垃圾收集器有各自的特点和适用场景,需要根据具体的应用情况来选择。

Java 中还有强引用、软引用、弱引用和虚引用等多种引用类型。强引用是默认的引用类型,当一个对象被强引用引用时,即使内存空间不足,也不会被回收;软引用是用来描述一些还有用但并非必需的对象,当内存空间不足时,这些对象会被回收;弱引用描述的是非必需对象,但它与软引用的区别是只有在 JVM 空间不足时才会被回收;虚引用一般用来描述一个对象的生命周期,它不会对对象的生命周期产生影响,其作用主要是为了跟踪对象是否被垃圾收集器回收。

如果要设置一个内存缓冲区,让垃圾收集器不对其进行操作,可以使用以下方法:

  1. 调用 System.gc() 方法,强制触发一次垃圾收集,这时可以通过 -Xmx、-Xms 参数来设置堆的大小,从而确保缓冲区不会被垃圾收集器回收。
  2. 创建一个自定义的类,并使用 ByteBuffer.allocateDirect() 方法来分配直接内存。直接内存是不会受到 Java 堆大小的限制的,所以可以避免垃圾收集器的干扰。

在 Java 中,可以使用 ByteBuffer 类的 allocateDirect() 方法来创建直接内存缓冲区。该方法会返回一个 ByteBuffer 对象,该对象的数据存储在堆外内存中,因此垃圾收集器不会对其进行处理。您可以手动管理直接内存缓冲区的生命周期,以便更好地控制其使用和释放。

除了使用直接内存之外,还有一些其他的处理方法可以将垃圾收集器对内存缓冲区的影响降到最低。比如,可以使用局部变量或者线程池等方式来对内存缓冲区进行引用。这样可以使得内存缓冲区的生命周期与函数或线程相关联,当函数或线程执行完毕后,内存缓冲区也随之被释放。不过,需要注意的是,这种方式只适用于短时间内的内存缓冲区分配。若需要较为长时间地使用内存缓冲区,则直接内存会是更好的选择。

8. JVM 内存分区。假设 Java 的 GC 时间过长,简述应该如何做来排查解决这个问题,jamp 的 dump 命令比较重,有什么代替的方法。

在 Java 应用中,如果 GC 时间过长可能会导致程序性能下降,严重时甚至会导致应用崩溃。为了解决这个问题,可以采取以下几种方法:

  1. 调整垃圾回收策略:对于不同的应用场景,选择合适的垃圾回收策略和垃圾回收器是很重要的。可以通过调整内存分配和垃圾回收参数,来改善 GC 的性能,比如增加堆大小等。
  2. 定位内存泄漏:垃圾回收时间过长可能是由于内存泄漏导致的。可以使用 Java 内置的 jmap 工具生成堆转储文件,并使用 Eclipse Memory Analyzer 分析堆转储数据,识别内存泄漏问题。
  3. 使用压测工具:使用压测工具模拟高并发、大数据量的情况,定位出请求响应时间过长的接口或者操作,从而找到需要优化的部分。
  4. 分析 GC 日志:通过 GC 日志可以查看垃圾回收的详细情况,包括堆大小、垃圾回收次数、停顿时间等,可以根据日志找到哪些对象占用了大量内存,哪些对象被频繁地 GC 等等,从而对应用进行优化。

如果 jmap 工具的 dump 命令比较重,可以考虑使用 jstat 工具来监控 JVM 的内存和 GC 状态,比如查看堆大小、垃圾回收次数和停顿时间等信息。如果仍然需要进行内存分析,可以使用第三方工具,比如 VisualVM、JProfiler 等。这些工具可以提供更加全面的内存分析功能,帮助找出内存泄漏等问题,并提供相应的解决方法。

9. 代码实现每五分钟一次 Minor GC,如果要 FullGC 呢

在 Java 应用程序中,垃圾回收机制的触发与具体实现方式相关,可以通过设置 JVM 的参数来实现。在实现每五分钟一次 Minor GC 的功能时,可以使用以下代码示例:

public class MinorGCDemo {

    public static void main(String[] args) {
        // 设置每五分钟进行一次 Minor GC
        long gcInterval = 5 * 60 * 1000;
        long lastGCTime = System.currentTimeMillis();

        while (true) {
            // do something
            // ...

            // 判断是否需要进行 Minor GC
            if (System.currentTimeMillis() - lastGCTime >= gcInterval) {
                System.gc();  // 进行 Minor GC
                lastGCTime = System.currentTimeMillis();
            }
        }
    }
}

在以上代码中,我们首先定义了一个 gcInterval 变量,表示每隔多少时间进行一次 Minor GC,这里设置为 5 分钟。然后,在主循环中,我们执行了一些操作,然后判断当前时间与上一次 Minor GC 的时间间隔是否超过了 gcInterval,如果是,则调用 System.gc() 方法手动触发一次 Minor GC,最后更新 lastGCTime 的值。

需要注意的是,手动触发垃圾回收机制可能会对应用程序的性能产生一定的影响,因此应该谨慎使用。同时,在实际编写代码时,应该遵循“减少创建对象,优化程序性能”的原则,尽量减少垃圾回收的次数。

在 Java 中,进行 Minor GC 的频率和 Full GC 的触发机制都是由 JVM 自动管理的,但是可以通过调整一些参数来间接地控制 GC 的行为。

如果要在 Java 应用中设置 Full GC 的触发时间,可以使用 JVM 提供的 -XX:+UseG1GC 参数来启用 G1 垃圾收集器,并设置 -XX:MaxGCPauseMillis 参数指定期望的 GC 周期时间,这个参数表示垃圾回收所允许的最大停顿时间,当达到该时间时,就会触发 Full GC。

具体而言,需要在启动应用程序时添加如下 JVM 参数:

-XX:+UseG1GC -XX:MaxGCPauseMillis=60000

这里将 MaxGCPauseMillis 设置为 60000,即期望每次 Full GC 的最长停顿时间为一分钟。JVM 会根据实际情况来调整垃圾回收的时间,确保内存正常运行并达到期望的吞吐量。

需要注意的是,Full GC 会导致整个 JVM 程序停止,性能开销比较大。因此,建议根据具体情况慎重设置 Full GC 的触发时间,只在必要时才进行 Full GC。同时,在代码编写中也要注意避免产生过多的内存对象和内存泄漏等问题。

在 Java 中,内存分为两个区域:年轻代(Young Generation)和老年代(Old Generation)。年轻代又分为 Eden 区和两个 Survivor 区。

Minor GC(Minor Garbage Collection)指的是针对年轻代内存区域进行的垃圾回收操作。当年轻代内存区域的空间不足时,就会触发 Minor GC 操作。

在 Minor GC 过程中,首先会对 Eden 区进行垃圾回收,将不再使用的对象清理出去,然后将存活的对象移动到一个 Survivor 区中,并且按照年龄从小到大进行排序。Survivor 区主要用来保存年龄较小的对象,经过一定数量的 Minor GC 后,任何一个 Survivor 区中存活的对象都会被移动到另一个 Survivor 区中,并且年龄加 1。而如果对象经过多次 Minor GC 还没有被清理掉,那么它就会被移动到老年代中。

由于年轻代中的对象生命周期比较短,因此 Minor GC 的频率通常比较高,这也是减少 Full GC 的压力,提高系统性能的有效手段之一。

需要注意的是,虽然 Minor GC 的效率比 Full GC 要高,但是也会导致一定的停顿时间,因此在编写代码的时候,应该尽量避免产生大量的临时对象,减少对年轻代内存的占用,从而减少 Minor GC 的频率,提高应用程序的性能。

在 Java 中,老年代(Old Generation)是用来存放长时间存活的对象的内存区域,一般情况下,这些对象都是由年轻代晋升到老年代的。与年轻代不同,老年代中的垃圾回收通常采用标记-清除算法(Mark-Sweep)和标记-整理算法(Mark-Compact)。

  1. 标记-清除算法(Mark-Sweep):

在标记-清除算法中,首先需要对堆中的所有对象进行标记,标记出哪些对象是存活的,哪些对象是可以被回收的。然后,对未被标记的对象进行清理,释放它们所占用的内存空间。标记-清除算法的优点在于简单、快速,但是会产生内存碎片问题。

  1. 标记-整理算法(Mark-Compact):

在标记-整理算法中,也需要对所有的对象进行标记,标记出哪些对象是存活的,哪些对象可以被回收。不同的是,在清理阶段,回收器会将所有存活的对象向一端移动,然后清理掉其它部分的空间,这样就避免了内存碎片的问题。标记-整理算法的优点在于可解决内存碎片问题,但是清理过程相对较慢。

需要注意的是,老年代中的垃圾回收频率较低,因此垃圾回收过程对系统造成的停顿时间也会更长。同时,老年代中的对象往往比较大,因此进行垃圾回收时,需要考虑堆内存的容量和回收算法的效率,有效地控制垃圾回收的次数和时间。

10. ThreadLocal 的具体是怎样的,为什么会有内存泄漏问题,怎样避免

ThreadLocal 是 Java 中一个用于实现线程本地存储的工具类,可以让每个线程拥有自己独立的变量副本。它通过在每个线程中创建一个独立的变量副本来解决多线程访问同一变量时的并发问题。

ThreadLocal 变量通常是 private static 类型,在需要使用时调用 get() 方法获取变量的值,在修改后需要调用 set() 方法重新设置变量的值,使用完成后需要调用 remove() 方法移除变量的副本。因为 ThreadLocal 存储的变量是每个线程独立的,所以在不同线程之间访问时不存在线程安全问题。

ThreadLocal 内存泄漏问题通常出现在长期运行的应用程序中,如果不及时清理 ThreadLocal 中的变量,可能会导致内存泄漏问题。具体原因是:由于 ThreadLocalMap 中的 Entry 对象是弱引用,而 ThreadLocalMap 自身是强引用,当某个 ThreadLocal 的作用域结束时,如果没有手动删除 ThreadLocal 变量,在下一次垃圾收集时,ThreadLocal 仍然存在于 ThreadLocalMap 的 table 数组中,导致 Entry 对象无法被回收,从而引发内存泄漏问题。

避免 ThreadLocal 内存泄漏问题的方法可以采用手动调用 remove() 方法或者使用 try-finally 块来确保在 ThreadLocal 变量使用结束后及时清理它。具体示例如下:

public class Example {
    private static final ThreadLocal<DataObject> tl = new ThreadLocal<>();

    public void doSomething() {
        try {
            tl.set(new DataObject());
            // 使用 tl.get() 获取 ThreadLocal 变量
            // 处理业务逻辑...
        } finally {
            tl.remove(); // 在 finally 块中删除 ThreadLocal 变量
        }
    }
}

上述示例代码中,在业务处理过程中使用 try-finally 块,确保在 finally 块中调用 remove() 方法清理 ThreadLocal 变量。另外,还可以通过定期清理 ThreadLocal 变量的方式来避免内存泄漏问题,例如在 Servlet 过滤器中注册监听器,定时清理 ThreadLocalMap 中的无效 Entry 对象。需要注意的是,定期清理 ThreadLocal 变量也要权衡清理的频率和性能消耗。

11. Java 的异常种类有哪些,平时自己是怎么处理异常的

Java 中的异常主要分为两大类:Checked Exception 和 Unchecked Exception。其中,Checked Exception 是在代码编译期间就必须处理的异常,例如 IOException、ClassNotFoundException 等;而 Unchecked Exception 则是在程序运行时才会出现的异常,如 NullPointerException、ArrayIndexOutOfBoundsException 等,通常不需要强制进行异常处理。

除此之外,Java 中还有 Error 类型的异常,它表示严重的系统错误,如 OutOfMemoryError、StackOverflowError 等,一般也不需要进行异常处理。

在平时编写代码时,我处理异常的方式通常分为以下几个步骤:

  1. 捕获异常:使用 try-catch 块捕获可能抛出的异常。

  2. 处理异常:根据具体情况对异常进行相应的处理,如打印日志、返回默认值、重新抛出异常等。

  3. 清理资源:在 finally 块中释放资源,确保程序正常结束。

12. Spring 的 IOC 和 AOP 及 MVC 机制,Sping 中的单例 bean 是否可以依赖多例 bean

  1. IOC(Inverse of Control,控制反转):IOC 是指将对象的创建、组装、管理等过程交给 Spring 容器来处理,通过依赖注入(Dependency Injection,DI)方式将被依赖的对象注入到需要它们的对象中。这样可以将对象之间的耦合度降低,方便维护和测试代码。

  2. AOP(Aspect Oriented Programming,面向切面编程):AOP 是指将程序中经常使用的功能或行为进行抽象,然后通过切面(Aspect)的方式在不修改原有代码的情况下进行功能的添加、修改或删除操作。比如,可以在不修改业务逻辑代码的情况下,通过 AOP 在方法执行前或执行后添加日志记录、权限校验等功能。

  3. MVC(Model-View-Controller,模型-视图-控制器):MVC 是一种设计模式,将应用程序分为三个部分:模型、视图和控制器。模型用于封装数据和业务逻辑,视图负责展现数据,控制器负责协调模型和视图,处理用户请求,更新模型数据等。

可以通过将多例 bean 注入到单例 bean 中,而不是让单例 bean 直接依赖多例 bean 来解决这个问题。可以在单例 bean 中使用工厂方法来获取多例 bean 的实例,这样每次都会创建一个新的多例 bean。例如:

@Component
public class SingletonBean {
    private PrototypeBean prototypeBean;

    @Autowired
    private ApplicationContext context;

    public void doSomething() {
        prototypeBean = context.getBean(PrototypeBean.class);
        System.out.println(prototypeBean.getData());
    }
}

@Component
@Scope("prototype")
public class PrototypeBean {
    private String data;
 
    public String getData() {
        return data;
    }
 
    public void setData(String data) {
        this.data = data;
    }
}

在上述示例代码中,SingletonBean 使用了 ApplicationContext 来获取 PrototypeBean 的实例,每次调用 doSomething() 方法时都会创建一个新的 PrototypeBean 实例,并输出其数据。

13. Springboot 起步依赖有什么好处,为什么使用 MyBatis 不适用数据库连接池

  1. 方便配置管理:Spring Boot 起步依赖为我们预先配置了许多通用的依赖项,如日志、缓存、数据库连接池等。这样可以让我们更加专注于应用程序的业务逻辑,而不是底层框架的配置。

  2. 简化项目结构:Spring Boot 起步依赖将常用的库集成在一起,维护起来更加方便,也可以简化项目结构。

  3. 提高开发效率:Spring Boot 起步依赖可以帮助我们快速启动应用程序,减少开发时间和成本。

在实际开发中,建议使用 MyBatis 和数据库连接池一起使用。这样可以避免频繁打开和关闭数据库连接所带来的额外开销,同时提高数据库资源的利用率和应用程序的响应速度。

一些常用的数据库连接池有 HikariCP、Druid、C3P0 等,它们都能够与 MyBatis 集成使用。当然,在使用数据库连接池的时候也需要注意一些配置参数,如最小连接数、最大连接数等,以达到最优的性能和可靠性。

14. tomcat 如果有两个项目,两个项目里面如果有相同的 class,那么 tomcat 是如何对其进行区别

每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。

当应用需要到某个类时,则会按照下面的顺序进行类加载

1 使用bootstrap引导类加载器加载

2 使用system系统类加载器加载

3 使用应用类加载器在WEB-INF/classes中加载

4 使用应用类加载器在WEB-INF/lib中加载

5 使用common类加载器在CATALINA_HOME/lib中加载

15. 简述 MySQL 的 ACID 性质及实现,ACID 有哪些一致性种类,乐观锁怎么实现的,简述 MySQL 的隔离机制及实现,简述 MySQL 索引结构及实现

MySQL 的 ACID 性质是指:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),它们分别表示数据库事务应该具有的四个特性。

  • 原子性(Atomicity):表示一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,即使在发生崩溃时也不能将其拆分为更小的部分。
  • 一致性(Consistency):表示一个事务执行前后,数据库中的数据必须保持一致性状态。这意味着一个事务执行完毕后,数据库所处于的状态与该事务执行前的状态相同。
  • 隔离性(Isolation):表示多个事务并发执行时,每个事务都应该像独立执行一样,并且不能互相影响;因此,在并发执行的事务之间需要进行隔离,以防止相互干扰产生的问题。
  • 持久性(Durability):表示一个事务完成后,它对数据库中的数据的修改应该是永久性的,即使发生了系统崩溃或停电等异常情况,也不应该丢失数据。

MySQL 实现 ACID 的方式是通过日志和锁机制实现。在事务执行过程中,MySQL 会记录所有的修改操作,即使服务器崩溃也能够通过重做日志来恢复数据。同时,MySQL 的锁机制可以保证并发访问下的数据一致性和隔离性。

ACID 一致性种类包括:强一致性、弱一致性、最终一致性、单调一致性和顺序一致性。其中,强一致性是指任何时刻读取的数据都是最新的、正确的;弱一致性、最终一致性、单调一致性和顺序一致性都是针对分布式系统而言的,其具体定义和实现方式略有不同。

乐观锁是一种并发控制的技术,它适用于读多写少的场景。乐观锁的实现方式是在数据库表中增加一个版本(或时间戳)字段,每次更新时将该字段加一,当多个事务同时更新同一行数据时,只有一个事务可以成功,其他事务需要重新执行,直到更新成功为止。

MySQL 的隔离机制有以下四个级别:

  • 读未提交(Read Uncommitted):一个事务可以读取另一个未提交事务的数据。
  • 读已提交(Read Committed):一个事务只能读取已经提交的事务数据,避免了脏读情况。
  • 可重复读(Repeatable Read):一个事务不会被其他事务所修改的数据,保证了在事务执行期间多次读取同一份数据时,返回的结果是相同的。
  • 串行化(Serializable):强制事务串行执行,避免任何并发问题。

MySQL 的索引结构包括 B+ 树和哈希表。其中,B+ 树是 MySQL 最常用的索引类型,它可以支持范围查询和排序操作,并且在插入、删除和查询时都有很好的性能表现。实现方式是在每个节点上维护一个键值对列表,按照 key 值排序,并通过指针连接起来形成一棵树。而哈希表适合用于精确查找,它的查询效率非常高,但在排序和范围查询方面效果较差。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值