前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >阿里太狠了,把人问蒙了

阿里太狠了,把人问蒙了

作者头像
小林coding
发布2024-05-17 18:56:56
1230
发布2024-05-17 18:56:56
举报
文章被收录于专栏:小林coding小林coding

图解学习网站:https://xiaolincoding.com

大家好,我是小林。

在互联网大厂Java后端面试中,阿里巴巴算是里面难度比较高的,面试都对Java 技术特别熟,所以问Java 都会比较深。

可能整场面试 80%的内容,都是深挖 Java 的技术问题,而计算机基础可能都不怎么问,跟字节和腾讯的风格刚好反过来。

今天就给大家分享一个阿里的Java 后端面经,基本全都是问 Java 了,基础->并发->虚拟机->Spring 占比面试 80 %,剩下 20% 是后端组件。

比如消息队列、mysql、redis,快接近 40 个问题了,真可以把人问蒙,然后再来写算法,压力确实大

Java虚拟机

JVM的内存区域和作用

根据 JVM8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。

JVM的内存结构主要分为以下几个部分:

  • 元空间:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
  • Java 虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。
  • 本地方法栈:与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。
  • 程序计数器:程序计数器可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。
  • 堆内存:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在队中。
  • 直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

常见的GC算法有哪些?。

Java的内存回收机制基于自动内存管理,开发人员无需手动释放内存。垃圾回收器会自动识别不再使用的对象,并回收它们所占用的内存空间。垃圾回收算法主要有 :

  • 标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
  • 复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。
  • 标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
  • 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

JVM中有哪些回收器?

  • Serial 收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
  • ParNew 收集器,ParNew 收集器其实就是 Serial 收集器的多线程版本。
  • Parallel 收集器,Parallel Scavenge 收集器类似 ParNew 收集器,Parallel 收集器更关注系统的吞吐量。
  • Parallel Old 收集器,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法
  • CMS 收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
  • G1 收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征

G1回收器的特色是什么?

G1 的特点:

  • G1最大的特点是引入分区的思路,弱化了分代的概念。
  • 合理利用垃圾收集各个周期的资源,解决了其他收集器、甚至 CMS 的众多缺陷

G1 相比较 CMS 的改进:

  • 算法:G1 基于标记--整理算法, 不会产生空间碎片,在分配大对象时,不会因无法得到连续的空间,而提前触发一次 FULL GC 。
  • 停顿时间可控:G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。
  • 并行与并发:G1 能更充分的利用 CPU 多核环境下的硬件优势,来缩短 stop the world 的停顿时间。

GC只会对堆进行GC吗?

JVM 的垃圾回收器不仅仅会对堆进行垃圾回收,它还会对方法区进行垃圾回收。

  1. 堆(Heap): 堆是用于存储对象实例的内存区域。大部分的垃圾回收工作都发生在堆上,因为大多数对象都会被分配在堆上,而垃圾回收的重点通常也是回收堆中不再被引用的对象,以释放内存空间。
  2. 方法区(Method Area): 方法区是用于存储类信息、常量、静态变量等数据的区域。虽然方法区中的垃圾回收与堆有所不同,但是同样存在对不再需要的常量、无用的类信息等进行清理的过程。

Java并发

你有哪些解决线程并发问题的方案?

  • 当只有一个线程写,其它线程都是读的时候,可以用volatile修饰变量
  • 当多个线程写,那么一般情况下并发不严重的话可以用Synchronized,Synchronized并不是一开始就是重量级锁,在并发不严重的时候,比如只有一个线程访问的时候,是偏向锁;当多个线程访问,但不是同时访问,这时候锁升级为轻量级锁;当多个线程同时访问,这时候升级为重量级锁。所以在并发不是很严重的情况下,使用Synchronized是可以的。不过Synchronized有局限性,比如不能设置锁超时,不能通过代码释放锁。ReentranLock 可以通过代码释放锁,可以设置锁超时。
  • 高并发下,Synchronized、ReentranLock 效率低,因为同一时刻只有一个线程能进入同步代码块,如果同时有很多线程访问,那么其它线程就都在等待锁。这个时候可以使用并发包下的数据结构,例如ConcurrentHashMap,LinkBlockingQueue,以及原子性的数据结构如:AtomicInteger。

悲观锁和乐观锁的区别。

  • 乐观锁:乐观锁假设多个事务之间很少发生冲突,因此在读取数据时不会加锁,而是在更新数据时检查数据的版本(如使用版本号或时间戳),如果版本匹配则执行更新操作,否则认为发生了冲突。
  • 悲观锁:悲观锁假设多个事务之间会频繁发生冲突,因此在读取数据时会加锁,防止其他事务对数据进行修改,直到当前事务完成操作后才释放锁。

那悲观锁和乐观锁使用场景的差别是什么?

  • 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总 是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
  • 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总 是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。

那Java中想实现一个乐观锁,都有哪些方式?

  1. CAS(Compare and Swap)操作: CAS 是乐观锁的基础。Java 提供了 java.util.concurrent.atomic 包,包含各种原子变量类(如 AtomicInteger、AtomicLong),这些类使用 CAS 操作实现了线程安全的原子操作,可以用来实现乐观锁。
  2. 版本号控制:增加一个版本号字段记录数据更新时候的版本,每次更新时递增版本号。在更新数据时,同时比较版本号,若当前版本号和更新前获取的版本号一致,则更新成功,否则失败。
  3. 时间戳:使用时间戳记录数据的更新时间,在更新数据时,在比较时间戳。如果当前时间戳大于数据的时间戳,则说明数据已经被其他线程更新,更新失败。

使用时间戳会不会有可见性问题?

会有可见性的问题,可见性问题指的是当一个线程修改了共享变量的值后,其他线程可能无法立即看到这个变化。

如果一个线程在读取数据的时间戳之后,另一个线程修改了数据并更新了时间戳,第一个线程可能无法感知到第二个线程的修改。这导致第一个线程在接下来的操作中基于过时的时间戳做出了错误的判断,从而可能发生数据不一致的情况。

为了避免时间戳导致的可见性问题,可以考虑使用 volatile关键字: 在时间戳字段上使用 volatile 关键字可以确保多线程之间的可见性,可以及时看到其他线程对共享变量的修改。

volatile能解决吗,就够了吗?

volatile 关键字仅仅保证了可见性,并没有提供原子性。在实现乐观锁时,如果仅仅依赖 volatile 关键字来保证时间戳的可见性,仍然可能面临以下问题:

  • 复合操作可能存在问题: 如果对时间戳字段的更新是一个复合操作,如读取时间戳、计算新值、更新时间戳,这样的操作并不具备原子性。在这种情况下,仅仅使用 volatile 是无法保证操作的原子性和线程安全的。

可以通过加锁的方式来保证对时间戳读写操作的并发安全。

除了加锁还有没有别的解法,绕开加锁使性能更好?

可以改用 Atomic 类来保证原子性,这些原子类是使用 CAS (Compare and Swap) 操作来保证数据的原子性。如 AtomicInteger、AtomicLong、AtomicReference 等,通过这些原子类可以实现 CAS 操作,从而确保对共享变量的原子操作。

synchronized 和 ReentrantLock 的区别

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

  • ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
  • ReentrantLock 可以获取各种锁的信息
  • ReentrantLock 可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word,这点我不能确定。

讲一讲ThreadLocal使用的时候需要注意哪些点。

ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。

  • set 给ThreadLocalMap设置值。
  • get 获取ThreadLocalMap。
  • remove 删除ThreadLocalMap类型的对象。

存在的问题

  1. 对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,造成一系列问题。
  2. 内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。

使用完ThreadLocal后,及时调用remove()方法释放内存空间,这样就能避免内存泄漏。

线程并发还有别的问题吗?

Java的线程安全在三个方面体现

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic和synchronized这两个关键字来确保原子性;
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,在Java中使用了synchronized和volatile这两个关键字确保可见性;
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。

常用的线程池有哪些呢?

  • ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。
  • FixedThreadPool:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。
  • CachedThreadPool:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。
  • SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。
  • SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。

简单讲一讲线程的生命周期

图片 线程池的五个状态如下

  • RUNNING: 接收新的任务,并能继续处理 workQueue 中的任务
  • SHUTDOWN: 不再接收新的任务,不过能继续处理 workQueue 中的任务
  • STOP: 不再接收新的任务,也不再处理 workQueue 中的任务,并且会中断正在处理任务的线程
  • TIDYING: 所有的任务都完结了,并且线程数量(workCount)为 0 时即为此状态,进入此状态后会调用 terminated() 这个钩子方法进入 TERMINATED 状态
  • TERMINATED: 调用 terminated() 方法后即为此状态

有什么办法能够提升Java线程的并发能力呢?

  • 使用线程池:合理配置线程池大小,避免频繁创建和销毁线程,提高线程的复用率,减少资源开销。
  • 使用并发集合:Java 并发包中提供了诸如 ConcurrentHashMap、ConcurrentLinkedQueue 等线程安全的集合类,使用这些并发集合可以减少在多线程环境下的锁竞争,提升并发性能
  • 避免锁粒度过细:过细的锁粒度会增加锁的竞争,影响并发性能,尽量使用合适的锁粒度来避免锁的争用。
  • 减少同步区域代码量:在同步代码块中尽量减少耗时操作,保持同步区域的代码量尽量少,以减少线程等待时间,提高并发性能。
  • 使用乐观锁:乐观锁机制可以提高并发性能,通过版本号或时间戳等方式,避免使用传统的悲观锁机制,减少不必要的阻塞。
  • 使用无锁算法:无锁的数据结构和算法可以减少锁的开销和线程之间的竞争,如CAS操作、原子类等。

Java基础

NIO了解吗?

  • BIO(blocking IO):就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
  • NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
  • AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

你知道有哪个框架用到NIO了吗?

Netty。

Netty 的 I/O 模型是基于非阻塞 I/O 实现的,底层依赖的是 NIO 框架的多路复用器 Selector。采用 epoll 模式后,只需要一个线程负责 Selector 的轮询。当有数据处于就绪状态后,需要一个事件分发器(Event Dispather),它负责将读写事件分发给对应的读写事件处理器(Event Handler)。事件分发器有两种设计模式:Reactor 和 Proactor,Reactor 采用同步 I/O, Proactor 采用异步 I/O。

Reactor 实现相对简单,适合处理耗时短的场景,对于耗时长的 I/O 操作容易造成阻塞。Proactor 性能更高,但是实现逻辑非常复杂,适合图片或视频流分析服务器,目前主流的事件驱动模型还是依赖 select 或 epoll 来实现。

讲一讲String、StringBuffer和StringBuilder的区别。

区别:

  • String 是 Java 中基础且重要的类,被声明为 final class,是不可变字符串。因为它的不可变性,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
  • StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类。它提供了 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列。在很多情况下我们的字符串拼接操作不需要线程安全,所以 StringBuilder 登场了。
  • StringBuilder 是 JDK1.5 发布的,它和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。

线程安全:

  • StringBuffer:线程安全
  • StringBuilder:线程不安全

速度:

  • 一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。

使用场景:

  • 操作少量的数据使用 String。
  • 单线程操作大量数据使用 StringBuilder。
  • 多线程操作大量数据使用 StringBuffer。

能讲一讲Java注解的原理吗?

注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。

我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。

Java注解的作用域呢?

注解的作用域(Scope)指的是注解可以应用在哪些程序元素上,例如类、方法、字段等。Java注解的作用域可以分为三种:

  1. 类级别作用域:用于描述类的注解,通常放置在类定义的上面,可以用来指定类的一些属性,如类的访问级别、继承关系、注释等。
  2. 方法级别作用域:用于描述方法的注解,通常放置在方法定义的上面,可以用来指定方法的一些属性,如方法的访问级别、返回值类型、异常类型、注释等。
  3. 字段级别作用域:用于描述字段的注解,通常放置在字段定义的上面,可以用来指定字段的一些属性,如字段的访问级别、默认值、注释等。

除了这三种作用域,Java还提供了其他一些注解作用域,例如构造函数作用域和局部变量作用域。这些注解作用域可以用来对构造函数和局部变量进行描述和注释。

Spring

Spring Bean的scope有哪些?

Bean 的 scope(作用域)定义了 Bean 实例的生命周期及可见范围,常见的 Bean scope:

  • Singleton(单例):在整个应用程序中只存在一个 Bean 实例。默认作用域,Spring 容器中只会创建一个 Bean 实例,并在容器的整个生命周期中共享该实例。
  • Prototype(原型):每次请求时都会创建一个新的 Bean 实例。次从容器中获取该 Bean 时都会创建一个新实例,适用于状态非常瞬时的 Bean。
  • Request(请求):每个 HTTP 请求都会创建一个新的 Bean 实例。仅在 Spring Web 应用程序中有效,每个 HTTP 请求都会创建一个新的 Bean 实例,适用于 Web 应用中需求局部性的 Bean。
  • Session(会话):Session 范围内只会创建一个 Bean 实例。该 Bean 实例在用户会话范围内共享,仅在 Spring Web 应用程序中有效,适用于与用户会话相关的 Bean。
  • Application:当前 ServletContext 中只存在一个 Bean 实例。仅在 Spring Web 应用程序中有效,该 Bean 实例在整个 ServletContext 范围内共享,适用于应用程序范围内共享的 Bean。
  • WebSocket(Web套接字):在 WebSocket 范围内只存在一个 Bean 实例。仅在支持 WebSocket 的应用程序中有效,该 Bean 实例在 WebSocket 会话范围内共享,适用于 WebSocket 会话范围内共享的 Bean。
  • Custom scopes(自定义作用域):Spring 允许开发者定义自定义的作用域,通过实现 Scope 接口来创建新的 Bean 作用域。

请讲一讲你了解的AOP的实现方式?

SpringAOP实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而SpringAOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理

  • 基于JDK的动态代理Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java语言的应该会对JDK动态代理有所了解。JDK实现动态代理需要两个组件,首先第一个就是InvocationHandler接口。我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。然后JDK动态代理需要使用的第二个组件就是Proxy这个类,我们可以通过这个类的newProxyInstance方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke方法。
  • 基于CGLIB的动态代理JDK的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK的动态代理将没有办法使用,于是Spring会使用CGLib的动态代理来生成代理对象。CGLib直接操作字节码,生成类的子类,重写类的方法完成代理。

除了JDK Proxy和CGLib还有别的实现AOP的方式么?

还有静态代理。

静态代理:通过 AOP 框架提供的命令进行编译,从而在编译阶段生成 AOP 代理类。这种方式也被称为编译时增强。静态代理包括编译时编织和类加载时编织两种方式。

讲一讲你对Spring Boot的理解,以及为什么要用Spring Boot?

在使用Spring框架进行开发的过程中,需要配置很多Spring框架包的依赖,如spring-core、spring-bean、spring-context等,而这些配置通常都是重复添加的,而且需要做很多框架使用及环境参数的重复配置,如开启注解、配置日志等。

Spring Boot致力于弱化这些不必要的操作,提供默认配置,当然这些默认配置是可以按需修改的,快速搭建、开发和运行Spring应用。

Spring Boot 的主要优点:

  1. 简化开发:Spring Boot通过约定大于配置的原则和自动化配置,大大简化了Java应用程序的开发和部署过程。开发者无需处理繁琐的配置,可以快速搭建项目并专注于业务逻辑的实现。
  2. 集成性强:Spring Boot提供了大量的起步依赖,涵盖了各种常见的库、框架和组件,使得集成第三方库和服务变得更加容易。同时,Spring Boot内置了大量的功能,如内置容器、安全性、监控等,可以快速集成这些功能到项目中。
  3. 独立运行:Spring Boot应用程序可以独立运行,内嵌了常用的Web服务器,如Tomcat、Jetty等,使得部署变得简单。开发者无需额外配置Web服务器,只需执行一个可执行的JAR文件即可启动应用。
  4. 微服务友好:Spring Boot非常适合构建微服务架构,其轻量级、模块化的特性使得微服务之间的通信和部署变得更加方便。结合Spring Cloud等微服务组件,可以更好地实现微服务架构下的各种功能。

请讲一讲Spring Boot简化配置具体是如何简化的?

Spring 虽然使Java EE轻量级框架,但由于其繁琐的配置,一度被人认为是“配置地狱”。各种XML、Annotation配置会让人眼花缭乱,而且配置多的话,如果出错了也很难找出原因。Spring Boot更多的是采用 Java Config 的方式,对 Spring 进行配置。

举个例子:我新建一个类,但是我不用 @Service注解,也就是说,它是个普通的类,那么我们如何使它也成为一个 Bean 让 Spring 去管理呢?只需要@Configuration 和@Bean两个注解即可,如下:

代码语言:javascript
复制
public class TestService {
    public String sayHello () {
        return "Hello Spring Boot!";
    }
}
代码语言:javascript
复制
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JavaConfig {
    @Bean
    public TestService getTestService() {
        return new TestService();
    }
}

@Configuration表示该类是个配置类,@Bean表示该方法返回一个 Bean。这样就把TestService作为 Bean 让 Spring 去管理了,在其他地方,我们如果需要使用该 Bean,和原来一样,直接使用@Resource注解注入进来即可使用,非常方便。

代码语言:javascript
复制
@Resource
private TestService testService;

另外,部署配置方面,原来 Spring 有多个 xml 和 properties配置,在 Spring Boot 中只需要个 application.yml即可。

Spring Boot是通过什么实现的约定大于配置?

Spring Boot通过「自动化配置」和「起步依赖」实现了约定大于配置的特性。

  • 自动化配置:Spring Boot根据项目的依赖和环境自动配置应用程序,无需手动配置大量的XML或Java配置文件。例如,如果项目引入了Spring Web MVC依赖,Spring Boot会自动配置一个基本的Web应用程序上下文。
  • 起步依赖:Spring Boot提供了一系列起步依赖,这些依赖包含了常用的框架和功能,可以帮助开发者快速搭建项目。通过引入适合项目需求的起步依赖,开发者可以快速构建出符合要求的应用程序,减少了配置的复杂度

后端

是否了解消息队列呢?

了解过,常见的消息队列有RabbitMQ、Kafka、RocketMQ。消息队列三大作用:

  • 解耦:可以在多个系统之间进行解耦,将原本通过网络之间的调用的方式改为使用MQ进行消息的异步通讯,只要该操作不是需要同步的,就可以改为使用MQ进行不同系统之间的联系,这样项目之间不会存在耦合,系统之间不会产生太大的影响,就算一个系统挂了,也只是消息挤压在MQ里面没人进行消费而已,不会对其他的系统产生影响。
  • 异步:加入一个操作设计到好几个步骤,这些步骤之间不需要同步完成,比如客户去创建了一个订单,还要去客户轨迹系统添加一条轨迹、去库存系统更新库存、去客户系统修改客户的状态等等。这样如果这个系统都直接进行调用,那么将会产生大量的时间,这样对于客户是无法接收的;并且像添加客户轨迹这种操作是不需要去同步操作的,如果使用MQ将客户创建订单时,将后面的轨迹、库存、状态等信息的更新全都放到MQ里面然后去异步操作,这样就可加快系统的访问速度,提供更好的客户体验。
  • 削峰:一个系统访问流量有高峰时期,也有低峰时期,比如说,中午整点有一个抢购活动等等。比如系统平时流量并不高,一秒钟只有100多个并发请求,系统处理没有任何压力,一切风平浪静,到了某个抢购活动时间,系统并发访问了剧增,比如达到了每秒5000个并发请求,而我们的系统每秒只能处理2000个请求,那么由于流量太大,我们的系统、数据库可能就会崩溃。这时如果使用MQ进行流量削峰,将用户的大量消息直接放到MQ里面,然后我们的系统去按自己的最大消费能力去消费这些消息,就可以保证系统的稳定,只是可能要跟进业务逻辑,给用户返回特定页面或者稍后通过其他方式通知其结果。

缓存系统有哪些,了解吗?

主要了解过Redis

Redis为什么性能这么好的原因,你了解吗?

官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:

之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:

  • Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
  • Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
  • Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

简单说一下索引是越多越好吗?

不是的,索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:

  • 需要占用物理空间,数量越大,占用空间越大;
  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
  • 会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。

所以,索引不是万能钥匙,它也是根据场景来使用的。

请说一下你对视图的理解。

  1. 视图是虚表,没有行,也没有列,更没有数据,只是通过一个查询语句构建的虚拟的表结构,具有和真实表类似的功能,但是每次使用视图,都会执行构建虚表的查询语句,所以查询的速度比较慢,我们查询一个商品的订单量,首先是一个复杂的嵌套查询,本身编写起来比较复杂,而且每个月查询一次,对查询效率的要求也不高,那么在这种场景下,使用视图先构建出来一个契合业务的表,然后基于这个表查询会方便很多。
  2. 视图本身是基于其他表的查询,所以可以用视图作为隔离,返回一张过滤后的表,这样对数据安全也是有好处的,订单表本身含有的信息比较多,在上面的这个需求当中实际上使用到的订单表的字段并不多,或者说只是数量统计,那么这个时候采用视图表,访问视图表会和原来的订单表和商品表形成隔离,也有了一定的安全意义。
  3. 视图更像是编程过程当中的函数,提高了代码的复用程度,便于sql语句的维护。

如果发现一个SQL执行的比较慢怎么办?

  • 通过 explain 执行结果,查看 sql 是否走索引,如果不走索引,考虑增加索引。
  • 可以通过建立联合索引,实现覆盖索引优化,减少回表,但是使用联合索引要符合最左匹配原则,不然会索引失效
  • 避免索引失效,比如不要用左模糊匹配、函数计算、表达式计算等等。
  • 联表查询最好要以小表驱动大表,并且被驱动表的字段要有索引,当然最好通过冗余字段的设计,避免联表查询。
  • 针对 limit n,y 深分页的查询优化,可以把Limit查询转换成某个位置的查询:select * from tb_sku where id>20000 limit 10;,该方案适用于主键自增的表,
  • 将字段多的表分解成多个表,有些字段使用频率高,有些低,数据量大时,会由于使用频率低的存在而变慢,可以考虑分开

如果Explain用到的索引不正确的话,有什么办法干预吗?

可以使用 force index,强制走索引。

比如:

代码语言:javascript
复制
EXPLAIN SELECT 
    productName, buyPrice
FROM
    products 
FORCE INDEX (idx_buyprice)
WHERE
    buyPrice BETWEEN 10 AND 80
ORDER BY buyPrice; 

输出:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-05-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小林coding 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java虚拟机
    • JVM的内存区域和作用
      • 常见的GC算法有哪些?。
        • JVM中有哪些回收器?
          • G1回收器的特色是什么?
            • GC只会对堆进行GC吗?
            • Java并发
              • 你有哪些解决线程并发问题的方案?
                • 悲观锁和乐观锁的区别。
                  • 那悲观锁和乐观锁使用场景的差别是什么?
                    • 那Java中想实现一个乐观锁,都有哪些方式?
                      • 使用时间戳会不会有可见性问题?
                        • volatile能解决吗,就够了吗?
                          • 除了加锁还有没有别的解法,绕开加锁使性能更好?
                            • synchronized 和 ReentrantLock 的区别
                              • 讲一讲ThreadLocal使用的时候需要注意哪些点。
                                • 线程并发还有别的问题吗?
                                  • 常用的线程池有哪些呢?
                                    • 简单讲一讲线程的生命周期
                                      • 有什么办法能够提升Java线程的并发能力呢?
                                      • Java基础
                                        • NIO了解吗?
                                          • 你知道有哪个框架用到NIO了吗?
                                            • 讲一讲String、StringBuffer和StringBuilder的区别。
                                              • 能讲一讲Java注解的原理吗?
                                                • Java注解的作用域呢?
                                                • Spring
                                                  • Spring Bean的scope有哪些?
                                                    • 请讲一讲你了解的AOP的实现方式?
                                                      • 除了JDK Proxy和CGLib还有别的实现AOP的方式么?
                                                        • 讲一讲你对Spring Boot的理解,以及为什么要用Spring Boot?
                                                          • 请讲一讲Spring Boot简化配置具体是如何简化的?
                                                            • Spring Boot是通过什么实现的约定大于配置?
                                                            • 后端
                                                              • 是否了解消息队列呢?
                                                                • 缓存系统有哪些,了解吗?
                                                                  • Redis为什么性能这么好的原因,你了解吗?
                                                                    • 简单说一下索引是越多越好吗?
                                                                      • 请说一下你对视图的理解。
                                                                        • 如果发现一个SQL执行的比较慢怎么办?
                                                                          • 如果Explain用到的索引不正确的话,有什么办法干预吗?
                                                                          相关产品与服务
                                                                          云数据库 Redis
                                                                          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                                                                          领券
                                                                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档