Java基础

目录:

1~5:
1.什么是面向对象?谈谈你对面向对象的理解
2.jre、jdk、jvm的区别
3.什么是方法重载?返回值算重载吗?
4.重写和重载的区别
5.抽象类和普通类有什么区别?
6~10:
6.抽象类和接口有什么区别?
7.什么是反射?使用的场景有哪些?
8.public/private/protected/default的区别
9.eques和==的区别
10.hashCode与equals
11~15:
11.哈希冲突的解决方案有哪些?
12.怎么比较两个日期的大小?
13.Java语言的特点
14.C语言和Java语言的区别
15.Java的8种基本数据类型
16~20:
16.Java的内存模型
17.简述final的作用
18.String、StringBuffer、StringBuilder的区别及使用场景
19.int和Integer的区别
20.ArrayList和LinkedList有什么区别?
21~25:
21.ArrayList和Vector有什么区别?
22.HashMap和Hashtable有什么区别?
23.HashMap和HashSet有什么区别?
24.HashMap的底层是如何实现的?
25.什么是负载因子?为什么是0.75?
26~30
26.List和Set的区别
27.为什么HashMap会出现死循环?
28.集合的使用场景总结
29.线程和进程的区别?
30.并行与并发的区别?
31~35
31.创建线程的方式有哪些?(高频)
32.runnable 和 callable有什么区别
33.线程的 run()start() 有什么区别?
34.线程包含了哪些状态,状态之间是如何变化的?(高频)
35.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
36~40
36.notify()和 notifyAll()有什么区别?
37.在 java中 wait 和 sleep 方法的不同?
38.如何停止一个正在运行的进程?
39.synchronized关键字的底层原理
40.JMM(Java内存模型)
41~45
41.synchronized和Lock有什么区别?
42.线程池的核心参数
43.线程池的执行流程
44.ThreadLocal

1~5

1.什么是面向对象?谈谈你对面向对象的理解?

什么是面向对象?

对比面向过程,是两种不同的处理问题的角度

比如:配送员送外卖

面向过程 会将任务拆解成一系列的步骤:1.去店铺取外卖->2.将外卖送到指定地点->3.客户取外卖

面向对象 会将任务拆成 商家、配送员、客户 这三个对象,他们各自做自己的事情,商家提供外卖,配送员配送外卖、客户噶外卖

从上面的例子中可以看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护。

面向对象的三大特性:封装、继承、多态

封装:
封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项。
内部的细节对外部调用透明,外部调用无需修改或关心内部实现。
比如封装了一个功能,把这个功能比喻成空调,那么我们只需要知道吹空调很凉快还有遥控器可以打开空调,而不需要关心空调制冷是怎么实现的

继承:
继承基类的方法,并做出自己的改变/或扩展
可以抽取出一些共性的、重复的方法作为父类,子类需要使用时直接继承父类,即可拿到父类的方法,而一些个性化的扩展交给子类来完成。

多态:

多态的前提条件: 1.有继承父类/实现接口;2.有方法重写;3.有父类引用指向子类对象。

父类引用指向子类对象是什么意思呢?
比如有一个宠物对象(父类),宠物有很多种,比如哈士奇,金毛,猫等等, 宠物 a = new 猫(); 像这样的,使用父类类型去接收子类对象就属于多态的一种体现。

多态的好处: 可以提高代码的扩展性和维护性

  • 扩展性:比如我们可以使用 宠物 类型去接收宠物的子类 类型,这就是提高扩展性的一种体现
  • 可维护性:比如我们要去修改猫对象时,只有猫类型的对象改变了,其它的狗啊、猪啊都没变

多态的弊端: 程序性能会下降,因为多态需要在运行的时候通过对象的实际类型来确定需要调用的方法,会增加程序运行时的开销,导致程序性能下降

编译时多态(静态多态)和运行时多态(动态多态):
编译时多态在编译阶段根据 参数类型和数量 确定要调用哪个方法,通常与方法重载相关。
运行时多态在程序运行时根据 对象的实际类型 确定方法的调用,通常与方法重写相关。

2.jre、jdk、jvm的区别和联系

JDK

Java DeveIpment Kit Java开发工具(开发人员使用)

JRE

Java Runtime Environment Java运行时环境(不一定是开发人员使用,我们运行Java程序就得安装JRE)

JVM

Java Virtual Machine Java虚拟机(编译和解析我们的Java代码,把它变成一个Java程序)

三者的联系

JDK包含JRE和Java工具,而JRE包含JVM和Java类库

3.什么是方法重载?返回值算重载吗?

在Java中,方法重载是指同一个类中定义多个方法,它们可以具有相同的名称,但参数列表不同,方法重载的定义如下:

public void myMethod(int arg1) {
    // 方法体
}

public void myMethod(int arg1, int arg2) {
    // 方法体
}

public void myMethod(String arg1) {
    // 方法体
}

返回值不同不算方法重载

public String myMethod(int arg1) {
    // 方法体
}

public int myMethod(int arg1) {
    // 方法体
}

因为不同类型的返回值Jvm不知道要调用哪个方法

// 方法调用
myMethod(1);

更深层次的原因: Jvm是通过方法签名来判断到底调用哪个方法的,而 方法签名 = 方法名称 + 参数类型 + 参数个数 组成的一个唯一值。从方法签名的组成可以看出,返回值类型不是方法签名的组成部分,所以不同的返回值类型也不能算是方法重载了。

4.重写和重载的区别

1.重载: 是在一个类里面,方法名字相同,而参数列表不同。返回值类型可以相同也可以不同。

2.重写 发生在父子类中,方法名、参数列表、返回值类型相同

public int add(int a,String b)
public String add(int a,String b)
//编译报错

5.普通类和抽象类有什么区别?

在Java种,普通类和抽象类是两种不同的类类型,普通类可以直接实例化,而抽象类不可以直接实例化。抽象类通常定义一些基本的行为和属性,而而具体的实现由其子类来完成,以下是普通类和抽象类的一些区别:

1.实例化:普通类可以直接实例化。抽象类不能直接实例化
2.方法:抽象类既可以包含抽象方法和普通方法,而普通类只能包含普通方法
3.实现:普通类实现接口需要重写实现类中的接口,而抽象类可以实现接口方法也可以不实现

以下是普通类和抽象类的示例代码

// 普通类
public class MyClass {
    public void myMethod() {
        System.out.println("我是普通类");
    }
}

// 抽象类
public abstract class MyAbstractClass {
    public abstract void myAbstractMethod(); //抽象方法
    public void myMethod() {
        System.out.println("我是抽象类");
    }
}

6~10

6.抽象类和接口有什么区别?

1.定义的关键字不一样,接口是interface,抽象类是abstract
2.抽象类中可以存在抽象方法和普通方法,而接口只能存在方法声明
3.抽象类中的成员变量可以是各种类型的,而接口中的成员变量默认是public类型的
4.抽象类只能继承一个,接口可以实现多个
以下是一个抽象类和一个接口的示例代码:

// 抽象类
public abstract class MyAbstractClass {
    public abstract void myAbstractMethod();
    private void myMethod() {
        System.out.println("This is a method in an abstract class.");
    }
    public int myVariable = 0;
    public static int myStaticVariable = 0;
    public MyAbstractClass() {
        System.out.println("This is a constructor in an abstract class.");
    }
}

// 接口
public interface MyInterface {
    void myAbstractMethod();
    int MY_CONSTANT = 0;
}

7.什么是反射?使用的场景有哪些?

Java反射是在运行时动态获取和操作类、方法、字段等结构的能力。

反射实现:

先定义一个需要被反射的类对象User:

public class User {
    public String name = "张三";
    private int age = 18;

    public void publicMethod() {
        System.out.println("do public method");
    }

    private void privateMethod() {
        System.out.println("do private method");
    }

    public static void staticMethod() {
        System.out.println("do static method");
    }
}

1.反射执行公共方法

// 1.反射得到对象
Class<?> clazz = Class.forName("User");
// 2.得到方法
Method method = clazz.getDeclaredMethod("publicMethod");
// 3.执行普通方法
method.invoke(clazz.getDeclaredConstructor().newInstance());

2.反射执行私有方法

// 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());

3.反射执行静态方法

// 1.反射得到对象
Class<?> clazz = Class.forName("User");
// 2.得到方法
Method method = clazz.getDeclaredMethod("publicMethod");
// 得到静态方法
Method staticMethod = clazz.getDeclaredMethod("staticMethod");
// 执行静态方法
staticMethod.invoke(clazz);

4.反射得到公共属性值

// 反射得到对象
Class<?> clazz = Class.forName("User");
// 得到公共属性
Field field = clazz.getDeclaredField("name");
// 得到属性值
String name = (String) field.get(
        clazz.getDeclaredConstructor().newInstance());
// 打印属性值
System.out.println("name -> " + name);

5.反射得到私有属性值

// 反射得到对象
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);

使用场景:

反射的使用场景有很多

1.编程开发使用的工具例如IDEA,写代码的时候会有提示(属性或方法名),就是用反射来实现的
2.很多知名的框架如Spring,为了使得程序更加简洁和优雅,也会使用反射,比如Spring中的依赖注入就是使用反射实现的
3.数据库连接框架也会使用反射来实现调用不同类型的数据库(驱动)。

反射的优缺点:

反射的优点如下:

1.灵活性:使用反射可以在运行时动态加载类,而不需要在编译时就将类加载到程序中。这对于需要动态扩展程序功能的情况非常有用。
2.可扩展性:使用反射可以使程序更加灵活和可扩展,同时也可以提高程序的可维护性和可测试性。
3.实现更多功能:许多框架都使用反射来实现自动化配置和依赖注入等功能。例如,Spring 框架就使用反射来实现依赖注入。

反射的缺点如下:

1.性能问题:使用反射会带来一定的性能问题,因为反射需要在运行时动态获取类的信息,这比在编译时就获取信息要慢。
2.安全问题:使用反射可以访问和修改类的字段和方法,这可能会导致安全问题。因此,在使用反射时需要格外小心,确保不会对程序的安全性造成影响。

小结:

反射是指在运行时检查和操作类、接口、字段、方法等程序结构的能力。通过反射,可以在运行时获取类的信息,创建类的实例,调用类的方法,访问和修改类的字段等。通过反射可以提高程序的灵活性和可扩展性,可以实现更多的功能。但在使用反射时需要考虑性能问题以及安全等问题。

8.public/private/protected/default的区别

1.public: public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用。
2.private: private表示私有,私有的意思就是除了class自己以外,任何人都不可以直接使用。
3.protected: protected介于public和private之间,protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他外部的类,protected就变成private。
4.default: 在同一个包下可以访问(大部分应用于接口中)

9.eques和==的区别

1.“==" 比较的是内存中的地址
2.equals在比较基本数据类型的时候,和"=="是一样的,都是比较内存地址;但在比较引用数据类型时(如String和Integer)则比较的是值(因为String和Integer重写了equlas)

/**
 * @Date: 2023/9/22 14:09
 * @author: Qeem
 */
public class a {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = new String("hello");
        String str3 = str2; //引用传递
        System.out.println(str1 == str2); //false
        System.out.println(str1 == str3); //false
        System.out.println(str2 == str3); //true
        System.out.println(str1.equals(str2)); //true
        System.out.println(str1.equals(str3)); //true
        System.out.println(str2.equals(str3)); //true
    }
}

那么两个对象的hashCode()相同,则eques()也一定为true,对吗?

● 不对,不一定为true

10.hashCode和equals

hashCode介绍:

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,Java中的任何类都包含有hashCode() 函数。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

hashCode和euqes的区别

hashCode用来获取对象的哈希值,而eques用来比较对象的值

如果两个对象相等,那么hashCode一定也是相同的,所以我们在判断两个对象是否相等时可以先判断hashCode是否相同,如果不相同不需要往下判断了。(hashCode相同两个对象不一定相同,但是HashCode不相同两个对象一定不相同,优点类似于布隆过滤器。)

重写eques一定要重写hashCode,如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

11~15

11.哈希冲突的解决方案有哪些?

哈希冲突是指在哈希表中,两个或多个元素映射到同一个位置的情况。

String str1 = "3C";
String str2 = "2b";
int hashCode1 = str1.hashCode();
int hashCode2 = str2.hashCode();
System.out.println("字符串: " + str1 + ", hashCode: " + hashCode1);
System.out.println("字符串: " + str2 + ", hashCode: " + hashCode2);

允许的程序如下

不同的字符串,却拥有了相同的HashCode,这就是哈希冲突。因为元素的位置是根据HashCode的值进行定位的,此时它们的HashCode相同,就会指向同一个位置,就产生了哈希冲突。

解决哈希冲突:

在Java中,解决哈希冲突的方法主要有三种:链地址法、开放地址法、再哈希法。

1.链地址法: 将哈希表中的每一个桶都设置为一个链表,当发生哈希冲突时,将新的元素插入到链表的末尾。---优点:通俗易懂,适用于元素较多的情况;---缺点:链表过长时,查询效率会降低。

2.开放地址法: 发生哈希冲突时,通过一定的探测方法(如线性探测、二次探测、双重哈希等)在哈希表中寻找下一个可用的位置。---优点:不需要额外的存储空间,适用于元素数量较少的情况。---缺点是容易产生聚集现象,即某些桶中的元素过多,而其他桶中的元素很少。

3.再哈希法: 当发生哈希冲突时,使用另一个哈希函数计算出一个新的哈希值,然后将元素插入到对应的桶中。---优点是简单易懂,适用于元素数量较少的情况。---缺点:需要额外的哈希函数,且当哈希函数不够随机时,容易产生聚集现象。

链地址法 VS 开放地址法

链地址法和开放地址法个人觉得以下几点不同:
1.存储结构不同: 链地址法规定了存储的结构为链表(每个桶为一个链表),每次将值存储到链表的末尾;而开放地址法未规定存储的结构,所以它可以是链表也可以是树结构等。
2.查找方式不同: 链地址法查找时,先通过哈希函数计算出哈希值,然后在哈希表中查找对应的链表,再遍历链表查找对应的值。而开放地址法查找时,先通过哈希函数计算出哈希值,然后在哈希表中查找对应的值,如果查找到的值不是要查找的值,就继续查找下一个值,直到查找到为止。
3.插入方法不同: 链地址法插入时,先通过哈希函数计算出哈希值,然后在哈希表中查找对应的链表,再将值插入到链表的末尾。而开放地址法插入时,是通过一定的探测方法,如线性探测、二次探测、双重哈希等,在哈希表中寻找下一个可用的位置。所以链地址法插入方法实现非常简单,而开放地址法插入方法实现相对复杂。

线性探测 VS 二次探测

线性探测是发生哈希冲突时,线性探测会在哈希表中寻找下一个可用的位置,具体来说,它会检查哈希表中下一个位置是否为空,如果为空,则将元素插入该位置;如果不为空,则继续检查下一个位置,直到找到一个空闲的位置为止。
二次探测是发生哈希冲突时,二次探测会使用一个二次探测序列来寻找下一个可用的位置,具体来说,它会计算出一个二次探测序列,然后依次检查哈希表中的每个位置,直到找到一个空闲的位置为止。二次探测的优点是相对于线性探测来说,它更加均匀地分布元素,缺点是当哈希表的大小改变时,需要重新计算二次探测序列。
具体来说,二次探测序列是一个二次函数,它的形式如下:

f(i) = i^2

其中,i 表示探测的步数,f(i) 表示探测的位置。
例如,当发生哈希冲突时,如果哈希表中的第 k 个位置已经被占用,那么二次探测会依次检查第 k+1^2、第 k-1^2、第 k+2^2、第 k-2^2、第 k+3^2、第 k-3^2……等位置,直到找到一个空闲的位置为止。
二次探测的优点是相对于线性探测来说,它更加均匀地分布元素,但缺点是容易产生二次探测聚集现象,即某些桶中的元素过多,而其他桶中的元素很少。

HashMap 如何解决哈希冲突?

在 Java 中,HashMap 使用的是链地址法解决哈希冲突的,对于存在冲突的 key,HashMap 会把这些 key 组成一个单向链表,之后使用尾插法把这个 key 保存到链表尾部。

12.怎么比较两个日期的大小?

两种方式:

1.比较之前先使用SimpleDateFormat把两个日期调整为同一种格式,然后使用Date类的after()方法或before()方法,把两个日期对象传进去,after()方法是当前时间晚于后面的时间时返回true,否则返回false;before()方法是当前时间早于后面的时间时返回true,否则返回false
2.调用Date的getTime()方法获取到毫秒数来进行比较

13.Java语言的特点

1、使用广泛;2、简单;3、面向对象;4、与平台无关;5、解释型;6、多线程;7、安全;8、动态

14.C语言和Java语言的区别

1.C语言是面向过程的,就是按行执行;JAVA语言是面向对象的,就是按模块执行(补充:当然,在C++,及C#上,C也有向面向对象模式转换)
2.C语言有指针,Java语言没有
3.C语言能操作内存,Java语言不能操作内存
4.C语言不容易跨平台,Java语言容易跨平台
5.Java语言开发效率比C语言高
6.C语言性能比Java语言高(面向过程的语言效率一般比面向对象的语言高)

15.Java的8种基本数据类型

byte、short、long、int、char、float、double、boolean

16~20

16.java的内存模型

首先,当问到 Java 内存模型的时候,一定要注意,这块不要和 JVM 内存布局(JVM 运行时数据区域)搞混了,这块问的是 Java 内存模型,Java Memory Model,简称 JMM,而不是 JVM 的内存布局。
Java 内存模型是用来定义 Java 线程和内存之间的操作规范的,目的是解决多线程正确执行的问题。
Java 内存模型规范的定义确保了多线程程序的可见性、有序性和原子性,从而保证了线程之间正确的交互和数据一致性。 Java 内存模型主要包括以下内容:
1.主内存(Main Memory):所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。
2.工作内存(Working Memory):每个线程拥有自己的工作内存,用于存储主内存中的数据的副本。线程只能直接操作工作内存中的数据。
3.内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。
4.原子性(Atomicity):JMM 保证基本数据类型(如 int、long)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。
5.可见性(Visibility):JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。
6.有序性(Ordering):JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等。
Java 内存模型通过以上规则和语义,提供了一种统一的内存访问方式,使得多线程程序的行为可预测、可理解,并帮助开发者编写正确和高效的多线程代码。开发者可以利用 JMM 提供的同步机制(如关键字 volatile、synchronized、Lock 等)来实现线程之间的同步和通信,以确保线程安全和数据一致性。

内存模型的简单执行示例图如下:

为什么要有Java内存模型?

只所以要有 Java 内存模型的原因有两个:

1.CPU 缓存和主内存数据一致性的问题
2.操作系统优化指令重排序的问题
我们分别来看下这两个问题。

缓存一致性问题

要讲明白缓存一致性问题,要从计算机的内存结构说起,它的结构是这样的:

所以从上面可以看出计算机的重要组成部分包含以下内容:
1.CPU
2.CPU 寄存器:也叫 L1 缓存,一级缓存。
3.CPU 高速缓存:也叫 L2 缓存,二级缓存。
4.(主)内存

当然,部分高端机器还有 L3 三级缓存。

由于主内存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存(L2)来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主内存中,这样就会导致多个线程在进行操作和同步时,导致 CPU 缓存和主内存数据不一致的问题。

操作系统优化和指令重排序问题

并且,由于 JIT(Just In Time,即时编译)技术的存在,它可能会对代码进行优化,比如将原本执行顺序为 a -> b -> c 的流程,“优化”成 a -> c -> b 了,但这样优化之后,可能会导致我们的程序在某些场景执行出错,比如单例模式双重效验锁的场景,这就是典型的好心办坏事的事例。

结论

所以,为了防止缓存一致性问题和操作系统指令重排序导致的问题,于是就有了 Java 内存模型,来规范和定义多线程的可见性、有序性和原子性,从而保证了线程之间正确的交互和数据一致性。
Java 内存模型定义了很多东西,比如以下这些:

  • 所有的变量都存储在主内存(Main Memory)中。
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
  • 不同的线程之间无法直接访问对方本地内存中的变量。

就是咱们文章刚开始画的那附图。

主内存和工作内存交互规范

为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现(以下内容只需要简单了解即可):

  • lock:锁定,作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock:解锁,作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read:读取,作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
  • load:载入,作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use:使用,作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign:赋值,作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store:存储,作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write:写入,作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。

注意:工作内存也就是本地内存的意思。

小结

Java 内存模型(Java Memory Model,JMM)是一种规范,定义了 Java 程序中多线程环境下内存访问和操作的规则和语义,主要是解决 CPU 缓存一致性问题和操作系统优化指令重排序的问题的。

17.简述final的作用

final作为Java中的关键字可以用于三个地方。用于修饰类、类属性和类方法。

特征:凡是引用final关键字的地方皆不可修改!

(1)修饰类:表示该类不能被继承;

(2)修饰方法:表示方法不能被重写;

(3)修饰变量:表示变量赋值一次后值不能被修改(常量)

final修饰变量的本质: final修饰的变量会指向一块固定的内存, 这块内存中的值不能改变,引用类型变量所指向的对象之所以可以修改, 是因为引用变量不是直接指向对象的数据, 而是指向对象的引用的. 所以被final修饰的引用类型变量将永远指向一个固定的对象, 不能被修改; 对象的数据值可以被修改.

18.String、StringBuffer、StringBuilder的区别及使用场景

区别:

  • String是final修饰的,不可变,每次操作都会产生新的String对象(String不能被继承,因为String是final修饰的)
  • StringBuffer和StringBuilder都是在原对象上操作
  • StringBuffer是线程安全的,StringBuilder是线程不安全的
  • StringBuffer方法都是syncchronized修饰的
  • 性能:StringBuilder > StringBuffer > String

场景:经常需要改变字符串内容时使用后面两个(StringBuffer、StringBuilder)

优先使用StringBuilder,多线程使用共享变量时使用StringBuffer

19.int和Integer的区别?

  • 数据类型不同:int是基本数据类型,Integer是包装数据类型
  • 默认值不同:int默认值为0,Integer默认值为null
  • 内存中的存储方式不同:int在内存中存储的是数据值,Integer存储的是对象引用
  • 变量比较方式不同:int可以使用==,Integer一定要使用equals比较
  • 实例化方式不同:int不需要实例化,Integer需要实例化

20.ArrayList和LinkedList的区别?

ArrayList和LinkedList都是Java种的List接口的实现类。

但它们有以下不同:

1.它们的底层实现不同:ArrayList是基于动态数组的数据结构,而LinkedList是基于链表的数据结构
2.随机访问性能不同:ArrayList可以通过下标进行访问,时间复杂度是O(1),而LinkedList则需要遍历链表,时间复杂度是O(n)
3.插入和删除的性能不同:ArrayList插入和删除的性能不如LinkedList,LinkedList插入和删除的时间复杂度是O(1),而ArrayList是O(n),因为ArrayList插入和删除中间的元素时,需要挪动数组
4.占用的内存不同:ArrayList是动态数组实现的,内存是连续的,更节省内存,而LinkedList除了要存储数据本身,还需要存储两个指针,更占用内存

ArrayList的基础用法

方法描述

  • add() 将元素插入到指定位置的 arraylist 中
  • addAll() 添加集合中的所有元素到 arraylist 中
  • clear() 删除 arraylist 中的所有元素
  • clone() 复制一份 arraylist
  • contains() 判断元素是否在 arraylist
  • get() 通过索引值获取 arraylist 中的元素
  • indexOf() 返回 arraylist 中元素的索引值
  • removeAll() 删除存在于指定集合中的 arraylist 里的所有元素
  • remove() 删除 arraylist 里的单个元素
  • size() 返回 arraylist 里元素数量
  • isEmpty() 判断 arraylist 是否为空
  • subList() 截取部分 arraylist 的元素
  • set() 替换 arraylist 中指定索引的元素
  • sort() 对 arraylist 元素进行排序
  • toArray() 将 arraylist 转换为数组
  • toString() 将 arraylist 转换为字符串
  • ensureCapacity() 设置指定容量大小的 arraylist
  • lastIndexOf() 返回指定元素在 arraylist 中最后一次出现的位置
  • retainAll() 保留 arraylist 中在指定集合中也存在的那些元素
  • containsAll() 查看 arraylist 是否包含指定集合中的所有元素
  • trimToSize() 将 arraylist 中的容量调整为数组中的元素个数
  • removeRange() 删除 arraylist 中指定索引之间存在的元素
  • replaceAll() 将给定的操作内容替换掉数组中每一个元素
  • removeIf() 删除所有满足特定条件的 arraylist 元素
  • forEach() 遍历 arraylist 中每一个元素并执行特定操作
public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        /*
        * 添加
        * */
        list.add("Java");
        list.add("MySQL");
        list.add("Redis");
        list.add("SSM");
        String str = list.get(1); //获取元素,通过索引获取
        System.out.println(str);
        /*
        * 修改元素
        * 第一个参数为索引下标,第二个参数为新的值
        * */
        list.set(3,"SpringBoot"); //修改第四个元素SSM变为SpringBoot
        System.out.println(list.get(3));
        /*
        * 删除元素
        * 通过索引下标删除
        * */
        list.remove(3);
        int size = list.size(); //集合大小
        System.out.println(size);
        for (int i = 0; i < size; i++) { //遍历输出ArrayList的所有元素
            System.out.println(list.get(i));
        }
    }

LinkedList的基础用法

 public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        /*
         * 添加
         * */
        list.add("Java");
        list.add("MySQL");
        list.add("Redis");
        list.add("SSM");
        String str = list.get(1); //获取元素,通过索引获取
        System.out.println(str);
        /*
         * 修改元素
         * 第一个参数为索引下标,第二个参数为新的值
         * */
        list.set(3,"SpringBoot"); //修改第四个元素SSM变为SpringBoot
        System.out.println(list.get(3));
        /*
         * 删除元素
         * 通过索引下标删除
         * */
        list.remove(3);
        int size = list.size(); //集合大小
        System.out.println(size);
        for (int i = 0; i < size; i++) { //遍历输出ArrayList的所有元素
            System.out.println(list.get(i));
        }
    }

小结:

ArrayList和LinkedList都是List接口的实现类,但它们的底层实现(结构)不同,随机访问的性能和添加、删除的效率不同,如果是随机访问比较多的业务场景,可以用Arraylist,如果是添加和删除比较多的业务场景可以用LinkedList

21~25

21.ArrayList和Vector有什么区别?

ArrayList和Vector实现了List接口,它们都是动态数组实现的,对元素进行增删改查等操作如下:

import java.util.ArrayList;
import java.util.Vector;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        Vector<String> vector = new Vector<>();

        // 添加元素
        arrayList.add("Java");
        arrayList.add("Python");
        arrayList.add("C++");

        vector.add("Java");
        vector.add("Python");
        vector.add("C++");

        // 获取元素
        System.out.println(arrayList.get(0));
        System.out.println(vector.get(0));

        // 删除元素
        arrayList.remove(0);
        vector.remove(0);

        // 获取元素个数
        System.out.println(arrayList.size());
        System.out.println(vector.size());
    }
}

但它们有以下区别

1.线程安全性:ArrayList是线程不安全的,而Vector是线程安全的
2.性能:由于Vector是线程安全的,所以单线程场景下性能要弱于ArrayList
3.初始化容量的增长方式不同:ArrayList当容量不足时(占用达到75%)会进行扩容,每次扩容50%,而Vector在扩容时容量会直接翻倍。这就意味着在添加元素时,ArrayList需要更频繁的扩容,而Vector更适合存储大量的数据

总结:在不考虑线程安全的场景下,可以使用ArrayList,在需要存储大量元素或多线程情况下,需要考虑线程安全,就可以使用Vector

22.HashMap和Hashtable有什么区别?

HashMap和Hashtable都实现了Map接口,都是Java中用于存储键值对的数据结构,它们的底层数据结构都是由链表+数组的形式实现的(默认情况下),但它们存在以下几种区别:

1.线程安全:HashMap是线程不安全的,而Hashtable是线程安全的
2.性能:因为Hashtable底层使用了synchronized 对整个方法添加了锁,所以性能要弱于HashMap
3.存储:HashMap允许 key value 为null,而Hashtable不允许

Hashtable 不能存储 null 键和 null 值是因为,它的 key 值要进行哈希计算,如果为 null 的话,无法调用该方法,还是会抛出空指针异常。而 value 值为 null 的话,Hashtable 源码中会主动抛出空指针异常。

HashMap 允许 key 和 value 为 null 的原因是因为在 HashMap 中对 null 值进行了特殊处理,如果为 null 时会把它赋值为 0,如下源码所示:

HashMap和Hashtable的基本使用

import java.util.HashMap;
import java.util.Hashtable;

public class HashtableDemo {
    public static void main(String[] args) {
        Hashtable<String, String> table = new Hashtable<>();
        table.put("A", "Apple");
        table.put("B", "Ball");
        table.forEach((k, v) -> System.out.println(k + " " + v));
        HashMap<String, String> map = new HashMap<>();
        map.put("A", "Apple");
        map.put("B", "Ball");
        map.forEach((k, v) -> System.out.println(k + " " + v));
    }
}

Hashtable不推荐使用

虽然hashtable是线程安全的,但是它在整个方法上添加了synchronized锁,性能非常差,所以在多线程情况下,需要考虑线程安全问题的时候更推荐使用性能更高的ConcurrentHashMap。

ConcurrentHashMap 锁粒度更细,在多线程环境下的性能表现更好。

23.HashMap和HashSet的区别?

HashMap和HashSet都是Java中的集合类,HashMap继承了Map接口,而HashSet继承了Set接口,它们有以下几种区别:

1.HsahSet存储对象,而HashMap存储键值对
2.HashSet底层是使用HashMap实现的,它的值存储到了HashMap的Key中,而且HashSet还封装了一些HashMap的方法
3.HashMap允许value重复,不允许Key重复;而HashSet相反,允许key重复,不允许Value重复

HashMap的基本使用:

Map<String, String> map = new HashMap<>();
map.put("A", "Apple");
map.put("B", "Ball");
map.put("C", "Cat");
map.forEach((k, v) -> System.out.println(k + " " + v));

HashSet的基本使用:

Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("C");
set.forEach(System.out::println);

小结

HashSet 适用于只存储对象的情况,而 HashMap 适用于需要存储键值对的情况,可以根据键快速查找值。HashSet 底层是用 HashMap 存储的,用它可以存储不重复的值。

24.HashMap底层是如何实现的?

1.HashMap在不同版本的实现是不同的,在JDK1.8之前,是使用数组+链表实现的,而在JDK1.8之后,除了使用数组和链表,还使用了红黑树( 引入红黑树是为了解决哈希冲突 )。
2.当链表个数大于阙值(默认是8)并且数组长度大于64时,就会进化成红黑树
3.当元素个数达到容量的75%时,HashMap就会进行扩容,变为原来总长度的1.5倍。
4.在进行删除操作时,当红黑树的节点数小于6时,又会退化成链表。

为什么使用红黑树而不用其他树(如AVL树)

红黑树和AVL树的查找性能是差不多的,但是红黑树:

  • AVL树是一种追求高度平衡的树,为了维持平衡,树的翻转频率会很频繁,总体性能弱于红黑树
  • 相对简单: 红黑树相对于其他自平衡树,比如AVL树,实现和维护相对简单。它的平衡性是通过颜色标记和少量的旋转来维持的,相对于其他复杂的自平衡树结构,这使得红黑树更容易实现和维护。
  • 常用操作高效: 红黑树的常用操作,如插入、删除和查找,都可以在O(log n)时间内完成。这样,即使在哈希表中,出现了哈希冲突,将链表转换为红黑树能够保持高效的操作。

HashMap为什么线程不安全?

1.在JDK1.7中,HashMap在并发场景下put元素触发扩容再get元素会形成环形链表的数据结构导致死循环。
2.在JDK1.8之后,HashMap虽然解决了死循环的问题但是在并发情况下put元素会出现元素覆盖的情况。

HashMap的基本使用

方法描述

  • clear() 删除 hashMap 中的所有键/值对
  • clone() 复制一份 hashMap
  • isEmpty() 判断 hashMap 是否为空
  • size() 计算 hashMap 中键/值对的数量
  • put() 将键/值对添加到 hashMap 中
  • putAll() 将所有键/值对添加到 hashMap 中
  • putIfAbsent() 如果 hashMap 中不存在指定的键,则将指定的键/值对插 - 入到 hashMap 中。
  • remove() 删除 hashMap 中指定键 key 的映射关系
  • containsKey() 检查 hashMap 中是否存在指定的 key 对应的映射关系。
  • containsValue() 检查 hashMap 中是否存在指定的 value 对应的映射关系。
  • replace() 替换 hashMap 中是指定的 key 对应的 value。
  • replaceAll() 将 hashMap 中的所有映射关系替换成给定的函数所执行的结果。
  • get() 获取指定 key 对应对 value
  • getOrDefault() 获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值
  • forEach() 对 hashMap 中的每个映射执行指定的操作。
  • entrySet() 返回 hashMap 中所有映射项的集合集合视图。
  • keySet() 返回 hashMap 中所有 key 组成的集合视图。
  • values() 返回 hashMap 中存在的所有 value 值。
  • merge() 添加键值对到 hashMap 中
  • compute() 对 hashMap 中指定 key 的值进行重新计算
  • computeIfAbsent() 对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hasMap 中
  • computeIfPresent() 对 hashMap 中指定 key 的值进行重新计算,前提是该 key 存在于 hashMap 中。
public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        /*
        * 添加元素 k,v
        * 第一个参数:key
        * 第二个参数:value
        * */
        map.put(1,"Java");
        map.put(2,"MySQL");
        map.put(3,"Redis");
        map.put(4,"SSM");
        map.get(1); //获取元素,根据key获取
        boolean success = map.containsKey(3);//检查该key是否存在
        System.out.println(success);
        int size = map.size(); //获取map的大小
        System.out.println(size);
        map.forEach((k,v)-> System.out.println(v)); //遍历map的value
        map.forEach((k,v)-> System.out.println(k)); //遍历map的key
        map.forEach((k,v)-> System.out.println("key为:" + k + ";value为:" + v)); //遍历map的key和value
        /*
        * 删除
        * */
        map.remove(4); //删除指定key,根据key进行删除
        map.clear(); //删除所有key和value
    }

25.什么是负载因子?为什么是0.75?

HashMap 负载因子 load factor,也叫做扩容因子和装载因子,它是 HashMap 在进行扩容时的一个阈值,当 HashMap 中的元素个数超过了容量乘以负载因子时,就会进行扩容。默认的负载因子是 0.75,也就是说当 HashMap 中的元素个数超过了容量的 75% 时,就会进行扩容。当然,我们也可以通过构造函数来指定负载因子,如下所示:

扩容计算公式

HashMap 扩容的计算公式是:initialCapacity * loadFactor = HashMap 扩容。 其中,initialCapacity 是初始容量,默认值为 16(懒加载机制,只有当第一次 put 的时候才创建),loadFactor 是负载因子,默认值为 0.75。也就是说当 16 * 0.75 = 12 时,HashMap 就会开始扩容。

为什么要进行扩容?

HashMap 扩容的目的是为了 减少哈希冲突,提高 HashMap 性能的。

为什么默认负载因子是 0.75?

HashMap 负载因子 loadFactor 的默认值是 0.75,为什么是 0.75 呢?官方给的答案是这样的:

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur

上面的意思,简单来说是默认负载因子为 0.75,是因为它提供了空间和时间复杂度之间的良好平衡。 负载因子太低会导致大量的空桶浪费空间,负载因子太高会导致大量的碰撞,降低性能。0.75 的负载因子在这两个因素之间取得了良好的平衡。

负载因子 0.75 的科学推测

也就是说官方并未对负载因子为 0.75 做过的的解释,只是大概的说了一下,0.75 是空间和时间复杂度的平衡,但更多的细节是未做说明的,然而 Stack Overflow 一位大神 https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmapopen in new window 从科学的角度推测了这个问题的答案。 简单来说是通过二项式哈希函数的冲突概率来解释 0.75 这个问题的。 假设一个哈希桶为空和非空的概率为 0.5,我们用 s 表示容量,n 表示已添加元素个数。 用 s 表示添加的键的大小和 n 个键的数目。根据二项式定理,桶为空的概率为:

P(0) = C(n, 0) * (1/s)^0 * (1 - 1/s)^(n - 0)

因此,如果桶中元素个数小于以下数值,则桶可能是空的:

log(2)/log(s/(s - 1))

当 s 趋于无穷大时,如果增加的键的数量是 P(0) = 0.5,那么 n/s 很快趋近于 log(2),而 log(2) ~ 0.693。 所以,合理值大概在 0.7 左右,这就是对负载因子为 0.75 的一个科学推测。

** 小结**

负载因子 loadFactor 是 HashMap 在进行扩容时的一个阈值,扩容的计算公式是:initialCapacity * loadFactor = HashMap 扩容。它的默认值为 0.75,此值提供了空间和时间复杂度之间的良好平衡。

参考文章

https://hollischuang.gitee.io/tobetopjavaer/#/basics/java-basic/hashmap-default-loadfactor

26~30

26.List和Set的区别

  • List:有序,可重复,允许多个Null元素对象,可以使用Lterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的对象
  • Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能使用Lterator接口取得所有元素,在逐一遍历各个元素

27.HashMap为什么会出现死循环?

在JDK1.8之前,HashMap增加元素时使用的是头插法,在多线程的情况下put,会形成循环链表的数据结构,然后在get元素的时候,就会出现死循环。

死循环的原因

1.HashMap采用头插法插入元素(JDK1.8之前)
2.多线程同时添加
3.触发了HashMap的扩容

什么是头插法?

头插法是指新来的值会插入到链表头部的位置,取代原来的值,如下图所示:

此时使用头插入插入一个元素 Z,如下图所示:

头插法会导致 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 才被唤醒,扩容之后的场景如下图所示:

从上图可知线程 T1 执行之后,因为是头插法,所以 HashMap 的顺序已经发生了改变,但线程 T2 对于发生的一切是不可知的,所以它的指向元素依然没变,如上图展示的那样,T2 指向的是 A 元素,T2.next 指向的节点是 B 元素。

死循环执行步骤3

当线程 T1 执行完,而线程 T2 恢复执行时,死循环就建立了,如下图所示:

因为 T1 执行完扩容之后 B 节点的下一个节点是 A,而 T2 线程指向的首节点是 A,第二个节点是 B,这个顺序刚好和 T1 扩完容完之后的节点顺序是相反的。T1 执行完之后的顺序是 B 到 A,而 T2 的顺序是 A 到 B,这样 A 节点和 B 节点就形成死循环了,这就是 HashMap 死循环导致的原因。

解决方案

HashMap解决死循环常用的解决方案有以下几个

1.升级到高版本 JDK(JDK 1.8 以上),高版本 JDK 使用的是尾插法插入新元素的,所以不会产生死循环的问题;
2.使用线程安全容器 ConcurrentHashMap 替代(推荐使用此方案);
3.使用线程安全容器 Hashtable 替代(性能低,不建议使用);
4.使用 synchronized 或 Lock 加锁 HashMap 之后,再进行操作,相当于多线程排队执行(比较麻烦,也不建议使用)。

小结

HashMap 死循环发生在 JDK 1.7 版本中,形成死循环的原因是 HashMap 在 JDK 1.7 使用的是头插法,头插法 + 多线程并发操作 + HashMap 扩容,这几个点加在一起就形成了 HashMap 的死循环,解决死循环可以采用线程安全容器 ConcurrentHashMap 替代。

28.集合的使用场景总结

一组对象(单列):Collection 接口

允许重复值:List
增删多:LinkedList(底层是一个双向链表)
改查多:ArrayList(底层是一个数组)
线程安全:Vector :底层是一个数组

不允许重复值:Set

无序:HashSet:底层是HashMap,维护一个(数组 + 链表 + 红黑树)
排序:TreeSet:TreeMap实现
插入和取出顺序一致:LinkedHashSet底层是LinkedHashMap 是由:数组 +双向链表;

一组键值对:Map

键无序:HashMap:底层h是Hash表:JDK7:数组 + 链表,JDK8:数组 +链表 + 红黑树
键排序;TreeMap:红黑树
插入和取出的顺序一致:LinkedHashMap :
读取文件:Properties;

29.线程和进程的区别?

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务(一个进程可以分为多个线程)
  • 不同的进程使用的是不同的内存空间,而在进程下的所有线程可以共享内存空间
  • 线程更小,一个线程切换到另一个线程要比一个进程切换到另一个进程成本低

30.并行与并发的区别?

  • 并行是指多个任务同时在不同的处理器核心或计算单元上执行。每个任务都在独立的处理器上独立运行,彼此之间互不干扰。例如,如果有四个处理器核心,可以将四个不同的任务分配给每个核心并行执行,就可以在相同的时间段内完成更多的工作。
  • 并发是指多个任务在同一时间段内交替执行。例如,一个操作系统可以在同一时间段内交替运行多个应用程序,使得用户感觉它们在同时运行,尽管实际上每个应用程序可能在不同的时间片内交替执行。
  • 区别就是"并行"是多个任务同时执行,每个任务在独立的处理器上独立运行,而"并发"是多个任务在同一时间段内交替执行,可能在同一个处理器上交替运行。

31~35

31.创建线程的方式有哪些?(高频)

有4种方式:

  • 继承Thread类,重写run()方法
  • 实现runnable接口,重写run()方法
  • 实现Callable接口,重写call()方法
  • 线程池创建线程

32.runnable 和 callable有什么区别

  • Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  • Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
  • Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

33.线程的run()和start()有什么区别?

  • start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
  • run(): 封装了要被线程执行的代码,可以被调用多次。

34.线程包含了哪些状态,状态之间是如何变化的?(高频)

1.新建状态
2.可执行状态(就绪/运行)
3.终结状态(死亡)
4.阻塞状态
5.等待状态
6.计时等待状态

35.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

可以使用join方法

join方法的作用:

  • 等待线程结束:join() 方法被调用的线程将等待指定线程执行完毕,才会继续执行。
  • 等待超时:join() 方法允许传递一个超时参数,如果在指定时间内等待的线程没有执行完毕,那么当前线程将不再等待,继续执行后续的操作。

在t2线程中使用t1线程调用join(),在t3线程中使用t2线程调用join()

示例代码:


/**
 * @Date: 2023/10/21 18:30
 * @author: Qeem
 */
public class JoinTest {
    public static void main(String[] args) {
        //创建线程对象
        Thread t1 = new Thread(() -> {
            System.out.println("线程t1");
        });
        Thread t2 = new Thread(() -> {
            try{
                t1.join(); //加入线程t1,只有线程t1执行完成后,再次执行该线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程t2");
        });
        Thread t3 = new Thread(() -> {
            try{
                t2.join(); //加入线程t2,只有线程t1执行完成后,再次执行该线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程t3");
        });
        //启动线程(不管先启动哪个,结果都是一样的)
        t3.start();
        t2.start();
        t1.start();

    }
}

执行结果:

36~40

36.notify()和 notifyAll()有什么区别?

● notifyAll:唤醒所有 wait 的线程
● notify:只随机唤醒一个 wait 线程

37.在 java中wait()和sleep()的不同?

共同点:

wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点:

方法归属不同: sleep(long) 是 Thread 的静态方法,而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有

醒来时机不同: 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来,wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去它们都可以被打断唤醒

锁特性不同(重点): wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制,wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用),而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)

38.如何停止一个正在运行的进程

有三种方式可以停止线程

1.使用退出标志,使线程正常退出,也就是当run()完成后线程终止
2.使用stop()强行终止(不推荐,方法已作废)
3.使用interrupt()中断线程

  • 打断阻塞的线程(sleep、wait、join),线程会抛出InterruptedException异常
  • 打断正常的线程,可以根据打断状态来标记是否退出线程

39.synchronized关键字的底层原理

  • Synchronized【对象锁】采用互斥的方式让同一时刻最多只有一个线程能持有【对象锁】
  • 它的底层是由monitor实现的,monitor是jvm级别的对象(C++实现的),线程获得锁需要使用对象(锁)关联monitor
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset
  • 其中owner是关联获得锁的线程,而且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

40.JMM(Java内存模型)

  • JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
  • JMM描述了Java程序中各种变量(线程共享变量)的访问规则还有在JVM里把变量存储到内存和从内存里读取变量这样的底层细节
  • 特点:
  1. 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

  2. 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

  3. 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

41~45

41.synchronized和Lock有什么区别?

  • 在语法上:
  1. synchronized 是关键字,源码在 jvm 里,是用 c++ 语言实现;Lock 是接口,源码是 jdk 提供的,是用 java 语言实现
  2. 使用 synchronized 的时候,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
  • 在功能上
  1. 都属于悲观锁、都有基本的互斥、同步、锁重入功能
  2. Lock 比 synchronized功能多,比如可以获取等待状态、公平锁,可以打断、可以超时
  3. 而且Lock 有适合不同场景的实现,比如像 ReentrantLock,ReentrantReadWriteLock这些
  • 在性能上
  1. 在没有竞争的时候,synchronized 比较强,而在竞争激烈时,Lock比较强

42.线程池的核心参数

线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数

(1). corePoolSize 核心线程数目
(2). maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
(3). keepAliveTime 生存时间 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
(4). unit 时间单位 救急线程的生存时间单位,如秒、毫秒等
(5). workQueue 工作队列 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
(6). threadFactory 线程工厂 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
(7). handler 拒绝策略 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

43.线程池的执行流程

44.ThreadLocal

ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享
在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为value,放入当前线程的 ThreadLocalMap 集合中,当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值,当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

ThreadLocal的底层原理实现

  • 是应为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动,依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。
end
Java

评论区

暂无评论