面试查漏补缺–java基础

导读:本篇文章讲解 面试查漏补缺–java基础,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

每日总结10条(高效,举一反三)


文章目录


前言

版权声明:本文为CSDN博主「程序员囧辉」的原创文章,本人根据学习需要进行笔记,对其中内容或摘抄、或拓展、或按照自己的理解进行修改,原文链接来文章底部。
本文旨在本人根据大神的博客查漏补缺,温故知新。并且通过其中的一个点进行扩张,形成知识网络

1、面向对象的三大基本特征

答:封装、继承、多态

  • 封装:隐藏部分对象的属性和实现的细节,对数据的访问只能通过公共接口。通过这种方式对内部数据进行不同程度的保护(通过一些修饰符如:private,public,protect…)
    • e.g.子封装对象,方法等
    • 封装实体类的使用通过set和get方法来获取和修改属性
  • 继承:子类继承父类的特征和行为(属性和方法),使得子类和父类有相同的行为
    • e.g. 使用extends去继承
  • 多态:对于同一种行为,子类有不一样的做法。多态存在的必要条件:(1)继承(2)重写父类方法(3)父类引用指向子类对象
    • e.g. 父类是动物,定义同一种行为是行动方式
    • 子类分别是鸟(行动方式为飞)、鱼(行动方式为游)、狗(行动方式为跑)

—— 用代码加深理解

package com.xu.hashCodeAndEquals;

/**
* @author xusj
* <br>CreateDate 2022/10/17 16:46
*/
public abstract class Animal {
    // private 修饰子类是不能访问的,只能当前类访问
    private String name;

    /**
     * 由于属性是private修饰所以外部不能获取,就需要通过get方法来获取===》封装的特征
     */
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    /**
	* 行为方式
	*/
    public abstract String runMethod();
}

class Dog extends Animal{

    @Override
    public String runMethod() {
        return "跑";
    }
}
class Fish extends Animal{
    @Override
    public String runMethod() {
        return "游";
    }
}
class Brid extends Animal{

    @Override
    public String runMethod() {
        return "飞";
    }
}

class Test{
    public static void main(String[] args) {
        // 多态
        Animal dog = new Dog();
        // 父类引用指向子类
        System.out.println(dog.runMethod());
    }
}


2、访问修饰符public,private,protected,以及不写时的区别?

![image.png](https://img-blog.csdnimg.cn/img_convert/6ebaf5ec92f466d34a914266eef6eeeb.png#clientId=u4e19f662-cd08-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u866d94b0&margin=[object Object]&name=image.png&originHeight=272&originWidth=781&originalType=url&ratio=1&rotation=0&showTitle=false&size=36628&status=done&style=none&taskId=u80eb318c-51ab-4509-9701-4e8b65f3079&title=)
public很牛逼,全部都能干。
protected保护我,其他包别想弄。
private很小气,只有本类能访问。
默认一搞子类和别包都别想弄。

3、下面两个代码块能正常编译和执行吗?

// 代码块1
short s1 = 1; s1 = s1 + 1;
// 代码块2
short s1 = 1; s1 += 1;

答:代码块1:编译报错,short转int精度丢失
代码块2:正常编译和执行

public class com.joonwhee.open.demo.Convert {
  public com.joonwhee.open.demo.Convert();
    Code:
       0: aload_0
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return
 
  public static void main(java.lang.String[]);
    Code:
       0: iconst_1 // 将int类型值1入(操作数)栈
       1: istore_1 // 将栈顶int类型值保存到局部变量1中
       2: iload_1 // 从局部变量1中装载int类型值入栈
       3: iconst_1 // 将int类型值1入栈
       4: iadd // 将栈顶两int类型数相加,结果入栈
       5: i2s // 将栈顶int类型值截断成short类型值,后带符号扩展成int类型值入栈。
       6: istore_1 // 将栈顶int类型值保存到局部变量1中
       7: return
}

重点:i2s 是 int to short 的缩写;其实,s1 += 1 相当于 s1 = (short)(s1 + 1)

4、基础考察,指出下题的输出结果

public static void main(String[] args) {
    Integer a = 128, b = 128, c = 127, d = 127;
    System.out.println(a == b); // false
    System.out.println(c == d);	// true
}

答:Integer a = 128 =》Integer a = Integer.valueOf(128),进行自动装箱(基本类型自动转换为包装类的过程)
IntegerCache(Integer缓存)范围(-128~127),命中true,不命中false
但是这个缓存范围时可以修改的,可能有些人不知道。可以通过JVM启动参数:-XX:AutoBoxCacheMax= 来修改上限值,如下图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/5a2a19597a6d6071adc03d339dcceee6.png#clientId=u4e19f662-cd08-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u021654f8&margin=[object Object]&name=image.png&originHeight=109&originWidth=491&originalType=url&ratio=1&rotation=0&showTitle=false&size=10828&status=done&style=none&taskId=u9b622d87-80cb-4eb4-972b-233876a0b9b&title=)

  • 包装类和基本数据类型的差别

1、声明方式不同,基本类型不适用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
2、存储方式及位置不同,基本类型是直接将变量值存储在堆栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
3、初始值不同,基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null
4、使用方式不同,基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。
5、包装类都是继承Number 接口实现Compareble 接口的

  • 自动装箱
  • 自动拆箱

通过代码来说

// 进行自动装箱
Integer a = 128 
Integer a = Integer.valueOf(128)// 自动拆箱
int res = 128;

[

](https://blog.csdn.net/weixin_43060298/article/details/96570391)

5、用最有效率的方法计算2乘以8?

答:使用为运算
2<<3(2左移三位,等于2*8)
注意:装B,被雷批。但是原理上的快慢还是需要知道的
进阶:通常情况下,可以认为位运算是性能最高的。但是,其实编译器现在已经“非常聪明了”,很多指令编译器都能自己做优化。所以在实际实用中,我们无需特意去追求实用位运算,这样不仅会导致代码可读性很差,而且某些自作聪明的优化反而会误导编译器,使得编译器无法进行更好的优化。

6、&和&&的区别?

答:两个都为逻辑运算符。

  • &:逻辑与运算符、按位与运算符,只有对应的两个二进位均为1时,结果位才为1 ,否则为0,不具有短路性
  • &&:短路运算符。当运算符左右两边的表达式都为 true,才返回 true。同时具有短路性,如果第一个表达式为 false,则直接返回 false。

7、String 是 Java 基本数据类型吗?

答:不是
Java 基本数据类型 byte、short、int、long、float、double、char、boolean;除了基本数据类型,其他的都是引用类型
基本数据类型:存放在栈上
引用数据类型:存放在堆上,栈上存储引用地址

8、String 类可以继承吗?

答:不能,String是final修饰的所以不能继承

9、String和StringBuilder、StringBuffer的区别?

答:
String: 不可变(String值被创建后不能改变) 线程不安全 效率低
StringBuilder: 可变 线程不安全 效率高
StringBuffer: 可变 线程安全(使用synchronized)效率中等

使用synchronized锁;这里使用了模板方法设计模式,父类定义方法,子类重写
在这里插入图片描述

在这里插入图片描述

10、String s = new String(“xyz”) 创建了几个字符串对象?

答:
一个或者两个:

  • 两个:当字符串常量池中没有时。其中一个字面xyz放在共享的字符串常量池中,此时的实例在堆中,字符串常量池中放的时引用;另一个通过new String()的方式创建并初始化,放在堆中
  • 一个:当字符串常量池中已经有值时,就时一个,还是在常量池中

11、String s = “xyz” 和 String s = new String(“xyz”) 区别?

答:

  • 两者都会去检查字符串常量池,有就不创建直接使用,没有则创建
  • new String(“xyz”)通过new的方式还会在堆中添加一个实例
  • 可以理解成new String(“xyz”)包含String s = “xyz”

12、== 和 equals 的区别是什么?

答:

  • ==:分别可以用于基本数据类型,和引用数据类型之间的比较
    • 用于基本数据类型的时候,是比较数值的大小
    • 用于应用数据类型的时候是比较其对象的地址是否相同
  • equals:Object定义的方法,通常用于比较两个对象的值是否相等
    • 对于底层来说,object定义方法和==相同,但是当我们在使用的时候,通常会重写equals来比较两个对象的值是否相同

Obj中equals的方法可知
在这里插入图片描述
以string为例子,重写obj的equals方法---先判断长度,后通过比较里面的每个char来比较地址
在这里插入图片描述

13、两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

答:不对。

当有 a.equals(b) == true 时,则 a.hashCode() == b.hashCode() 必然成立,

反过来,当 a.hashCode() == b.hashCode() 时,a.equals(b) 不一定为 true。

equals永远是最屌的,因为他底层比的是内存地址
其实这个也很好理解,大家都使用过hashMap,为什么会有hash冲突。
可以简单的看一下下面的代码,就是put值得时候,先判断hash值在使用equals判断,同样也说明了题目得不对
在这里插入图片描述

14、什么是反射?

答:
反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为反射机制。
理论:总结就是在运行得时候可以获取任意对象的属性,并可以调用他们的方法

package com.xu.hashCodeAndEquals;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * @author xusj
 * <br>CreateDate 2022/10/18 18:41
 */
public class ShowMethods {
    static final String usage =
            "usage: \n" +
                    "ShowMethods qualified.class.name\n" +
                    "To show all methods in class or: \n" +
                    "ShowMethods qualified.class.name word\n" +
                    "To search for methods involving 'word'";
    public static void main(String[] args) {
        if(args.length < 1) {
            System.out.println(usage);
            System.exit(0);
        }
        try {
            // 通过类名获取类信息
            Class<?> c = Class.forName(args[0]);
            // 获取方法
            Method[] m = c.getMethods();
            // 获取构造器
            Constructor[] ctor = c.getConstructors();
            if(args.length == 1) {
                for (int i = 0; i < m.length; i++) {
                    System.out.println(m[i].toString());
                }
                for (int i = 0; i < ctor.length; i++) {
                    System.out.println(ctor[i].toString());
                }
            }
            else {
                for (int i = 0; i < m.length; i++)
                    if(m[i].toString().contains(args[1])) {
                        System.out.println(m[i].toString());
                    }
                for (int i = 0; i < ctor.length; i++)
                    if(ctor[i].toString().contains(args[1])) {
                        System.out.println(ctor[i].toString());
                    }
            }
        } catch (ClassNotFoundException e) {
            System.out.println("No such class: " + e);
        }
    }
}

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

数据分为基本数据类型和引用数据类型。
基本数据类型:数据直接存储在栈中;
引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。

浅拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。

深拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。

浅拷贝:

在这里插入图片描述

深拷贝
实现Cloneable接口,调用clone方式
在这里插入图片描述

深拷贝相比于浅拷贝速度较慢并且花销较大。

16、并发和并行有什么区别?

并发:同一时间抢着干一件事
并行:同一时间干多件事情

17、构造器是否可被 重写?

答:不能
构造器只能被重载,不能被重写

18、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

答:值传递,
Java 中只有值传递,对于对象参数,值的内容是对象的引用。

  • 值传递:是指在调用函数时,将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,就不会影响到实际参数
    在这里插入图片描述

  • 引用传递:是指在调用函数时,将实际参数的地址传递到函数中,那么在函数中对参数进行修改,将会影响到实际参数

在这里插入图片描述

19、Java 静态变量和成员变量的区别。

public class Demo {
    /**
     * 静态变量:又称类变量,static修饰。可用类名直接调用
     */
    public static String STATIC_VARIABLE = "静态变量";
    /**
     * 实例变量:又称成员变量,没有static修饰,通过类去调用
     */
    public String INSTANCE_VARIABLE = "实例变量";
}

答:

  • 成员变量属于对象,和对象共存亡(对象的创建和销毁),在堆中,
  • 静态变量属于类,随着类的加载而存在,在方法区中

20、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?

答:
分为两种情况,根据是否显示创建了对象实例判断
在这里插入图片描述

21、初始化考察,请指出下面程序的运行结果。

public class InitialTest {
    public static void main(String[] args) {
        A ab = new B();
        ab = new B();
    }
}
class A {
    static { // 父类静态代码块
        System.out.print("A");
    }
    public A() { // 父类构造器
        System.out.print("a");
    }
}
class B extends A {
    static { // 子类静态代码块
        System.out.print("B");
    }
    public B() { // 子类构造器
        System.out.print("b");
    }
}

答:ABabab
先父类,先静态,构造器最后(总)

当有父类时,完整的初始化顺序为:父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器 ->子类非静态变量(非静态代码块)->子类构造器 。

22、重载(Overload)和重写(Override)的区别?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载:一个类中有多个同名的方法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。

重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。

23、为什么不能根据返回类型来区分重载?

如果我们有两个方法如下,当我们调用:test(1) 时,编译器无法确认要调用的是哪个。【1、编译器无法确认

// 方法1
int test(int a);
// 方法2
long test(int a);

方法的返回值只是作为方法运行之后的一个“状态”,但是并不是所有调用都关注返回值,所以不能将返回值作为重载的唯一区分条件。【返回值只是一种运行结果的状态,不能作为重载的唯一区分条件

24、抽象类(abstract class)和接口(interface)有什么区别?

抽象类只能单继承,接口可以多实现。[例子略]

抽象类可以有构造方法,接口中不能有构造方法。

抽象类中可以有成员变量,接口中没有成员变量,只能有常量(默认就是 public static final)

抽象类中可以包含非抽象的方法,在 Java 7 之前接口中的所有方法都是抽象的,在 Java 8 之后,接口支持非抽象方法:default 方法、静态方法等。Java 9 支持私有方法、私有静态方法。

抽象类中的方法类型可以是任意修饰符,Java 8 之前接口中的方法只能是 public 类型,Java 9 支持 private 类型。

  • 接口
    在这里插入图片描述
  • 抽象类
    在这里插入图片描述
    设计思想的区别:

接口是自上而下的抽象过程,接口规范了某些行为,是对某一行为的抽象。我需要这个行为,我就去实现某个接口,但是具体这个行为怎么实现,完全由自己决定。

抽象类是自下而上的抽象过程,抽象类提供了通用实现,是对某一类事物的抽象。我们在写实现类的时候,发现某些实现类具有几乎相同的实现,因此我们将这些相同的实现抽取出来成为抽象类,然后如果有一些差异点,则可以提供抽象方法来支持自定义实现。

骚例子:

普通类像亲爹 ,他有啥都是你的。(子类extends后,你有的我都有)

抽象类像叔伯,有一部分会给你,还能指导你做事的方法。

接口像干爹,可以给你指引方法,但是做成啥样得你自己努力实现。(子类实现抽象方法,去自己去实现)

25、Error 和 Exception 有什么区别?

Error 和 Exception 都是 Throwable 的子类,用于表示程序出现了不正常的情况。区别在于:
在这里插入图片描述

Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。
系统级问题,如内存泄漏

Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况。(在企业业务开发中经常会自定义业务异常,进行友好的抛出)
代码层面问题,需要捕获或者异常处理的

26、Java 中的 final 关键字有哪些用法?

比作阉割成太监

修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract 和 final。【抽象类本来就是被继承的加上final就没意义了】

修饰方法:该方法不能被子类重写。

修饰变量:
该变量必须在声明时给定初值,而在以后只能读取,不可修改。
如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。

public class FinalDemo {
    // 修饰基本数据类型,不可再修改该变量的值
    public static final int FINAL_VARIABLE = 0;
    // 修改对象,不可再修改该变量的引用,但是可以直接修改属性值
    public static final User USER = new User();
    public static void main(String[] args) {
        // 输出:User(id=0, name=null, age=0)
        System.out.println(USER);
        // 直接修改属性值
        USER.setName("test");
        // 输出:User(id=0, name=test, age=0)
        System.out.println(USER);
    }
}

27、阐述 final、finally、finalize 的区别。

其实是三个完全不相关的东西,只是长的有点像。。

final 如上所示。

finally:finally 是对 Java 异常处理机制的最佳补充,通常配合 try、catch 使用,用于存放那些无论是否出现异常都一定会执行的代码。在实际使用中,通常用于释放锁、数据库连接等资源,把资源释放方法放到 finally 中,可以大大降低程序出错的几率。

finalize:Object 中的方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。finalize()方法仅作为了解即可,在 Java 9 中该方法已经被标记为废弃,并添加新的 java.lang.ref.Cleaner,提供了更灵活和有效的方法来释放资源。这也侧面说明了,这个方法的设计是失败的,因此更加不能去使用它。

28、try、catch、finally 考察,请指出下面程序的运行结果。

public class TryDemo {
    public static void main(String[] args) {
        System.out.println(test());
    }
    public static int test() {
        try {
            return 1;
        } catch (Exception e) {
            return 2;
        } finally {
            System.out.print("3");
        }
    }
}

答:我做错了,我的答案是13。如果是如下的方法注意一下,
返回结果是1,3,11111
在这里插入图片描述
原题是:

执行结果:31。

try、catch。finally 的基础用法,在 return 前会先执行 finally 语句块,所以是先输出 finally 里的 3,再输出 return 的 1。

29、try、catch、finally 考察2,请指出下面程序的运行结果。

public class TryDemo {
    public static void main(String[] args) {
        System.out.println(test1());
    }
    public static int test1() {
        try {
            return 2;
        } finally {
            return 3;
        }
    }
}

执行结果:3。

这题有点陷阱,但也不难,try 返回前先执行 finally,结果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。

finally 里面使用 return 仅存在于面试题中,实际开发中千万不要这么用。

30、try、catch、finally 考察3,请指出下面程序的运行结果【变态了】

public class TryDemo {
    public static void main(String[] args) {
        System.out.println(test1());
    }
    public static int test1() {
        int i = 0;
        try {
            i = 2;
            return i;
        } finally {
            i = 3;
        }
    }
}

执行结果:2。

这边估计有不少同学会以为结果应该是 3,因为我们知道在 return 前会执行 finally,而 i 在 finally 中被修改为 3 了,那最终返回 i 不是应该为 3 吗?确实很容易这么想,我最初也是这么想的,当初的自己还是太年轻了啊。

这边的根本原因是,在执行 finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,所以即使这边 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。

这边其实根据字节码可以很容易看出来,在进入 finally 之前,JVM 会使用 iload、istore 两个指令,将结果暂存,在最终返回时在通过 iload、ireturn 指令返回暂存的结果。

31、JDK1.8之后有哪些新特性?

接口默认方法:Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可

Lambda 表达式和函数式接口:Lambda 表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中),使用 Lambda 表达式使代码更加简洁,但是也不要滥用,否则会有可读性等问题,《Effective Java》作者 Josh Bloch 建议使用 Lambda 表达式最好不要超过3行。

Stream API:用函数式编程方式在集合类上进行复杂操作的工具,配合Lambda表达式可以方便的对集合进行处理。Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

方法引用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

日期时间API:Java 8 引入了新的日期时间API改进了日期时间的管理。

Optional 类:著名的 NullPointerException 是引起系统失败最常见的原因。很久以前 Google Guava 项目引入了 Optional 作为解决空指针异常的一种方式,不赞成代码被 null 检查的代码污染,期望程序员写整洁的代码。受Google Guava的鼓励,Optional 现在是Java 8库的一部分

32、wait() 和 sleep() 方法的区别

  • 简单介绍一下,wait/notify原理
    在这里插入图片描述

wait当线程抢到锁时,但是他有某些条件不满足,所以他不能继续执行临界区中的代码,那么总不能让他一直占着锁吧,可以让他先区waitSet中去等待,让别的线程先干活,等他需要的条件满足了,再唤醒他起来干活就行了。——由此可见wait会释放锁,不然别的线程没办法拿到锁干活啊。【你要是老板的话,你也会这么安排活给手下去干,极度压榨】

  • wait和sleep之间的区别
    1、sleep是线程的方法,wait是object的方法
    2、sleep不需要和synchronized配合使用,wait必须要和它配合
    3、sleep不释放锁,wait释放锁

33、线程的 sleep() 方法和 yield() 方法有什么区别?

答:线程执行 sleep() 方法后进入超时等待(TIMED_WAITING)状态,而执行 yield() 方法后进入就绪(READY)状态。

sleep() 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield() 方法只会给相同优先级或更高优先级的线程以运行的机会。

在这里插入图片描述

sleep

  1. 调用sleep 会让当前线程从_Running _进入_Timed Waiting _状态(阻塞)
  2. 其它线程可以使用interrupt 方法打断正在睡眠的线程,这时sleep 方法会抛出InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用TimeUnit 的sleep 代替Thread 的sleep 来获得更好的可读性
    代码:
Thread thread = new Thread(() -> {

});
TimeUnit.SECONDS.sleep(1);
Thread.sleep(1);

Yield(让出,谦让)

  1. 调用yield 会让当前线程从_Running _进入_Runnable _就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器,仅仅是让线程进入到就绪状态进行竞争

线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它(和yield类似) 如果cpu 比较忙,那么优先级高的线程会获得更多的时间片,但cpu 闲时,优先级几乎没作用

Runnable task1 = () -> { int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> { int count = 0;
for (;;) {
// Thread.yield();
System.out.println("	---->2 " + count++);
}
};

Thread t1 = new Thread(task1, "t1");


34、线程的 join() 方法是干啥用的?

答:用于等待当前线程终止。如果一个线程A执行了 threadB.join() 语句,其含义是:当前线程A等待 threadB 线程终止之后才从 threadB.join() 返回继续往下执行自己的代码

join

  • 等待某一个线程运行结束后继续运行
  • join可以设置等待时间
static int r = 0;
public static void main(String[] args) throws InterruptedException { test1();
}
private static void test1() throws InterruptedException { log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug(" 开 始 "); 
sleep(1);// 睡觉了
log.debug("结束"); 
r = 10;
});
t1.start();

t1.join();//主线程等待t1运行结束

log.debug("结果为:{}", r); log.debug("结束");
}

35、编写多线程程序有几种实现方式?

通常来说,可以认为有三种方式:
1)继承 Thread 类;
2)实现 Runnable 接口;
3)实现 Callable 接口。

1、new Thread();
2、将线程和任务(线程中需要执行的任务)分开
使用Runnable 配合Thread

Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

原理:Thread和Runnable之间的关系

3、FutureTask 配合Thread
使用get方法获取执行结果,FutureTask能接收Callable类型的参数,用来处理返回的结果

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(
() -> { log.debug("hello");
return 100;
});

// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();

// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get(); 
log.debug("结果是:{}", result);

其中,Thread 其实也是实现了 Runable 接口。Runnable 和 Callable 的主要区别在于是否有返回值。

36、Thread 调用 start() 方法和调用 run() 方法的区别

run():普通的方法调用,在主线程中执行,不会新建一个线程来执行。

start():新启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到 CPU 时间片,就开始执行 run() 方法。

public class TestThreadRunStart {
 
	public static void main(String[] args) {
		Thread t = new Thread(){
			@Override
			public void run() {
				//休眠3秒
				try {
					Thread.sleep(3000);
					System.out.println("休眠3秒");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("Thread running...");
			}
		};
		
		testRun(t);
//		testStart(t);
	}
	
	private static void testRun(Thread t) {
		t.run();
		//休眠1秒
		try {
			Thread.sleep(1000);
			System.out.println("休眠1秒");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	private static void testStart(Thread t) {
		t.start();
		//休眠1秒
		try {
			Thread.sleep(1000);
			System.out.println("休眠1秒");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

testRun运行结果:

休眠3秒
Thread running…
休眠1秒

testStart运行结果:
休眠1秒
休眠3秒
Thread running…

可得结论:run是马上获取cpu马上执行线程中方法体中得方法;start只是让线程进入到就绪状态等待获取cup时间片

37、线程的状态流转

在这里插入图片描述
网上这个例子完美:
举个通俗一点的例子来解释上面五种状态,比如上厕所:

你平时去商城上厕所,准备去上厕所就是新建状态(new),上厕所要排队,排队就是就绪状态(Runnable),有坑位了,轮到你了,拉屎就是运行状态(Running),你拉完屎发现没有手纸,要等待别人给你送纸过来,这个状态就是阻塞(Blocked),等你上完厕所出来,上厕所这件事情结束了就是死亡状态了。

注意:便秘也是阻塞状态,你便秘太久了,别人等不及了,把你赶走,这个就是挂起,还有一种情况,你便秘了,别人等不及了,跟你说你先出去酝酿一下,5分钟后再过来拉屎,这就是睡眠。

38、synchronized 和 Lock 的区别(在并发编程中这里都是要深究的)

1)Lock 是一个接口;synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;

2)Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,很可能会造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;synchronized 不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生;

class MyReentrantLock {
    private ReentrantLock LOCK = new ReentrantLock();

    public void run() {
        //上锁(阿里代码规约中,lock和try和finally共同使用)
        LOCK.lock();
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("当前线程名: " + Thread.currentThread().getName() + " ,i = " + i);
            }
        } finally {
            //释放锁
            LOCK.unlock();
        }
    }
}

3)Lock 的使用更加灵活,可以有响应中断、有超时时间等;而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,直到获取到锁;

4)在性能上,随着近些年 synchronized 的不断优化,Lock 和 synchronized 在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官方推荐尽量使用 synchronized,除非 synchronized 无法满足需求时,则可以使用 Lock。

39、synchronized 各种加锁场景的作用范围

1.作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。

public synchronized void method() {}

2.作用于静态方法,锁住的是类的Class对象,因为Class的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。

public static synchronized void method() {}

3.作用于 Lock.class,锁住的是 Lock 的Class对象,也是全局只有一个。

synchronized (Lock.class) {}

4.作用于 this,锁住的是对象实例,每一个对象实例有一个锁。

synchronized (this) {}

5.作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。

public static Object monitor = new Object(); synchronized (monitor) {}

40、如何检测死锁?

死锁的四个必要条件:

1)互斥条件:进程对所分配到的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。(你把门锁了我必须等你开门我才能进去,排他)

2)请求和保持条件:进程已经获得了至少一个资源,但又对其他资源发出请求,而该资源已被其他进程占有,此时该进程的请求被阻塞,但又对自己获得的资源保持不放。你已经先进房间了,这时候我再来就不行了,请求和保持

3)不可剥夺条件:进程已获得的资源在未使用完毕之前,不可被其他进程强行剥夺,只能由自己释放。必须等你在房间里把事情弄完,我才能进去,不可剥夺

4)环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中 Pi 等待的资源被 P(i+1) 占有(i=0, 1, …, n-1),Pn 等待的资源被 P0占 有,如下图所示。这里有比较有名的哲学家就餐问题
在这里插入图片描述

41、怎么预防死锁?

破掉必要条件
预防死锁的方式就是打破四个必要条件中的任意一个即可。

1)打破互斥条件:在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。。

2)打破请求和保持条件:1)采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待。 2)每个进程提出新的资源申请前,必须先释放它先前所占有的资源。

3)打破不可剥夺条件:当进程占有某些资源后又进一步申请其他资源而无法满足,则该进程必须释放它原来占有的资源。

4)打破环路等待条件:实现资源有序分配策略,将系统的所有资源统一编号,所有进程只能采用按序号递增的形式申请资源。

42、为什么要使用线程池?直接new个线程不是很舒服?

避免开销,方便管理
如果我们在方法中直接new一个线程来处理,当这个方法被调用频繁时就会创建很多线程,不仅会消耗系统资源,还会降低系统的稳定性,一不小心把系统搞崩了,就可以直接去财务那结帐了。

如果我们合理的使用线程池,则可以避免把系统搞崩的窘境。总得来说,使用线程池可以带来以下几个好处:

降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
增加线程的可管理型。线程是稀缺资源,使用线程池可以进行统一分配,调优和监控。

43、线程池的核心属性有哪些?

threadFactory(线程工厂):用于创建工作线程的工厂。

corePoolSize(核心线程数):当线程池运行的线程少于 corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。

workQueue(队列):用于保留任务并移交给工作线程的阻塞队列。

maximumPoolSize(最大线程数):线程池允许开启的最大线程数。

handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时。

keepAliveTime(保持存活时间):如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过 keepAliveTime 时会被终止。
可以看一下我的一片文章介绍了线程池
线程池

44、说下线程池的运作流程

在这里插入图片描述
线程池
在这里插入图片描述

45、线程池有哪些拒绝策略?

AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。

DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。

DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。

CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。

46、List、Set、Map三者的区别?

建议跟一下集合的源码
List(对付顺序的好帮手): List 接口存储一组不唯一(可以有多个元素引用相同的对象)、有序的对象。

Set(注重独一无二的性质):不允许重复的集合,不会有多个元素引用相同的对象。

Map(用Key来搜索的专业户): 使用键值对存储。Map 会维护与 Key 有关联的值。两个 Key可以引用相同的对象,但 Key 不能重复,典型的 Key 是String类型,但也可以是任何对象。

47、ArrayList 和 LinkedList 的区别。

答:

  • ArrayList:底层数据结构式数组,更适合查询多得场景。
  • LinkedList:底层数据结构式链表,更适合增加删除得场景。

对于按 index 索引数据(get/set方法):ArrayList 通过 index 直接定位到数组对应位置的节点,而 LinkedList需要从头结点或尾节点开始遍历,直到寻找到目标节点,因此在效率上 ArrayList 优于 LinkedList。

对于随机插入和删除:ArrayList 需要移动目标节点后面的节点(使用System.arraycopy 方法移动节点),而 LinkedList 只需修改目标节点前后节点的 next 或 prev 属性即可,因此在效率上 LinkedList 优于 ArrayList。

对于顺序插入和删除:由于 ArrayList 不需要移动节点,因此在效率上比 LinkedList 更好。这也是为什么在实际使用中 ArrayList 更多,因为大部分情况下我们的使用都是顺序插入。

48、ArrayList 和 Vector 的区别。

Vector 和 ArrayList 几乎一致,唯一的区别是 Vector 在方法上使用了 synchronized 来保证线程安全,因此在性能上 ArrayList 具有更好的表现。
在这里插入图片描述

有类似关系的还有:StringBuilder 和 StringBuffer、HashMap 和 Hashtable。

49、介绍下 HashMap 的底层数据结构

数组+链表+红黑树
在这里插入图片描述

50、为什么要改成“数组+链表+红黑树”?

答:
主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。

51、那在什么时候用链表?什么时候用红黑树?

答:
链表=》红黑树:对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后超过8个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。
同时满足两个条件才会进行扩容,1、数组长度>=64;2、链表长度大于8

红黑树=》链表:对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。

52、HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

答:
默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。

2的n次方原理:

对Table的表长度减一再与生产的hash值进行相与:

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

在这里插入图片描述
由上可得,使用反证法:
如果是15,那么-1后,得14,14的二进制,最后一位一直是0,0和任何数做相与运算都是都是0。那么导致的结果是散列不够完全

53、HashMap 的插入流程是怎么样的?

在这里插入图片描述

54、HashMap 的扩容(resize)流程是怎么样的?

在这里插入图片描述

55、除了 HashMap,还用过哪些 Map,在使用时怎么选择?

在这里插入图片描述

56、HashMap 和Hashtable 的区别?HashMap 允许 key 和 value 为 null,Hashtable 不允许。

HashMap 的默认初始容量为 16,Hashtable 为 11。

HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。

HashMap 是非线程安全的,Hashtable是线程安全的。

HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。

HashMap 去掉了 Hashtable 中的 contains 方法。

HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。

57、Java 内存结构(运行时数据区)

在这里插入图片描述

58、什么是双亲委派模型?

类加载器分类(从上至下)

引导类加载器:负责加载jre中lib下的核心类库
扩展类加载器:负责加载jre下lib下ext扩展目录中的类
应用类加载器:负责加载classPath下的,我们自己写类
自定义加载器:用户自定义下的类包
在这里插入图片描述

  • 第一种我们自己编写一个Test类加载过程如下:

系统刚启动时(所有类加载器什么都没有是空的,所以)
首先由自定义加载器开始加载=》再到应用类加载器=》再到扩展类加载器=》再到引导类加载器(一直向上委托,但是顶层加载器都没有,然后再向下由其子加载器加载)
再引导类。。=》扩展类。。=》应用类。。(到应用类加载器就可以开始加载了)
下次再次使用时就直接由对应的类加载器加载就行

  • 使用双亲委派机制的原因

1、沙箱安全机制:避免核心类库API被改动
2、避免类的重复加载:若父加载器已经加载了那么子加载器就没必要加载了,保证类的唯一性

目的就是:安全
像String类JRE环境的包,如果我们自己也定义了一个String类入侵的话,就不安全了。

59、Java虚拟机中有哪些类加载器?

见上题

60、类加载的过程

见58

61、介绍下垃圾收集机制(在什么时候,对什么,做了什么)?

答:
在什么时候?

在触发GC的时候,具体如下,这里只说常见的 Young GC 和 Full GC。

触发Young GC:当新生代中的 Eden 区没有足够空间进行分配时会触发Young GC。

触发Full GC:

当准备要触发一次Young GC时,如果发现统计数据说之前Young GC的平均晋升大小比目前老年代剩余的空间大,则不会触发Young GC而是转为触发Full GC。(通常情况)
如果有永久代的话,在永久代需要分配空间但已经没有足够空间时,也要触发一次Full GC。
System.gc()默认也是触发Full GC。
heap dump带GC默认也是触发Full GC。
CMS GC时出现Concurrent Mode Failure会导致一次Full GC的产生。
对什么?

对那些JVM认为已经“死掉”的对象。即从GC Root开始搜索,搜索不到的,并且经过一次筛选标记没有复活的对象。

做了什么?

对这些JVM认为已经“死掉”的对象进行垃圾收集,新生代使用复制算法,老年代使用标记-清除和标记-整理算法。

62、GC Root有哪些?

在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。

63、垃圾收集有哪些算法,各自的特点?

96、垃圾收集有哪些算法,各自的特点?
标记 – 清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

标记 – 整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。

一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。

写在后面:

首先感谢囧辉大佬的分享。
对于这些总结:
自己还需要做的事:

  • 1、java容器源码
  • 2、并发实战大哥李的书刷一遍
  • 3、JVM的课还是需要学一下

原文链接:https://blog.csdn.net/v123411739/article/details/115364158

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/96172.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!