一、Java基础

1.Java语言具有那些特点?
  1. Java为纯面向对象的语言。它能够直接反映现实生活中的对象
  2. 具有平台无关性。java利用Java虚拟机运行字节码,无论是在Windows、linux还是MacOS等其他平台对Java程序进行编译,编译后的程序可以在其他平台运行
  3. Java为解释性语言,编码器把Java代码编译成平台无关的中间代码,然后在JVM上解释运行,具有很好的可移植性
  4. Java提供了很多内置类库。如对多线程支持,对网络通信支持,最重要的一点是提供了垃圾回收器
  5. Java具有较好的安全性和健壮性。Java提供了异常处理和垃圾回收机制,去除了C++中难以理解的指针特性
  6. Java语言提供了对Web应用开发的支持
2.面向对象的三大特性?
  1. 继承:一个新类可以从现有的类中派生,派生类可以从它的基类那里继承方法和实例变量,且派生类可以修改或新增新的方法使之更适合特殊的需求。
  2. 封装:将客观事务抽象成类,每个类可以把自身的数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏
  3. 多态:指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为。且上转型后子类方法覆盖父类方法呈现多态。
3.字节序定义以及Java属于哪种字节序?

字节序是指多字节数据在计算机内存中存储或网络传输时各字节的存储顺序。通常由小端和大端两种方式。

  1. 小端:低位字节存放在内存的低地址端,高位字节存放在内存的高地址端
  2. 大端:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端

Java语言的字节序是大端

4.JDK与JRE有什么区别?

JDK:Java开发工具包(Java Development Kit),提供了Java的开发环境和运行环境
JRE:Java运行环境(Java Runtime Environment),提供了Java运行所需的环境

JDK包含了JRE。如果只运行Java程序,安装JRE即可。要编写Java程序需安装JDK

5.简述Java访问修饰符

default:默认访问修饰符,在同一包内可见
private:在同一类内可见,不能修饰类
protected:对同一包内的类和所有子类可见,不能修饰类
public:对所有类可见

6.构造方法、成员变量初始化以及静态成员变量三者的初始化顺序?

先后顺序:静态成员变量、成员变量、构造方法
详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数

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

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

7.接口和抽象类的相同点和区别?

相同点:
1.都不能被实例化
2.接口的实现类或抽象类的子类需实现接口或抽象类中相应的方法才能被实例化

不同点:
1.接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现
2.实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但只能继承一个抽象类
3.当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的累积。当功能不需要,希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。

8.为什么Java语言不支持多重继承?
  1. 为了程序的结构能够更加清晰从而便于维护。假设Java语言支持多重继承,类C继承自类A和类B,如果类A和B都有自定义的成员方法f(),那么当代码中调用类C的f()会产生二义性。Java语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不能有方法的实现,类C继承接口A与接口B时即使他们都有方法f(),也不能直接调用方法,需实现具体的f()方法才能调用,不会产生二义性。
  2. 多重继承会使类型转换、构造方法的调用顺序变得复杂,会影响到性能。
9.Java提供的多态机制?

Java提供了两种用于多态的机制,分别是重载与覆盖
1.重载:重载是指同一个类中有多个同名的方法,但这些方法有着不同的参数,因此在编译时就可以确定到底调用哪个方法,它是一种编译时多态。重载可以被看作一个类中的方法多态性。
2.覆盖:子类可以覆盖父类的方法,因此同样的方法会在父类与子类中有着不同的表现形式。在 Java 语言中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象。同样,接口的引用变量也可以指向其实现类的实例对象,而程序调用的方法在运行期才动态绑定(绑定指的是将一个方法调用和一个方法主体连接到一起),就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。通过这种动态绑定的方法实现了多态。由于只有在运行时才能确定调用哪个方法,因此通过方法覆盖实现的多态也可以被称为运行时多态。

10.重载与覆盖的区别?
  1. 覆盖是父类与子类之间的关系,是垂直关系;重载是同一类中方法之间的关系,是水平关系
  2. 覆盖只能由一个方法或一对方法产生关系;重载是多个方法之间的关系
  3. 覆盖要求参数列表相同;重载要求参数列表不同
  4. 覆盖中,调用方法体是根据对象的类型来决定的,而重载是根据调用时实参表与形参表来对应选择方法体
  5. 重载方法可以改变返回值的类型;覆盖方法不能改变返回值的类型
11.final、finally和finalize的区别是什么?
  1. final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。
  2. finally作为异常处理的一部分,只能在try/catch语句中使用,finally附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下。
  3. finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的finalize()方法。当垃圾回收器准备好释放对象占用空间时,首先会调用finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存。
12.出现在Java程序中的finally代码块是否一定会执行?

当遇到下面情况不会执行。
1.当程序在进入try语句块之前就出现异常时会直接结束。
2.当程序在try块中强制退出时,如使用System.exit(0),也不会执行finally块中的代码。

其它情况下,在try/catch/finally语句执行的时候,try块先执行,当有异常发生,catch和finally进行处理后程序就结束了,当没有异常发生,在执行完finally中的代码后,后面代码会继续执行。值得注意的是,当try/catch语句块中有return时,finally语句块中的代码会在return之前执行。如果try/catch/finally块中都有return语句,finally块中的return语句会覆盖try/catch模块中的return语句。

13.Java语言中关键字static的作用是什么?

static的主要作用有两个:
1.为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间。
2.使得某个方法或属性与类关联在一起而不是对象,在不创建对象的情况下可通过类直接调用方法或使用类的属性。

具体而言static又可分为4种使用方式:
1.修饰成员变量。用static关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用’‘类.静态变量’‘和’‘对象.静态变量’'的方法使用。
2.修饰成员方法。static修饰的方法无需创建对象就可以被调用。static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态成员方法。
3.修饰代码块。JVM在加载类的时候会执行static代码块。static代码块常用于初始化静态变量。static代码块只会被执行一次。
4.修饰内部类。static内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。

14.Java代码块执行顺序
  1. 父类静态代码块(只执行一次)
  2. 子类静态代码块(只执行一次)
  3. 父类构造代码块
  4. 父类构造函数
  5. 子类构造代码块
  6. 子类构造函数
  7. 普通代码块
15.Java中一维数组和二维数组的声明方式?

一维数组的声明方式:
1.type arrayName[]
2.type[] arrayName

二维数组的声明方式:
1.type arrayName[][]
2.type[][] arrayName
3.type[] arrayName[]

其中type为基本数据类型或类,arrayName为数组名字

16.String和StringBuffer有什么区别?

String用于字符串操作,属于不可变类。String对象一旦被创建,其值将不能被改变。而StringBuffer是可变类,当对象创建后,仍然可以对其值进行修改。

17.判等运算符==与equals的区别?

== 比较的是引用,equals比较的是内容。

  1. 如果变量是基础数据类型,== 用于比较其对应值是否相等。如果变量指向的是对象,== 用于比较两个对象是否指向同一块存储空间。
  2. equals是Object类提供的方法之一,每个Java类都继承自Object类,所以每个对象都具有equals这个方法。Object类中定义的equals方法内部是直接调用 == 比较对象的。但通过覆盖的方法可以让它不是比较引用而是比较数据内容。需保证equals方法相同对应的对象hashCode也相同。
18.为什么要把String设计为不变量?
  1. 节省空间:字符串常量存储在JVM的字符串池中可以被用户共享。
  2. 提高效率:String会被不同线程共享,是线程安全的。在涉及多线程操作中不需要同步操作。
  3. 安全:String常被用于用户名、密码、文件名等使用,由于其不可变,可避免黑客行为对其恶意修改。
19.序列化是什么?

序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造成一个相同的对象。

20.简述Java中Class对象

java中对象可以分为实例对象和Class对象,每一个类都有一个Class对象,其包含了与该类有关的信息。
获取Class对象的方法:

  • Class.forName(“类的全限定名”)
  • 实例对象.getClass()
  • 类名.class
21.Java反射机制是什么?

Java反射机制是指在程序的运行过程中可以构造任意一个类的对象、获取任意一个类的成员变量和成员方法、获取任意一个对象所属的类信息、调用任意一个对象的属性和方法。反射机制使得Java具有动态获取程序信息和动态调用对象方法的能力。可以通过以下类调用反射API。

  • Class类:可获得类属性方法
  • Field类:获得类的成员变量
  • Method类:获取类的方法信息
  • Construct类:获取类的构造方法等信息
22.简述注解

Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。
其可以用于提供信息给编译器,在编译阶段时给软件提供信息进行相关的处理,在运行时处理相应代码,做对应操作。

23.简述元注解

元注解可以理解为注解的注解,即在注解中使用,实现想要的功能。其具体分为:

  • @Retention: 表示注解存在阶段是保留在源码,还是在字节码(类加载)或者运行期(JVM中运行)。
  • @Target:表示注解作用的范围。
  • @Documented:将注解中的元素包含到 Javadoc 中去。
  • @Inherited:一个被@Inherited注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。
  • @Repeatable:被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。
24.简述Java异常的分类

Java异常分为Error(程序无法处理的错误),和Exception(程序本身可以处理的异常)。这两个类均继承Throwable。
Error常见的有StackOverFlowError,OutOfMemoryError等等。
Exception可分为运行时异常和非运行时异常。对于运行时异常,可以利用try catch的方式进行处理,也可以不处理。对于非运行时异常,必须处理,不处理的话程序无法通过编译。

25.简述throw与throws的区别

throw一般是用在方法体的内部,由开发者定义当程序语句出现问题后主动抛出一个异常。
throws一般用于方法声明上,代表该方法可能会抛出的异常列表。

26.简述泛型

泛型,即“参数化类型”,解决不确定对象具体类型的问题。在编译阶段有效。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型在类中称为泛型类、接口中称为泛型接口和方法中称为泛型方法。

27.简述泛型擦除

Java编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理时被擦除,这个过程被称为泛型擦除。

28.简述Java基本数据类型
  • byte: 占用1个字节,取值范围-128 ~ 127
  • short: 占用2个字节,取值范围-215 ~ 215-1
  • int:占用4个字节,取值范围-2^31 ~ 2^31-1
  • long:占用8个字节
  • float:占用4个字节
  • double:占用8个字节
  • char: 占用2个字节
  • boolean:占用大小根据实现虚拟机不同有所差异
29.简述自动装箱拆箱

对于Java基本数据类型,均对应一个包装类。
装箱就是自动将基本数据类型转换为包装器类型,如int->Integer
拆箱就是自动将包装器类型转换为基本数据类型,如Integer->int

30.简述重载与重写的区别

重写即子类重写父类的方法,方法对应的形参和返回值类型都不能变。
重载即在一个类中,方法名相同,参数类型或数量不同。

31.简述java的多态

Java多态可以分为编译时多态和运行时多态。
编译时多态主要指方法的重载,即通过参数列表的不同来区分不同的方法。
运行时多态主要指继承父类和实现接口时,可使用父类引用指向子类对象。

运行时多态的实现:主要依靠方法表,方法表中最先存放的是Object类的方法,接下来是该类的父类的方法,最后是该类本身的方法。如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法。因此可以实现运行时多态。

32.简述抽象类与接口的区别

抽象类:体现的是is-a的关系,如对于man is a person,就可以将person定义为抽象类。
接口:体现的是can的关系。是作为模板实现的。如设置接口fly,plane类和bird类均可实现该接口。
一个类只能继承一个抽象类,但可以实现多个接口。

33.简述Object类常用方法
  1. hashCode:通过对象计算出的散列码。用于map型或equals方法。需要保证同一个对象多次调用该方法,总返回相同的整型值。
  2. equals:判断两个对象是否一致。需保证equals方法相同对应的对象hashCode也相同。
  3. toString: 用字符串表示该对象
  4. clone:深拷贝一个对象
34.简述内部类及其作用
  • 成员内部类:作为成员对象的内部类。可以访问private及以上外部类的属性和方法。外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。外部类也可访问private修饰的内部类属性。
  • 局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的final变量。
  • 匿名内部类:只能使用一次,没有类名,只能访问外部类的final变量。
  • 静态内部类:类似类的静态成员变量。
35.简述String/StringBuffer与StringBuilder

String类采用利用final修饰的字符数组进行字符串保存,因此不可变。如果对String类型对象修改,需要新建对象,将老字符和新增加的字符一并存进去。
StringBuilder,采用无final修饰的字符数组进行保存,因此可变。但线程不安全。
StringBuffer,采用无final修饰的字符数组进行保存,可理解为实现线程安全的StringBuilder。

36.简述Java序列化与反序列化的实现

序列化:将java对象转化为字节序列,由此可以通过网络对象进行传输。
反序列化:将字节序列转化为java对象。
具体实现:实现Serializable接口,或实现Externalizable接口中的writeExternal()与readExternal()方法。

37.简述JAVA的List

List是一个有序队列,在JAVA中有两种实现方式:
ArrayList 使用数组实现,是容量可变的非线程安全列表,随机访问快,集合扩容时会创建更大的数组,把原有数组复制到新数组。
LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。

38.Java中线程安全的基本数据结构有哪些

HashTable: 哈希表的线程安全版,效率低
ConcurrentHashMap:哈希表的线程安全版,效率高,用于替代HashTable
Vector:线程安全版Arraylist
Stack:线程安全版栈
BlockingQueue及其子类:线程安全版队列

39.简述JAVA的Set

Set 即集合,该数据结构不允许元素重复且无序。JAVA对Set有三种实现方式:
1.HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,Value系统自定义一个名为PRESENT 的 Object 类型常量。判断元素是否相同时,先比较hashCode,相同后再利用equals比较,查询O(1)
2.LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。
3.TreeSet 通过 TreeMap 实现的,底层数据结构是红黑树,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。查询O(logn)

40.简述JAVA的HashMap

JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树。主要成员变量包括存储数据的table 数组、元素数量 size、加载因子 loadFactor。
HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的hash 值一样,就会发生哈希冲突,被放到同一个链表上。
table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。在JDK8后链表超过8会转化为红黑树。

若当前数据/总数据容量>负载因子,Hashmap将执行扩容操作。
默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。

41.为何HashMap线程不安全

在JDK1.7中,HashMap采用头插法插入元素,因此并发情况下会导致环形链表,产生死循环。
虽然JDK1.8采用了尾插法解决了这个问题,但是并发下的put操作也会使前一个key被后一个key覆盖。由于HashMap有扩容机制存在,也存在A线程进行扩容后,B线程执行get方法出现失误的情况。

42.简述java的TreeMap

TreeMap是底层利用红黑树实现的Map结构,底层实现是一棵平衡的排序二叉树,由于红黑树的插入、删除、遍历时间复杂度都为O(logN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树可以按照键的值的大小有序输出。

43.Collection和Collections有什么区别?
  1. Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口,所有集合都是它的子类,比如List、Set等。
  2. Collections是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法:
    Collections.sort(list);提供的反转方法:Collections.reverse(list)。
44.ArrayList、Vector和LinkedList有什么共同点与区别?
  1. ArrayList、Vector和LinkedList都是可伸缩的数组,即可以动态改变长度的数组。
  2. ArrayList和Vector都是基于存储元素的Object[] array来实现的,它们会在内存中开辟一块连续的空间来存储,支持下标、索引访问。但在涉及插入元素时可能需要移动容器中的元素,插入效率较低。当存储元素超过容器的初始化容量大小,ArrayList与Vector均会进行扩容。
  3. Vector是线程安全的,其大部分方法是直接或间接同步的。ArrayList不是线程安全的,其方法不具有同步性质。LinkedList也不是线程安全的。
  4. LinkedList采用双向链表实现,对数据索引需要从头开始遍历,因此随机访问效率较低,但在插入元素的时候不需要对数据进行移动,插入效率较高。
45.HashMap和Hashtable有什么区别?
  1. HashMap是Hashtable的轻量级实现,HashMap允许key和value为null,但最多允许一条记录的key为null.而HashTable不允许。
  2. HashTable中的方法是线程安全的,而HashMap不是。在多线程访问HashMap需要提供额外的同步机制。
  3. Hashtable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。
46.如何决定使用HashMap还是TreeMap?

如果对Map进行插入、删除或定位一个元素的操作更频繁,HashMap是更好的选择。如果需要对key集合进行有序的遍历,TreeMap是更好的选择。

47.fail-fast和fail-safe迭代器的区别是什么?
  1. fail-fast直接在容器上进行,在遍历过程中,一旦发现容器中的数据被修改,就会立刻抛出ConcurrentModificationException异常从而导致遍历失败。常见的使用fail-fast方式的容器有HashMap和ArrayList等。
  2. fail-safe这种遍历基于容器的一个克隆。因此对容器中的内容修改不影响遍历。常见的使用fail-safe方式遍历的容器有ConcurrentHashMap和CopyOnWriteArrayList。
48.HashSet中,equals与hashCode之间的关系?

equals和hashCode这两个方法都是从object类中继承过来的,equals主要用于判断对象的内存地址引用是否是同一个地址;hashCode根据定义的哈希规则将对象的内存地址转换为一个哈希码。HashSet中存储的元素是不能重复的,主要通过hashCode与equals两个方法来判断存储的对象是否相同:
1.如果两个对象的hashCode值不同,说明两个对象不相同。
2.如果两个对象的hashCode值相同,接着会调用对象的equals方法,如果equlas方法的返回结果为true,那么说明两个对象相同,否则不相同。

50.拆箱装箱原理

装箱过程是通过调用包装器的valueOf方法实现的,将原值赋给对应类。
拆箱过程是通过调用包装器的 intValue/doubleValue等方法实现,返回基本的数据类型。

51.java反射原理

Java会在编译期装载所有的类,并将其元信息保存至Class类对象中。
因此可以设计x.class/x.getClass()/Class.forName()等方法获取Class对象。所以在反射调用Field/Method/Constructor对象时,可根据Class类对象进行进一步操作。

52.Comparator和Comparable的区别

Comparable 是一个接口,用于对象内部的比较方式,该接口需要实现的方法是:

public interface Comparable<T> {
    public int compareTo(T o);
}

AI运行代码

  • 1
  • 2
  • 3

Comparator 也是一个接口,该接口有个compare方法,该接口需要实现的方法是:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

AI运行代码

  • 1
  • 2
  • 3

除该方法外,comparator还可以实现其他方法。

53.动态代理实现方式
  1. 利用JDK反射机制,实现代理接口
  2. 利用CGLib,对指定类生成子类,进行代理。
54.简述OOM(out of memory)

当JVM分配内存不够会抛出out of memory异常

55.简述StackOverFlowError

调用栈深度超过限制产生的异常。
一般会在递归调用时出现。

二、Java多线程
1.简述java内存模型(JMM)

java内存模型定义了程序中各种变量的访问规则。其规定所有变量都存储在主内存,线程均有自己的工作内存。
工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。

2.简述as-if-serial

编译器等会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果和用户原始程序输出预定结果一致。

3.简述happens-before八大原则
  1. 程序顺序原则。即在一个线程内必须保证语义的串行性(as-if-serial),也就是说按照代码顺序执行;
  2. 锁原则。解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,在一个锁被解锁后,再加锁,那么加锁的动作必须在解锁动作之后(操作同一个锁);
  3. volatile原则。volatile变量的写,发生于读之前,这样保证了volatile变量的可见性。简单理解就是volatile变量在每次被线程访问时,都强迫于从主内存中读取该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时候,不同的线程总能看到该变量的最新值;
  4. 线程启动原则。线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见;
  5. 传递性原则。A先于B,B先于C,那么A必然先于C;
  6. 线程终止原则。线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B在终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见;
  7. 线程中断原则。对线程interrupt()方法的调用先发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断;
  8. 对象的终结原则。对象的构造函数执行,结束先于finalize方法。
4.as-if-serial 和 happens-before 的区别

as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。

5.简述原子性操作

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。

6.简述线程的可见性

可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile,synchronized,final都能保证可见性。

7.简述有序性

即虽然多线程存在并发和指令优化等操作,在本线程内观察该线程的所有执行操作是有序的。

8.简述java中volatile关键字作用
  1. 保证变量对所有线程的可见性。
    当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。

  2. 禁止指令重排序优化。使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前。

9.java线程的实现方式
  1. 实现Runnable接口
  2. 继承Thread类。
  3. 实现Callable接口
10.简述java线程的状态

线程状态有New, RUNNABLE, BLOCK, WAITING, TIMED_WAITING, THERMINATED

NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。
RUNNABLE: 运行状态。其表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。
BLOCKED:阻塞状态。线程等待获取锁,锁还没获得。
WAITING: 等待状态。线程内run方法运行完语句Object.wait()/Thread.join()进入该状态。
TIMED_WAITING:限期等待。在一定时间之后跳出状态。调用Thread.sleep(long) Object.wait(long)
Thread.join(long)进入状态。其中这些参数代表等待的时间。
TERMINATED:结束状态。线程调用完run方法进入该状态。

11.简述线程通信的方式
  1. volatile 关键词修饰变量,保证所有线程对变量访问的可见性。
  2. synchronized关键词。确保多个线程在同一时刻只能有一个处于方法或同步块中。
  3. wait/notify方法
  4. IO通信
12.简述线程池

没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后执行接下来任务,复用已创建的线程,降低开销、控制最大并发数。
线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。
将任务派发给线程池时,会出现以下几种情况
1.核心线程池未满,创建一个新的线程执行任务。
2.如果核心线程池已满,工作队列未满,将线程存储在工作队列。
3.如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。
4.如果超过最大线程数,按照拒绝策略来处理任务。

13.线程池参数
  1. corePoolSize:常驻核心线程数。超过该值后如果线程空闲会被销毁。
  2. maximumPoolSize:线程池能够容纳同时执行的线程最大数。
  3. keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。
  4. workQueue:工作队列。
  5. threadFactory:线程工厂,用来生产一组相同任务的线程。
  6. handler:拒绝策略。有以下几种拒绝策略:
  • AbortPolicy:丢弃任务并抛出异常
  • CallerRunsPolicy: 重新尝试提交该任务
  • DiscardOldestPolicy:抛弃队列里等待最久的任务并把当前任务加入队列
  • DiscardPolicy:表示直接抛弃当前任务但不抛出异常。
14.线程池创建方法
  1. newFixedThreadPool,创建固定大小的线程池。
  2. newSingleThreadExecutor,使用单线程线程池。
  3. newCachedThreadPool,maximumPoolSize 设置为 Integer 最大值,工作完成后会回收工作线程
  4. newScheduledThreadPool:支持定期及周期性任务执行,不回收工作线程。
  5. newWorkStealingPool:一个拥有多个任务队列的线程池。
15.简述Executor框架

Executor框架目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。产生线程池的函数ThreadPoolExecutor也是Executor的具体实现类。

16.简述Executor的继承关系
  • Executor:一个接口,其定义了一个接收Runnable对象的方法executor,该方法接收一个Runable实例执行这个任务。
  • ExecutorService:Executor的子类接口,其定义了一个接收Callable对象的方法,返回 Future 对象,同时提供execute方法。
  • ScheduledExecutorService:ExecutorService的子类接口,支持定期执行任务。
  • AbstractExecutorService:抽象类,提供 ExecutorService 执行方法的默认实现。
  • Executors:实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。
  • ThreadPoolExecutor:继承AbstractExecutorService,用于创建线程池。
  • ForkJoinPool: 继承AbstractExecutorService,Fork 将大任务分叉为多个小任务,然后让小任务执行,Join 是获得小任务的结果,类似于map reduce。
  • ScheduledThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建带定时任务的线程池。
17.简述线程池的状态
  • Running:能接受新提交的任务,也可以处理阻塞队列的任务。
  • Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方法,会进入该状态。
  • Stop:不接受新任务,不处理存量任务,调用shutdownnow进入该状态。
  • Tidying:所有任务已经终止了,worker_count(有效线程数)为0。
  • Terminated:线程池彻底终止。在tidying模式下调用terminated方法会进入该状态。

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

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

18.简述阻塞队列

阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:

  • ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
  • LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
  • PriorityBlockingQueue:阻塞优先队列。
  • DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
  • SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作
  • LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。
  • LinkedBlockingDeque:双向阻塞队列。
19.谈一谈ThreadLocal

ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。

  • set 给ThreadLocalMap设置值。
  • get 获取ThreadLocalMap。
  • remove 删除ThreadLocalMap类型的对象。

存在的问题

  1. 对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,造成一系列问题。
  2. 内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。
20.聊聊你对java并发包下unsafe类的理解

对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全(safe)的。
Java 有个类叫 Unsafe 类,这个类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。这个类可以说是 Java 并发开发的基础。

21.JAVA中的乐观锁与CAS算法

对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。
到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。
乐观锁一般都采用 Compare And Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。
CAS 算法的思路如下:

  1. 该算法认为不同线程对变量的操作时产生竞争的情况比较少。
  2. 该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。
  3. 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。
  4. 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。
22.ABA问题及解决方法简述

CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。
juc 包提供了一个 AtomicStampedReference,即在原始的版本下加入版本号戳,解决 ABA 问题。

23.简述常见的Atomic类

在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的++或者–方案,使用synchronized关键字和lock固然可以实现,但代价比较大,此时用原子类更加方便。
基本数据类型的原子类有:

  • AtomicInteger 原子更新整形
  • AtomicLong 原子更新长整型
  • AtomicBoolean 原子更新布尔类型

Atomic数组类型有:

  • AtomicIntegerArray 原子更新整形数组里的元素
  • AtomicLongArray 原子更新长整型数组里的元素
  • AtomicReferenceArray 原子更新引用类型数组里的元素。

Atomic引用类型有:

  • AtomicReference 原子更新引用类型
  • AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记
  • AtomicStampedReference 原子更新带有版本号的引用类型

FieldUpdater类型:

  • AtomicIntegerFieldUpdater 原子更新整形字段的更新器
  • AtomicLongFieldUpdater 原子更新长整形字段的更新器
  • AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器
24.简述Atomic类基本实现原理

以AtomicIntger 为例:
方法getAndIncrement:以原子方式将当前的值加1,具体实现为:

  1. 在 for 死循环中取得 AtomicInteger 里存储的数值
  2. 对 AtomicInteger 当前的值加 1
  3. 调用 compareAndSet 方法进行原子更新
  4. 先检查当前数值是否等于 expect
  5. 如果等于则说明当前值没有被其他线程修改,则将值更新为 next,
  6. 如果不是会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。
25.简述CountDownLatch

countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。
只能一次性使用,不能reset。

26.简述CyclicBarrier

CyclicBarrier 主要功能和countDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。但是其可以重复使用(reset)。

27.简述Semaphore

Semaphore即信号量。
Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计数器值为0,线程进入休眠。

28.简述Exchanger

Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。线程通过exchange 方法交换数据,第一个线程执行 exchanger 方法后会阻塞等待第二个线程执行该方法。当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。

29.简述ConcurrentHashMap

JDK7采用锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。

get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到Segment,最后通过散列算法定位到元素。
put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。

JDK8的改进

  1. 取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败再使用 synchronized 加锁添加元素
  2. 引入红黑树结构,当某个槽内的元素个数超过8且 Node数组 容量大于 64 时,链表转为红黑树。
  3. 使用了更加优化的方式统计集合内的元素数量。
30.Synchronized底层实现原理

Java 对象底层都关联一个的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。

synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是synchronized 括号里的对象。

执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。

31.Synchronized关键词使用方法
  1. 直接修饰某个实例方法
  2. 直接修饰某个静态方法
  3. 修饰代码块
32.简述java偏向锁

JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。

其申请流程为:

  1. 首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断;
  2. 判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4;
  3. 判断是否需要重偏向。如果不用的话,直接获得偏向锁;
  4. 利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。
33.简述轻量级锁

轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。

其申请流程为:

  1. 如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前Mark Word 的拷贝。
  2. 虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针
  3. 如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。
  4. 如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧
  5. 如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行
  6. 如果不是则说明锁对象已经被其他线程抢占。
  7. 如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。
34.简述锁优化策略

即自适应自旋、锁消除、锁粗化、锁升级等策(略偏)。

35.简述java的自旋锁

线程获取锁失败后,可以采用这样的策略,可以不放弃 CPU ,不停的重试,这种操作也称为自旋锁。

36.简述自适应自旋锁

自适应自旋锁自旋次数不再人为设定,通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。

37.简述锁粗化

锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。

38.简述锁消除

锁消除是一种更为彻底的优化,在编译时,java编译器对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。

39.简述Lock与ReentrantLock

Lock 是 java并发包的顶层接口。
可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。

40.简述AQS

AQS(AbstractQueuedSynchronizer)抽象的队列式同步器。
AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。
AQS是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。
子类通过继承同步器并实现它的抽象方法getState、setState 和 compareAndSetState对同步状态进行更改。

41.AQS获取独占锁/释放独占锁原理

获取:(acquire)

  1. 调用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过addWaiter 方法加入到同步队列的尾部,在队列中自旋。
  2. 调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞。

释放:(release)

  1. 调用 tryRelease 方法释放同步状态
  2. 调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。
42.AQS获取共享锁/释放共享锁原理

获取锁(acquireShared)

  1. 调用 tryAcquireShared 方法尝试获取同步状态,返回值不小于 0 表示能获取同步状态。

释放(releaseShared)

  1. 释放,并唤醒后续处于等待状态的节点。
43.线程池类型
  1. newCachedThreadPool 可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟后自动销毁。
  2. newFixedThreadPool 指定工作线程数量线程池。
  3. newSingleThreadExecutor 单线程Executor。
  4. newScheduleThreadPool 支持定时任务的指定工作线程数量线程池。
  5. newSingleThreadScheduledExecutor 支持定时任务的单线程Executor。
44.synchronized关键字作用

保证只有一个线程可以获取对象的锁,并执行代码块,其他线程不能在该线程执行代码块时执行。

45.简述三色标记法

三色标记法是垃圾收集器CMS和G1使用的标记方法,该方法把对象分为三种颜色:

  1. 白色,该对象尚被未访问。
  2. 灰色,该对象已被访问,但该对象引用的其他对象并没有被访问。
  3. 黑色,该对象和引用的其他对象均被访问。

因此,对三色标记法来说,所有对象都可以看作由白色集合,黑色集合,灰色集合组成。通过这种标记方法的访问过程如下:
4. 初始所有对象均在白色集合
5. 将GC root直接引用的对象移动至灰色集合。
6. 从灰色集合中取出一个对象,将该对象引用的白色集合对象,移动至灰色集合
7. 移动完成后,将该对象移动至黑色集合
8. 重复3-4操作。

三、Java虚拟机

1.简述JVM内存模型

线程私有的运行时数据区: 程序计数器、Java 虚拟机栈、本地方法栈。
线程共享的运行时数据区: Java 堆、方法区。

2.简述程序计数器

程序计数器表示当前线程所执行的字节码的行号指示器。
程序计数器不会产生StackOverflowError和OutOfMemoryError。

3.简述虚拟机栈

Java 虚拟机栈用来描述 Java 方法执行的内存模型。线程创建时就会分配一个栈空间,线程结束后栈空间被回收。

栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和返回地址等信息。

虚拟机栈会产生两类异常:
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。
OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。

4.简述本地方法栈

本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为本地方法服务。可以将虚拟机栈看作普通的java函数对应的内存模型,本地方法栈看作由native关键词修饰的函数对应的内存模型。

本地方法栈会产生两类异常:
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。
OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。

5.简述JVM中的堆

堆主要作用是存放对象实例,Java 里几乎所有对象实例都在堆分配内存,堆也是内存管理中最大的一块。Java的垃圾回收主要就是针对堆这一区域进行。

可通过 -Xms 和 -Xmx 设置堆的最小和最大容量。

堆会抛出 OutOfMemoryError异常。

6.简述方法区

方法区用于存储被虚拟机加载的类信息、常量、静态变量等数据。

JDK6之前使用永久代实现方法区,容易内存溢出。JDK7 把放在永久代的字符串常量池、静态变量等移出,JDK8 中抛弃永久代,改用在本地内存中实现的元空间来实现方法区,把 JDK 7 中永久代内容移到元空间。

7.简述运行时常量池

运行时常量池存放常量池表,用于存放编译器生成的各种字面量与符号引用。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。除此之外,也会存放字符串基本类型。

JDK8之前,放在方法区,大小受限于方法区。JDK8将运行时常量池存放堆中。

8.简述直接内存

直接内存也称为堆外内存,就是把内存对象分配在JVM堆外的内存区域。这部分内存不是虚拟机管理,而是由操作系统来管理。

Java通过DriectByteBuffer对其进行操作,避免了在 Java 堆和 Native堆来回复制数据。

9.简述java创建对象的过程
  1. 检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
  2. 检查通过后虚拟机将为新生对象分配内存。
  3. 完成内存分配后虚拟机将成员变量设为零值
  4. 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。
  5. 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
10.简述JVM给对象分配内存的策略
  1. 指针碰撞: 这种方式在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一边,通过指针挪动完成分配。
  2. 空闲列表: 对于 Java 堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
11.java对象内存分配是如何保证线程安全的
  1. 对分配内存空间采用CAS机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。
  2. 每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配。一般采用这种策略。
12.简述对象的内存布局

对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。

对象头主要包含两部分数据: MarkWord、类型指针。MarkWord 用于存储哈希码(HashCode)、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。

类型指针即对象指向他的类元数据指针,如果对象是一个 Java 数组,会有一块用于记录数组长度的数据。

实例数据存储代码中所定义的各种类型的字段信息。

对齐填充起占位作用。HotSpot 虚拟机要求对象的起始地址必须是8的整数倍,因此需要对齐填充。

13.如何判断对象是否是垃圾

引用计数法:设置引用计数器,对象被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。会存在对象间循环引用的问题,一般不使用这种方法。

可达性分析:通过 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,如果某个对象没有被搜到,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。

14.简述java的引用类型

强引用: 被强引用关联的对象不会被回收。一般采用 new 方法创建强引用。

软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。一般采用 SoftReference 类来创建软引用。

弱引用:垃圾收集器碰到即回收,也就是说它只能存活到下一次垃圾回收发生之前。一般采用WeakReference 类来创建弱引用。

虚引用: 无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用。

15.简述标记清除算法、标记整理算法和标记复制算法

标记清除算法:先标记需清除的对象,之后统一回收。这种方法效率不高,会产生大量不连续的碎片。

标记整理算法:先标记存活对象,然后让所有存活对象向一端移动,之后清理端边界以外的内存

标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。

16.简述分代收集算法

根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代,对这两块采用不同的算法。

新生代使用:标记复制算法
老年代使用:标记清除或者标记整理算法

17.简述Serial垃圾收集器

单线程串行收集器。垃圾回收的时候,必须暂停其他所有线程。新生代使用标记复制算法,老年代使用标记整理算法。简单高效。

18.简述ParNew垃圾收集器

可以看作Serial垃圾收集器的多线程版本,新生代使用标记复制算法,老年代使用标记整理算法。

19.简述Parallel Scavenge垃圾收集器

注重吞吐量,即cpu运行代码时间/cpu耗时总时间(cpu运行代码时间+ 垃圾回收时间)。新生代使用标记复制算法,老年代使用标记整理算法。

20.简述CMS垃圾收集器

注重最短时间停顿。CMS垃圾收集器为最早提出的并发收集器,垃圾收集线程与用户线程同时工作。采用标记清除算法。该收集器分为初始标记、并发标记、并发预清理、并发清除、并发重置这么几个步骤。

初始标记:暂停其他线程(stop the world),标记与GC roots直接关联的对象。

并发标记:可达性分析过程(程序不会停顿)。

并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象,重新标记,暂停虚拟机(stop theworld)扫描CMS堆中剩余对象。

并发清除:清理垃圾对象(程序不会停顿)。

并发重置:重置CMS收集器的数据结构。

21.简述G1垃圾收集器

和之前收集器不同,该垃圾收集器把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。

初始标记:标记与GC roots直接关联的对象。

并发标记:可达性分析。

最终标记:对并发标记过程中,用户线程修改的对象再次标记一下。

筛选回收:对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间制定回收计划并回收。

22.简述Minor GC

Minor GC指发生在新生代的垃圾收集,因为 Java 对象大多存活时间短,所以 Minor GC 非常频繁,一般回收速度也比较快。

23.简述Full GC

Full GC 是清理整个堆空间—包括年轻代和永久代。调用System.gc(),老年代空间不足,空间分配担保失败,永生代空间不足会产生full gc。

24.常见内存分配策略

大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。

大对象需要大量连续内存空间,直接进入老年代区分配。

如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1,并且每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。

如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。

空间分配担保。MinorGC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。如果不,JVM会查看HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将Minor GC,否则改成一次 FullGC。

25.简述JVM类加载过程

加载:

  1. 通过全类名获取类的二进制字节流.
  2. 将类的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成类的Class对象,作为方法区数据的入口。

验证:对文件格式,元数据,字节码,符号引用等验证正确性。
准备:在方法区内为类变量分配内存并设置为0值。
解析:将符号引用转化为直接引用。
初始化:执行类构造器clinit方法,真正初始化。

26.简述JVM中的类加载器

BootstrapClassLoader启动类加载器:加载/lib下的jar包和类。C++编写。
ExtensionClassLoader扩展类加载器: /lib/ext目录下的jar包和类。java编写。
AppClassLoader应用类加载器,加载当前classPath下的jar包和类。java编写。

27.简述双亲委派机制

一个类加载器收到类加载请求之后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法完成时才尝试自己加载。

加载类顺序:BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader

检查类是否加载顺序:
CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader

28.双亲委派机制的优点
  1. 避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派保证了java程序的稳定运行。
  2. 保证核心API不被修改。
29.如何破坏双亲委派机制

重载loadClass()方法,即自定义类加载器。

30.如何构建自定义类加载器
  1. 新建自定义类继承自java.lang.ClassLoader
  2. 重写findClass、loadClass、defineClass方法
31.JVM常见调优参数
  • -Xms 初始堆大小
  • -Xmx 最大堆大小
  • -XX:NewSize 年轻代大小
  • -XX:MaxNewSize 年轻代最大值
  • -XX:PermSize 永生代初始值
  • -XX:MaxPermSize 永生代最大值
  • -XX:NewRatio 新生代与老年代的比例
32.调用system.gc()一定会发生垃圾收集吗?为什么?

调用System.gc()的时候,其实并不会马上进行垃圾回收,只会把这次gc请求记录下来。需配合System.runFinalization()才会进行真正回收

33.静态变量存储位置

在1.8以前,静态成员变量存在方法区,在1.8后,由于JDK8取消永生代,静态变量存储到了堆中

四、Redis

1.Redis单线程原理

首先必须明确,Redis单线程指的是网络请求模块使用了一个线程,其他模块仍用了多个线程。并不是一个线程完成了所有功能。
原理上,其采用了epoll的多路复用特性,因此可以采用单线程处理其网络请求。

2.Redis数据类型

String:字符串类型,最简单的类型
Hash:类似于Map的一种结构。
List:有序列表。
Set:无序集合。
ZSet:带权值的无序集合,即每个ZSet元素还另有一个数字代表权值,集合通过权值进行排序。

3.什么情况下使用redis
  1. 针对热点数据进行缓存
  2. 对于特定限时数据的存放
  3. 针对带热点权值数据的排序list
  4. 分布式锁
4.redis与memcache的区别
  1. redis处理网络请求采用单线程模型,而memcache采用多线程异步IO的方式
  2. redis支持数据持久化,memcache不支持
  3. redis支持的数据格式比memcache更多
5.简述缓存穿透

缓存穿透指缓存和数据库均没有需要查询的数据,攻击者不断发送这种请求,使数据库压力过大。

6.简述缓存穿透的解决方法
  1. 在数据库操作访问前进行校验,对不合法请求直接返回。
  2. 对于经常被访问的,并且数据库没有的键,缓存层记录键=null。
7.简述缓存击穿

缓存击穿指缓存中没有数据,但数据库中有该数据。一般这种情况指特定数据的缓存时间到期,但由于并发用户访问该数据特别多,因此去数据库去取数据,引起数据库访问压力过大

8.简述缓存穿透的解决方法
  1. 设置热点数据永远不过期。
  2. 对并发读数据设置并发锁,降低并发性
9.简述缓存雪崩

缓存雪崩指缓存中一大批数据到过期时间,而从缓存中删除。但该批数据查询数据量巨大,查询全部走数据库,造成数据库压力过大。

10.简述缓存雪崩的解决方法
  1. 缓存数据设置随机过期时间,防止同一时间大量数据过期。
  2. 设置热点数据永远不过期。
  3. 对于集群部署的情况,将热点数据均匀分布在不同缓存中。
11.Redis有哪些集群部署方式
  1. 主从复制
  2. 哨兵模式
  3. Cluster集群模式
12.简述主从复制模式

在主从复制中,有主库(Master)节点和从库(Slave)节点两个角色。
从节点服务启动会连接主库,并向主库发送SYNC命令。

主节点收到同步命令,启动持久化工作,工作执行完成后,主节点将传送整个数据库文件到从库,从节点接收到数据库文件数据之后将数据进行加载。此后,主节点继续将所有已经收集到的修改命令,和新的修改命令依次传送给从节点,从节点依次执行,从而达到最终的数据同步。

通过这种方式,可以使写操作作用于主库,而读操作作用于从库,从而达到读写分离。

13.简述哨兵模式

哨兵模式监控redis集群中Master的工作的状态。在Master主服务器宕机时,从slave中选择新机器当作master,保证系统高可用。

每个哨兵每10秒向主服务器,slave和其他哨兵发送ping。
客户端通过哨兵,由哨兵提供可供服务的redis master节点。
哨兵只需要配master节点,会自动寻找其对应的slave节点。
监控同一master节点的哨兵会自动互联,组成哨兵网络,当任一哨兵发现master连接不上,即开会投票,投票半数以上决定Master下线,并从slave节点中选取master节点。

14.cluster集群

cluster提出了虚拟槽的概念。

  1. redis cluster默认有16384个槽,在集群搭建的时候,需要给节点分配哈希槽尽可能相同数量虚拟槽。
  2. 如果目前redis执行get操作,redis先对这个key经过CRC16 hash运算,并把结果对16384取余,得到槽编号。
  3. 根据槽编号,寻找到其对应的redis节点,在节点上执行hash命令。
  4. 如果此时执行set操作,节点先验证该key对应的槽编号是不是归本节点管,如果是则保存数据。如果不是,则发送正确节点编号给客户端。
15.简述Redis的RDB

RDB即将当前数据生成快照,并保存于硬盘中。可以通过手动命令,也可以设置自动触发。

16.简述Redis的save命令

save命令是redis手动触发RDB过程的命令。使用该命令后,服务器阻塞,直到RDB过程完成后终止。该过程占用内存较多。

17.简述Redis的bgsave命令

bgsave命令不阻塞主进程(严格意义上也不是完全不阻塞,详看下面过程),该命令fork一个子进程用于执行RDB过程。其具体过程为:

  1. 判断此时有没有子进程用于RDB,有的话直接返回。
  2. redis进行fork子进程过程,此时父进程处于阻塞状态。
  3. 子进程创建RDB文件,完成后返回给父进程
18.简述Redis自动触发RDB机制
  1. 通过配置文件,设置一定时间后自动执行RDB
  2. 如采用主从复制过程,会自动执行RDB
  3. Redis执行shutdown时,在未开启AOF后会执行RDB
19.简述Redis的AOF

AOF通过日志,对数据的写入修改操作进行记录。这种持久化方式实时性更好。通过配置文件打开AOF。

20.简述AOF的持久化策略
  1. always。每执行一次数据修改命令就将其命令写入到磁盘日志文件上。
  2. everysec。每秒将命令写入到磁盘日志文件上。
  3. no。不主动设置,由操作系统决定什么时候写入到磁盘日志文件上。
21.简述AOF的重写

随着客户端不断进行操作,AOF对应的文件也越来越大。redis提供了bgrewriteaof函数,针对目前数据库中数据,在不读取原有AOF文件的基础上,重写了一个新的AOF文件,减少文件大小。

22.RDB与AOF优缺点比较

AOF占用的文件体积比RDB大。一般来说利用AOF备份对系统的消耗比RDB低。对于备份时出现系统故障,RDB数据可能会全丢,但AOF只会损失一部分。
RDB恢复速度比AOF快。

23.简述Redis淘汰机制
  1. noeviction:默认禁止驱逐数据。内存不够使用时,对申请内存的命令报错。
  2. volatile-lru:从设置了过期时间的数据集中淘汰最近没使用的数据。
  3. volatile-ttl:从设置了过期时间的数据集中淘汰即将要过期的数据。
  4. volatile-random:从设置了过期时间的数据中随机淘汰数据。
  5. allkeys-lru:淘汰最近没使用的数据。
  6. allkeys-random:随机淘汰数据。
24.MySQL与Redis区别

mysql是关系型数据库,并且其将数据存储在硬盘中,读取速度较慢。
redis是非关系型数据库,并且其将数据存储在内存中,读取速度较快。

25.简述Redis过期策略
  1. 定期删除,redis默认是每100ms就随机抽取一些设置了过期时间的key,并检查其是否过期,如果过期就删除。因此该删除策略并不会删除所有的过期key。
  2. 惰性删除,在客户端需要获取某个key时,redis将首先进行检查,若该key设置了过期时间并已经过期就会删除。
    实际上redis结合上述两种手段结合起来,保证删除过期的key。
26.Redis基本数据类型实现原理

字符串:采用类似数组的形式存储
list:采用双向链表进行具体实现
hash:采用hashtable或者ziplist进行具体实现
集合:采用intset或hashtable存储
有序集合:采用ziplist或skiplist+hashtable实现

27.Redis快的原因
  1. redis是基于内存的数据库,内存数据读取存储效率远高于硬盘型
  2. redis采用多路复用技术通过采用epoll的非阻塞IO,提升了效率

五、设计模式

1.简述设计模式七大原则
开放封闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。
单一职责原则:一个类、接口或方法只负责一个职责,降低代码复杂度以及变更引起的风险。
依赖倒置原则:针对接口编程,依赖于抽象类或接口而不依赖于具体实现类。
接口隔离原则:将不同功能定义在不同接口中实现接口隔离。
里氏替换原则:任何基类可以出现的地方,子类一定可以出现。
迪米特原则:每个模块对其他模块都要尽可能少地了解和依赖,降低代码耦合度。
合成复用原则:尽量使用组合(has-a)/聚合(contains-a)而不是继承(is-a)达到软件复用的目的。

2.简述设计模式的分类
创建型模式:在创建对象的同时隐藏创建逻辑,不使用 new 直接实例化对象。有工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式:通过类和接口间的继承和引用实现创建复杂结构的对象。有适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式:通过类之间不同通信方式实现不同行为。有策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

3.简述简单工厂模式
简单工厂模式指由一个工厂对象来创建实例,适用于工厂类负责创建对象较少的情况。例子:Spring 中的 BeanFactory 使用简单工厂模式,产生 Bean 对象。

4.简述工厂模式
工厂方法模式指定义一个创建对象的接口,让接口的实现类决定创建哪种对象,让类的实例化推迟到子类中进行。例子:Spring 的 FactoryBean 接口的 getObject 方法也是工厂方法。

5.简述抽象工厂模式
抽象工厂模式指提供一个创建一系列相关或相互依赖对象的接口,无需指定它们的具体类。例子:java.sql.Connection 接口。

6.简述单例模式
一个单例类在任何情况下都只存在一个实例。

饿汉式实现

public class Singleton {
private Singleton(){}
private static Singleton instance = new Singleton();

public static Singleton getInstance() {
    return instance;
}

AI运行代码

  • 1
  • 2
  • 3

}
懒汉式实现

public class Singleton {
private DoubleCheckSingleton(){}
private volatile static Singleton instance;

public static Singleton getInstance() {
    if(instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

AI运行代码

}
7.简述代理模式
代理模式为其他对象提供一种代理以控制对这个对象的访问。优点是可以增强目标对象的功能,降低代码耦合度,扩展性好。缺点是在客户端和目标对象之间增加代理对象会导致请求处理速度变慢,增加系统复杂度。

静态代理:在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。

动态代理:程序运行期间动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。

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

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

8.简述适配器模式
适配器模式将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作。

9.简述模板模式
模板模式定义了一个操作中的算法的骨架,并将一些步骤延迟到子类,适用于抽取子类重复代码到公共父类。
可以封装固定不变的部分,扩展可变的部分。但每一个不同实现都需要一个子类维护,会增加类的数量。

10.简述装饰器模式
装饰器模式可以动态地给对象添加一些额外的属性或行为,即需要修改原有的功能,但又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。

11.简述观察者模式
观察者模式表示的是一种对象与对象之间具有依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

六、数据结构与算法

1.简述数据结构栈

栈是一种线性表,其限制只能在表尾进行插入或删除操作。由于该特性又称为后进先出的线性表。

2.简述数据结构队列

队列是一种先进先出的线性表。其限制只能在线性表的一端进行插入,而在另一端删除元素。

3.简述二叉树

二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成。

4.简述满二叉树

除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树

5.简述完全二叉树

一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

6.简述二叉树的前中后序遍历算法

前序遍历:若二叉树为空树,则执行空逻辑,否则:

  1. 访问根节点
  2. 递归前序遍历左子树
  3. 递归前序遍历右子树

中序遍历:若二叉树为空树,则执行空逻辑,否则:

  1. 递归中序遍历左子树
  2. 访问根节点
  3. 递归中序遍历右子树

后序遍历:若二叉树为空树,则执行空逻辑,否则:

  1. 递归后序遍历左子树
  2. 递归后序遍历右子树
  3. 访问根节点
7.简述解决Hash冲突的方法

开放定址法:当发生哈希冲突时,如果哈希表未被装满,那么可以把这个值存放到冲突位置中的下一个空位置中去

链地址法:对相同的哈希地址,设置一个单链表,单链表内放的都是哈希冲突元素。

8.简述AVL树

AVL树是一种改进版的搜索二叉树,其引入平衡因子(左子支高度与右子支高度之差的绝对值),通过旋转使其尽量保持平衡。
任何一个节点的左子支高度与右子支高度之差的绝对值不超过1。

9.简述红黑树

红黑树本身是有2-3树发展而来,红黑树是保持平衡的二叉树,其查找会比AVL树慢一点,添加和删除元素会比AVL树快一点。增删改查统计性能上讲,红黑树更优。
红黑树主要特征是在每个节点上增加一个属性表示节点颜色,可以红色或黑色。红黑树和 AVL 树类似,都是在进行插入和删除时通过旋转保持自身平衡,从而获得较高的查找性能。红黑树保证从根节点到叶尾的最长路径不超过最短路径的 2 倍,所以最差时间复杂度是 O(logn)。红黑树通过重新着色和左右旋转,更加高效地完成了插入和删除之后的自平衡调整。

10.简述稳定排序和非稳定排序的区别

稳定排序:排序前后两个相等的数相对位置不变,则算法稳定
非稳定排序:排序前后两个相等的数相对位置发生了变化,则算法不稳定

11.常见的稳定排序算法有哪些

插入排序、冒泡排序、归并排序

12.常见的不稳定排序算法有哪些

希尔排序、直接选择排序、堆排序、快速排序

13.简述插入排序

插入排序:每一趟将一个待排序记录按其关键字的大小插入到已排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
排序算法稳定。时间复杂度 O(n²),空间复杂度 O(1)。

14.简述希尔排序

希尔排序:把记录按下标的一定增量分组,对每组进行直接插入排序,每次排序后减小增量,当增量减至 1 时排序完毕。
排序算法不稳定。时间复杂度 O(nlogn),空间复杂度 O(1)。

15.简述直接选择排序

直接选择排序:每次在未排序序列中找到最小元素,和未排序序列的第一个元素交换位置,再在剩余未排序序列中重复该操作直到所有元素排序完毕。
排序算法不稳定。时间复杂度 O(n²),空间复杂度 O(1)。

16.简述堆排序

堆排序:将待排序数组看作一个树状数组,建立一个二叉树堆。通过对这种数据结构进行每个元素的插入,完成排序工作。
排序算法不稳定,时间复杂度 O(nlogn),空间复杂度 O(1)。

17.简述冒泡排序

冒泡排序:比较相邻的元素,如果第一个比第二个大就进行交换,对每一对相邻元素做同样的工作。
排序算法稳定,时间复杂度 O(n²),空间复杂度 O(1)。

18.简述快速排序

快速排序:随机选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全部小于等于基准元素,一部分全部大于等于基准元素,再按此方法递归对这两部分数据进行快速排序。
排序算法不稳定,时间复杂度 O(nlogn),空间复杂度 O(logn)。

19.简述归并排序

归并排序:将待排序序列分成两部分,然后对两部分分别递归排序,最后进行合并。
排序算法稳定,时间复杂度都为 O(nlogn),空间复杂度为 O(n)。

20.简述图

图是由顶点集合和顶点之间的边集合组成的一种数据结构,分为有向图和无向图。
有向图:边具有方向性
无向图:边不具有方向性

21.简述邻接矩阵

用一个二维数组存放图顶点间关系的数据,这个二维数组称为邻接矩阵。
对于无向图,邻接矩阵是对称矩阵

22.简述邻接表

邻接表是通过链表表示图连接关系的一种方。对于表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。

23.简述图的深度优先搜索DFS

将图中每个顶点的访问标志设为 FALSE, 之后搜索图中每个顶点,如果未被访问,则以该顶点V0为起始点出发,访问此顶点,然后依次从V0的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和V0有路径相通的顶点都被访问到。

24.简述图的广度优先搜索

从图中的某个顶点V0出发,并在访问此顶点之后依次访问V0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和V0有路径相通的顶点都被访问到。

25.简述最小生成树和其对应的算法

对于有 n 个结点的原图,生成原图的极小连通子图,其包含原图中的所有 n 个结点,并且有保持图连通的最少的边。

普里姆算法:取图中任意一个顶点 v 作为生成树的根,之后往生成树上添加新的顶点 w。在添加的顶点w 和已经在生成树上的顶点v 之间必定存在一条边,并且该边的权值在所有连通顶点 v 和 w 之间的边中取值最小。之后继续往生成树上添加顶点,直至生成树上含有 n-1 个顶点为止。

克鲁斯卡尔算法:先构造一个只含 n 个顶点的子图 SG,然后从权值最小的边开始,若它的添加不使SG 中产生回路,则在 SG 上加上这条边,如此重复,直至加上 n-1 条边为止。

26.简述最短路径算法

Dijkstral算法为求解一个点到其余各点最小路径的方法,其算法为:
假设我们求解的是顶点v到其余各个点的最短距离。n次循环至n个顶点全部遍历:

  1. 从权值数组中找到权值最小的,标记该边端点k
  2. 打印该路径及权值
  3. 如果存在经过顶点k到顶点i的边比v->i的权值小
  4. 更新权值数组及对应路径
27.简述堆

堆是一种完全二叉树形式,其可分为最大值堆和最小值堆。
最大值堆:子节点均小于父节点,根节点是树中最大的节点。
最小值堆:子节点均大于父节点,根节点是树中最小的节点。

28.简述set

Set是一种集合。集合中的对象不按特定的方式排序,并且没有重复对象。

29.说一下对于树的理解

数据结构树是一种由有限节点组成的层次关系的集合。其特点如下:

  1. 每个节点有零个或多个子节点;
  2. 只有一个节点没有父节点,该节点称为根节点;
  3. 除根节点外,每个节点有且只有一个父节点;
30.简述二叉查找树
  1. 二叉查找树的左子树若不为空,则左子树上所有结点的值均小于它的根结点的值;
  2. 二叉查找树的右子树若不为空,则右子树上所有结点的值均大于它的根结点的值;
  3. 二叉查找树的左、右子树也分别为二叉查找树;
  4. 没有键值相等的结点。

七、操作系统

1.什么是操作系统?请简要概述一下

操作系统是管理计算机硬件和软件资源的计算机程序,提供一个计算机用户与计算机硬件系统之间的接口。
向上对用户程序提供接口,向下接管硬件资源。
操作系统本质上也是一个软件,作为最接近硬件的系统软件,负责处理器管理、存储器管理、设备管理、文件管理和提供用户接口。

2.操作系统有哪些分类?

操作系统常规可分为批处理操作系统、分时操作系统、实时操作系统。
若一个操作系统兼顾批操作和分时的功能,则称该系统为通用操作系统。
常见的通用操作系统有:Windows、Linux、MacOS等。

3.什么是内核态和用户态?

为了避免操作系统和关键数据被用户程序破坏,将处理器的执行状态分为内核态和用户态。

内核态是操作系统管理程序执行时所处的状态,能够执行包含特权指令在内的一切指令,能够访问系统内所有的存储空间。

用户态是用户程序执行时处理器所处的状态,不能执行特权指令,只能访问用户地址空间。

用户程序运行在用户态,操作系统内核运行在内核态。

4.如何实现内核态和用户态的切换?

处理器从用户态切换到内核态的方法有三种:系统调用、异常和外部中断。

  1. 系统调用是操作系统的最小功能单位,是操作系统提供的用户接口,系统调用本身是一种软中断。
  2. 异常,也叫做内中断,是由错误引起的,如文件损坏、缺页故障等。
  3. 外部中断,是通过两根信号线来通知处理器外设的状态变化,是硬中断。
5.并发和并行的区别
  1. 并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核cpu上的多任务。但是从微观上看两个程序的指令是交织着运行的,指令之间交错执行,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率(如降低某个进程的相应时间)。

  2. 并行(parallelism):指严格物理意义上的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu都是往多核方面发展。

6.什么是进程?

进程是操作系统中最重要的抽象概念之一,是资源分配的基本单位,是独立运行的基本单位。

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。

上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它是栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

进程一般由以下的部分组成:

  1. 进程控制块PCB,是进程存在的唯一标志,包含进程标识符PID,进程当前状态,程序和数据地址,进程优先级、CPU现场保护区(用于进程切换),占有的资源清单等。
  2. 程序段
  3. 数据段
7.进程的基本操作

以Unix系统举例:

  1. 进程的创建:fork()。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的 PID。fork函数是有趣的(也常常令人迷惑), 因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork 返回子进程的 PID。在子进程中,fork 返回 0。因为子进程的 PID 总是为非零,返回值就提供一个明 确的方法来分辨程序是在父进程还是在子进程中执行。
    pid_t fork(void);

  2. 回收子进程:当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个进程可以通过调用waitpid 函数来等待它的子进程终止或者停止。
    pid_t waitpid(pid_t pid, int *statusp, int options);

  3. 加载并运行程序:execve 函数在当前进程的上下文中加载并运行一个新程序。
    int execve(const char *filename, const char *argv[], const char *envp[]);

  4. 进程终止:
    void exit(int status);

8.简述进程间通信方法

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

不同进程间的通信本质:进程之间可以看到一份公共资源;而提供这份资源的形式或者提供者不同,造成了通信方式不同。

进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。

9.进程如何通过管道进行通信

管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:

  1. 其本质是一个伪文件(实为内核缓冲区)
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,从读端流出。

管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区实现。

管道的局限性:

  1. 数据自己读不能自己写。
  2. 数据一旦被读走,便不在管道中存在,不可反复读取。
  3. 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
  4. 只能在有公共祖先的进程间使用管道。
10.进程如何通过共享内存通信?

它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。

特点:

  1. 共享内存是最快的一种IPC,因为进程是直接对内存进行操作来实现通信,避免了数据在用户空间和内核空间来回拷贝。
  2. 因为多个进程可以同时操作,所以需要进行同步处理。
  3. 信号量和共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
11.什么是信号

一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。 Linux 系统上支持的30 种不同类型的信号。 每种信号类型都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。

  1. 发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。发送
    信号可以有如下两种原因:
    • 内核检测到一个系统事件,比如除零错误或者子进程终止。
    • 一个进程调用了kill 函数, 显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
  2. 接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。
12.如何编写正确且安全的信号处理函数
  1. 处理程序要尽可能简单。 避免麻烦的最好方法是保持处理程序尽可能小和简单。
    例如,处理程序可能只是简单地设置全局标志并立即返回;所有与接收信号相关的处理都由主程序执行,它周期性地检查(并重置)这个标志。

  2. 在处理程序中只调用异步信号安全的函数。
    所谓异步信号安全的函数(或简称安全的函数)能够被信号处理程序安全地调用,原因有二:要么它是可重入的(例如只访问局部变量),要么它不能被信号处理程序中断。

  3. 保存和恢复errno。
    许多Linux 异步信号安全的函数都会在出错返回时设置errno在处理程序中调用这样的函数可能会干扰主程序中其他依赖。解决方法是在进人处理程序时把errno 保存在一个局部变量中,在处理程序返回前恢复它。注意,只有在处理程序要返回时才有此必要。如果处理程序调用_exit终止该进程,那么就不需要这样做了。

  4. 阻塞所有的信号,保护对共享全局数据结构的访问。
    如果处理程序和主程序或其他处理程序共享一个全局数据结构,那么在访问(读或者写)该数据结构时,你的处理程序和主程序应该暂时阻塞所有的信号。这条规则的原因是从主程序访问一个数据结构d 通常需要一系列的指令,如果指令序列被访问d 的处理程序中断,那么处理程序可能会发现d 的状态不一致,得到不可预知的结果。在访问d时暂时阻塞信号保证了处理程序不会中断该指令序列。

  5. 用volatile 声明全局变量。
    考虑一个处理程序和一个main 函数,它们共享一个全局变量g 。处理程序更新g,main 周期性地读g, 对于一个优化编译器而言,main 中g的值看上去从来没有变化过,因此使用缓存在寄存器中g 的副本来满足对g 的每次引用是很安全的。如果这样,main 函数可能永远都无法看到处理程序更新过的值。可以用volatile 类型限定符来定义一个变量,告诉编译器不要缓存这个变量。例如:volatile 限定符强迫编译器毎次在代码中引用g时,都要从内存中读取g的值。一般来说,和其他所有共享数据结构一样,应该暂时阻塞信号,保护每次对全局变量的访问。
    volatile int g;

  6. 用sig_atomic_t声明标志。
    在常见的处理程序设计中,处理程序会写全局标志来记录收到了信号。主程序周期性地读这个标志,响应信号,再清除该标志。对于通过这种方式来共享的标志,C 提供一种整型数据类型sig_atomic_t对它的读和写保证会是原子的(不可中断的)。

  7. 信号的一个与直觉不符的方面是未处理的信号是不排队的。
    因为 pending 位向量中每种类型的信号只对应有一位,所以每种类型最多只能有一个未处理的信号。关键思想是如果存在一个未处理的信号就表明至少有一个信号到达了。

13.进程调度的时机
  1. 当前运行的进程运行结束。
  2. 当前运行的进程由于某种原因阻塞。
  3. 执行完系统调用等系统程序后返回用户进程。
  4. 在使用抢占调度的系统中,具有更高优先级的进程就绪时。
  5. 分时系统中,分给当前进程的时间片用完。
14.不能进行进程调度的情况
  1. 在中断处理程序执行时。
  2. 在操作系统的内核程序临界区内。
  3. 其它需要完全屏蔽中断的原子操作过程中。
15.进程的调度策略
  1. 先到先服务调度算法
  2. 短作业优先调度算法
  3. 优先级调度算法
  4. 时间片轮转调度算法
  5. 高响应比优先调度算法
  6. 多级队列调度算法
  7. 多级反馈队列调度算法
16.进程调度策略的基本设计指标
  1. CPU利用率
  2. 系统吞吐率,即单位时间内CPU完成的作业的数量。
  3. 响应时间。
  4. 周转时间。是指作业从提交到完成的时间间隔。从每个作业的角度看,完成每个作业的时间也是很关键
    • 平均周转时间
    • 带权周转时间
    • 平均带权周转时间
17.进程的状态与状态转换

进程在运行时有三种基本状态:就绪态、运行态和阻塞态。

  1. 运行(running)态:进程占有处理器正在运行的状态。进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态; 在多处理机系统中,则有多个进程处于执行状态。

2.就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行的状态。 当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。

3.阻塞(wait)态:又称等待态或睡眠态,指进程不具备运行条件,正在等待某个时间完成的状态。

各状态之间的转换:

  1. 就绪→执行 处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。
  2. 执行→就绪 处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。
  3. 执行→阻塞 正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。
  4. 阻塞→就绪 处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态。
18.什么是孤儿进程?僵尸进程?
  1. 孤儿进程: 父进程退出,子进程还在运行的这些子进程都是孤儿进程,孤儿进程将被init进程(1号进程)所收养,并由init进程对他们完成状态收集工作。
  2. 僵尸进程: 进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中的这些进程是僵尸进程。
19.什么是线程?
  1. 线程是进程划分的任务,是一个进程内可调度的实体,是CPU调度的基本单位,用于保证程序的实时性,实现进程内部的并发。
  2. 线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。
  3. 每个线程完成不同的任务,但是属于同一个进程的不同线程之间共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
20.为什么需要线程?

线程产生的原因:进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;
但是其具有一些缺点:

  1. 进程在同一时刻只能做一个任务,很多时候不能充分利用CPU资源。
  2. 进程在执行的过程中如果发生阻塞,整个进程就会挂起,即使进程中其它任务不依赖于等待的资源,进程仍会被阻塞。

引入线程就是为了解决以上进程的不足,线程具有以下的优点:

  1. 从资源上来讲,开辟一个线程所需要的资源要远小于一个进程。
  2. 从切换效率上来讲,运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间(这种时间的差异主要由于缓存的大量未命中导致)。
  3. 从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的地址空间,要进行数据的传递只能通过进程间通信的方式进行。线程则不然,属于同一个进程的不同线程之间共享同一地址空间,所以一个线程的数据可以被其它线程感知,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要一些同步措施)。
21.简述线程和进程的区别和联系
  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
  2. 进程在执行过程中拥有独立的地址空间,而多个线程共享进程的地址空间。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
  3. 进程是资源分配的最小单位,线程是CPU调度的最小单位。
  4. 通信:由于同一进程中的多个线程具有相同的地址空间,使它们之间的同步和通信的实现,也变得比较容易。进程间通信 IPC ,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要一些同步方法,以保证数据的一致性)。
  5. 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
  6. 进程间不会相互影响;一个进程内某个线程挂掉将导致整个进程挂掉。
  7. 进程适应于多核、多机分布;线程适用于多核。
22.进程和线程的基本API

进程API以Unix系统为例,线程相关的API属于Posix线程(Pthreads)标准接口

进程源语 线程源语 描述
fork pthread_create 创建新的控制流
exit pthread_exit 从现有的控制流中退出
waitpid pthread_join 从控制流中得到退出状态
atexit pthread_cancel_push 注册在退出控制流时调用的函数
getpid pthread_self 获取控制流的ID
abort pthread_cancel 请求控制流的非正常退出
23.多线程模型
  1. 多对一模型。将多个用户级线程映射到一个内核级线程上。该模型下,线程在用户空间进行管理,效率较高。缺点就是一个线程阻塞,整个进程内的所有线程都会阻塞。几乎没有系统继续使用这个模型。
  2. 一对一模型。将内核线程与用户线程一一对应。优点是一个线程阻塞时,不会影响到其它线程的执行。该模型具有更好的并发性。缺点是内核线程数量一般有上限,会限制用户线程的数量。更多的内核线程数目也给线程切换带来额外的负担。linux和Windows操作系统家族都是使用一对一模型。
  3. 多对多模型。将多个用户级线程映射到多个内核级线程上。结合了多对一模型和一对一模型的特点。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐