Java面试之Java基础篇(offer 拿来吧你)

现在关于java面试的资料是层出不穷,对于选择困难症的同学来说,无疑是陷入了一次次的抉择与不安中,担心错过了关键内容,现在小曾哥秉持着"融百家之所长,汇精辟之文档"的思想,整理一下目前主流的一些八股文,以达到1+1 > 2 的效果!

Java特性篇

1、Java语言的特点

  • 简单易学;
  • 面向对象(封装,继承,多态);
  • 平台无关性( Java 虚拟机实现平台无关性);
  • 支持多线程( C++语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
  • 可靠性;
  • 安全性;
  • 支持网络编程并且很方便
  • 编译与解释并存;

2、 解释下什么是面向对象?面向对象和面向过程的区别?

面向对象是一种基于面向过程的编程思想,是向现实世界模型的自然延伸,这是一种”万物皆对象”的编程思想。由执行者变为指挥者,在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。面向对象的编程是以对象为中心以消息为驱动。
区别:
(1)编程思路不同:面向过程以实现功能的函数开发为主,用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,而面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
(2)封装性:都具有封装性,但是面向过程是封装的是功能,而面向对象封装的是数据和功能。
(3)面向对象具有继承性和多态性,而面向过程没有继承性和多态性,所以面向对象优势很明显

面向对象是以功能来划分问题,而不是步骤

3、面向对象的三大特性?分别解释下?

  • 封装:通常认为封装是把数据和操作数据的⽅法封装起来,对数据的访问只能通过已定义的接⼝。
  • 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为⽗类(超类/基类),得到继承信息的被称为⼦类(派⽣类)。
  • 多态:分为编译时多态(⽅法重载)和运⾏时多态(⽅法重写)。要实现多态需要做两件事:⼀是⼦类继承⽗类并重写父类中的⽅法,⼆是⽤⽗类型引⽤⼦类型对象,这样同样的引⽤调⽤同样的⽅法就会根据⼦类对象的不同⽽表现出不同的⾏为。

4、什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。

5、JDK和JRE的区别是什么?

  • JDK >> JRE >>JVM (>>代表包含)
  • JDK顾名思义是java开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。
  • 如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。

6、为什么Java代码可以实现一次编写、到处运行?

JVM(Java虚拟机)是Java跨平台的关键。

  • 在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
  • 同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。

注意事项
编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行;
跨平台的是Java程序、而不是JVM,JVM是用C/C++开发的软件,不同平台下需要安装不同版本的JVM。

机器码和字节码的区别:
机器码,完全依附硬件而存在~并且不同硬件由于内嵌指令集不同,即使相同的0 1代码 意思也可能是不同的
我们知道JAVA是跨平台的,为什么呢?因为他有一个jvm,不论那种硬件,只要你装有jvm,那么他就认识这个JAVA字节码~~~~至于底层的机器码,咱不用管,有jvm搞定,他会把字节码再翻译成所在机器认识的机器码~~

7、Java 和 C++ 的区别?

Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:

  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
  • C++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。

Java语法基础篇

1、continue、break 和 return 的区别是什么?

在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词:

  1. continue :指跳出当前的这一次循环,继续下一次循环。
  2. break :指跳出整个循环体,继续执行循环下面的语句。
  3. return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:return; :直接使用 return 结束方法执行,用于没有返回值函数的方法;return value;:return 一个特定值,用于有返回值函数的方法。

2、成员变量与局部变量的区别?

  • 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  • 存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  • 生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
  • 默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

3、说一说你对Java访问权限的了解?

java 提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围):

  • 公开级别:用public 修饰,对外公开
  • 受保护级别:用protected 修饰,对子类和同一个包中的类公开
  • 默认级别:default没有修饰符号,向同一个包的类公开.
  • 私有级别:用private 修饰,只有类本身可以访问,不对外公开.

4、重载和重写有什么区别?

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法

重载:发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写:就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。

“两同两小一大”:
“两同”即方法名相同、形参列表相同;
“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

5、为什么Java只有值传递? 传参机制是什么?

形参和实参
实参(实际参数) :用于传递给函数/方法的参数,必须有确定的值。【say(hello)】
形参(形式参数) :用于定义函数/方法,接收实参,不需要有确定的值。[say(String str)]

基本类型和引用类型

特点:
1、基本的数据类型,传递的是值(值拷贝),形参的任何改变不影响实参
2、引用数据类型(数组):引用类型传递的是地址(传递也是值,但是值是地址),可以通过形参影响实参!

基本数据类型
public static void main(String[] args) {
	int a = 10;
	int b = 20;
	//创建AA 对象名字obj
	AA obj = new AA();
	obj.swap(a, b); //调用swap
	System.out.println("main 方法a=" + a + " b=" + b);//a=10 b=20
}

class AA {
	public void swap(int a,int b){
		System.out.println("\na 和b 交换前的值\na=" + a + "\tb=" + b);//a=10 b=20
		//完成了a 和b 的交换
		int tmp = a;
		a = b;
		b = tmp;
		System.out.println("\na 和b 交换后的值\na=" + a + "\tb=" + b);//a=20 b=10
	}
}

输出结果:
a 和b 交换前的值 a=10 b=20
a 和b 交换后的值 a=20 b=10
main 方法 a=10 b=20
也充分证明了在基本数据类型中,形参的任何改变不会影响实参值

引用数据类型
public class bb {
    public static void main(String[] args) {
        //测试数组
        B b = new B();
        int[] arr = {1, 2, 3};
        b.test100(arr);//调用方法
        System.out.println(" main 的arr 数组");
        //遍历数组
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + "\t");
        }
        System.out.println();
		
		// 测试对象
		Person p = new Person();
        p.name = "jack";
        p.age = 10;
        b.test200(p);
        System.out.println("main 的p.age=" + p.age);

    }
}

class B {
	// 数组
    public void test100(int[] arr) {
        arr[0] = 200;//修改元素
        //遍历数组
        System.out.println(" test100 的arr 数组");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + "\t");
        }
        System.out.println();
    }
	// 对象
	public void test200(Person p) {
        p.age = 1000; //修改对象属性
        
        // 特例1
        //p = null;
        // 特例2
        //p = new Person();
		//p.name = "tom";
		//p.age = 99;
    }
}

输出结果:
test100的arr数组:[200,2,3]
main的arr数组:[200,2,3]
main的p.age=1000
分别从引用类型(数组和对象)的角度来举例,可以发现在引用数据类型中传递的是地址,可以通过形参影响实参!

如果大家对上述例子有所了解,下面再添加几个特例

特例1
p = null;

特例2
p = new Person();
p.name = "tom";
p.age = 99;

System.out.println("main 的p.age=" + p.age);

如果test200 执行的是p = null ,下面的结果是10
如果test200 执行的是p = new Person();…, 下面输出的是10
为什么会这样呢?
p = null 代表指向p的地址已经断开,不影响p对象的值
p = new Person() 代表指向了一个新的地址,无论怎么传递值,都不会影响p对象的值。
具体可以查看Java基础补充–查漏补缺(二)

6、Java支持的数据类型有哪些?什么是自动拆装箱?

Java语言支持的8种基本数据类型是: byte short int long float double boolean char

自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;

以Integer对象为例子:
Integer.parseInt(“”);是将字符串类型转换为int的基础数据类型
Integer.valueOf(“”)是将字符串类型数据转换为Integer对象
Integer.intValue();是将Integer对象中的数据取出,返回一个基础数据类型int

基本类型和包装类型的区别?

  • 成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
  • 包装类型可用于泛型,而基本类型不可以。
  • 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
  • 相比于对象类型, 基本数据类型占用的空间非常小。

7、int和Integer有什么区别,二者在做==运算时会得到什么结果?

int是基本数据类型,Integer是int的包装类。二者在做==运算时,Integer会自动拆箱为int类型,然后再进行比较。届时,如果两个int值相等则返回true,否则就返回false。

8、Java中,什么是构造方法?什么是构造方法重载?什么是复制构造方法?

当新对象被创建的时候,构造方法会被调用。每一个类都有构造方法。在程序员没有给类提供构造方法的情况下,Java编译器会为这个类创建一个默认的构造方法。

构造方法特点如下:

  • 名字与类名相同。
  • 没有返回值,但不能用 void 声明构造函数。
  • 生成类的对象时自动执行,无需调用。

构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名;Java中构造方法重载和方法重载很相似。可以为一个类创建多个构造方法。每一个构造方法必须有它自己唯一的参数列表。

9、Java支持多继承么?

Java中类不支持多继承,只支持单继承(即一个类只有一个父类)。 但是java中的接口支持多继承,即一个子接口可以有多个父接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,类就扩展了相应的功能)。

10、深拷贝和浅拷贝区别了解吗?什么是引用拷贝?

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
  • 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

11、Object 的常用方法有哪些?

clone方法:用于创建并返回当前对象的一份拷贝;
getClass方法:用于返回当前运行时对象的Class;
toString方法:返回对象的字符串表示形式; .
finalize方法:实例被垃圾回收器回收时触发的方法;
equals方法:用于比较两个对象的内存地址是否相等,一般需要重写;
hashCode方法:用于返回对象的哈希值;
notify方法:唤醒一个在此对象监视器上等待的线程。如果有多个线程在等待只会唤醒一一个。
notifyAll方法:作用跟notify() 一样,只不过会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
wait方法:让当前对象等待;

12、说一说hashCode()和equals()的关系?

hashCode()用于获取哈希码(散列码),eauqls()用于比较两个对象是否相等,它们应遵守如下规定:

  • 如果两个对象相等,则它们必须有相同的哈希码。
  • 如果两个对象有相同的哈希码,则它们未必相等。

为什么要重写hashCode()和equals()?

Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。

类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

==和equals()有什么区别?
对于基本数据类型来说,== 比较的是值。
对于引用数据类型来说,== 比较的是对象的内存地址。

13、String、StringBuffer、StringBuilder 的区别?

主要从可变性、线程安全性、性能三方面进行考虑:

  • 可变性:String 是不可变的(使用final关键字修饰字符数组来保存字符串),StringBuilder和StringBuffer非常类似,均代表可变的字符序列,而且方法也一样,都是继承 AbstractStringBuilder。
  • 线程安全性 :String 中的对象是不可变的,也就可以理解为常量,线程安全;StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
  • 性能:每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象;StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用;相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

    效率: StringBuilder > StringBuffer > String

String、StringBuffer 和StringBuilder 的选择

  • 1.如果字符串存在大量的修改操作,一般使用StringBuffer或StringBuilder
  • 2.如果字符串存在大量的修改操作,并在单线程的情况,使用StringBuilder
  • 3.如果字符串存在大量的修改操作,并在多线程的情况,使用StringBuffer
  • 4.如果我们字符串很少修改,被多个对象引用,使用String,比如配置信息等

操作少量的数据: 适用 String
单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

14、说一说你对字符串拼接的理解?

  • +运算符:如果拼接的都是字符串直接量,则适合使用 + 运算符实现拼接;
  • StringBuilder:如果拼接的字符串中包含变量,并不要求线程安全,则适合使用StringBuilder;
  • StringBuffer:如果拼接的字符串中包含变量,并且要求线程安全,则适合使用StringBuffer;
  • String类的concat方法:如果只是对两个字符串进行拼接,并且包含变量,则适合使用concat方法;

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

语法区别(构造方法、静态方法、普通成员变量、非抽象的普通方法、访问类型,继承)

1.抽象类可以有构造方法,接口中不能有构造方法。
2.抽象类中可以有普通成员变量,接口中没有普通成员变量
3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
4.抽象类中的抽象方法的访问类型可以是public,protected、但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
5.抽象类中可以包含静态方法,接口中不能包含静态方法;抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
7.一个类可以实现多个接口,但只能继承一个抽象类。

应用区别(系统架构、代码的重用):

接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用

异常篇

Java 异常类层次结构图概览 :

1、Exception 和 Error 有什么区别?

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

2、运行时异常(RuntimeException)包含哪些?

RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):

NullPointerException(空指针错误)
IllegalArgumentException(参数错误比如方法入参类型错误)
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
ArrayIndexOutOfBoundsException(数组越界错误)
ClassCastException(类型转换错误)
ArithmeticException(算术错误)
SecurityException (安全错误比如权限不够)
UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

3、try-catch-finally 如何使用?

  • try块 : 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块: 用于处理 try 捕获到的异常。
  • finally 块 : 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
} finally {
    System.out.println("Finally");
}
输出:
Try to do something
Catch Exception -> RuntimeException
Finally

1、不管有木有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return语句执行之后,返回之前执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前就已经确定了;
4、finally中如果包含return,那么程序将在这里返回,而不是try或catch中的return返回,返回值就不是try或catch中保存的返回值了。

4、异常使用有哪些需要注意的地方?

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
  • 抛出的异常信息一定要有意义。
  • 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException。
  • 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。

泛型

Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList persons = new ArrayList() 这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。

ArrayList<E> extends AbstractList<E>

优点:1,类型安全、2,消除强制类型转换、3,潜在的性能收益

注意:泛型只是提高了数据传输安全性,并没有改变程序运行的性能

1、什么是类型擦除?

类型擦除:泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如则会被转译成普通的Object类型,如果指定了上限如< T extends String >则类型参数就被替换成类型上限。

List<String> list = new ArrayList<String>()

、两个 String 其实只有第⼀个起作⽤,后⾯⼀个没什么卵⽤,只不过 JDK7 才开始⽀持 Listlist = new ArrayList<> 这种写法。
2、第⼀个 String 就是告诉编译器,List 中存储的是 String 对象,也就是起类型检查的作⽤,之后编译器会擦除泛型占位符,以保证兼容以前的代码。

2、项目中哪里用到了泛型?

  • 自定义接口通用返回结果 ***monResult 通过参数 T 可根据具体的返回类型动态指定结果的数据类型
  • 定义 Excel 处理类ExcelUtil 用于动态指定 Excel 导出的数据类型
  • 构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。

反射

1、什么是反射?

每个类都有一个Class对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的.class文件,该文件内容保存着Class对象。类加载相当于Class对象的加载,类在第一次使用时才动态加载到JVM中。也可以使用Class.forName(“***.mysql.jdbc.Driver”)这种方式来控制类的加载,该方法会返回一个Class对象。

具体来说,通过反射机制,我们可以实现如下的操作:

  • 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;
  • 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;
  • 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。

2、反射机制的优缺点?

  • 优点:运行期类型的判断,class.forName() 动态加载类,提⾼代码的灵活度;
  • 缺点:尽管反射⾮常强⼤,但也不能滥⽤;1、性能开销 :反射涉及了动态类型的解析,所以 JVM ⽆法对这些代码进⾏优化。2、安全限制 :使⽤反射技术要求程序必须在⼀个没有安全限制的环境中运⾏。如果⼀个程序必须在有安全限制的环境中运⾏,如 Applet,那么这就是个问题了;3、内部暴露(可能导致代码功能失调并破坏可移植性);

3、在实际项目中有哪些应用场景?

  • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
  • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。

Java序列化

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

  • 序列化: 将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程

序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

1、实际开发中有哪些用到序列化和反序列化的场景?

  • 1、对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 2、将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
  • 3、将对象存储到缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

2、序列化协议对应于 TCP/IP 4 层模型的哪一层?

我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢?

  • 应用层 、传输层 、网络层 、网络接口层

    表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?

因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。

集合类

1、Java中有哪些容器(集合类)?

2、ArrayList 与 LinkedList 区别?

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  • 底层数据结构: ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构
  • 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响
  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。
  • 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间

3、Arraylist 和 Vector 的区别?

  • ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
  • Vector 是 List 的古老实现类,底层使用 Object[ ]存储,线程安全的。

4、ArrayList的扩容机制

使用ArrayList()创建ArrayList对象时,不会定义底层数组的长度,当第一次调用add(E e) 方法时,初始化定义底层数组的长度为10,之后调用add(E e)时,如果需要扩容,则调用grow(int minCapacity) 进行扩容,长度为原来的1.5倍。

5、HashMap 和 HashSet 的区别

HashSet 底层就是基于 HashMap 实现的

6、HashMap 和 Hashtable 的区别

  • 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap );
  • 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点;
  • 对 Null key 和 Null value 的支持:HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
  • 初始容量大小和每次扩充容量大小的不同 :1、Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1;HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍;2、创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。
  • 底层数据结构:HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。

7、HashMap底层实现原理

HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

JDK1.7之前:数组 + 链表

现在JDK1.8之后:数组+链表+红黑树进行数据的存储,当链表上的元素个数超过 8 个并且数组⻓度 >= 64 时⾃动转化成红⿊树,节点变成树节点,以提⾼搜索效率和插⼊效率到 O(logN)。

红黑树特点:
1、每个节点或者是黑色,或者是红色。
2、根节点是黑色。
3、每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
4、如果一个节点是红色的,则它的子节点必须是黑色的。
5、从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。[这里指到叶子节点的路径]包含n个内部节点的红黑树的高度是 O(log(n)).

优点:红黑树是一种平衡树,他复杂的定义和规则都是为了保证树的平衡性。如果树不保证他的平衡性就是下图:很显然这就变成一个链表了。

8、HashMap为什么用红黑树,而不是用B+树、AVL树?

红黑树 和 AVL树区别
  • 两者都是平衡二叉树,但是红黑树不要求所有节点高度差不超过1,只要求1给节点到所有节点的路径中,最长路径不能超过最短路径的两倍,所以红黑树保持的是大致平衡
  • 在进行插入或者删除元素后,平衡二叉树的旋转操作 消耗高于红黑树(省略了没有必要的调整)
  • 红黑树的插入、删除的效率高于AVL树
红黑树 和 B+树区别

1、如果用B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面。这个时候遍历效率就退化成了链表
2、B和B+树主要用于数据存储在磁盘上的场景,比如数据库索引就是用B+树实现的;而红黑树多用于内存中排序,也就是内部排序。

9、HashMap实现存储和读取?

Put方法的执行过程
  • 首先判断数组是否为空,如果是,则进行初始化。
  • 其次,根据**(n - 1) &hash**求出要添加对象所在的索引位置,判断此索引的内容是否为空,如果是,则直接存储。
  • 如果不是,则判断索引位置的对象和要存储的对象是否相同,首先判断hash值知否相等,在判断key是否相等。(1.两个对象的hash值不同,一定不是同一个对象。2.hash值相同,两个对象也不一定相等)。
  • 如果是同一个对象,则直接进行覆盖,返回原值。
  • 如果不是,则判断是否为树节点对象,如果是,直接添加
  • 当既不是相同对象,又不是树节点,直接将其插入到链表的尾部。在进行判断是否需要进行树化。
  • 最后,判断hashmap的size是否达到阈值,进行扩容resize()处理。
Get方法的执行过程

通过 key 的 hash 值找到在 table 数组中的索引处的 Entry,然后返回该 key 对应的 value 即可。

在这⾥能够根据 key 快速的取到 value 除了和 HashMap 的数据结构密不可分外,还和 Entry 有莫大的关系。HashMap 在存储过程中并没有将 key,value 分开来存储,⽽是当做⼀个整体 key-value 来处理的,这个整体就是Entry 对象。同时 value 也只相当于 key 的附属⽽已。在存储的过程中,系统根据 key 的 HashCode 来决定 Entry 在 table 数组中的存储位置,在取的过程中同样根据 key 的 HashCode 取出相对应的 Entry 对象(value 就包含在
⾥⾯)

10、HashMap的扩容机制?

有两种情况会调用resize 方法:
1.第一次调用HashMap的put方法时,会调用resize方法对table数组进行初始化,如果不传入指定值,默认大小为16。
2.扩容时会调用resize,即size > threshold时,table 数组大小翻倍。

11、HashMap 的 size 为什么必须是 2 的整数次方?

为了快速运算出键值对存储的索引和让键值对均匀分布

首先计算键值对的索引要满足两个要求:不能越界、均匀分布;而 h % length (h根据key计算出来的哈希值)就能满足这一点,但是取模运算速度较慢。
容量为2的次幂时、而 h & (length-1)刚好也能满足,而且按位与运算速度很快。

12、HashTable 和 ConcurrentHashMap 的区别?

HashMap 与 ConcurrentHashMap的区别是:HashMap不是线程安全的,ConcurrentHashMap是线程安全的。

HashTable 和 ConcurrentHashMap 相⽐,效率低。Hashtable 之所以效率低主要是使⽤了 synchronized 关键字对 put 等操作进⾏加锁,而 synchronized 关键字加锁是对整张 Hash 表的,即每次锁住整张表让线程独占,致使效率低下;ConcurrentHashMap 在对象中保存了⼀个 Segment 数组,即将整个 Hash 表划分为多个分段;而每个Segment元素,即每个分段则类似于⼀个Hashtable;在执行 put 操作时⾸先根据 hash 算法定位到元素属于哪个 Segment,然后对该 Segment 加锁即可,因此,ConcurrentHashMap 在多线程并发编程中可是实现多线程 put操作。

13、Iterator 怎么使用?有什么特点?

迭代器是⼀种设计模式,它是⼀个对象,它可以遍历并选择序列中的对象,⽽开发⼈员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价⼩。Java 中的 Iterator 功能⽐较简单,并且只能单向移动:

  1. 使⽤⽅法 iterator() 要求容器返回⼀个 Iterator。第⼀次调⽤ Iterator 的 next() ⽅法时,它返回序列的第⼀个元素。注意:iterator() ⽅法是 java.lang.Iterable 接⼝,被 Collection 继承。
  2. 使⽤ next() 获得序列中的下⼀个元素。
  3. 使⽤ hasNext() 检查序列中是否还有元素。
  4. 使⽤ remove() 将迭代器新返回的元素删除。

14、Iterator 和 Enumeration 接口的区别?

与 Enumeration 相⽐,Iterator 更加安全,因为当⼀个集合正在被遍历的时候,它会阻⽌其它线程去修改集合。否则会抛出 ConcurrentModificationException 异常。

  1. Iterator 的⽅法名比 Enumeration 更科学;
  2. Iterator 有 fail-fast 机制,⽐ Enumeration 更安全;
  3. Iterator 能够删除元素,Enumeration 并不能删除元素。

快速失败机制是java集合的一种错误检测机制,当迭代集合时集合的结构发生改变,就会产生fail-fast机制。
一旦发现遍历的同时,其他人来修改,就立刻抛出异常。fail_salf:当遍历的时候,其他人来修改,应当有相应的策略,例如牺牲一致性来遍历整个数组。

IO

1、什么是IO?

I/O(Input/Outpu) 即输入/输出 。

输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。

为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ); 用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间

常见的 IO 模型?
UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。

2、Java 中 3 种常见 IO 模型

BIO (Blocking I/O)

BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO (Non-blocking/New I/O)

NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。

Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。

但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。

IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

AIO (Asynchronous I/O)

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

总结:简单总结一下 Java 中的 BIO、NIO、AIO

3、介绍一下Java中的IO流

IO(Input Output)用于实现对数据的输入与输出操作,Java把不同的输入/输出源(键盘、文件、网络等)抽象表述为流(Stream)。流是从起源到接收的有序数据,有了它程序就可以采用同一方式访问不同的输入/输出源。

  • 按照数据流向,可以将流分为输入流和输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。
  • 按照数据类型,可以将流分为字节流和字符流,其中字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。
  • 按照处理功能,可以将流分为节点流和处理流,其中节点流可以直接从/向一个特定的IO设备(磁盘、网络等)读/写数据,也称为低级流,而处理流是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也称为高级流。

黑色字体的是抽象基类,其他所有的类都继承自它们。红色字体的是节点流,蓝色字体的是处理流。

4、Java IO涉及的设计模式有哪些?

装饰器模式

装饰器(Decorator)模式 可以在不改变原有对象的情况下拓展其功能
装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
对于字节流来说, FilterInputStream (对应输入流)和FilterOutputStream(对应输出流)是装饰器模式的核心,分别用于增强 InputStream 和OutputStream子类对象的功能。

我们常见的BufferedInputStream(字节缓冲输入流)、DataInputStream 等等都是FilterInputStream 的子类,BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。

装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口

适配器模式

适配器(Adapter Pattern)模式主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。

适配器模式中存在被适配的对象或者类称为 适配者(Adaptee) ,作用于适配者的对象或者类称为适配器(Adapter) 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。

适配器模式和装饰器模式有什么区别呢?

装饰器模式 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且,装饰器模式支持对原始类嵌套使用多个装饰器。

适配器模式 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。

适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。

并发编程

1、什么是线程和进程?

进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程:是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
区别: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

程序计数器为什么是私有的?
1、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
2、在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

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

  • 继承Thread类创建线程;
  • 实现Runnable接口创建线程;
  • 通过Callable和Future创建线程;
  • 通过线程池创建线程。

2、并发与并行的区别

  • 并发:两个及两个以上的作业在同一 时间段 内执行。
  • 并行:两个及两个以上的作业在同一 时刻 执行。

3、同步和异步的区别

  • 同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
  • 异步:调用在发出之后,不用等待返回结果,该调用直接返回。

4、为什么要使用多线程呢?

  • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销
  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能

5、使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

6、说说线程的生命周期和状态?

在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。

  • 新建(New):当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。
  • 就绪(Ready):当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。
  • 运行(Running):如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。
  • 阻塞(Blocked):当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。

发生以下情况,将会进入阻塞状态
线程调用sleep()方法主动放弃所占用的处理器资源。
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
线程在等待某个通知(notify)。
程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

解除阻塞:
调用sleep()方法的线程经过了指定时间。
线程调用的阻塞式IO方法已经返回。
线程成功地获得了试图取得的同步监视器。
线程正在等待某个通知时,其他线程发出了一个通知。
处于挂起状态的线程被调用了resume()恢复方法。

7、什么是线程死锁?如何避免死锁?

线程死锁是指由于两个或多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

死锁的四个必要条件:
  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放.
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
如何预防和避免线程死锁?

如何预防死锁? 破坏死锁的产生的必要条件即可:

  • 破坏请求与保持条件 :一次性申请所有的资源。
  • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

8、sleep() 方法和 wait() 方法对比

共同点 :两者都可以暂停线程的执行。

区别 :

  • sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  • wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

为什么 wait() 方法不定义在 Thread 中?
wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。
sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

9、Java多线程之间的通信方式

  1. wait()、notify()、notifyAll(): 如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信(Object类);wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
  2. await()、signal()、signalAll() : 如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信(Condition接口)。
  3. BlockingQueue : 当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。

10、synchronized与Lock的区别

  1. synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。
  2. synchronized可以用在代码块上、方法上;Lock只能写在代码里
  3. synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally中显示释放锁。
  4. synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间。
  5. synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。
  6. synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。

11、在 Java 程序中怎么保证多线程的运行安全?

线程安全在三个方面体现:

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic, synchronized);
  • 可见性: 一个线程对主内存的修改可以及时地被其他线程看到,(synchronized、 volatile) ;
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序, (happens- before原则)。

volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序

12、Java 线程同步的几种方法?

  1. 使用Synchronized 关键字;
  2. wait 和 notify;
  3. 使用特殊域变量 volatile 实现线程同步;
  4. 使用可重⼊锁实现线程同步;
  5. 使用阻塞队列实现线程同步;
  6. 使用信号量 Semaphore。

13、谈谈对ThreadLocal的理解?

ThreadLocal 有什么用?

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?

ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

ThreadLocal的实现原理?

Thread类中有个变量threadLocals,它的类型为ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,就是一个普通的Java类,但是实现的类似map的功能。每个线程都有自己的一个map,map是一个数组的数据结构存储数据,每个元素是一个Entry,entry的key是ThreadLocal的引用,也就是当前变量的副本,value就是set的值。

原文链接:https://blog.csdn.***/fengxi_tan/article/details/106629280
原文链接:https://blog.csdn.***/qq_37258531/article/details/122350750
guide哥:https://javaguide.***/database/mysql/mysql-questions-01.html
牛客网:https://www.nowcoder.***/tutorial/94/e07fdcfc369c49e8a95ea23de51d58b5
帅地玩编程-- Java面试必知必会

转载请说明出处内容投诉
CSS教程_站长资源网 » Java面试之Java基础篇(offer 拿来吧你)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买