文章目录
- 1.JVM内存结构
- 2.垃圾回收机制GC
- 3.垃圾回收的调优方法
- 4.堆和栈的区别
- 5.JVM使用命令
- 6.class.forName和Classload区别
- 7.谈谈jdk1.8特性有哪些?
- 8.反射获取类中的所有方法和获取类中的所有属性.
- 9.懒汉和饿汉模式的区别?口述两种模式。
- 10.如何保证单例模式在多线程中的线程安全性
- 11.Spring运用了什么设计模式?讲一下哪些部分用到了这些设计模式
- 12.说说你都知道哪些设计模式?最常用的有哪些?总共有几种设计模式?
- 13.redis可以存储哪几种数据类型?你的项目里都用redis存储哪些数据
- 14.redis有哪些常用操作?(命令)
- 15.Redis缓存和数据库怎么保持一致?
- 16.redis可以持久化么?如何持久化?
- 17.redis分布式锁怎么使用?
- 18.Redis缓存数据丢失怎么办?
- 19.Redis如果崩溃了如何快速恢复?
- 20.redis是如何部署的?是单个部署还是集群部署?为什么这么做?
- 21.什么是缓存穿透、缓存击穿、缓存雪崩?如何解决?
- 22.List和set和map的区别?
- 23.Arrarylist,Linkedlist;Arraylist内部扩容机制是怎样的?
- 24.HashMap扩容机制,HashMap的底层原理
- 25.Hashmap的底层用什么存储的?线程是否安全?
- 26.final和finally和finalize的区别
- 27.String、StringBuilder、StringBuffer的区别
- 28.重写equals已经能比较两个对象了,为什么还要重写hashcode方法
- 29.基本数据类型和包装数据类型的区别?
- 30.什么是多态?举个例子
- 31.抽象类和接口的区别
- 32.Java中的几种基本类型,各占用多少字节?
- 33. 运行时异常有哪些?
- 34.对面向对象的理解?面向对象的特征有哪些?
- 35.HTTP报文由哪几部分组成?Get/Post有什么区别?
- 36.转发(forward)和重定向(redirect)的区别
- 37.什么是AOP?什么是I0C?
- 38.AOP用到什么设计模式
- 39.SpringBean的生命周期
- 40.SpringI0C是怎么实现了依赖注入,有几种依赖注入方式?
- 41SpringMVC的执行流程
- 42.SpringMVC与springboot的区别?
- 43.SpringMVC的事务有用过哪几种?怎么处理的?
- 44.SpringBoot在初始化以前执行一个方法,应该怎么做?初始化以后,再执行一个方法应该怎么做,有哪几种方法去做
- 45.在项目中会经常用到哪些注解?讲出10个
- 46.@Autowired注入和自己new一个对象有什么区别?
- 47.MyBatis 防sql注入的操作
- 48.Mybaties都要用到哪些标签
- 50.MyBatis如何实现循环?
- 51.@Component,@Controller,@Repository,@Service 有何区别?
- 52.SpringBoot中的注解?SpringBoot的核心注解是什么?
- 53.Spring Boot要发布一个接口需要几个注解?
- 54.Spring Boot的运行原理/启动流程?
- 55.Spring Boot项日启动类上有什么注解?除了常用的还有什么注解?
- 56.SpringBoot配置文件怎么配?
- 57.Spring Boot 怎么整合MyBatis
- 58.Spring Boot 项目启动时配置文件加载顺序
- 59.事务的隔离级别
- 60.事务的七种传播方式
- 61.过滤器和拦截器的区别
- 62.Spring运用了什么设计模式?讲一下哪些部分用到了这些设计模式
- 63.去重的SQL语句怎么写?
- 64.MySq大批量数据如何导入?
- 65.MySql如何做大数据量分页
- 66.MySql索引类型
- 67.怎么避免索引失效
- 68.了解过mysql数据库储存引擎
- 69.mysql最左匹配原则
- 70.笛卡尔积是什么
- 71.脏读,幻读,不可重复读
- 72.数据库的优化方案
- 73.慢日志怎么使用
- 74.分库分表是怎么做的?数据库分库分表后怎么保证其一致性?
- 75.mysql表为什么产生死锁?如何避免死锁?
- 76.oracle与mysql的区别?oracle分页与mysql分页的区别?
- 77.mysql默认的事务隔离级别
- 78.mysql索引的数据结构
- 79.mysql中count()与count(1)的区别
- 80.sql看执行计划
- 81.创建线程的⽅式及其区别?
- 82.mysql锁的粒度?并发编程锁到⾏还是表?(介绍⻚级,表级,⾏级)
- 83.什么是线程和进程,它们之间的区别是什么?
- 84.请解释一下线程的几种状态以及状态之间的转换关系
- 85.什么是线程上下文切换?
- 86.项目中是否用到过多线程?怎么用的?
- 87.Sleep()和Wait()的区别?Start()和run()的区别?wait,sleep,notify的区别?
- 88.线程安全指的是什么?如何保障线程安全?
- 89.线程锁有几种方式?使用什么关键字?
- 90.什么是乐观锁和悲观锁?
- 92.如何保证单例模式在多线程中的线程安全性?
- 93.线程池常见类型有哪些?
- 94.线程池中线程的停止?
- 92.如何保证单例模式在多线程中的线程安全性?
- 93.线程池常见类型有哪些?
- 94.线程池中线程的停止?
1.JVM内存结构
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
- 堆
- 方法区
程序计数器
- 功能:每个线程都有一个程序计数器,它可以看作是当前线程正在执行的字节码指令的地址。由于JVM是多线程的,程序计数器是线程私有的,线程切换时不会影响它的内容。
- 作用:程序计数器用于控制线程的执行流程,是线程上下文的一部分。每个线程都有一个独立的计数器。
java虚拟机栈
-
功能:每个线程都有一个独立的虚拟机栈,用于存储方法的局部变量、操作数栈、动态链接、方法返回地址等信息。
-
结构:
- 每个方法在调用时会创建一个栈帧,栈帧包括:局部变量表、传递的参数、操作数栈、方法返回地址等。
- 局部变量表:用于存放方法的输入参数和局部变量(包括基本数据类型和引用类型)。局部变量表是按顺序分配的,每个局部变量都有一个索引位置。基本数据类型(int、float、long等)和对象引用()会存储在这里。
- 操作数栈:用于执行字节码指令时存放的中间结果。
- 方法返回地址:指向方法执行完毕后返回的地方。
-
内存管理:虚拟机栈是线程私有的,它的生命周期与线程的生命周期一致,栈的大小可以通过JVM参数进行调整。
本地方法栈
- 功能:与虚拟机栈类似,但是它专门用于处理Java中的本地方法(native methods),即通过JNI(Java Native Interface)调用的C/C++方法。
- 作用:存放本地方法的相关信息,如操作系统栈和C/C++代码的执行状态。
堆
-
功能:堆是JVM中最大的一块内存区域,用于存放所有的对象实例和数组。几乎所有的Java对象(包括数组)都分配在堆内存中。
-
结构
年轻代(Young Generation):存放新创建的对象,通常包括Eden区和两个Survivor区(S0和S1)。对象会在年轻代内存中经过多次垃圾回收后晋升到老年代。
老年代(Old Generation):存放经过多次GC回收仍然存活的对象。通常是长期存在的对象,例如长时间存在的缓存、大对象等。
永久代/元空间(Metaspace):存放JVM加载的类信息、方法信息等元数据。永久代在JDK 8之后被元空间取代,元空间是存储在本地内存中的,而不是堆内存。
方法区
- 功能:存储类的结构信息,如类名、常量池、字段、方法等数据。
- 结构:方法区包括了类信息、静态变量、常量池等信息。方法区在JDK8之后已经被“元空间”(Metaspace)替代,元空间存储在本地内存中,不再是堆内存的一部分。
【额外】
jvm组成:
- ClassLoader类加载器:加载class字节码文件中的内容到内存中
- 运行时数据区域:负责管理jvm使用到的内存,如:创建对象、销毁对象
- 执行引擎:将字节码文件中的指令解释成机器码,同时使用即时编译器优化性能
- 本地接口:调用本地已经编译的方法,比如虚拟机中的c/c++的方法
堆和方法区是所有线程共有的,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。
2.垃圾回收机制GC
是Java虚拟机(JVM)自动管理内存的一个重要部分。GC的目的是回收那些不再被引用的对象,从而释放内存,防止内存泄漏,并提高系统的性能。
GC触发的条件有两种:
- 程序调用System.gc时可以触发;
- 系统自身来决定GC触发的时机。
垃圾回收的过程
- 标记阶段:从GC Roots开始,遍历所有可达的对象,标记这些对象为“活跃”。如果一个对象没有被标记为“活跃”,那么就认为它是垃圾对象。
- 清除阶段:对于那些没有被标记为“活跃”的对象,可以将其销毁并释放内存。
- 整理阶段(标记-整理算法):回收后,对内存中的存活对象进行整理,防止内存碎片。
垃圾回收算法
-
标记-清除算法
步骤:
- 标记阶段:从GC Roots开始,遍历所有可以到达的对象,标记为“活跃”。
- 清除阶段:遍历堆中的所有对象,清除那些没有被标记为“活跃”的对象。
优点:
- 实现简单,容易理解。
缺点:
- 清理过程中会产生内存碎片,因为对象可能会分散存放在内存中,导致堆中的空闲区域不连续。
-
复制算法
步骤:将内存分为两个区域,每次只使用一个区域。对象从一个区域复制到另一个区域,存活的对象被复制到空闲区域。
优点:
- 不会产生内存碎片,整个内存区域都是连续的。
缺点:
- 内存使用率低,通常只有一半的内存空间被利用(需要两倍的内存区域来进行复制)。
- 对于大对象的复制性能较差。
-
标记-整理算法
步骤:
- 标记阶段:标记出所有存活的对象。
- 整理阶段:将存活的对象按顺序移动到堆的一端,并清理出剩余空间。
优点:
- 不产生内存碎片,空间利用率较高。
缺点:
- 需要移动对象,增加了内存回收的成本。
3.垃圾回收的调优方法
垃圾回收的效率和停顿时间会对应用性能产生很大影响。常见的调优方法包括:
- 调整堆的大小和分配:通过-Xms和-Xmx来设置JVM堆的初始大小和最大大小。
- 选择合适的垃圾回收器:根据应用需求(低延迟、高吞吐量)选择合适的垃圾回收器。
- 监控GC日志:通过 -Xloggc 打开垃圾回收日志,分析垃圾回收的频率、时间等。
- 调节GC策略:例如,通过-XX:+UseG1GC选择G1垃圾回收器,或者通过-XX:+UseConcMarkSweepGC选择CMS垃圾回收器。
调整堆的大小和分配
通过-Xms
和-Xmx
来设置JVM堆的初始大小和最大大小。
选择合适的垃圾回收器
根据应用需求(低延迟、高吞吐量)选择合适的垃圾回收器。
- 串行垃圾回收器 (Serial GC):适用于单核或内存小的环境,采用单线程进行回收,适合小型应用。
-
- 启用方式:
-XX:+UseSerialGC
- 启用方式:
- 并行垃圾回收器 (Parallel GC):通过多个线程进行垃圾回收,适合多核机器上的大多数应用。
-
- 启用方式:
-XX:+UseParallelGC
- 启用方式:
- 并发标记清除垃圾回收器 (CMS GC):通过多个线程并发执行回收,减少停顿时间,适用于低延迟要求的应用。
-
- 启用方式:
-XX:+UseConcMarkSweepGC
- 启用方式:
- G1垃圾回收器 (G1 GC):适用于大内存应用,目标是实现可预测的停顿时间,适合高吞吐量和低延迟的应用。
-
- 启用方式:
-XX:+UseG1GC
- 启用方式:
- ZGC (Z Garbage Collector):是一种低延迟垃圾回收器,旨在减少暂停时间,适用于大内存、高并发应用。
-
- 启用方式:
-XX:+UseZGC
- 启用方式:
- Shenandoah GC:也是一个低延迟垃圾回收器,类似于ZGC,但适用于不同的环境。
-
- 启用方式:
-XX:+UseShenandoahGC
- 启用方式:
监控GC日志
通过 -Xloggc
打开垃圾回收日志,分析垃圾回收的频率、时间等。
- 启用 GC 日志:
-Xlog:gc*
(适用于 Java 9 及以上版本) - 生成 GC 日志到文件:
-Xlog:gc*:file=gc.log
调节GC策略
例如,通过-XX:+UseG1GC
选择G1垃圾回收器,或者通过-XX:+UseConcMarkSweepGC
选择CMS垃圾回收器。
4.堆和栈的区别
定义与用途
- 堆(Heap):用于存储对象实例和数组。堆是动态分配内存的区域,所有在Java中通过
new
关键字创建的对象都会分配在堆中。堆的大小可以通过JVM参数进行设置。 - 栈(Stack):用于存储方法调用时的局部变量和方法调用的状态信息。每个线程在运行时都会有一个自己的栈空间。栈中的内容包括局部变量、方法调用的返回地址、以及一些中间计算结果。
生命周期
- 堆的生命周期:堆中的对象生命周期由垃圾回收机制(GC)控制。当对象不再被引用时,垃圾回收器会回收这些对象并释放内存。
- 栈的生命周期:栈中的局部变量和方法调用信息由方法的调用和返回控制。当方法调用结束后,该方法对应的栈帧就会被销毁,局部变量的内存空间被回收。
内存管理
- 堆:
- 堆内存的分配是动态的,需要垃圾回收机制来管理内存。
- 堆内存的对象分配和释放较为复杂,涉及到GC(垃圾回收)的过程,可能会导致停顿。
- 堆空间通常较大,可以通过JVM参数调整,如
-Xms
和-Xmx
来控制堆的初始大小和最大大小。 - 堆空间的管理可能会导致内存碎片问题,特别是在频繁的内存分配和回收时。
- 栈:
- 栈内存的分配是静态的,局部变量的空间由JVM自动分配并管理。
- 栈空间的内存回收非常高效,因为栈的内存分配和释放是按顺序进行的。方法调用结束后,栈帧会被自动销毁。
- 栈空间较小,通常在几百KB到几MB之间,过多的栈空间分配可能导致
StackOverflowError
异常。
内存分配方式
- 堆
- 堆的内存分配不依赖于栈的顺序,而是动态的,按需分配。
- 通过
new
关键字创建的对象和数组会在堆上分配空间。 - 堆内存的访问速度相对较慢,因为需要通过垃圾回收管理其生命周期。
- 栈
- 栈的内存分配是基于栈帧的,方法调用时会分配一个新的栈帧,方法返回时栈帧会被销毁。
- 栈内存的访问速度非常快,因为栈的内存分配遵循LIFO(后进先出)原则,栈帧的分配和销毁非常高效。
数据存储类型
-
堆
- 存储的是Java中的对象(包括实例对象、数组等),这些对象可以在程序的不同方法之间共享。
- 堆中的对象可以通过引用传递,这意味着对象的生命周期可以跨方法调用。
-
栈
- 存储的是局部变量、方法调用信息和方法的返回地址,这些数据通常只在当前方法的调用过程中存在。
- 栈中的数据在方法执行完毕后就会被销毁。
- 对于基本数据类型(如
int
、float
等),会直接存储在栈上;对于引用类型(如String
、对象等),则会存储其引用(指针)。
线程与栈
- 堆:堆是共享的,所有线程都可以访问堆中的对象,因此它是多线程共享的内存区域。
- 栈:每个线程有自己独立的栈空间,因此栈是线程私有的。
内存大小
- 堆
- 堆的大小可以通过JVM参数调整,通常较大(几十MB到几GB)。
- 对于大内存的对象,堆的大小可能会显得更大,尤其是当对象生命周期较长时。
- 栈
- 每个线程的栈空间较小,通常在几十KB到几MB之间。
- 如果栈空间分配过大或递归调用过深,可能会导致
StackOverflowError
异常。
线程安全
- 堆:堆中的对象是共享的,不同线程可以访问相同的对象。因此,如果多个线程访问堆中的对象,通常需要通过同步机制(如
synchronized
)来保证线程安全。 - 栈:每个线程有自己的栈空间,因此栈中的数据是线程私有的,不会发生线程安全问题。
GC与栈的关系
- 堆:垃圾回收器负责管理堆内存中的对象,当对象不再被引用时,GC会回收它们,释放内存。
- 栈:栈内存的回收非常简单,方法执行完毕,栈帧就会被销毁,局部变量的内存也会被释放,JVM不需要进行特殊的回收操作。
总结
特性 | 堆(Heap) | 栈(Stack) |
---|---|---|
用途 | 存储对象和数组 | 存储方法调用时的局部变量和方法调用信息 |
生命周期 | 由垃圾回收器管理 | 根据方法调用和返回自动管理 |
分配方式 | 动态分配 | 静态分配,遵循LIFO原则 |
内存管理 | 通过垃圾回收机制管理 | 自动管理,方法调用结束后栈帧销毁 |
存储内容 | 对象实例和数组 | 局部变量、方法返回地址等 |
线程共享 | 共享的,多个线程可以访问 | 每个线程都有独立的栈空间 |
内存大小 | 较大(几MB到几GB) | 较小(几十KB到几MB) |
访问速度 | 相对较慢(GC影响) | 快(LIFO分配) |
垃圾回收 | 有垃圾回收机制 | 无垃圾回收,栈帧自动销毁 |
5.JVM使用命令
-Xms 和 -Xmx
-
-Xms: 设置 JVM 启动时的初始堆内存大小(单位:字节)。
-
-Xmx: 设置 JVM 最大堆内存大小(单位:字节)。
java -Xms512m -Xmx1024m MyClass
-Xss
-
设置每个线程的堆栈大小。适用于多线程应用程序中,控制线程的内存使用量。
java -Xss512k MyClass
-XX:+PrintGCDetails
-
打印垃圾回收(GC)详细信息。这对于调试和优化垃圾回收行为非常有帮助。
java -XX:+PrintGCDetails MyClass
调试相关命令
- -Xdebug 和 -Xrunjdwp:启用 JVM 调试功能,这对于远程调试 Java 程序非常有用
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 MyClass
JVM 监控和分析命令
- jps:显示当前 Java 进程的信息,包括进程 ID 和启动的主类。
jps
- jstat:显示 JVM 的性能统计信息,例如堆内存、垃圾回收等。
jstat -gc <pid>
- jmap:显示或生成堆的转储信息。用于分析内存使用情况。
jmap -heap <pid>
- jstack:打印当前 JVM 进程的线程堆栈。对于分析死锁和性能问题非常有用。
jstack <pid>
- jconsole:启动 Java 控制台工具,用于监控和管理 Java 应用程序的运行时行为。
jconsole
- jvisualvm:启动 VisualVM 工具,用于性能分析、内存分析、垃圾回收分析等。
jvisualvm
JVM 调优示例
java -Xms512m -Xmx2g -XX:+UseG1GC -XX:+PrintGCDetails -Dapp.config=/path/to/config.properties -jar myapp.jar
这个命令做了如下配置:
-
设置堆内存初始大小为 512MB,最大堆内存为 2GB。
-
启用 G1 垃圾回收器,并打印垃圾回收的详细信息。
-
设置系统属性
app.config
为指定的配置文件路径。 -
执行
myapp.jar
文件。 -
设置每个线程的堆栈大小。适用于多线程应用程序中,控制线程的内存使用量。
6.class.forName和Classload区别
class.forName
Class.forName
是Java反射API中的一个静态方法,用于加载指定名称的类。它会在运行时查找并加载该类,然后返回一个Class
对象表示该类。Class.forName
方法常用于动态加载类,尤其是在需要反射机制时。
- 功能:
Class.forName
根据传入的类的全限定名(即包名 + 类名)加载该类。 - 加载时机:
Class.forName
会立即加载类并初始化类。如果类实现了静态代码块,静态代码块也会在Class.forName
调用时执行。 - 返回值:返回一个
Class
对象,表示被加载的类。
特点:
Class.forName
会触发类的加载和初始化,意味着如果类的初始化过程中有静态变量或静态代码块,都会被执行。Class.forName
常用于数据库驱动、Spring框架、Hibernate等需要动态加载类的场景。- 如果类没有被找到,
ClassNotFoundException
会被抛出。
ClassLoader
ClassLoader
是Java中的一个类加载器,用于加载Java类。Java的类加载机制是通过ClassLoader
来实现的。ClassLoader
有不同的实现,比如URLClassLoader
、ClassLoader
的子类等,通常用于从不同的来源(如文件系统、网络等)加载类。
- 功能:
ClassLoader
通过指定的类路径加载类,loadClass
方法用于加载指定名称的类。 - 加载时机:
ClassLoader
的loadClass
方法是延迟加载(懒加载)的,即在你实际使用该类时才会加载它,而不像Class.forName
会立即加载类。 - 返回值:返回一个
Class
对象,但不会初始化类(即不会执行静态代码块和静态变量的初始化)。
特点:
ClassLoader
用于自定义类加载的场景,比如从JAR文件、网络或其他非标准路径加载类。ClassLoader
加载类时不会自动执行类的初始化。- 通常,
loadClass
方法只负责加载类本身,不会执行静态初始化。如果你想要初始化类,必须手动调用Class.forName
。
特性 | Class.forName |
ClassLoader.loadClass |
---|---|---|
加载时机 | 立即加载并初始化类(包括静态代码块) | 延迟加载,仅加载类但不初始化 |
初始化类 | 会执行类的静态初始化代码(包括静态块和静态变量) | 不会执行类的静态初始化代码 |
常见用途 | 动态加载类,通常用于框架(如数据库驱动、反射) | 自定义类加载器,加载类文件(如从JAR、网络加载) |
方法的返回值 | 返回Class 对象并触发初始化 |
返回Class 对象,但不触发初始化 |
异常处理 | 如果类未找到,抛出ClassNotFoundException |
如果类未找到,抛出ClassNotFoundException |
使用场景 | 需要反射机制或者在运行时动态加载和初始化类 | 用于自定义类加载器,或从不同来源加载类 |
何时使用哪一个?
Class.forName
:
-
当你需要动态加载并初始化类时,
Class.forName
是一个方便的选择。特别是当你知道类名并且希望立即加载并初始化该类时(例如数据库驱动类的加载)。 -
例如,数据库驱动的加载:
java 复制代码 Class.forName("com.mysql.cj.jdbc.Driver");
ClassLoader
:
-
如果你有自定义的类加载需求(例如从非标准路径加载类,或者加载JAR包中的类),则可以使用
ClassLoader
。ClassLoader
适用于需要按需加载类,并且不一定要立即初始化类的场景。 -
例如,从JAR文件中加载类:
java复制代码 ClassLoader classLoader = getClass().getClassLoader(); Class<?> clazz = classLoader.loadClass("com.example.MyClass");
小结
Class.forName
用于在运行时加载并初始化类,适用于你已经知道类名并且希望触发类的初始化(包括静态代码块和静态变量)时使用。ClassLoader
用于加载类的过程,通常用于自定义类加载器或按需加载类,且不会自动触发类的初始化。
7.谈谈jdk1.8特性有哪些?
jdk1.8新特性知识点:
- Lambda表达式
- 函数式接口
- 方法引用(构造器调用)
- Stream API
- 接口中的默认方法和静态方法
- 时间日期API
Lambda表达式
注意点:
- Lambda表达式基本作用:可以简化匿名内部类的书写
- Lambda表达式只能简化函数式接口的匿名内部类的写法
- Lambda表达式使用前提:接口必须是匿名内部类,接口中只能有一个抽象方法。
格式:
( ) ->{
}
():对应着方法的形参
->:固定格式
{
}:方法体
代码示例:
Arrays.asList( "a", "b", "d" )
.sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
//上面等价于下面内名内部类形式
List<String> list = Arrays.asList("a", "b", "c");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
函数式接口
- 接口中只能有一个抽象方法,但是可以有多个非抽象方法的接口
- 可以有静态方法和默认方法
- 可以加
@FunctionalInterface
标记,也可以不加 - 默认方法可以被覆写
可以通过 Lambda 表达式实例化,常见的函数式接口有:
-
Runnable
:表示一个可以在不同线程中执行的任务。Runnable
主要用于线程的创建和执行。Runnable
接口不返回任何结果,它的run()
方法是无返回值的。适用于不需要返回值的并发任务。 -
Callable
:类似于Runnable
,但是它可以返回结果。Callable
通常用于执行并发任务时需要返回值或异常处理的场景。Callable
接口有一个抽象方法call()
,返回一个结果,类型由泛型指定。适用于需要返回结果或抛出异常的并发任务。 -
Comparator
:用于对象排序的接口,可以定义自定义的排序规则。它允许对一个对象集合进行排序,并且可以按照不同的标准对对象进行比较。可以对不同的属性进行排序,并且一个对象可以有多个排序规则。Comparator
接口的compare()
方法返回一个整数值(通常是负数、零或正数)来表示两个对象的顺序:- 如果返回负数,表示第一个对象小于第二个对象。
- 如果返回零,表示两个对象相等。
- 如果返回正数,表示第一个对象大于第二个对象。
-
Consumer
:Consumer<T>
T 作为输入 ,没有输出 -
Function
:Function<T,R>
T 作为输入,返回的 R 作为输出 -
Predicate
:Predicate<T>
T 作为输入 ,返回 boolean 值的输出
方法引用
方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。
方法引用符:::
方法引用:就是把已经有的方法拿过来用,当作函数式接口中抽象方法的方法体。
注意点:
- 需要有函数式接口
- 被引用方法必须已经存在
- 被引用方法的形参和返回值需要跟抽象方法保持一致
- 被引用方法的功能要满足当前需求
public class Test {
public static void main(String[] args) {
//需求:创建一个数组,进行倒序排列
Integer[] arr = {
3, 5, 4, 1, 6, 2};
//1.匿名内部类形式
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
//2.lambda表达式形式
//因为第二个参数的类型Comparator是一个函数式接口
Arrays.sort(arr,(o1 ,o2)->{
return o2- o1;
});
//3.lambda表达式简写形式
Arrays.sort(arr,(o1 ,o2)-> o2- o1);
// 4.方法引用
//表示引用Test类里面的静态subtraction方法
//把这个方法当做抽象方法的方法体
Arrays.sort(arr,Test::subtraction);
//输出数组
System.out.println(Arrays.toString(arr));
}
//静态subtraction方法
public static int subtraction(int num1 ,int num2){
return num2 - num1 ;
}
Stream API
用于处理集合(如 List、Set 等)支持对集合的批量数据进行筛选、排序、映射、统计等操作。通过 Lambda 表达式配合 Stream API,可以写出简洁且易于理解的代码。
public static void main(String[] args) {
//单列集合==default Stream<E> stream() == Collection中的默认方法
// 创建单列集合
ArrayList<String> list = new ArrayList<>();
// 添加元素给集合
Collections.addAll(list,"a","b","c");
// // 1.获取Stream流
// Stream<String> stream1 = list.stream();
// // 循环打印Stream流的每个元素
// stream1.forEach(new Consumer<String>() {
// @Override
// public void accept(String s) {
// s:依次表示流水线上的每一个数据
// System.out.println(s);
// }
// });
// 2.获取Stream流并循环打印每个元素
list.stream().forEach(s -> System.out.print(s+" "));
//a b c
}
Stream流中间方法:
1.中间方法,返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程
2.修改Stream流中的数据,不会影响原来集合或者数组中的数据
方法名 | 说明 |
---|---|
Stream filter(Predicate<? super T> predicate) | 过滤 |
Stream limit(long maxSize) | 获取前几个元素 |
Stream skip(long n) | 跳过前几个元素 |
Stream distinct() | 元素去重,依赖(hashCode和equals方法) |
static Stream concat(Stream a,Stream b) | 合并a和b两个流为一个流 |
Stream map(Function<super T, R> mapper) | 转换流中的数据类型 |
map(Function mapper ) | 将元素转换成其他形式或提取信息。 |
sorted() | 对元素进行排序 |
Stream流终结方法:
方法名 | 说明 |
---|---|
void forEach(Consumer action) | 遍历 |
long count() | 统计 |
toArray() | 收集流中的数据,放到数组中 |
collection(Collection collection) | 收集流中数据,放到集合中 |
collect(Collector collector) 收集流中的数据,放到集合中 (List 、Set、 Map)
如果我们要收集到Map集合当中,键不能重复,否则会报错
接口中的默认方法和静态方法
Java 8 允许接口中定义静态方法。静态方法可以通过接口名直接调用,而不需要通过实现类。接口中的静态方法通过 static
关键字定义。
Java 8 还引入了接口中的默认方法,这使得接口可以有方法实现,而不需要继承类。接口中的默认方法通过 default
关键字定义。默认方法允许我们在接口里添加新的方法,而不会破坏实现这个接口的已有类的兼容性,也就是说不会强迫实现接口的类实现默认方法。
时间日期API
Java 8 引入了全新的日期和时间 API(java.time
包),它提供了更强大且灵活的处理日期和时间的方式。相比于旧的 java.util.Date
和 java.util.Calendar
,新 API 更加易用且线程安全。
几乎所有的时间对象都实现了 Temporal
接口,所以接口参数一般都是 Temporal
- Instant: 表示时间线上的一个点,参考点是标准的Java纪元(epoch),即1970-01-01T00:00:00Z(1970年1月1日00:00 GMT)
- LocalDate: 日期值对象如 2019-09-22
- LocalTime: 时间值对象如 21:25:36
- LocalDateTime: 日期 时间值对象
- ZoneId: 时区
- ZonedDateTime: 日期 时间 时区值对象
- DateTimeFormatter: 用于日期时间的格式化
- Period: 用于计算日期间隔
- Duration: 用于计算时间间隔
8.反射获取类中的所有方法和获取类中的所有属性.
获取类中的所有方法
要获取类中的所有方法,使用 Class
类的 getDeclaredMethods()
或 getMethods()
方法。
getDeclaredMethods()
:获取类中声明的所有方法(包括私有的、保护的和公共的)。getMethods()
:获取类中公共的方法,包括继承自父类的公共方法。
获取类中的所有属性
获取类中的所有属性(字段),可以使用 Class
类的 getDeclaredFields()
或 getFields()
方法。
getDeclaredFields()
:获取类中声明的所有属性(字段)包括私有的、保护的和公共的。getFields()
:获取类中公共的属性(字段),包括继承自父类的公共字段。
9.懒汉和饿汉模式的区别?口述两种模式。
单例设计模式分类两种:
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式
-
是立即加载的方式,无论是否会用到这个对象,都会加载。
-
如果在构造方法里写了性能消耗较大,占时较久的代码,比如建立与数据库的连接,那么就会在启动的时候感觉稍微有些卡顿。
-
由于类的加载和初始化是由 JVM 管理的,它天生是线程安全的。
-
缺点:不管是否使用该对象,类加载时都会实例化对象,可能浪费资源,特别是当对象的创建比较复杂时。
懒汉式
- 是延迟加载的方式,只有使用的时候才会加载。 并且有线程安全的考量。
- 使用懒汉式,在启动的时候,会感觉到比饿汉式略快,因为并没有做对象的实例化。 但是在第一次调用的时候,会进行实例化操作,感觉上就略慢。
- 线程安全问题:在多线程环境下,如果不加锁,可能会创建多个实例。需要通过同步来保证线程安全。
- 缺点:如果没有加锁处理,可能会出现线程不安全的问题。加锁可能带来性能损耗,尤其在高并发的情况下。
懒汉模式与饿汉模式的区别总结
特性 | 懒汉模式 | 饿汉模式 |
---|---|---|
实例化时机 | 延迟实例化,只有在第一次访问时才创建对象 | 类加载时立即实例化对象 |
线程安全 | 默认线程不安全(需要加锁来保证线程安全) | 天生线程安全 |
性能 | 只有在需要时才创建对象,节省资源 | 无论是否使用都会创建对象,可能浪费资源 |
内存消耗 | 在实例化之前不会占用内存 | 在程序启动时就占用内存 |
使用场景 | 适用于需要延迟加载、实例化过程较为复杂的场景 | 适用于实例化过程简单且总是需要的单例类 |
实现方式 | 通过 if 判断是否需要创建实例(可能需要加锁) |
通过静态初始化时创建实例,无需加锁 |
10.如何保证单例模式在多线程中的线程安全性
在 Java 中,单例模式在多线程环境下需要特别小心,以避免多个线程同时创建多个实例,导致违反单例模式的基本原则。为了保证单例模式的线程安全性,我们需要采取一些额外的措施
双重检查锁定
多线程环境下实现懒汉模式的一个常见方案。它通过减少同步的范围,既保证了线程安全,又避免了过多的性能开销。
工作原理:
- 第一次检查是否已经创建实例,如果已经创建,则直接返回实例(此时不需要加锁)。
- 如果没有创建实例,才会进入同步块进行加锁。在同步块内,再次检查实例是否已经创建,确保只有一个线程能够创建实例。
关键点:
- 使用
volatile
关键字:确保多线程环境下对instance
变量的可见性,防止发生指令重排可能会导致程序的执行顺序不符合预期。 - 通过双重检查减少了同步锁的粒度,避免了每次访问
getInstance()
方法时都进行加锁,提升了性能
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;
}
}
使用 synchronized
方法
通过将获取实例的方法设置为 synchronized
,可以保证多线程环境下只有一个线程能够访问该方法,从而保证线程安全。
缺点:
- 每次调用
getInstance()
方法时都要加锁,性能较差,尤其在多线程高并发的情况下,因为每次访问getInstance()
都需要进行同步。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
静态内部类(最推荐方式)
利用 静态内部类来实现单例的方式。这种方式不仅线程安全,而且避免了同步带来的性能损耗。静态内部类在第一次被加载时才会实例化,因此它实现了懒加载,同时确保了线程安全。
静态属性由于被static修饰,保证只被实例化一次,并且严格保证实例化顺序。
它结合了 懒汉式 和 饿汉式 的优点,同时提供了 线程安全 和 延迟加载 的特点。
工作原理:
- 静态内部类在外部类加载时不会被加载,只有在调用
getInstance()
方法时,内部类才会被加载,这样就保证了懒加载。 - Java 的类加载机制可以保证对静态变量的初始化是线程安全的,因此无需手动加锁。
优点:
- 线程安全:类的加载机制保证了线程安全性。
- 延迟加载:只有在首次调用
getInstance()
时,SingletonHelper
类才会被加载,避免了不必要的内存浪费。 - 性能高:没有同步开销。
public class Singleton {
private Singleton() {
}
private static class SingletonHelper {
// 静态初始化器
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
枚举单例(推荐)
Java 枚举类型是实现单例的最佳方式之一。**枚举类本身由 JVM 保证线程安全,同时还能够防止反序列化重新创建实例对象的问题(这对于其他单例模式可能是一个潜在问题)。**在枚举类型中,单例实例是由 JVM 确保只会创建一次,并且能够保证反序列化时不会创建新的实例。
优点:
- 简洁且线程安全:枚举类型是由 JVM 自身保证线程安全和单例的。
- 防止反序列化:枚举类型是唯一可以防止反序列化时破坏单例的方式,因为枚举类的反序列化机制保证了枚举实例的唯一性。
public enum Singleton {
INSTANCE;
public void someMethod() {
// 这里可以实现其他方法
System.out.println("Singleton method");
}
}
总结
实现方式 | 线程安 全性 | 性能 | 适用场景 | 优缺点 |
---|---|---|---|---|
双重检查锁定 | 线程安全 | 高效,减少锁开销 | 多线程高并发时 | 需要 volatile ,实现复杂,但性能优异。 |
同步方法 | 线程安全 | 性能较低 | 线程不频繁访问 | 每次调用都需要加锁,性能差,适合访问较少的场景。 |
静态内部类 | 线程安全 | 性能优异 | 推荐使用 | 线程安全、懒加载、无需加锁,是最推荐的方式。 |
枚举单例 | 线程安全 | 性能优异 | 最推荐使用 | 代码简洁,线程安全,防止反序列化破坏,但不可继承。 |
11.Spring运用了什么设计模式?讲一下哪些部分用到了这些设计模式
- 单例模式
- 工厂模式
- 代理模式
- 观察者模式
- 适配器模式
- 装饰器模式
- 模板方法模式
- 策略模式
单例模式
- 应用场景:Spring中的Bean的默认作用域就是单例。也就是说,当一个Bean被创建后,Spring容器会缓存它并在后续的请求中返回同一个实例。意味着在整个应用程序中只有一个Bean实例,由Spring容器负责管理
- 实现方式:Spring使用单例模式管理所有的Bean实例,在Bean的作用域是单例的情况下,容器只会创建一个实例并重复使用。你可以通过
@Scope("singleton")
来显式指定Bean为单例(这是Spring的默认行为)。
工厂模式
工厂模式是一种创建型设计模式。Spring 使用工厂模式可以通过 BeanFactory
或 ApplicationContext
创建 Bean 对象。Spring容器本质上是一个工厂,它通过ApplicationContext
或者BeanFactory
来管理Bean的生命周期、创建和依赖注入。隐藏了具体实例化的细节,使得应用程序更易于扩展和维护。
BeanFactory
:延迟注入(使用到某个 Bean 的时候才会注入),相比于ApplicationContext
来说会占用更少的内存,程序启动速度更快。- ApplicationContext:容器启动的时候,不管你用没用到,一次性创建所有 Bean。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory,除了有 BeanFactory 的功能还有额外更多功能,所以一般开发人员使用 AplicationContext 更多。
应用场景:Spring容器本质上是一个工厂,它通过ApplicationContext
或者BeanFactory
来管理Bean的生命周期、创建和依赖注入。
实现方式:Spring通过BeanFactory
接口和ApplicationContext
来实现工厂模式。BeanFactory
负责根据配置文件或者注解创建Bean实例,并管理它们的生命周期。
代理模式
代理模式是一种结构型设计模式,它允许通过代理类控制对其他对象的访问。
能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如:事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
-
应用场景:Spring AOP(面向切面编程)使用代理模式来处理横切关注点(如日志记录、事务管理等)。在Spring AOP中,代理对象被用来拦截方法的调用,并在方法执行前后加入额外的处理逻辑。
(AOP的核心思想是将横切关注点(如日志、事务管理、权限控制等)从业务逻辑中分离出来,通过“切面”(Aspect)来动态地增强目标对象的行为。)
-
实现方式:Spring AOP的默认实现基于JDK动态代理或CGLIB代理。在JDK动态代理中,如果目标类实现了接口,则会创建一个实现接口的代理类;如果目标类没有实现接口,则会使用CGLIB动态生成目标类的子类。
观察者模式
应用场景:Spring事件驱动模型使用观察者模式。Spring的事件机制允许应用程序在某些事件发生时进行处理,多个监听器可以对同一事件做出反应。
实现方式:ApplicationEventPublisher
和ApplicationListener
接口提供了事件发布和事件监听的机制。当某个事件发生时,所有注册的监听器会被通知并进行处理。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。
适配器模式
适配器模式用于将一个类的接口转换成客户端期望的另一个接口,从而使原本因接口不兼容而不能一起工作的类能够一起工作。在SpringMVC中,HandlerAdapter允许不同类型的 处理器 适配 到处理器接口,以实现统一的处理器调用。
实现过程:DispatcherServlet根据HandlerMapping返回的handler,向HandlerAdapter发起请求,处理handler。HandlerAdapter根据规则找到对应的Handler并让其执行,执行完毕后Handler会向HandlerAdapter返回一个ModelAndView,最后由HandlerAdapter向DispatcherServlet返回一个
- 应用场景:Spring通过适配器模式,使得不同类型的对象可以通过统一的接口进行交互。比如,SpringMVC的
HandlerAdapter
和ViewResolver
就是典型的适配器模式应用。 - 实现方式:
HandlerAdapter
接口允许不同的控制器(如SimpleControllerHandlerAdapter
、AnnotationMethodHandlerAdapter
等)使用统一的方式来处理请求。
装饰器模式
装饰器模式允许在不改变原有对象的情况下,通过包装一个对象来增加新的功能。在Spring中,BeanWrapper允许在不修改原始Bean类的情况下添加额外的功能。
模板方法模式
模板方法模式定义了一个操作中的算法骨架,将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在Spring中,JdbcTemplate、HibernateTemplate等类使用了模板方法模式,提供了统一的接口同时允许用户根据自己的需求进行定制。
一般情况下,我们都是使用继承的方法来实现模板模式,但是 Spring 并没有使用这种方式,而是使用 Callback
模板与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。
策略模式
策略模式定义了一系列的算法,并将每一个算法封装起来,使它们可以相互替换。在Spring中,策略模式用于实现不同的算法或策略,例如任务调度策略
举例:Spring 框架的资源访问 Resource
接口。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。
12.说说你都知道哪些设计模式?最常用的有哪些?总共有几种设计模式?
设计模式总共有 23 种,可以分为创建型、结构型和行为型三类。
创建型(Creational)设计模式
这些模式主要关注如何实例化对象。它们提供了一个灵活的方式来创建对象,解决了对象创建的复杂性。
- 单例模式(Singleton):确保一个类只有一个实例,并提供全局访问点。
- 工厂方法模式(Factory Method):定义一个接口来创建对象,但由子类决定实例化哪一个类。
- 抽象工厂模式(Abstract Factory):提供一个创建一系列相关或依赖对象的接口,而无需指定具体类。
- 建造者模式(Builder):使用多个简单的对象一步步构建成一个复杂的对象。
- 原型模式(Prototype):通过复制现有的实例来创建新对象,而不是通过类的构造方法来创建对象。
结构型(Structural)设计模式
这些模式主要关注如何将类和对象组合成更大的结构。它们通常关注的是如何通过继承和接口来更好地组织和结构化代码。
- 适配器模式(Adapter):将一个类的接口转换成客户端希望的另一个接口。
- 桥接模式(Bridge):将抽象部分与其实现部分分离,使得两者可以独立变化。
- 组合模式(Composite):将对象组合成树形结构来表示“部分-整体”的层次结构。
- 装饰器模式(Decorator):通过继承来动态地给一个对象添加额外的功能。
- 外观模式(Facade):为子系统中的一组接口提供一个统一的高层接口。
- 享元模式(Flyweight):通过共享小的对象来减少内存使用,提高性能。
- 代理模式(Proxy):为其他对象提供一种代理以控制对这个对象的访问。
行为型(Behavioral)设计模式
这些模式主要关注对象之间的交互和职责分配。它们有助于改善对象之间的通信、协作和行为模式。
- 责任链模式(Chain of Responsibility):通过多个处理者对象来处理请求,每个处理者负责处理请求的一部分。
- 命令模式(Command):将请求封装成对象,从而使您能够使用不同的请求、队列或日志请求,以及支持可撤销操作。
- 解释器模式(Interpreter):为语言中的每个符号定义一个解释器对象,通过上下文解释整个表达式。
- 迭代器模式(Iterator):提供一种方法访问一个集合对象中的各个元素,而又不暴露该对象的内部表示。
- 中介者模式(Mediator):定义一个对象来封装一组对象之间的交互,促进松耦合。
- 备忘录模式(Memento):在不暴露对象内部结构的情况下,捕获对象的状态,以便以后恢复。
- 观察者模式(Observer):定义对象之间的一对多依赖关系,使得一个对象状态变化时,其依赖的所有对象都会自动更新。
- 状态模式(State):允许一个对象在其内部状态改变时改变其行为,看起来像是改变了其类。
- 策略模式(Strategy):定义一系列算法,并将每个算法封装起来,使它们可以互换。
- 模板方法模式(Template Method):定义一个操作中的算法的框架,将一些步骤延迟到子类中实现。
- 访问者模式(Visitor):表示一个作用于某对象结构的操作,它可以在不改变对象结构的前提下定义新的操作。
最常用的设计模式
- 单例模式(Singleton)
这可能是最常用的设计模式,常用于确保某个类只有一个实例,并提供全局访问点。例如,数据库连接池、配置类等。 - 工厂模式(Factory Method)
工厂模式可以有效地解耦客户端代码与具体的对象创建过程。它非常适用于需要创建不同类型对象的场景。 - 观察者模式(Observer)
观察者模式用于实现事件驱动的机制,通常用于 GUI 编程、消息通知等场景。例如,当用户注册为某个事件的监听器时,系统可以自动推送相关数据给用户。 - 策略模式(Strategy)
策略模式用于定义一系列算法,并且可以根据需求动态地选择不同的算法实现,避免了使用大量的条件语句。 - 代理模式(Proxy)
代理模式广泛应用于懒加载、远程方法调用等场景。通过代理对象控制对目标对象的访问,从而实现对对象访问的控制。 - 装饰器模式(Decorator)
装饰器模式可以在运行时动态地为对象增加功能,比继承更灵活,可以用于增强对象的功能,而无需修改其类。
13.redis可以存储哪几种数据类型?你的项目里都用redis存储哪些数据
Redis支持五种主要的数据类型,它们分别是:
-
字符串(String): 最简单的数据类型,可以存储任何类型数据,比如文本、数字等。String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
-
**哈希表(Hash):**键值对集合, 更适合对象的存储,类似于其他编程语言中的关联数组或者对象。每个哈希表可以存储多个键值对。常用于存储用户信息、商品信息等。
-
列表(List): 一个有序的字符串元素集合,可以存储重复元素,支持从两端压入和弹出元素,可以用作栈(stack)或队列(queue)。应用场景:消息队列、文章列表、历史记录、最新动态。
-
集合(Set): 无序的字符串元素集合,每个元素不能重复。可以执行集合操作,如并集、交集、差集等。应用场景:点赞、标签系统、共同关注、好友关系、唯一访客统计、抽奖活动
-
有序集合zset(Sorted Set): 类似于集合,元素不能重复。但每个元素都关联一个分数,根据分数进行排序。可以用于实现排行榜等功能。应用场景:排序场景,比如排行榜、时间线、范围查询、以及一些需要延迟队列的场景
14.redis有哪些常用操作?(命令)
据类型相关命令
-
字符串(String):
SET key value
:设置指定键的值。GET key
:获取指定键的值。INCR key
:将指定键的值加1。
-
哈希表(Hash):
HSET key field value
:设置哈希表中指定字段的值。HGET key field
:获取哈希表中指定字段的值。HGETALL key
:获取哈希表中所有字段和值。
-
列表(List):
LPUSH key value
:将一个值插入到列表头部。RPUSH key value
:将一个值插入到列表尾部。LPOP key
:移除并返回列表的第一个元素。
-
集合(Set):
SADD key member
:向集合中添加一个或多个成员。SMEMBERS key
:获取集合中的所有成员。SINTER key1 key2
:返回两个集合的交集。
-
有序集合(Sorted Set):
ZADD key score member
:将一个成员的分数加到有序集合中。ZRANGE key start stop
:通过索引区间返回有序集合指定区间内的成员。
服务器管理命令
-
信息命令:
INFO
:获取关于 Redis 服务器的各种信息和统计数值。PING
:检测服务器是否可用。
-
持久化:
SAVE
:同步保存数据到硬盘。BGSAVE
:异步保存数据到硬盘。
-
复制:
SLAVEOF host port
:将当前服务器设置为指定服务器的从服务器。
事务相关命令
- 事务:
MULTI
:标记一个事务块的开始。EXEC
:执行所有事务块命令。DISCARD
:取消事务,放弃执行事务块内的所有命令。
其他常用命令
-
键操作:
DEL key
:删除一个键。EXISTS key
:检查键是否存在。KEYS pattern
:查找所有符合给定模式的键。
-
过期时间:
EXPIRE key seconds
:为键设置过期时间。TTL key
:获取键的剩余过期时间。
-
发布与订阅:
PUBLISH channel message
:将消息发送到指定频道。SUBSCRIBE channel
:订阅一个或多个频道。
这只是 Redis 命令的一小部分,实际应用中可能会根据具体场景使用更多的命令。不同版本的 Redis 可能会有新增或废弃的命令,建议查阅官方文档获取详细信息。
15.Redis缓存和数据库怎么保持一致?
- 更新缓存时同步数据库
- 设置缓存过期时间,避免缓存中的数据长时间过期。
- 删除缓存后重新加载,保证数据的一致性。
- 双写一致性,保证在数据库写操作时同时更新缓存。
- 使用异步更新缓存方式来减少操作的阻塞。
- 使用消息队列、事件机制等手段来解耦数据库与缓存的更新过程。
更新缓存时同步数据库
最常见的方法是在更新数据库时同步更新缓存。在执行更新操作时,确保缓存与数据库保持一致。
- 场景:当应用对数据进行增、删、改(写操作)时,除了操作数据库,还需要更新缓存。这样确保数据库和缓存的数据一致。
- 常见操作
- 更新操作:数据更新时,先修改数据库,修改成功后更新缓存。
- 删除操作:删除数据库中的数据后,删除缓存中的数据。
- 新增操作:新增数据时,先插入数据库,再将数据插入缓存。
流程:
- 执行数据库的增、删、改操作。
- 数据库操作成功后,更新或删除Redis中的缓存数据。
- 如果Redis操作失败,可以通过重试机制或错误日志进行处理,保证数据最终一致性。
示例:假设我们有一个商品信息的缓存,更新商品信息时,既要更新数据库,也要更新Redis缓存。
public void updateProduct(Product product) {
// 更新数据库
productDao.updateProduct(product);
// 更新缓存
redisTemplate.opsForValue().set("product:" + product.getId(), product);
}
缓存过期策略
为了避免缓存与数据库数据的长时间不一致,通常使用缓存过期策略来保证缓存的数据不会长期存活。在数据过期后,再次请求时会重新加载数据到缓存中。
- 场景:设置缓存的过期时间,通常与数据的更新频率相关,保证在数据更新时,缓存失效能被及时替换。
- 实现方式
- 为缓存设置一个合理的过期时间(TTL,Time-To-Live),根据业务需求决定。
- 数据过期后,缓存将失效,下次请求时会从数据库中读取数据并重新更新缓存。
示例:
// 设置缓存过期时间为1小时
redisTemplate.opsForValue()
.set("product:" + productId, product, 1, TimeUnit.HOURS);
删除缓存后重新加载
当数据库发生写操作时(比如更新、删除数据),可以选择直接删除缓存,然后等待下一次请求来重新加载缓存。这种方式有助于避免缓存与数据库数据的长时间不一致。
场景:每当数据库数据变动时,删除缓存数据,下一次请求时缓存会重新加载,保证缓存中数据的正确性。
实现方式:
- 删除缓存:数据库修改后,删除缓存中的相关数据。
- 下一次请求:当缓存失效后,从数据库重新读取并加载到缓存中。
双写一致性(双写策略)
双写一致性指的是,在更新数据库时,确保同时更新Redis缓存。可以通过事务机制来确保双写操作的一致性。
- 场景:进行数据库和缓存的双写时,避免在操作过程中出现缓存和数据库数据不一致的情况。
- 实现方式
- 在更新数据库后,立即更新缓存,确保数据库和缓存的数据同步。
- 如果数据库更新成功,但缓存更新失败,需要有补偿机制(如重试)来确保缓存数据最终与数据库一致。
这种方式是最常见的保证一致性的方法,但它的缺点是可能出现“脏读”现象(在缓存更新之前就有请求进来读取到旧缓存数据)。
异步更新缓存
可以通过异步更新缓存来降低数据库与缓存操作的同步开销。在这种情况下,数据更新时,首先更新数据库,然后通过异步任务更新缓存。
- 场景:当写入操作很频繁时,可以异步更新缓存,避免同步更新带来的性能瓶颈。
- 实现方式
- 数据库更新:数据库数据更新后,立即返回。
- 异步更新缓存:通过消息队列(如Kafka、RabbitMQ)或者后台异步任务来更新缓存。
使用消息队列(异步一致性)
如果你想在保证缓存一致性的同时,又不影响系统性能,可以考虑通过消息队列来实现数据库和缓存的一致性。可以在写操作时将消息发送到消息队列,然后异步处理消息来更新缓存。
场景:将缓存更新操作推送到消息队列,通过异步消费消息来更新缓存,从而避免阻塞主流程。
实现方式:
-
在更新数据库的同时,将“更新缓存”的任务推送到消息队列中。
-
消费者从队列中取出任务后,更新缓存。
(然后有一个消费者从队列中读取消息并更新缓存。)
基于事件的机制(例如观察者模式)
利用事件机制,在数据库发生变更时(如数据增删改),可以发布事件,通知系统其他组件(如缓存更新模块)去更新缓存。这可以通过事件监听机制来实现。
- 场景:每当数据库发生变动时,触发相应的事件,其他模块(如缓存模块)监听该事件并更新缓存。
- 实现方式
- 数据库修改时发布事件。
- 监听该事件并更新缓存。
16.redis可以持久化么?如何持久化?
Redis可以持久化。Redis 提供了几种持久化机制,主要包括 RDB(Redis Database) 和 AOF(Append-Only File),此外,还可以结合使用两者。
Redis支持两种主要的持久化方式,分别是:
-
RDB(Redis DataBase): RDB 是一种快照(snapshot)持久化方式,通过将当前数据集的状态保存到磁盘上的二进制文件中。这个文件包含了某个时间点上所有键的值。RDB 是一种紧凑且经济的持久化方式,适用于数据备份和恢复。
- 触发条件:管理员手动执行 SAVE 或者 BGSAVE 命令,或者根据配置文件中的自动保存规则(save 指令)进行定期触发。
- 文件格式:默认以
dump.rdb
命名,可以通过配置文件指定其他名称。
-
AOF(Append Only File): AOF 是一种追加日志文件方式,记录每个写操作的命令,以追加的形式写入到磁盘文件中。通过重放这些命令,可以恢复数据集的状态。AOF 提供了更好的持久化实时性,适用于要求更小数据丢失的场景。
- 触发条件:每个写操作都会被追加到 AOF 文件。
- 文件格式:默认以
appendonly.aof
命名,可以通过配置文件指定其他名称。
- 文件格式:默认以
RDB持久化
RDB 持久化是 Redis 默认的持久化机制,采用快照方式,将 Redis 数据的某一时刻保存为一个二进制文件。
RDB 的优缺点:
优点
- 快照是一次性的全量保存,保存速度较快。
- Redis 在持久化过程中是非阻塞的,因为它是在后台进行的。
- RDB 文件比较紧凑,占用磁盘空间小。
缺点
- **RDB 持久化的周期性保存可能会导致数据丢失。**例如,如果 Redis 在快照之间崩溃,则会丢失最近几秒或几分钟的操作数据。
RDB 持久化原理:
- Redis 会根据配置的条件定期将内存中的数据保存为一个 RDB 文件,该文件通常位于磁盘的
/var/lib/redis/dump.rdb
。 - 快照的生成是通过创建一个内存快照(即持久化到磁盘)实现的。
- 可以通过 配置
save
规则,来决定什么条件下进行快照。
RDB 配置:
在 Redis 配置文件(redis.conf
)中,save
选项控制了持久化的频率。save
后面跟着的是两个参数:seconds
和 changes
,意思是:在某个时间内,如果有多少次写操作发生,Redis 会进行 RDB 持久化。
例如,以下配置意味着:
- 如果 900 秒内有至少 1 次写操作,生成一个 RDB 快照。
- 如果 300 秒内有至少 10 次写操作,生成一个 RDB 快照。
- 如果 60 秒内有至少 10000 次写操作,生成一个 RDB 快照。
save 900 1
save 300 10
save 60 10000
触发 RDB 持久化的操作:
- 手动触发:使用
BGSAVE
命令进行后台保存,或者使用SAVE
命令进行同步保存(会阻塞 Redis 直到保存完成)。 - 配置触发:根据
save
配置的规则,Redis 会自动保存。
AOF持久化
AOF 的优缺点:
优点
- AOF 可以更精确地保证数据的持久化。相比 RDB,AOF 丢失数据的可能性更小,因为每次写操作都会被记录。
- 配置灵活,可以选择不同的同步策略,提供了不同的性能和数据安全性折中。
缺点
- AOF 文件会不断增长,因为每个写操作都会被记录下来,可能导致 AOF 文件较大。
- AOF 写入过程中会增加系统负担,尤其是在
appendfsync always
模式下,性能损失较大。 - 启动时需要重新执行 AOF 中的所有命令,相比于 RDB,恢复时间较长。
AOF 持久化原理:
- Redis 会将写命令以“追加”日志的方式保存到 AOF 文件中。每个命令都以可执行的格式记录,因此 Redis 启动时可以通过重放 AOF 文件来恢复数据。
- AOF 文件默认存储路径是
appendonly.aof
,通常位于/var/lib/redis/
目录。
AOF 配置:
在 redis.conf
配置文件中,可以配置 AOF 文件的持久化策略,主要是 同步策略,即在每次写入操作后,Redis 如何将命令写入到 AOF 文件。
AOF 持久化有三种不同的同步策略:
- always:每次写操作后都同步写入 AOF 文件(最安全,但性能差)。
- everysec:每秒同步一次,适中选择,性能和安全性折中。
- no:从不自动同步,需要手动控制同步(最高性能,最不安全)。
appendonly yes # 启用 AOF 持久化
appendfsync everysec # 每秒同步一次
RDB 和 AOF 混合模式
Redis 允许同时开启 RDB 和 AOF 持久化,即同时使用这两种方式来保证数据的持久性。这样可以兼顾两者的优点,减少数据丢失的风险。
如何配置 RDB 和 AOF 混合使用:
-
在配置文件中启用 RDB 和 AOF:
save 900 1 save 300 10 save 60 10000 appendonly yes appendfsync everysec
-
使用 RDB 做周期性保存,同时通过 AOF 记录每个写操作的命令,以提高数据的持久性。
-
AOF 重写(AOF Rewrite):当 AOF 文件过大时,Redis 会进行 AOF 重写,创建一个新的 AOF 文件,包含当前数据库状态的最小写操作。这可以通过
BGREWRITEAOF
命令手动触发,Redis 会在后台进行重写,而不影响当前操作。AOF 重写的好处是通过压缩文件,去掉冗余命令,减少 AOF 文件的大小。BGREWRITEAOF
持久化策略总结
持久化方式 | 特点 | 优点 | 缺点 |
---|---|---|---|
RDB | 快照持久化,通过生成时间点的全量数据保存到磁盘。 | 快速、占用磁盘少;适合备份数据。 | 存在数据丢失风险,恢复较慢。 |
AOF | 将每个写命令追加到文件中,按顺序重放命令恢复数据。 | 数据丢失风险小,可灵活配置同步策略。 | AOF 文件较大,写操作性能较差。 |
混合使用 | 同时使用 RDB 和 AOF。 | 数据安全性较高;可以灵活选择策略。 | 可能导致磁盘空间占用较大,性能折中。 |
选择建议:
- 如果你需要高性能且数据丢失可以容忍的情况,可以选择 RDB 持久化。
- 如果你要求严格的数据持久性,且希望减少数据丢失的概率,可以选择 AOF 持久化。
- 如果你既希望有较好的数据持久性,又希望能够快速恢复,可以同时启用 RDB 和 AOF 混合持久化。
17.redis分布式锁怎么使用?
在分布式系统中,为了避免多个进程/线程并发访问共享资源导致的数据不一致问题,进而使用分布式锁来保证数据安全。Redis由于其高效的读写能力,常被用来实现分布式锁。
Redis的分布式锁通常使用SETNX
命令(SET if Not eXists)来实现。具体步骤如下:
- 客户端尝试在Redis中设置一个锁键,使用
SETNX
命令。如果该键不存在,设置成功并返回1
,表示获取锁成功。 - 如果该键已经存在,说明锁已被其他客户端占用,获取锁失败则返回
0
。 - 锁通常会设置一个过期时间(比如5秒),防止死锁。
- 获取锁后,客户端完成任务,最后删除锁键。
Jedis jedis = new Jedis("localhost");
// 尝试获取锁
String lockKey = "lock_key";
String lockValue = UUID.randomUUID().toString(); // 唯一标识
long expireTime = 5; // 锁的过期时间,单位秒
// SETNX命令尝试获取锁
if (jedis.setnx(lockKey, lockValue) == 1) {
// 设置过期时间
jedis.expire(lockKey, expireTime);
// 执行业务逻辑
// 释放锁
if (lockValue.equals(jedis.get(lockKey))) {
jedis.del(lockKey); // 删除锁
}
} else {
// 获取锁失败,重试或返回失败信息
}
18.Redis缓存数据丢失怎么办?
如果Redis缓存数据丢失,可以通过以下方式应对:
-
持久化机制:开启Redis的持久化(RDB或AOF),定期将数据保存到磁盘,以避免数据丢失。
如果提前进行了Redis持久化操作,并且以RDB快照方式持久化将数据写入内存磁盘中,在数据丢失时可以重新启动Redis加载快照文件,进而恢复数据。
如果进行的是AOF以追加日志文件方式写入内存磁盘,在数据丢失后也可以重新启动Redis加载AOF文件,进行数据回复,但是由于AOF会记录每个写操作命令,所以回复时比较慢
-
备份:定期备份Redis数据快照(RDB文件)或AOF日志,确保数据可以恢复。
-
主从复制:设置Redis主从复制,主节点数据丢失时可以从从节点恢复。
-
使用外部存储:将重要数据存储在数据库中,Redis作为缓存层,避免依赖单一存储系统。
19.Redis如果崩溃了如何快速恢复?
Redis 崩溃后的快速恢复可以通过以下方式实现:
- 持久化设置:开启RDB(快照)或AOF(追加文件)持久化。RDB在周期性保存数据,AOF记录每个写操作。
- 启用AOF重写:定期重写AOF文件,避免文件过大。
- 启动时自动加载持久化数据:Redis启动时会自动加载RDB或AOF文件,恢复数据。
- 定期备份:定期将持久化文件备份到外部存储,防止数据丢失。
- 自动化恢复脚本:在生产环境中,可以编写自动化的恢复脚本,当 Redis 崩溃时,自动触发恢复操作。比如,当 Redis 服务崩溃时,自动重启 Redis 实例,并恢复 RDB 或 AOF 文件。
20.redis是如何部署的?是单个部署还是集群部署?为什么这么做?
单节点部署:
-
单节点部署就是将 Redis 作为一个单独的实例运行,所有的 Redis 数据都存储在一个 Redis 服务器上。适用于数据量较小或负载不高的应用场景。
-
适用场景:
-
数据量相对较小。
-
高可用性和分布式的需求较低。
-
单节点足以承载业务的并发请求。
-
可以快速搭建原型,进行开发和测试。
优点:
- 部署和配置简单,维护方便。
- 性能非常高,因为只有一个 Redis 实例,所有数据存储在内存中,访问速度极快。
- 适用于小规模的项目或对数据一致性要求较低的场景。
缺点:
- 单点故障:如果 Redis 实例崩溃,数据会丢失或者服务不可用,除非启用了持久化。
- 扩展性差:随着数据量的增长,单节点会变得性能瓶颈明显,无法满足大规模数据的需求。
- 容错性差:只有一个实例,如果它出现故障,整个服务会中断。
-
主从复制(主从部署)
一个主节点和一个或多个从节点,主节点负责写操作,从节点负责读操作,实现读写分离,分担主节点的压力。
主从模式的优缺点:
- 优点:实现读写分离,分担主节点的压力。
- 缺点:当主节点宕机时,需要手动切换或等待重启,不具备自动故障恢复功能。
- 适用场景:适用于对数据一致性和高可用性要求不高的场景
==============================================================
一个主节点和一个或多个从节点,数据的写操作都由主节点处理,从节点负责同步主节点的数据。主从复制通常用于 读写分离,即主节点负责写操作,从节点负责读操作。
- 主节点:接收所有写请求,修改数据,并将数据同步到从节点。
- 从节点:从主节点复制数据,可以处理读请求,但不能处理写请求。
优点:
- 负载均衡:通过将读请求分配到从节点,主节点的负载得到减轻,提高整体性能。
- 数据备份:从节点可以作为主节点的备份,主节点崩溃时,可以快速切换到从节点,减少停机时间。
- 高可用性:通过增加从节点,可以提高 Redis 系统的可用性和容错性。
缺点:
- 数据一致性:主节点写入的数据需要通过复制同步到从节点,这会导致主从节点之间可能会有一定的延迟,因此会存在短暂的数据不一致问题。
- 故障转移:主节点崩溃时,必须手动或通过其他机制(如 Redis Sentinel)进行主从切换。
- 当主节点宕机时,需要手动切换或等待重启,不具备自动故障恢复功能。
适用场景:
- 需要高可用、容错和负载均衡的场景。
- 读操作较多,写操作相对较少的应用(如缓存数据库)。
- 用于主节点的备份和高可用性需求。
- (适用于对数据一致性和高可用性要求不高的场景。)
哨兵模式
哨兵系统用于监控多个Redis服务器,当主服务器出现问题时,哨兵会自动进行故障迁移,选举一个新的主服务器。
哨兵模式的优缺点:
- 优点:自动故障迁移,实现高可用性。
- 缺点:配置复杂,需要部署哨兵系统。
- 适用场景:适用于对高可用性有较高要求的场景。
集群部署
Redis Cluster 是 Redis 官方提供的分布式部署方案,支持数据的自动分片。它可以将数据分散到多个节点上,同时提供高可用性和自动故障转移。
Redis Cluster将数据分片存储在多个节点上,实现自动分片和负载均衡。
Redis Cluster通过将键空间分割为16384个槽位,每个主节点负责一部分槽位,实现数据的分布式存储和高可用性
优点:
- 高可用性:Redis Cluster 自动进行故障转移和数据备份。
- 水平扩展:支持数据分片,可以根据需要增加更多的节点以扩展容量。
- 分布式存储:数据分布在多个节点上,提高了存储能力和性能。
缺点:
- 配置复杂:集群配置相对复杂,需要配置多个节点、管理数据分片和故障转移机制。
- 数据一致性问题:由于数据分布在多个节点上,可能会面临短暂的数据不一致和网络分区问题。
适用场景:
-
适用于大规模、高并发、高可用的场景。
-
数据量非常大,单节点无法满足需求。
-
高并发、高可用的分布式场景,如大规模缓存、分布式数据库等。
-
需要对 Redis 进行水平扩展的场景。
部署方式 | 描述 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
单机部署 | Redis运行在单个实例上,数据存储在该实例中。 | 简单易部署,适合小型项目,低成本。 | 单点故障,扩展性差,容灾能力差。 | 小型应用,缓存需求较低的场景。 |
主从复制 | 一个主节点提供写操作,从节点同步主节点数据,支持读操作。 | 提高读取性能,避免单点故障。 | 写操作仍然集中在主节点,主节点压力大。 | 读多写少的场景,提高读性能。 |
哨兵部署 | 使用Redis Sentinel进行监控,自动故障转移。 | 提高高可用性,支持自动故障切换。 | 配置和管理复杂,需要多台机器。 | 对高可用性有需求的场景。 |
集群部署 | Redis Cluster将数据分片存储在多个节点上,实现自动分片和负载均衡。 | 水平扩展性强,支持大规模数据存储。 | 配置复杂,数据迁移和管理较为复杂。 | 大规模分布式应用,需要高吞吐量的场景。 |
总结:
- 单机部署适合小规模、低并发场景。
- 主从复制适合读多写少的场景,提高读取性能。
- 哨兵部署提供高可用性,适合对可靠性要求高的场景。
- 集群部署适合大规模分布式应用,提供水平扩展性和高吞吐量。
21.什么是缓存穿透、缓存击穿、缓存雪崩?如何解决?
雪崩,穿透,击穿,都是高并发场景下,大量请求查询,为找到redis存的数据,直接去查询了数据库,造成数据库压力大大量请求
雪崩:redis大量缓存同时过期,同时请求数据库,造成数据库压力大 解决方案:随机设置缓存的失效时间
击穿:热点数据缓存刚过期,大量请求绕过缓存请求数据库,造成数据库压力大 解决方案: 缓存预热,key永不过期或者使用期间内不过期
穿透:查询缓存和数据库都不存在的数据,导致数据库压力大, 解决方案:缓存空对象,布隆过滤器
问题类型 | 描述 | 解决方案 |
---|---|---|
缓存穿透 | 请求的数据在缓存和数据库中都不存在,导致每次都访问数据库。 | 1. 使用布隆过滤器:快速判断数据是否存在,避免无效请求访问数据库。 2. 缓存空值:对于不存在的数据,缓存一个空值,设置短期过期时间。 |
缓存击穿 | 缓存中的某个热点数据在高并发时失效,多个请求同时访问数据库。 | 1. 互斥锁:多个请求同时请求数据时,只有一个请求去查询数据库,其他请求等待缓存结果。 2. 使用分布式锁:避免多个请求同时访问数据库。 |
缓存雪崩 | 缓存中的大量数据同时过期或失效,导致大量请求同时访问数据库。 | 1. 缓存过期时间错峰:避免缓存数据在同一时间过期。 2. 使用多级缓存:设置不同层级的缓存,减少对数据库的直接访问。 3. 加随机过期时间:避免大批缓存数据在同一时刻失效。 |
详细解释:
- 缓存穿透:指请求的数据既不在缓存中也不在数据库中,导致每次都直接查询数据库。可以通过布隆过滤器来快速判断请求数据是否存在,如果不存在则直接返回,避免无意义的数据库查询。
- 缓存击穿:指缓存中的某个热点数据在高并发下刚好失效,多个请求同时查询数据库,导致数据库压力增大。解决方法是通过互斥锁或分布式锁,确保同一时刻只有一个请求查询数据库,其他请求等待缓存结果。
- 缓存雪崩:指缓存中大量数据同时失效,导致大量请求同时访问数据库,造成数据库压力过大。可以通过设置不同的过期时间、错峰过期策略、增加缓存层级和随机化缓存过期时间来避免。
22.List和set和map的区别?
List
、Set
和 Map
是 Java 中常用的集合类型,它们的区别主要体现在以下几个方面:
特 性 | List | Set | Map |
---|---|---|---|
存储结构 | 有序集合,允许重复元素。 | 无序集合,不允许重复元素。 | 键值对(key-value)集合,键唯一,值可以重复。 |
元素顺序 | 保持插入顺序,可以通过索引访问元素。 | 不保证元素顺序(HashSet)或有序(TreeSet)。 | 键有序(LinkedHashMap、TreeMap),值无序。 |
允许重复 | 允许重复元素。 | 不允许重复元素。 | 键不能重复,值可以重复。 |
访问方式 | 通过索引访问元素。 | 无法通过索引访问,只能通过迭代器遍历。 | 通过键(key)访问对应的值(value)。 |
常用实现 | ArrayList 、LinkedList |
HashSet 、TreeSet 、LinkedHashSet |
HashMap 、TreeMap 、LinkedHashMap |
线程安全 | 默认不安全(CopyOnWriteArrayList 是线程安全的)。 |
默认不安全(CopyOnWriteArraySet 是线程安全的)。 |
默认不安全(ConcurrentHashMap 是线程安全的)。 |
性能特点 | 支持随机访问,查找元素效率高,但插入、删除可能较慢。 | 查找、插入、删除操作效率高,尤其是HashSet 。 |
基于哈希表,插入、查找、删除操作效率高。 |
详细说明:
-
List:
-
是一个有序的集合,元素按照插入顺序排序。
-
允许重复元素,元素可以根据索引位置进行访问。
-
常用实现:
ArrayList
(支持快速随机访问)、LinkedList
(支持高效插入和删除)。 -
有序:存和取的顺序是一致的
有索引:可以通过索引操作元素
可重复:存储的元素可以重复
-
适用场景:当你需要按照顺序存储元素,并且可以有重复元素时使用
List
。如果你需要频繁地根据索引位置访问元素,ArrayList
(基于数组实现)可能是一个好的选择。
-
-
Set:
-
是一个无序集合,不允许重复元素。
-
不保证元素的顺序(
HashSet
),也可以保证元素的顺序(LinkedHashSet
),或者按自然顺序排序、自定义排序(TreeSet
)。 -
常用于去重操作。
-
Set系列集合的实现类
HashSet:无序、不重复、无索引
LinkedHashSet:有序、不重复、无索引
TreeSet:可排序、不重复、无索引
-
适用场景:当你需要保证集合中的元素不重复,且不关心元素的顺序时使用
Set
。如果需要根据元素的自然顺序排序,可以使用TreeSet
。
-
-
Map:
-
是一个键值对集合,每个元素包含一个键和一个值。
-
键(key)必须唯一,值(value)可以重复。
-
Map的实现不保证键值对的顺序,具体顺序取决于实现类
-
常用实现:
HashMap
(无序)、TreeMap
(按键排序)、LinkedHashMap
(保证插入顺序,存储和取出元素的顺序是一致的。)。 -
双列集合一次需要存一对数据,分别为键和值
键不能重复,值可以重复
键和值一一对应,每个键只能找自己的对应的值
键+值这个整体我们成为“键值对”或者“键值对对象”,在Java中叫“Entry对象
-
适用场景:当你需要关联键和值,并且每个键只对应一个值时使用
Map
。例如,用Map
存储学生ID和学生姓名的对应关系。
-
总结:
- List 适用于需要保持元素顺序并允许重复的场景。
- Set 适用于需要去重并且不关心元素顺序的场景。
- Map 适用于需要通过键来映射值的场景,键是唯一的,但值可以重复。
23.Arrarylist,Linkedlist;Arraylist内部扩容机制是怎样的?
ArrayList
和 LinkedList
是 Java 中实现 List
接口的两种常见集合,它们的内部结构和扩容机制有显著区别。下面是它们的对比及 ArrayList
的扩容机制详细说明:
ArrayList vs LinkedList
特性 | ArrayList | LinkedList |
---|---|---|
存储结构 | 基于动态数组 | 基于双向链表 |
访问方式 | 支持通过索引快速随机访问。 | 访问元素需要遍历链表,效率较低。 |
插入/删除效率 | 在末尾插入删除效率高,其他位置的插入/删除较慢 | 插入/删除操作在链表两端高效;在中间低效。 |
内存占用 | 内存消耗较少,只需存储元素。 | 内存消耗多,每个元素需要额外存储前后指针。 |
线程安全 | 默认不安全,可以使用 CopyOnWriteArrayList 实现线程安全。 |
默认不安全,可以使用 CopyOnWriteArrayList 实现线程安全。 |
扩容机制 | 动态数组扩容(具体机制见下文)。 | 不涉及扩容,链表按需动态分配内存。 |
总结
对比点 | ArrayList | LinkedList |
---|---|---|
底层实现 | 动态数组 | 双向链表 |
访问效率 | 随机访问快 | 随机访问慢 |
插入/删除效率 | 尾部操作快,中间操作慢 | 首尾操作快,中间操作较慢 |
内存消耗 | 相对较少 | 较高 |
适用场景 | 读多写少,需随机访问场景 | 写多读少,动态操作频繁场景 |
使用 ArrayList 的场景
- 需要频繁访问元素(随机访问场景,如根据索引读取)。
- 插入和删除操作较少(特别是中间位置的操作)。
- 需要更低的内存占用。
使用 LinkedList 的场景
- 需要频繁插入和删除元素(尤其是在列表的首尾操作)。
- 元素数量动态变化较大,不确定具体容量,避免数组扩容的开销。
- 操作顺序数据(如队列或栈的实现)。
ArrayList 内部扩容机制
ArrayList
使用动态数组作为底层数据结构。当 ArrayList
中的元素数量超过当前数组的容量时,ArrayList
会进行扩容,扩容的过程如下:
扩容是通过 创建一个新的、更大的数组 实现的,而不是直接对原数组扩容。Java 数组的大小是固定的,不能动态调整,因此:无法直接对原数组扩容。
- 初始容量:
- 默认初始容量是 10。如果创建时没有指定容量,
ArrayList
会使用默认容量。 - 可以通过构造函数设置初始容量:
new ArrayList<>(initialCapacity)
。
- 默认初始容量是 10。如果创建时没有指定容量,
- 扩容过程:
- 当
ArrayList
的元素个数超过当前数组的容量时,会进行扩容。 - 扩容的规则是:当前容量的 1.5 倍。
- 比如,初始容量为 10,当元素个数达到 10 时,会扩容到 15;再当元素个数达到 15 时,会扩容到 22(即 15 * 1.5 ≈ 22)。
- 当
- 扩容后的数据拷贝:
- 扩容时,
ArrayList
会创建一个新的数组,将原数组中的元素复制到新数组中(Arrays.copyof()),替换原数组引用,使其指向新数组。 - 这个过程是 O(n),即随着元素数量的增加,扩容的开销也会增大。
- 扩容时,
- 扩容的代价:
- 扩容过程虽然可以保证
ArrayList
的容量随元素增加而动态增长,但每次扩容时都需要进行数组的复制,因此在频繁扩容的情况下可能会对性能产生影响。 - 过度扩容也会导致内存浪费,因此在创建
ArrayList
时如果能预估元素的数量,建议显式指定初始容量。
- 扩容过程虽然可以保证
24.HashMap扩容机制,HashMap的底层原理
-
创建一个默认长度16,默认加载因子为0.75的数组,数组名table
16*0.75 = 12,如果存入的数据达到12,则数组自动扩容为原来的2倍
-
根据元素的哈希值跟数组的长度计算出元素应存入的位置
int index = (数组长度-1) & 哈希值;
-
如果位置为null,直接存入
-
如果位置不为null 表示有元素,则调用equals方法比较属性值
一样:不存 不一样:存入数组,形成链表
jdk8以前:如果在数组中的同一个位置插入不同的元素的话,新元素存入数组,老元素挂在新元素下面,形成链表
jdk8以后:如果在数组中的同一个位置插入不同的元素的话,新元素直接挂在老元素下面
-
jdk8以后,当链表长度超过8,且数组长度大于等于64时,链表自动转换为红黑树。
-
如果集合中存储的时自定义对象,必须重写hashCode方法和equals方法。
HashMap 扩容机制
HashMap
是 Java 中一个非常常用的键值对集合,它基于哈希表(Hash Table)实现,具有很高的查找、插入效率。HashMap
的扩容机制主要包括以下几个关键点:
- 初始容量(Initial Capacity):
- 默认初始容量为 16,但可以通过构造函数设置。
- 构造函数:
new HashMap<>(initialCapacity)
,其中initialCapacity
是初始容量。
- 负载因子(Load Factor):
- 负载因子决定了 HashMap 的何时扩容。默认负载因子为 0.75,表示当 HashMap 中的元素个数超过
容量 * 负载因子
时,就会触发扩容。 - 例如,初始容量为 16,负载因子为 0.75,那么当元素个数超过 12(16 * 0.75)时,HashMap 会扩容。
- 负载因子决定了 HashMap 的何时扩容。默认负载因子为 0.75,表示当 HashMap 中的元素个数超过
- 扩容机制:
- 扩容时,
HashMap
会将容量扩大为原来的 2 倍,并重新计算所有元素的哈希值并重新分配到新的桶中。 - 这意味着,扩容后,HashMap 的容量将变为
容量 * 2
。 - 扩容的代价是 O(n),因为需要重新计算每个元素的哈希值并将它们重新放入新的数组中。
- 扩容时,
- 扩容触发条件:
- 当元素个数大于
容量 * 负载因子
时触发扩容。 - 举例来说,如果初始容量为 16,负载因子为 0.75,当元素个数超过 12 时触发扩容(16 * 0.75 = 12)。
- 当元素个数大于
- 扩容时的性能问题:
- 由于扩容时需要将所有元素重新计算哈希位置并放入新的数组,因此扩容是一个较为耗时的操作,尤其是当元素非常多时。
- 为了避免频繁扩容,通常可以根据预期元素数量来调整初始容量。
HashMap 的底层原理
HashMap
底层是基于数组和链表/红黑树(JDK 8及以后)实现的,具有以下几个重要组件和工作机制:
- 数组 + 链表/红黑树:
HashMap
内部维护了一个数组(称为桶数组),每个桶可以存储多个键值对。当发生哈希冲突时,多个元素会被存储在同一个桶中,通常通过链表(JDK 7及以前)或红黑树(JDK 8及以后)来解决冲突。
- 哈希桶(Bucket):
HashMap
使用哈希函数计算键的哈希值,然后将键值对放入相应的桶中。桶的下标是通过hash(key) % array.length
计算得到的。- 如果两个键的哈希值相同,它们会被放入同一个桶,这就是哈希冲突。
- 链表(在哈希冲突时):
- 在 JDK 7 及以前,如果两个元素的哈希值相同,它们会被放在同一个桶的链表中。
- 查找时,链表的长度决定了查找的效率,链表越长,查找的时间复杂度越高。
- 红黑树(JDK 8及以后):
- 从 JDK 8 开始,如果某个桶中的元素个数超过 8 且数组的大小超过 64,
HashMap
会将链表转换为红黑树。 - 红黑树是一种自平衡的二叉查找树,它可以保证最坏情况下的查找时间复杂度为 O(log n),大大提高了性能。
- 从 JDK 8 开始,如果某个桶中的元素个数超过 8 且数组的大小超过 64,
- 哈希函数:
- 哈希函数是
HashMap
关键的性能因素。Java 使用了hashCode()
方法和扰动函数来计算键的哈希值,从而将元素均匀地分布到不同的桶中。 HashMap
使用扰动函数(如hash ^ (hash >>> 16)
)来减少哈希冲突,确保哈希值分布更均匀。
- 哈希函数是
hashmap的底层是面试的重点详细请参考:https://blog.csdn.net/weixin_47084555/article/details/122351835
25.Hashmap的底层用什么存储的?线程是否安全?
HashMap
的底层是基于哈希表的结构来实现的,jdk8以前哈希表由数组+链表组成,jdk8以后哈希表由数组+链表+红黑树组成。
HashMap
的底层是基于 数组 + 链表 + 红黑树 的结构实现的。
- 数组:
HashMap
底层维护了一个数组,称为 哈希桶(bucket)。- 每个数组的元素是一个链表或红黑树的头结点,存储着键值对(
Entry
或Node
对象)。
- 链表:
- 当两个或多个键计算出的哈希值相同时,它们会被存储在同一个桶中,形成链表。
- 链表中每个节点存储一个键值对。
- 红黑树(从 JDK 8 开始):
- 当链表的长度超过 8 且数组容量大于 64 时,链表会转换为红黑树。
- 红黑树的查找效率为 O(log n),相比链表(O(n))效率更高。
线程安全性
-
HashMap 是非线程安全的:
HashMap
在多线程环境下可能会导致数据不一致,例如在并发修改时会产生竞态条件。- 在扩容过程中,可能会导致循环引用(JDK 7 中的死循环问题),从而引发程序崩溃。
-
解决线程安全问题的方法:
-
**使用 **
**Collections.synchronizedMap()**
:-
可以通过
Collections.synchronizedMap()
方法包装一个HashMap
,使其线程安全。该方法返回一个线程安全的
Map
,它通过 同步 的方式来保证线程安全。
-
-
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
2. 使用ConcurrentHashMap:
ConcurrentHashMap 是 HashMap的线程安全版本,支持高效的并发操作。
它通过分段锁(JDK 7)或 CAS 和细粒度锁(JDK 8)实现了线程安全。
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
3. 手动同步:
在访问 HashMap 时手动添加同步代码块。
synchronized (hashMap) {
hashMap.put("key", "value");
}
总结
特性 | 描述 |
---|---|
底层结构 | 数组 + 链表 + 红黑树 |
线程安全性 | 非线程安全 |
线程安全解决方案 | 使用 Collections.synchronizedMap() 或 ConcurrentHashMap |
26.final和finally和finalize的区别
final
、finally
和 finalize
是 Java 中三个不同的概念,尽管它们有相似的词根,但各自有不同的用途。
final
- 用途:
final
是一个修饰符,用于定义常量、方法、类和局部变量。 - 适用场景:
- 常量:声明常量(即不可修改的变量):
final int MAX_VALUE = 100;
- 方法:表示方法不能被重写(不可覆写的方法):
public final void myMethod() {
// 方法内容
}
- 类:表示类不能被继承:
public final class MyClass {
// 类内容
}
- 局部变量:声明局部变量为常量(赋值后不可修改):
final int x = 10;
finally
- 用途:
finally
是用于异常处理的关键字,表示一块无论是否发生异常都会执行的代码块。 - 适用场景:
finally
用于try-catch
语句中,保证无论是否有异常发生,finally
中的代码都会执行,常用于资源释放(例如关闭文件流)。
try {
// 代码块
} catch (Exception e) {
// 异常处理
} finally {
// 无论如何都会执行的代码块(例如:资源释放)
System.out.println("This is always executed.");
}
finalize
- 用途:
finalize
是Object
类中的一个方法,用于垃圾回收器回收对象之前进行清理工作。该方法是 自动调用 的,但并不能保证一定会在对象被回收时被调用。 - 适用场景:
finalize
方法在对象被垃圾回收之前自动调用,通常用来释放资源或进行其他清理工作。- 注意:
finalize