讲一讲JVM
JVM(Java虚拟机)是Java程序能够跨平台运行的关键,它提供了一个抽象的计算机,它允许Java程序在不同操作系统和硬件架构上运行,而无需重新编译。首先介绍JVM的组成:
组成与功能
-
** 类加载器(Class Loader)**:
- 负责将字节码文件加载到JVM内存中。类加载器从类路径、JAR包或网络中加载类,并将它们转换为JVM能理解的内存结构。
- 类加载器根据加载顺序分为:启动类加载器、扩展类加载器和应用类加载器。
-
运行时数据区(Runtime Data Area):
- 方法区:存储类的元数据(例如类的结构、常量池、字段、方法等)。
- 堆(Heap):用于存储对象实例,几乎所有的对象都在堆中分配内存。
- 栈(Stack):每个线程有独立的栈,用于存储局部变量、操作数栈和方法调用的上下文。
- 程序计数器(Program Counter Register):指示当前线程执行的字节码指令的地址。
- 本地方法栈(Native Method Stack):为JVM调用本地方法(例如使用C/C++编写的代码)提供支持。
-
执行引擎(Execution Engine):
- 解释器:逐条解释字节码指令,适合初期运行。
- JIT编译器(Just-In-Time Compiler):将热点代码编译为本地机器码以提高执行效率。
- 垃圾回收器(Garbage Collector, GC):负责自动回收不再使用的内存,避免内存泄漏。
垃圾回收
- 引用技术法:引用计数法是一种最简单的垃圾回收算法,通过维护对象的引用计数来判断对象是否可以被回收。不能处理循环引用问题。
- 标记-清除(Mark-and-Sweep):
- 标记所有可达的对象,然后清除不可达的对象。这是最基本的垃圾回收算法,但可能导致内存碎片。能够有效解决循环引用问题。可能会产生大量的内存碎片。
- 复制算法(Copying):
- 将存活的对象从一个区域复制到另一个区域,较大区域的垃圾回收后通过压缩内存来减少碎片。通常用于新生代的回收。
- 标记-压缩(Mark-Compact):
- 在标记阶段与清除阶段之后,回收器会将存活的对象压缩在内存的一端,从而减少内存碎片。
- 分代收集:
- JVM将堆划分为新生代(Young Generation)、老年代(Old Generation)和持久代(Permanent Generation)。新生代主要存放短生命周期的对象,老年代则存放长生命周期的对象。JVM根据对象的存活时间选择不同的垃圾回收策略。一般新生代使用复制算法,老年带使用标记-整理算法。
- Minor GC:发生在新生代对象较多时,通常较快。
- Major GC(或Full GC):发生在老年代对象较多时,通常较慢,回收的对象较大。
- 分区回收:
- 分区算法将堆内存划分为多个大小相等的区域(Region),每个区域可以动态地分配给新生代或老年代使用。垃圾回收时,选择部分区域进行回收,而不是回收整个堆内存。避免了对整个堆的回收,降低了回收的停顿时间。
- 垃圾回收器的类型:
- Serial GC:单线程回收器,适用于单核或内存较小的机器。
- Parallel GC:多线程回收器,适用于多核系统,能够提高垃圾回收的吞吐量。
- CMS(Concurrent Mark-Sweep)GC:并发标记清除算法,旨在减少停顿时间。
- G1 GC:较为现代的垃圾回收器,设计上平衡了低停顿与高吞吐量,G1 GC的工作原理和其他GC算法相比,采用了更多的分代和区域化策略,能够进行多次并行标记和清理操作。
G1 GC
G1(Garbage First)垃圾回收器是JVM中一种相对较新的垃圾回收器,它的设计目标是能够在大内存系统中提供较好的回收效果,特别是在长时间运行的应用中,G1 GC力图在保证高吞吐量的同时,减少停顿时间,适合需要低延迟的应用场景。
G1 GC的工作原理和其他GC算法相比,采用了更多的分代和区域化策略,以下是它的详细介绍:
G1 GC的主要特点
-
分区化(Region-Based):
- G1 GC把堆内存划分为多个大小相等的区域(Regions),每个区域的大小一般为几MB(具体可以配置)。这与传统的堆划分为新生代、老年代、持久代的方式不同,G1通过将堆内存划分为不同的区域,能更灵活地管理内存。
- 每个区域可以是新生代的一部分,也可以是老年代的一部分,也可以是一个Humongous区域(大对象存储区),根据实际需要动态调整。
-
并行和并发:
- G1 GC通过多个线程并行地进行垃圾回收操作,尤其在标记和回收过程中使用多个线程提高回收效率。
- 在标记阶段,它会并发地进行标记工作,减少停顿时间。
-
优先回收“垃圾最多”的区域:
- G1 GC的一个重要特点是它优先回收那些“垃圾最多”的区域。它的回收过程会根据每个区域的回收成本(回收所需的时间)来决定是否回收该区域。
- 在进行垃圾回收时,G1 GC会根据当前的回收目标(例如,停顿时间目标)选择性地回收一部分区域,尽可能减少应用的停顿。
-
停顿时间目标:
- G1 GC允许开发者通过
-XX:MaxGCPauseMillis
参数来设置停顿时间目标,G1会尽量确保垃圾回收的停顿时间不超过设定值。 - G1的目标是尽量平衡吞吐量和停顿时间。它使用一个预测模型来估算不同回收区域的回收时间,从而在达到停顿时间目标的同时尽量回收最多的垃圾。
- G1 GC允许开发者通过
-
增量式标记:
- 标记过程被拆分为多个阶段:初始标记、并发标记、最终标记。这种增量式标记可以减少停顿时间。
- 初始标记:快速标记根对象,这个过程是Stop-The-World(STW)的,时间较短。
- 并发标记:与应用程序线程并行进行,标记存活的对象。
- 最终标记:回收过程中最后对存活对象的处理,这也可能是STW的过程。
- 标记过程被拆分为多个阶段:初始标记、并发标记、最终标记。这种增量式标记可以减少停顿时间。
-
混合垃圾回收:
- G1 GC会将年轻代和老年代的回收结合起来进行。例如,在Minor GC过程中,G1不仅回收年轻代,还会回收一部分老年代的区域,尤其是在老年代压力较大的情况下。
-
Humongous对象处理:
- G1 GC会单独处理大对象(大于一个区域大小的对象),这些大对象称为Humongous对象。它们通常直接分配到多个区域(Region)中,G1会专门处理这些对象的回收。
G1 GC的工作流程
G1 GC的工作流程包括以下几个关键步骤:
-
初始标记(Initial Mark):
- 标记根对象,通常会导致应用线程暂停(STW),时间较短。
-
并发标记(Concurrent Mark):
- 在应用线程运行的同时,标记所有活跃的对象。这一阶段不会暂停应用程序。
-
最终标记(Final Mark):
- 最后对标记过程中的一些细节进行处理,可能会暂停应用线程。它也包括一些并发操作。
-
清理(Cleanup):
- 在整个标记阶段完成后,清理掉不再使用的对象,并准备进行回收。
-
混合回收(Mixed GC):
- 根据目标停顿时间,G1 GC会选择一部分区域进行回收。回收的区域可以是新生代和老年代的交集。
两个文件里存了50亿条url,总共大小320G,内存限制4G,如何找出两个文件中相同的url?
方案1:分治法+哈希
- 将a、b 两个文件,用相同的哈希函数(把url换成数字的话,哈希函数更容易构造),分解为1000个独立哈希值相同的小文件
- 哈希值相同的url必然在序号对应的文件中,因此只要在序号对应的两个文件中进行url的相互匹配即可
- 比较每对序号对应的小文件时,可以使用hash_set
==方案2:==布隆过滤器
利用布隆过滤器的特性,在允许一定错误率的情况下,可以快速判断一个元素是否在一个集合中,适用于在有限内存内处理大量数据。
- 先遍历文件a,将其中的url逐一填充到布隆过滤器中
- 再遍历文件b,判断其中的url是否在布隆过滤器当中。注意:布隆过滤器,只能判断某数据一定不存在,不能判断其一定存在,存在误差。
线程之间如何通信
- 共享内存: 线程可以通过共享内存来进行通信。这通常通过共享对象或者共享变量来实现。在这种方式中,多个线程访问同一块内存区域(如共享的变量、对象等),从而可以相互传递信息。 需要使用锁或者violate来保证读写正确。
- 使用
wait()
和notify()
来实现线程通信 - 使用JUC中的工具类
BlockingQueue
、CountDownLatch
、CyclicBarrier
、Semaphore
、Exchanger
AQS如何通过无锁实现的
在关键操作处使用CAS+无限循环,例如AQS入队操作,修改state变量操作(获取锁)。
hashmap&linkedhashmap&hashtable区别
特性 | HashMap | LinkedHashMap | Hashtable |
---|---|---|---|
线程安全性 | 不保证线程安全 | 不保证线程安全 | 线程安全 |
存储顺序 | 无序 | 按照插入顺序或访问顺序 | 无序 |
是否允许 null 键/值 | 允许一个 null 键和多个 null 值 | 允许一个 null 键和多个 null 值 | 不允许 null 键和 null 值 |
性能 | 快 | 相对较慢(因维护顺序) | 慢(由于同步开销) |
迭代顺序 | 不确定 | 按插入顺序或访问顺序 | 不确定 |
使用场景 | 单线程或外部同步的多线程环境 | 保持顺序的场景,如缓存管理 | 线程安全需求较高的场景,但性能差 |
JAVA的引用类型和应用场景
Java 中有四种引用类型,分别是 强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和 虚引用(Phantom Reference)。这些引用类型的不同之处在于它们的垃圾回收机制对对象的影响不同。下面详细解释每种引用类型及其应用场景。
强引用(Strong Reference)
-
定义:这是 Java 中最常见的引用类型,也是默认的引用类型。当一个对象通过强引用进行引用时,垃圾回收器不会回收该对象,除非该对象不再有任何引用。
-
示例:
Object obj = new Object(); // 强引用
-
应用场景:适用于一般的对象引用,凡是我们需要在程序中持续使用的对象,通常使用强引用。例如,普通的对象创建和操作。
-
特点:
- 如果一个对象仅有强引用指向它,那么该对象在没有其他引用的情况下将永远不会被垃圾回收器回收。
- 它不会主动触发垃圾回收,除非内存紧张导致。
软引用(Soft Reference)
-
定义:软引用是指如果一个对象仅有软引用,它在系统内存不足时会被垃圾回收器回收,但在系统内存足够时,它会被保留在内存中。
-
示例:
SoftReference<Object> softRef = new SoftReference<>(new Object());
-
应用场景:适用于缓存实现,例如图片缓存、数据缓存等。软引用非常适用于需要较长时间保存但又能接受被回收的对象,比如一个缓存系统,在内存充足的情况下,缓存会保存,但当内存紧张时,缓存会被回收。
-
特点:
- 软引用适用于一些可以在内存不足时被丢弃的对象,避免内存泄漏。
- 当内存充足时,软引用对象不会被回收,只有当内存不足时,才会被回收。
弱引用(Weak Reference)
-
定义:弱引用与软引用类似,但不同的是,当一个对象仅有弱引用时,无论系统内存是否足够,垃圾回收器都会在下一次垃圾回收时回收它。
-
示例:
WeakReference<Object> weakRef = new WeakReference<>(new Object());
-
应用场景:适用于需要缓存、但是对缓存不做强依赖的对象。例如:注册表、事件监听器、回调方法等场景,可以使用弱引用。当对象不再需要时,弱引用会让它尽早被垃圾回收。
-
特点:
- 弱引用适合实现对象的临时缓存,特别是那些不再需要时应立即回收的对象。
- 弱引用对象一旦不再有强引用指向它,就会被垃圾回收器回收。
虚引用(Phantom Reference)
-
定义:虚引用是所有引用类型中最弱的一种,虚引用指向的对象几乎可以说没有任何实际的引用。虚引用的作用是在垃圾回收器准备回收一个对象时收到一个系统通知,它提供了一种机制,可以在对象被垃圾回收之前进行一些清理工作。
-
示例:
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());
-
应用场景:虚引用适用于需要在垃圾回收时做某些清理工作的场景,例如,直接内存的管理,或者需要在对象销毁时做一些资源的释放和清理工作。虚引用主要用于对对象的清理操作,例如清理文件、数据库连接等资源。
-
特点:
- 虚引用对象几乎不会对对象的生命周期产生影响,垃圾回收器在回收一个对象之前会先将其放入与虚引用关联的
ReferenceQueue
中。 - 虚引用不能单独使用,必须与
ReferenceQueue
配合使用。
- 虚引用对象几乎不会对对象的生命周期产生影响,垃圾回收器在回收一个对象之前会先将其放入与虚引用关联的
引用类型总结表
引用类型 | 特点 | 是否参与 GC | 应用场景 |
---|---|---|---|
强引用 | 最常见的引用类型,对象只要有强引用,垃圾回收器不会回收它 | 不会被回收 | 普通对象的引用,用于持久化对象或常用对象的管理。 |
软引用 | 在内存不足时,软引用对象会被回收 | 内存不足时会回收 | 实现缓存机制,如图像缓存、文件缓存等。 |
弱引用 | 无论内存是否充足,只要没有强引用,弱引用对象就会被回收 | 会被立即回收 | 临时性缓存对象,事件监听器,回调函数等。 |
虚引用 | 在对象被回收时,通过 ReferenceQueue 得到回收通知 | 被回收时通知 | 清理操作,如文件关闭、数据库连接释放等。 |
总结:
- 强引用是默认的引用方式,适用于普通对象存储。
- 软引用适用于缓存场景,能够在内存不足时回收对象。
- 弱引用适用于那些对对象存在弱依赖的场景,可以在对象没有强引用时立即被回收。
- 虚引用则是最弱的引用类型,主要用于在对象被回收之前进行清理工作。
正确选择引用类型能在不同场景中有效管理内存,减少内存泄漏或过早回收的风险,确保程序高效运行。
##threadlocal及其底层实现,key为什么不可以是软引用
ThreadLocal 及其应用
-
定义:
ThreadLocal
提供了每个线程都能拥有一个独立的变量副本。每个线程访问其本地副本,避免了线程间共享数据带来的竞争问题。常用于存储线程相关的信息,如数据库连接、会话状态、用户请求等。 -
应用场景:
ThreadLocal
主要用于需要为每个线程分配独立的存储空间的场景。例如,数据库连接池、线程池等。它可以避免线程之间的共享资源竞争,提高并发效率。
ThreadLocal 的底层实现
-
ThreadLocal
的底层实现通过线程内部的数据结构(通常是Thread
类中的一个ThreadLocalMap
)来存储每个线程独有的变量副本。每个线程都有一个ThreadLocalMap
,它用于存储该线程的ThreadLocal
对象的副本。 -
关键部分:
- 每个
ThreadLocal
对象在底层有一个ThreadLocalMap
来存储对应线程的局部变量。 ThreadLocalMap
的 key 是ThreadLocal
对象本身,value 是线程局部变量的值。ThreadLocalMap
内部使用了弱引用(WeakReference)来引用ThreadLocal
对象的 key,防止ThreadLocal
对象无法被垃圾回收。
- 每个
-
关键点:每个线程有一个
ThreadLocalMap
,存储了所有ThreadLocal
对象的副本。为了避免内存泄漏,ThreadLocalMap
的key
使用了弱引用 (WeakReference
),当ThreadLocal
对象没有强引用时,它会被回收,ThreadLocalMap
会清理对应的条目。
为什么 ThreadLocalMap
中的 key 不能是软引用(SoftReference)?
关键问题在于 垃圾回收机制。
-
弱引用(WeakReference) vs 软引用(SoftReference):
- 弱引用:弱引用的对象在下一次垃圾回收时会被回收,不管系统内存是否充足。
- 软引用:软引用的对象在内存不足时会被回收,而在内存足够时则会被保留。
-
问题分析:
- 如果
ThreadLocalMap
中的key
使用软引用(SoftReference
),那么当内存充足时,ThreadLocal
对象会被保留,即便这个ThreadLocal
对象已经不再被其他地方使用。 - 这种设计就会导致 内存泄漏,因为即使
ThreadLocal
对象不再需要,依然无法被及时回收,ThreadLocalMap
中的引用依然存在。只有在内存不足时才会被回收,这可能导致不必要的内存占用。
例如:
- 如果一个线程创建了一个
ThreadLocal
,并将其作为key
存储在ThreadLocalMap
中,在程序执行过程中,某些线程可能会结束,ThreadLocal
对象的key
在内存充足时不会被回收,这样就会使得这些ThreadLocal
对象无法被垃圾回收,造成内存泄漏。
- 如果
-
避免内存泄漏:
- 使用弱引用可以确保当
ThreadLocal
对象不再被使用时,垃圾回收器会及时回收它,并清理ThreadLocalMap
中的条目,从而避免内存泄漏的问题。 - 在
ThreadLocalMap
的实现中,当ThreadLocal
对象不再被强引用时,ThreadLocalMap
中的条目会被清除,避免了资源的浪费。
- 使用弱引用可以确保当
总结
ThreadLocal
允许每个线程有独立的变量副本,避免了线程间的数据共享和同步问题。- 底层实现使用了
ThreadLocalMap
来存储每个线程的局部变量,其中ThreadLocal
的 key 是弱引用,防止内存泄漏。 - 不能使用软引用作为
ThreadLocalMap
的 key,因为软引用的对象只有在内存紧张时才会被回收,这可能会导致对象被过晚回收,从而造成内存泄漏。弱引用可以在对象没有其他引用时立即回收,确保内存的及时回收。
这种设计确保了 ThreadLocal
的对象不会因为线程结束或其他原因导致内存泄漏,并保证了 ThreadLocalMap
的清理机制不会受内存状况影响。
从输入URL到出现页面都发生了什么?
-
检查URL合法性
-
检查本地是否缓存了该页面的HTML文件或相关资源。这个过程依赖于HTTP缓存头。 如果浏览器缓存中已经存在资源,并且资源未过期,浏览器会直接使用缓存的数据进行渲染,从而跳过 HTTP 请求和响应过程。 缓存分为强制缓存和协商缓存
- 强制缓存:如果资源没有过期,浏览器直接从缓存中获取。
- 协商缓存:即便资源过期,浏览器也可以通过向服务器发送请求(带有
If-Modified-Since
或If-None-Match
头部)来检查服务器端该资源是否已更改。如果资源未更改,服务器会返回一个 304 Not Modified 响应,浏览器则从本地缓存中使用该资源。
-
DNS查询,查询URL中域名对应的IP地址, 如果浏览器的 DNS 缓存中已经有了该域名的 IP 地址,则直接使用该地址。 否则,浏览器会依次查询本地 DNS 缓存、操作系统缓存、路由器缓存以及上游 DNS 服务器(通常是运营商提供的 DNS 服务器)直到获得该域名的 IP 地址。
-
三次握手建立TCP连接
-
发送HTTP请求
-
服务器处理并返回请求
-
接受HTTP相应并解析
-
渲染页面
-
执行JS
-
显示页面
-
四次挥手释放连接
http请求过程中,会有一个encode操作,知道吗?为什么要有encode操作?
在 HTTP 请求过程中,编码(encode)操作是为了确保数据的正确传输。常见的编码操作包括:
- URL 编码:确保 URL 中的特殊字符不引起解析错误。
- 请求体编码:将请求的数据转换为适合传输的格式,如 JSON 编码、URL 编码表单数据或
multipart
编码。 - 请求头编码:如 Base64 编码,用于传输认证信息或二进制数据。
通过这些编码操作,HTTP 请求能够更可靠地传输数据,避免因字符冲突或解析错误导致的请求失败。
LRU缓存算法
LRU(Least Recently Used)缓存算法是一种常用的缓存淘汰策略,用于在缓存满时移除最近最少使用的条目,以保证空间留给新的数据。LRU 算法常用于内存管理、数据库缓存等应用场景。
LRU 缓存算法的概念
- 核心思想:LRU 缓存算法会在缓存满时移除最久未使用的缓存条目。每次访问或插入数据时,都会将该数据标记为最近使用的。如果缓存满了,就移除最近最少使用的数据。
- 实现方法:典型的实现使用 双向链表 和 哈希表 组合,以提供
O(1)
时间复杂度的插入、删除和访问操作。
LRU 缓存的实现思路
- 双向链表:用于维护条目的访问顺序。最新访问的条目放在链表头部,最少使用的条目放在链表尾部。
- 哈希表:用于快速访问缓存中的条目,存储键值对。
实现 LRU 缓存算法(Java 示例)
下面是用 Java 实现一个简单的 LRU 缓存。
import java.util.HashMap;
public class LRUCache<K, V> {
private final int capacity;
private final HashMap<K, Node<K, V>> map;
private final DoublyLinkedList<K, V> cacheList;
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>();
this.cacheList = new DoublyLinkedList<>();
}
public V get(K key) {
if (!map.containsKey(key)) {
return null; // Key not found
}
Node<K, V> node = map.get(key);
cacheList.moveToHead(node);
return node.value;
}
public void put(K key, V value) {
if (map.containsKey(key)) {
Node<K, V> node = map.get(key);
node.value = value;
cacheList.moveToHead(node);
} else {
if (map.size() >= capacity) {
Node<K, V> tail = cacheList.removeTail();
if (tail != null) {
map.remove(tail.key);
}
}
Node<K, V> newNode = new Node<>(key, value);
cacheList.addToHead(newNode);
map.put(key, newNode);
}
}
private static class Node<K, V> {
K key;
V value;
Node<K, V> prev;
Node<K, V> next;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
private static class DoublyLinkedList<K, V> {
private Node<K, V> head;
private Node<K, V> tail;
DoublyLinkedList() {
head = new Node<>(null, null);
tail = new Node<>(null, null);
head.next = tail;
tail.prev = head;
}
void addToHead(Node<K, V> node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
void moveToHead(Node<K, V> node) {
removeNode(node);
addToHead(node);
}
Node<K, V> removeTail() {
if (tail.prev == head) {
return null;
}
Node<K, V> node = tail.prev;
removeNode(node);
return node;
}
void removeNode(Node<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
}
public static void main(String[] args) {
LRUCache<Integer, String> lruCache = new LRUCache<>(3);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.put(3, "C");
System.out.println(lruCache.get(2)); // Output: B
lruCache.put(4, "D"); // Removes key 1 (least recently used)
System.out.println(lruCache.get(1)); // Output: null (key 1 was evicted)
System.out.println(lruCache.get(3)); // Output: C
}
}
代码解释
HashMap<K, Node<K, V>>
:用于存储键与对应节点的映射,以快速访问节点。DoublyLinkedList<K, V>
:用于维护缓存的访问顺序,最新使用的节点在头部,最少使用的在尾部。addToHead()
:将节点添加到链表的头部。moveToHead()
:将已经存在的节点移到头部,表示最近访问。removeTail()
:移除链表尾部节点,表示最近最少使用的条目。
大数字相加
使用 String
实现大数字相加是一个常见的算法问题,适用于超过基本数据类型(如 int
或 long
)存储范围的数字相加。下面是如何实现这一功能的 Java 代码。
实现思路
- 从后往前遍历字符串:从最低位开始逐位相加,模拟手工计算的过程。
- 处理进位:每一位相加可能会产生进位,需要在下一位计算时考虑。
- 处理不同长度的字符串:当两个字符串长度不同时,可以在较短的字符串前补零,或者直接处理。
- 结果拼接:将结果逐位拼接成一个新的
String
。
Java 实现代码
public class BigNumberAddition {
public static String addStrings(String num1, String num2) {
// 将两个字符串从后往前逐位相加
int i = num1.length() - 1;
int j = num2.length() - 1;
int carry = 0; // 进位
StringBuilder result = new StringBuilder();
// 当有进位或者还有数字没处理时继续循环
while (i >= 0 || j >= 0 || carry != 0) {
int digit1 = (i >= 0) ? num1.charAt(i) - '0' : 0;
int digit2 = (j >= 0) ? num2.charAt(j) - '0' : 0;
// 当前位相加并加上进位
int sum = digit1 + digit2 + carry;
carry = sum / 10; // 更新进位
result.append(sum % 10); // 将当前位加入结果
i--; // 移动到下一位
j--;
}
// 结果需要反转,因为是从低位开始加的
return result.reverse().toString();
}
public static void main(String[] args) {
String num1 = "123456789012345678901234567890";
String num2 = "987654321098765432109876543210";
String result = addStrings(num1, num2);
System.out.println("Sum: " + result);
}
}
版本号排序
要对版本号进行排序,需要根据版本号的各个部分进行逐位比较,将字符串版本号拆分为数字进行对比。下面是一个实现此功能的 Java 代码。
实现思路
- 拆分版本号:使用
.
分隔符将版本号拆分为多个部分。 - 逐部分比较:将每一部分转换为整数并进行比较。如果一部分较大,则该版本号更大。
- 处理不同长度的版本号:当两个版本号长度不同时,缺少的部分视为
0
。
Java 实现代码
import java.util.Arrays;
import java.util.Comparator;
public class VersionSorter {
public static String[] sortVersions(String[] versions) {
Arrays.sort(versions, new Comparator<String>() {
@Override
public int compare(String v1, String v2) {
String[] parts1 = v1.split("\\.");
String[] parts2 = v2.split("\\.");
int length = Math.max(parts1.length, parts2.length);
for (int i = 0; i < length; i++) {
int num1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0;
int num2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0;
if (num1 != num2) {
return num1 - num2;
}
}
return 0; // 如果所有部分都相等
}
});
return versions;
}
public static void main(String[] args) {
String[] versions = { "1.45", "1.8", "13.11", "13.8", "2.0", "1.0.1" };
String[] sortedVersions = sortVersions(versions);
System.out.println("Sorted versions: " + Arrays.toString(sortedVersions));
}
}
代码说明
split("\\.")
:使用正则表达式\\.
分割版本号字符串。- 比较逻辑:
- 循环逐部分比较,每部分转换为整数。
- 如果一个版本号比另一个短,例如
1.0
与1.0.1
,则视缺少的部分为0
。
Arrays.sort
:使用自定义比较器实现版本号排序。
介绍进程、线程、协程
进程、线程和协程是计算机程序执行的不同方式和概念,它们有各自的特点和应用场景。下面是它们的介绍:
进程(Process)
- 定义:进程是操作系统分配资源和调度的基本单位。它是一个正在运行的程序,每个进程都有自己独立的内存空间、数据段、堆和栈。
- 特点:
- 隔离性:进程之间是相互独立的,一个进程的崩溃不会影响其他进程。
- 资源消耗大:创建、销毁进程和进程间通信(IPC)开销相对较大。
- 多任务处理:操作系统通过进程调度来实现多任务处理。
- 应用场景:用于需要较高隔离性的应用,如浏览器的多标签运行、多用户登录会话等。
线程(Thread)
- 定义:线程是进程中的一个执行单元,是操作系统调度的最小单位。一个进程可以包含多个线程,线程共享同一进程的内存空间(如代码段、数据段和堆)。
- 特点:
- 共享内存:线程共享进程内存,可以更高效地进行通信和数据共享。
- 轻量级:线程的创建和销毁开销相对较小,切换速度快。
- 并发执行:多线程允许同一进程内的代码并发执行,提高程序的效率。
- 安全性问题:由于线程共享内存,可能会引发线程安全问题,需要使用同步机制(如
synchronized
、ReentrantLock
)。
- 应用场景:适合需要并发执行但不需要进程级隔离的场景,如并行数据处理、Web 服务器、实时应用等。
协程(Coroutine)
- 定义:协程是一种更轻量级的线程,用户级的并发执行单元。它由程序控制,而非操作系统调度。协程可以在代码中手动暂停和恢复,允许在一个线程内实现多任务处理。
- 特点:
- 低开销:协程是用户级别的调度,不涉及系统调用,创建和切换的开销非常小。
- 非抢占式:协程在执行时不会被操作系统中断,只有在显式调用
yield
或await
等暂停函数时才会让出执行权。 - 简单的并发模型:避免了多线程的竞争条件,使用同步代码编写异步逻辑,易于理解和维护。
- 应用场景:适合 I/O 密集型操作、异步编程、事件驱动开发,如 Web 服务器、网络爬虫、游戏开发等。
介绍Http1.0、Http2.0、Http3.0
HTTP/1.0
- 特点:
- 单个请求-响应模型:每次请求需要建立一次 TCP 连接,处理完毕后立即关闭连接。这种方式导致了较大的连接开销。
- 无状态:HTTP 本质上是无状态协议,每次请求都是独立的。
- 无持久连接:默认情况下,HTTP/1.0 不支持持久连接(
Connection: keep-alive
是后来引入的扩展)。 - 性能瓶颈:由于每次请求都要重新建立和关闭连接,会造成网络延迟和资源浪费。
HTTP/2.0
- 特点:
- 二进制分帧:HTTP/2.0 使用二进制格式而非 HTTP/1.x 的文本格式,提升了解析和传输的效率。
- 多路复用:允许多个请求和响应通过一个 TCP 连接并发进行,解决了 HTTP/1.x 的队头阻塞问题。
- 头部压缩:通过 HPACK 算法对头部进行压缩,减少了传输的开销和延迟。
- 服务器推送:服务器可以主动将资源推送到客户端缓存中,以减少延迟。
- 持久连接:HTTP/2.0 默认使用持久连接,避免了频繁建立和关闭 TCP 连接的问题。
- 优点:相较于 HTTP/1.0 和 HTTP/1.1,HTTP/2.0 提供了更高的传输性能、更快的页面加载速度和更好的用户体验。
HTTP/3.0
- 特点:
- 基于 QUIC:HTTP/3.0 运行在 QUIC 协议之上,而 QUIC 是一种基于 UDP 的传输协议,旨在解决 TCP 的传输延迟和队头阻塞问题。
- 改进的连接建立速度:QUIC 集成了加密和连接建立的过程,实现了 0-RTT(零往返时间)连接建立,使得连接速度更快。
- 无队头阻塞:由于 QUIC 是基于 UDP 之上的多路复用协议,它在单个数据流被阻塞时不会影响到其他流的传输。
- 内置加密:HTTP/3.0 默认使用 TLS 1.3 加密,实现了更安全的数据传输。
- 优点:
- 低延迟:HTTP/3.0 可以减少首字节时间(TTFB),提高用户体验。
- 流量控制和拥塞控制:QUIC 采用更灵活的流量控制机制,能更好地应对网络波动。
- 局限性:由于是基于 UDP 之上的新协议,一些防火墙或网络设备可能不支持或需要更新以允许 HTTP/3.0 的流量。
比较总结
特性 | HTTP/1.0 | HTTP/2.0 | HTTP/3.0 |
---|---|---|---|
传输协议 | TCP | TCP | QUIC(基于 UDP) |
连接复用 | 不支持 | 支持多路复用 | 支持多路复用 |
数据格式 | 文本 | 二进制 | 二进制 |
头部压缩 | 无 | 支持(HPACK) | 支持(QPACK) |
加密 | 无(可用 HTTPS) | 无(可用 HTTPS) | 默认加密(内置 TLS 1.3) |
队头阻塞 | 存在 | 缓解 | 消除 |
连接建立 | 高延迟(TCP 三次握手) | 高延迟(TCP + TLS 握手) | 低延迟(QUIC 一次握手) |
设计模式
挑了5个
单例模式(Singleton Pattern)
-
核心概念:确保一个类只有一个实例,并提供全局访问点。
-
应用场景:需要全局唯一的对象,如配置管理器、日志记录器、数据库连接池。
-
示例:线程安全的懒汉式单例实现。
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; } }
工厂模式(Factory Pattern)
-
核心概念:定义一个创建对象的接口,但让子类决定实例化哪个类。
-
应用场景:需要动态创建对象且无需指定确切类名时,如创建不同类型的文档、数据库连接等。
-
示例:简单工厂或工厂方法模式,用于创建形状对象。
public interface Shape { void draw(); } public class Circle implements Shape { public void draw() { System.out.println("Drawing a Circle"); } } public class ShapeFactory { public Shape getShape(String shapeType) { if ("CIRCLE".equalsIgnoreCase(shapeType)) { return new Circle(); } // 添加其他形状创建逻辑 return null; } }
策略模式(Strategy Pattern)
-
核心概念:定义一系列算法,将每个算法封装起来,使它们可以互换。
-
应用场景:需要在运行时切换算法时,如支付方式、数据压缩方式等。
-
示例:支付方式的选择。
public interface PaymentStrategy { void pay(int amount); } public class CreditCardPayment implements PaymentStrategy { public void pay(int amount) { System.out.println("Paid " + amount + " using credit card."); } } public class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy = strategy; } public void executePayment(int amount) { strategy.pay(amount); } }
观察者模式(Observer Pattern)
-
核心概念:定义对象间的一对多依赖关系,当一个对象改变时,依赖它的对象会自动收到通知并更新。
-
应用场景:发布-订阅系统、GUI 事件系统。
-
示例:新闻发布订阅系统。
public interface Observer { void update(String message); } public class NewsSubscriber implements Observer { public void update(String message) { System.out.println("Received news update: " + message); } } public class NewsPublisher { private List<Observer> subscribers = new ArrayList<>(); public void subscribe(Observer observer) { subscribers.add(observer); } public void notifyObservers(String message) { for (Observer observer : subscribers) { observer.update(message); } } }
代理模式(Proxy Pattern)
-
核心概念:为其他对象提供代理,以控制对原对象的访问。
-
应用场景:远程代理、虚拟代理、保护代理等。
-
示例:虚拟代理用于延迟加载。
public interface Image { void display(); } public class RealImage implements Image { private String fileName; public RealImage(String fileName) { this.fileName = fileName; loadFromDisk(); } private void loadFromDisk() { System.out.println("Loading " + fileName); } public void display() { System.out.println("Displaying " + fileName); } } public class ProxyImage implements Image { private RealImage realImage; private String fileName; public ProxyImage(String fileName) { this.fileName = fileName; } public void display() { if (realImage == null) { realImage = new RealImage(fileName); } realImage.display(); } }
针对单例模式深入一下
//懒汉式:核心,当我们需要时在创建对象
public class LazyMan {
//构造器私有
private LazyMan(){
if (lazyMan!=null){
throw new RuntimeException("不要试图使用反射破坏单例!");
}
System.out.println(Thread.currentThread().getName()+"\t初始化成功");
}
private static volatile LazyMan lazyMan;
/**
* 这个简单的懒汉模式在单线程下没问题,但是显然在多线程中会出现严重的多线程不安全问题
*/
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static LazyMan getInstance2(){
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan();
}
}
}
return lazyMan;
}
//多线程破坏单例模式
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
//此处的Thread是常规的函数式接口,使用lambda来简写
new Thread(()->{
LazyMan.getInstance2();
}).start();
}
//显然执行结果有多条,破坏了单例模式
}
}
public enum EnumSingle {
INSTANCE; //枚举本身线程安全且防反射破坏
public EnumSingle getInstance(){
return INSTANCE;
}
}
我们尝试使用反射破坏单例,发现枚举类并没有无参够着,然后我们通过查看枚举类源码可知,枚举类只有一个有参构造,参数为Sting和int型,我们再次使用反射攻击发现报错,不能通过反射破坏枚举的单例,我们查看反射的源码可知,在对枚举使用newInstance时是有一个判断的。所以枚举类天生便防反射破坏。
标签:面试题,缓存,字节,收集,对象,回收,线程,引用,public From: https://blog.csdn.net/qq2501055281/article/details/143692093