2024年经典Java面试题汇总及答案解析

1、八种基本数据类型的大小,以及他们的封装

基本类型大小(字节)默认值封装类
整型byte1(byte)0Byte
short2(short)0Short
int40Integer
long80LLong
浮点型float40.0fFloat
double80.0dDouble
布尔型boolean-falseBoolean
字符型char2\u000(null)Character

注:

        1、int是基本数据类型, Integer 是 int 的封装类,是引用类型。 int 默认值是 0 ,而 Integer 默认值 是null ,所以 Integer 能区分出 0 和 null 的情况。一旦 java 看到 null ,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。

        2、基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间, 必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另 一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。

        3、虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有 任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java 虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素 boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字 节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里是指的 是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。

2、标识符和命名规则

在Java中,标识符是用来标识变量、方法、类、包等命名元素的名称。Java的标识符命名规则如下:

        1.标识符可以由字母、数字、下划线和美元符号组成。

        2.标识符必须以字母、下划线或美元符号开头,不能以数字开头。

        3.标识符区分大小写,例如"myVariable"和"myvariable"是不同的标识符。

        4.标识符不能是Java的关键字,例如"public"、"class"等。

        5.标识符应具有描述性,以便于理解和维护代码。

以下是一些符合Java标识符命名规则的示例:

        1.合法的标识符:myVariable, _count, $price, MAX_VALUE

        2.非法的标识符:2ndNumber, public, class

3、重载和重写的区别

        重载(Overloading)是指在同一个作用域内,可以有多个同名函数,但它们的参数列表不同。通过参数的类型、个数或顺序的不同,编译器可以区分它们,并根据调用时的参数类型来选择合适的函数进行调用。重载可以提高代码的可读性和灵活性。

        重写(Override)是指子类重新定义了父类中已有的方法。子类通过继承父类的方法,并在子类中重新实现该方法,以满足子类自身的需求。重写可以实现多态性,即通过父类引用指向子类对象时,调用的是子类中重写的方法。

区别:

  1. 定义:重载是指在同一个类中定义多个同名但参数列表不同的函数,以便根据不同的参数类型或个数来调用不同的函数。重写是指子类重新定义父类中已有的方法,以实现自己的功能需求。

  2. 发生的位置:重载发生在同一个类中,而重写发生在子类中。

  3. 参数列表:重载函数的参数列表必须不同,可以是参数类型不同、参数个数不同或者参数顺序不同。重写函数的参数列表必须与父类中被重写的方法完全相同。

  4. 返回类型:重载函数的返回类型可以相同也可以不同。重写函数的返回类型必须与父类中被重写的方法相同或者是其子类。

  5. 功能实现:重载函数通过参数列表的不同来区分,可以实现不同的功能。重写函数是为了改变父类方法的行为,实现自己特定的功能。

  6. 调用方式:重载函数根据参数类型或个数的不同来决定调用哪个函数。重写函数在多态的情况下,根据对象的实际类型来决定调用哪个方法。

4、equals与==的区别

        == 操作符用于比较两个对象的引用是否相等。也就是说,它比较的是对象在内存中的地址。如果两个对象的引用指向同一个内存地址,那么它们被认为是相等的;否则,它们被认为是不相等的。

        equals 方法用于比较两个对象的内容是否相等。默认情况下,equals 方法与 == 操作符的行为相同,即比较对象的引用。但是,可以通过重写 equals 方法来改变比较的方式。在重写 equals 方法时,通常会比较对象的属性值是否相等,而不仅仅是比较引用。

        需要注意的是,对于基本数据类型(如int、char等),== 操作符比较的是它们的值是否相等。而对于引用类型(如String、自定义类等),== 操作符比较的是它们的引用是否相等。

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

5、Hashcode的作用

         Hashcode是Java中的一个方法,它用于计算对象的哈希码。哈希码是一个整数值,用于快速确定对象在哈希表中的位置。Hashcode的作用主要有以下几个方面:

        1. 在集合中查找和存储:哈希码可以用于快速查找和存储对象。在使用HashSet、HashMap等集合类时,通过计算对象的哈希码可以确定对象在集合中的位置,从而提高查找和存储的效率。

        2. 对象相等性判断:哈希码也可以用于判断两个对象是否相等。在Java中,如果两个对象的哈希码相等,不一定表示它们相等,但如果两个对象不相等,它们的哈希码一定不相等。因此,在重写equals方法时,通常也需要重写hashCode方法,以保证对象相等时它们的哈希码也相等。

        3. 分布式系统中的数据分片:在分布式系统中,数据通常会被分散存储在不同的节点上。通过计算对象的哈希码,可以将数据均匀地分布到不同的节点上,从而实现负载均衡和高效的数据访问。

        4. 安全性校验:哈希码也可以用于安全性校验。例如,在密码存储时,通常会将密码的哈希码存储在数据库中,而不是明文密码。当用户输入密码时,系统会计算输入密码的哈希码,并与数据库中存储的哈希码进行比较,以验证密码的正确性。

6、StringString StringBuffer  StringBuilder 的区别是什?

        1. String是不可变的,而StringBuffer和StringBuilder是可变的。这意味着在对String进行操作时,每次都会创建一个新的String对象,而对StringBuffer和StringBuilder进行操作时,可以在原有对象上进行修改,避免了频繁创建对象的开销。

        2. String是线程安全的,而StringBuffer是线程安全的,而StringBuilder是非线程安全的。这是因为String的不可变性使得它可以被多个线程共享而不会出现问题,而StringBuffer和StringBuilder的可变性使得它们需要考虑线程安全的问题。

        3. String拼接字符串时效率较低,而StringBuffer和StringBuilder拼接字符串时效率较高。由于String的不可变性,每次拼接字符串都会创建一个新的String对象,导致内存开销较大。而StringBuffer和StringBuilder通过修改原有对象来拼接字符串,避免了创建新对象的开销。

        综上所述,如果需要频繁对字符串进行修改操作,并且在多线程环境下使用,应该使用StringBuffer;如果在单线程环境下使用,并且对性能要求较高,可以使用StringBuilder;如果不需要修改字符串,并且需要保证线程安全,可以使用String。

7、ArrayList和LinkedList的区别

        ArrayList和LinkedList是中两种常见的集合,它们都实现了List接口,但在内部实现和性能方面有一些区别。

       注 :List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承Collection

1. 内部实现:
        ArrayList:基于数组实现的动态数组,它可以自动扩容和缩容。当元素数量超过当前容量时,ArrayList会创建一个更大的数组,并将原数组中的元素复制到新数组中。
        LinkedList:基于双向链表实现的,每个节点都包含了当前元素的值以及指向前一个节点和后一个节点的引用。

2. 访问效率:
        ArrayList:访问效率较高,因为它可以通过索引直接访问元素,时间复杂度为O(1)。但在插入和删除元素时,需要移动其他元素,时间复杂度为O(n)。
        LinkedList:访问效率较低,因为它需要从头节点开始遍历链表,直到找到目标位置。但在插入和删除元素时,只需要修改节点的引用,时间复杂度为O(1)。

3. 内存占用:
        ArrayList:在内存中连续存储元素,因此占用的内存空间相对较小。
        LinkedList:在内存中不连续存储元素,每个节点都需要额外的空间存储前后节点的引用,因此占用的内存空间相对较大。

4. 适用场景:
        如果需要频繁地进行随机访问操作,例如根据索引获取元素或者修改元素的值,建议使用ArrayList
        如果需要频繁地进行插入和删除操作,例如在集合的开头或结尾插入或删除元素,建议使用LinkedList

        注:当然,这些对比都是指数据量很大或者操作很频繁。

8、HashMapHashTable的区别

 HashMap和HashTable都是用于存储键值对的数据结构,但它们之间有一些重要的区别:

1. 线程安全性:

        HashTable是线程安全的,而HashMap不是HashTable的方法都是同步的,可以在多线程环境下使用,但这也导致了性能上的一些损失。而HashMap在多线程环境下需要额外的同步措施来保证线程安全。

2. 允许空键值:

        HashMap允许键和值都为null,而HashTable不允许。如果在HashMap中插入null键或null值,它们会被正常处理。但在HashTable中,如果尝试插入null键或null值,会抛出NullPointerException。

3. 迭代器:

        HashMap的迭代器是fail-fast的,即在迭代过程中如果其他线程修改了HashMap的结构,会抛出ConcurrentModificationException异常。而HashTable的迭代器不是fail-fast的,不会抛出异常。

4. 初始容量和扩容机制:

        HashMap的初始容量和扩容机制更加灵活。可以通过构造函数指定初始容量,并且在容量不足时会自动扩容。而HashTable的初始容量固定为11,并且在容量不足时会自动扩容为原来的两倍加一。

5. 继承关系:

        HashMap继承自AbstractMap类,而HashTable继承自Dictionary类。

9、List,Set,Map三者的区别?

        List、Set和Map是Java集合框架中的三个常用接口,它们分别用于存储和操作不同类型的数据。

1. List:
           - List是一个有序的集合,可以包含重复的元素。
           - List允许通过索引访问元素,可以根据元素的位置进行增删改查操作。
           - 常见的List实现类有ArrayList和LinkedList。

2. Set:
           - Set是一个不允许包含重复元素的集合。
           - Set不保证元素的顺序,即不按照插入顺序或者排序顺序进行存储。
           - Set提供了判断元素是否存在的方法,可以用于去重。
           - 常见的Set实现类有HashSet和TreeSet。

3. Map:
           - Map是一种键值对的映射表,每个键对应一个值。
           - Map中的键是唯一的,但值可以重复。
           - Map提供了根据键获取值的方法,也可以根据键删除或更新对应的值。
           - 常见的Map实现类有HashMap和TreeMap。

总结:
        - List适用于需要按照顺序存储和访问元素的场景,允许重复元素。
        - Set适用于需要去重的场景,不保证元素的顺序。
        - Map适用于需要根据键值对进行存储和访问的场景,键是唯一的。

10、Collection包结构,与Collections的区别

        Collection是集合类的上级接口,子接口有 Set 、 List 、 LinkedList 、 ArrayList 、 Vector 、 Stack 、

Set ;

        Collections 是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种 集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java 的 Collection框架。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

11、Java的四种引用,强弱软虚

Java中有四种引用类型,它们分别是强引用软引用弱引用虚引用

        1. 强引用(Strong Reference):是最常见的引用类型,如果一个对象具有强引用,那么垃圾回收器绝不会回收它。即使内存不足时,JVM也会抛出OutOfMemoryError而不是回收这些对象。

        2. 软引用(Soft Reference):是一种相对强引用弱化一些的引用类型。如果一个对象只有软引用,那么在内存不足时,垃圾回收器可能会回收它。软引用通常用于实现内存敏感的缓存。

        3. 弱引用(Weak Reference):是比软引用更弱化的引用类型。如果一个对象只有弱引用,那么在下一次垃圾回收时,无论内存是否充足,都会被回收。弱引用通常用于实现一些特定的功能,如监视对象是否已被回收。

        4. 虚引用(Phantom Reference):是最弱化的引用类型。虚引用主要用于跟踪对象被垃圾回收的状态,无法通过虚引用访问对象的任何属性或方法。虚引用必须与引用队列(ReferenceQueue)一起使用。

        

12、泛型常用特点

        泛型是一种编程语言的特性,它允许我们编写可以适用于多种数据类型的代码。以下是泛型的常用特点:

        1. 参数化类型:泛型允许我们在定义类、接口或方法时使用参数来表示类型,这样可以在使用时指定具体的类型。例如,我们可以定义一个泛型类`List<T>`,其中的`T`可以代表任意类型。

        2. 类型安全:泛型在编译时进行类型检查,可以确保代码在运行时不会出现类型错误。这样可以减少运行时错误的可能性,并提高代码的可靠性。

        3. 代码复用:通过使用泛型,我们可以编写通用的代码,可以在不同的数据类型上进行操作,而不需要为每种数据类型编写重复的代码。这样可以提高代码的复用性和可维护性。

        4. 高效性:泛型在编译时进行类型擦除,即将泛型类型转换为其原始类型,这样可以减少运行时的开销。同时,泛型还可以提供更好的性能,因为它避免了装箱和拆箱操作。

13、Java创建对象有几种方式?

  1. new创建新对象
  2. 通过反射机制
  3. 采用clone机制
  4. 通过序列化机制

14、深拷贝和浅拷贝的区别是什么?

        深拷贝浅拷贝是在编程中用于复制对象的两种不同方式。

        浅拷贝:是指创建一个新对象,将原始对象的成员变量的值复制到新对象中。但是,如果原始对象的成员变量是引用类型,那么浅拷贝只会复制引用,而不会创建新的引用对象。这意味着原始对象和浅拷贝对象将共享相同的引用对象。因此,对其中一个对象进行修改可能会影响到另一个对象。

        深拷贝:则是创建一个新对象,并将原始对象的所有成员变量的值复制到新对象中,包括引用类型的成员变量。这意味着深拷贝会创建新的引用对象,而不是共享原始对象的引用。因此,对其中一个对象进行修改不会影响到另一个对象。

总结一下:
        - 浅拷贝只复制引用,共享相同的引用对象。
        - 深拷贝复制所有成员变量的值,包括引用类型,创建新的引用对象。

17、final有哪些用法?

        final关键字在Java中有以下几种用法:

        1. final修饰类:当一个类被final修饰时,表示该类不能被继承,即该类是最终类,不能有子类。

        2. final修饰方法:当一个方法被final修饰时,表示该方法不能被子类重写,即该方法是最终方法,子类不能对其进行修改。

        3. final修饰变量:当一个变量被final修饰时,表示该变量是一个常量,一旦被赋值后就不能再改变。

        4. final修饰引用类型变量:当一个引用类型变量被final修饰时,表示该变量的引用地址不能改变,但是可以修改引用对象的属性。

        5. final修饰参数:当一个参数被final修饰时,表示该参数在方法内部不能被修改。

18、static都有哪些用法?

        static关键字在Java中有以下几种用法:

        1. 静态变量:使用static修饰的变量称为静态变量,也叫类变量。静态变量属于类,而不是属于类的实例。静态变量在类加载时被初始化,并且只有一份拷贝,所有实例共享该变量的值。可以通过类名直接访问静态变量。

        2. 静态方法:使用static修饰的方法称为静态方法,也叫类方法。静态方法属于类,而不是属于类的实例。静态方法可以直接通过类名调用,无需创建类的实例。静态方法只能访问静态成员(包括静态变量和静态方法),不能访问非静态成员。

        3. 静态代码块:使用static关键字定义的代码块称为静态代码块。静态代码块在类加载时执行,并且只会执行一次。静态代码块常用于初始化静态变量或执行一些只需执行一次的操作。

        4. 静态内部类:使用static修饰的内部类称为静态内部类。静态内部类与外部类没有直接的关联,可以直接通过外部类名访问。静态内部类可以拥有自己的静态成员,但不能访问外部类的非静态成员。

        5. 静态导入:使用static关键字可以导入类的静态成员,使得在使用时可以省略类名。例如,可以使用"import static java.lang.Math.*;"来导入Math类的所有静态成员,然后直接使用Math类的静态方法和常量。

        需要注意的是,静态成员属于类级别的,不依赖于类的实例化。因此,在使用静态成员时要注意遵循相关的访问规则和限制。

19、有没有可能两个不相等的对象有相同的hashcode(哈希冲突) 

        是的,有可能两个不相等的对象具有相同的哈希码。这种情况被称为哈希冲突。哈希冲突是指不同的对象在经过哈希函数计算后得到相同的哈希码。哈希函数是将对象映射到一个整数值的函数,而哈希码是这个整数值。

        在Java中,每个对象都有一个默认的hashCode()方法实现,它返回对象的哈希码。默认情况下,hashCode()方法是根据对象的内存地址计算得到的。然而,由于哈希码的范围是有限的,而对象的数量是无限的,所以在某些情况下会出现不同对象具有相同哈希码的情况。

        为了解决哈希冲突,Java中的哈希表数据结构(如HashMap、HashSet等)使用了链表或者红黑树等数据结构来存储具有相同哈希码的对象。当发生哈希冲突时,这些数据结构会将具有相同哈希码的对象存储在同一个桶中,并通过equals()方法来判断它们是否真正相等。

        因此,尽管两个不相等的对象可能具有相同的哈希码,但它们仍然可以通过equals()方法进行区分。在使用哈希表数据结构时,equals()方法用于判断两个对象是否相等,而哈希码则用于确定对象在哈希表中的位置。

    

20、如何解决哈希冲突?

        哈希冲突是指在哈希表中,不同的键值经过哈希函数计算后得到相同的索引位置。解决哈希冲突的常用方法有以下几种:

        1. 链地址法(拉链法):将哈希表的每个位置设置为链表的头节点,当发生哈希冲突时,将冲突的元素插入到对应位置的链表中。这样可以解决冲突,并且可以存储大量的元素。

        2. 开放地址法:当发生哈希冲突时,通过一定的探测方法找到下一个可用的位置。常见的探测方法有线性探测、二次探测和双重哈希等。线性探测是指依次往后查找,直到找到一个空闲位置;二次探测是指通过二次方程计算下一个位置;双重哈希是指使用第二个哈希函数来计算下一个位置。

        3. 再哈希法:当发生哈希冲突时,使用另一个哈希函数重新计算索引位置。如果仍然发生冲突,可以继续使用其他哈希函数进行再哈希,直到找到一个空闲位置。

        4. 建立公共溢出区:将所有发生冲突的元素都放在同一个溢出区中,需要时进行查找。这种方法可以解决冲突,但是查找效率会降低。

        以上是常见的解决哈希冲突的方法,选择哪种方法取决于具体的应用场景和需求。在实际应用中,可以根据数据量、数据分布情况和性能要求等因素来选择合适的解决方法。

21、Object 有哪些常用方法?方法的含义    

        Object类是Java中所有类的根类,它定义了一些常用的方法。下面是Object类的一些常用方法及其含义:

        1. equals(Object obj):判断当前对象是否与给定对象相等。默认情况下,equals方法比较的是对象的引用是否相等,即是否指向同一个内存地址。如果需要比较对象的内容是否相等,需要在具体类中重写equals方法。

        2. hashCode():返回当前对象的哈希码值。哈希码是根据对象的内部状态计算得出的一个整数值,用于快速查找和比较对象。在重写equals方法时,通常也需要重写hashCode方法。

        3. toString():返回当前对象的字符串表示。默认情况下,toString方法返回的是对象的类名和哈希码值的组合。可以根据需要在具体类中重写toString方法,以便返回更有意义的字符串表示。

        4. getClass():返回当前对象所属的类的Class对象。Class对象包含了关于类的各种信息,可以用于获取类的名称、字段、方法等。

        5. clone():创建并返回当前对象的一个副本。默认情况下,clone方法会创建一个浅拷贝,即只复制对象的引用而不复制对象本身。如果需要实现深拷贝,需要在具体类中重写clone方法。

        6. finalize():在垃圾回收器回收对象之前调用。可以在具体类中重写finalize方法,以便在对象被销毁之前执行一些清理操作。

        以上是Object类的一些常用方法及其含义。需要注意的是,这些方法都是被所有类继承的,因此可以在任何类的对象上调用这些方法。

二、JVM

1、JVM知识点汇总

JVM(Java Virtual Machine)是Java虚拟机的缩写,它是Java程序运行的基础。下面是JVM的一些重要知识点总汇:

  1. JVM的作用:JVM是Java程序的运行环境,它负责将Java字节码文件解释或者编译成机器码,并在操作系统上执行。

  2. JVM的组成:JVM由三个主要的子系统组成:类加载器(ClassLoader)、运行时数据区(Runtime Data Area)和执行引擎(Execution Engine)。

  3. 类加载器:类加载器负责将Java字节码文件加载到内存中,并生成对应的Class对象。JVM中有三种类加载器:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。

  4. 运行时数据区:运行时数据区是JVM用来存储程序运行时数据的区域。主要包括方法区、堆、栈、本地方法栈和程序计数器。

  5. 方法区:方法区用于存储类的结构信息,如类的字段、方法、常量池等。在JDK8及之前,方法区是永久代(Permanent Generation)的一部分,而在JDK8之后,方法区被移除,取而代之的是元空间(Metaspace)。

  6. 堆:堆是用于存储对象实例的区域。所有通过new关键字创建的对象都会被分配到堆上。堆可以分为新生代(Young Generation)和老年代(Old Generation)。

  7. 栈:栈用于存储方法的调用和局部变量。每个线程在运行时都会有一个独立的栈,栈中的数据是线程私有的。

  8. 本地方法栈:本地方法栈用于存储本地方法(Native Method)的调用和参数。

  9. 程序计数器:程序计数器用于记录当前线程执行的字节码指令地址。

  10. 执行引擎:执行引擎负责解释或者编译字节码,并执行相应的机器码。JVM中有两种执行引擎:解释器(Interpreter)和即时编译器(Just-In-Time Compiler,JIT)。

  11. 垃圾回收:JVM通过垃圾回收机制自动管理内存。垃圾回收器负责回收不再使用的对象,并释放内存空间。

  12. JIT编译器:即时编译器将热点代码(HotSpot)编译成机器码,以提高程序的执行效率。

三、多线程&并发

1、Java中实现多线程的几种方法

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口( JDK1.5>= )
  •  线程池方式创建

1、继承Thread类:创建一个继承自Thread类的子类,并重写其run()方法。然后通过创建子类的实例来启动线程。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

  1. class MyThread extends Thread {

  2. public void run() {

  3. // 线程执行的代码

  4. }

  5. }

  6. public class Main {

  7. public static void main(String[] args) {

  8. MyThread thread = new MyThread();

  9. thread.start();

  10. }

  11. }

2、实现Runnable接口:创建一个实现了Runnable接口的类,并实现其run()方法。然后通过创建该类的实例,并将其作为参数传递给Thread类的构造方法来启动线程。

 
  1. class MyRunnable implements Runnable {

  2. public void run() {

  3. // 线程执行的代码

  4. }

  5. }

  6. public class Main {

  7. public static void main(String[] args) {

  8. MyRunnable runnable = new MyRunnable();

  9. Thread thread = new Thread(runnable);

  10. thread.start();

  11. }

  12. }

3、使用匿名内部类:可以直接使用匿名内部类来实现多线程,省去了创建新类的步骤。

 
  1. public class Main {

  2. public static void main(String[] args) {

  3. Thread thread = new Thread(new Runnable() {

  4. public void run() {

  5. // 线程执行的代码

  6. }

  7. });

  8. thread.start();

  9. }

  10. }

4、使用线程池:通过使用线程池可以更好地管理和复用线程资源,提高性能和效率。

 
  1. import java.util.concurrent.ExecutorService;

  2. import java.util.concurrent.Executors;

  3. public class Main {

  4. public static void main(String[] args) {

  5. ExecutorService executor = Executors.newFixedThreadPool(5);

  6. for (int i = 0; i < 10; i++) {

  7. executor.execute(new Runnable() {

  8. public void run() {

  9. // 线程执行的代码

  10. }

  11. });

  12. }

  13. executor.shutdown();

  14. }

  15. }

以上是Java中实现多线程的几种常用方法。每种方法都有其适用的场景和特点,根据具体需求选择合适的方法来实现多线程功能。

2、如何停止一个正在运行的线程

注:千万不要回答使用stop方法强行终止,stop方法虽然可以强行终止,但是不推荐这个方法,因为它可能会导致一些严重的问题。(stop方法是Thread类中的一个方法,它可以立即终止一个线程的执行。然而,这种终止方式是突然的,不会给线程任何机会来清理资源或者完成一些必要的操作。它会导致线程的状态不一致,可能会引发一些难以预料的问题。

要停止一个正在运行的线程,可以使用以下方法:

        1、使用标志位:在线程内部定义一个boolean类型的标志位,当标志位为true时,线程继续执行;当标志位为false时,线程停止执行。在需要停止线程的地方,将标志位设置为false即可。线程在执行过程中需要不断检查标志位的状态,以决定是否继续执行。

示例代码如下:

 
  1. public class MyThread extends Thread {

  2. private volatile boolean running = true;

  3. public void stopThread() {

  4. running = false;

  5. }

  6. @Override

  7. public void run() {

  8. while (running) {

  9. // 线程执行的代码

  10. }

  11. }

  12. }

在需要停止线程的地方,调用stopThread()方法即可停止线程。

        2、使用interrupt()方法:可以调用线程对象的interrupt()方法来停止线程。当调用interrupt()方法时,会给线程设置一个中断标志,线程可以通过检查中断标志来决定是否继续执行。需要注意的是,interrupt()方法只是设置了中断标志,并不会立即停止线程的执行,需要在线程的代码中主动检查中断标志并做出相应的处理。

示例代码如下:

 
  1. public class MyThread extends Thread {

  2. @Override

  3. public void run() {

  4. while (!Thread.currentThread().isInterrupted()) {

  5. // 线程执行的代码

  6. }

  7. }

  8. }

在需要停止线程的地方,调用interrupt()方法即可停止线程。

         需要注意的是,以上两种方法都是一种协作式的方式来停止线程,线程在执行过程中需要主动检查标志位或中断标志来决定是否停止执行。因此,在编写线程代码时,需要合理地设计线程的执行逻辑,以便在需要停止线程时能够及时响应。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

3、notify()notifyAll()有什么区别?

        notify()和notifyAll()都是Java中用于线程间通信的方法,它们的主要区别在于通知的对象不同。

        notify()方法:用于唤醒在该对象上等待的单个线程。如果有多个线程在该对象上等待,那么只会唤醒其中一个线程,具体唤醒哪个线程是不确定的,取决于操作系统的调度。

        notifyAll()方法:用于唤醒在该对象上等待的所有线程。如果有多个线程在该对象上等待,那么所有的线程都会被唤醒,并且它们会竞争获取对象的锁。

        需要注意的是,notify()和notifyAll()方法只能在同步代码块或同步方法中调用,并且必须拥有该对象的锁。否则会抛出IllegalMonitorStateException异常。

        这两个方法的使用场景不同,根据具体需求选择合适的方法。如果只需要唤醒一个线程,可以使用notify()方法;如果需要唤醒所有等待的线程,可以使用notifyAll()方法。

4、sleep()wait() 有什么区别?

sleep()和wait()都是Java中用于线程控制的方法,但是它们的作用和使用方式有所不同。

        1、sleep()方法: sleep()方法是Thread类的静态方法,用于使当前线程暂停执行一段时间。它接受一个以毫秒为单位的时间参数,表示线程暂停的时间长度。当线程调用sleep()方法后,它会进入阻塞状态,不会占用CPU资源,直到指定的时间过去后才会重新进入就绪状态,等待CPU调度执行。

示例代码如下:

 
  1. try {

  2. Thread.sleep(1000); // 暂停1秒

  3. } catch (InterruptedException e) {

  4. e.printStackTrace();

  5. }

        

        2、wait()方法: wait()方法是Object类的方法,用于使当前线程进入等待状态,直到其他线程调用notify()或notifyAll()方法唤醒它。wait()方法必须在synchronized块中调用,因为它要求当前线程释放对象的锁,以便其他线程可以访问该对象。

示例代码如下:

 
  1. synchronized (obj) {

  2. try {

  3. obj.wait(); // 当前线程进入等待状态

  4. } catch (InterruptedException e) {

  5. e.printStackTrace();

  6. }

  7. }

需要注意的是,wait()方法和sleep()方法的区别在于:

  • wait()方法必须在synchronized块中调用,而sleep()方法可以在任何地方调用。
  • wait()方法会释放对象的锁,而sleep()方法不会释放锁。
  • wait()方法需要被其他线程调用notify()或notifyAll()方法来唤醒,而sleep()方法会在指定的时间过去后自动唤醒。

5、volatile 是什么?可以保证有序性吗?

        volatile是Java中的一个关键字,用于修饰变量。它的作用是告诉编译器和JVM,该变量可能会被多个线程同时访问,因此需要特殊处理以确保线程之间的可见性和有序性。

        具体来说,当一个变量被声明为volatile时,每次访问该变量时,都会从主内存中读取最新的值,而不是使用线程的本地缓存。同时,对volatile变量的写操作也会立即刷新到主内存中,而不是延迟到某个时刻。

        虽然volatile可以保证可见性,即一个线程对volatile变量的修改对其他线程是可见的,但它并不能保证有序性。也就是说,volatile不能保证多个线程对该变量的操作按照一定的顺序执行。

        要保证有序性,可以使用其他手段,比如使用synchronized关键字或者Lock对象来实现同步。这些机制可以确保多个线程对共享变量的操作按照一定的顺序执行,从而保证有序性。

总结一下:
        - volatile关键字可以保证可见性,即一个线程对volatile变量的修改对其他线程是可见的。
        - volatile关键字不能保证有序性,即不能保证多个线程对该变量的操作按照一定的顺序执行。

6、Thread 类中的start()  run() 方法有什么区别?

        在Java中,Thread类是用于创建和操作线程的类。start()和run()方法是Thread类中的两个重要方法,它们有以下区别:

1. start()方法:
           - start()方法用于启动一个新的线程,并使其执行run()方法中的代码。
           - 在调用start()方法后,系统会为线程分配资源,并在合适的时机调用线程的run()方法。
           - start()方法是一个非阻塞方法,即它会立即返回并继续执行后续代码,不会等待线程执行完毕。

2. run()方法:
           - run()方法是线程的入口点,线程在执行过程中会调用run()方法中的代码。
           - run()方法定义了线程的具体逻辑,包含了线程要执行的任务。
           - 直接调用run()方法并不会创建新的线程,而是在当前线程中顺序执行run()方法中的代码。
           - run()方法是一个阻塞方法,即它会一直执行直到方法中的代码执行完毕。

        总结起来,start()方法用于启动一个新的线程并执行run()方法中的代码,而直接调用run()方法只是在当前线程中顺序执行run()方法中的代码。通常情况下,我们应该使用start()方法来启动线程,而不是直接调用run()方法。

7、为什么wait, notify  notifyAll这些方法不在thread类里面?  

        wait、notify和notifyAll这些方法不在Thread类中的原因是因为它们是与对象的锁相关的方法,而不是与线程本身相关的方法。

        在Java中,每个对象都有一个锁(也称为监视器),用于控制对该对象的并发访问。wait、notify和notifyAll这些方法是用于实现线程间的协作和通信的机制,它们需要与对象的锁一起使用。

        1、wait方法:用于使当前线程进入等待状态,同时释放对象的锁,直到其他线程调用该对象的notify或notifyAll方法来唤醒等待的线程。

        2、notify方法:用于唤醒在该对象上等待的一个线程,如果有多个线程在等待,则只会唤醒其中一个线程。

        3、notifyAll方法:用于唤醒在该对象上等待的所有线程。

        由于wait、notify和notifyAll方法需要与对象的锁一起使用,所以它们被定义在Object类中,而不是Thread类中。每个对象都可以调用这些方法来实现线程间的协作和通信。

        另外,Thread类提供了一些其他与线程相关的方法,如start、run、sleep等,这些方法是直接与线程的创建、执行和控制相关的。因此,wait、notify和notifyAll这些方法不适合放在Thread类中。

8、为什么waitnotify方法要在同步块中调用?

        wait()和notify()方法是Java中用于线程间通信的方法,它们必须在同步块中调用的原因是为了确保线程安全性。

        在Java中,每个对象都有一个锁(也称为监视器),线程在访问对象的同步代码块或同步方法时需要获取该对象的锁。wait()和notify()方法必须在获取了对象的锁之后才能调用,否则会抛出IllegalMonitorStateException异常。

        当一个线程调用wait()方法时,它会释放对象的锁,并进入等待状态,直到其他线程调用了该对象的notify()方法或notifyAll()方法来唤醒它。而notify()方法则是用于唤醒等待中的线程。

        如果wait()和notify()方法不在同步块中调用,就无法保证线程安全性。因为如果多个线程同时调用wait()或notify()方法,它们可能会竞争对象的锁,导致不可预测的结果。而将wait()和notify()方法放在同步块中,可以确保只有一个线程能够获取到对象的锁,并且按照预期的顺序执行。

        另外,需要注意的是,wait()和notify()方法只能在已经获取到对象锁的情况下调用,否则会抛出IllegalMonitorStateException异常。因此,在调用wait()和notify()方法之前,必须先通过synchronized关键字或者Lock对象来获取对象的锁。

        总结一下,wait()和notify()方法要在同步块中调用的原因是为了确保线程安全性,避免多个线程竞争对象的锁导致不可预测的结果。

9、Java中synchronized 和 ReentrantLock 有什么不同?

        在Java中,synchronized和ReentrantLock都是用于实现线程同步的机制,但它们有一些不同之处。

        1. 锁的获取方式:synchronized是隐式锁,它在代码块或方法上加上synchronized关键字后,线程进入代码块或方法时会自动获取锁,并在退出时释放锁。而ReentrantLock是显式锁,需要手动调用lock()方法获取锁,并在使用完毕后调用unlock()方法释放锁。

        2. 锁的可重入性:synchronized是可重入锁,即同一个线程可以多次获取同一个锁,而不会造成死锁。ReentrantLock也是可重入锁,并且提供了更灵活的重入性,可以通过设置公平性来决定锁的获取顺序。

        3. 锁的公平性:synchronized是非公平锁,即线程获取锁的顺序是不确定的。而ReentrantLock可以通过构造函数传入参数来设置为公平锁或非公平锁,默认为非公平锁。公平锁会按照线程的请求顺序来获取锁,而非公平锁则允许插队。

        4. 锁的灵活性:ReentrantLock相比synchronized提供了更多的灵活性。例如,ReentrantLock可以通过tryLock()方法尝试获取锁,如果锁已被其他线程占用,则返回false,而synchronized没有类似的方法。此外,ReentrantLock还提供了Condition接口,可以通过Condition实现更灵活的线程等待和唤醒机制。

        总的来说,synchronized是Java语言内置的关键字,使用简单,但功能相对有限。而ReentrantLock是一个类,提供了更多的功能和灵活性,但使用起来相对复杂一些。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

10、有三个线程A1,A2,A3,如何保证顺序执行?

        可以使用Java中的线程同步机制来实现A2在A1执行完后执行,A3在A2执行完后执行的需求。一种常用的方法是使用join()方法。

  join()方法是Thread类中的一个方法,它的作用是等待调用该方法的线程执行完毕。在本例中,我们可以让A1线程调用A2线程的join()方法,让A2线程调用A3线程的join()方法。

具体实现如下:

 
  1. Thread A1 = new Thread(new Runnable() {

  2. @Override

  3. public void run() {

  4. // A1线程的执行逻辑

  5. }

  6. });

  7. Thread A2 = new Thread(new Runnable() {

  8. @Override

  9. public void run() {

  10. try {

  11. A1.join(); // 等待A1线程执行完毕

  12. } catch (InterruptedException e) {

  13. e.printStackTrace();

  14. }

  15. // A2线程的执行逻辑

  16. }

  17. });

  18. Thread A3 = new Thread(new Runnable() {

  19. @Override

  20. public void run() {

  21. try {

  22. A2.join(); // 等待A2线程执行完毕

  23. } catch (InterruptedException e) {

  24. e.printStackTrace();

  25. }

  26. // A3线程的执行逻辑

  27. }

  28. });

  29. A1.start();

  30. A2.start();

  31. A3.start();

        在上述代码中,A1、A2、A3分别表示三个线程。A1线程在启动后,会执行自己的逻辑。当A1线程执行完毕后,A2线程会调用A1线程的join()方法,等待A1线程执行完毕。同理,A3线程会调用A2线程的join()方法,等待A2线程执行完毕。这样就可以保证A2在A1执行完后执行,A3在A2执行完后执行。

需要注意的是:join()方法可能会抛出InterruptedException异常,需要进行异常处理。

11、SynchronizedMapConcurrentHashMap有什么区别?

        SynchronizedMapConcurrentHashMap都是用于实现线程安全的Map的类,但它们在实现方式和性能上有一些区别。

        SynchronizedMap:是通过在每个方法上添加synchronized关键字来实现线程安全的。这意味着在同一时间只能有一个线程访问该Map,其他线程需要等待。虽然SynchronizedMap可以确保线程安全,但在高并发环境下性能可能较低,因为多个线程需要竞争同一个锁。

        ConcurrentHashMap:是Java提供的高效的线程安全的Map实现。它使用了一种不同的机制来实现线程安全,即分段锁(Segment Locking)。ConcurrentHashMap将整个Map分成多个段(Segment),每个段都有自己的锁。这样,在大多数情况下,多个线程可以同时访问不同的段,从而提高了并发性能。只有在同一个段内的操作才需要竞争锁。

        另外,ConcurrentHashMap还具有更好的扩展性。当需要增加并发性时,可以通过增加段的数量来提高并发性能。而SynchronizedMap则无法做到这一点。

总结一下:
        - SynchronizedMap使用synchronized关键字实现线程安全,性能较低。
        - ConcurrentHashMap使用分段锁实现线程安全,性能较高,并且具有更好的扩展性。

12、什么是线程安全

        线程安全是指在多线程环境下,一个方法或者一个类的实例能够正确地处理多个线程的访问,而不会出现数据不一致、数据丢失或者其他意外结果的情况。

        个人认为:如果你的代码 在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的

        在多线程环境中,多个线程可能同时访问共享的资源,比如变量、对象、文件等。如果没有采取适当的措施来保护这些共享资源,就可能会导致数据的不一致性或者错误的结果。

为了保证线程安全,可以采取以下几种方式:
        1. 互斥锁(Mutex):使用互斥锁可以确保同一时间只有一个线程可以访问共享资源。当一个线程进入临界区(访问共享资源的代码段)时,其他线程需要等待,直到该线程释放锁。
        2. 同步方法(Synchronized):使用synchronized关键字修饰方法或者代码块,确保同一时间只有一个线程可以执行该方法或者代码块。其他线程需要等待,直到该线程执行完毕。
        3. 原子操作(Atomic Operation):原子操作是指不可被中断的操作,要么全部执行成功,要么全部不执行。Java提供了一些原子类(如AtomicInteger、AtomicLong),可以保证对变量的操作是原子性的。
        4. 并发容器(Concurrent Collections):Java提供了一些线程安全的容器类(如ConcurrentHashMap、CopyOnWriteArrayList),可以在多线程环境下安全地操作集合。

        需要注意的是,虽然使用上述方式可以保证线程安全,但也会带来一定的性能开销。因此,在设计和实现多线程程序时,需要根据具体情况权衡性能和线程安全性。

13、线程安全有那几个级别?

        在Java中,线程安全是指多个线程同时访问一个共享资源时,不会出现数据不一致或者不可预期的结果。为了实现线程安全,可以采取以下几个级别的措施:

        1. 不可变(Immutable):不可变对象是指一旦创建后就不能被修改的对象。由于不可变对象的状态不会发生改变,所以多个线程同时访问不会引发线程安全问题。常见的不可变类有String、Integer等。

        2. 线程封闭(Thread confinement):线程封闭是指将共享资源限制在单个线程内部,其他线程无法访问。例如,使用ThreadLocal类可以实现线程封闭,每个线程都有自己独立的ThreadLocal变量副本。

        3. 互斥同步(Mutex synchronization):互斥同步是指通过加锁来保证同一时间只有一个线程可以访问共享资源。Java提供了synchronized关键字和Lock接口来实现互斥同步。当一个线程获得锁后,其他线程必须等待该线程释放锁才能继续执行。

        4. 无同步方案(No synchronization):如果共享资源的访问不涉及到多个线程之间的竞争,也就是说没有并发访问的情况,那么就不需要进行同步操作。这种情况下,共享资源是线程安全的。

        5. 并发容器(Concurrent collections):Java提供了一些并发容器类,如ConcurrentHashMap、ConcurrentLinkedQueue等,它们内部实现了线程安全的机制,可以在多线程环境下安全地进行操作。

        需要根据具体的场景和需求选择适当的线程安全级别。不同的级别有不同的实现方式和性能开销,需要根据实际情况进行权衡和选择。

四、Spring

 1、什么是spring?

        Spring是一个开源的Java框架,用于构建企业级应用程序。它提供了一种轻量级的、非侵入式的方式来开发Java应用程序,通过使用Spring框架,可以更加简化和加速Java应用程序的开发过程。

        Spring框架提供了一系列的模块,包括依赖注入、面向切面编程、事务管理、数据访问、Web开发等功能。其中最核心的特性是依赖注入(Dependency Injection,简称DI),它通过将对象之间的依赖关系交给Spring容器来管理,从而实现了松耦合和可测试性。

        通过使用Spring框架,开发者可以更加专注于业务逻辑的实现,而无需过多关注底层的技术细节。同时,Spring还提供了丰富的扩展机制和插件支持,可以与其他框架和技术无缝集成,使得开发更加灵活和高效。

        总结一下,Spring是一个功能强大、灵活且易于使用的Java框架,它可以帮助开发者构建可扩展、可维护和高效的企业级应用程序。

2、你们项目中为什么使用Spring框架?

注:(这里直接说Spring的好处就可以了)

 在我们项目中使用Spring框架有以下几个原因:

        1. 轻量级和非侵入性:Spring框架是一个轻量级的框架基本的版本大约2MB。,它不会强制你使用特定的编程模型或者类库。你可以根据自己的需求选择使用Spring的哪些功能,而不需要将整个框架引入项目中。这种非侵入性的设计使得Spring框架非常灵活,可以与其他框架和技术无缝集成。

        2. 依赖注入(DI):Spring框架通过依赖注入(Dependency Injection)来管理对象之间的依赖关系。通过DI,我们可以将对象之间的依赖关系交给Spring容器来管理,而不需要在代码中显式地创建和管理对象。这样可以降低代码的耦合度,提高代码的可维护性和可测试性。

        3. 面向切面编程(AOP):Spring框架提供了面向切面编程的支持。通过AOP,我们可以将与业务逻辑无关的横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,使得代码更加清晰和可维护。

        4. 容器管理:Spring框架提供了一个容器(ApplicationContext),用于管理和组织应用程序中的各个组件。通过容器,我们可以方便地管理对象的生命周期、配置和管理各种资源(如数据库连接、线程池等),以及实现各种功能(如国际化、事件驱动等)。

        5. 集成其他框架和技术:Spring框架提供了与其他框架和技术的无缝集成,如与Hibernate、MyBatis等ORM框架的集成,与Spring MVC、Spring Boot等Web框架的集成,以及与消息队列、缓存等中间件的集成。这使得我们可以更加方便地使用这些框架和技术,并且可以通过Spring的统一配置和管理来简化开发和维护工作。

        总之,Spring框架提供了丰富的功能和灵活的设计,可以帮助我们更加高效地开发和管理Java应用程序。它的轻量级、非侵入性和可扩展性使得它成为Java开发中最受欢迎的框架之一。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值