手撕:HashMap

自我介绍

负载均衡

1.常见负载均衡算法(这题可以微服务、SpringCloud、Net等等角度切入考察)

简单轮询:将请求按顺序分发给后端服务器,不关心服务器当前状态(性能、当前负载等)
加权轮询:将请求按顺序和权重(根据性能设置权重)分发给后端服务器,让高性能机器处理更多请求
简单随机:将请求随机分发给后端服务器,请求越多,各个服务器收到的请求越平均
加权随机:将请求按权重(根据性能设置权重)随机分发给后端服务器
一致性哈希:根据请求的客户端IP、或请求参数通过哈希算法得到一个数值,利用该数值取模映射出对应的后端服务器,保证同一个客户端或相同参数的请求每次都使用同一台服务器
最小活跃数:将请求分发给请求活跃数最少(统计每台服务器当前正在处理的请求数目)的后台服务器

系统设计角度的负载均衡:

硬件负载均衡:使用专用的硬件设备来分配流量。优点是,性能强大,支持高并发;缺点是太贵,一个就百万。
软件负载均衡:软件应用程序,比如Nginx。优点,灵活便宜,易于修改和拓展;缺点,性能不如硬件方案,但其实够用。
DNS负载均衡:通过DNS服务器将流量分配到多个服务器上,不同客户端请求可能获得不同的IP地址。优点,简单易用,无需额外硬件或软件;缺点,无法感知服务器的实时状态,缓存问题可能导致不均匀负载。
内容分发网络(CDN):CDN是一种分布式的网络结构,可以将内容分发到距离用户最近的节点上。优点,低延迟高体验;缺点,适用于静态内容,动态请求仍需要其他形式的负载均衡。

Nginx(位于7层模型中的应用层)支持的负载均衡策略(算法)

轮询:将请求......,最简单,但无法处理某个节点变慢或者客户端操作有连续性的情况
加权轮询:将请求......,可以提高高性能服务器的利用率
最短响应时间:将请求优先分发给响应时间短的服务器,可以将请求发送到响应时间快的服务器,应对某个节点变慢的情况
IP哈希:根据客户端IP地址的哈希值来确定分配请求的后端服务器,应对客户端操作有连续性的情况,如会话保持
URL哈希:按访问的URL的哈希结果来分配请求,使每个URL定向到一台后端服务器,进一步提高后端缓存服务器的效率,同样可以应对客户端有连续性操作的情况

传输对象、多线程通信

2.常见序列化方式
先了解序列化是干嘛的

和反序列化成对出现,作用是把一个对象从一个JVM 转移到另外一个JVM,过程是:先序列化为字节流,发送到另一个JVM,然后反序列化恢复对象。JAVA提供ObjectOutputStream和ObjectInputStream来实现

能实现相同作用(对象在不同JVM之间传输)的还有

1.消息传递机制:利用消息队列(RabbitMQ、Kafka)或者网络套接字进行通信,将对象从一个JVM发送到另外一个。需要自定义协议来序列化对象并在另外一个JVM中反序列化。
2.使用远程方法调用(RPC)框架,如gRPC,可以在分布式系统中调用远程JVM上的对象的方法。
3.将对象存储在共享数据库(MySQL、PostgreSQL)或共享缓存(Redis),不同JVM可以访问共享数据,适用于需要共享数据,但不需要直接传输对象的场景。

介绍各种序列化方式:

1.Java默认的序列化和反序列化,虽然方便,但存在不跨语言、安全漏洞以及性能差等缺陷

2.FastJson框架

3.Protobuf框架

3.各种序列化方式的优缺点

多线程安全

4.hashmap的线程安全
hashmap不是线程安全的,hashmap在多线程会存在下面的问题:

JDK 1.7 HashMap 采用数组 + 链表的数据结构,多线程背景下,在数组扩容的时候,存在 Entry 链死循环和数据丢失问题。
JDK 1.8 HashMap 采用数组 + 链表 + 红黑二叉树的数据结构,解决了 Entry 链死循环和数据丢失问题。但是多线程背景下,put 方法存在数据覆盖的问题。

要保证线程安全,可以通过以下方法来保证

1.多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。
2.ConcurrentHashmap在JDK1.7,使用Segment+HashEntry分段锁的方式实现
3.ConcurrentHashmap在JDK1.8,抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。

5.如果有一个long类型变量,一个线程要修改,另一个线程要读,这个时候需要加锁吗
两种情况都可以
不加锁:如果是简单修改,直接使用volatile关键字即可,可以实现一旦修改立即刷新到主内存,确保所有线程看到该变量的最新值。
加锁:如果是复合修改操作(自增),则使用synchronized关键字加锁。

6.还有什么其他方式来保证这个long类型变量多线程的安全问题

这里抽象一下问题,考察的点是怎么去保证多线程安全
对于代码块和方法:

synchronized关键字:加锁,确保同一时刻只有一个线程可以访问这些代码。通过synchronized关键字锁定对象的监视器(monitor)实现对象锁。

对于变量:

volatile关键字:一旦修改立即刷新到主内存,确保所有线程看到的是该变量的最新值,而不是可能存储在本地寄存器中的副本。
原子类:Java并发库(java.util.concurrent.atomic)提供了原子类,如AtomicInteger、AtomicLong等,这些类提供了原子操作,可以用于更新基本类型的变量而无需额外的同步。
线程局部变量:ThreadLocal类可以为每个线程提供独立的变量副本,每个线程都拥有自己的变量,消除了竞争条件。

通用的方法

Lock接口和ReentrantLock类:java.util.concurrent.locks.Lock接口提供了比synchronized更强大的锁定机制,ReentrantLock是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。
并发集合:使用java.util.concurrent包中的线程安全集合,如ConcurrentHashMap、ConcurrentLinkedQueue等,这些集合内部已经实现了线程安全的逻辑。
JUC工具类:使用java.util.concurrent包中的一些工具类可以用于控制线程间的同步和协作。例如:Semaphore和CyclicBarrier等。

内存泄露

7.什么情况下会有内存泄露

内存泄露是什么、有什么影响、产生的原因、如何避免
是什么:

指程序在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少。

影响

如果有对象仍被不再使用的引用持有,垃圾收集器无法回收这些内存,导致程序的内存使用不断增加

产生的原因

静态集合:使用静态数据结构(如HashMapArrayList)存储对象,且未清理。
事件监听:未取消对事件源的监听,导致对象持续被引用。
线程:未停止的线程可能持有对象引用,无法被回收。

如何避免
针对大量使用静态属性(static)导致内存泄露

1.减少静态变量
2.如果使用单例,尽量采用懒加载。

针对未关闭的资源导致内存泄露

1.始终记得在finally中进行资源的关闭
2.关闭连接的自身代码不能发生异常
3.Java7以上版本可使用try-with-resources代码方式进行资源关闭

针对ThreadLocal导致的内存泄露:具体情况是,如果线程一直不结束,且有用到ThreadLocal。每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object;ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal就会被回收,ThreadLocalMap的key就会变成null,就没有办法访问对应的value了。而value存在一条强引用链:Thread Ref ->Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏

1.使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;
2.不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。
3.最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。

垃圾回收

8.介绍下可达性分析(垃圾回收知识点)
和引用计数器法一样,都是判断对象是否为垃圾的算法。

引用计数器法:

可达性分析算法:

9.什么东西可以作为GC roots(垃圾收集根,垃圾回收知识点)

拓展:
垃圾回收算法 是什么、有什么用、有哪些
垃圾回收器 是什么、有什么用、有哪些

垃圾回收算法是什么、有什么用

一种自动检测和回收不再使用的对象,从而释放它们所占用的内存空间的机制
避免内存泄漏(一些对象被分配了内存却无法被释放,导致内存资源的浪费)
防止内存溢出(即程序需要的内存超过了可用内存的情况)。

垃圾回收算法有哪些

标记-清除算法:两个阶段,标记,通过可达性分析,标记出所有需要回收的对象;清除,统一回收所有被标记的对象。两个缺点,标记和清除的过程效率都不高;清除结束后会造成大量的碎片空间,在申请大块内存的时候如果没有足够的连续空间会再次 GC。
复制算法:为了解决碎片空间的问题而出现。原理,将内存分成A、B两块,先使用A来分配内存,当A内存不够时,将内存块A中所有存活的复制到内存块B上,再整个清除内存块A。新缺点,每次申请内存只能使用一半的内存空间,利用率很低。
标记-整理算法(又叫标记-压缩算法):改良标记-清除算法,标记之后不会直接清理,而是将所有存活对象都移动到内存的一端,再清理掉剩余部分。两个优点,存活对象多的情况下比复制算法效率高;整理操作减少了碎片空间。
分代回收算法:依据对象的生存周期(经历过的 GC 次数),将内存划分成了新生代和老年代。对象创建时,一般在新生代申请内存。对象每活过一次 GC,年龄 +1。对象活过一定年龄(默认15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,进入老年代。

垃圾回收器有哪些
复制算法垃圾收集器

Serial收集器: 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
ParNew收集器 : 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
Parallel Scavenge收集器: 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

标记-清除算法垃圾回收器

CMS(Concurrent Mark Sweep)收集器: 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

标记-整理算法垃圾回收器

Serial Old收集器 : 老年代单线程收集器,Serial收集器的老年代版本;
Parallel Old收集器 : 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
G1(Garbage First)收集器 : Java堆(包括新生代、老年代)并行收集器,G1收集器是JDK1.7提供的一个新收集器,基于“标记-整理”算法实现,也就是说不会产生内存碎片。重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代

Java21新特性

10.kotlin了解吗

11.最新Java版本的新特性
最新的LTS(long time support)版本是21
新特性主要分成两块
1.新语言特性

Switch 语句的模式匹配:允许在switch的case标签中使用模式匹配,减少了样板代码和潜在错误,使得操作更灵活和类型安全。例如,对于不同类型的账户类,可以在switch语句中直接根据账户类型的模式来获取响应的余额,如case savingsAccount sa ->result = sa.getSavings();
数组模式:模式匹配拓展到数组中,允许在条件语句中更高效的解构和检查数组内容。例如,if (arr instanceof int[] {1, 2, 3}),可以直接判断数组arr是否匹配指定的模式
字符串模板(预览版):提供了一种更可读、更易于维护的方式来构建复杂字符串,支持在字符串字面量中直接嵌入表达式。变化如下,"hello " + name + ", welcome to the geeksforgeeks!" ,现在是hello {name}, welcome to the geeksforgeeks!

2.新并发特性

虚拟线程:一种轻量级并发的新选择。通过共享堆栈的方式,大大降低了内存消耗,提高了应用程序的吞吐量和响应速度。可以通过静态构建方法、构建器或ExecutorService来创建和使用虚拟线程。
范围值(Scoped Values):一种在线程间共享不可变数据的新方式,避免使用传统的线程局部存储,促进了更好的封装性和线程安全。在不通过方法参数传递的情况下,也能传递上下文信息,如用户会话或配置设置。