JAVA基础校招必备
2024-11-02 14:27:56 0 举报
AI智能生成
校招必备基础八股之一 JAVA基础
作者其他创作
大纲/内容
对于 Object 来说,其 equals 方法底层实现就是“==”,都是比较对象的引用是否相同
JDK 中其他类通常会重写 equals 以实现具体的值比较,例如 Integer 中的 equals 和 String 中的 equals等
Integer 会先将 Integer 对象转换成基础类型 int 值来进行比较,所以此时就不再对比两个对象的引用了,而是对比两个对象的值相等
String 和 Integer 一样,是将 Object 中的引用比较重写成值比较
典型回答
对于 Object 来说,== 和 equals 都是一样的,都是用来比较两个对象的引用是否相同
而其他 Java 类中,通常都会重写 equals 让其变为比较具体的值是否相同,而非引用是否相同
通常会使用 == 来对比两个对象引用是否相同;使用 equals 来对比两个值是否相同
考点分析
== 与 equals
指同一个类中定义多个方法,它们具有相同的名称但参数列表不同
定义
因为返回值不同 JVM 无法分辨到底调用哪个方法
1. JVM 调用方法是通过方法签名来判断到底要调用哪个方法
2. 方法签名 = 方法名称 + 参数类型 + 参数个数组成的唯一值
3. 返回类型不是方法签名的组成部分,所以不同的返回类型也就不算方法重载,因为它不能让 JVM 确定要调用哪个方法
返回值不同不算方法重载
方法重载
finally 中的代码一定会执行的
正常情况
1. 程序在 try 块中遇到 System.exit() 方法,会立即终止程序执行
2. 在 try 中遇到 Runtime.getRuntime().halt() 代码,强制终止正在运行的 JVM。与上面方法不同,此方法不会触发 JVM 关闭程序。因此调用halt方法时,都不会执行关闭钩子和终结器
3. 程序在 try 块中遇到无限循环或者死锁等情况
4. 掉电问题(停电)
5. JVM 异常崩溃
异常情况
是一种由父类提供的空或默认实现的方法,子类可以选择性地重写或扩展该方法
钩子是一段自定义代码,它在特定的事件或操作发生时被调用。通过钩子,程序员可以在不修改现有代码的情况下扩展或改变程序的行为。
在 Java 中,最常见的一种钩子是“关闭钩子”(ShutdownHook),用于在 Java 虚拟机(JVM)关闭时执行一些特定的任务。这些任务可能包括资源释放、临时文件删除、数据保存等清理操作。
钩子方法解释
System.exit():Java.lang.System类的一个静态方法,会执行 JVM 关闭钩子或终结器
Runtime.getRuntime().halt():Runtime 类的一个实例方法,不会执行 JVM 关闭钩子或终结器
System.exit() 和 Runtime.getRuntime().halt()
finally
它指的是将对象的状态(属性)和行为(方法)封装在对象内部,外部的代码不能直接访问或修改对象的内部状态,而是通过公开的接口(通常是方法)来与对象进行交互。
封装就是将数据和操作数据的方法封装在一个类中,通过限制对数据的直接访问来保护对象的状态,同时提供合理的接口让外部与对象交互。这种方式不仅保护了对象的内部状态,还提高了代码的可维护性和灵活性。
封装
继承是一种让一个类可以继承另一个类的属性和方法的机制,旨在实现代码的重用和扩展。通过继承,子类可以继承父类已有的功能,也可以添加新的功能或修改已有的功能,从而实现更灵活和高效的代码设计。
继承
方法重写和方法重载 是多态的实现方式,不是多态的实现原理
多态是面向对象编程中的一个重要概念,它允许通过父类类型的引用变量来引用子类对象,并在运行时根据实际对象的类型来确定调用哪个方法。
一个对象可以根据不同的情况表现出多种形态
多态性 就是指用同样的方式(如调用 makeSound() 方法)对待不同的对象(如 Dog 或 Cat),而它们会表现出各自不同的行为。这使得程序更灵活、更易于扩展。
可替换性: 子类对象可以随时替代父类对象,向上转型。
可扩展性:通过添加新的子类,可以扩展系统的功能。
接口统一性:可以通过父类类型的引用访问子类对象的方法,统一对象的接口。
代码灵活性和可维护性:通过多态,可以将代码编写成通用的、松耦合的形式,提高代码的可维护性。
特点
指的是在编译时,Java 编译器只能知道变量的声明类型,而无法确定其实际的对象类型。
而在运行时,Java 虚拟机(JVM)会通过动态绑定来解析实际对象的类型。
这意味着,编译器会推迟方法的绑定(即方法的具体调用)到运行时。正是这种动态绑定机制,使得多态成为可能。
动态绑定
在 Java 中,所有的非私有、非静态和非 final 方法都是被隐式地指定为虚拟方法。
虚拟方法调用是在运行时根据实际对象的类型来确定要调用的方法的机制。
当通过父类类型的引用变量调用被子类重写的方法时,虚拟机会根据实际对象的类型来确定要调用的方法版本,而不是根据引用变量的声明类型
虚拟方法调用
动态绑定和虚拟方法调用
1. 创建父类类型的引用变量,并将其赋值为子类对象。2. 在运行时,通过动态绑定确定引用变量所指向的实际对象的类型。3. 根据实际对象的类型,调用相应的方法版本。
实现原理综述
实现原理
多态
Java 中 List 接口的实现类
相同
1. 底层实现不同:ArrayList 是基于动态数组的数据结构,LinkList 是基于链表的数据结构
2. 随机访问性能不同:ArrayList 优于 LinkedList,因为 ArrayList 可以根据下标以O(1) 时间复杂度对元素进行随机访问。 LinkedList 的访问时间复杂度为 O(n),因为它需要遍历整个链表才能找到指定的元素。
3. 插入和删除性能不同:LinkedList 优于 ArrayList,因为 LinkedList 的插入和删除操作时间复杂度为 O(1),而 ArrayList 的时间复杂度为 O(n)。
区别
ArrayList 和 LinkedList
ArrayList 和 Vector 实现了 List 接口,它们都是动态数组的实现,它们也拥有相同的方法,对元素进行添加、删除、查找等操作
1. 线程安全性:Vector 是线程安全的,而 ArrayList 不是。所以在多线程环境下,应该使用 Vector。
2. 性能:由于 Vector 是线程安全的,所以它的性能通常比 ArrayList 差。在单线程环境下,ArrayList 比 Vector 快。
3. 初始容量增长方式:当容量不足时,ArrayList 默认会增加 50% 的容量,而 Vector 会将容量翻倍。ArrayList 需要更频繁地进行扩容操作,而 Vector 则更适合于存储大量数据
ArraryList 和 Vector
普通类是可以直接实例化的类,而抽象类则不能直接实例化。抽象类通常用于定义一些基本的行为和属性,而具体的实现则由其子类来完成。
1. 实例化:普通类可以直接实例化,而抽象类不能直接实例化。
2. 方法:抽象类中既包含抽象方法又可以包含具体的方法,而普通类只能包含普通方法。
3. 实现:普通类实现接口需要重写接口中的方法,而抽象类可以实现接口方法也可以不实现。
抽象类和普通类
这里面试官其实想问的是:为什么 ConcurrentHashMap 不能插入 null?
1. 在 HashMap 中,key 和 value 值都可以为 null。2. 在 ConcurrentHashMap 中,key 或者是 value 值都不能为 null。
HashMap 和 ConcurrentHashMap的区别
ConcurrentHashMap 添加元素的第一行源码就可以看出,当 key 或 value 为 null 时,会直接抛出空指针异常
在 Java 中,ConcurrentHashMap 是用于并发环境中执行的线程安全的容器,而 HashMap 是用于单线程环境下执行的非线程安全的容器而并发环境下的运行更复杂,如果我们允许 ConcurrentHashMap 的 key 或者是 value 为 null 的情况下,就会存在经典的“二义性问题”。
为什么不能插入null?
代码或表达式存在多种理解或解释,导致程序的含义不明确或模糊。
1. 这个值本身设置的是 null,null 在这里表示的是一种具体的“null”值状态。2. null 还表示“没有”的意思,因为没有设置,所以啥也没有。
什么是二义性问题?
HashMap 的设计是给单线程使用的,而单线程下的二义性问题是能被证明真伪的,所以也就不存在二义性问题了(能被证明的问题就不是二义性问题)。
当我们给 HashMap 的 key 设置为 null 时,我们可以通过 hashMap.containsKey(key) 的方法来区分这个 null 值到底是存入的 null?还是压根不存在的 null?
可证伪的HashMap
因为 ConcurrentHashMap 是设计在多线程下使用的,而多线程下的二义性问题是不能被证明真伪的,所以二义性问题是真实存在的。
不可证伪的ConcurrentHashMap
为什么 HashMap 允许插入 null
为什么 Map 不能插入 null
它们都不能直接实例化,并且它们都是用来定义一些基本的属性和方法的
1. 定义:定义的关键字不同,抽象类是 abstract,而接口是 interface。
2. 方法:抽象类可以包含抽象方法和具体方法,而接口只能包含方法声明(抽象方法)。
3. 方法访问控制符:抽象类无限制,只是抽象类中的抽象方法不能被 private 修饰;而接口有限制,接口默认的是 public 控制符。
4. 实现:一个类只能继承一个抽象类,但可以实现多个接口。
5. 变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量。
6. 构造函数:抽象类可以有构造函数,而接口不能有构造函数。
抽象类和接口
HashMap 和 Hashtable 都实现了 Map 接口,都是 Java 中用于存储键值对的数据结构,它们的底层数据结构都是数组加链表的形式(默认情况下)
1. 线程安全:Hashtable 是线程安全的,而 HashMap 是非线程安全的。
2. 性能:性能:因为 Hashtable 使用了 synchronized 给整个方法添加了锁,所以相比于 HashMap 来说,它的性能不如 HashMap。
3. 存储:HashMap 允许 key 和 value 为 null,而 Hashtable 不允许存储 null 键和 null 值。
因为 Hashtable 是给整个方法添加 synchronized 来实现线程安全的,所以它的性能很差。
Hashtable 不推荐使用
HashMap 和 Hashtable
HashMap 和 HashSet 都是 Java 中的集合类
1. HashSet 实现了 Set 接口,只存储对象;HashMap 实现了 Map 接口,用于存储键值对。2. HashSet 底层是用 HashMap 存储的,HashSet 封装了一系列 HashMap 的方法,HashSet 将(自己的)值保存到 HashMap 的 Key 里面了。3. HashSet 不允许集合中有重复的值(如果有重复的值,会插入失败),而 HashMap 键不能重复,值可以重复(如果键重复会覆盖原来的值)。
HashSet 适用于只存储对象的情况,而 HashMap 适用于需要存储键值对的情况,可以根据键快速查找值。HashSet 底层是用 HashMap 存储的,用它可以存储不重复的值。
小结
HashMap 和 HashSet
它是 HashMap 在进行扩容时的一个阈值,当 HashMap 中的元素个数超过了容量乘以负载因子时,就会进行扩容。
initialCapacity * loadFactor = HashMap 扩容。 其中,initialCapacity 是初始容量,默认值为 16(懒加载机制,只有当第一次 put 的时候才创建)
扩容计算公式
为了减少哈希冲突,提高 HashMap 性能的。
目的
它提供了空间和时间复杂度之间的良好平衡。
为什么默认负载因子是 0.75?
负载因子
通过数组 + 链表实现的
JDK 1.7
1. HashMap 内部维护了一个数组,每个数组元素又是一个链表或者红黑树,每个链表或者红黑树节点存储了一个键值对。2. 当需要存储新的键值对时,HashMap 会根据键的哈希值确定其在数组中的位置,如果该位置已经有了其他键值对,则通过链表或红黑树解决冲突,将新的键值对添加到链表或红黑树的末尾。3. 当链表或红黑树长度达到一定程度后,HashMap 会自动将链表转换为红黑树,以提高查找效率。
通过数组 + 链表或红黑树实现的。
JDK 1.8
1. 链表的数量大于阈值(默认是 8)2. 数组长度大于 64 时
当满足两个条件
1. 创建新的红黑树对象,并将链表内所有的键值对全部添加到红黑树中。2. 将原来的链表引用指向新创建的红黑树。
将链表升级为红黑树进行存储
链表升级为红黑树
当进行了删除操作,导致红黑树的节点小于等于 6 时,会发生退化,将红黑树转换为链表。
1. 从红黑树的根节点开始,按照中序遍历的顺序将所有节点加入到一个新的链表中。2. 将原来的红黑树引用指向新创建的链表
流程
红黑树退化为链表
链表和红黑树互转流程
HashMap 底层实现
HashMap 死循环发生在 JDK 1.8 之前的版本中
1. HashMap 使用头插法进行数据插入(JDK 1.8 之前);2. 多线程同时添加;3. 触发了 HashMap 扩容。
原因
头插法是指新来的值会取代原有的值,插入到链表的头部
头插法会导致 HashMap 在进行扩容时,链表的顺序发生反转
因为在 HashMap 扩容时,会先从旧 HashMap 的头节点读取并插入到新 HashMap 节点中,旧节点的读取顺序是 A -> B -> C,于是插入到新 HashMap 中的顺序就变成了 C -> B -> A,这样就破坏了链表的顺序,导致了链表反转。
什么是头插法?
1. 死循环是因为并发 HashMap 扩容导致的,并发扩容的第一步,线程 T1 和线程 T2 要对 HashMap 进行扩容操作,此时 T1 和 T2 指向的是链表的头结点元素 A,而 T1 和 T2 的下一个节点,也就是 T1.next 和 T2.next 指向的是 B 节点
2. 死循环的第二步操作是,线程 T2 时间片用完进入休眠状态,而线程 T1 开始执行扩容操作,一直到线程 T1 扩容完成后,线程 T2 才被唤醒
3. 当线程 T1 执行完,而线程 T2 恢复执行时,死循环就建立了,因为 T1 执行完扩容之后 B 节点的下一个节点是 A,而 T2 线程指向的首节点是 A,第二个节点是 B,这个顺序刚好和 T1 扩完容完之后的节点顺序是相反的。T1 执行完之后的顺序是 B 到 A,而 T2 的顺序是 A 到 B,这样 A 节点和 B 节点就形成死循环了
死循环产生过程
1. 升级到高版本 JDK(JDK 1.8 以上),高版本 JDK 使用的是尾插法插入新元素的,所以不会产生死循环的问题;2. 使用线程安全容器 ConcurrentHashMap 替代(推荐使用此方案);3. 使用线程安全容器 Hashtable 替代(性能低,不建议使用);4. 使用 synchronized 或 Lock 加锁 HashMap 之后,再进行操作,相当于多线程排队执行(比较麻烦,也不建议使用)
解决方案
HashMap 死循环
哈希冲突是指在哈希表中,两个或多个元素被映射到了同一个位置的情况。
1. 链地址法:将哈希表中的每个桶都设置为一个链表,当发生哈希冲突时,将新的元素插入到链表的末尾。这种方法的优点是简单易懂,适用于元素数量较多的情况。缺点是当链表过长时,查询效率会降低。2. 开放地址法:当发生哈希冲突时,通过一定的探测方法(如线性探测、二次探测、双重哈希等)在哈希表中寻找下一个可用的位置。这种方法的优点是不需要额外的存储空间,适用于元素数量较少的情况。缺点是容易产生聚集现象,即某些桶中的元素过多,而其他桶中的元素很少。3. 再哈希法:当发生哈希冲突时,使用另一个哈希函数计算出一个新的哈希值,然后将元素插入到对应的桶中。这种方法的优点是简单易懂,适用于元素数量较少的情况。缺点是需要额外的哈希函数,且当哈希函数不够随机时,容易产生聚集现象。
1. 存储结构不同:链地址法规定了存储的结构为链表(每个桶为一个链表),每次将值存储到链表的末尾; 开放地址法未规定存储的结构,所以它可以是链表也可以是树结构等。2. 查找方式不同:链地址法查找时,先通过哈希函数计算出哈希值,然后在哈希表中查找对应的链表,再遍历链表查找对应的值。 开放地址法查找时,先通过哈希函数计算出哈希值,然后在哈希表中查找对应的值,如果查找到的值不是要查找的值,就继续查找下一个值,直到查找 到为止。3. 插入方法不同:链地址法插入时,先通过哈希函数计算出哈希值,然后在哈希表中查找对应的链表,再将值插入到链表的末尾。 开放地址法插入时,是通过一定的探测方法,如线性探测、二次探测、双重哈希等,在哈希表中寻找下一个可用的位置。所以链地址法插入方法实现非 常简单,而开放地址法插入方法实现相对复杂。
链地址法 VS 开放地址法
线性探测是发生哈希冲突时,线性探测会在哈希表中寻找下一个可用的位置,具体来说,它会检查哈希表中下一个位置是否为空,如果为空,则将元素插入该位置;如果不为空,则继续检查下一个位置,直到找到一个空闲的位置为止。
线性探测
二次探测是发生哈希冲突时,二次探测会使用一个二次探测序列来寻找下一个可用的位置,具体来说,它会计算出一个二次探测序列,然后依次检查哈希表中的每个位置,直到找到一个空闲的位置为止。二次探测的优点是相对于线性探测来说,它更加均匀地分布元素,缺点是当哈希表的大小改变时,需要重新计算二次探测序列。
当发生哈希冲突时,如果哈希表中的第 k 个位置已经被占用,那么二次探测会依次检查第 k+1^2、第 k-1^2、第 k+2^2、第 k-2^2、第 k+3^2、第 k-3^2……等位置,直到找到一个空闲的位置为止。
二次探测的优点是相对于线性探测来说,它更加均匀地分布元素,但缺点是容易产生二次探测聚集现象,即某些桶中的元素过多,而其他桶中的元素很少
二次探测
线性探测 VS 二次探测
在 Java 中,HashMap 使用的是链地址法解决哈希冲突的,对于存在冲突的 key,HashMap 会把这些 key 组成一个单向链表,之后使用尾插法把这个 key 保存到链表尾部。
HashMap 如何解决哈希冲突?
哈希冲突
在 Java 中,反射是指在运行时检查和操作类、接口、字段、方法等程序结构的能力。通过反射,可以在运行时获取类的信息,创建类的实例,调用类的方法,访问和修改类的字段等。
1.反射得到对象Class<?> clazz = Class.forName(\"User\");2.得到方法Method method = clazz.getDeclaredMethod(\"publicMethod\");3.执行普通方法method.invoke(clazz.getDeclaredConstructor().newInstance());
1. 反射执行公共方法
1.反射得到对象Class<?> clazz = Class.forName(\"User\");2.得到方法Method method = clazz.getDeclaredMethod(\"publicMethod\");得到私有方法Method privateMethod = clazz.getDeclaredMethod(\"privateMethod\");设置私有方法可访问privateMethod.setAccessible(true);执行私有方法privateMethod.invoke(clazz.getDeclaredConstructor().newInstance());
2. 反射执行私有方法
// 1.反射得到对象Class<?> clazz = Class.forName(\"User\");// 2.得到方法Method method = clazz.getDeclaredMethod(\"publicMethod\");// 得到静态方法Method staticMethod = clazz.getDeclaredMethod(\"staticMethod\");// 执行静态方法staticMethod.invoke(clazz);
3. 反射执行静态方法
// 反射得到对象Class<?> clazz = Class.forName(\"User\");// 得到公共属性Field field = clazz.getDeclaredField(\"name\");// 得到属性值String name = (String) field.get( clazz.getDeclaredConstructor().newInstance());// 打印属性值System.out.println(\"name -> \" + name);
4. 反射得到公共属性值
// 反射得到对象Class<?> clazz = Class.forName(\"User\");// 得到私有属性Field privateField = clazz.getDeclaredField(\"age\");// 设置私有属性可访问privateField.setAccessible(true);// 得到属性值int age = (int) privateField.get(clazz.getDeclaredConstructor().newInstance());// 打印属性值System.out.println(\"age -> \" + age);
5. 反射得到私有属性值
先定义一个需要被反射的类对象User
反射实现
1. 编程开发工具的代码提示,如 IDEA 或 Eclipse 等,在写代码时会有代码(属性或方法名)提示,这就是通过反射实现的。2. 很多知名的框架如 Spring,为了让程序更简洁、更优雅,以及功能更丰富,也会使用到反射,比如 Spring 中的依赖注入就是通过反射实现的。3. 数据库连接框架也会使用反射来实现调用不同类型的数据库(驱动)。
使用场景
1. 灵活性:使用反射可以在运行时动态加载类,而不需要在编译时就将类加载到程序中。这对于需要动态扩展程序功能的情况非常有用。2. 可扩展性:使用反射可以使程序更加灵活和可扩展,同时也可以提高程序的可维护性和可测试性。3. 实现更多功能:许多框架都使用反射来实现自动化配置和依赖注入等功能。例如,Spring 框架就使用反射来实现依赖注入。
优点
1. 性能问题:使用反射会带来一定的性能问题,因为反射需要在运行时动态获取类的信息,这比在编译时就获取信息要慢。2. 安全问题:使用反射可以访问和修改类的字段和方法,这可能会导致安全问题。因此,在使用反射时需要格外小心,确保不会对程序的安全性造成影响。
缺点
优缺点分析
反射
反射在程序运行期间动态获取类和操纵类的一种技术。通过反射机制,可以在运行时动态地创建对象、调用方法、访问和修改属性,以及获取类的信息。
1. 动态代理:反射是动态代理的底层实现,即在运行时动态地创建代理对象,并拦截和增强方法调用。这常用于实现 AOP 功能,如日志记录、事务管理等。2. Bean 创建:Spring/Spring Boot 项目中,在项目启动时,创建的 Bean 对象就是通过反射来实现的。3. JDBC 连接:JDBC 中的 DriverManager 类通过反射加载并注册数据库驱动,这是 Java 数据库连接的标准做法。
应用
1. 得到类:Class.forName(\"类名\")2. 得到所有字段:getDeclaredFields()3. 得到所有方法:getDeclaredMethods()4. 得到构造方法:getDeclaredConstructor()5. 得到实例:newInstance()6. 调用方法:invoke()
实现
1. 查找方法:当通过 java.lang.reflect.Method 对象调用 invoke 方法时,Java 虚拟机(JVM)首先确认该方法是否存在并可以访问。这包括检查方法的访问权限、方法签名是否匹配等。2. 安全检查:如果方法是私有的或受保护的,还需要进行访问权限的安全检查。如果当前调用者没有足够的权限访问这个方法,将抛出 IllegalAccessException。3. 参数转换和适配:invoke 方法接受一个对象实例和一组参数,需要将这些参数转换成对应方法签名所需要的类型,并且进行必要的类型检查和装箱拆箱操作。4. 方法调用:对于非私有方法,Java 反射实际上是通过 JNI(Java Native Interface,Java 本地接口)调用到 JVM 内部的 native 方法,例如java.lang.reflect.Method.invoke0()。这个 native 方法负责完成真正的动态方法调用。对于 Java 方法,JVM 会通过方法表、虚方法表(vtable)进行查找和调用;对于非虚方法或者静态方法,JVM 会直接调用相应的方法实现。5. 异常处理:在执行方法的过程中,如果出现任何异常,JVM 会捕获并将异常包装成 InvocationTargetException 抛出,应用程序可以通过这个异常获取到原始异常信息。6. 返回结果:如果方法正常执行完毕,invoke 方法会返回方法的执行结果,或者如果方法返回类型是 void,则不返回任何值。
invoke方法执行流程
底层实现原理
反射底层实现
克隆是指创建一个与原始对象相同的新对象。这个新对象通常具有与原始对象相同的属性和方法,但是它们是两个不同的对象,它们在内存中的位置不同。
什么是克隆
克隆出来的新对象与原始对象共享引用类型的属性。也就是说,新对象中的引用类型属性指向的是原始对象中相同的引用类型属性。如果修改了新对象中的引用类型属性,原始对象中的相应属性也会被修改。
浅克隆
克隆出来的新对象与原始对象不共享引用类型的属性。也就是说,新对象中的引用类型属性指向的是新的对象,而不是原始对象中相同的引用类型属性。如果修改了新对象中的引用类型属性,原始对象中的相应属性不会被修改。
1. 所有引用属性都实现克隆,整个对象就变成了深克隆。2. 使用 JDK 自带的字节流序列化和反序列化对象实现深克隆。3. 使用第三方工具实现深克隆,比如 Apache Commons Lang。4. 使用 JSON 工具,如 GSON、FastJSON、Jackson 序列化和反序列化对象实现深克隆。
深克隆
浅克隆和深克隆
从 Java 8 开始,便引入了一种称为“流式 API”的编程风格,它主要是通过设置方法的返回值,让返回值变为对象自身,从而实现连续的方法调用,这种风格就叫做“链式设置”或“链式调用”。
1. 简洁性:链式设置使得代码更加简洁和易读。通过连续的方法调用,可以在一行代码中完成多个操作,减少了代码的冗余和嵌套。2. 可读性:链式设置可以提供更清晰、更自然的代码流。每个方法调用都可以形成一个语义上的整体,使得代码更易于理解。3. 可组合性:链式设置可以方便地组合多个操作。每个方法返回的是对象自身或包含对象自身的容器,使得可以连续地进行多个操作,从而实现更复杂的功能。4. 可扩展性:链式设置使得添加、修改或移除操作更加灵活。由于每个方法都是在对象自身上操作,并返回对象自身或包含对象自身的容器,可以轻松地添加新的操作或修改现有的操作。
Setter 原生方式Lombok @Accessors 注解方式Lombok @Builder 注解方式Hutool GenericBuilder 方式
链式调用
在于解决多线程环境下并发执行时的内存可见性和一致性问题。
存在原因
在现代计算机系统中,尤其是多处理器架构下,每个处理器都有自己的高速缓存,而主内存(RAM)是所有处理器共享的数据存储区域。当多个线程同时访问和修改同一块共享数据时,如果没有适当的同步机制,就可能导致以下问题
1. 可见性:一个线程对共享变量所做的修改可能不会立即反映到另一个线程的视角中,因为这些修改可能只存在于本地缓存中,并未刷新回主内存。
2. 有序性:编译器和处理器为了优化性能,可能会对指令进行重排序,这可能导致程序在单线程环境中看似按照源代码顺序执行,但在多线程环境中的实际执行顺序却与预期不同。
3. 原子性:即使是最简单的读取或赋值操作,在硬件层面也不一定保证是原子性的,即在没有同步的情况下,多线程下可能看到操作只执行了一部分的结果。
如果没有 Java 内存模型就会出现以下两大问题:1. CPU 和 内存一致性问题。2. 指令重排序问题。
为什么要有?
由于主内存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存(L2)来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主内存中,这样就会导致多个线程在进行操作和同步时,导致 CPU 缓存和主内存数据不一致的问题。
一致性问题
由于有 JIT(Just In Time,即时编译)技术的存在,它可能会对代码进行优化,比如将原本执行顺序为 a -> b -> c 的流程,“优化”成 a -> c -> b 了,但这样优化之后,可能会导致我们的程序在某些场景执行出错,比如单例模式双重效验锁的场景,这就是典型的好心办坏事的事例。
重排序问题
规范了 Java 虚拟机与计算机内存之间是如何协同工作的。
1. 主内存(Main Memory):所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。2. 工作内存(Working Memory):每个线程拥有自己的工作内存,用于存储主内存中的数据的副本,线程只能直接操作工作内存中的数据。3. 内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。4. 原子性(Atomicity):JMM 保证基本数据类型(如 int、long)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。5. 可见性(Visibility):JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。6. 有序性(Ordering):JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等。
规范内容
happens-before(先行发生)原则是 Java 内存模型中定义的用于保证多线程环境下操作执行顺序和可见性的一种重要手段。举个例子来说,例如 A happens-before B,也就是 A 线程早于 B 线程执行,那么 A happens-before B 可以保障以下两项内容:可见性:B 读取到 A 最新修改的值(通过内存屏障)。顺序性:编译器优化、处理器重排序等因素不会影响先执行 A 再执行 B 的顺序。
什么是 happens-before 原则?
Java 内存模型
是一种计算机操作系统中用于提高数据传输效率的优化策略。零拷贝技术通过直接共享数据的内存地址,避免了中间的拷贝过程,从而提高了数据传输的效率。
1. 用户态和内核态 用户态(User Mode) 是指应用程序运行时的执行环境。在用户态下,应用程序只能访问受限资源,如应用程序自身的内存空间、CPU 寄存器等,并且不能直接访问操作系统的底层资源和硬件设备。内核态(Kernel Mode) 是指操作系统内核运行时的执行环境。在内核态下,操作系统具有更高的权限,可以直接访问系统的硬件和底层资源,如 CPU、内存、设备驱动程序等。
2. DMADMA(Direct Memory Access,直接内存访问)技术,绕过 CPU,直接在内存和外设之间进行数据传输。这样可以减少 CPU 的参与,提高数据传输的效率。
传统IO执行流程
零拷贝技术可以利用 Linux 下的 MMap、sendFile 等手段来实现,使得数据能够直接从磁盘映射到内核缓冲区,然后通过 DMA 传输到网卡缓存,整个过程中 CPU 只负责管理和调度,而无需执行实际的数据复制指令。
1. MMap:MMap(Memory Map)是 Linux 操作系统中提供的一种将文件映射到进程地址空间的一种机制,通过 MMap 进程可以像访问内存一样访问文件,而无需显式的复制操作。
2. 在 Linux 操作系统中 sendFile() 是一个系统调用函数,用于高效地将文件数据从内核空间直接传输到网络套接字(Socket)上,从而实现零拷贝技术。这个函数的主要目的是减少 CPU 上下文切换以及内存复制操作,提高文件传输性能。
零拷贝技术的实现
NIO(New I/O)通道:java.nio.channels.FileChannel 提供了 transferTo() 和 transferFrom() 方法,可以直接将数据从一个通道传输到另一个通道,例如从文件通道直接传输到 Socket 通道,整个过程无需将数据复制到用户空间缓冲区,从而实现了零拷贝。Socket Direct Buffer:在 JDK 1.4 及更高版本中,Java NIO 支持使用直接缓冲区(DirectBuffer),这类缓冲区是在系统堆外分配的,可以直接由网卡硬件进行 DMA 操作,减少数据在用户态与内核态之间复制次数 ,提高网络数据发送效率。Apache Kafka 或者 Netty 等高性能框架:这些框架在底层实现上通常会利用 Java NIO 的上述特性来优化数据传输,如 Kafka 生产者和消费者在传输消息时会用到零拷贝技术以提升性能。
哪些地方用到了零拷贝技术?
零拷贝
ThreadLocal 线程本地变量,每个线程都拥有一份该变量的独立副本,即使是在多线程环境下,每个线程也只能修改和访问自己的那份副本,从而避免了线程安全问题,实现了线程间的隔离。
1. 性能问题
存在的问题
ThreadLocal
FastThreadLocal 相比于 ThreadLocal 存在以下两个主要优点:性能更高:FastThreadLocal 使用了数组的方式来存储元素,所以它的查询时间复杂度 O(1) 相比于 ThreadLocal 的哈希表操作效率更高。安全性更高:FastThreadLocal 中的 FastThreadLocalRunnable 在最后执行完之后会自动调用 removeAll() 将集合中所有对象都清理掉,可以避免内存泄漏的问题,所以它的安全性更高。
FastThreadLocal
JAVA 基础
收藏
0 条评论
回复 删除
下一页