首页 > 基础资料 博客日记

Java 基础常见面试题整理

2024-07-15 15:00:06基础资料围观139

文章Java 基础常见面试题整理分享给大家,欢迎收藏Java资料网,专注分享技术知识

目录

1、java的基本数据类型有哪些?

Java的基本数据类型包括以下八种:

  • boolean: 用于存储逻辑值,只能存储true和false。
  • byte: 用于存储字节数据,占用8位(一个字节)内存空间。
  • short: 用于存储较短整数,占用16位(两个字节)内存空间。
  • int: 用于存储整数,占用32位(四个字节)内存空间。
  • long: 用于存储长整数,占用64位(八个字节)内存空间。
  • float: 用于存储单精度浮点数,占用32位(四个字节)内存空间。
  • double: 用于存储双精度浮点数,占用64位(八个字节)内存空间。
  • char: 用于存储单个字符,占用16位(两个字节)内存空间,采用Unicode编码。

2、java为什么要有包装类型?

主要原因包括以下几点:

  • 处理基本数据类型的 null 值:基本数据类型(如 int,double 等)不能直接赋值为 null,而包装类型(如 Integer、Double)可以表示 null 值,这对于某些业务逻辑和数据处理来说非常有用。
  • 提供额外功能:包装类型提供了一些额外的方法和功能,这些方法可以用于对基本数据类型进行操作和处理。例如,Integer 类提供了
    parselnt()方法用于将字符串转换为整数。
  • 泛型支持:泛型只能接受对象类型,无法直接使用基本数据类型。因此,使用包装类型可以很方使地在泛型中使用基本数据类型。
  • 自动装箱与拆箱: Java 提供了自动装箱(autoboxing)和自动拆箱(unboxing)的功能,使得基本类型与其对应的包装类型之间的转换更加便捷。
  • 与对象集合的兼容性: Java的集合类(如ArrayList, HashMap等)只能存储对象,不能直接存储基本数据类型。使用包装类型可以方便地将基本数据类型转换为对象类型,从而与集合类兼容。

3、String a = “123” 和 String a = new String(“123”) 区别?

String a = "123"; // a 是一个变量,对字符串常量池中“123”这个对象的引用
// "123" 是一个字符串字面量(字符串对象),它会被存储在字符串常量池中(String Constant Pool)。
// 当字符串常量池不存在“123”对象会创建一个对象,存在则不会创建  
  
String b = new String("123"); // b 是一个变量,但它引用堆的 String 对象  
// 这里,首先 JVM 会检查字符串常量池中是否已经有 "123" 这个字符串。  
// 如果有,它会用常量池中的字符串对象作为参数去调用 String 类的构造函数来创建一个新的 String 对象。  
// 这个新的 String 对象会被分配在堆内存中,并且它的内容(字符数组)会是常量池中字符串对象的一个拷贝。  
// 变量 b 持有对这个新创建的堆上 String 对象的引用,而不是直接引用常量池中的字符串。
// 最多创建2个对象,最少一个

4、String、StringBuilder和StringBuffer的区别?

String、StringBuilder和StringBuffer是Java中用来处理字符串的类,它们之间的区别主要在于性能和线程安全性。

  • String: 一旦创建就不能被修改,任何对String的操作都会产生一个新的String对象。
    不可变性使得String在并发环境下是线程安全的,但是频繁的字符串操作会产生大量临时对象,影响性能。

  • StringBuilder: StringBuilder是可变的,可以进行插入、追加、删除等操作而不会产生新的对象。
    由于StringBuilder是非线程安全的,所以在单线程环境下比StringBuffer具有更好的性能。

  • StringBuffer: 与StringBuilder类似,也是可变的,但是它是线程安全的,所有的方法都是同步的。
    在多线程环境下,为了确保线程安全性,可以使用StringBuffer,但性能相对较差,因为是通过在方法上面加synchronized 关键字来保证同步的。

因此,如果在单线程环境下需要频繁修改字符串,建议使用StringBuilder;如果在多线程环境下需要频繁修改字符串,则应该使用StringBuffer;如果字符串不需要被修改,那么使用String即可。

5、如何理解面向对象和面向过程?

面向过程是一种以过程为中心的编程方法,它将问题分解为一系列步骤函数。每个函数负责完成一个特定的任务,通过依次调用这些函数来解决问题。

其优点包括:

  • 流程清晰:按照步骤进行编程,易于理解和调试。

然而,它也存在一些局限性:

  • 代码复用性差:功能封装在函数中,但难以复用和扩展。
  • 维护困难:代码结构较为松散,修改可能影响多个部分。

面向对象则是一种以对象为中心的编程方法。它将问题抽象为对象,每个对象具有属性和行为。
其优点包括:

  • 代码复用性高:通过继承和多态实现代码的重用和扩展。
  • 维护方便:修改一个对象的行为不会影响其他部分。
  • 模拟现实世界:更符合人类的思维方式。

面向对象编程的关键概念包括:

  • 对象:表示现实世界中的实体。
  • 类:对象的抽象描述。
  • 封装:隐藏对象的内部实现,只暴露必要的接口。
  • 继承:实现代码的重用和扩展。
  • 多态:不同对象对同一消息的不同响应。

总之,面向对象编程更加注重代码的封装、复用和可扩展性,使得代码更易于维护和扩展。而面向过程编程则更适合一些简单、流程性强的问题。在实际编程中,可以根据具体情况选择合适的编程方法。

6、面向对象的三大基本特征是什么?如何理解?

面向对象的三大基本特征是封装、继承和多态

封装是将对象的属性和行为封装在一起,对外只提供必要的接口。它的意义在于:

  • 信息隐藏:隐藏内部实现细节,提高安全性和可靠性。
  • 模块独立性:减少模块之间的耦合,便于模块的独立开发和测试。

继承允许子类继承父类的属性和方法,从而实现代码的重用和扩展。理解继承可以从以下几个方面考虑:

  • 代码复用:避免重复编写相同的代码。
  • 扩展功能:子类可以在父类的基础上添加新的功能。

多态是指同一个方法在不同的对象上有不同的实现。它的好处包括:

  • 灵活性:根据具体对象的类型执行相应的操作。
  • 可扩展性:方便添加新的子类并实现不同的行为。

7、java是面向对象还是面向过程?

Java 是一种面向对象的编程语言。

8、什么是反射?为什么需要反射?

反射是指在运行时动态地访问和操作类的信息。

需要反射的原因包括:

  • 灵活性:可以在运行时获取类的信息、创建对象、调用方法等,提供了更大的灵活性。
  • 动态加载:支持在运行时动态加载和使用类,无需在编译时确定。
  • 框架和工具开发:便于开发通用的框架和工具,可适应不同的业务需求。
  • 插件机制:支持插件式的扩展,方便添加新的功能。
  • XML 配置:与配置文件结合,实现基于配置的动态功能。
  • 类操作:对类进行各种操作,如修改属性、方法等。
  • 跨模块交互:方便不同模块之间的交互和集成。

通过反射,程序可以在运行时动态地了解和操作类的结构和行为,从而实现更灵活和可扩展的系统设计。

9、为什么不能用浮点数表示金额? 有什么方法解决?

浮点数在计算机内部的表示方法采用的是 IEEE 标准,它兼顾了数据的精度和大小。32 位的浮点数由 1 比特的符号位、8 比特的阶码和 23 比特的尾数组成。浮点数能表示的数据大小范围由阶码决定,而能够表示的精度完全取决于尾数的长度。对于金额,舍去不能表示的部分,就会产生精度丢失
十进制的 0.1 在二进制下将是一个无线循环小数,同样,在进行加法、减法、乘法和除法等运算时,这种精度损失可能会累积,导致结果不正确。为了避免这种情况,建议使用 java.math.BigDecimal 类来表示和计算金额。BigDecimal 提供了用于高精度算术运算的方法,能够精确地表示十进制小数,避免浮点数表示和计算中的精度损失问题。

10、为什么不能用字符串来存储金额?

使用字符串来存储金额有一些局限性:

  • 计算困难:进行数值计算时,需要额外的处理来转换和解析字符串。
  • 效率较低:在涉及大量金额操作时,性能可能受到影响。
  • 不支持数学运算:无法直接进行加减乘除等常见的数学运算。
  • 易出错:处理字符串转换可能引入错误。
  • 数据类型不一致:与其他数值类型的交互可能需要额外的转换。

然而,在某些情况下,可能会选择使用字符串来存储金额:

  • 格式灵活:可以方便地表示特定的金额格式。
  • 非数值场景:如仅用于显示或存储。

为了更准确和高效地处理金额,通常使用专门的数值类型或类,例如 Java 中的BigDecimal,它提供了高精度的数值计算功能,能够避免常见的数值计算问题。

11、为什么需要克隆?如何实现对象的克隆?深拷贝和浅拷贝的区别?

需要克隆的原因有以下几点:

  • 独立性:创建独立的副本,避免对原始对象的修改影响到其他使用该对象的部分。
  • 隔离性:在不同的上下文或操作中使用相同对象的副本,以防止冲突。
  • 安全性:确保原始对象的完整性和稳定性。

实现对象的克隆有多种方式,以下是一种常见的方法:

  • 实现 Cloneable 接口:确保类实现了 Cloneable 接口。
  • 重写 clone 方法:在类中重写 clone 方法。

深拷贝和浅拷贝的区别在于:

深拷贝:复制对象及其引用的所有嵌套对象。

  • 副本与原始对象完全独立。
  • 修改副本不会影响原始对象。

浅拷贝:只复制对象本身,不复制引用的嵌套对象。

  • 嵌套对象仍与原始对象共享。
  • 修改副本的嵌套对象会影响原始对象。

深拷贝确保了副本的完全独立性,而浅拷贝在处理嵌套对象时可能会出现问题。在需要完全独立的副本时,应使用深拷贝。

12、try-catch-finally中,如果catch中return了,finally还会执行吗?

在 try-catch-finally 语句中,即使在 catch 块中执行了 return 语句,finally 块仍然会执行。

finally 块的作用是确保无论在 try 块中是否发生异常,某些特定的操作都会被执行,例如资源的释放、清理等。

当执行到 catch 块中的 return 语句时,会先将返回值保存起来,然后执行 finally 块中的代码。最后,再返回保存的返回值。

finally 块的执行具有以下特点:

  • 无论是否发生异常,finally 块都会执行。
  • 即使在 finally 块中抛出异常,也会继续执行。
  • finally 块中的返回值不会影响 try 或 catch 块中的返回值。

使用 finally 块可以确保资源的正确释放,以避免资源泄漏等问题。

13、String为什么设计成不可变的?

String 被设计成不可变有以下几个原因:

  1. 安全性和稳定性:避免在多个线程中同时修改字符串时可能出现的竞态条件。
  2. 效率:许多操作可以直接利用字符串的不可变性进行优化。
  3. 缓存友好:相同字符串可以共享,减少内存占用。
  4. 线程安全:无需进行额外的同步操作。
  5. 代码简洁:无需处理可变字符串带来的复杂情况。
  6. 避免误操作:防止意外修改字符串。
  7. 易于实现和理解:简化字符串的内部实现。

不可变的特性使得 String 在多线程环境中更加可靠,并且提高了性能和内存效率。

14、Error和Exception的区别和联系?以及常见的RuntimeException?

区别:

  • Error: Error是指Java虚拟机无法解决的严重问题,通常由系统内部错误引起,程序无法通过捕获错误来恢复。一般来说,程序不应该捕获Error类型的异常,而应该在发生Error时让程序终止。常见的Error包括OutOfMemoryError(内存耗尽)和StackOverflowError(栈溢出)等。
  • Exception: Exception是指程序运行时可能发生的问题,它可以通过捕获和处理来使程序继续执行。Exception又分为受检异常(Checked Exception)和非受检异常(Unchecked Exception)。受检异常是指在程序编译时需要处理的异常,而非受检异常是指在编译时不需要处理的异常。常见的Exception包括IOException(输入输出异常)和SQLException(数据库访问异常)等。

联系:

Error和Exception都继承自Throwable类,因此它们具有一些共同的特性,如堆栈跟踪和异常信息等。

常见的RuntimeException:

  • NullPointerException(空指针异常): 当试图在一个空对象上调用方法或访问属性时抛出。
  • ArrayIndexOutOfBoundsException(数组下标越界异常): 当试图访问数组中不存在的索引时抛出。
  • IllegalArgumentException(非法参数异常): 当传递给方法的参数不符合方法的要求时抛出。
  • ArithmeticException(算术异常): 当出现除以零的运算时抛出。
  • ClassCastException(类转换异常): 当试图将一个对象转换为它不是的类型时抛出。

15、抽象类和接口的区别是什么?

抽象类和接口是面向对象编程中两种不同的概念,它们在Java中有着明显的区别。

抽象类(Abstract Class):

  • 抽象类可以包含抽象方法和非抽象方法。抽象方法是没有实际实现的方法,需要子类去实现。非抽象方法有默认实现,子类可以选择性地重写这些方法。
  • 一个类只能继承一个抽象类(单继承性)。
  • 抽象类可以包含成员变量,可以有构造函数,可以拥有普通方法。
  • 抽象类可以有访问控制修饰符,可以定义成员变量,可以包含构造方法。

接口(Interface):

  • 接口中所有的方法都是抽象方法,没有方法体。
  • 一个类可以实现多个接口(多继承性)。
  • 接口中的成员变量隐式地是static和final的。
  • 接口不能包含构造函数。
  • 接口中的方法默认是public的,可以省略访问控制符,不能使用其他访问修饰符。

16、==和equals的区别

在 Java 中,== 和 equals() 方法的区别主要包括以下几点:

  • ==:它是一种基本的数据比较操作符,用于比较两个对象的引用(内存地址)是否相等。
  • equals():它是 Object 类中的方法,通常用于定义对象之间的逻辑相等性。

Object默认是判断地址是否相等, Long、Integer、Date、String等都重写了equals方法。

Object类

public boolean equals(Object obj) {
        return (this == obj);
    }

Long类

public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
    }

17、super和this的区别是什么?

super用于访问父类的成员,this用于引用当前对象本身的成员

18、Java中的集合类有哪些?说说他们的特点?

List(列表)
特点:有序、可重复

  • ArrayList:基于数组实现的动态数组,可以根据需要自动增长容量。它提供了快速的随机访问和在末尾添加/删除元素的性能。适合需要频繁随机访问元素,以及需要在末尾进行添加/删除操作的场景。
  • LinkedList:基于双向链表实现的列表,可以高效地在任意位置进行添加/删除操作。它提供了快速的插入和删除性能。
  • Vector:与ArrayList类似,但它是线程安全的,因此性能通常较低。现在较少使用,更多地被ArrayList和CopyOnWriteArrayList替代。

Set(集合)
特点:无序、不可重复

  • HashSet:基于哈希表实现的集合,不允许重复元素。它提供了快速的添加、删除和查找性能。
  • TreeSet:基于红黑树实现的有序集合,可以按照自然顺序或自定义顺序对元素进行排序。它提供了快速的查找和有序遍历性能。
  • LinkedHashSet:保持元素插入顺序的HashSet,基于LinkedHashMap实现。

Queue(队列)
特点:先进先出

  • LinkedList:除了作为列表使用,LinkedList还可以用作队列,实现FIFO(先进先出)的数据结构。
  • PriorityQueue:基于优先级堆的无界队列,元素按照其自然顺序或者创建PriorityQueue时所提供的Comparator进行排序。
  • ArrayDeque:一个由数组结构提供的双端队列,可以作为栈使用。

Map(映射)
特点:键值对存储、无序

  • HashMap:基于哈希表实现的键值对映射,不允许重复键。它提供了快速的添加、删除和查找性能。
  • TreeMap:基于红黑树实现的键值对映射,按键的自然顺序或自定义顺序对键值对进行排序。
  • ConcurrentHashMap:基于分段锁实现的并发哈希表,支持高并发的读取和部分并发的写入操作。
  • Hashtable:与HashMap类似,但它是线程安全的,并且不允许键或值为null。现在较少使用,更多地被ConcurrentHashMap替代。

19、集合的排序方式的实现方案?

List自带的 sort 方法、集合工具类 Collections 下面的sort方法、stream 流中的 sorted方法

区别:

List.sort:
List.sort 是 List 接口的默认方法,可以直接在列表对象上调用。它也是一个对原始列表进行排序的原地排序方法,可以使用自定义的 Comparator 进行排序。

Collections.sort:
Collections.sort 是对实现了 List 接口的集合进行排序的静态方法。它实际上会调用列表对象的 sort 方法来完成排序操作。

list.stream().sorted:
list.stream().sorted 是使用流的排序方法,它会产生一个新的经过排序的流,可以在后续收集为一个列表或执行其他操作。
如果使用 sorted(null),则会使用默认的自然排序进行排序。

性能对比:

在大多数情况下,原地排序的方法(Collections.sort 和 List.sort)的性能会优于使用流进行排序(list.stream().sorted)。
原地排序方法直接在原始列表上进行操作,不需要额外的内存分配,因此速度更快。
流排序方法(list.stream().sorted)通常会引入额外的内存和计算,因此在速度和性能上可能会略逊于原地排序。

20、ArrayList、LinkedList与Vector的区别?

1. 数据结构:

  • ArrayList 基于数组实现。
  • LinkedList 基于双向链表实现。
  • Vector 也基于数组实现。

2. 线程安全性:

  • ArrayList是非线程安全的。

  • LinkedList是非线程安全的。

  • Vector是线程安全的。

3. 性能:

  • 随机访问时,ArrayList性能较好。

  • 频繁插入和删除时,LinkedList性能较好。

  • Vector在某些情况下性能可能相对较低。

4. 扩容机制:

  • ArrayList和Vector都有扩容机制,ArrayList是1.5倍扩容,Vector是两倍扩容。
  • LinkedList是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。

21、HashMap、Hashtable和ConcurrentHashMap的区别?

1. 线程安全性:

  • HashMap 是非线程安全的。
  • Hashtable 是线程安全的,但效率较低。
  • ConcurrentHashMap 是线程安全的,支持高并发环境。

2. 性能:

  • 一般情况下,HashMap 的性能较好,Hashtable 由于线程安全的实现,性能相对较差,ConcurrentHashMap
    在保证线程安全的同时,性能也比较优秀。

3. null 值:

  • HashMap 允许键和值为 null。

  • Hashtable 不允许键和值为 null。

  • ConcurrentHashMap 允许键为 null,但不允许值为 null。

22、HashMap的初始化,put流程,get流程,扩容流程说明

推荐参考:ArrayList 和 HashMap 源码解析

初始化:

  • HashMap 在创建时可以指定初始容量和负载因子。
  • 如果没有指定,默认初始容量为 16,负载因子为 0.75。

put 流程:

  • 计算键的哈希值。

  • 根据哈希值确定存储位置(通过取模运算)。

  • 如果该位置没有元素,直接插入。

  • 如果该位置已有元素,判断是否与要插入的键相等,若相等则更新值;否则形成链表结构。

get 流程:

  • 计算键的哈希值。

  • 根据哈希值找到对应的存储位置。

  • 在该位置遍历链表或直接返回对应的值。

扩容流程:

  • 当元素数量超过当前容量与负载因子的乘积时,进行扩容。
  • 扩容时,创建一个新的容量为原来两倍的数组,并将原数组中的元素重新哈希并迁移到新数组中。

23、HashMap、HashSet、ArrayList是线程安全的吗?

HashMap、HashSet和ArrayList都不是线程安全的。

24、创建线程的几种方式?

继承Thread类、实现Runnable接口、使用线程池

25、线程同步的方式有哪些方式?

synchronized关键字、加锁、volatile、原子类

26、synchronized如何使用?加在普通方法上和加在静态方法的区别?

当 synchronized 关键字加在普通方法上时,它会锁定对象实例;而加在静态方法上时,它锁定的是类的Class对象。让我通过一个简单的Java类来说明这两种情况。

public class SynchronizedExample {
    // 用于演示锁定对象实例的普通方法
    public synchronized void synchronizedMethod() {
        // 同步的操作
    }

    // 用于演示锁定类的Class对象的静态方法
    public static synchronized void synchronizedStaticMethod() {
        // 同步的操作
    }
}

现在我们创建两个线程来演示这两种情况:

public class Main {
    public static void main(String[] args) {
        final SynchronizedExample example = new SynchronizedExample();

        // 在普通方法上加锁的示例
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                example.synchronizedMethod();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                example.synchronizedMethod();
            }
        });

        // 在静态方法上加锁的示例
        Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedExample.synchronizedStaticMethod();
            }
        });

        Thread thread4 = new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedExample.synchronizedStaticMethod();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

在这个例子中,thread1和thread2演示了加在普通方法上的 synchronized 关键字,它们都是针对同一个SynchronizedExample对象实例的。
thread3和thread4演示了加在静态方法上的 synchronized 关键字,它们是针对SynchronizedExample类的Class对象的。
在静态方法上使用 synchronized 关键字时,该关键字锁定的是类的 Class 对象,而不是类的实例对象。这种机制保证了无论类的实例有多少个,同一时刻只能有一个线程执行该类的静态 synchronized 方法,从而确保了对静态方法的同步访问。

springMVC中的service是单例的,因此在service的实现类impl中加 synchronized 关键字,也能使controller层的请求排队。

27、java级别的锁都有哪些?你怎么分类?

推荐参考:java锁介绍

28、什么情况下需要对对象进行序列化?

对象序列化是一个将对象状态转换为字节流的过程,主要用于实现对象的完全保存或网络传输。当需要满足以下场景时,通常需要对对象进行序列化:

  • 网络传输:在分布式系统中,对象经常需要在不同的进程或机器之间进行传输。为了在网络中有效地传输对象,需要将对象序列化为字节流,以便在接收端进行反序列化并恢复为原始对象。
  • 数据备份和恢复:当需要将对象状态保存在计算机中以备将来使用时,序列化是一种有效的手段。通过序列化,可以方便地将对象保存到文件或数据库中,以便在需要时进行恢复。
  • 持久化存储:序列化是实现对象持久化存储的一种常见方法。通过将对象序列化为字节流并保存到文件中,可以确保对象状态的长期保存,即使程序关闭或重启后也能恢复对象状态。

29、什么是AIO、BIO和NIO?

AIO、BIO和NIO都是IO模型,它们在处理输入输出操作时具有不同的特点和适用场景。

BIO(Blocking I/O):

  • BIO是同步阻塞I/O模型。在BIO方式中,一个连接对应一个线程。当客户端发起连接请求后,服务端就需要启动一个线程进行处理。如果这个连接不做任何事情就会造成不必要的线程开销。这种方式的缺点在于,如果有大量客户端并发请求,就需要创建大量的线程来处理,这将导致系统资源的大量消耗,严重情况下甚至会导致系统崩溃。BIO方式适用于连接数量少且固定的场景。

NIO(Non-blocking I/O):

  • NIO是同步非阻塞IO模型。它引入了通道(Channel)和缓冲区(Buffer)的概念。数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。这种模型允许一个线程要求一个通道将数据读入一个缓冲区,在数据从通道读入缓冲区的期间,这个线程可以进行其他操作,直到数据完成从通道读入到缓冲区时,线程再继续处理它。数据从缓冲区写入通道时也是如此。此外,NIO还引入了选择器的概念,使得一个线程可以监听多个通道的数据传输情况。NIO适用于连接数目多且业务比较轻的场景,如聊天服务器。

AIO(Asynchronous I/O):

  • AIO是异步非阻塞IO模型。在JDK1.7之后,Java提供了异步的相关通道实例。AIO的最大特点是具备异步功能,需要借助操作系统,当底层操作系统具有异步IO模型时,AIO可以在对应的read/write/accept/connection等方法上异步执行,完成后会主动调用回调函数,实现一个CompletionHandler对象。AIO消除了用户态和内核态的切换耗时,使多任务的发展更加容易。此外,它的缓冲机制使得不同的文件读写有更少的线程切换和上下文引起的性能损失。AIO适用于连接数目多且连接比较长(业务重操作)的场景,需要操作系统充分参与并发操作。

30、synchronized的锁升级过程是怎样的?

推荐参考:Synchronized和ReenTrantLock锁的区别

31、synchronized的锁优化是怎样的?

推荐参考:Synchronized和ReenTrantLock锁的区别

32、synchronized和reentrantLock区别?

推荐参考:Synchronized和ReenTrantLock锁的区别

33、volatile能保证原子性吗?为什么?

不可以,volatile 关键字能够保证变量的可见性,但是不能保证原子性。

在没有使用 volatile 关键字的情况下,当一个线程修改了变量的值,这个修改之后的值会先被保存在线程的工作内存中,并不会立即刷新到主内存中。这是由于 CPU 和内存优化的特性所决定的。

数据最终会从线程的工作内存刷新到主内存中,但具体刷新的时间并没有明确定义。这取决于底层的架构、CPU 的具体行为,以及 JVM 和编译器应用的各种优化。

34、JUC并发包常用的工具类,分别有什么特性,适用场景?

JUC(java.util.concurrent)是Java中的一个重要包,它提供了一系列用于解决并发问题的工具类。主要包括atomic(原子类)、locks(锁)和一些并发类(如线程安全类和线程池相关:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、Executors、CompletableFuture等)
以下是JUC并发包中常用的一些工具类及其特性和适用场景:

ReentrantLock:

  • 特性:是一个可重入的互斥锁,支持公平锁和非公平锁。通过它,可以灵活地控制多个线程对共享资源的访问。
  • 适用场景:适用于需要实现复杂同步控制的场景,比如需要手动控制锁的获取和释放,或者需要实现更细粒度的锁控制。

Semaphore:

  • 特性:俗称信号量,用于控制同时访问某个特定资源的线程数量。通过它,可以实现流量控制,防止过多的线程同时访问某个资源。
  • 适用场景:适用于需要对资源进行访问控制的场景,比如数据库连接池、线程池等,确保资源不会被过度消耗。

CountDownLatch:

  • 特性:一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。当计数器的值减至0时,等待的线程会被唤醒。
  • 适用场景:适用于需要等待一组线程完成某项操作后才能继续执行的场景,比如启动多个线程进行并行计算,然后等待所有线程计算完成后进行汇总。

CyclicBarrier:

  • 特性:一个同步辅助类,它允许一组线程互相等待,直到所有线程都到达某个公共屏障点(common barrier point)。
  • 适用场景:适用于需要将一组线程划分为几个阶段执行,并且每个阶段都需要所有线程都完成后才能进入下一个阶段的场景。

Exchanger:

  • 特性:用于两个线程之间交换数据。当一个线程进入交换时,它会等待另一个线程也进入交换,然后这两个线程交换数据。
  • 适用场景:适用于两个线程需要互相交换数据的场景,比如生产者消费者模型中,生产者和消费者需要交换数据。

ConcurrentHashMap:

  • 特性:一个线程安全的HashMap实现,它支持高并发读写操作。通过分段锁技术,实现了高效的并发性能。
  • 适用场景:适用于需要存储大量键值对,并且需要支持高并发读写的场景,比如缓存系统、分布式系统等。

35、ConcurrentHashMap在哪些地方做了并发控制,保证线程安全的?

推荐参考:深入理解ConcurrentHashMap源码解析

36、SimpleDateFormat线程安全性?

SimpleDateFormat 类在 Java 中是非线程安全的。这意味着如果多个线程同时共享同一个 SimpleDateFormat 实例,并尝试使用它进行日期格式化或解析,可能会遇到不可预测的结果和并发问题。

SimpleDateFormat 的非线程安全性主要源于其内部状态(如日期字段、数字等)在格式化或解析过程中可能会被修改。如果多个线程同时访问这些内部状态,就可能导致数据竞争和不一致的行为。

为了解决这个问题,有几种常见的做法:

  • 每个线程使用自己的 SimpleDateFormat 实例:这是最简单且最直接的方法。通过为每个线程分配一个单独的
    SimpleDateFormat 实例,可以避免线程间的数据竞争。然而,这可能会增加内存消耗,特别是在高并发场景下。
  • 使用同步块或锁:通过在访问 SimpleDateFormat 实例时添加同步块或锁,可以确保每次只有一个线程能够修改其内部状态。这可以减少内存消耗,但可能会降低性能,因为线程需要等待锁的释放。
  • 使用线程安全的替代方案:Java 8 引入了新的日期和时间 API(如 java.time.format.DateTimeFormatter),这些 API 是线程安全的。如果可能的话,考虑使用这些新的 API
    来替代 SimpleDateFormat。
  • 使用第三方库:有些第三方库提供了线程安全的日期格式化功能。这些库通常经过优化,可以在高并发环境下提供更好的性能。

37、AQS和CAS分别是什么,如何理解?

推荐参考:深入理解Java AQS:从原理到源码分析

AQS是一个Java提供的底层同步工具类,用于构建锁和同步器的框架性组件。它是Java并发包中ReentrantLock、Semaphore、ReentrantReadWriteLock等同步器的基础。AQS的主要特点包括支持独占模式和共享模式,并为这些同步器提供了一个统一的基础框架,让开发人员可以基于此进行扩展和定制化。通过使用AQS,开发人员可以避免自己重复实现同步器的底层机制,从而更加专注于业务的实现。此外,AQS还提供了高效的并发性能,适用于实现计数器、累加器、分布式数据同步、并发队列、内存管理以及自旋等待机制等各种场景。

CAS是一种无锁技术,涉及三个操作数——内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置V的值等于预期原值A时,将内存位置V的值设置为新值B。否则,处理失败,什么都不做。一般情况下是一个自旋操作,即不断地重试。CAS操作具有原子性,它在多线程环境下可以保证对一个变量的操作过程中不会被其他线程干扰。CAS操作常用于实现高性能的并发队列、内存管理和自旋等待机制等。

38、 ConcurrentHashMap为什么1.8取消了使用ReentrantLock锁?

推荐参考:深入理解ConcurrentHashMap源码解析

ConcurrentHashMap在Java 1.8中取消了使用ReentrantLock锁,主要基于以下几个原因:

  • 减少内存开销:使用ReentrantLock需要节点继承AQS(AbstractQueuedSynchronizer)来获得同步支持,这增加了内存开销。相比之下,Java 1.8中的ConcurrentHashMap采用了更轻量级的同步机制,如CAS(Compare and Swap)和synchronized,减少了节点的内存占用。
  • 优化并发性能:在Java 1.8中,ConcurrentHashMap的存储结构改为了Node数组+链表/红黑树。当冲突链表达到一定长度时,链表会转换成红黑树,以进一步优化查询性能。同时,ConcurrentHashMap对每个Node节点进行加锁,这种细粒度的锁控制可以提高并发度,减少线程间的争用。
  • 简化和统一并发机制:通过采用CAS和synchronized等轻量级同步机制,Java 1.8中的ConcurrentHashMap简化了并发控制逻辑,使得代码更加简洁易懂。同时,这也使得Java的并发机制更加统一和一致。

39、HashMap是如何解决Hash冲突的?解决hash冲突都有哪些方案?

HashMap 解决哈希冲突的主要方法是通过链地址法(Separate Chaining)。当发生哈希冲突时,即不同的键具有相同的哈希值,HashMap 会在哈希表的每个桶(bucket)中维护一个链表(或者在链表长度较长的情况下,可以转换为红黑树)来存储具有相同哈希值的键值对。

40、HashMap和ArrayLsit区别,以及他们的key是否可重复/为空?

41、平常用过线程池吗?你该注意哪些问题?

应该注意核心线程数、队列、拒绝策略、最大线程数、超时时间这些参数的设置。

推荐参考:Java之线程池

42、线程池的等待队列有哪几种实现方式?

在Java的java.util.concurrent包中,线程池框架提供了几种不同的队列实现方式,以适应不同的应用场景和需求。以下是线程池等待队列的一些常见实现方式:

ArrayBlockingQueue:

  • ArrayBlockingQueue是一个基于数组的有界阻塞队列。它按照FIFO(先进先出)的原则对元素进行排序。当试图向一个已满的队列添加元素时,添加操作会被阻塞;当试图从一个空的队列中移除元素时,移除操作也会被阻塞。
  • 适用于已知大概任务量的情况,因为可以预先设定队列容量。

LinkedBlockingQueue:

  • LinkedBlockingQueue是一个基于链表的无界(或指定容量的)阻塞队列。与ArrayBlockingQueue相比,它的容量可以动态增长,但也可以指定一个最大容量。
  • 当队列为空时,获取元素的线程会被阻塞;当队列已满时,尝试添加元素的线程也会被阻塞(如果队列设置了最大容量)。
  • 适用于任务量可能会动态变化的情况,因为队列可以动态扩展。

PriorityBlockingQueue:

  • PriorityBlockingQueue是一个支持优先级的无界阻塞队列。元素按照它们的自然顺序或者通过提供的Comparator进行排序。
  • 适用于任务之间有优先级区分的情况,可以确保优先级高的任务优先得到执行。

SynchronousQueue:

  • SynchronousQueue是一个不存储元素的阻塞队列。每一个插入操作必须等待一个相应的删除操作,反之亦然。它支持公平和非公平两种模式。
  • 适用于高并发且任务处理速度非常快的情况,因为任务几乎是在提交后立即得到处理,不需要在队列中等待。

DelayQueue:

  • DelayQueue是一个支持延时获取元素的无界阻塞队列。队列中的元素必须实现Delayed接口,这允许元素在队列中等待特定的时间才能被取出。
  • 适用于需要定时或延时执行任务的情况,例如定时任务调度。

43、java的四种引用,强弱软虚的区别?

推荐参考:Java 中的四种引用类型和它们的使用场景

44、为什么说CAS是乐观锁?底层对应那个类?

CAS(Compare-And-Swap)之所以被称为乐观锁,是因为它在数据更新时持有一种乐观的态度,认为在数据被更新的过程中不会有其他线程来修改它。这与悲观锁(如传统的数据库锁)形成对比,悲观锁总是假设最坏的情况,即在数据被处理时总是会有其他线程来修改它,因此需要在整个数据处理过程中锁定数据。

CAS是一种无锁机制,它通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果内存中的值与期望值相等,则执行交换操作;否则,不执行。这种机制避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。

在CAS的底层实现中,Unsafe类起到了核心作用。Unsafe是CAS的核心类,它提供了直接访问底层操作系统的功能,使得CAS能够在底层进行比较和交换操作。通过Unsafe类中的compareAndSwap方法,CAS能够实现无锁的数据结构,保证并发安全。

请注意,CAS并不是严格意义上的锁,而是通过原子性来保证数据的同步。它不会保证线程同步,而是确保在数据更新期间的一致性。此外,CAS也存在一些潜在的问题,如ABA问题(即在CAS操作期间,一个值可能被其他线程多次修改后又改回原来的值,导致CAS操作无法正确感知数据的实际变动)。

综上所述,CAS之所以被称为乐观锁,是因为它在处理并发数据时持有一种乐观的态度,并通过底层的Unsafe类实现无锁操作,从而提高了程序的并发性能。

45、ThreadLocal有使用过吗?底层原理是什么?使用它该注意什么?有什么问题?

是的,我使用过ThreadLocal。ThreadLocal是Java中一个非常有用的类,它提供了线程本地变量。这些变量不同于它们的正常变量,因为每一个访问这个变量的线程都有其自己独立初始化的变量副本。

底层原理:

  • ThreadLocal的底层实现主要依赖于ThreadLocalMap,这是一个特殊的Map,其键为ThreadLocal对象,值为线程变量的副本。每个Thread都持有一个ThreadLocalMap的引用,这个map被用来存储该线程的本地变量。当线程首次访问一个ThreadLocal变量时,通过ThreadLocal的set或get方法在ThreadLocalMap中为其创建一个新的条目。

使用注意事项:

  • 内存泄漏:由于ThreadLocal的生命周期和线程的生命周期不同,如果不注意及时清理ThreadLocal变量,可能会导致内存泄漏。因此,在不再需要使用ThreadLocal时,应调用remove()方法将其从当前线程中清除,避免线程结束后仍然持有对该变量的引用。
  • 共享变量问题:尽管ThreadLocal为每个线程提供了独立的变量副本,但它并不能解决线程间共享变量的同步问题。如果多个线程共享同一个ThreadLocal变量,需要自行处理线程间的同步操作,确保线程安全。
  • 内部使用慎重:在一些特定的情况下,如使用线程池或者异步任务执行框架,使用ThreadLocal需要格外小心。因为线程池中的线程可能会被复用,如果不正确地处理ThreadLocal变量,可能会导致数据混乱。

存在的问题:

  • 性能问题:虽然ThreadLocal可以提高并发性能,但因为它需要在每个线程中存储变量副本,所以会增加内存消耗。同时,对ThreadLocalMap的访问也需要一定的时间,这可能会影响性能。
  • 使用不当导致的错误:如果在使用ThreadLocal时没有正确地处理变量的初始化和清理,可能会导致数据混乱或内存泄漏等问题。

46、JMM内存模型是什么?为什么需要JMM内存模型?

推荐参考:深入理解Java内存模型(JMM)

47、如何设计一个高并发系统?

设计一个高并发系统是一个复杂且需要多方面考虑的任务。以下是一些关键步骤和考虑因素,帮助你设计一个能够处理高并发请求的系统:

1. 需求分析:

  • 明确系统的业务场景和目标。
  • 分析预期的并发量、请求频率和数据量。
  • 识别可能的瓶颈和性能敏感点。

2. 架构设计:

  • 选择合适的架构模式,如微服务、分布式系统等。
  • 设计无状态的服务,以便水平扩展。
  • 使用负载均衡器来分发请求到多个服务实例。

3. 数据库设计:

  • 选择适合高并发的数据库技术,如NoSQL数据库或分布式关系数据库。
  • 设计合理的索引和查询优化策略。
  • 考虑使用读写分离读写、分库分表等技术来分散数据库压力。

4. 缓存策略:

  • 使用缓存来减少数据库访问次数,如Redis或Memcached。
  • 设计合理的缓存失效和更新策略。

5. 异步处理:

  • 对于非实时性要求较高的操作,使用消息队列(如Kafka、RabbitMQ)进行异步处理。
  • 解耦核心逻辑和耗时操作,提高系统响应速度。

6. 并发控制:

  • 使用锁、信号量等机制控制并发访问共享资源。
  • 考虑使用分布式锁来确保跨服务的并发安全。

7. 限流与降级:

  • 设计限流策略,防止系统被过量请求压垮。
  • 实现降级机制,在部分服务不可用时保证整体系统的稳定性。

8. 监控与告警:

  • 部署监控系统,实时收集系统性能数据。

  • 设置告警阈值,及时发现并处理潜在问题。

9. 代码优化:

  • 使用高效的数据结构和算法。
  • 避免不必要的资源占用和内存泄漏。
  • 使用连接池等技术来减少资源创建和销毁的开销。

10. 水平扩展:

  • 设计系统时考虑无状态服务,以便通过增加服务实例来水平扩展系统处理能力。
  • 使用自动化部署和容器化技术(如Docker和Kubernetes)来简化扩展过程。

11. 安全性考虑:

  • 确保系统具备足够的安全措施,如身份验证、授权、数据加密等。
  • 对抗DDoS攻击、SQL注入等常见安全威胁。

12.测试与调优:

  • 进行压力测试和性能测试,确保系统在高并发场景下能够稳定运行。
  • 根据测试结果对系统进行调优,优化性能瓶颈。

在设计高并发系统时,还需要注意以下几点:

  • 模块化与解耦:将系统拆分为多个独立的模块,降低模块间的耦合度,提高系统的可维护性和可扩展性。
  • 文档化:编写详细的系统设计文档和接口文档,方便团队成员理解和维护系统。
  • 团队协作与沟通:建立高效的团队协作机制,确保团队成员之间的信息畅通和及时沟通。

48、aio 和 nio 它是跟什么相关的?是 Java 做的还是系统底层做的

NIO,即New I/O,是Java 1.4中引入的java.nio包,它提供了一套新的I/O抽象,如Channel、Selector和Buffer等,用于构建多路复用的、同步非阻塞的I/O程序。NIO是基于块的,它以块为基本单位处理数据,性能上比基于流的方式要好一些。同时,NIO提供了更接近操作系统底层高性能的数据操作方式。

而AIO,即Asynchronous I/O,是Java 1.7之后引入的包,作为NIO的升级版本,它提供了异步非阻塞的I/O操作方式。异步IO是基于事件和回调机制实现的,应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

虽然这些I/O模型在Java中实现,但它们背后的原理和概念是与操作系统底层I/O操作紧密相关的。Java的I/O库是对操作系统提供的I/O功能的封装,使得Java程序员可以在不直接操作底层系统调用的情况下,使用更高级别的抽象进行I/O操作。因此,虽然这些I/O模型是在Java中实现的,但它们的应用和效果是与操作系统底层I/O操作紧密相关的。

49、io流有哪些

推荐参考:【IO流】JAVA基础篇(一)

50、为什么我们常用的是NIO而不是AIO?

因为我们一般是部署到linux系统上的,linux系统对nio的支持好一些

51、java里面线程分为哪两类

  • 用户线程(User Thread):这是最常见的线程类型,由用户程序创建和管理。用户线程通过start()方法启动,并可以异步执行特定的任务。在Java中,用户线程默认是非守护线程,即它们会一直运行,直到完成自己的任务或者因为某种原因(如异常)而终止。
  • 守护线程(Daemon Thread):守护线程主要用于在后台为其他线程(特别是用户线程)提供服务。与用户线程不同,守护线程在Java虚拟机(JVM)退出时会自动结束,不会阻止JVM的关闭。典型的守护线程包括垃圾回收线程。要将一个线程设置为守护线程,可以在调用start()方法之前调用线程的setDaemon(true)方法。

以下是一些守护线程在Java中的常见应用场景:

  • 垃圾回收:Java中的垃圾回收器就是一个守护线程。它会在程序运行过程中自动回收不再使用的内存,从而释放资源并减轻程序员的负担。当没有其他线程在运行时,垃圾回收器会自动执行垃圾回收操作。
  • 自动保存:在一些需要定时保存数据的场景中,可以使用守护线程来定时执行自动保存操作。例如,定时将缓存数据持久化到数据库,以防止意外情况导致数据丢失。这种机制可以在不影响主线程运行的情况下,确保数据的安全性和一致性。
  • 定时任务:守护线程也适用于执行定时任务,比如定时发送心跳包、定时更新缓存等。这些任务不需要实时响应,而是可以按照预定的时间间隔在后台执行。
  • 服务监控:守护线程还可以用于监控程序运行状态,比如监控CPU、内存的使用情况,以及网络连接是否正常等。这有助于及时发现潜在问题并进行处理,确保程序的稳定运行。

52、java内存模型 三大特性是什么

Java内存模型(Java Memory Model,JMM)是一种规范,定义了Java程序中各种变量的访问方式、存储方式和内存可见性等行为,以确保不同线程对共享变量的操作能够正确、可靠地进行。JMM关注的是Java程序中的内存访问规则和多线程并发操作的语义。

Java 内存模型(Java Memory Model,JMM)主要包括以下三大特性:

原子性(Atomicity):原子性指的是一个操作是不可中断的。即使在多线程的环境下,一个操作一旦开始,就一定会执行完毕,不会被其他线程的操作中断。在 Java 中,可以通过synchronized关键字或者使用java.util.concurrent.atomic包下的原子类来实现原子性。

可见性(Visibility):可见性指的是当一个线程修改了共享变量的值,其他线程能立即看到修改后的值。在多核处理器的系统中,每个线程在运行时都有自己的工作内存,而共享变量存储在主内存中。可见性确保了线程对共享变量的修改对其他线程是可见的。在 Java 中,可以通过volatile关键字或者通过synchronized和Lock来实现可见性。

有序性(Ordering):有序性指的是程序执行的顺序按照代码的先后顺序执行,即保证线程内串行语义的一致性。Java 内存模型通过 happens-before 原则来保证多线程执行时对共享变量的操作是有序的。具体来说,如果一个操作 A happens-before 另一个操作 B,那么操作 A 的结果对于操作 B 是可见的,并且操作 A 发生在操作 B 之前。

这三大特性构成了 Java 内存模型的核心,它们保证了在多线程环境下共享变量的正确性和一致性。

53、java内存模型和jvm区别是什么

Java内存模型是关于多线程并发编程的规范,而JVM(Java Virtual Machine)是Java程序的运行环境,它负责将Java字节码翻译成特定平台的机器码并执行。JVM包括了内存管理、垃圾回收、线程调度等方面的功能

54、线程状态有哪些,如何让线程中断

在Java中,线程状态主要有以下几种:

  1. 新建状态(NEW):当用new关键字创建一个线程对象后,该线程对象就处于新建状态。此时它已经有了相应的内存空间和其它资源,但是还没有开始执行。
  2. 就绪状态(RUNNABLE):当线程对象调用了start()方法后,该线程就进入就绪状态。此时,线程已经准备好运行,但是否真正执行取决于操作系统的调度。
  3. 运行状态(RUNNING):当线程获得CPU资源并执行任务时,它就处于运行状态。在任意时刻,只有一个线程处于运行状态,其他线程则在等待或竞争资源。
  4. 阻塞状态(BLOCKED):线程因为某种原因(如等待锁)放弃CPU使用权,暂时停止运行。当线程处于阻塞状态时,它不会占用任何CPU资源。
  5. 等待状态(WAITING):线程通过调用对象的wait()方法进入等待状态。此时,线程需要等待其他线程做出一些特定动作(如通知)。
  6. 超时等待状态(TIMED_WAITING):线程通过调用对象的sleep(long time)、wait(long time)或线程的join(long time)方法,或者是等待某个已触发的Thread.sleep、Object.wait、Thread.join等结构的超时时间到达,进入超时等待状态。
  7. 终止状态(TERMINATED):当线程因为run()方法执行完毕或者因为异常而退出,线程就处于终止状态。

要让线程中断,Java提供了几种方法:

  • 使用interrupt()方法:调用线程的interrupt()方法并不会直接停止线程,而是会在线程中设置一个中断状态。线程需要定期检查这个中断状态(通常通过
  • List item

Thread.currentThread().isInterrupted()或Thread.interrupted()方法),并根据需要做出响应。如果线程在sleep()、wait()或join()等阻塞状态下被中断,会抛出InterruptedException异常。
2. 使用stop()方法:虽然Java提供了stop()方法,但这个方法已经被废弃,不建议使用。因为它会立即停止线程,这可能导致线程中的数据不一致,或者线程未完成的清理工作得不到完成(如文件、数据库等的关闭)。
3. 使用退出标志:另一种方法是使用退出标志。在线程的run()方法中,你可以定期检查一个共享变量(即退出标志),如果该变量表示线程应该退出,则线程可以通过正常的方式(如完成当前循环或任务)来结束执行。

55、线程安全问题的本质是什么?JMM解决了什么问题

本质上来说线程安全问题就是多线程不能同时满足:可见性、原子性、有序性。而JMM提供了这些问题的解决方案

56、怎么知道synchronized锁在哪一个状态

使用Java监控和管理工具:

  • JConsoleVisualVM 是Java提供的两个图形化监控工具,它们可以显示线程的状态,包括哪些线程正在等待锁。这可以帮助你推断出哪些 synchronized 块或方法当前被锁定。
  • JStackjcmd 线程 print 命令可以生成线程堆栈跟踪。通过分析这些堆栈跟踪,你可以找到哪些线程正在执行synchronized` 块或方法。

57、synchronized 方法块、普通方法、静态方法 分别锁的对象是什么

58、核心线人数和那个队列长度一般怎么样去配置?

计算公式:qps * tp99 + 系统抖动
一个普通的做法是进行压力测试,模拟不同的负载场景,然后根据应用程序的实际表现来调整参数。
实际上线后需要监控2-3天

如何具体配置取决于你的应用程序和硬件特性。通常,你需要基于以下几个因素来决定:

任务的性质:CPU密集型任务(计算密集)、IO密集型任务(等待I/O操作)或者两者的混合。
服务器的硬件资源:如处理器核心数、内存大小等。
系统的预期负载:即预计的并发任务数和任务产生的速率。
性能指标:比如,最大响应时间、吞吐量等。

在实际操作过程中,你可能需要对线程池的配置进行调整和优化,以达到最佳的性能。一个普通的做法是进行压力测试,模拟不同的负载场景,然后根据应用程序的实际表现来调整参数。
使用Java中的ThreadPoolExecutor时,你可以通过监视任务的等待时间、执行时间、队列长度以及线程池大小等指标来帮助决策。在测试期间,这些指标会指示现有配置的效能,并且可以用来判断是否需要调整核心线程数或队列长度。

59、多线程如何实现任务编排?

可以通过 CompletableFuture 实现

60、reentrantlock 如何实现公平和非公平锁的

具体实现都是在 reentrantlock 的NonfairSync 和 fairSync方法里面。重写了 aqs 的 tryAcquire 方法。

ReentrantLock 是 Java 中的一个可重入锁,它提供了公平和非公平两种锁的实现方式。理解这两种实现方式的关键在于理解它们如何决定哪个线程应该获得锁。

  • 非公平锁

在非公平锁的实现中,当锁被释放时,任何正在等待获取锁的线程都有机会立即获取锁,而不考虑它们等待锁的顺序。这种实现方式可能导致线程“插队”,即等待时间较长的线程可能会被等待时间较短的线程抢先获取锁。

非公平锁的实现通常具有更高的吞吐量,因为它减少了线程之间的切换和上下文切换的开销。但是,它可能导致某些线程长时间得不到执行,即出现饥饿现象。

  • 公平锁

在公平锁的实现中,线程按照它们请求锁的顺序来获取锁。也就是说,等待时间最长的线程会优先获取锁。这种实现方式保证了等待的线程不会因为其他线程的插队而长时间得不到执行。

公平锁的实现通常具有更好的可预测性,因为它遵循了一种先到先得的原则。但是,由于它需要在等待队列中维护线程的顺序,并可能需要更复杂的调度机制,因此其性能可能略低于非公平锁。

ReentrantLock 通过内部的同步队列(Sync Queue)来实现这两种锁的语义。当线程请求锁时,它会被添加到同步队列中。对于非公平锁,当一个线程释放锁时,它会直接尝试获取锁,而不是查看同步队列中是否有其他线程正在等待。而对于公平锁,当一个线程释放锁时,它会检查同步队列的头部是否有等待的线程,如果有,则将该线程从队列中移除并允许它获取锁。

61、链路追踪中的父子线程传递怎么实现(threadlocal)

当涉及到父子线程传递时,ThreadLocal默认情况下并不会自动将父线程的变量传递给子线程。这是因为ThreadLocal的设计初衷就是为了存储线程局部变量,这些变量的生命周期与线程的生命周期相同。

然而,你可以通过一些方法来模拟父子线程之间的ThreadLocal变量传递。以下是一些可能的方法:

1. 显式传递

在创建子线程时,你可以显式地从父线程的ThreadLocal中获取值,并将其作为参数传递给子线程。然后,子线程可以在其自己的ThreadLocal中设置这个值。

public class MyRunnable implements Runnable {  
    private final MyContext context;  
  
    public MyRunnable(MyContext context) {  
        this.context = context;  
    }  
  
    @Override  
    public void run() {  
        try (ThreadLocal<MyContext> threadLocal = new ThreadLocal<>()) {  
            threadLocal.set(context);  
            // ... 执行任务 ...  
        }  
    }  
}  
  
// 在父线程中  
MyContext parentContext = ...; // 从ThreadLocal中获取  
MyRunnable runnable = new MyRunnable(parentContext);  
new Thread(runnable).start();

2. 使用InheritableThreadLocal:

Java提供了一个InheritableThreadLocal类,它是ThreadLocal的一个子类。与ThreadLocal不同,InheritableThreadLocal允许父线程将其值传递给子线程。这是通过Java线程模型的inheritableThreadLocals字段实现的,该字段在创建新线程时会被复制到子线程中。

public class MyContextHolder {  
    private static final ThreadLocal<MyContext> contextHolder = new InheritableThreadLocal<>();  
  
    public static void setContext(MyContext context) {  
        contextHolder.set(context);  
    }  
  
    public static MyContext getContext() {  
        return contextHolder.get();  
    }  
  
    public static void clearContext() {  
        contextHolder.remove();  
    }  
}  
  
// 在父线程中  
MyContext parentContext = ...; // 创建或获取  
MyContextHolder.setContext(parentContext);  
  
// 创建并启动子线程  
new Thread(() -> {  
    MyContext childContext = MyContextHolder.getContext(); // 获取父线程的context  
    // ... 执行任务 ...  
}).start();

需要注意的是,虽然InheritableThreadLocal提供了父子线程之间的值传递,但它也可能导致一些意想不到的问题,尤其是在复杂的线程模型(如线程池)中。因此,在使用它时要格外小心,确保你了解其工作原理和潜在的影响。

3.使用上下文传递工具

除了上述方法外,你还可以考虑使用一些上下文传递工具或框架,如OpenTracing、Sleuth等。这些工具通常提供了更强大和灵活的上下文传递机制,可以更容易地集成到你的应用中。

62、线程死锁的条件是什么,如何解决

线程死锁是多线程编程中常见的问题,它指的是两个或更多个线程因为争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法向前推进。以下是线程死锁通常需要满足的四个条件:

  • 互斥条件:一个资源只能被一个线程使用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源,在未完成之前不释放。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

为了解决线程死锁问题,可以采取以下几种方法:

  • 避免嵌套锁:在使用多个锁的时候,避免使用嵌套锁,因为嵌套锁会增加死锁的风险。
  • 避免循环等待:在使用多个锁的时候,避免循环等待。如果出现循环等待的情况,可以采取破坏循环等待的方式,例如通过按照固定的顺序获取锁来避免死锁。
  • 设置超时时间:在使用锁的时候,设置超时时间。如果在超时时间内没有获取到锁,放弃锁并进行其他的处理。
  • 使用非阻塞算法:非阻塞算法会在没有锁的情况下执行操作,如果发现有其他线程正在使用资源,它会尝试重新执行操作,从而避免了死锁的风险。
  • 破坏死锁条件:可以通过破坏上述四个死锁条件中的任何一个来避免死锁。例如,通过让线程按照特定的顺序获取资源,可以破坏循环等待条件。

63、为什么从永久代变成元空间

永久代在Java 8及之前的版本中,是JVM中用于存储类信息的内存区域。然而,永久代存在一些问题:

  • 大小调整困难:永久代的大小在JVM启动时就需要指定,并且后续很难动态调整。这导致在应用程序运行时,如果加载的类过多,可能会引发OutOfMemoryError(OOM)错误。
  • 垃圾回收效率问题:永久代使用Java堆的垃圾回收器进行垃圾回收,但由于其存储的是类信息,垃圾回收的效率可能不如其他类型的内存区域。

相比之下,元空间在Java 8及之后的版本中引入,用于替代永久代。元空间在本地内存中分配,具有以下几个优势:

  • 动态扩展性:元空间的大小只受本地内存限制,可以根据应用程序的需要自动调整大小,从而减少了OOM的风险。
  • 垃圾回收优化:元空间使用专门的垃圾回收器进行垃圾回收,这种回收器针对类元数据的特点进行了优化,提高了垃圾回收的效率。
  • 更好的性能:由于元空间在本地内存中分配,与Java堆分开,因此可以减少堆内存的压力,提高应用程序的性能。

此外,StringTable(字符串常量池)的存放位置也从永久代转移到了堆中。这是因为永久代的回收频率相对较低,只有在Full GC时才会被回收。如果大量字符串被创建并放置在永久代中,可能会因为永久代空间不足而导致性能问题。将StringTable放到堆中,可以及时回收不再使用的字符串对象,从而避免空间不足的问题。

64、sleep()和wait()的区别

1.所属类与调用方式:

  • sleep()是Thread类的一个静态方法,它可以直接通过当前线程对象调用,如Thread.sleep(1000);。
  • wait()是Object类的一个实例方法,因此它必须通过某个对象来调用,通常是同步代码块或方法中的this或某个对象的引用。

2.使用场景与语法:

  • sleep()方法用于让当前线程暂停执行指定的时间,不依赖于同步块或方法。它可以在任何线程中调用,用于等待特定的时间间隔。
  • wait()方法通常用于多线程之间的通信,它必须在synchronized代码块或方法内部调用,以确保线程在调用wait()时已经获得了某个对象的锁。

3.唤醒方式:

  • sleep()方法到时间后会自动唤醒,不需要其他线程进行干预。
  • wait()方法则依赖于其他线程调用同一对象上的notify()或notifyAll()方法。此外,wait()方法也可以接收一个超时参数,如果在此时间内没有调用notify()或notifyAll(),线程将自动唤醒。

4.锁机制:

  • sleep()方法不会释放当前线程所持有的锁(如果有的话)。
  • wait()方法在调用时会释放当前线程所持有的锁,使得其他线程可以获取该锁并进入synchronized代码块或方法。当线程被唤醒并重新获取锁后,它才能继续执行。

5.异常处理:

  • 两者都会抛出InterruptedException,当线程在等待、休眠或占用某个通道(阻塞模式)时,如果其他线程中断了当前线程,就会抛出此异常。在捕获此异常后,通常应该检查线程的中断状态,并决定是继续等待还是立即退出循环。

6.用途:

  • sleep()通常用于简单的线程休眠,如等待某个周期性任务。
  • wait()和notify()/notifyAll()通常用于多线程之间的通信和同步,以实现更复杂的并发逻辑。

文章来源:https://blog.csdn.net/weixin_44183847/article/details/134363509
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云