admin 管理员组文章数量: 1184232
Redis
Redis全称(Remote Dictionary Server)本质上是一个Key-Value类型的内存数据库,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。 Redis的出色之处不仅仅是性能,
Redis最大的优点是支持保存多种数据结构,此外单个value的最大限制是1GB因此Redis可以用来实现很多有用的功能,比方说用他的List来做FIFO双向链表,实现一个轻量级的高性 能消息队列服务,用他的Set可以做高性能的tag系统等等。另外Redis也可以对存入的Key-Value设置expire时间,
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上
redis基础知识和数据结构
redis的主要数据结构
redis的结构主要有八种:
- String: 用于存储简单的键值对数据,适合缓存、计数器、会话数据等场景。
- List: 支持双向链表,适合消息队列、任务列表等需要顺序存取的场景。
- Set: 无序集合,适用于去重、标签、好友列表等场景。
- Sorted Set: 内部以分数排序的集合,适合排行榜、延时队列等场景。
- Hash: 键值对集合,适合存储对象、用户信息等场景。
- Bitmap、HyperLogLog、Geospatial(Geo) 等:分别用于大规模二进制位存储、基数统计和地理位置相关的应用。
这些数据结构各具特点,能满足不同场景下对数据存储、查询和排序的需求。
redis的主要特点是什么?为什么怎么快?
1.键值型数据库,value支持多种数据结构
2.单线程,每个命令都有原子性(redis是纯内存操作,执行速度非常的快。它的性能瓶颈不是执行速度,而是网络延迟,变成多线程并不会带来多大的性能提升。
多线程会导致上下文切换,带来不必要的开销,还有线程安全问题)
3.采取内存操作,并且IO复路多用(允许单个线程或进程同时监控多个I/O流(如网络套接字或文件描述符)的状态变化,从而在任何一个I/O操作就绪时通知应用程序进行处理。)
4.支持数据持久化
redis中Zset是什么?他的底层数据结构是什么?
Redis 中的 Zset(有序集合)是一种集合数据结构,其中每个成员都会关联一个浮点数的分数(score),元素按 score 从小到大排序。Zset 的底层主要采用两种数据结构:
-
字典(Hash Table): 用于存储每个成员和它对应的分数,方便通过成员名称快速查找对应的 score。
-
跳表(Skip List): 用于按 score 顺序存储所有元素,支持高效的范围查询、排序以及有序遍历。
什么是跳表?
- 跳表(Skip List)是一种基于概率的动态数据结构,主要用于有序数据的高效查找、插入和删除操作,其时间复杂度平均为 O(log n)。下面是关于跳表的详细说明:
- 结构组成:
- 跳表由多层链表组成,最底层是一条完整的有序链表,每一层都包含部分节点,这些高层链表相当于对底层链表的“索引”。每个节点可能会在多个层级中出现,这些层级通过随机算法决定。
- 工作原理:
- 查找过程: 从最高层开始,顺着链表向右查找,直到发现下一个节点大于或等于目标值,然后下移到下一层继续查找,直至到达最底层。
- 插入和删除: 插入时,首先查找插入位置,然后根据随机算法决定新节点应插入的层数,并在相应层级中更新指针;删除时则需要在所有包含该节点的层中移除节点指针。
- 优点:
- 实现相对简单,不需要像红黑树等平衡二叉树那样复杂的旋转操作。
- 在大多数情况下能提供与平衡树相当的性能(O(log n)的时间复杂度)。
- 空间利用率较高,且能很好地支持范围查询等操作,这也是 Redis 的有序集合(Zset)选择跳表作为底层数据结构的原因之一。
- 应用场景:
- 跳表非常适用于需要频繁查找、插入和删除有序数据的场景,例如缓存系统、排行榜、数据库索引等。在 Redis 中,跳表与字典结构结合使用,既保证了成员的快速查找,又支持按分数排序进行高效的范围查询。
redis持久化
Redis 是如何实现数据持久化的?两种持久化机制各有什么优缺点?
Redis 提供两种主要的持久化方式:
-
RDB(快照): 定期将内存中的数据快照保存到磁盘上。优点是对数据恢复速度较快,文件体积较小;缺点是持久化间隔期间数据可能丢失,不适合对数据实时性要求很高的场景。
-
AOF(追加文件): 记录所有写命令并追加到日志文件中。优点是数据恢复更精确,丢失的数据非常少;缺点是文件较大,重写 AOF 文件时会增加额外 I/O 压力。
在实际使用中,很多系统会根据业务场景选择其中一种,也有混合使用两种持久化方式以兼顾性能与数据安全。
radis的数据一致性相对于本地缓存比较好,是怎么保证redis和数据库的数据一致性?
- 缓存旁路(Cache-Aside)模式:
- 应用读取数据时,先从 Redis 缓存获取,如果命中则直接返回;如果未命中,则从数据库中查询数据并将结果写入 Redis。
- 对于写操作,通常先更新数据库,然后删除或更新对应的缓存记录。这样可以保证下一次读取时,缓存会重新加载最新数据。
- 这种方式虽然可能出现短暂的不一致,但经过缓存失效后能实现最终一致性。
- 写穿(Write-Through)和写回(Write-Behind)模式:
- 写穿缓存: 每次写操作同时更新 Redis 和数据库,保证两边数据一致,但这会牺牲一定的性能。
- 写回缓存: 先写入 Redis,异步将更新数据写入数据库;这种方式能提高写入性能,但需要设计合适的容错和补偿机制,确保异步写入过程不丢失数据。
- 分布式锁和事务控制:
- 在高并发场景下,通过分布式锁(例如 Redis 的 setnx)来保护更新操作,确保对同一数据的并发写操作不会导致数据不一致。
- 配合数据库的事务机制和乐观锁策略,可以进一步防止并发更新冲突。
- 异步消息队列:
- 当需要将大量写操作异步同步到数据库时,可以利用消息队列(例如 Kafka、Redis Stream 等)将更新操作异步处理,从而缓解数据库压力,同时通过幂等性设计确保数据一致。
- 缓存失效与定时同步机制:
- 通过设置合理的 TTL,使缓存数据在一定时间内自动失效,从而定期从数据库加载最新数据。
- 另外可以通过后台任务定时同步数据库和缓存,确保数据的最终一致性。
redis缓存更新机制、缓存穿透、缓存击穿、缓存雪崩
缓存更新机制
当我们从数据库中拿到新数据时,redis缓存还可能存放旧的数据,有三种更新策略
- 内存淘汰:当内存不足时自动淘汰旧数据,下次查询时更新。维护成本低,但数据一致性差
- 超时剔除:给数据增加TTl时间,到期后,自动剔除。一致性一般,成本低
- 主动更新:自己编写业务逻辑,更新数据库的同时更新redis缓存:
-
缓存旁路(Cache-Aside)模式:
- 应用读取数据时,先从 Redis 缓存获取,如果命中则直接返回;如果未命中,则从数据库中查询数据并将结果写入 Redis。
- 对于写操作,通常先更新数据库,然后删除或更新对应的缓存记录。这样可以保证下一次读取时,缓存会重新加载最新数据。
- 这种方式虽然可能出现短暂的不一致,但经过缓存失效后能实现最终一致性。
-
缓存穿透
-
问题描述:
缓存穿透是指请求查询的数据既不在缓存中,也不存在于数据库中。攻击者或恶意请求者可以通过大量请求不存在的 key,使得每次请求都直接访问数据库,从而对数据库造成压力。 - 解决方案:
- 布隆过滤器: 利用布隆过滤器在请求到达数据库前先判断该 key 是否可能存在,从而拦截不存在的请求。
- 缓存空对象: 对于查询结果为空的数据,也将其写入缓存,并设置短时间过期,避免频繁查询数据库。
缓存击穿(热点Key问题)
-
问题描述:
缓存击穿是指某个热点数据在缓存失效后,短时间内大量并发请求直接打到数据库上,导致数据库压力骤增,甚至崩溃。这种情况通常发生在缓存设置了相同的过期时间,热门 key 同时失效。 - 解决方案:
- 热点数据预热与延长 TTL: 针对热点数据设置较长的过期时间,或在失效前主动刷新缓存。
- 随机过期时间: 在设置缓存过期时间时增加一个随机值,分散大量 key 同时失效的可能性。
- 互斥锁: 当缓存失效时,通过分布式锁控制只有一个请求更新缓存,其他请求等待缓存更新后再访问数据库。
缓存雪崩
-
问题描述:
缓存雪崩指的是在某个时刻,大量缓存同时失效或因 Redis 服务宕机而导致缓存不可用,结果使得所有请求直接打到数据库上,引起数据库压力激增,系统整体性能迅速下降。 - 解决方案:
- 合理设置过期时间: 分散不同 key 的过期时间,避免大量缓存同时失效。
- 多级缓存机制: 在 Redis 之外引入本地缓存或其他中间层,形成多级缓存,提供备用缓存。
- 降级限流策略: 当缓存服务不可用时,系统可以提前进行限流或返回默认值,保护数据库不被瞬间淹没。
- 集群部署与容错: 部署 Redis 集群,保证单点故障不会导致整个缓存层不可用。
Redis主从、哨兵、集群
Redis 主从复制(Master-Slave Replication)
- 基本概念:
- 主从复制指的是一个 Redis 实例作为主节点(Master),负责写操作;一个或多个 Redis 实例作为从节点(Slave),负责复制主节点的数据,主要用于读操作扩展和数据备份。
- 特点与优势:
- 读扩展: 从节点可以分担主节点的读取请求,提高系统的并发处理能力。
- 数据备份: 从节点持有主节点的数据副本,能在主节点故障时提供数据恢复支持。
- 简单易用: 配置相对简单,适合对数据一致性要求不是非常高的场景。
- 局限性:
- 数据同步是异步进行的,可能会出现短暂的数据不一致。
- 主节点写压力集中,不适合大规模写操作场景。
Redis 哨兵(Sentinel)
- 基本概念:
- Redis 哨兵是一个高可用性解决方案,它负责监控主从架构中各个节点的状态,自动进行故障检测和主从切换(failover)。
- 主要功能:
- 监控: 定期检查主节点和从节点的运行状态。
- 通知: 在节点出现故障时,通知管理员或其他系统组件。
- 自动故障转移: 当主节点不可用时,自动将其中一个从节点升级为主节点,并更新配置。
- 服务发现: 客户端可以通过哨兵获取当前主节点的信息,确保连接到正确的节点。
- 特点与优势:
- 提供了自动故障转移功能,提高系统高可用性。
- 不需要改动应用程序的业务逻辑,哨兵集群独立工作,协同管理 Redis 实例。
- 局限性:
- 哨兵本身需要部署多个实例,且在某些场景下可能存在短暂的切换延迟。
- 对于大规模写操作场景,其自动故障转移可能带来数据丢失风险,因为复制是异步的。
Redis 集群(Cluster)
- 基本概念:
- Redis 集群将数据分片存储在多个节点上,每个节点只存储部分数据,通过分布式哈希槽将数据均匀分布到各节点,实现数据水平扩展和高可用。
- 主要特点:
- 数据分片: Redis 集群将 16384 个哈希槽分配到各个节点,每个节点负责一部分槽内数据,从而支持大规模数据存储。
- 高可用性: 集群模式支持主从复制和故障转移,单个节点故障不会影响整个集群的运行。
- 无中心架构: 集群中的每个节点都是平等的,没有单点故障,客户端可以直接根据哈希槽路由请求。
- 优点:
- 能够横向扩展,适合海量数据存储和高并发场景。
- 自动分片和容错机制,使得系统在节点故障时依然能够继续工作。
- 局限性:
- 配置和管理相对复杂,需要考虑数据分布、槽迁移等问题。
- 某些操作(如跨槽事务)受限于集群模式的设计,不支持全局事务。
分布式锁
什么是分布式锁?
分布式锁是一种在分布式系统中用来控制多个进程或节点对共享资源进行互斥访问的机制。它保证在同一时刻只有一个客户端能够持有锁,从而避免并发修改造成的数据不一致或资源竞争问题。常见应用场景包括订单处理、秒杀系统、分布式缓存更新等。
Redis 分布式锁的原理
- 原子操作:
- 使用 Redis 的
SET NX PX命令保证加锁操作的原子性,确保在高并发场景下,只有一个客户端能够成功设置锁。
- 使用 Redis 的
- 过期机制:
- 为了避免某个客户端因异常或网络问题持有锁而导致系统死锁,在加锁时设置超时时间,保证锁会在超时后自动释放。
- 唯一标识:
- 每个客户端在获取锁时生成唯一标识(例如 UUID),在释放锁时进行校验,确保只有持有该锁的客户端才能释放锁,防止误删。
- 解锁原子性:
- 通过 Lua 脚本保证检查与删除锁的操作是原子执行的,从而防止并发环境下锁被错误释放。
Redission实现分布式锁的原理
-
基于 Redis 原子操作:
Redisson 使用 Redis 的 SET 命令(带 NX 和 PX 参数)来尝试设置锁键,这保证了在高并发场景下只有一个客户端能成功设置该键。每个锁都会关联一个唯一标识(如 UUID),以便后续校验释放时确保只有持有者能释放锁。 -
自动续期机制(Watchdog 机制):
为防止业务执行过程中锁超时失效(尤其是当业务时间超过预设 TTL 时),Redisson 内置了看门狗机制:- 在获取锁成功后,会启动一个后台定时任务,周期性地检查锁是否还处于持有状态,并在必要时自动延长锁的 TTL。
- 这种自动续期机制可以确保只要业务还在执行,锁就不会意外失效,从而避免误释放问题。
-
可重入锁:
Redisson 的分布式锁支持可重入,即同一个线程或同一请求在持有锁期间可以多次调用加锁操作而不会被阻塞。内部通过记录锁的重入次数来实现这一特性,确保只有在重入次数归零时才真正释放锁。 -
解锁的原子性:
Redisson 在释放锁时,通过 Lua 脚本来保证解锁操作的原子性。它会检查锁键的值是否与当前线程持有的唯一标识一致,只有匹配的情况下才执行删除操作,从而防止误删他人持有的锁。 -
扩展的锁模式支持:
除了基本的互斥锁,Redisson 还提供了公平锁、读写锁、信号量等多种分布式同步原语,所有这些锁都基于类似的实现机制,但在细节上做了相应扩展以满足不同业务场景的需求。
如何解决高并发场景
- 前端负载均衡,如果是web,可以前后端分离,cdn处理静态资源。
- ngnix7层负载均衡,转发到多个应用服务器。
- 在数据库之前加上redis缓存。
- 数据库读写分离。主从部署。
- 如果有秒杀场景,再通过消息队列对请求进行削峰再到达缓存层。
- 机器配置提升,代码优化,使用并发编程,使用异步IO或者多路复用
Redis数据和MySQL数据库的一致性如何实现
一、 延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒(根据具体的业务时间来定)
4)再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。
二、设置缓存的过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
三、如何写完数据库后,再次删除缓存成功?
上述的方案有一个缺点,那就是操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。这里,我们需要提供一个保障重试的方案。
Java相关基础知识
java的三个特征
封装:对象只需要选择性的对外公开一些属性和行为。
继承:子对象可以继承父对象的属性和行为,并且可以在其之上进行修改以适合更特殊的场景需求。
多态:允许不同类的对象对同一消息做出响应。
java的基本类型
- 整型:int
- 长整型:long
- 短整型:short
- 字符型:char
- 浮点型:double(8)、float(4)
- 布尔型:booler
Static
static 关键字用于声明“静态成员”,包括静态变量(类变量)、静态方法、静态代码块和静态内部类。
- 静态方法可以通过
类名.方法名()的方式直接调用,也可通过类的实例调用(但不推荐)。 - 静态方法在类加载时就与类一起存在于方法区中,不依赖任何实例对象,非静态方法则要等到创建对象后,才会在堆中分配对应的信息。
- 在静态方法内部,不能直接访问非静态字段或调用非静态方法,因为此时没有任何特定对象,只能访问其他静态字段或静态方法。
- 继承:子类会继承父类的静态方法,但静态方法是“隐藏”(hide),不是多态。
- 重写:不能真正“重写”静态方法,只能在子类中定义同名同签名的静态方法,它会隐藏父类方法,调用时根据引用类型决定调用哪一个。
!!可以看见Myclass中的静态方法hello,可以直接Myclass.hello()调用,而非静态方法hello1(),必须要实例化以后才可以调用
!!静态方法不能直接访问非静态方法和字段,可以看见非静态类Myclass无法实例化,费那个太字段x也无法访问
重写和重载的区别
- 重载发生在本类,重写发生在父类与子类之间
- 重载的方法名必须相同,重写的方法名相同且返回值类型必须相同
- 重载的参数列表不同,重写的参数列表必须相同
- 重写的访问权限不能比父类中被重写的方法的访问权限更低
- 构造方法不能被重写
String、StringBuilder、StringBuffer的区别及使用场景
String一旦定义就不可改变,可空赋值。操作少量数据时使用,每次操作都会生成新的 String 对象,将指针指向新的 String 对象
StringBuilder 可改变,线程不安全。操作单线程大量数据时使用。
StringBuffer 可改变,线程安全。操作多线程大量数据时使用。
StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,
只是StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。
在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全而StringBuffer则每次都需要判断锁,效率相对更低
抽象类
抽象类使用关键字 abstract 声明,其主要作用是为子类提供公共的成员变量和方法,并对某些方法仅提供方法签名而不提供具体实现。由于抽象类不能直接实例化,它通常作为基类使用。
在抽象类中可以包含抽象方法,这类方法不包含方法体,必须由子类具体实现。也可以包含普通的(具体的)方法,这些方法有完整的实现,子类可以直接使用或进行重写。
// 定义一个抽象类
public abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;}
// 抽象方法:各个动物的叫声不同,必须由子类实现
public abstract void makeSound();
// 具体方法:通用行为,可以直接使用
public void sleep() {
System.out.println(name + " is sleeping.");}}
// 子类实现抽象类中的抽象方法
class Dog extends Animal {
public Dog(String name) {
super(name);}
@Override
public void makeSound() {
System.out.println(name + " barks.");}}
class Cat extends Animal {
public Cat(String name) {
super(name);}
@Override
public void makeSound() {
System.out.println(name + " meows.");}}
public class Main {
public static void main(String[] args) {
// 无法直接实例化抽象类 Animal
// Animal animal = new Animal("Generic Animal"); // 编译错误
Animal dog = new Dog("Buddy");
Animal cat = new Cat("Kitty");
dog.makeSound(); // 输出:Buddy barks.
cat.makeSound(); // 输出:Kitty meows.
dog.sleep(); // 输出:Buddy is sleeping.
cat.sleep(); // 输出:Kitty is sleeping.}}
接口
接口(Interface)是一种用于定义类行为规范的引用类型。接口规定了一组抽象方法(以及常量或从 Java 8 之后的默认方法与静态方法),它们描述了类可以“做什么”,而不关心这些行为的具体实现细节,具体实现逻辑有继承的接口去实现。
接口定义了一组类应该遵循的行为,可以看作是一种“合同”,任何实现该接口的类都必须实现接口中声明的方法,从而保证了行为的一致性。一个类可以实现多个接口
public interface Shape {
// 定义常量(默认 public static final)
double PI = 3.141592653589793;
// 抽象方法:不需要加 abstract 关键字,默认就是 public abstract
double getArea();
double getPerimeter();}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override//具体实现了getArea方法
public double getArea() {
return PI * radius * radius;}
@Override
public double getPerimeter() {
return 2 * PI * radius;}}
接口与抽象类的区别
接口:在 Java 8 之前,只能包含抽象方法;Java 8 以后可以包含默认方法和静态方法。
抽象类:可以包含抽象方法和具体方法(有完整实现)。
接口:一个类可以实现多个接口。
抽象类:一个类只能继承一个抽象类。
接口:只能包含 public static final 类型的常量。
抽象类:可以有非静态成员变量,通过构造方法进行初始化。
接口:没有构造方法,不能实例化。
抽象类:可以有构造方法,但不能直接实例化,只能通过子类调用父类构造器。
集合是什么
什么是自动拆装箱 int和Integer有什么区别
基本数据类型,如int,float,double,boolean,char,byte,不具备对象的特征,不能调用方法。
- 装箱:将基本类型转换成包装类对象
- 拆箱:将包装类对象转换成基本类型的值
而自动装箱和拆箱功能多发生于Java集合中,因为Collection中的List集合只能存放对象,不能存放基本类型,所以要装箱。
而包装对象和基本类型的区别主要又:
Integer是int的包装类,int则是java的一种基本数据类型
Integer变量必须实例化后才能使用,而int变量不需要
Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
Integer的默认值是null,int的默认值是0
!!==和equals区别
==
基本数据类型比较如 int、float、boolean 等,== 用来比较它们的值是否相等。
引用类型比较(即对象),== 比较的是引用(或地址)是否相等,也就是两个变量是否指向内存中的同一个对象。
int a = 5;
int b = 5;
System.out.println(a == b); // 输出 true
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // 输出 false,因为 s1 和 s2 指向不同的对象实例
System.out.println(s1.equals(s2)); // 输出 true,因为字符串内容相同
equals() 方法
equals() 方法是 java.lang.Object 类中的一个方法,默认实现与 == 相同,即比较对象的引用。但是很多类会重写这个方法,使其比较对象的内容或逻辑相等性。String 类重写了 equals() 方法来比较字符串的内容,所以两个内容相同的字符串会被认为是相等的。
String s1 = new String("world");
String s2 = new String("world");
System.out.println(s1.equals(s2)); // 输出 true
//如果你设计了自己的类,并希望能够比较两个对象的“逻辑”相等性,则需要重写 equals() 方法(同时建议重写 hashCode() 方法,以满足 Java 的通用约定)
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
public static void main(String[] args) {
// 创建两个具有相同内容的 Person 对象
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Alice", 30);
// 创建一个与 person1 指向同一对象的引用
Person person3 = person1;
// 使用 == 比较
System.out.println(person1 == person2); // 输出: false
System.out.println(person1 == person3); // 输出: true
// 使用 equals() 比较
System.out.println(person1.equals(person2)); // 输出: true
System.out.println(person1.equals(person3)); // 输出: true
}
Final的作用
- final 修饰的类叫最终类,该类不能被继承。
- final 修饰的方法不能被重写。
- final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
ArrarList和LinkedList区别
ArrayList与LinkedList都是实现了List接口的集合类,所以在声明的时候建议以List接口声明变量,并且声明泛型,避免编译器检查数据时,进行强制类型转换
List<Integer> list = new ArrayList<>();
List<Integer> list = new LinkedList<>();
ArrayList是基于动态数组的实现,适合频繁读取操作,随机访问快,但在中间插入删除时可能较慢。LinkedList是基于双向链表的实现,在插入和删除操作上具有优势,尤其是在元素移动较多时,但随机访问较慢且内存开销更大。
泛型
在使用泛型之前,集合(如List、Map)通常存储Object类型的数据,在取出元素时需要进行强制类型转换,这不仅冗长,而且容易产生运行时ClassCastException。泛型使得在编译时就能检查类型,降低了出错的风险。
- 泛型提供了编译期的类型安全检查,大大降低了运行时错误的风险。
- 泛型提高了代码复用性和可读性,使得算法和数据结构设计更加通用。
- 通过边界和通配符,可以设计更灵活的API,满足各种场景下的类型需求。
- 类型擦除机制是Java泛型的一个重要特性,它保证了与旧代码的兼容性,但也带来了一些运行时限制
字符流和字节流
java反射
反射的作用是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;其中class代表的时类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组成部分。
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法
Java中面向对象的设计原则
SOLID 设计原则旨在使软件设计更健壮、灵活和易于维护:
-
单一职责原则(SRP:Single Responsibility Principle)
每个类应该只有一个单一的职责,即只负责完成一种功能。这样在需求变化时,只需要修改对应的类,降低维护成本。例如,一个用户管理类只负责用户数据的操作,而不应同时处理日志记录或权限管理。 -
开闭原则(OCP:Open/Closed Principle)
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。通过抽象和接口机制,可以在不改变原有代码的情况下,通过继承或组合扩展功能。例如,通过接口定义规范,不同实现类可以在不修改客户端代码的情况下替换或扩展。 -
里氏替换原则(LSP:Liskov Substitution Principle)
子类对象应该能够替换父类对象而不改变程序的正确性。即继承关系中,子类必须保持父类的行为契约,不应破坏父类的方法预期。例如,一个正方形类如果继承自矩形类,在方法调用时必须符合矩形的逻辑,否则可能引发逻辑错误。 -
接口隔离原则(ISP:Interface Segregation Principle)
不应该强迫一个客户端依赖它不需要的接口。大而全的接口应该拆分成多个小接口,使得实现类只需实现自己真正需要的方法。这样既能减少不必要的实现负担,也使得接口更加灵活和稳定。 -
依赖倒置原则(DIP:Dependency Inversion Principle)
高层模块不应依赖低层模块,两者都应依赖抽象;抽象不应依赖于细节,细节应依赖于抽象。通过依赖注入、接口或抽象类,将具体实现与业务逻辑解耦,提升系统灵活性和可测试性。
线程的状态
- 创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
- 就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
- 阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
- 死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
sleep方法和wait方法
锁的释放
sleep():线程休眠时不会释放锁,其他线程必须等待锁释放后才能执行同步代码。wait():线程等待时立即释放锁,其他线程可以立即获取锁执行同步代码。
唤醒机制
sleep():休眠结束后自动恢复,无需唤醒。wait():必须通过notify()/notifyAll()显式唤醒,否则线程会无限等待。
调用位置
sleep():可在任何位置调用,无需同步块。wait():必须在同步块中调用,否则抛出IllegalMonitorStateException。
所属类
sleep():Thread类的静态方法。wait():Object类的实例方法。public class SleepVsWaitDemo { private static final Object lock = new Object(); // 共享锁对象 public static void main(String[] args) throws InterruptedException { // 示例1:使用 sleep(),不释放锁 Thread sleepThread = new Thread(() -> { synchronized (lock) { System.out.println("sleepThread 获取锁,开始休眠3秒(不释放锁)"); try { Thread.sleep(3000); // 休眠3秒,不释放锁 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("sleepThread 休眠结束,释放锁"); } }); Thread waitingThread1 = new Thread(() -> { System.out.println("waitingThread1 尝试获取锁..."); synchronized (lock) { System.out.println("waitingThread1 成功获取锁"); } }); // 示例2:使用 wait(),释放锁 Thread waitThread = new Thread(() -> { synchronized (lock) { System.out.println("waitThread 获取锁,调用 wait() 释放锁并等待"); try { lock.wait(); // 释放锁并等待,需其他线程唤醒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("waitThread 被唤醒,重新获取锁"); } }); Thread waitingThread2 = new Thread(() -> { synchronized (lock) { System.out.println("waitingThread2 获取锁,唤醒 waitThread"); lock.notify(); // 唤醒等待的线程 } }); // 执行示例1 System.out.println("===== 测试 sleep() 不释放锁 ====="); sleepThread.start(); Thread.sleep(100); // 确保 sleepThread 先启动 waitingThread1.start(); // 等待示例1完成 sleepThread.join(); waitingThread1.join(); // 执行示例2 System.out.println("\n===== 测试 wait() 释放锁 ====="); waitThread.start(); Thread.sleep(100); // 确保 waitThread 先启动 waitingThread2.start(); } }-
示例1 展示了
sleep()导致其他线程阻塞等待锁,直到休眠结束。 -
示例2 展示了
wait()释放锁后,其他线程能立即获取锁并通过notify()唤醒等待线程。
notify()和notifyALL()
public class NotifyVsNotifyAllDemo {
private static final Object lock = new Object(); // 共享锁对象
public static void main(String[] args) throws InterruptedException {
// 创建 3 个等待线程
Runnable waitTask = () -> {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " 进入等待状态");
lock.wait(); // 释放锁并等待
System.out.println(Thread.currentThread().getName() + " 被唤醒并继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建 3 个等待线程
Thread t1 = new Thread(waitTask, "Thread-1");
Thread t2 = new Thread(waitTask, "Thread-2");
Thread t3 = new Thread(waitTask, "Thread-3");
// ----------------- 测试 notify() -----------------
System.out.println("===== 测试 notify() =====");
t1.start();
t2.start();
t3.start();
// 确保所有线程进入等待状态
Thread.sleep(100);
synchronized (lock) {
System.out.println("\n主线程调用 notify()");
lock.notify(); // 随机唤醒一个线程
}
Thread.sleep(1000); // 等待结果输出
// ----------------- 测试 notifyAll() -----------------
System.out.println("\n===== 测试 notifyAll() =====");
t1 = new Thread(waitTask, "Thread-A");
t2 = new Thread(waitTask, "Thread-B");
t3 = new Thread(waitTask, "Thread-C");
t1.start();
t2.start();
t3.start();
Thread.sleep(100);
synchronized (lock) {
System.out.println("\n主线程调用 notifyAll()");
lock.notifyAll(); // 唤醒所有线程
}
Thread.sleep(1000); // 等待结果输出
}
}
===== 测试 notify() =====
Thread-1 进入等待状态
Thread-3 进入等待状态
Thread-2 进入等待状态
主线程调用 notify()
Thread-1 被唤醒并继续执行
===== 测试 notifyAll() =====
Thread-A 进入等待状态
Thread-B 进入等待状态
Thread-C 进入等待状态
主线程调用 notifyAll()
Thread-3 被唤醒并继续执行
Thread-C 被唤醒并继续执行
Thread-B 被唤醒并继续执行
Thread-A 被唤醒并继续执行
Thread-2 被唤醒并继续执行
-
3 个线程进入等待状态后,
notify()随机唤醒一个线程(如示例中的 Thread-1) -
3 个线程进入等待状态后,
notifyAll()唤醒所有线程 -
所有线程会竞争锁资源,最终按 JVM 调度顺序依次执行
Java线程池
线程池是一种预先创建一定数量线程的容器,当系统有任务需要处理时,这些线程被分配去处理任务。任务完成后,线程不会销毁,而是返回池中,等待下次任务的到来。这种方式可以避免频繁地创建和销毁线程,提升系统性能。
为什么使用线程池
-
资源复用:避免每次任务都新建线程,降低资源和时间开销。
-
线程管理:可以控制线程的最大并发数,防止由于过多线程同时运行而导致系统资源耗尽。
-
任务调度:线程池可以安排任务的执行顺序、任务重试等。
-
提高响应速度:任务等待时间减少,提高系统整体响应能力。
线程池的实现
java.util.concurrent 包,提供了标准化的线程池实现。主要包括两大核心类:
1.ThreadPoolExecutor
ThreadPoolExecutor 是一个非常灵活且可配置的线程池实现类。其构造方法允许通过四个主要参数来配置线程池行为:
- corePoolSize:核心线程数,即保持在池中不论是否空闲的线程数量。
- maximumPoolSize:线程池允许的最大线程数。
- keepAliveTime:当线程数超过核心线程数时,多余线程的闲置存活时间。
- unit:
keepAliveTime参数的时间单位。 - workQueue:用于保存等待执行任务的阻塞队列,常见的有
LinkedBlockingQueue、ArrayBlockingQueue等。 - handler:任务拒绝执行时采用的拒绝策略(例如:
AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy)。
2.Executors 工厂方法
Java为线程池提供了便捷的工厂方法,如:
-
Executors.newFixedThreadPool(int nThreads):返回一个固定大小的线程池,该线程池中始终维持固定数目的线程。
-
Executors.newCachedThreadPool():返回一个根据需要创建新线程的线程池,适合处理大量耗时较短的任务。
-
Executors.newSingleThreadExecutor():返回一个单线程化的线程池,保证所有任务按照指定顺序(FIFO, LIFO等)执行。
-
Executors.newScheduledThreadPool(int corePoolSize):创建支持定时及周期性任务执行的线程池。
线程池底层工作原理
第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务
第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务
第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
在聊天室项目中使用线程池
用户连接请求
│
▼
任务封装提交至线程池
│
▼
┌──────────── 线程池中的线程 ────────────┐
│ 1. 处理连接,建立 Socket 连接 │
│ 2. 保存用户连接到共享数据结构 │
│ 3. 监听用户消息 │
└──────────────────────────────────┘
│
▼
用户发送消息
│
▼
监听任务读取消息到消息处理模块
│
▼
消息广播/定向转发任务提交线程池
│
▼
线程池中线程负责将消息发送给目标用户
│
▼
消息传送到各客户端
JVM
JVM (java虚拟机)是 Java 程序的运行时环境,负责将编译后的字节码加载、解释和执行。它提供了一系列核心功能:
-
内存管理:自动进行内存分配和垃圾回收。
-
跨平台性:Java 程序编译成字节码后,可以在任何安装了 JVM 的平台上运行。
-
安全性:通过沙箱机制保护系统安全。
-
即时编译:JVM 的 JIT 编译器可以将热点代码编译成机器码,提高执行效率。
Java文件编译的过程
- 程序员编写的.java文件
- 由javac编译成字节码文件.class:(为什么编译成class文件,因为JVM只认识.class文件)
- 在由JVM编译成电脑认识的文件 (对于电脑系统来说 文件代表一切)
JVM分区
Java堆(java heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
Java虚拟机栈(Java Virtual Machine Stacks)
java虚拟机是线程私有的,它的生命周期和线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
JVM的垃圾回收机制,垃圾回收机制简称GC
GC主要用于Java堆的管理。Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。
主要的垃圾回收算法
- 标记-清除(Mark-Sweep): 首先标记所有存活对象,然后清除未标记的对象。缺点是会产生内存碎片。
- 标记-整理(Mark-Compact): 在标记存活对象后,将它们整理到一起,清除空闲内存,有效解决内存碎片问题,但整理过程可能较耗时。
- 复制算法(Copying): 将内存分成两个区域,每次只使用其中一块,存活的对象复制到另一块,适用于年轻代,能高效回收大量短生命周期对象。
JVM的一个对象从创建到回收的过程
JVM(Java Virtual Machine)中,对象的生命周期涵盖了从内存分配、初始化、使用到最终垃圾回收(GC)释放内存的全过程。这一过程涉及多个内存区域和不同的垃圾收集器策略,
创建:每当代码中调用 new 关键字创建对象时,JVM 会从堆内存中分配一块连续的内存空间用于存储对象数据。
构造方法调用:分配好内存后,JVM 会调用对象的构造方法对其进行初始化,包括对成员变量赋初值、执行必要的逻辑等。
新生代(Young Generation)
-
Eden 区:新创建的对象最初都会分配到 Eden 区。这里对象的分配速度很快,但由于大部分对象都是临时的,很快就会变得不可达。
-
Survivor 区:
新生代中有两个 Survivor 区(通常称为 S0 和 S1,也叫做 From 和 To 区)。经过一次 GC 后,从 Eden 中存活下来的对象会被复制到其中一个 Survivor 区。接下来的 GC 会在两个 Survivor 区之间进行相互复制,随着存活次数的增加,对象会被赋予一个“年龄”。
老年代(Old Generation)
-
长期存活的对象:当对象在新生代经过多次 GC 仍然存活(达到预定的年龄阈值)后,就会被转移到年老代。年老代主要保存长生命周期的对象,如全局缓存、长期持有的业务数据等。
方法区(Method Area)与元空间(Metaspace)
-
方法区(永久代):存储类的元数据、常量池、静态变量等。很少发生垃圾回收
JUC
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。此包包括了几个小的、已标准化的可扩展框架,并提供一些功能实用的类,没有这些类,一些功能会很难实现或实现起来冗长乏味。
进程线程,并行并发,同步异步
进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。线程运行在进程中,一个进程可以有多个线程,如果一个进程崩溃了,所有线程也会崩溃;而线程崩溃了,进程可能还会继续运行。
- 进程 = 一个大公司(独立的资源空间)。
- 线程 = 公司里的员工(共享资源,任务执行)。
并发(Concurrency):并发关注的是在多个任务之间如何进行切换和协调,以使它们能够在共享的资源(如CPU)上“同时”进行,但并不是在同一时刻真正地执行多个任务。在单核CPU上,通过线程或进程的时间片轮转,使得多个任务交替执行,给人的感觉是“同时”执行,但实际上每个时刻只有一个任务在运行。
并行(Parallelism):并行指的是多个任务在同一时刻同时执行。在并行场景下,多个任务可以同时进行,每个任务拥有自己的处理单元(例如CPU核心),从而实现真正的并行执行。
同步时指任务按照顺序依次执行,异步是指任务可以同时执行
Java中线程之间常见的通信方式主要有以下几种
Object的wait/notify/notifyAll机制
- 通过在synchronized块中使用wait()方法使当前线程释放锁并进入等待状态,等待其他线程调用notify()或notifyAll()来唤醒它。
Lock和Condition
- 利用ReentrantLock配合Condition对象,可以实现类似wait/notify的功能,但更灵活。
- 可以创建多个Condition,用于区分不同的等待队列,从而实现更细粒度的线程协调。
BlockingQueue(阻塞队列)
- Java.util.concurrent包中的阻塞队列(如ArrayBlockingQueue、LinkedBlockingQueue等)提供了一种线程安全的通信机制。
- 生产者可以通过put()方法将数据放入队列,消费者通过take()方法获取数据,队列会自动处理线程等待和唤醒。
synchronized隐式锁
因为开发者无需手动申请或释放锁,而是通过关键字直接在代码中声明需要同步的代码块或方法。
ReentrantLock显示锁
ReentrantLock 是 Java 中 java.util.concurrent.locks 包下的一个显式锁实现,它实现了Lock 接口,提供了比 synchronized 更灵活和细粒度的控制方式。
特点:1.可重入性:即一个线程在持有锁的情况下,可以再次获得该锁并不会被阻塞,内部会维护一个计数器,每次加锁计数器加 1,解锁时减 1,直到计数器归零,锁才真正释放。
2.公平锁: 可以构造一个公平的 ReentrantLock,保证等待时间最长的线程最先获得锁。这样虽然能降低线程饥饿风险,但在高并发情况下性能可能较低。
非公平锁(默认): 允许线程抢占式获得锁,不保证请求顺序,从而通常能获得更高的吞吐量,但可能存在线程饥饿问题。
3.多 Condition 支持:ReentrantLock 允许通过 newCondition() 方法创建一个 Condition 对象,可以用来实现比 Object.wait/notify 更细粒度的线程等待/唤醒机制。一个 ReentrantLock 可以创建多个 Condition,以满足不同等待条件的需求。
Synchronized vs Lock
1. 语言级别 vs. 库级别
synchronized隐式、自动
- 是 Java 语言内置的关键字,由 JVM 提供支持,不需要额外依赖外部库。
- 语法简单,使用方便,不需要手动释放锁,进入同步代码块时自动获得锁,退出时自动释放。
Lock显示、手动
- 是 Java.util.concurrent.locks 包下的接口和实现(如 ReentrantLock),属于库级别的实现。
- 提供了比 synchronized 更灵活的锁机制,必须显示调用 lock() 和 unlock(),因此需要开发者手动管理。
2. 功能与灵活性
synchronized简单、自动、不可中断、无公平性
- 自动释放锁:异常时自动释放锁,使用简单。
- 不可中断:当线程在等待获取锁时,不支持中断操作,线程无法在等待锁时响应中断。
- 不支持公平性控制:锁的调度是由 JVM 内部实现,无法设置公平/非公平策略。
Lock复杂、公平性执行、更细致的操控、可中断
- 可中断锁获取:通过
lockInterruptibly()方法,允许在等待锁的过程中响应中断,提升程序的响应性。 - 尝试获取锁:使用
tryLock()可以尝试非阻塞方式获取锁,或在超时时间内尝试获取锁。 - 公平性控制:例如 ReentrantLock 可以构造公平锁或非公平锁,开发者可以根据需求选择。
- 多 Condition 支持:可以通过一个 Lock 创建多个 Condition 对象,实现更细粒度的线程等待与通知,适用于复杂的线程协作场景。
3. 性能与使用场景
synchronized简单、易维护、但无法适合复杂场景
- 简单场景:适合较为简单、竞争不激烈的同步场景,写法简单、容易维护。
- 性能优化:JVM 对 synchronized 做了很多优化,如锁粗化、锁消除和偏向锁等,低竞争下性能已经非常优秀。
- 局限性:在需要复杂控制、锁等待中断、尝试锁获取或需要多个条件变量时,synchronized 无法满足需求。
Lock适合复杂的高并发场景、更加细粒度的操作,但需手动释放获取、操作复杂,易死锁
- 高并发场景:在复杂的高并发场景下,Lock 提供的可中断、尝试获取、以及公平性策略可以更好地控制并发性能。
- 灵活性要求高:当需要多个 Condition 来细分等待队列、实现细粒度通知时,Lock 显得更为适用。
- 额外复杂性:需要手动释放锁,开发者必须确保在 finally 块中调用 unlock(),否则容易引起死锁或资源泄露。
4. 错误处理与调试
synchronized
- 自动管理:锁的自动获取和释放减少了错误,异常情况下也会自动释放锁,调试相对简单。
- 调试工具支持:JVM 内置监控可以捕捉 synchronized 锁竞争情况,但细节可控性有限。
Lock
- 手动控制:虽然提供更多控制手段,但也增加了开发者管理锁的复杂性,容易因忘记释放锁而导致问题。
- 监控和调试:ReentrantLock 提供了如获取当前持有锁的线程、等待队列长度等信息,有助于调试锁竞争问题。
总结
-
synchronized 适合简单、低竞争环境下的同步需求,开发简单且安全,JVM 的优化也使其在大多数场景下表现良好。
-
Lock 则适用于需要更高灵活性、可中断、尝试锁获取以及公平性控制等高级需求的场景,但使用时需要开发者更细致地管理锁的生命周期。
CASCompare And Swap(比较并交换)自旋锁
基本思想:
CAS 操作需要三个参数:
- 内存地址:需要操作的变量位置
- 预期值:当前线程期望该位置的值
- 新值:如果当前位置的值与预期值一致,则更新为这个新值
操作过程:
CAS 先读取目标内存地址的当前值,然后将其与预期值进行比较。如果相等,则将新值写入;如果不相等,则不做任何操作,并返回失败。整个过程在硬件级别上是原子性的,不会被其他线程中断。
优势
- 无锁机制:
- CAS 允许线程在不加锁的情况下更新共享变量,避免了线程阻塞和上下文切换带来的开销,从而提高了并发性能。
- 乐观锁思想:
- CAS 基于乐观假设,即认为冲突情况较少,只有在实际检测到数据不一致时才会进行重试,这样在大多数情况下可以实现高效并发更新。
- 简单高效:
- 在硬件支持下,CAS 操作通常非常快速,能充分利用现代 CPU 的原子指令,适用于高并发场景下的小粒度数据更新。
缺陷与挑战
- ABA 问题:
- 假设一个线程读取变量值为 A,然后其他线程将其改为 B,又改回 A。此时第一个线程执行 CAS 时,会认为值没有变化(仍为 A),但实际上数据已经被修改过。解决方案包括使用带版本号的 CAS(如 AtomicStampedReference)。
- 自旋问题:
- 当多个线程不断失败并重试时,会进入忙等待(自旋),这在高争用场景下可能导致 CPU 资源浪费,甚至出现活锁。
- 只适用于单个变量:
- CAS 适用于对单个共享变量的原子更新,而对于多个变量或复杂数据结构的原子操作,CAS 的实现和应用会更加复杂。
Java死锁
造成死锁的几个原因
- 1.互斥:一个资源每次只能被一个线程使用
- 2.占有并等待:一个线程在阻塞等待某个资源时,不释放已占有资源
- 3.不可抢占:一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
- 4.循环等待:若干线程形成头尾相接的循环等待资源关系
若这四个条件同时满足,就可能产生死锁。
public class DeadlockExample {
// 两个资源
private final Object resourceA = new Object();
private final Object resourceB = new Object();
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
example.initiateDeadlock();
}
public void initiateDeadlock() {
// 线程1:先锁定 resourceA,再锁定 resourceB
Thread thread1 = new Thread(() -> {
synchronized (resourceA) {
System.out.println("线程1:持有 resourceA,等待 resourceB");
try {
// 模拟处理时间
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceB) {
System.out.println("线程1:成功获取 resourceB");
}
}
});
// 线程2:先锁定 resourceB,再锁定 resourceA
Thread thread2 = new Thread(() -> {
synchronized (resourceB) {
System.out.println("线程2:持有 resourceB,等待 resourceA");
try {
// 模拟处理时间
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA) {
System.out.println("线程2:成功获取 resourceA");
}
}
});
thread1.start();
thread2.start();
}
}
- 线程1 在获取到
resourceA后等待获取resourceB。 - 线程2 在获取到
resourceB后等待获取resourceA。 - 两个线程因此互相等待,导致死锁。
预防与避免死锁的策略
- 统一加锁顺序:在多个线程需要同时获取多个资源时,设定统一的锁获取顺序,确保所有线程按照相同顺序申请锁,从而避免循环等待条件。
- 使用定时锁(tryLock):尝试在一定时间内获取锁,如果获取失败则可以主动放弃,避免长时间等待。
- 减少锁的粒度:尽可能减小锁定的范围,避免在同一代码块中嵌套多个锁的使用,从而降低死锁风险。
- 使用无锁数据结构:采用
ConcurrentHashMap、CopyOnWriteArrayList等无锁或轻量级锁的数据结构来减少同步需求。 - 避免持有不必要的资源:在设计线程协作时,只持有必需的锁资源,及时释放已经使用完的资源。
Java的Exception类型
Throwable
Java 异常处理的根类,所有异常和错误都继承自 Throwable。它有两个直接子类:
Error:用于指示严重的问题,通常由 JVM 抛出,如内存溢出(OutOfMemoryError)、虚拟机错误(VirtualMachineError)等,应用程序一般不捕获或处理 Error。
Exception:用于指示程序中可预见的异常情况,程序员可以捕获和处理这些异常。异常分为两大类,运行时异常和编译时异常
- 运行时异常RuntimeException,编译器检查不出来。一般是指编程时的逻辑错误,是程序员应该避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常对于运行时异常可以不做处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响
- NullPointerException 空指针异常,当应用程序试图在需要对象的地方使用null时,会抛出此异常
- 2. ArithmeticException 数学运算异常当出现异常的运算条件时,会抛出此异常
- 3. ArrayIndexOutOfBoundsException 数组下标越界异常用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引
- 4. ClassCastException 类型转换异常当试图将对象强制转换为不是实例的子类时,会抛出此异常
- 5. NumberFormatException 数字格式不正确异常当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式,会抛出此异常—>使用异常我们可以确保输入是满足条件数字
- 编译时异常FileNotFountException是编译器要求必须处置的异常
java框架
SSM框架
SSM框架作为传统的构建应用程序框架,Spring + Spring MVC + MyBatis是传统 Java Web 开发的经典组合框架。传统的项目构建是需要Spring中提供众多的子项目来构建Spring应用程序,Spring Framework提供核心功能,其他的负责数据获取消息传递等等,其他的子项目都依赖于Spring Framework,整合成一个完整的项目。
随着业务需求的复杂化,大型化,传统的方式已经不再满足,比如,
- 导入依赖繁琐:需要手动引入依赖,并且jar包互相有冲突
- 项目配置繁琐:还需要写大量相应的配置文件.xml,在applicationContext.xml中要声明大量的bean对象,因为使用Spring这些对象,要先声明在使用
Spring Boot
与传统构建项目相比,Springboot就是Spring的一个子项目,用于快速构建应用程序,有以下特点起步依赖:sb提供一个起步依赖,整合了一个功能所需要的其他依赖,一节更比六节强!
独立运行:Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
简化配置:spring-boot-starter-web启动器自动依赖其他组件,减少了maven的配置。除此之外,还提供了各种启动器,开发者能快速上手。
自动配置:Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。
XML配置:无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的
应用监控:Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
SpringBoot和SpringCloud是什么关系
Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架; Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。
可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。
SpringCloud都用过哪些组件 介绍一下作用
Nacos--作为注册中心和配置中心,实现服务注册发现和服务健康监测及配置信息统一管理
Gateway--作为网关,作为分布式系统统一的出入口,进行服务路由,统一鉴权等
OpenFeign--作为远程调用的客户端,实现服务之间的远程调用
Sentinel--实现系统的熔断限流
Sleuth--实现服务的链路追踪
SpringMVC
它实现了经典的模型-视图-控制器(MVC)设计模式,有效分离了业务逻辑、数据模型与表现层。下面介绍一些核心概念及特点:
- 前端控制器 DispatcherServlet作为整个请求处理流程的入口,DispatcherServlet 接收所有 HTTP 请求,并负责分派给具体的 Controller 处理。它根据配置好的 HandlerMapping 决定哪个 Controller 或方法来响应当前请求。
- 注解驱动的 Controller通过 @Controller 和 @RequestMapping 注解,SpringMVC 支持声明式地映射 URL 请求到具体的方法,同时可以自动进行参数绑定、类型转换与数据校验。这大大降低了开发门槛,提高了开发效率。
- 视图解析与数据绑定Controller 处理完业务逻辑后,返回 ModelAndView 对象,由 ViewResolver 根据配置选取对应的视图模板(例如 JSP、Thymeleaf),将 Model 中的数据渲染出来。RESTful 风格可直接使用 @RestController 返回 JSON 数据。
- 拦截器与异常处理SpringMVC 提供 HandlerInterceptor 拦截器机制,可以在请求处理的前后分别插入自定义操作(如日志记录、安全校验等)。同时,通过全局异常处理机制(@ControllerAdvice)实现统一的异常处理,提升系统健壮性。
- 与 Spring Boot 的整合在 Spring Boot 项目中,SpringMVC 通常作为默认的 Web 框架出现,开发者可以快速构建和部署 RESTful 应用,降低配置和管理的复杂度。
SpringMVC常用的5个注解
- @Component 基本注解,标识一个受Spring管理的组件
- @Controller 标识为一个表示层的组件
- @Service 标识为一个业务层的组件
- @Repository 标识为一个持久层的组件
- @Autowired 自动装配
- @Qualifier("") 具体指定要装配的组件的id值
- @RequestMapping() 完成请求映射
- @PathVariable 映射请求URL中占位符到请求处理方法的形参
谈谈你对Spring的理解
Spring 是一个 IOC 和 AOP 容器框架。
控制反转(IOC):
而IOC的思想就是使用对象时,有主动new创建对象转换为由外部提供对象,此过程中对象创建控制权有程序转移到外部,整个过程的目的就是充分解耦
spring提供了一个容器,成为IOC容器,充当IOC思想中的外部,用于管理对象的创建和初始化的过程
而由IOC容器创建和管理的对象成为Bean
依赖注入(DI):在容器中简历bean和bean之间的依赖关系的过程,通过依赖注入将对象的创建和依赖管理交给容器,比如,dao层要依赖于service层实现save方法,而两个bean都在IOC容器中,所以IOC容器就为两个bean创建了依赖关系
Spring中的设计模式
- 工厂模式Spring的BeanFactory和ApplicationContext内部使用工厂模式来创建和管理Bean。这种模式使得对象的创建过程透明化,并能在运行时根据配置动态生成实例。
- 单例模式默认情况下,Spring容器中管理的Bean是单例模式,确保每个Bean只有一个共享实例,节约资源并保证状态一致性。当然,Spring也支持prototype等其他作用域。
- 代理模式在AOP(面向切面编程)中,Spring通过代理模式实现横切关注点(如事务、日志、安全等)的无缝织入,从而增强目标对象的功能。
- 模板方法模式Spring提供了如JdbcTemplate、RestTemplate等模板类,这些类封装了对底层API的调用流程和异常处理,降低了重复代码的编写,让开发者专注于业务逻辑。
- 观察者模式Spring的事件发布机制基于观察者模式,ApplicationContext充当事件发布者,而实现ApplicationListener接口的组件作为观察者,订阅并响应特定的事件,实现了应用组件之间的松耦合。
Spring的三级缓存
Spring 中的三级缓存主要用于解决单例 Bean 的循环依赖问题,
-
一级缓存(singletonObjects)
存储完全初始化完成的单例 Bean。每当一个 Bean 创建和初始化结束后,会被放入一级缓存,供后续直接获取使用。 -
二级缓存(earlySingletonObjects)
存储“早期曝光”的 Bean 实例,即已经实例化但尚未完成依赖注入或初始化的 Bean。 -
三级缓存(singletonFactories)
存储的是 ObjectFactory 对象,该工厂能够创建 Bean 的早期引用(例如,创建 AOP 代理对象)。当一个 Bean 正在实例化但尚未放入二级缓存时,三级缓存提供一种延迟获取 Bean 实例的方式。如果出现循环依赖,Spring 会先从三级缓存中调用 ObjectFactory 得到一个早期引用,然后将该引用放入二级缓存供后续使用。
循环依赖是指两个或多个组件(如类、模块或Bean)在彼此创建或使用时相互依赖,形成了一个闭环。例如,A类需要依赖B类,而B类又需要依赖A类才能正常工作,这就构成了循环依赖。在Spring容器中,如果Bean A依赖于Bean B,而Bean B同时依赖于Bean A,就会导致在实例化过程中出现无法确定先后关系的问题,从而可能导致无限递归或初始化失败。
Bean
spring提供了一个容器,成为IOC容器,充当IOC思想中的外部,用于管理对象的创建和初始化的过程,而由IOC容器创建和管理的对象成为Bean
生命周期
(1)默认情况下,IOC容器中bean的生命周期分为五个阶段:
- 实例化:调用构造器 或者是通过工厂的方式创建Bean对象
- 注入:给bean对象的属性注入值
- 通过 setter 方法注入
- 通过构造方法注入
- 初始化:调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.
- 使用阶段
- 销毁:IOC容器关闭时, 销毁Bean对象.
Bean定义与注册:在容器启动时,Spring会读取配置(XML、注解、Java配置类),解析出每个Bean的定义信息(包括作用域、依赖关系、初始化方法、销毁方法等),并将这些Bean定义注册到容器中。
Bean实例化
Spring支持多种方式创建Bean对象,常见的有:
- FactoryBean机制:可以自定义实现 FactoryBean 接口,容器调用其
getObject()方法来获取实际的Bean实例。 - 实例工厂方法实例化:先通过工厂Bean获取工厂实例,再调用实例方法创建Bean。
- 静态工厂方法实例化:由静态工厂方法返回一个实例,如
MyBean myBean = MyBeanFactory.createMyBean()。 - 构造器实例化:通过无参构造函数或带参构造函数调用
new关键字创建对象。 -
依赖注入(属性注入):Spring根据Bean定义中配置的依赖关系(包括构造器参数和setter注入),将其他Bean、配置值或资源注入到当前Bean中,完成依赖注入(DI)
初始化:Spring会调用Bean定义中配置的初始化方法(如init-method属性或实现InitializingBean接口的afterPropertiesSet()方法)完成最后的初始化工作。此时Bean已经可用于应用逻辑。
使用阶段:Bean加载完成后就处于容器中,可以由业务逻辑调用。对于单例Bean来说,它在容器整个生命周期内都是唯一且共享的。
销毁:当Spring容器关闭时,会调用单例Bean的销毁方法,对于原型Bean,由于容器只负责创建和依赖注入,销毁阶段则需要由使用者自行管理。
Spring的事务管理
事务管理方式
-
声明式事务(Declarative Transaction Management)利用 AOP 拦截机制,通过在方法或类上使用 @Transactional 注解实现事务控制,无需在业务逻辑里写入事务代码。
-
编程式事务(Programmatic Transaction Management)基于 PlatformTransactionManager 提供的 TransactionTemplate 或直接使用 TransactionManager API,显式地在代码中控制事务的开始、提交和回滚。适合细粒度控制,但耦合代码较高,不太推荐在业务逻辑中广泛使用。
关键概念与实现机制
-
事务传播行为(Propagation Behavior)Spring 提供了多种传播行为,REQUIRED 表示如果当前存在事务则加入,否则创建新事务;REQUIRES_NEW 则总是创建独立的新事务,常用于独立提交场景。
-
事务隔离级别(Isolation Level)为解决并发数据访问问题,Spring 支持 READ_COMMITTED读已提交、READ_UNCOMMITTED读未提交、REPEATABLE_READ可重复读、SERIALIZABLE 串行化等隔离级别,用户可以根据业务需求和性能考量进行设置。
-
事务回滚规则:默认情况下,Spring 只对运行时异常回滚,
-
AOP 与代理机制:声明式事务管理依赖 AOP(面向切面编程)生成代理对象,在调用被 @Transactional 修饰的方法时,代理拦截并在方法执行前开启事务,在方法成功返回时提交,在抛异常时回滚。这一机制使得事务控制完全透明,不需要改动业务逻辑代码。
HTTP
GET 和POST 的区别
GET:
- 目的: 用于从服务器读取或获取资源。
- 适用场景: 主要用于查询操作,比如获取网页、下载图片、搜索等。
- 位置: 参数直接附加在 URL 后面,通过查询字符串的形式传递(例如
?param1=value1¶m2=value2)。 - 缓存性: GET 请求的结果通常是可缓存的,浏览器和中间代理会依据 HTTP 头(如
Cache-Control)存储响应结果。 - 历史记录: 浏览器会在历史记录中保存 GET 请求的 URL。
- GET: 是幂等的。无论重复调用多少次,服务器状态都不会改变(只进行数据的读取)。
- GET: 由于参数明文显示,敏感信息暴露的风险较高;同时,URL 中的信息可能会出现在浏览器日志中。
- GET 请求由于数据在 URL 中,其传输的数据量受限;
POST:
- 目的: 用于向服务器提交数据,以便服务器对资源进行处理(例如创建、更新)。
- 位置: 参数通过 HTTP 请求体(Body)传输,不直接显示在 URL 中。
- 可见性: 数据隐藏在请求体中,不易直接在浏览器地址栏中看到,更适合传输敏感信息。
- 缓存性: POST 请求通常不被浏览器缓存,因为其目的是修改服务器上的资源;不过可以通过特定的头部设置改变这一行为。
- 历史记录: 浏览器一般不会保存 POST 请求的相关内容。
- POST 请求的解析可能涉及多种内容类型(如
application/x-www-form-urlencoded、multipart/form-data或application/json),服务器需要按照相应的格式进行解析处理。 - 通常不是幂等的。多次提交可能会导致数据重复创建、更新或其它副作用(例如重复下单)。POST 能够传输较大数据,适用于文件上传等场景。
GET 更侧重于数据的读取,便于缓存、书签保存,但数据量和安全性方面存在局限。POST 则适用于传输大量数据和涉及状态改变的操作,能够更好地隐匿传输内容,但对缓存和重复提交处理上需要额外注意。
Cookie 和Session 的区别
Cookie:
- 客户端存储:Cookie 数据保存在客户端浏览器中。服务器通过 HTTP 响应头将 Cookie 发送给客户端,客户端在后续请求中会将其携带回来。
- 数据量有限:Cookie 的存储空间通常比较小,单个 Cookie 的大小通常限制在 4KB 左右,
- 可设定过期时间:Cookie 可以设置失效时间,从而决定它是一个会话级 Cookie(关闭浏览器即失效)还是持久化 Cookie
- 数据暴露风险:Cookie 存放在客户端,可能会受到攻击,同时还易受篡改
- 场景:如记录用户偏好、主题选择、浏览记录等非敏感数据。当数据不需要长期保存在服务器上或无需敏感处理时,可采用 Cookie。
Session:
- 服务端存储:Session 数据保存在服务器端,服务器根据每个用户的会话生成并维护一份数据副本。
- 标识传递:客户端只保存一个会话标识(通常是 session ID),服务器依据该标识获取对应的会话数据。
- 数据容量大:Session 存储在服务器端,可以存储较大的数据,适合保存需要长时间保持的信息。
- 安全性较高:服务器端的数据不容易被客户端直接获取和篡改,适合存储敏感数据。
总结对比
- 存储位置:Cookie 存储于客户端;Session 存储于服务器端。
- 数据容量:Cookie 有容量限制且易暴露;Session 没有明显的数据量限制,但消耗服务器资源。
- 安全性:Cookie 易受攻击,传输敏感数据要加密和设置安全标志;Session 在服务器端存储,相对安全,但需要保护 Session ID。
- 生命周期:Cookie 生命周期可控且由客户端浏览器决定;Session 生命周期由服务器端管理,通常在用户不活跃一段时间后销毁。
Mysql
什么是mysql事务?
MySQL 事务是一组数据库操作的逻辑工作单元,这组操作要么全部执行成功,要么全部不执行,从而保证数据的一致性和完整性。事务通常遵循 ACID 四大特性:
-
原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不执行,不会出现部分成功、部分失败的情况。
-
一致性(Consistency):事务开始前和结束后,数据库都应处于一致的状态,
-
隔离性(Isolation):多个事务并发执行时,每个事务都应独立,彼此之间的操作不会互相干扰。
-
持久性(Durability):一旦事务提交,其对数据库的修改是永久性的,即使系统发生故障也不会丢失。
数据库四种隔离级别
数据库事务的隔离级别定义了一个事务内对数据修改的可见性和并发执行时可能出现的数据异常情况。常见的四种隔离级别由低到高分别是:
- 读未提交(READ UNCOMMITTED)允许事务读取其他未提交事务的修改,可能会产生“脏读”。
- 读已提交(READ COMMITTED)每次读取只看到其他事务已提交的数据,避免了脏读问题。
- 可重复读(REPEATABLE READ)在同一事务中,多次读取同一数据返回的结果是一致的,避免了脏读和不可重复读。
- 串行化(SERIALIZABLE)将事务完全串行化执行,所有事务之间都像依次顺序执行,彻底避免脏读、不可重复读和幻读。
事务的并发问题
- 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
- 不可重复读:事务 A 多次读取同一数据,事务A多次读取的过程中,事务B对数据作了更新并提交,导致事务A多次读取同一数据结果不一致
- 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
主键和外键区别
主键是唯一标识表中每一行记录的字段
- 一个表只能有一个主键
- 主键只能是唯一的,不能为空
- 主键是查询、更新、删除是定位的凭证
外键是用来建立其他表的关系,如一对一、一对多
- 外键可以重复、可以为空
- 外键用于保证数据的引用完整性。
- 外键值必须要么是目标表中存在的主键值,要么为 NULL。
数据库的索引
数据库的索引类似于书籍的目录,其主要作用是加速数据的查询。通过在表的特定列上建立索引,数据库可以迅速定位到符合查询条件的数据行,而无需对整个表进行全表扫描。
数据库索引的实现
- B+树索引: 最常见的索引类型,适用于大多数查询操作。B+树结构可以保证查找、插入和删除操作的时间复杂度为 O(log N)。
- 哈希索引: 通过哈希函数直接定位数据,适合等值查询,但不支持范围查询。
- 位图索引: 对低基数(如性别、状态等)的列非常有效,常用于数据仓库或在线分析处理(OLAP)场景。
- 全文索引: 用于处理文本搜索,可以快速定位到包含特定关键词的文本行。
索引的基本类型
- 主键索引: 通常自动建立,并且要求数据唯一,通常采用聚集索引方式存储数据。
- 唯一索引: 保证列中数据的唯一性,但允许一个空值的存在(具体规则视数据库而定)。
- 普通索引: 没有唯一性限制,适用于提高查询效率。
- 组合索引: 针对多个列联合建立的索引,可以优化那些在多个列上都有过滤条件的查询。
- 聚集索引与非聚集索引: 聚集索引决定数据的物理存储顺序,而非聚集索引则存储数据的指针,两者各有优缺点和适用场景。
分库分表
指在数据量大、访问并发高的情况下,通过将单一数据库或单张数据表的数据拆分成多个独立的物理数据库或表来实现负载均衡、提高访问效率和扩展系统容量。
水平分库/分表
- 水平分库:按照某个分片键(例如用户ID、订单ID等)将数据分布到多个数据库中,每个数据库存储整体数据子集。
- 水平分表:将单个大表按照某些规则(例如哈希、范围、日期等)拆分成多个较小的表,这些表可以在同一数据库中,也可以再结合分库一起使用。
- 优势:能有效降低单库压力,提高查询和写入性能,便于后续动态扩展。
- 挑战:需要考虑跨分片的查询、事务的一致性以及路由和重组数据的问题。
垂直分库
- 将不同业务模块或功能对应的数据表放到不同的数据库中,如订单、用户、产品数据分别存储在单独的数据库中。
- 优势:不同模块间数据库资源互不干扰,便于运维优化。
- 挑战:模块间数据关联较强时,跨库查询可能会增加复杂性。
面对聊天室项目实现分库分表
- 聊天记录表:通常聊天记录(例如消息表)数据量最大,可以按会话或用户作为分片键。
- 对于一对一聊天,可以采用发送者和接收者ID的组合计算Hash值,按Hash取模分散到多个表;
- 对于群聊,使用群组ID作为分片键,同样通过Hash或范围分片分布数据。
- 时间切分:对于历史消息量激增的情况,可以结合时间维度对旧数据进行归档或单独存储,既优化查询也便于维护。
- 缓存热点数据:对于热点数据可以采用 Redis 缓存,加速实时聊天数据的读取;
- 将历史数据定期归档到独立的数据库或表中,减轻主库压力。
MYSQL优化
- 查询重写:避免SELECT *,尽量只查询所需要的字段;对复杂查询进行拆分,适时使用子查询或JOIN,提高执行效率。
- 索引优化:合理设计单列索引和复合索引,注意索引选择性;避免在索引字段上进行函数运算或不必要的类型转换,这会导致索引失效。
- 数据类型选择:选择合适的数据类型,避免使用过大或不必要的数据类型;例如VARCHAR与CHAR的选择、数字类型的精简等。
- 表结构设计:在符合业务规范的情况下,考虑适当的反范式化以减少JOIN操作;对于高频访问且数据量庞大的表,可以使用分区或者分库分表策略。
数据库范式
- 第一范式主要是保证数据表中的每一个字段的值必须具有原子性,也就是数据表中的每个字段的值是不可再拆分的最小数据单元
- 第二范式要求在满足第一范式的基础上,还要满足数据表里的每一条数据记录,都是可唯一标识的,而且所有的非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。
- 第三范式建立在已经满足第二范式的基础上,数据表中的每一个非主键字段都和主键字段直接相关,也就是说数据表中的所有非主键字段不能依赖于其他非主键字段,这个规则的意思是所有非主属性之间不能有依赖关系,它们是互相独立的,这里的主键可以拓展成为候选键。
HashTable哈希表也叫散列表
如上图所示,通过左侧数组的快速查找,快速增删到右侧相对应的链表中结合了数组和链表的优点,比如
- 哈希表(hash table)在JDK1.7中实现是 数组 + 链表
- 哈希表(hash table)JDK1.8中实现是 数组 + 链表 + 红⿊树 。
最基本的结构就是两种,一个是数组,另外一个是链表,所有的数据结构都可以用这两个基本结构来构造的。
查找的过程是,当你拿着key来哈希表中寻找相应的value时,首先,该key值会被哈希函数(最常见的就是对长度取余)转换为哈希值,然后再将哈希值转换为数组的索引,从数组中快速查找到该索引所在的位置,找到对应的value值。
所以本质上的存储结构是:哈希表通常由一个数组和一个哈希函数组成。数组的每个元素称为桶(Bucket),它可以存储一个或多个键-值对
哈希冲突(Hash Collision)
不同的键被哈希函数映射到相同的索引时,就发生了哈希冲突。常用的解决哈希冲突的方式有链地址法和开放地址法
链地址法(拉链法)
- 原理: 在数组的每个位置存储一个链表或其它数据结构(例如红黑树),所有被映射到同一位置的键值对都存放在这个链表中。
- 优点: 实现简单,能够灵活地应对大量冲突,理论上链表长度没有上限。
- 缺点: 如果冲突频繁,链表会变长,导致查找效率下降,从而降低平均性能。
开放定址法(Open Addressing)
原理: 当冲突发生时,哈希表会尝试在其它位置存放新的键值对。常见的探测方法包括:
- 线性探测(Linear Probing): 如果当前位置已占用,则检查下一个位置,依次向后查找,直到找到空位。
- 二次探测(Quadratic Probing): 根据探测步长的平方(例如1², 2², 3²…)来确定新位置,以减少线性探测中容易产生的“堆积”(cluster)问题。
- 双重散列(Double Hashing): 使用第二个哈希函数来决定步长,降低探测序列中连续相邻位置产生相似冲突的可能性。
优点: 数据密度较高时能保持较好的性能,并且不需要额外的链表内存。
缺点: 当负载因子(已存数据占总数组大小的比例)较高时,探测次数会增多,性能急剧下降,因此通常保持负载因子在较低水平(如0.7以下)。
HashMap
HashMap 是一个常用的数据结构,它实现了Map接口,允许我们通过键值对的形式存储和快速查找数据。HashMap的底层是基于哈希表(hash table)的实现,它的高效性和灵活性使其在各种编程场景中广受欢迎。
hashmap底层使用数组来存储数据,数组的每个元素通常称为一个“桶”,用于存放具有相同哈希索引(节点包含键、值、哈希值以及指向下一个节点的引用,即链表中的下一个元素)的节点。
由于不同的键可能经过哈希函数计算后得到相同的数组索引,HashMap 需要链表机制来处理这种冲突,当两个或多个键经过哈希计算后落在同一桶中,HashMap 会使用链表的结构把它们串在一起,每个节点包含对应的键值对。
当大量元素落在同一个桶中(即哈希碰撞非常频繁)时,单纯使用链表查找其时间复杂度为 O(n),严重影响性能。所以采取红黑树来优化查找效率,红黑树保证了最坏情况下的查找、插入和删除操作时间复杂度为 O(log n)
HashMap 的底层实现主要依赖于一个数组,该数组中的每个元素既可以以链表的形式存储多个具有相同哈希索引的节点,也可根据实际情况(当链表过长时)转换为红黑树来优化查找效率。通过对哈希函数的良好设计以及动态扩容机制,HashMap 能够在大部分情况下保持高效的存取性能。理解这些底层机制有助于在设计与调试程序时做出更合理的性能优化和异常处理策略。
hashmap VS hashtable
Hashtable 和 HashMap 都是基于哈希表实现的键值映射数据结构,但它们在设计理念、实现方式和适用场景上存在一些重要区别。
Hashtable 的大多数方法都使用了同步(synchronized)关键字来保证线程安全。
- 线程安全,考虑单线程环境
- 不允许键或值为 null
- 性能差,都被 synchronized 修饰,使得每次方法调用都需要获取锁,高并发下有性能瓶颈
HashMap 默认并不是线程安全的。
- 线程非安全
- 允许一个 null 键及多个 null 值
- 性能好
哈希表(Hash Table / 哈希表): 用于高效存储和检索数据。
Hashtable 与 HashMap: 都是哈希表的实现,前者默认线程安全(但性能可能受限),后者适合非并发场景且性能更优。
哈希树(Hash Tree / 默克尔树): 用于验证数据集合的完整性,常见于区块链、分布式系统和文件校验中。
Mybatis
mybatis是什么?有什么特点?
它是一款半自动的ORM(Object Relation Mapping,对象关系映射)持久层框架,具有较高的SQL灵活性,支持高级映射(一对一,一对多),动态SQL,延迟加载和缓存等特性,但它的数据库无关性较低,
MyBatis 并不完全隐藏 SQL,而是允许开发者手写 SQL 语句,同时也封装了大量的JCBD操作,然后通过映射文件(XML)或注解将 SQL 查询与 Java 对象之间建立映射关系,从而实现数据持久化操作。比如下面这个mapper.xml文件
ORM
比如用一个Java的Student类,去对应数据库中的一张student表,类中的属性和表中的列一一对应。Student类就对应student表,一个Student对象就对应student表中的一行数据
Mybatis 中一级缓存与二级缓存
一级缓存是SqlSession级别的缓存,默认开启。
二级缓存是NameSpace级别(Mapper)的缓存,多个SqlSession可以共享,使用时需要进行配置开启。
缓存的查找顺序:二级缓存 => 一级缓存 => 数据库
消息队列
Kafka
-
分布式日志系统:Kafka 将消息存储在分布式的日志(Log)中,每个主题被划分为多个分区(Partition),每个分区内部的消息按照严格的顺序追加写入。
-
高吞吐与持久化:Kafka 设计初衷是支持高吞吐量和低延迟的数据流传输,通过批量处理和顺序写入磁盘,能够高效持久化大量数据。
-
生产者-消费者模型:
-
生产者:将消息写入指定主题的分区,可以支持消息的分区策略和自定义分区规则。
-
消费者:以消费者组的形式消费消息,每个消费者组内同一分区只会被一个消费者消费,实现消息分摊;同时每个消费者能够维护消费位移(Offset),实现断点续传。
-
RabbitMQ
-
基于 AMQP 协议:RabbitMQ 实现了高级消息队列协议(AMQP),提供了可靠的消息传递机制。
-
消息中间件:作为一个成熟的消息代理,它支持多种消息传递模式,如点对点、发布/订阅、主题路由和 RPC 模型。
-
交换机与队列:
-
交换机(Exchange):负责接收生产者发送的消息,并根据绑定规则将消息路由到一个或多个队列。
-
队列(Queue):消息在队列中等待消费者拉取处理。
-
路由模式(Direct、Topic、Fanout、Headers)使得 RabbitMQ 在消息路由上具有较高的灵活性。
-
Linux
1. 文件与目录操作
2. 文件内容查看
3. 文本处理与管道
数据结构
时间复杂度和空间复杂度
时间复杂度:指算法语句的执行次数。O(1),O(n),O(logn),O(n2)并不是算法的执行时间
空间复杂度:就是一个算法在运行过程中临时占用的存储空间大小,换句话说就是被创建次数最多的变量,它被创建了多少次,那么这个算法的空间复杂度就是多少。有个规律,如果算法语句中就有创建对象,那么这个算法的时间复杂度和空间复杂度一般一致,很好理解,算法语句被执行了多少次就创建了多少对象。
数组和链表结构简单对比
数组:先固定长度,在添加数据,查找快O(1),增删慢,因为需要指针遍历整个数组,O(n)
链表:适合动态的增删数据,增删O(1),查找慢O(N)
树的遍历
先序遍历:根左右
后序遍历:左右根
中序遍历:左根右
层序遍历:每一层从左到右访问每一个节点。
排序
快速排序(Quick Sort)
-
选择一个枢纽(pivot)元素,将数组分成两部分:
-
一部分所有元素均小于或等于枢纽,
-
另一部分所有元素均大于枢纽。
-
-
对这两个部分递归采用相同的排序策略,最后将所有排序部分合并。
package com.zhangkai; public class QuickSort { public static void quickSort(int[] data, int low, int high) { int i, j, temp, t; if (low > high) { return; } i = low; j = high; //temp就是基准位 temp = data[low]; System.out.println("基准位:" + temp); while (i < j) { //先看右边,依次往左递减 while (temp <= data[j] && i < j) { j--; } //再看左边,依次往右递增 while (temp >= data[i] && i < j) { i++; } //如果满足条件则交换 if (i < j) { System.out.println("交换:" + data[i] + "和" + data[j]); t = data[j]; data[j] = data[i]; data[i] = t; System.out.println(java.util.Arrays.toString(data)); } } //最后将基准位与i和j相等位置的数字交换 System.out.println("基准位" + temp + "和i、j相遇的位置" + data[i] + "交换"); data[low] = data[i]; data[i] = temp; System.out.println(java.util.Arrays.toString(data)); //递归调用左半数组 quickSort(data, low, j - 1); //递归调用右半数组 quickSort(data, j + 1, high); } public static void main(String[] args) { int[] data = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48}; System.out.println("排序之前:\n" + java.util.Arrays.toString(data)); quickSort(data, 0, data.length - 1); System.out.println("排序之后:\n" + java.util.Arrays.toString(data)); } }上述代码的思想是使用双指针:
- 使用两个指针
i和j,分别从低端和高端开始,向中间移动。 - 先从右侧开始:当
data[j]不小于枢纽时递减j; - 再从左侧开始:当
data[i]不大于枢纽时递增i; - 当两个指针未相遇时,交换两者指向的元素。
- 当
i和j相遇时,再把枢纽(最初存储在data[low])与相遇位置的元素交换,确保枢纽在最终位置上。
本文标签: 最全
版权声明:本文标题:全网最全!!4W字总结大厂面试的八股文!!! 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1766533798a3467510.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论