Java面试知识点概览(持续更新)

Java

基础

类与对象的关系?

类是对象的抽象,对象时类的具体,类是对象的模板,对象是类的实例

Super与this表示什么?

Super表示当前类的父类对象
This表示当前类的对象

Collections和Collection有什么区别?

java.utilCollection是一个集合接口(集合类的一个顶级接口),它提供了对集合对象进行基本操作的通用接口方法,Collection接口在java类型中有很多具体的实现,Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接集成接口有List和Set

Collctions则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序,搜索以及线程安全等各种操作

在Queue中poll和remove有什么区别?

相同点:都是返回第一个元素,并在队列中删除返回的对象
不同点:如果没有元素poll会返回null,而remove会直接抛出NoSuchElementException异常

什么是隐式转换,什么是显式转换?

显示转换就是类型强转,把一个大类型的数据强制赋值给小类型的数据,隐式转换就是大范围的变量能够接收小范围的数据,隐式转换和显式转换其实就是自动类型转换和强制类型转换

java中有没有指针?

有指针,但是隐藏了,开发人员无法直接操作指针,由jvm来操作指针

java是值传递还是引用传递?

理论上来说,java都是引用传递,对于基本数据类型,传递是值的副本,而不是值本身,对于对象类型,传递是对象的引用,当在一个方法操作参数的时候,其实操作的是引用所指向的对象

假设吧实例化的数组的变量当成方法参数,当方法执行的时候改变了数组内的元素,那么在方法外,数组元素有发生改变吗?

改变了,因为传递是对象的引用,操作的是引用所指向的对象

throw与throws区别

  1. throws:用来声明一个方法可能产生的所有异常,不做任何处理而是将异常往上传,谁调用我我就抛给睡
  • 用在方法声明后面,跟的是异常类名
  • 可以跟多个异常类名,用逗号隔开
  • 表示抛出异常,由该方法的调用者来处理
  • throws表示出现异常的一种可能性,并不一定会发生这些异常
  1. throw:则是用来抛出一个具体的异常类型
  • 用在方法体内,跟的是异常对象名
  • 只能抛出一个异常对象名
  • 表示抛出异常,由方法体内的语句处理
  • throw则是抛出了异常,执行throw则一定抛出了某种异常

什么是Class文件?Class文件主要的信息结构有哪些?

Class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按顺序排列
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这样的伪结构仅仅有两种数据类型:无符号数和表
无符号数:是基本数据类型,以U1,U2,U4,U8分别代表一个字节,两个字节,四个字节,八个字节的无符号数,能够用来描写叙述数字,索引引用,数量值或者依照UTF-8编码构成的字符串值
表:由多个无符号数或者其他表作为数据项构成的符合数据类型,全部表习惯性的以_info结尾

形参与实参

形参:全称为:“形式参数”,是在定义方法名和方法体的时候使用的参数,用于接收调用该方法是传入的实际值
实参:全称为"实际参数",是调用该方法时传递给该方法的实际值

用代码演示三种代理

静态代理:
由程序员创建或工具生成代理类的源码,再编译代理类,所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托的关系在运行前就确定了
缺点:每个需要代理的对象都需要自己重复编写代理,很不舒服
优点:但是可以面相实际对象或者是接口的方式实现代理

动态代理:
也叫做JDK代理,接口代理,动态代理的对象,是利用JDK的API,动态的在内存中构建代理对象(是根据被代理的接口来动态生成代理类的class文件,并加载运行的过程),这就是动态代理
优点:不用关心代理类,只需要在运行阶段才指定代理哪一个对象

Java与语言特点

  1. ⾯向对象(封装,继承,多态)
  2. 平台⽆关性( Java 虚拟机实现平台⽆关性)
  3. 支持多线程
  4. ⽀持⽹络编程并且很⽅便( Java 语⾔诞⽣本身就是为简化⽹络编程设计的,因此 Java 语⾔不仅⽀持⽹络编程⽽且很⽅便)
  5. 编译与解释并存

分代收集算法

当前主流VM垃圾收集都采用分代收集(Fenerational Collection)算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的新生代,老年代,永久代,这样就可以根据个年代特点分别采用最适合的GC算法

Java中的编译器常量是什么?使用它有什么风险?

公共静态不可变(public static final)变量也即是我们所说的编译器常量,这里的public是可选的,实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变,这种存在的一个问题是你使用了一个内部的或第三方库中的共有编译时常量,当时这个值后面被其他人改变了,当时你的客户端仍然在使用老的值,甚至你已经部署了一个洗呢jar,为了避免这种情况,当你在更新依赖jar文件时,确保重新编译你的程序

什么是"依赖注入"和"控制反转"?

控制反转(IOC)是Spring框架的核心思想,用我自己的话说,就是你要做一件事,别自己可劲new了,你就说你要干啥,然后外包出去就好
依赖注入(DI)在我浅薄的想法中,就是通过接口的引用和构造方法的表达,将一些事情整好了反过来传给需要用到的地方

面向对象

⾯向对象易维护、易复⽤、易扩展。 因为⾯向对象有封装、继承、多态性的特
性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,⾯向对象性能
⽐⾯向过程低。

String str = “i” 和 String str = new String(“i”)一样吗?

不一样,因为内存的分配方式不一样,String str =“i” 的方式,Java虚拟机会将其分配到常量池中,而String str = new String (“i”)则会被分到堆内存中

如果对象的引用被设置为null,垃圾回收期是否会立即释放对象占用的内存

不会,在下一个垃圾回收周期中,这个对象将是可被回收的

是否了解连接池,使用连接池有什么好处?

数据库连接是非常消耗资源的,影响到程序的性能指标,连接池是用来分配,管理释放数据库连接的,可以使应用重复使用同一个数据库连接,而不是每次都创建一个新的数据库连接连接,通过释放空闲时间较长的数据库连接避免使用数据库因为创建太多的连接而造成的连接遗漏问题,提高了程序性能

你所了解的数据源技术有哪些?使用数据源有什么好处?

Dbcp,c3p0den,用的最多的还是c3p0,因为更加稳定,安全,通过配置文件的形式来维护数据库信息,而不是通过硬编码,当连接的数据库信息发生改变时,不需要再更改程序代码就实现了数据库信息的更新

抽象类能使用final修饰吗?

不能,定义抽象类就是让其他类继承的,如果定义为final该类就不能被基础,这样彼此就会产生矛盾,所以final不能修饰抽象类

Java数据类型

Java中数据类型分两种:
1.基本类型:long,int,byte,float,double,char
2.对象类型:Long,Integer,Byte,Float,Double其它一切java提供的,或者你自己创建的类。其中Long叫 long的包装类。Integer、Byte和Float也类似,一般包装类的名字首写是数值名的大写开头。
ID用long还是Long?
hibernate、el表达式等都是包装类型,用Long类型可以减少装箱/拆箱
在hibernate中的自增的hid在实体中的类型要用Long 来定义而不是long。否则在DWR的匹配过程中会出现Marshallingerror:null的错误提示。
到底是选择Long 还是long这个还得看具体环境,如果你认为这个属性不能为null,那么就用long,因为它默认初值为0,如果这个字段可以为null,那么就应该选择Long。

JVM JDK 和 JRE

什么是JVM?java虚拟机包括什么?

JVM:java虚拟机,运用硬件或软件手段实现的虚拟的计算机
Java虚拟机包括:寄存器,堆栈,处理器

JVM

Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果。

什么是字节码?采⽤字节码的好处是什么?
JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的⽂件),它不⾯向任何特定的处理器,只⾯向虚拟机。由于字节码并不针对⼀种特定的机器,因此,Java 程序⽆须重新编译便可在多种不同操作系统的计算机上运⾏。

Java程序从源代码到运行一般步骤:
.java文件(源代码)经过JDK中的javac编译,生成.class文件(JVM中可理解的Java字节),JVM生成机器可执行的二进制机器码

为什么java是编译与解释共存的语言?
.class->机器码 这⼀步。有些⽅法和代码块是经常需要被调⽤的(也就是所谓的热点代码),所以后⾯引进了 JIT 编译器,⽽ JIT 属于运行时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使⽤。机器码的运⾏效率肯定是⾼于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语⾔。

Java 既有解释执行,也有编译执行,为了解决解释器的性能瓶颈问题,优化 Java 的性能,引入了即时编译器,大幅度的提高运行效率。

java代码执行过程
Java面试知识点概览(持续更新)_第1张图片

JDK 和 JRE

JDK 是 Java Development Kit,它是功能⻬全的 Java SDK。
JRE 是 Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟(JVM),Java 类库,java 命令和其他的⼀些基础构件。

Java 和 C++的区别?

  • 都是⾯向对象的语⾔,都⽀持封装、继承和多态
  • Java 的类是单继承的,C++ ⽀持多重继承;虽然 Java 的类不可以多继承,但是接⼝可以多
    继承。
  • Java 有⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存

字符型常量和字符串常量的区别?

  1. 形式上: 字符常量是单引号引起的⼀个字符; 字符串常量是双引号引起的若⼲个字符
  2. 含义上: 字符常量相当于⼀个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表⼀个地
    址值
    (该字符串在内存中存放位置)
  3. 占内存⼤⼩ 字符常量只占 2 个字节; 字符串常量占若⼲个字节 (注意: char 在 Java 中占两
    个字节)

什么是B/S架构?什么是C/S架构?

B/S(Browser/Server),浏览器/服务器程序
C/S(Clent/Server),客户端/服务端,桌面应用程序

Java都有哪些开发平台?

JAVA SE:主要用在客户端开发
JAVA EE:主要用在web应用程序开发
JAVA ME:主要用在嵌入式应用程序开发

四大特性

面向对象思想OOP
抽象
关键词abstract声明的类叫作抽象类,abstract声明的⽅法叫抽象⽅法
⼀个类⾥包含了⼀个或多个抽象⽅法,类就必须指定成抽象类
抽象⽅法属于⼀种特殊⽅法,只含有⼀个声明,没有⽅法体
抽象支付:pay(金额,订单号),默认实现是本地支付,微信支付,支付宝支付,银行卡支付
封装
封装是把过程和数据包围起来,对数据的访问只能通过已定义的接⼝即⽅法
在java中通过关键字private,protected和public实现封装。
封装把对象的所有组成部分组合在⼀起,封装定义程序如何引⽤对象的数据,
封装实际上使⽤⽅法将类的数据隐藏起来,控制⽤户对类的修改和访问数据的程度。 适当的
封装可以让代码更容易理解和维护,也加强了代码的安全性
类封装
⽅法封装
继承
⼦类继承⽗类的特征行为,使得⼦类对象具有⽗类的方法属性(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。⽗类也叫基类,具有公共的⽅法和属性
动物<-猫
动物<-狗

        abstract class AbsPay{
        }
        WeixinPay extends AbsPay{
        }
        AliPay extends AbsPay{
        }

多态
所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
多态性分为编译时的多态性和运行时的多态性。方法重载实现的是编译时的多态性,而方法重写实现的是运行时的多态性。
优点:减少耦合、灵活可拓展
⼀般是继承类或者重写⽅法实现

构造器 Constructor 是否可被 override?

Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。

重写和重载的区别

重载Overload:表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同,参数个数或类型不同
重写Override:重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法
重写发⽣在运⾏期,是⼦类对⽗类的允许访问的⽅法的实现过程进⾏重新编写。

  1. 返回值类型、⽅法名、参数列表必须相同,抛出的异常范围⼩于等于⽗类,访问修饰符范围⼤于等于⽗类。
  2. 如果⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰的⽅法能够被再次声明。
  3. 构造⽅法⽆法被重写
    综上:重写就是⼦类对⽗类⽅法的重新改造,外部样⼦不能改变,内部逻辑可以改变

什么情况下会出现内存溢出,内存泄漏?

内存泄漏的原因很简单:

  1. 对象是可达的(一直被引用)
  2. 当时对象不会被使用

常见的内存泄漏的例子:
解决这个内存泄漏问题也很简单,将set设置为null,那就可以避免上述内存泄漏问题了,其他内存内存泄漏得一步一步分析了

内存溢出的原因:

  1. 内存溢出导致堆栈内存不断增大,从而引发内存溢出
  2. 大量的jar,class文件加载,装载类的空间不够,溢出
  3. 操作大量的对象导致对聂村空间已经用满了,溢出
  4. nio直接操作内存,内存过大导致溢出

解决:
查看程序是否存在内存泄漏的问题
设置参数加大空间
代码中是否存在死循环或者循环产生过多重复的对象实
查看是否使用nio直接操作内存

String StringBuffer 和 StringBuilder 的区别是什么?

String为什么是不可变的?
可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串, private final char value[] (在 Java 9 之后,String 类的实现改⽤ byte 数组存储字符串 private final byte[] value),所以 String 对象是不可变的。
⽽ StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是StringBuilder 与 StringBuffer 的公共⽗类,定义了⼀些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共⽅法。StringBuffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁,所以是线程安全的。StringBuilder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。

以StringBuffer的apend举例:
Java面试知识点概览(持续更新)_第2张图片
性能
每次对 String 类型进⾏改变的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String对象。StringBuffer 每次都会对 StringBuffer 对象本身进⾏操作,⽽不是⽣成新的对象并改变对象引⽤。
相同情况下使⽤ StringBuilder 相⽐使⽤ StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的⻛险

总结:

  1. 操作少量的数据: 适⽤ String
  2. 单线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuilder
  3. 多线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuffer

自动装箱与拆箱

装箱:将基本类型⽤它们对应的引⽤类型包装起来;
拆箱:将包装类型转换为基本数据类型;

在一个静态方法内调用⼀个非静态成员为什么是非法的?

由于静态⽅法可以不通过对象进⾏调⽤,因此在静态⽅法⾥,不能调用其他非静态变量,也不可以访问⾮静态变量成员。
Non-static field ‘a’ cannot be referenced from a static context

无参构造

Java 程序在执⾏⼦类的构造⽅法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造⽅法”。因此,如果⽗类中只定义了有参数的构造⽅法,⽽在⼦类的构造⽅法中⼜没有⽤ super() 来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为 Java 程序在⽗类中找不到没有参数的构造⽅法可供执⾏。解决办法是在⽗类⾥加上⼀个不做事且没有参数的构造⽅法。

接口

接口是否可以继承接口?接口是否支持多继承?类是否支持多继承?接口里面是否可以有方法实现?

接⼝⾥可以有静态⽅法和⽅法体
接⼝中所有的⽅法必须是抽象⽅法(JDK8之后就不是)
接⼝不是被类继承了,而是要被类实现
接⼝⽀持多继承, 类不⽀持多个类继承
⼀个类只能继承⼀个类,但是能实现多个接⼝,接⼝能继承另⼀个接⼝,接⼝的继承使⽤extends关键字,和类继承⼀样

JDK8接口新特性
interface中可以有static方法,但必须有⽅法实现体,该⽅法只属于该接⼝,接⼝名直接调⽤该⽅法
接⼝中新增default关键字修饰的方法,default⽅法只能定义在接⼝中,可以在⼦类或⼦接⼝ 中被重写default定义的⽅法必须有⽅法体
⽗接⼝的default⽅法如果在⼦接⼝或⼦类被重写,那么⼦接⼝实现对象、⼦类对象,调⽤该方法,以重写为准
本类、接⼝如果没有重写⽗类(即接⼝)的default⽅法,则在调⽤default⽅法时,使⽤⽗类(接口) 定义的default⽅法逻辑

接口和抽象类

  1. 接⼝的⽅法默认是 public ,所有⽅法在接⼝中不能有实现,即只能有抽象方法(Java 8 开始接⼝⽅法可以有默认实现),而抽象类可以有非抽象的⽅法。
  2. 接⼝中除了 static 、 final 变量,不能有其他变量,⽽抽象类中则不⼀定。
  3. ==⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。==接口自己本身可以通过 extends 关键字扩展多个接⼝。
  4. 接⼝⽅法默认修饰符是 public ,抽象⽅法可以有 public 、 protected 和 default 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)。
  5. 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,而接口是对行为的抽象,是⼀种行为的规范。

成员变量与局部变量的区别有哪些?

  1. 从语法形式上看:成员变量是属于类的,而局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  2. 从变量在内存中的存储⽅式来看:如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。
  3. 从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调用而⾃动消失。
  4. 成员变量如果没有被赋初值:则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值。

构造方法

⼀个类的构造⽅法的作⽤是什么? 若⼀个类没有声明构造⽅法,该程序能正确执⾏吗? 为什么?
主要作用是完成对类对象的初始化⼯作。可以执⾏。因为⼀个类即使没有声明构造⽅法也会有默认的不带参数的构造⽅法。

构造⽅法有哪些特性?

  1. 名字与类名相同。
  2. 没有返回值,但不能⽤ void 声明构造函数。
  3. ⽣成类的对象时⾃动执⾏,⽆需调⽤。

静态方法和实例方法有何不同

  1. 在外部调⽤静态⽅法时,可以使⽤"类名.⽅法名"的⽅式,也可以使⽤"对象名.⽅法名"的⽅式。⽽实例⽅法只有后⾯这种⽅式。也就是说,调⽤静态⽅法可以⽆需创建对象。
  2. 静态⽅法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态⽅法),⽽不允许访问实例成员变量和实例⽅法;实例⽅法则⽆此限制。

== 与 equals(重要)

两个等号,如果是基本数据类型判断的是值,引用数据类型判断的是内存地址
equals() : 它的作⽤也是判断两个对象是否相等。但它⼀般有两种使⽤情况:
情况 1:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

  • String 中的 equals ⽅法是被重写过的,因为 object 的 equals ⽅法是比较的对象的内存地址,而String 的 equals ⽅法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引⽤。如果没有就在常量池中重新创建⼀个 String 对象

hashCode 与 equals (重要)

hashCode

==hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数。==这个哈希码的作⽤是确定该对象在哈希表中的索引位置。 hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode ⽅法是本地⽅法,也就是⽤ c 语⾔或 c++ 实现的,该⽅法通常⽤来将对象的 内存地址 转换为整数之后返回。

public native int hashCode();

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就
利⽤到了散列码!(可以快速找到所需要的对象)

为什么重写 equals 时必须重写 hashCode ⽅法?

如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。

final关键字

主要⽤在三个地⽅:变量、⽅法、类。
变量:如果是基本数据类型,那么加上final字段后,其值就不能进行更改,如果是引用数据类型,那么就不能让其指向另一个对象

方法:1.锁定方法,防止继承类修改它的含义;2.是效率。在早期的 Java 实现版本中,会将 final ⽅法转为内嵌调⽤。但是如果⽅法过于庞大,可能看不到内嵌调⽤带来的任何性能提升(现在的 Java 版本已经不需要使⽤final ⽅法进⾏这些优化了)。类中所有的 private ⽅法都隐式地指定为 final。

:加了final字段的类不允许被继承,其中所有成员方法被隐式地在指定为final方法

异常

Java异常类结构层次图

Java面试知识点概览(持续更新)_第3张图片
在 Java 中,所有的异常都有⼀个共同的祖先 java.lang 包中的 Throwable 类。 Throwable 类有两个重要的子类** Exception (异常)**和 Error (错误)。Exception 能被程序本身处理( try catch ),Error 是⽆法处理的(只能尽量避免)。

异常处理总结

  • try 块: ⽤于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟⼀个 finally 块。
  • catch 块: ⽤于处理 try 捕获到的异常。
  • finally 块: ⽆论是否捕获或处理异常, finally 块⾥的语句都会被执⾏。当在 try 块或catch 块中遇到 return 语句时, finally 语句块将在⽅法返回之前被执⾏。

在以下 3 种特殊情况下, finally 块不会被执⾏:

  1. 在 try 或 finally 块中⽤了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常语句之后, finally 还是会被执⾏
  2. 程序所在的线程死亡。
  3. 关闭 CPU。

Java序列化

Java 序列化中如果有些字段不想进⾏序列化,怎么办?
使用transient或者transient注解
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。

键盘输入

⽅法 1:通过 Scanner

Scanner sc = new Scanner(System.in);

方法2:通过BufferedReader

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

JAVA注解

Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。

四种标准元注解

@Target

@Target说明了Annotation所修饰的对象范围: Annotation可被用于packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数。在 Annotation 类型的声明中使用了 target 可更加明晰其修饰的目标

@Retention 定义 被保留的时间长短

Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描
述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)由:
SOURCE:在源文件中有效(即源文件保留)
CLASS:在 class 文件中有效(即 class 保留)
RUNTIME:在运行时有效(即运行时保留)

@Documented 描述-javadoc

@ Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。

@Inherited 阐述了某个被标注的类型是被继承的

@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个annotation 将被用于该class 的子类。

注解处理器

 /1*** 定义注解*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
 /**供应商编号*/
public int id() default -1;
/*** 供应商名称*/
 public String name() default ""/** * 供应商地址*/
 public String address() default "";
}
//2:注解使用
public class Apple {
 @FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路")
 private String appleProvider;
 public void setAppleProvider(String appleProvider) {
 this.appleProvider = appleProvider;
 }
 public String getAppleProvider() {
 return appleProvider;
 } }/3*********** 注解处理器 ***************/
public class FruitInfoUtil {
 public static void getFruitInfo(Class<?> clazz) {
 String strFruitProvicer = "供应商信息:";
 Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
 for (Field field : fields) {
 if (field.isAnnotationPresent(FruitProvider.class)) {
 FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
//注解信息的处理地方 
strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
 + fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
 System.out.println(strFruitProvicer);
 }
 }
 } }
 public class FruitRun {
 public static void main(String[] args) {
 FruitInfoUtil.getFruitInfo(Apple.class);
/***********输出结果***************/
// 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延
 } }

反射

反射机制是什么

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

Person p=new Student();
其中编译时类型为 Person,运行时类型为 Student。

反射能做什么

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法;
  • 在运行时调用任意一个对象的方法;
  • 生成动态代理

I/O

分类

  • 按照流的流向划分:输入流和输出流
  • 按照操作单元划分:字节流和字符流
  • 按照留的角色划分:节点流和处理流

既然有了字节流,为什么还要有字符流?

不管是⽂件读写还是⽹络发送接收,信息的最⼩存储单元都是字节,那为什么I/O 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐好。

BIO,NIO,AIO 有什么区别?

BIO (Blocking I/O): 同步阻塞 I/O 模式
NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是异步⾮阻塞的 IO 模型。

深拷贝 vs 浅拷贝

浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容

异常

如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
Java面试知识点概览(持续更新)_第4张图片

异常分类

**Throwable **是 Java 语言中所有错误或异常的超类。下一层分为 **Error 和 Exception **
Error

  1. Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。

Exception(RuntimeException、CheckedException)
2. Exception 又有两个分支,一个是运行时异常 RuntimeException ,一个是CheckedException。
RuntimeException 如 : NullPointerException 、 ClassCastException ;一个是检查异常CheckedException,如 I/O 错误导致的 IOException、SQLException。 RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。

JAVA内部类

Java 类中不仅可以定义变量和方法,还可以定义类,这样定义在类内部的类就被称为内部类。根据定义的方式不同,内部类分为静态内部类,成员内部类,局部内部类,匿名内部类四种。
静态内部类:public static class Inner
成员内部类:public class Inner
局部内部类:定义在方法中的类,就是局部类。如果一个类只在某个方法中使用,则可以考虑使用局部类。

 public void test(final int c) {
 final int d = 1;
 class Inner {
 public void print() {
 System.out.println(c);
 }
 }
 }

匿名内部类:

test.test(new Bird() {
 public int fly() {
 return 10000;
 }
 public String getName() {
 return "大雁";
 }
 });

JDBC加载驱动

  1. 加载数据库驱动类
  2. 打开数据库链接
  3. 执行sql语句
  4. 处理返回结果
  5. 关闭资源

在使用jdbc的时候,如何防止出现sql注入

使用CallableStatement

怎么在JDBC内调用一个存储过程

使用PreparedStatement类,而不是使用Statement类

JAVA序列化

保存(持久化)对象及其状态到内存或者磁盘
Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。
Java 对象序列化就能够帮助我们实现该功能。

序列化对象以字节数组保持-静态成员不保存
使用 Java 对象序列化,==在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。==必须注意地是,==对象序列化保存的是对象的”状态”,即它的成员变量。==由此可知,对象序列化不会关注类中的静态变量。

序列化用户远程对象传输
==除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时,==都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。

Serializable 实现序列化
在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。
ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化
通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。writeObject 和 readObject 自定义序列化策略
在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。

序列化 ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

Transient 关键字阻止该变量被序列化到文件中

  1. 在变量声明前加上 Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
  2. 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

集合

Array 和 ArrayList 有何区别?

Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

Collection 和 Collections 有什么区别?

Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。

迭代器Iterator是什么?

地带起是一种设计模式,它是一个兑现,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层机构,迭代器通常被称为"轻量级"对象,因为创建它的代价小

怎么确保一个集合不能被修改?

可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java.langUnsupportedoperationException异常

List

Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类:分别是 ArrayList、Vector 和 LinkedList。

ArrayList(数组)

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

Vector(数组,线程同步)

Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。

LinkList(链表)

LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

Set

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。

说一下HashSet的实现原理?

HashSet底层由HashMap实现
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT

HashSet(Hash表)

哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。

说说List,Set,Map三者的区别?

List (对付顺序的好帮⼿): 存储的元素是有序的、可重复的。
Set (注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。
**Map **(⽤ Key 来搜索的专家): 使⽤键值对(kye-value)存储,类似于数学上的函数y=f(x),“x”代表 key,"y"代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。

RandomAccess 接⼝

public interface RandomAccess {
}

在这里插入图片描述
RandomAccess 接⼝中什么都没有定义。所以,RandomAccess 接⼝不过是⼀个标识罢了。标识什么? 标识实现这个接⼝的类具有随机访问功能。在 binarySearch ⽅法中,它要判断传⼊的 list 是否RamdomAccess 的实例,如果是,调用 indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法

比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排
序。

List

什么是 Fail-Fast、什么是 Fail-Safe?

Fail-Fast:一旦发现遍历的同时其他人来修改,则立刻抛出异常
Fail-Safe:发现遍历的同事其他人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成

  • ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败
  • CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

Vector和ArrayList、LinkedList联系和区别

从线程安全角度:
ArrayList:底层是数组实现,线程不安全,查询和修改非常快根,根据下标就可以进行操作时间复杂度1,但是增加和删除慢,需要移动大量的元素,时间复杂度n
LinkedList: 底层是双向链表,线程不安全,查询和修改速度慢,需要进行遍历操作,时间复杂度为n,但是增加和删除速度快,时间复杂度1
Vector: 底层是数组(Object[] )实现,线程安全的,操作的时候使用synchronized进行加锁
使用场景:
Vector已经很少用了
增加和删除场景多则用LinkedList
查询和修改多则用ArrayList

如果需要保证线程安全,ArrayList应该怎么做,用有几种方式

方式一:自己写个包装类,根据业务一般是add/update/remove加锁
方式二:Collections.synchronizedList(new ArrayList<>()); 使用synchronized加锁
//本质还是加锁
List list2 = Collections.synchronizedList(list1);
方式三:CopyOnWriteArrayList<>() 使用ReentrantLock加锁

CopyOnWriteArrayList和Collections.synchronizedList实现线程安全有什么区别

CopyOnWriteArrayList:执行修改操作时,会拷贝一份新的数组进行操作(add、set、remove等),代价十分昂贵,在执行完修改后将原来集合指向新的集合来完成修改操作,源码里面用ReentrantLock可重入锁来保证不会有多个线程同时拷贝一份数组
以添加元素源码举例:

    public boolean add(E e) {
        synchronized (lock) {
            Object[] es = getArray();
            int len = es.length;
            es = Arrays.copyOf(es, len + 1);
            es[len] = e;
            setArray(es);
            return true;
        }
    }

场景:读高性能,适用读操作远远大于写操作的场景中使用(读的时候是不需要加锁的,直接获取,删除和增加是需要加锁的, 读多写少)
Collections.synchronizedList:线程安全的原因是因为它几乎在每个方法中都使用了synchronized同步锁
场景:CopyOnWriteArrayList适合读多的场景,synchronizedList适合写多的场景
场景:写操作性能比CopyOnWriteArrayList好,读操作性能并不如CopyOnWriteArrayList

CopyOnWriteArrayList的设计思想是怎样的,有什么缺点?

设计思想:读写分离+最终一致
缺点:内存占用问题,写时复制机制,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象,如果对象过大(大对象会直接保存在老生代)则容易发生Yong GC和Full GC

ArrayList的扩容机制

注意:JDK1.7之前ArrayList默认大小是10,JDk1.8开始是未指定集合容量,默认是0,若已经指定的大小,(小于集合大小,小于10),当集合第一次添加元素的时候,集合大小扩容为10
ArrayList的元素个数大于其容量,扩容的大小=原始大小+原始大小/2

tips:关于ArraysList.addAll扩容机制详解
addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)
if(list.size=0&&addAll.size>10) then list.size = addAll.size else if ((addAll+list.size)> 下次扩容容量) then list.size扩容 = addAll+list.size else if((addAll+list.size)< 下次扩容容量) then list.size扩容 = 下次扩容的容量

Map

了解Map吗?用过哪些Map的实现

HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap
HashMap:底层是基于数组+链表,JDK8以后引入了红黑树,当链表大于8的时候,则会转成红黑树,非线程安全的,默认容量是16、允许有空的健和值
Hashtable:基于哈希表实现,线程安全的(加了synchronized),默认容量是11,不允许有null的健和值

HashMap 和 Hashtable 的区别

  1. HashMap 是⾮线程安全的, HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。
  2. 效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。
  3. 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。
  4. 初始容量⼤⼩和每次扩充容量⼤⼩的不同 :创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。

hashCode()和equals()

hashcode
顶级类Object里面的方法,所有的类都是继承Object,返回是一个int类型的数
根据一定的hash规则(存储地址,字段,长度等),映射成一个数组,即散列值
equals
顶级类Object里面的方法,所有的类都是继承Object,返回是一个boolean类型
根据自定义的匹配规则,用于匹配两个对象是否一样,一般逻辑如下
//判断地址是否一样
//非空判断和Class类型判断
//强转
//对象里面的字段一一匹配
使用场景:对象比较、或者集合容器里面排重、比较、排序

hashCode() 与 equals() 的相关规定:

  1. 如果两个对象相等,则 hashcode ⼀定也是相同的
  2. 两个对象相等,对两个 equals() ⽅法返回 true
  3. 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
  4. 综上, equals() ⽅法被覆盖过,则 hashCode() ⽅法也必须被覆盖
  5. hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)。

手写Hashcode和equals

public class User {
    private int age;
    private  String name;
    private Date time;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Date getTime() {
        return time;
    }
    public void setTime(Date time) {
        this.time = time;
    }
    @Override
    public int hashCode() {
        //int code = age/name.length()+time.hashCode();
        //return code
        return Objects.hash(age,name,time);
    }
    @Override
    public boolean equals(Object obj) {
        if(this == obj) return true;
        if(obj == null || getClass() != obj.getClass()) return false;
        User user = (User) obj;
        return age == user.age && Objects.equals(name, user.name) && Objects.equals(time, user.time);
    }
}

HashMap 多线程操作导致死循环问题

主要原因在于并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。

HashMap和TreeMap应该怎么选择,使用场景

hashMap: 散列桶(数组+链表),可以实现快速的存储和检索,但是确实包含无序的元素,适用于在map中插入删除和定位元素
treeMap:使用存储结构是一个平衡二叉树->红黑树,可以自定义排序规则,要实现Comparator接口,能便捷的实现内部元素的各种排序,但是一般性能比HashMap差,适用于安装自然排序或者自定义排序规则(写过微信支付签名工具类就用这个类)

Set和Map的关系

核心就是不保存重复的元素,存储一组唯一的对象
set的每一种实现都是对应Map里面的一种封装,
HashSet对应的就是HashMap,treeSet对应的就是treeMap

Set如何解决线程不安全问题

使用CopyOnWriteSet解决

常见Map的排序规则是怎样的?

按照添加顺序使用LinkedHashMap,按照自然排序使用TreeMap,自定义排序 TreeMap(Comparetor c)

如果需要线程安全,且效率高的Map,应该怎么做?

多线程环境下可以用concurrent包下的ConcurrentHashMap, 或者使用Collections.synchronizedMap(),
ConcurrentHashMap虽然是线程安全,但是他的效率比Hashtable要高很多

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤分段的数组+链表 实现,JDK1.8采⽤的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑⼆叉树。 Hashtable 和JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
实现线程安全的⽅式(重要):
① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁)对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和 CAS来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable (同⼀把锁) :使⽤** synchronized** 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不
能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。

为什么Collections.synchronizedMap后是线程安全的?

使用Collections.synchronizedMap包装后返回的map是加锁的

介绍下你了解的HashMap

索引计算

索引计算方法

  • 首先,计算对象的 hashCode()
  • 再进行调用 HashMap 的 hash() 方法进行二次哈希
    • 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
  • 最后 & (capacity – 1) 得到索引

数组容量为何是 2 的 n 次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

注意

  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable

树化与退化

树化意义

  • 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
  • hash 表的查找,更新的时间复杂度是 O ( 1 ) O(1) O(1),而红黑树的查找,更新的时间复杂度是 O ( l o g 2 ⁡ n ) O(log_2⁡n ) O(log2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

树化规则
Java面试知识点概览(持续更新)_第5张图片

  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

退化规则

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表

HashMap底层(数组+链表+红黑树 jdk8才有红黑树)
数组中每一项是一个链表,即数组和链表的结合体
Node[] table 是数组,数组的元素是Entry(Node继承Entry),Entry元素是一个key-value的键值对,它持有一个指向下个Entry的引用,table数组的每个Entry元素同时也作为当前Entry链表的首节点,也指向了该链表的下个Entry元素
在JDK1.8中,链表的长度大于8,链表会转换成红黑树

能否解释下什么是Hash碰撞?常见的解决办法有哪些,hashmap采用哪种方法

hash碰撞的意思是不同key计算得到的Hash值相同,需要放到同个bucket中
常见的解决办法:链表法、开放地址法、再哈希法,二次寻址法等
HashMap采用的是链表法

HashMap底层是 数组+链表+红黑树,为什么要用这几类结构呢?

数组 Node[] table ,根据对象的key的hash值进行在数组里面是哪个节点
链表的作用是解决hash冲突,将hash值一样的对象存在一个链表放在hash值对应的槽位,红黑树 JDK8使用红黑树来替代超过8个节点的链表,主要是查询性能的提升,从原来的O(n)到O(logn),通过hash碰撞,让HashMap不断产生碰撞,那么相同的key的位置的链表就会不断增长,当对这个Hashmap的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树

为啥选择红黑树而不用其他树,比如二叉查找树,为啥不一直开始就用红黑树,而是到8的长度后才变换?
二叉查找树在特殊情况下也会变成一条线性结构,和原先的链表存在一样的深度遍历问题,查找性能就会慢,使用红黑树主要是提升查找数据的速度,红黑树是平衡二叉树的一种,插入新数据后会通过左旋,右旋、变色等操作来保持平衡,解决单链表查询深度的问题
数据量少的时候操作数据,遍历线性表比红黑树所消耗的资源少,且前期数据少平衡二叉树保持平衡是需要消耗资源的,所以前期采用线性表,等到一定数之后变换到红黑树

说下hashmap的put和get的核心逻辑(JDK8以上版本)

put:

  1. 判断table是否为空或者长度为0,那么则进行扩容操作
  2. 否则hash分析命中那个桶是否有值,没有值的话,直接保存插入
  3. 有值的话,判断key值是否一样,一样的话进行覆盖操作
  4. 不一样的话,判断是否为树节点,如果为树节点,直接插入,此时的话已经是一颗红黑树了
  5. 如果不是树节点,则还是链表,那么需要进行的是遍历插入
  6. 插入进去之后,进行判断,其长度是否大于8,大于8,则转成红黑树,不大于8的话,直接保存插入.
    get:
  7. 判断table是否为空或者长度为0
  8. 根据key算出bucket.然后获取首节点,hash碰撞概率小,通常链表第一个节点就是值,没必要进行遍历
  9. 如果不只一个值,就需要循环遍历,存在多个hash碰撞
  10. 然后查找的时候判断是否是红黑树,是的话则调用树的查找,如果不是则代表是链表结构,循环遍历获取节点

了解ConcurrentHashMap吗?为什么性能比hashtable高?

ConcurrentHashMap线程安全的Map, hashtable类基本上所有的方法都是采用synchronized进行线程安全控制高并发情况下效率就降低ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化

jdk1.7和jdk1.8里面ConcurrentHashMap实现的区别

JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全
技术点:Segment+HashEntry
JKD8的版本:取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率,CAS(读)+Synchronized(写)
技术点:Node+Cas+Synchronized

说下ConcurrentHashMap的put的核心逻辑(JDK8以上版本)

不允许key或者value为空
spread(key.hashCode()) 二次哈希,减少碰撞概率
tabAt(i) 获取table中索引为i的Node元素
casTabAt(i) 利用CAS操作获取table中索引为i的Node元素
put的核心流程
1、key进行重哈希spread(key.hashCode())
2、对当前table进行无条件循环
3、如果没有初始化table,则用initTable进行初始化
4、如果没有hash冲突,则直接用cas插入新节点,成功后则直接判断是否需要扩容,然后结束
5、(fh = f.hash) == MOVED 如果是这个状态则是扩容操作,先进行扩容
6、存在hash冲突,利用synchronized (f) 加锁保证线程安全
7、如果是链表,则直接遍历插入,如果数量大于8,则需要转换成红黑树
8、如果是红黑树则按照红黑树规则插入
9、最后是检查是否需要扩容addCount()

并发

进程、线程、协程、程序

进程: 本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位

在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。

协程: 又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程就叫协程,一个线程可以多个协程,线程进程都是同步机制,而协程则是异步
Java的原生语法中并没有实现协程,目前python、Lua和GO等语言支持

关系:一个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最小执行单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗大量的CPU,CPU上真正运行的是线程,线程可以对应多个协程

程序:是含有指令和数据的⽂件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

继承Thread类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

实现Runnable接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。

public class MyThread extends OtherClass implements Runnable { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
}

ExecutorService、Callable、Future 有返回值线程

有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程

//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>(); 
for (int i = 0; i < taskSize; i++) { 
Callable c = new MyCallable(i + " "); 
// 执行任务并获取 Future 对象
Future f = pool.submit(c); 
list.add(f); 
} 
// 关闭线程池
pool.shutdown(); 
// 获取所有并发任务的运行结果
for (Future f : list) { 
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString()); 
}

基于线程池的方式

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

 // 创建线程池
 ExecutorService threadPool = Executors.newFixedThreadPool(10);
 while(true) {
 threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
 @Override
 public void run() {
 System.out.println(Thread.currentThread().getName() + " is running ..");
 try {
 Thread.sleep(3000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 });
 } }

四种线程池

Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而
只是一个执行线程的工具。真正的线程池接口是 ExecutorService。

newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。==调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。==因此,长时间保持空闲的线程池不会使用任何资源

newFixedThreadPool

==创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。==在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

 ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); 
 scheduledThreadPool.schedule(newRunnable(){ 
 @Override 
 public void run() {
 System.out.println("延迟三秒");
 }
 }, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){ 
 @Override 
 public void run() {
 System.out.println("延迟 1 秒后每三秒执行一次");
 }
 },1,3,TimeUnit.SECONDS);

newSingleThreadExecutor

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

如果你提交任务时,线程池队列已满,这时候会发生什么?

两种可能:

  1. 如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
  2. 如果使用的是有界队列比如ArrayBlockingQueue中,ArrayBlock满了,会根据maximumPoolSize的值增加线程数量,如果还是处理不过来导致线程池再次满的话,那么则会触发线程池的拒绝策略RejectedExecutionHandler处理满了的任务

协程对于多线程优缺点

优点:
非常快速的上下文切换,不用系统内核的上下文切换,减小开销
单线程即可实现高并发,单核CPU可以支持上万的协程
由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁
缺点:
协程无法利用多核资源,本质也是个单线程
协程需要和进程配合才能运行在多CPU上
目前java没成熟的第三方库,存在风险
调试debug存在难度,不利于发现问题

为什么要使用多线程?

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

再深⼊到计算机底层来探讨:

单核时代: 在单核时代多线程主要是为了提⾼ CPU 和 IO 设备的综合利⽤率。举个例⼦:当只有⼀个线程的时候会导致 CPU 计算时,IO 设备空闲;进⾏ IO 操作时,CPU 空闲。我们可以简单地说这两者的利⽤率⽬前都是 50%左右。但是当有两个线程的时候就不⼀样了,当⼀个线程执⾏ CPU 计算时,另外⼀个线程可以进⾏ IO 操作,这样两个的利⽤率就可以在理想情况下达到 100%了。

多核时代: 多核时代多线程主要是为了提高CPU利用率。举个例⼦:假如我们要计算⼀个复杂的任务,我们只⽤⼀个线程的话,CPU 只会⼀个 CPU 核⼼被利⽤到,⽽创建多个线程就可以让多个 CPU 核⼼被利⽤到,这样就提⾼了 CPU 的利⽤率。

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

并发编程的⽬的就是为了能提高程序的执行效率和提高程序运行速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下文切换、死锁

什么是上下文切换?

多线程编程中⼀般线程的个数都⼤于 CPU 核心的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使用,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当⼀个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
概括来说就是:当前任务在执⾏完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。

上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。

并发和并行的区别

串行:串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。相当于一条流水线执行一组任务一样
并发 concurrency一台处理器上同时处理任务, 这个同时实际上是交替处理多个任务,程序中可以同时拥有两个或者多个线程,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,并发指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。要解决大并发问题,通常是将大任务分解成多个小任务
并行 parallellism:多个CPU上同时处理多个任务,一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行

并发指在一段时间内宏观上去处理多个任务。 并行指同一个时刻,多个任务确实真的同时运行。

例子:
并发是一心多用,听课和看电影,但是CPU大脑只有一个,所以轮着来
并行:火影忍者中的影分身,有多个你出现,可以分别做不同的事情

一个项目经理A和3个程序BCD的故事
单线程:A给B讲完,等B做完,给C讲,等C完成,给D讲,等D完成
并发:A给B讲完需求,B自己去实现,期间A继续给C和D讲,不用等待某个程序员去完成,期间项目经理没空闲下来
并行:直接找3个项目经理分别分配给3个程序员

你知道java里面实现多线程有哪几种方式,有什么不同,比较常用哪种

  • 继承Thread
    继承Thread,重写里面run方法,创建实例,执行start
    优点:代码编写最简单直接操作
    缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差
public class ThreadDemo1 extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread实现多线程,名称:"+Thread.currentThread().getName());
    }
}
public static void main(String[] args) {
      ThreadDemo1 threadDemo1 = new ThreadDemo1();
      threadDemo1.setName("demo1");
      threadDemo1.start();
      System.out.println("主线程名称:"+Thread.currentThread().getName());
}
  • 实现Runnable
    自定义类实现Runnable,实现里面run方法,创建Thread类,使用Runnable接口的实现对象作为参数传递给Thread对象,调用Strat方法
    优点:线程类可以实现多个几接口,可以再继承一个类
    缺点:没返回值,不能直接启动,需要通过构造一个Thread实例传递进去启动
public class ThreadDemo2 implements Runnable {
    @Override
    public void run() {
        System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());
    }
}
public static void main(String[] args) {
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        Thread thread = new Thread(threadDemo2);
        thread.setName("demo2");
        thread.start();
        System.out.println("主线程名称:"+Thread.currentThread().getName());
}
JDK8之后采用lambda表达式
public static void main(String[] args) {
    Thread thread = new Thread(()->{
                System.out.println("通过Runnable实现多线程,称:"+Thread.currentThread().getName());
            });
    thread.setName("demo2");
    thread.start();
    System.out.println("主线程名称:"+Thread.currentThread().getName());
}
  • 通过Callable和FutureTask方式
    创建callable接口的实现类,并实现call方法,结合FutureTask类包装Callable对象,实现多线程
    优点:有返回值,拓展性也高
    缺点:jdk5以后才支持,需要重写call方法,结合多个类比如FutureTask和Thread类
public class MyTask implements Callable<Object> {
    @Override
    public Object call() throws Exception {
        System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
        return "这是返回值";
    }
}
 public static void main(String[] args) {
        FutureTask<Object> futureTask = new FutureTask<>(()->{
            System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
            return "这是返回值";
        });
//        MyTask myTask = new MyTask();
//        FutureTask futureTask = new FutureTask<>(myTask);
        //FutureTask继承了Runnable,可以放在Thread中启动执行
        Thread thread = new Thread(futureTask);
        thread.setName("demo3");
        thread.start();
        System.out.println("主线程名称:"+Thread.currentThread().getName());
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            //阻塞等待中被中断,则抛出
            e.printStackTrace();
        } catch (ExecutionException e) {
            //执行过程发送异常被抛出
            e.printStackTrace();
        }
    }
 
  
  • 通过线程池创建线程
    自定义Runnable接口,实现run方法,创建线程池,调用执行方法并传入对象
    优点:安全高性能,复用线程
    缺点: jdk5后才支持,需要结合Runnable进行使用
public class ThreadDemo4 implements Runnable {
    @Override
    public void run() {
        System.out.println("通过线程池+runnable实现多线程,名称:"+Thread.currentThread().getName());
    }
}
public static void main(String[] args) {
		//指定线程池的大小为3
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for(int i=0;i<10;i++){
            executorService.execute(new ThreadDemo4());
        }
        System.out.println("主线程名称:"+Thread.currentThread().getName());
        //关闭线程池
        executorService.shutdown();
}
  1. corePoolSize 核心线程数目 - 池中会保留的最多线程数
  2. maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
  3. keepAliveTime 生存时间 - 救急线程(也就是)的生存时间,生存时间内没有新任务,此线程资源会释放
  4. unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
  5. workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
  6. threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  7. handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
    抛异常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy
    由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
    丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
    丢弃最早排队任务 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy

线程池中,有四个重要的参数,决定影响了拒绝策略:
corePoolSize - 核心线程数,也即最小的线程数。
workQueue - 阻塞队列 。
maximumPoolSize -最大线程数
拒绝策略

当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

线程池

为什么要使用线程池

池化技术相⽐⼤家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应⽤。池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。

线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。 每个线程池还维护⼀些基本统计信息,例如已完成任务的数量。

使用线程池的好处
  • 降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
  • 提高线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。
线程池拒绝策略:

CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
DiscardPolicy: 直接丢弃,其他啥都没有
DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列workQueue 中最老的一个任务,并将新任务加入

自定义线程池:

自定义线程池:常驻线程数量,最大线程数量,过期时间,单位,阻塞队列,线程工厂,拒绝策略
ThreadPoolExecutor(2, 5, 2L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等与maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

执⾏ execute()方法和 submit()⽅法的区别是什么呢?
  1. execute() ⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;
  2. submit() ⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这个 Future对象可以判断任务是否执⾏成功,并且可以通过 Future 的 get() ⽅法来获取返回值, get() ⽅法会阻塞当前线程直到任务完成,⽽使⽤ get( long timeout, TimeUnit unit) ⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。

Runnable和Callable接口区别

(1)是否有返回值
(2)是否抛出异常
(3)实现方法名称不同,一个是run方法,一个是call方法

java线程状态

JDK的线程状态分6种,JVM里面9种,我们一般说JDK的线程状态
常见的5种状态
创建(NEW): 生成线程对象,但是并没有调用该对象start(), new Thread()
就绪(Runnable):当调用线程对象的start()方法,线程就进入就绪状态,但是此刻线程调度还没把该线程设置为当前线程,就是没获得CPU使用权。 如果线程运行后,从等待或者睡眠中回来之后,也会进入就绪状态
运行(Running):程序将处于就绪状态的线程设置为当前线程,即获得CPU使用权,这个时候线程进入运行状态,开始运行run里面的逻辑
注意:有些文档把就绪和运行两种状态统一称为 “运行中”
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回
阻塞(Blocked)

  • 等待阻塞:进入该状态的线程需要等待其他线程作出一定动作(通知或中断),这种状态的话CPU不会分配过来,他们需要被唤醒,可能也会无限等待下去。比如调用wait(状态就会变成WAITING状态),也可能通过调用sleep(状态就会变成TIMED_WAITING), join或者发出IO请求,阻塞结束后线程重新进入就绪状态
  • 同步阻塞:线程在获取synchronized同步锁失败,即锁被其他线程占用,它就会进入同步阻塞状态

死亡(TERMINATED):一个线程run方法执行结束,该线程就死亡了,不能进入就绪状态

lock vs synchronized

三个层面

不同点

  • 语法层面
    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
    • Lock 是接口,源码由 jdk 提供,用 java 语言实现
    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
  • 性能层面
    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能

公平锁

  • 公平锁的公平体现
    • 已经处在阻塞队列中的线程(不考虑超时)始终都是公平的,先进先出
    • 公平锁是指未处于阻塞队列中的线程来争抢锁,如果队列不为空,则老实到队尾等待
    • 非公平锁是指未处于阻塞队列中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的
  • 公平锁会降低吞吐量,一般不用

条件变量

  • ReentrantLock 中的条件变量功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构
  • 与 synchronized 的等待集合不同之处在于,ReentrantLock 中的条件变量可以有多个,可以实现更精细的等待、唤醒控制

多线程开发常用方法

  • sleep
    属于线程Thread的方法
    让线程暂缓执行,等待预计时间之后再恢复
    交出CPU使用权,不会释放锁
    进入阻塞状态TIME_WAITGING,睡眠结束变为就绪Runnable
  • yield
    属于线程Thread的方法
    t1/t2/t3
    暂停当前线程的对象,去执行其他线程
    交出CPU使用权,不会释放锁,和sleep类似
    作用:让相同优先级的线程轮流执行,但是不保证一定轮流
    注意:不会让线程进入阻塞状态,直接变为就绪Runnable,只需要重新获得CPU使用权
  • join
    属于线程Thread的方法
    在主线程上运行调用该方法,会让主线程休眠,不会释放已经持有的对象锁
    让调用join方法的线程先执行完毕,在执行其他线程
    类似让救护车警车优先通过
  • wait
    属于Object的方法
    当前线程调用对象的wait方法,会释放锁,进入线程的等待队列
    需要依靠notify或者notifyAll唤醒,或者wait(timeout)时间自动唤醒
  • notify
    属于Object的方法
    唤醒在对象监视器上等待的单个线程,选择是任意
  • notifyAll
    属于Object的方法
    唤醒在对象监视器上等待的全部线程

说说 sleep() ⽅法和 wait() 方法区别和共同点?

两者最主要的区别在于: sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁

两者都可以暂停线程的执⾏。
wait() 通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执⾏。
线程是否会自动苏醒:wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或 者 notifyAll() ⽅法。 sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout) 超时后线程会⾃动苏醒。

start与run区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

为什么我们调用 start() 方法时会执行 run() 方法?

new ⼀个 Thread,线程进⼊了新建状态。调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间片后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏ run() ⽅法的内容,这是真正的多线程⼯作。 但是,直接执⾏ run() ⽅法,会把 run()⽅法当成⼀个 main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
总结: 调用 start() 方法方可启动线程并使线程进⼊就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

线程池中 submit() 和 execute() 方法有什么区别?

execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。

多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

平时业务代码里面使用过多线程吗,能举例几个多线程的业务场景吗?

异步任务:用户注册、记录日志
定时任务:定期备份日志、备份数据库
分布式计算:Hadoop处理任务mapreduce,master-wark(单机单进程)
服务器编程:Socket网络编程,一个连接一个线程

说一下 atomic 的原理?

atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法来保证原子操作,从而避免synchronized 的高开销,执行效率大为提升。

能举几个不是线程安全的数据结构吗?

HashMap、ArrayList、LinkedList

JAVA 后台线程

  1. 定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
  2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
  3. 设置:通过 **setDaemon(true)**来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在线程对象创建 之前 用线程对象的 setDaemon 方法。
  4. 在 Daemon 线程中产生的新线程也是 Daemon 的。
  5. 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。
  6. example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,==所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。==它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
  7. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

sleep() 和 wait() 有什么区别?

类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。

CAS

CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时
使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂
起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,
CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

ThreadLocal 了解么?

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。
如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使⽤ get()和 set()⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
再举个简单的例⼦:
⽐如有两个⼈去宝屋收集宝物,这两个共⽤⼀个袋⼦的话肯定会产⽣争执,但是给他们两个⼈每个⼈分配⼀个袋⼦的话就不会出现这样的问题。如果把这两个⼈⽐作线程的话,那么ThreadLocal 就是⽤来避免这两个线程竞争的。

ThreadLocal 内部维护的是⼀个类似 Map 的 ThreadLocalMap 数据结构, key 为当前对象的 Thread 对象,值为 Object 对象。

ThreadLocal 内存泄露问题了解不?

==ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。==所以,如果ThreadLocal 没有被外部强引⽤的情况下,==在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。==这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法

在Java中可以有哪些方法来保证线程安全

  • 加锁,比如synchronize/ReentrantLock
  • 使用volatile声明变量,轻量级同步,不能保证原子性(需要解释)
  • 使用线程安全类(原子类AtomicXXX,并发容器,同步容器CopyOnWriteArrayList/ConcurrentHashMap等
  • ThreadLocal本地私有变量/信号量Semaphore等

读写锁

第一:无锁状态,多线程抢夺资源,乱
第二:使用synchronized和ReentrantLock,都是独占的,每次只能来一个操作,读读1,读写1,写写1
第三:读写锁 reentrantReadWriteLock,读读,可共享,提升性能,同时多人进行读操作,写写1
reentrantReadWriteLock缺点(1):造成锁饥饿,一直读,没有写操作(2)读时候,不能进行写操作,只有完成之后才能进行写操作,写操作可以读

volatile关键字

原子性

  • 起因:多线程下,不同线程的指令发生了交错导致的共享变量的读写混乱
  • 解决:用悲观锁或乐观锁解决,volatile 并不能解决原子性

可见性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致的对共享变量所做的修改另外的线程看不到
  • 解决:用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

有序性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致
  • 解决:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
  • 注意:
    • volatile 变量写加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
    • volatile 变量读加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
    • volatile 读写加入的屏障只能防止同一线程内的指令重排

volatile和synchronized区别

volatile是轻量级的synchronized,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象
volatile:保证可见性,但是不能保证原子性
synchronized:保证可见性,也保证原子性
使用场景
1、不能修饰写入操作依赖当前值的变量,比如num++、num=num+1,不是原子操作,肉眼看起来是,但是JVM字节码层面不止一步
2、由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱

为什么会出现脏读?

JAVA内存模型简称 JMM
JMM规定所有的变量存在在主内存,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,不能直接对主内存就行操作,使用volatile修饰变量,每次读取前必须从主内存属性获取最新的值,每次写入需要立刻写到主内存中
volatile关键字修修饰的变量随时看到的自己的最新值,假如线程1对变量v进行修改,那么线程2是可以马上看见

volatile可以避免指令重排,能否解释下什么是指令重排指令重排序分两类 编译器重排序和运行时重排序

JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)
int a = 3 //1
int b = 4 //2
int c =5 //3
int h = abc //4
定义顺序 1,2,3,4
计算顺序 1,3,2,4 和 2,1,3,4 结果都是一样
虽然指令重排序可以提高执行效率,但是多线程上可能会影响结果,有什么解决办法?
解决办法:内存屏障
解释:内存屏障是屏障指令,使CPU对屏障指令之前和之后的内存操作执行结果的一种约束

happens-before

先行发生原则,volatile的内存可见性就体现了该原则之一
例子:
//线程A操作
int k = 1;
//线程B操作
int j = k;
//线程C操作
int k = 2
分析:
假设线程A中的操作“k=1”先行发生于线程B的操作“j=k”,那确定在线程B的操作执行后,变量j的值一定等于1,依据有两个:一是先行发生原则,“k=1”的结果可以被观察到;二是第三者线程C还没出现,线程A操作结束之后没有其他线程会修改变量k的值。
但是考虑线程C出现了,保持线程A和线程B之间的先行发生关系,线程C出现在线程A和线程B的操作之间,但是线程C与线程B没有先行发生关系,那j的值会是多少?答案是1和2都有可能,因为线程C对变量k的影响可能会被线程B观察到,也可能不会,所以线程B就存在读取到不符合预期数据的风险,不具备多线程安全性
八大原则
1、程序次序规则
2、管程锁定规则
3、volatile变量规则
4、线程启动规则
5、线程中断规则
6、线程终止规则
7、对象终结规则
8、传递性

并发编程三要素

原子性:一个不可再被分割的颗粒,原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题

int num = 1; // 原子操作
num++; // 非原子操作,从主内存读取num到线程工作内存,进行 +1,再把num写到主内存, 除非用原子类,即java.util.concurrent.atomic里的原子变量类
解决办法是可以用synchronized 或 Lock(比如ReentrantLock) 来把这个多步操作“变成”原子操作,但是volatile,前面有说到不能修饰有依赖值的情况

public class XdTest {
    private int num = 0;
    //使用lock,每个对象都是有锁,只有获得这个锁才可以进行对应的操作
    Lock lock = new ReentrantLock();
    public  void add1(){
        lock.lock();
        try {
            num++;
        }finally {
            lock.unlock();
        }
    }
    //使用synchronized,和上述是一个操作,这个是保证方法被锁住而已,上述的是代码块被锁住
    public synchronized void add2(){
        num++;
    }
}

解决核心思想:把一个方法或者代码块看做一个整体,保证是一个不可分割的整体

有序性: 程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序,JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)(volatile禁止了指令重排)
int a = 3 //1
int b = 4 //2
int c =5 //3
int h = abc //4
上面的例子 执行顺序1,2,3,4 和 2,1,3,4 结果都是一样,指令重排序可以提高执行效率,但是多线程上可能会影响结果
假如下面的场景,正常是顺序处理
//线程1
before();//处理初始化工作,处理完成后才可以正式运行下面的run方法
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){
run(); //核心业务代码
}
指令重排序后,导致顺序换了,程序出现问题,且难排查
//线程1
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){
run(); //核心业务代码
}
before();//处理初始化工作,处理完成后才可以正式运行下面的run方法

可见性: 一个线程A对共享变量的修改,另一个线程B能够立刻看到

// 线程 A 执行
int num = 0;
// 线程 A 执行
num++;
// 线程 B 执行
System.out.print("num的值:" + num);

线程A执行 i++ 后再执行线程 B,线程 B可能有2个结果,可能是0和1。
因为 i++ 在线程A中执行运算,并没有立刻更新到主内存当中,而线程B就去主内存当中读取并打印,此时打印的就是0;也可能线程A执行完成更新到主内存了,线程B的值是1。
所以需要保证线程的可见性
synchronized、lock和volatile能够保证线程可见性

调度算法

先来先服务调度算法
按照作业/进程到达的先后顺序进行调度 ,即:优先考虑在系统中等待时间最长的作业
排在长进程后的短进程的等待时间长,不利于短作业/进程
短作业优先调度算法
短进程/作业(要求服务时间最短)在实际情况中占有很大比例,为了使得它们优先执行
对长作业不友好
高响应比优先调度算法:
在每次调度时,先计算各个作业的优先权:优先权=响应比=(等待时间+要求服务时间)/要求服务时间,
因为等待时间与服务时间之和就是系统对该作业的响应时间,所以 优先权=响应比=响应时间/要求服务时间,选择优先权高的进行服务需要计算优先权信息,增加了系统的开销
时间片轮转调度算法:
轮流的为各个进程服务,让每个进程在一定时间间隔内都可以得到响应
由于高频率的进程切换,会增加了开销,且不区分任务的紧急程度
优先级调度算法:
根据任务的紧急程度进行调度,高优先级的先处理,低优先级的慢处理
如果高优先级任务很多且持续产生,那低优先级的就可能很慢才被处理

线程的调度策略

线程调度器选择优先级最高的线程运行,但是,如果发生一下情况就回终止线程的运行:

  1. 线程体中调用了yield方法让出了对cpu的占用权利
  2. 线程体中调用了sleep方法时线程进入睡眠状态
  3. 线程由于IO操作受到阻塞
  4. 另外一个更高优先级线程出现
  5. 在支持时间片的系统中,该线程的时间片用完

常见的线程间的调度算法

线程调度是指系统为线程分配CPU使用权的过程,主要分两种
协同式线程调度(分时调度模式)线程执行时间由线程本身来控制,==线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。==最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里
抢占式线程调度每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞

Java线程调度就是抢占式调度,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择一个线程
所以我们如果希望某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。
JAVA的线程的优先级,以1到10的整数指定。当多个线程可以运行时,VM一般会运行最高优先级的线程(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)
在两线程同时处于就绪runnable状态时,优先级越高的线程越容易被系统选择执行。但是优先级并不是100%可以获得,只不过是机会更大而已。

有人会说,wait,notify不就是线程本身控制吗?
其实不是,wait是可以让出执行时间,notify后无法获取执行时间,随机等待队列里面获取而已

java常见的锁

悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized

乐观锁:每次去拿数据的时候都认为别人不会修改,更新的时候会判断是别人是否会去更新数据,通过版本号来判断,如果数据被修改了就拒绝更新,在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作,比如CAS是乐观锁,但严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败
小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁多

公平锁:指多个线程按照申请锁的顺序来获取锁,简单来说 如果一个线程组里,能保证每个线程都能拿到锁 比如ReentrantLock(底层是同步队列FIFO:First Input First Output来实现)
非公平锁获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,一直拿不到锁,比如synchronizedReentrantLock :在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
小结:
非公平锁:效率高因为能重复利用CPU的时间,不过可能存在线程饿死
公平锁:效率相对较低

可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,synchronized(隐式)和lock(显式)
synchronized:

        Object o = new Object();
        new Thread(()->{
            synchronized (o){
                System.out.println(Thread.currentThread().getName()+"外层");
                synchronized (o){
                    System.out.println(Thread.currentThread().getName()+"中层");
                    synchronized (o){
                        System.out.println(Thread.currentThread().getName()+"内层");

                    }
                }
            }
        },"T1").start();

lock:

        Lock lock = new ReentrantLock();
        new Thread(()->{
            lock.lock();
            System.out.println("aaaa");
            lock.unlock();
        },"aa").start();

        new Thread(()->{
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"外层");
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName()+"中层");
                    try {
                        lock.lock();
                        System.out.println(Thread.currentThread().getName()+"内层");

                    }finally {
                        lock.unlock();
                    }
                }finally {
                    lock.unlock();
                }
            }finally {
                lock.unlock();
            }
        },"T1").start();

不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞

小结:可重入锁能一定程度的避免死锁 synchronized、ReentrantLock 重入锁

    private void meathA(){
            //获取锁 TODO
        meathB();
    }
    private void meathB(){
            //获取锁 TODO
            //其他操作
    }

分段锁、行锁、表锁

自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁.

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
小结:不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU
常见的自旋锁:TicketLock,CLHLock,MSCLock

共享锁:也叫S锁/读锁,能查看但无法修改和删除的一种数据锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享
互斥锁:也叫X锁/排它锁/写锁/独占锁/独享锁/ 该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果 线程A 对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得互斥锁的线程即能读数据又能修改数据

Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现
ReentrantReadWriteLock。

分段锁:分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践

下面三种是Jvm为了提高锁的获取与释放效率而做的优化针对Synchronized的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程,
偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,获取锁的代价更低
轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但不会阻塞,且性能会高点
重量级锁:当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会一直循环下去,当自旋一定次数的时候且还没有获取到锁,就会进入阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能也会降低

死锁

两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去
死锁代码:

        new Thread(()->{
            synchronized (a){
                System.out.println(Thread.currentThread().getName()+"持有a,试图获取b");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){
                    System.out.println(Thread.currentThread().getName()+"获取b");

                }

            }

        },"a").start();
        new Thread(()->{
            synchronized (b){
                System.out.println(Thread.currentThread().getName()+"持有b,试图获取a");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a){
                    System.out.println(Thread.currentThread().getName()+"获a");

                }
            }

        },"b").start();
如何查看死锁?
jps 
jstack 30288
死锁的4个必要条件

互斥条件:资源不能共享,只能由一个线程使用
请求与保持条件:线程已经获得一些资源,但因请求其他资源发生阻塞,对已经获得的资源保持不释放
不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强行回收,只能由线程使用完自己释放
循环等待条件:多个线程形成环形链,每个都占用对方申请的下个资源

只要发生死锁,上面的条件都成立;只要一个不满足,就不会发生死锁

如何避免死锁?
  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件 :⼀次性申请所有的资源。
  3. 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

synchronized

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
synchronized是解决线程安全的问题,常用在 同步普通方法、静态方法、代码块中
是非公平锁和可重入锁
每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性

两种形式:
方法:生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象,也叫隐式同步
代码块:加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令,每个monitor维护着一个记录着拥有次数的计数器, 未被拥有的monitor的该计数器为0,当一个线程获执行monitorenter后,该计数器自增1;当同一个线程执行monitorexit指令的时候,计数器再自减1。当计数器为0的时候,monitor将被释放.也叫显式同步
两种本质上没有区别,底层都是通过monitor来实现同步, 只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成

jdk1.6后进行了优化,你知道哪些大的变化
有得到锁的资源进入Block状态,涉及到操作系统用户模式和内核模式的切换,代价比较高
jdk6进行了优化,增加了从偏向锁到轻量级锁再到重量级锁的过渡,但是在最终转变为重量级锁之后,性能仍然较低

说说自己是怎么使用 synchronized 关键字

synchronized 关键字最主要的三种使⽤⽅式:
1.修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁

synchronized void method() {
 //业务代码
}

作用于方法时,锁住的是对象的实例(this);

2.修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得 当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。所以,如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。

synchronized void staic method() {
 //业务代码
}

当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;

3.修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized( .class) 表示进⼊同步代码前要获得 当前 class 的锁

synchronized(this) {
 //业务代码
}

总结:

  • synchronized 关键字加到 static 静态⽅法和 synchronized(class) 代码块上都是是给 Class类上锁。
  • synchronized 关键字加到实例⽅法上是给对象实例上锁。尽量不要使⽤ synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

双重校验锁实现对象单例(线程安全)

public class Singleton {
 private volatile static Singleton uniqueInstance;
 private Singleton() {
 }
 public static Singleton getUniqueInstance() {
 //先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
 if (uniqueInstance == null) {
 //类对象加锁
 synchronized (Singleton.class) {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 }
 }
 return uniqueInstance;
 }
}

另外,需要注意 uniqueInstance 采⽤ volatile 关键字修饰也是很有必要。uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执⾏:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址
    但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不
    会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执
    ⾏了 1 和 3,此时 T2 调⽤ getUniqueInstance () 后发现 uniqueInstance 不为空,因此返回
    uniqueInstance ,但此时 uniqueInstance 还未被初始化。
    使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。

构造方法可以使⽤ synchronized 关键字修饰么?

构造⽅法本身就属于线程安全的,不存在同步的构造⽅法⼀说。

讲⼀下 synchronized 关键字的底层原理

synchronized 同步语句块的情况

Java面试知识点概览(持续更新)_第6张图片
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。

当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在执⾏ monitorenter 时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执⾏ monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前
线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。

synchronized 修饰方法的的情况

Java面试知识点概览(持续更新)_第7张图片
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调
⽤。

总结:
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。

不过两者的本质都是对对象监视器 monitor 的获取。

ReentrantLock

ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。

ReentrantLock 与 synchronized

  1. ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会 被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
  2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock。

Semaphore 信号量

Semaphore 是一种基于计数的信号量。==它可以设定一个阈值,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。===Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池
实现互斥锁(计数器为 1)
我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,
表示两种互斥状态。

多线程的5种通信方式

问题:有两个线程,A 线程向一个集合里面依次添加元素“abc”字符串,一共添加十次,当添加到第五次的时候,希望 B 线程能够收到 A 线程的通知,然后 B 线程执行相关的业务操作。线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。

  • 使用 volatile 关键字
    基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想。大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
public class TestSync {
    //定义共享变量来实现通信,它需要volatile修饰,否则线程不能及时感知
    static volatile boolean notice = false;

    public static void main(String[] args) {
        List<String>  list = new ArrayList<>();
        //线程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                list.add("abc");
                System.out.println("线程A添加元素,此时list的size为:" + list.size());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (list.size() == 5)
                    notice = true;
            }
        });
        //线程B
        Thread threadB = new Thread(() -> {
            while (true) {
                if (notice) {
                    System.out.println("线程B收到通知,开始执行自己的业务...");
                    break;
                }
            }
        });
        //需要先启动线程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 再启动线程A
        threadA.start();
    }
}
  • 使用 Object 类的 wait()/notify()
    Object 类提供了线程间通信的方法:wait()、notify()、notifyAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
    注意:wait/notify 必须配合 synchronized 使用,wait 方法释放锁,notify 方法不释放锁。wait 是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify(),notify并不释放锁,只是告诉调用过wait()的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放,调用 wait() 的一个或多个线程就会解除 wait 状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。
public class TestSync {
    public static void main(String[] args) {
        //定义一个锁对象
        Object lock = new Object();
        List<String>  list = new ArrayList<>();
        // 线程A
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                for (int i = 1; i <= 10; i++) {
                    list.add("abc");
                    System.out.println("线程A添加元素,此时list的size为:" + list.size());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (list.size() == 5)
                        lock.notify();//唤醒B线程
                }
            }
        });
        //线程B
        Thread threadB = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    if (list.size() != 5) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("线程B收到通知,开始执行自己的业务...");
                }
            }
        });
        //需要先启动线程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //再启动线程A
        threadA.start();
    }
}

由输出结果,在线程 A 发出 notify() 唤醒通知之后,依然是走完了自己线程的业务之后,线程 B 才开始执行,正好说明 notify() 不释放锁,而 wait() 释放锁。

  • 使用JUC工具类 CountDownLatch
    jdk1.5 之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了并发编程代码的书写,CountDownLatch 基于 AQS 框架,相当于也是维护了一个线程间共享变量 state。
public class TestSync {
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        List<String>  list = new ArrayList<>();
        //线程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                list.add("abc");
                System.out.println("线程A添加元素,此时list的size为:" + list.size());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (list.size() == 5)
                    countDownLatch.countDown();
            }
        });
        //线程B
        Thread threadB = new Thread(() -> {
            while (true) {
                if (list.size() != 5) {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程B收到通知,开始执行自己的业务...");
                break;
            }
        });
        //需要先启动线程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //再启动线程A
        threadA.start();
    }
}
  • 使用 ReentrantLock 结合 Condition
public class TestSync {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        List<String> list = new ArrayList<>();
        //线程A
        Thread threadA = new Thread(() -> {
            lock.lock();
            for (int i = 1; i <= 10; i++) {
                list.add("abc");
                System.out.println("线程A添加元素,此时list的size为:" + list.size());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (list.size() == 5)
                    condition.signal();
            }
            lock.unlock();
        });
        //线程B
        Thread threadB = new Thread(() -> {
            lock.lock();
            if (list.size() != 5) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程B收到通知,开始执行自己的业务...");
            lock.unlock();
        });
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }
}

这种方式使用起来并不是很好,代码编写复杂,而且线程 B 在被 A 唤醒之后由于没有获取锁还是不能立即执行,也就是说,A 在唤醒操作之后,并不释放锁。这种方法跟 Object 的 wait()/notify() 一样。

  • 基本 LockSupport 实现线程间的阻塞和唤醒
    LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。
public class TestSync {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //线程B
        final Thread threadB = new Thread(() -> {
            if (list.size() != 5) {
                LockSupport.park();
            }
            System.out.println("线程B收到通知,开始执行自己的业务...");
        });
        //线程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                list.add("abc");
                System.out.println("线程A添加元素,此时list的size为:" + list.size());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (list.size() == 5)
                    LockSupport.unpark(threadB);
            }
        });
        threadA.start();
        threadB.start();
    }
}

JVM

JM的主要组成部分?及其作用?

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Natice Interface)

组建的作用:首先通过类加载器(ClassLoader)会把Java代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存,而字节码文件只是JVM的一套指令集规范,不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言的本地库接口(Natic Interface)来实现这个程序的功能

JVM运行时数据区?

不同虚拟机的运行时数据区可能略有不同,但都会遵循java虚拟机规范,java虚拟机规范规定的区域分为以下5个部分:

  • 程序计数器(PC):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,啦选取吓一跳需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能,都需要依赖这个计数器来完成
  • java虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表,操作数栈,动态链接,方法出口等信息
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的
  • Java堆(Java Heap):java虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息,常量,静态变量,即使编译后的代码等数据

Java内存分配

  • 寄存器:我们无法控制
  • 静态域:static定义的静态成员
  • 常量池:编译时被确定保存在.class文件中的(final)常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)
  • 非RAM存储:硬盘等永久存储空间
  • 堆内存:new创建的对象和数组,由java虚拟机自动垃圾回收器管理,存取速度很慢
  • 栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性

说一下JVM调优的工具?

JDK自带了很多监控工具,都位于JDK的bin目录下,其中最常用的是jconsole和jvisualvm这两款视图监控工具

  • jconsole:用于对JVM中的内存,线程和类等进行监控
  • jvisualvm:JDK自带的全能分析工具,可以分析:内存快照,线程快照,程序死锁,监控内存的变化,gc的变化等

什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器
主要有以下四种类加载器:

  • 启动类加载器(Bootstrap ClassLoader)用来加载Java核心类库,无法被Java程序直接引用
  • 扩展类加载器(extensions classloader):它用来加载Java的扩展库,Java虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载java类
  • 系统类加载器(System classloader):它根据java应用的类路径(CLASSPATH)来加载Java类,一般来说java应用的类都是由它来加载完成的,可以通过ClassLoader.getSystemClassLoader()来获取它
  • 用户自定义类加载器,通过集成java,lang.ClassLoader类的方式实现

类加载器双亲委派模型机制?

当一个类收到了类加载请求,不会自己先去加载这个类,而是将其委派给父类,有父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载

Java类加载过程?

编译 》加载 》验证 》准备 》解析 》初始化

编译:将java代码编译为字节码文件

加载:查找并通过io读入字节码文件,在内存中生出一个代表类的class对象,作为访问方法区的输入入口,使用到类的时候才会加载

验证:字节码的校验,是否正确

准备:给类的静态变量分配内存,并赋予默认值

解析:将符号引用替换为直接引用,静态链接过程

初始化:对类的静态遍历,初始化指定值,执行静态代码块

什么是GC,为什么要有GC?

GC是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至奔溃,java提供的gc功能可以自动检测对象是否超过作用域从而达到自动回收内存的目的,java语言没有提供释放已分配内存的显示操作方法

简述java垃圾回收机制

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自动执行,在jvm中,有一个垃圾回收线程,它是低优先级的在正常情况下不会指定的,只有一个虚拟机空闲或者当前堆内存不足才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到回收的集合中,进行回收

垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址,大小以及使用情况,通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象,通过这种方式确定那些对象是可达的那些对象是不可达的,当gc确定一些对象为不可达的时候,gc就有责任回收这些内存空间,程序员可以手动执行System,gc(),通知gc运行,但是java语言规范并不保证gc一定会执行

System.gc()和Runtime.gc()会做什么事情?

这两个方法用来提示JVM要进行垃圾回收,但是立即开始还是延迟进行垃圾回收是取决于JVM的

如果对象的人引用被置为null,垃圾回收器是否会立即释放对象占用的内存

不会,在下一个垃圾回收周期中,这个对象是可被回收的

什么是分布式垃圾回收(DGC)?它是如何工作的?

DGC叫做分布式垃圾回收,rmi使用dgc来做自动垃圾回收,因为rmi包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的,DGC使用引用技术算法来给远程对象提供自动内存管理

串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

吞吐量版本使用并行版本的新生代收集器,它用于中等规模和大规模数据的应用程序,而串行收集器对大对数的小应用(在现在处理器上需要大概100M左右的内存)就足够了

在java中,对象时候可以被垃圾回收?

当对象对当前使用这个兑现的应用程序变得不可触及的时候,这个对象就可以被回收了

简述java内存分配与回收速率以及MinorGC和MajorGc

对象优先在堆的Eden区分配
大对象直接进入老年代
长期存活的对象直接进入老年代
当Eden区没有足够的空间进行分配时,虚拟机会执行一次MinorGC,MinorGC通常发生在新生代的Eden区,在这个区的对象生存周期短,往往GC的频率较高,回收速度比较快,FullGC/MajorGC发生在老年代,一般情况下,触发老年代GC的时候不会触发MinorGC,但是可以通过配置,可以在FullGC之前进行一次MinorGC这样可以加快老年代的回收速度

JVM的永久代中会发生垃圾回收吗?

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(FullGC)
注:java8中已经移除了永久代,新佳乐一个叫做元数据区的native内存区

堆和栈的区别?

功能方面:堆是用来存放对象的,栈是用来执行程序的
共享性:堆是线程共享的,栈是线程私有的
空间大小:堆大小远远大于栈

队列和栈是什么?有什么区别?

队列和栈都是用来预存储数据的
队列雨荨先进先出检索元素,但也有例外的情况,Deque接口云讯从两端检索元素
栈和队列很相似,但是它运行对元素进行先进后出的检索

怎么判断对象是否可以被回收?

  • 引用计数法:为每个对象创建一个引用计数,有对象引用时计数器+1,应用被释放时计数-1,当计数器为0时就可以被回收,它有一个缺点不能解决循环引用的问题
  • 可达性分析:从GCRoots开始向下搜索,搜索所有中国的路径称之为引用链,当一个对象到GCRoots没有任何引用链相连接时,则证明此对象是可以被回收的

简述分带垃圾收集器是怎么工作的?

分代回收期有两个分区:

java中会存在内存泄漏吗,请简单描述

所谓内存泄漏就是指一个不再被程序使用的对象或变量一直被占据在内存中,java中有垃圾回收机制,它可以保证对象不再被引用的时候,即对象变成孤儿的时候,对象将自动被垃圾回收器从内存中清楚点

由于java使用有向图的方式进行垃圾回收,可以消除引用循环依赖的问题,例如有两个对象,相互引用,只要他们和进程不可达的,那么gc也是可以回收它们的

浅拷贝和深拷贝

简单来说就是复制,克隆
Person p = new Person(“张三”)
浅拷贝就是对对象中的数据成员进行简单复制,如果存在动态成员或者指针就会报错,深拷贝就是对对象中存在的动态成员或指针重新开辟内存空间

浅克隆:当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
深克隆:除了对象本身被复制外,对象所包含的所有成员变量也将复制。

JVM内存参数

  • -Xms 最小堆内存(包括新生代和老年代)

  • -Xmx 最大对内存(包括新生代和老年代)

  • 通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好

  • -XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制

  • -Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等

  • 保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同

  • -XX:NewRatio=2:1 表示老年代占两份,新生代占一份

  • -XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份

  • class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制

  • non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)

  • class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制

注意:

  • 这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启

  • 如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起

  • 否则,分成三个区域

    • non-nmethods - JVM 自己用的代码
    • profiled nmethods - 部分优化的机器码
    • non-profiled nmethods - 完全优化的机器码

JVM垃圾回收

JVM垃圾回收算法

标记清除法
Java面试知识点概览(持续更新)_第8张图片

解释:

  1. 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
  2. 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
  3. 清除阶段:释放未加标记的对象占用的内存

要点:

  • 标记速度与存活对象线性关系
  • 清除速度与内存大小线性关系
  • 缺点是会产生内存碎片

标记整理法
Java面试知识点概览(持续更新)_第9张图片
解释:

  1. 前面的标记阶段、清理阶段与标记清除法类似
  2. 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生

特点:

  • 标记速度与存活对象线性关系
  • 清除与整理速度与内存大小成线性关系
  • 缺点是性能上较慢

标记复制法
Java面试知识点概览(持续更新)_第10张图片
解释:

  1. 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
  2. 标记阶段与前面的算法类似
  3. 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
  4. 复制完成后,交换 from 和 to 的位置即可

特点:

  • 标记与复制速度与存活对象成线性关系
  • 缺点是会占用成倍的空间

GC 与分代回收算法

GC

GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度

GC 要点:

  • 回收区域是堆内存,不包括虚拟机栈
  • 判断无用对象,使用可达性分析算法三色标记法标记存活对象,回收未标记对象
  • GC 具体的实现称为垃圾回收器
  • GC 大都采用了分代回收思想
    • 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
    • 根据这两类对象的特性将回收区域分为新生代老年代,新生代采用标记复制法、老年代一般采用标记整理法
  • 根据 GC 的规模可以分成 Minor GCMixed GCFull GC
垃圾回收

伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代,
Java面试知识点概览(持续更新)_第11张图片
当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
Java面试知识点概览(持续更新)_第12张图片
将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
Java面试知识点概览(持续更新)_第13张图片
将 from 和 to 交换位置
Java面试知识点概览(持续更新)_第14张图片
经过一段时间后伊甸园的内存又出现不足
Java面试知识点概览(持续更新)_第15张图片
标记伊甸园与 from(现阶段没有)的存活对象
Java面试知识点概览(持续更新)_第16张图片
将存活对象采用复制算法复制到 to 中
Java面试知识点概览(持续更新)_第17张图片
Java面试知识点概览(持续更新)_第18张图片
复制完毕后,伊甸园和 from 内存都得到释放
Java面试知识点概览(持续更新)_第19张图片
将 from 和 to 交换位置
Java面试知识点概览(持续更新)_第20张图片
老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

垃圾回收器 - Parallel GC

  • eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
  • old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
  • 注重吞吐量

垃圾回收器 - ConcurrentMarkSweep GC

  • 它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法
    • 并发标记时不需暂停用户线程
    • 重新标记时仍需暂停用户线程
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
  • 注重响应时间

垃圾回收器 - G1 GC

  • 响应时间与吞吐量兼顾
  • 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
  • 分成三个阶段:新生代回收、并发标记、混合收集
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
GC的规模
  • Minor GC 发生在新生代的垃圾回收,暂停时间短

  • Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有

  • Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免

三色标记

即用三种颜色记录对象的标记状态

  • 黑色 – 已标记
  • 灰色 – 标记中
  • 白色 – 还未标记

起始的三个对象还未处理完成,用灰色表示
Java面试知识点概览(持续更新)_第21张图片

该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
Java面试知识点概览(持续更新)_第22张图片
依次类推
Java面试知识点概览(持续更新)_第23张图片
沿着引用链都标记了一遍
Java面试知识点概览(持续更新)_第24张图片
最后为标记的白色对象,即为垃圾
Java面试知识点概览(持续更新)_第25张图片

并发漏标问题

比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:

如图所示标记工作尚未完成
Java面试知识点概览(持续更新)_第26张图片
用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾
Java面试知识点概览(持续更新)_第27张图片
但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标
Java面试知识点概览(持续更新)_第28张图片
如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题
Java面试知识点概览(持续更新)_第29张图片
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:

  1. Incremental Update 增量更新法,CMS 垃圾回收器采用
    • 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍
  2. Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
    • 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
    • 新加对象会被记录
    • 被删除引用关系的对象也被记录

内存溢出

误用线程池导致的内存溢出
查询数据量太大导致的内存溢出
动态生成类导致的内存溢出

类加载

类加载过程的三个阶段

  1. 加载
    1. 将类的字节码载入方法区,并创建类.class 对象
    2. 如果此类的父类没有加载,先加载父类
    3. 加载是懒惰执行
  2. 链接
    1. 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
    2. 准备 – 为 static 变量分配空间,设置默认值
    3. 解析 – 将常量池的符号引用解析为直接引用
  3. 初始化
    1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 方法,在初始化时被调用
    2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
    3. 初始化是懒惰执行

内存结构

JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。

  • 执行 javac 命令编译源代码为字节码
  • 执行 java 命令
    1. 创建 JVM,调用类加载子系统加载 class,将类的信息存入方法区
    2. 创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码
    3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
    4. 需要创建对象,会使用内存来存储对象
    5. 不再使用的对象,会由垃圾回收器在内存不足时回收其内存
    6. 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
    7. 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行
    8. 调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续
    9. 对于非 java 实现的方法调用,使用内存称为本地方法栈(见说明)
    10. 对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能

说明

  • 加粗字体代表了 JVM 虚拟机组件
  • 对于 Oracle 的 Hotspot 虚拟机实现,不区分虚拟机栈和本地方法栈

会发生内存溢出的区域

  • 不会出现内存溢出的区域 – 程序计数器
  • 出现 OutOfMemoryError 的情况
    • 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
    • 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
    • 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
  • 出现 StackOverflowError 的区域
    • JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用

方法区、永久代、元空间

  • 方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
  • 永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
  • 元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间

.class字节码文件通过jvm解释生成特定机器码
Java面试知识点概览(持续更新)_第30张图片
Java面试知识点概览(持续更新)_第31张图片

JVM内存区域(运行时数据区)

Java面试知识点概览(持续更新)_第32张图片
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。

程序计数器

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

虚拟机栈(栈线程私有)

是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法区(线程私有)

  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用
  • 本地方法是使用C语言实现的
  • 本地方法区和** Java Stack 作用类似**, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
  • 虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。

堆(Heap-线程共享)-运行时数据区

是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

方法区/永久代(线程共享)

永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区
⽅法区是 Java 虚拟机规范中的定义,是⼀种规范,而永久代是⼀种实现,⼀个是标准⼀个是实现

JVM运行时内存(堆)

Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代
Java面试知识点概览(持续更新)_第33张图片

新生代

由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区(伊甸园区)、ServivorFrom、ServivorTo 三个区。

Eden区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。

ServivorFrom

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

ServivorTo

保留了一次 MinorGC 过程中的幸存者。

MinorGC 采用复制算法。
谁空谁是TO

老年代

主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。

MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

Java是否需要开发人员回收内存垃圾?

大多数情况下是不需要的,Java提供了一个系统级的线程来跟踪内存分配,不再使用的内存区将会自动回收

方法区的作用是什么?

方法区用于存储被虚拟机加载的类型信息,常量,静态变量,集市编译器编译后的代码缓存等数据
JDK8之前使用永生代实现方法区,容易内存溢出,因为永生代有上线,集市不设置也有默认大小,JDK7把永生代的字符串常量池,静态变量等移除,JDK8中永生代完全废弃,改用在本地内存中实现的元空间代替,把JDK7中永久代剩余内容(主要是类型信息)全部移到元空间

虚拟机贵方对方法区的约束宽松,除和堆一样不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收,垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载,如果方法区无法满足新的内存分配需求,将抛出OOM

永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

JAVA8与元数据

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

垃圾回收与算法

引用计数法

在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。

可达性分析

通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

分代收集算法

JAVA引用类型

强引用

在 Java 中最常见的就是强引用,把==一个对象赋给一个引用变量,这个引用变量就是一个强引用。==当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
相当于必不可少对的生活物品

软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
相当于可有可无的生活物品

弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
相当于可有可无的生活物品

虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

JVM类加载机制

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化。

JAVA对象的创建过程

  1. 类加载检查
  2. 分配内存
  3. 初始化零值
  4. 设置对象头
  5. 执行init方法

类加载器

启动类加载器(Bootstrap ClassLoader)

  1. 负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
    扩展类加载器(Extension ClassLoader)
  2. 负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
    应用程序类加载器(Application ClassLoader)
  3. 负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
    Java面试知识点概览(持续更新)_第34张图片

双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
Java面试知识点概览(持续更新)_第35张图片

计算机基础知识

计算机网络系统

http 响应码 301 和 302 代表的是什么?有什么区别?

301:永久重定向。
302:暂时重定向。
它们的区别是,301 对搜索引擎优化(SEO)更加有利;302 有被提示为网络拦截的风险。

forward 和 redirect 的区别?

forward 是转发 和 redirect 是重定向:
地址栏 url 显示:foward url 不会发生改变,redirect url 会发生改变;
数据共享:forward 可以共享 request 里的数据,redirect 不能共享;
效率:forward 比 redirect 效率高。

简述 tcp 和 udp 的区别?

tcp 和 udp 是 OSI 模型中的运输层中的协议。tcp 提供可靠的通信传输,而 udp 则常被用于让广播和细节控制交给应用的通信传输。
两者的区别大致如下:
tcp 面向连接,udp 面向非连接即发送数据前不需要建立链接;
tcp 提供可靠的服务(数据传输),udp 无法保证;
tcp 面向字节流,udp 面向报文;
tcp 数据传输慢,udp 数据传输快;

tcp 为什么要三次握手,两次不行吗?为什么?

如果采用两次握手,那么只要服务器发出确认数据包就会建立连接,但由于客户端此时并未响应服务器端的请求,那此时服务器端就会一直在等待客户端,这样服务器端就白白浪费了一定的资源。若采用三次握手,服务器端没有收到来自客户端的再此确认,则就会知道客户端并没有要求建立请求,就不会浪费服务器的资源。

说一下 tcp 粘包是怎么产生的?

tcp 粘包可能发生在发送端或者接收端,分别来看两端各种产生粘包的原因:
发送端粘包:发送端需要等缓冲区满才发送出去,造成粘包;
接收方粘包:接收方不及时接收缓冲区的包,造成多个包接收。

OSI

物理层:传输比特流,比特,网卡工作层
数据链路层:如何格式化数据以进行传输,差错检测,保证数据传输的可靠性,帧,交换机
网络层:路由器,数据包
传输层:数据间传输,TCP和UDP,分段
会话层:
表示层:
应用层:

TCP/IP

Java面试知识点概览(持续更新)_第36张图片

Http

常见的Http Method有哪些,使用场景分别是?

http1.0定义了三种:
GET: 向服务器获取资源,比如常见的查询请求
POST: 向服务器提交数据而发送的请求
Head: 和get类似,返回的响应中没有具体的内容,用于获取报头

http1.1定义了六种
PUT:一般是用于更新请求,比如更新个人信息、商品信息全量更新
PATCH:PUT 方法的补充,更新指定资源的部分数据
DELETE:用于删除指定的资源
OPTIONS: 获取服务器支持的HTTP请求方法,服务器性能、跨域检查等
CONNECT: 方法的作用就是把服务器作为跳板,让服务器代替用户去访问其它网页,之后把数据原原本本的返回给用户,网页开发基本不用这个方法,如果是http代理就会使用这个,让服务器代理用户去访问其他网页,类似中介
TRACE:回显服务器收到的请求,主要用于测试或诊断

常见http状态码解析

浏览器向服务器请求时,服务端响应的消息头里面有状态码,表示请求结果的状态

分类
1XX: 收到请求,需要请求者继续执行操作,比较少用

2XX: 请求成功,常用的 200

3XX: 重定向,浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取;

好处:网站改版、域名迁移等,多个域名指向同个主站导流
必须记住: 301:永久性跳转,比如域名过期,换个域名 302:临时性跳转

4XX: 客服端出错,请求包含语法错误或者无法完成请求
必须记住:
400: 请求出错,比如语法协议
403: 没权限访问
404: 找不到这个路径对应的接口或者文件
405: 不允许此方法进行提交,Method not allowed,比如接口一定要POST方式,而你是用了GET

5XX: 服务端出错,服务器在处理请求的过程中发生了错误
必须记住:
500: 服务器内部报错了,完成不了这次请求
503: 服务器宕机

Cookie和Session

说下Cookie和Session的区别和联系
cookie数据保存在客户端,session数据保存在服务端
cookie不是很安全,容易泄露,不能直接明文存储信息
Cookie大小和数量存储有限制

你们公司C端业务登录的是怎样做的(业务量大,集群部署)
部分业务是采用redis替代本身的tomcat单机session (业务需要高度可控)
还有其他业务是使用JSON Web token (C端普通业务)

说下常用浏览器输入一个url到用户看到结果,中间经过哪些流程

1、浏览器输入url, 解析url地址是否合法
​2、浏览器检查是否有缓存, 如果有直接显示。如果没有跳到第三步。
​3、在发送http请求前,需要域名解析(DNS解析),解析获取对应过的ip地址。
​4、浏览器向服务器发起tcp链接,完成tcp三次握手
​5、握手成功后,浏览器向服务器发送http请求
​6、服务器收到处理的请求,将数据返回至浏览器
​7、浏览器收到http响应。
​8、浏览器解析响应。如果响应可以缓存,则存入缓存
​9、浏览器进行页面渲染

输入url,解析url是否合法,查找浏览器是否有缓存,有的话直接进行显示,没有的话,进行dns域名解析,拿到ip地址,然后发起tcp连接,三次握手,服务器对请求作出响应,发回数据到浏览器,浏览器收集癖到jttp响应,进行解析和进行数据渲染。

浏览器同源策略

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。
由Netscape提出的一个著名的安全策略。
当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面
当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,
即检查是否同源,只有和百度同源的脚本才会被执行。
如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发
出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收

为什么会出现跨域,有什么常见的解决方案
跨域:浏览器同源策略 1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。 最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"
协议相同 http https
域名相同 www.baidu.com
端口相同 80 81

一句话:浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域

浏览器控制台跨域提示:
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.
解决方法

  • JSONP
  • 页面这层再包装一层服务,目前最多就是nodejs
  • Http响应头配置允许跨域
  • nginx代理服务器
  • 后端程序代码配置

程序代码中处理 SpringBoot 通过拦截器配置
//表示接受任意域名的请求,也可以指定域名
response.setHeader(“Access-Control-Allow-Origin”, request.getHeader(“origin”));
//该字段可选,是个布尔值,表示是否可以携带cookie
response.setHeader(“Access-Control-Allow-Credentials”, “true”);
response.setHeader(“Access-Control-Allow-Methods”, “GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS”);
response.setHeader(“Access-Control-Allow-Headers”, “*”);

操作系统

算法

数据库

MySQL

数据库三大范式

第一范式:列都是不可再分的
第二范式:每个表只描述一件事情
第三范式:不存在对非主键列的传递依赖

MySQL事务四大特性

原子性Atomicity: 一个事务必须被事务不可分割的最小工作单元,整个操作要么全部成功,要么全部失败,一般就是通过commit和rollback来控制
一致性Consistency: 数据库总能从一个一致性的状态转换到另一个一致性的状态,只要有任何一方发生异常就不会成功提交事务,比如下单支付成功后,开通视频播放权限
隔离性Isolation: 一个事务相对于另一个事务是隔离的,一个事务所做的修改是在最终提交以前,对其他事务是不可见的
持久性Durability:==一旦事务提交,则其所做的修改就会永久保存到数据库中。==此时即使系统崩溃,修改的数据也不会丢失

脏读、不可重复读、幻读

脏读: 事务中的修改即使没有提交,其他事务也能看见,事务可以读到未提交的数据称为脏读
不可重复读: 同个事务前后多次读取,不能读到相同的数据内容,中间另一个事务也操作了该同一数据
幻读当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,发现两次不一样,产生幻读

幻读和不可重复读的区别是:幻读是一个范围,不可重复读是本身,从总的结果来看, 两者都表现为两次读取的结果不一致

事务隔离级别

事务的隔离级别越高,事务越安全,但是并发能力越差。
Read Uncommitted(未提交读,读取未提交内容):事务中的修改即使没有提交,其他事务也能看见,事务可以读到为提交的数据称为脏读,也存在不可重复读、幻读问题
例子:一个活动,原价500元的课程,配置成50元,但是事务没提交。你刚好看到那么便宜准备购买,但是马上回滚了事务,重新配置并提交了事务,你准备下单的时候发现价格变回了500元
Read Committed(提交读,读取提交内容):一个事务开始后只能看见已经提交的事务所做的修改,在事务中执行两次同样的查询可能得到不一样的结果,也叫做不可重复读(前后多次读取,不能读到相同的数据内容),也存幻读问题
例子:你有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分,但是女友同时也在别的地方登录,把1000积分兑换了《SpringCloud微服务专题课程》,且在你之前提交事务;当系统帮你兑换《面试专题课程》是发现积分预计没了,兑换失败。
事务A事先读取了数据,事务B紧接了更新了数据且提交了事务,事务A再次读取该数据时,数据已经发生了改变
Repeatable Read(可重复读,mysql默认的事务隔离级别):解决脏读、不可重复读的问题,存在幻读的问题,使用 MMVC机制 实现可重复读
例子:有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分,女友同时也在别的地方登录先兑换了这个《面试专题课程》,事务提交的时候发现存在了,之前读取的没用了,像是幻觉
幻读问题:MySQL的InnoDB引擎通过MVCC自动帮我们解决,即多版本并发控制
Serializable(可串行化):解决脏读、不可重复读、幻读,可保证事务安全,但强制所有事务串行执行,所以并发效率低

Mysql常见的存储引擎

常见的有:InnoDB、MyISAM、MEMORY、MERGE、ARCHIVE、CSV等
一般比较常用的有InnoDB、MyISAM
MySQL 5.5以上的版本默认是InnoDB,5.5之前默认存储引擎是MyISAM

mysql的存储引擎 innodb和myisam区别

区别项 Innodb myisam
事务 支持 不支持
锁粒度 行锁,适合高并发 表锁,不适合高并发
是否默认 默认 非默认
支持外键(物理) 支持外键 不支持
适合场景 读写均衡,写大于读场景,需要事务 读多写少场景,不需要事务
全文索引 不支持,可以通过插件实现, 更多使用ElasticSearch 支持全文索引

是否⽀持事务和崩溃后的安全恢复:
MyISAM 强调的是性能,每次查询具有原⼦性,其执⾏速度⽐InnoDB类型更快,但是不提供事务⽀持。
InnoDB 提供事务⽀持事务,外部键等⾼级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能⼒(crash recoverycapabilities)的事务安全(transaction-safe (ACID compliant))型表。

是否⽀持MVCC :
仅 InnoDB ⽀持。应对⾼并发事务, MVCC⽐单纯的加锁更⾼效;MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下⼯作;MVCC可以使⽤ 乐 观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统⼀。

锁机制与InnoDB锁算法

MyISAM采⽤表级锁(table-level locking)
InnoDB⽀持**⾏级锁(row-level locking)**和表级锁,默认为⾏级锁

表级锁和⾏级锁对⽐:
表级锁: MySQL中锁定 粒度最⼤ 的⼀种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最⼤,触发锁冲突的概率最⾼,并发度最低,MyISAM和 InnoDB引擎都⽀持表级锁。
⾏级锁: MySQL中锁定 粒度最⼩ 的⼀种锁,只针对当前操作的⾏进⾏加锁。 ⾏级锁能⼤⼤减少数据库操作的冲突。其加锁粒度最⼩,并发度⾼,但加锁的开销也最⼤,加锁慢,会出现死锁。

InnoDB存储引擎的锁的算法有三种:

  • Record lock:单个⾏记录上的锁
  • Gap lock:间隙锁,锁定⼀个范围,不包括记录本身
  • Next-key lock:record+gap 锁定⼀个范围,包含记录本身

索引

索引的出现就是为了提高查询的效率,就像一本书的目录。对于一张表来说,索引其实就是它的目录。
MySQL索引使⽤的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝⼤多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余⼤部分场景,建议选择BTree索引。
MySQL的BTree索引使⽤的是B树中的B+Tree,但对于主要的两种存储引擎的实现⽅式是不同的。

  • MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”
    叶节点存的是地址

  • InnoDB: 其数据⽂件本身就是索引⽂件。相⽐MyISAM,索引⽂件和数据⽂件是分离的,其表数据⽂件本身就是按B+Tree组织的⼀个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据⽂件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。⽽其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值⽽不是地址,这也是和MyISAM不同的地⽅。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再⾛⼀遍主索引。 因此,在设计表的时候,不建议使⽤过⻓的字段作为主键,也不建议使⽤⾮单调的字段
    作为主键,这样会造成主索引频繁分裂。
    树节点存的是完整的数据记录

mysql的功能索引

索引名称 特点 创建语句
普通索引 最基本的索引,仅加速查询 CREATE INDEX idx_name ON table_name(filed_name)
唯一索引 加速查询,列值唯一,允许为空;
组合索引则列值的组合必须唯一 CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2)
主键索引 加速查询,列值唯一,
一个表只有1个,不允许有空值 ALTER TABLE table_name ADD PRIMARY KEY ( filed_name )
组合索引 加速查询,多条件组合查询 CREATE INDEX idx_name ON table_name(filed_name_1,filed_name_2);
覆盖索引 索引包含所需要的值,不需要“回表”查询,比如查询 两个字段,刚好是 组合索引 的两个字段
全文索引 对内容进行分词搜索,仅可用于Myisam, 更多用ElasticSearch做搜索 ALTER TABLE table_name ADD FULLTEXT ( filed_name )

你们线上数据量每天有多少新增,都是存储在mysql库吗,有没做优化

中型公司或者业务发展好的公司,一天新增几百万数据量
业务核心数据存储在Mysql里面,针对业务创建合适的索引
打点数据、日志等存储在ElasticSearch或者MongoDB里面

索引的优缺点

考虑点:结合实际的业务场景,在哪些字段上创建索引,创建什么类型的索引

  • 索引好处:
    快速定位到表的位置,减少服务器扫描的数据
    有些索引存储了实际的值,特定情况下只要使用索引就能完成查询
  • 索引缺点:
    索引会浪费磁盘空间,不要创建非必要的索引
    插入、更新、删除需要维护索引,带来额外的开销
    索引过多,修改表的时候重构索引性能差
  • 索引优化实践
    前缀索引,特别是TEXT和BLOG类型的字段,只检索前面几个字符,提高检索速度
    尽量使用数据量少的索引,索引值过长查询速度会受到影响
    选择合适的索引列顺序
    内容变动少,且查询频繁,可以建立多几个索引
    内容变动频繁,谨慎创建索引
    根据业务创建适合的索引类型,比如某个字段常用来做查询条件,则为这个字段建立索引提高查询速度
    组合索引选择业务查询最相关的字段

查询指令执行顺序

说下执行顺序 select、where、from、group by、having、order by
from 从哪个表查询
where 初步过滤条件
group by 过滤后进行分组[重点]
having 对分组后的数据进行二次过滤[重点]
select 查看哪些结果字段
order by 按照怎样的顺序进行排序返回[重点]
select video_id,count(id) num from chapter group by video_id having num >10 order by video_id desc

大表优化

当MySQL单表记录数过⼤时,数据库的CRUD性能会明显下降,⼀些常见的优化措施如下:
限定数据的范围:务必禁止不带任何限制数据范围条件的查询语句。比如:我们当⽤户在查询订单历史的时候,我们可以控制在⼀个⽉的范围内;

读/写分离:经典的数据库拆分⽅案,主库负责写,从库负责读;

垂直分区:根据数据库⾥⾯数据表的相关性进⾏拆分。 例如,⽤户表中既有⽤户的登录信息⼜有⽤户的基本信息,可以将⽤户表拆分成两个单独的表,甚⾄放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分,把⼀张列⽐较多的表拆分为多张表。

Java面试知识点概览(持续更新)_第37张图片

  • 垂直拆分的优点: 可以使得列数据变⼩,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
  • 垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应⽤层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
    水平分区:保持数据表结构不变,通过某种策略存储数据分⽚。这样每⼀⽚数据分散到不同的表或者库中,达到了分布式的⽬的。⽔平拆分可以⽀撑⾮常⼤的数据量。⽔平拆分是指数据表⾏的拆分,表的⾏数超过200万⾏时,就会变慢,这时可以把⼀张的表的数据拆成多张表来存放。
    举个例⼦:我们可以将⽤户信息表拆分成多个⽤户信息表,这样就可以避免单⼀表数据量过⼤对性能造成影响。
    Java面试知识点概览(持续更新)_第38张图片
    ⽔平拆分可以⽀持⾮常⼤的数据量。需要注意的⼀点是:分表仅仅是解决了单⼀表数据过⼤的问题,但由于表的数据还是在同⼀台机器上,其实对于提升MySQL并发能⼒没有什么意义,所以⽔平拆分最好分库 。
    ⽔平拆分能够⽀持⾮常⼤的数据量存储,应⽤端改造也少,但分⽚事务难以解决 ,跨节点Join性能较差,逻辑复杂。尽量不要对数据进⾏分⽚,因为拆分会带来逻辑、部署、运维的各种复杂度 ,⼀般的数据表在优化得当的情况下⽀撑千万以下的数据量是没有太⼤问题的。如果实在要分⽚,尽量选择客户端分⽚架构,这样可以减少⼀次和中间件的⽹络I/O。
    下⾯补充⼀下数据库分⽚的两种常⻅⽅案:
    客户端代理: 分⽚逻辑在应⽤端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当⽹的Sharding-JDBC 、阿⾥的TDDL是两种⽐较常⽤的实现。
    中间件代理: 在应⽤和数据中间加了⼀个代理层。分⽚逻辑统⼀维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、⽹易的DDB等等都是这种架构的实现。

什么是数据库连接池?为什么需要数据库连接池?

池化设计应该不是⼀个新名词。我们常⻅的如java线程池、jdbc连接池、redis连接池等就是这类设计的代表实现。
这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好⽐你去⻝堂打饭,打饭的⼤妈会先把饭盛好⼏份放那⾥,你来了就直接拿着饭盒加菜即可,不⽤再临时⼜盛饭⼜打菜,效率就⾼了。
除了初始化资源,池化设计还包括如下这些特征:池⼦的初始值、池⼦的活跃值、池⼦的最⼤值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。
数据库连接本质就是⼀个socket的连接。数据库服务端还要维护⼀些缓存和⽤户权限信息之类的,所以占⽤了⼀些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重⽤这些连接。为每个⽤户打开和维护数据库连接,尤其是对动态数据库驱动的⽹站应⽤程序的请求,既昂贵⼜浪费资源。在连接池中,创建连接后,将其放置在池中,并再次使⽤它,因此不必建⽴新的连接。如果使⽤了所有连接,则会建⽴⼀个新连接并将其添加到池中。 连接池还减少了⽤户必须等待建⽴与数据库的连接的时间。

分库分表之后,id 主键如何处理?

因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要⼀个全局唯⼀的id 来⽀持。

⽣成全局 id 有下⾯这⼏种⽅式:

UUID:不适合作为主键,因为太⻓了,并且⽆序不可读,查询效率低。比较适合⽤于⽣成唯⼀的名字的标示⽐如⽂件的名字。
数据库自增 id : 两台数据库分别设置不同步⻓,⽣成不重复ID的策略来实现⾼可⽤。这种⽅式⽣成的 id 有序,但是需要独⽴部署数据库实例,成本⾼,还会有性能瓶颈。
利⽤ redis ⽣成 id : 性能比较好,灵活⽅便,不依赖于数据库。但是,引⼊了新的组件造成系统更加复杂,可⽤性降低,编码更加复杂,增加了系统成本。
Twitter的snowflake算法 :Github 地址:https://github.com/twitter-archive/snowflake。
美团的Leaf分布式ID生成系统 :Leaf 是美团开源的分布式ID⽣成器,能保证全局唯⼀性、趋势递增、单调递增、信息安全,⾥⾯也提到了⼏种分布式⽅案的对⽐,但也需要依赖关系数据库、Zookeeper等中间件。

MySQL中的varchar和char有什么区别,应该怎么选择

varchar(len) char(len) len存储的是字符
Java面试知识点概览(持续更新)_第39张图片

MySQL中的datetime和timestamp有什么区别

Java面试知识点概览(持续更新)_第40张图片

  • 为什么timestamp只能到2038年
    MySQL的timestamp类型是4个字节,最大值是2的31次方减1,结果是2147483647,
    转换成北京时间就是2038-01-19 11:14:07

千万级Mysql数据表分页查询优化

线上数据库的一个商品表数据量过千万,做深度分页的时候性能很慢,有什么优化思路
现象:千万级别数据很正常,比如数据流水、日志记录等,数据库正常的深度分页会很慢
慢的原因:select * from product limit N,M
MySQL执行此类SQL时需要先扫描到N行,然后再去取M行,N越大,MySQL扫描的记录数越多,SQL的性能就会越差

1、后端、前端缓存

2、使用ElasticSearch分页搜索

3、合理使用 mysql 查询缓存,覆盖索引进行查询分页
select title,cateory from product limit 1000000,100

4、如果id是自增且不存在中间删除数据,使用子查询优化,定位偏移位置的 id
select * from oper_log where type=‘BUY’ limit 1000000,100; //5.秒

select id from oper_log where type=‘BUY’ limit 1000000,1; // 0.4秒

select * from oper_log where type=‘BUY’ and id>=(select id from oper_log where type=‘BUY’ limit 1000000,1) limit 100; //0.8秒

数据库性能监控和优化

针对线上的数据库,你会做哪些监控,业务性能 + 数据安全 角度分析
大厂一般都有数据库监控后台,里面指标很多,但是开发人员也必须知道

业务性能
1、应用上线前会审查业务新增的sql,和分析sql执行计划
比如是否存在 select * ,索引建立是否合理
2、开启慢查询日志,定期分析慢查询日志
3、监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
4、吞吐量QPS/TPS,一天内读写随着时间的变化统计图
数据安全
1、短期增量备份,比如一周一次。 定期全量备份,比如一月一次
2、检查是否有非授权用户,是否存在弱口令,网络防火墙检查
3、导出数据是否进行脱敏,防止数据泄露或者黑产利用
4、数据库 全量操作日志审计,防止数据泄露
5、数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
6、高可用 主从架构,多机房部署

mysql常见日志

redo 重做日志
作用:确保事务的持久性,防止在发生故障,脏页未写入磁盘。重启数据库会进行redo log执行重做,到达事务一致性

undo 回滚日志
作用:保证数据的原子性,记录事务发生之前的数据的一个版本,用于回滚。
innodb事务的可重复读和读取已提交 隔离级别就是通过mvcc+undo实现

errorlog 错误日志
作用:Mysql本身启动、停止、运行期间发生的错误信息

slow query log 慢查询日志
作用:记录执行时间过长的sql,时间阈值可以配置,只记录执行成功

binlog 二进制日志
作用:用于主从复制,实现主从同步

relay log 中继日志
作用:用于数据库主从同步,将主库发送来的binlog先保存在本地,然后从库进行回放

general log 普通日志
作用:记录数据库操作明细,默认关闭,开启会降低数据库性能

数据库主从同步

你们搭建数据库主从复制的目的有哪些

容灾使用,用于故障切换
业务需要,进行读写分离减少主库压力

既然你们搭建了主从同步,且你们日增量数据量也不少,有没遇到同步延迟问题
为什么会有同步延迟问题,怎么解决?
保证性能第一情况下,不能百分百解决主从同步延迟问题,只能增加缓解措施。

现象:主从同步,大数据量场景下,会发现写入主库的数据,在从库没找到。

原因:
1、主从复制是单线程操作,当主库TPS高,产生的超过从库sql线程执行能力

2、从库执行了大的sql操作,阻塞等待

3、服务器硬件问题,如磁盘,CPU,还有网络延迟等

解决办法:
1、业务需要有一定的容忍度,程序和数据库直接增加缓存,降低读压力

2、业务适合的话,写入主库后,再写缓存,读的时候可以读缓存,没命中再读从库

3、读写分离,一主多从,分散主库和从库压力

4、提高硬件配置,比如使用SSD固态硬盘、更好的CPU和网络

5、进行分库分表,减少单机压力

什么场景下会出现主从数据不一致
1、本身复制延迟导致
2、主库宕机或者从库宕机都会导致复制中断
3、把一个从库提升为主库,可能导致从库和主库的数据不一致性

是否有做过主从一致性校验,你是怎么做的,如果没做过,你计划怎么做
如果不一致你会怎么修复
Mysql主从复制是基于binlog复制,难免出现复制数据不一致的风险,引起用户数据访问前后不一致的风险
所以要定期开展主从复制数据一致性的校验并修复,避免这些问题

解决方案之一,使用Percona公司下的工具

pt-table-checksum工具进行一致性校验

原理:
主库利用表中的索引,将表的数据切割成一个个chunk(块),然后进行计算得到checksum值。
从库也执相应的操作,并在从库上计算相同数据块的checksum,然后对比主从中各个表的checksum是否一致并存储到数据库,最后通过存储校验结果的表就可以判断出哪些表的数据不一致

pt-table-sync(在从库执行)工具进行修复不一致数据,可以修复主从结构数据的不一致,也可以修复非主从结构数据表的数据不一致

原理:在主库上执行数据的更改,再同步到从库上,不会直接更改成从的数据。在主库上执行更改是基于主库现在的数据,也不会更改主库上的数据,可以同步某些表或整个库的数据,但它不同步表结构、索引,只同步不一致的数据

注意:
默认主库要检查的表在从库都存在,并且同主库表有相同的表结构
如果表中没有索引,pt-table-checksum将没法处理,一般要求最基本都要有主键索引
pt-table-sync工具会修改数据,使用前最好备份下数据,防止误操作

pt-table-checksum怎么保证某个chunk的时候checksum数据一致性?
当pt工具在计算主库上某chunk的checksum时,主库可能在更新且从库可能复制延迟,那该怎么保证主库与从库计算的是”同一份”数据,答案把要checksum的行加上for update锁并计算,这保证了主库的某个chunk内部数据的一致性

Redis

简单介绍⼀下 Redis 呗!

使用C语言开发,基于内存,效率特别高,单线程的,运用了IO多路复用技术,主要用来做缓存,和分布式锁,甚至可以用来做消息队列,Redis还支持事务和持久化,lua脚本的方案.此外,还可以做到高性能和高可用,高性能的话主要是由于使用了单线程,可以通过搭建主从架构还有哨兵模式实现redis的高可用方案
简单来说 Redis 就是⼀个使⽤ C 语⾔开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,也就是它是内存数据库,所以读写速度⾮常快,因此 Redis 被⼴泛应⽤于缓存⽅向。另外,Redis 除了做缓存之外,Redis 也经常⽤来做分布式锁,甚⾄是消息队列。Redis 提供了多种数据类型来⽀持不同的业务场景。Redis 还⽀持事务 、持久化、Lua 脚本、多种集群⽅案。

你们业务用了redis,为啥不用其他缓存,比如memcached呢

  • redis数据结构比memcached更丰富,基本可以完全替换
  • redis社区比较活跃,性能也强大,也支持持久化等功能
  • 要和业务结合,比如电商系统的热销商品,需要用到zset,所以使用redis

Redis哪些数据结构? 说下这些结构的使用场景有哪些

String

  1. 介绍 :string 数据结构是简单的 key-value 类型。虽然 Redis 是⽤ C 语⾔写的,但是 Redis并没有使⽤ C 的字符串表示,⽽是⾃⼰构建了⼀种简单动态字符串(simple dynamic string,SDS)。相⽐于 C 的原⽣字符串,Redis 的 SDS 不光可以保存⽂本数据还可以保存⼆进制数据,并且获取字符串⻓度复杂度为O(1)(C 字符串为 O(N)),除此之外,Redis 的SDS API 是安全的,不会造成缓冲区溢出。
  2. 常⽤命令: set,get,strlen,exists,dect,incr,setex 等等。
  3. 应⽤场景 :⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等等。

hash
4. 介绍 :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,
Redis 的 hash 做了更多优化。另外,hash 是⼀个 string 类型的 field 和 value 的映射表,
特别适合⽤于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的
值。 ⽐如我们可以 hash 数据结构来存储⽤户信息,商品信息等等。
5. 常⽤命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
6. 应⽤场景: 系统中对象数据的存储,购物车。

list
7. 介绍 :==list 即是链表。==链表是⼀种常见的数据结构,特点是易于数据元素的插⼊和删除并且且可以灵活调整链表⻓度,但是链表的随机访问困难。许多⾼级编程语⾔都内置了链表的实现⽐如 Java 中的LinkedList,但是 C 语⾔并没有实现链表,所以 Redis 实现了⾃⼰的链表数据结构。Redis 的 list 的实现为⼀个双向链表,即可以⽀持反向查找和遍历,更⽅便操作,不过带来了部分额外的内存开销。
8. 常⽤命令: rpush,lpop,lpush,rpop,lrange、llen 等。
9. 应⽤场景: 发布与订阅或者说消息队列、慢查询。

set
10. 介绍 : set 类似于 Java 中的 HashSet。Redis 中的 set 类型是⼀种无序集合,集合中的元素没有先后顺序。当你需要存储⼀个列表数据,⼜不希望出现重复数据时,set 是⼀个很好的选择,并且 set 提供了判断某个成员是否在⼀个 set 集合内的重要接⼝,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。⽐如:你可以将⼀个⽤户所有的关注⼈存在⼀个集合中,将其所有粉丝存在⼀个集合。Redis 可以⾮常⽅便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
11. 常⽤命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
12. 应⽤场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景

sroted set
13. 介绍: 和 set 相⽐,sorted set 增加了⼀个权重参数 score,使得集合中的元素能够按 score进⾏有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap和 TreeSet 的结合体。
14. 常⽤命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
15. 应⽤场景: 需要对数据根据某个权重进⾏排序的场景。⽐如在直播系统中,实时排⾏信息包含直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息(可以理解为按消息维度的消息排⾏榜)等信息。

缓存数据的处理流程是怎样的?

  1. 如果⽤户请求的数据在缓存中就直接返回。
  2. 缓存中不存在的话就看数据库中是否存在。
  3. 数据库中存在的话就更新缓存中的数据。
  4. 数据库中不存在的话就返回空数据。
    查询是否在缓存中,在的话直接返回,不在的话查询数据库,查到的话更新缓存,没有查到的话返回空的数据

为什么要用 Redis/为什么要用缓存?

假如⽤户第⼀次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,⽤户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心i地将该⽤户访问的数据存在缓存中。这样有什么好处呢? 那就是保证⽤户下⼀次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。不过,要保持数据库和缓存中的数据的⼀致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
⾼并发:
⼀般像 MySQL 这类的数据库的 QPS ⼤概都在 1w 左右(4 核 8g) ,但是使⽤ Redis 缓存之后很容易达到 10w+,甚⾄最⾼能达到 30w+(就单机 redis 的情况,redis 集群的话会更⾼)。

QPS(Query Per Second):服务器每秒可以执⾏的查询次数;

所以,直接操作缓存能够承受的数据库请求数量是远远⼤于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样⽤户的⼀部分请求会直接到缓存这⾥⽽不⽤经过数据库。进而,我们也就提⾼的系统整体的并发。

Redis 单线程模型详解

Redis 基于 Reactor 模式来设计开发了⾃⼰的⼀套⾼效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是⾼性能 IO 的基⽯),这套事件处理模型对应的是 Redis中的⽂件事件处理器(file event handler)。由于⽂件事件处理器(file event handler)是单线程⽅式运⾏的,所以我们⼀般都说 Redis 是单线程模型。

既然是单线程,那怎么监听⼤量的客户端连接呢?
Redis 通过IO 多路复用程序来监听来⾃客户端的⼤量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发⽣。这样的好处⾮常明显: I/O 多路复⽤技术的使⽤让 Redis 不需要额外创建多余的线程来监听客户端的⼤量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。另外, Redis 服务器是⼀个事件驱动程序,服务器需要处理两类事件: 1. ⽂件事件; 2. 时间事件。

redis是单线程,为什么这么快?

  • 基于内存,绝大部分请求是纯粹的内存操作,CPU不是Redis的瓶颈
  • 避免了不必要的CPU上下文切换和其他竞争条件,比如锁操作等
  • 底层是使用多路I/O复用模型,非阻塞IO
  • Redis6 后支持多线程,但是默认不开启

Redis 给缓存数据设置过期时间有啥用?

为了处理只在一段时间内有效的数据,比如用户验证码.还有分布式锁的时候也需要指定过期时间,防止持有锁的线程down掉之后锁未释放

因为内存是有限的,如果缓存中的所有数据都是⼀直保存的话,分分钟直接Out of memory。
过期时间除了有助于缓解内存的消耗,还有什么其他⽤么?

很多时候,我们的业务场景就是需要某个数据只在某⼀时间段内存在,⽐如我们的短信验证码可能只在1分钟内有效,⽤户登录的 token 可能只在 1 天内有效。如果使⽤传统的数据库来处理的话,⼀般都是⾃⼰判断过期,这样更麻烦并且性能要差很多。

过期的数据的删除策略了解么?

如果假设你设置了⼀批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进⾏删除的呢?

  1. 惰性删除 :只会在取出key的时候才对数据进⾏过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
  2. 定期删除 : 每隔⼀段时间抽取⼀批 key 执⾏删除过期key操作。并且,Redis 底层会通过限制删除操作执⾏的时⻓和频率来减少删除操作对CPU时间的影响。

定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采⽤的是 定期
删除+惰性/懒汉式删除 。

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉
了很多过期 key 的情况。这样就导致⼤量过期 key 堆积在内存⾥,然后就Out of memory了。
怎么解决这个问题呢?答案就是: Redis 内存淘汰机制

缓存淘汰策略

一般会使用淘汰策略
常见的淘汰策略有 FIFO、LRU、LFU
能分别说下FIFO、LRU、LFU这些策略不

  • 先进先出First In,First Out
    新访问的数据插入FIFO队列尾部,数据在FIFO队列中顺序移动,淘汰FIFO队列头部的数据

  • 最近最少使用 Least recently used
    根据数据的历史访问记录来进行数据淘汰,如果数据最近被访问过,那么将来被访问的几率也更高
    新数据插入到链表头部,每当缓存数据被访问,则将数据移到链表头部,当链表满的时候,将链表尾部的数据丢弃。

  • 最近不经常使用 Least Frequently Used
    根据数据的历史访问频率来淘汰数据,如果数据过去被访问多次,那么将来被访问的频率也更高
    把数据加入到链表中,按频次排序,一个数据被访问过,把它的频次+1,发生淘汰的时候,把频次低的淘汰掉

redis持久化

支持AOF和RDB持久化

  • AOF
    以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录
    支持秒级持久化、兼容性好,对于相同数量的数据集而言,AOF文件通常要大于RDB文件,所以恢复比RDB慢

  • RDB
    在指定的时间间隔内将内存中的数据集快照写入磁盘,可以指定时间归档数据(形成冷数据),但不能做到实时持久化
    文件紧凑,体积小,对于灾难恢复而言,RDB是非常不错的选择,相比于AOF机制,如果数据集很大,RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
    有save和bgsave两种方式,bgsave会在后台fork一个子进程进行持久化操作

Redis事务

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。
使⽤ MULTI命令后可以输⼊多个命令。Redis不会⽴即执⾏这些命令,⽽是将它们放到队列,当调⽤了EXEC命令将执⾏所有命令。
我们知道事务具有四⼤特性:1. 原⼦性,2. 隔离性,3. 持久性,4. ⼀致性。
Redis 是不⽀持 roll back 的,因⽽不满⾜原⼦性的(⽽且不满⾜持久性)

缓存穿透、击穿和雪崩

  • 缓存击穿 (某个热点key缓存失效了)
    缓存中没有但数据库中有的数据,假如是热点数据,那key在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力增大。
    和缓存雪崩的区别在于这里针对某一key缓存,后者则是很多key。
    预防:设置热点数据不过期,定时任务定时更新缓存,或者设置互斥锁

  • 缓存雪崩 (多个热点key都过期)
    大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩
    预防:存数据的过期时间设置随机,防止同一时间大量数据过期现象发生,设置热点数据永远不过期,定时任务定时更新

  • 缓存穿透(查询不存在数据)
    查询一个不存在的数据,由于缓存是不命中的,并且出于容错考虑,如发起为id为“-1”不存在的数据
    如果从存储层查不到数据则不写入缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。存在大量查询不存在的数据,可能DB就挂掉了,这也是黑客利用不存在的key频繁攻击应用的一种方式。
    预防:接口层增加校验,数据合理性校验,缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,设置短点的过期时间,防止同个key被一直攻击

系统设计

常用框架

Spring

Spring 基础

什么是 Spring 框架?

Spring是一款开源的轻量级的Java开发框架,一般说的Spring框架都是SpringFramework,它是很多模块的集合,通过这些模块可以很快速的完成我们的开发工作,比如AOP和IOC,还有集成测试,同时也可以很方便的集成第三方组件,比如邮件啊缓存啊各种。

Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发,比如说 Spring 支持 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。
Spring 最核心的思想就是不重新造轮子,开箱即用,提高开发效率。

Spring 翻译过来就是春天的意思,可见其目标和使命就是为 Java 程序员带来春天啊!感动!
Spring 提供的核心功能主要是 IoC 和 AOP。学习 Spring ,一定要把 IoC 和 AOP 的核心思想搞懂!

Spring 包含的模块有哪些?

Spring5.x 版本
Java面试知识点概览(持续更新)_第41张图片
Spring5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。

Spring 各个模块的依赖关系如下:
Java面试知识点概览(持续更新)_第42张图片
Core Container
Spring 框架的核心模块,也可以说是基础模块,主要提供 IoC 依赖注入功能的支持。Spring 其他所有的功能基本都需要依赖于该模块,我们从上面那张 Spring 各个模块的依赖关系图就可以看出来。

  • spring-core :Spring 框架基本的核心工具类。
  • spring-beans :提供对 bean 的创建、配置和管理等功能的支持。
  • spring-context :提供对国际化、事件传播、资源加载等功能的支持。
  • spring-expression :提供对表达式语言(Spring Expression Language) SpEL 的支持,只依赖于 core 模块,不依赖于其他模块,可以单独使用。
    AOP
  • spring-aspects :该模块为与 AspectJ 的集成提供支持。
  • spring-aop :提供了面向切面的编程实现。
  • spring-instrument :提供了为 JVM 添加代理(agent)的功能。具体来讲,它为 Tomcat 提供了一个织入代理,能够为 Tomcat 传递类文 件,就像这些文件是被类加载器加载的一样。没有理解也没关系,这个模块的使用场景非常有限。
    Data Access/Integration
  • spring-jdbc :提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。
  • spring-tx :提供对事务的支持。
  • spring-orm :提供对 Hibernate、JPA 、iBatis 等 ORM 框架的支持。
  • spring-oxm :提供一个抽象层支撑 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。
  • spring-jms : 消息服务。自 Spring Framework 4.1 以后,它还提供了对 spring-messaging 模块的继承。
    Spring Web
  • spring-web :对 Web 功能的实现提供一些最基础的支持。
  • spring-webmvc :提供对 Spring MVC 的实现。
  • spring-websocket :提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
  • spring-webflux :提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。
    Messaging
    spring-messaging是从 Spring4.0 开始新加入的一个模块,主要职责是为 Spring 框架集成一些基础的报文传送应用。

Spring Test
Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。

Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。

Spring,Spring MVC,Spring Boot 之间什么关系?

很多人对 Spring,Spring MVC,Spring Boot 这三者傻傻分不清楚!这里简单介绍一下这三者,其实很简单,没有什么高深的东西。

Spring 包含了多个功能模块(上面刚刚提高过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。

下图对应的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。
Java面试知识点概览(持续更新)_第43张图片
Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
Java面试知识点概览(持续更新)_第44张图片
使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!

Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。

Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!

为什么要使用Spring?
  • Spring提供ioc技术,容器会帮你管理依赖的对象,从而不需要自己创建和管理依赖对象了,更轻松的实现了代码的解耦
  • Spring提供了事务支持,使得事务操作变得更加方便
  • Spring提供了面向切面编程,这样可以更方便的处理某一类的问题
  • 更方便的框架集成,Spring可以很方便的集成其他框架,比如Mybtis,hibernate等
使用Spinrg框架的好处是什么?

轻量:Spring是轻量的,基本的版本大约2MB
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象
面向切面编程(AOP):Spring支持面向切面编程,并且把应用业务逻辑和系统服务分开
容器:Spring包含并管理应用中对象的生命周期和配置
MVC框架:Spring的WEB框架是个精心设计的框架,是web框架的一个很好的替代品
事务管理:Spring提供了一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)
异常处理:Spring提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转换为一致的unchecked异常

Spring IoC

IOC的优点是什么?

IOC或依赖注入把应用的代码量降到最低,它使应用容易测试,单元测试不再需要单例和JNDI查找机制,最小的代价和最小的侵入性使松散耦合得以实现,IOC容易支持加载服务时的饿汉式初始化和懒加载

谈谈自己对于 Spring IoC 的了解

**IoC(Inverse of Control:控制反转)**是一种设计思想,而不是一个具体的技术实现。==IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。==不过, IoC 并非 Spring 特有,在其他语言中也有应用。

为什么叫控制反转?

控制 :指的是对象创建(实例化、管理)的权力
**反转 **:控制权交给外部环境(Spring 框架、IoC 容器)
Java面试知识点概览(持续更新)_第45张图片
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。

Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是SpringBoot 注解配置就慢慢开始流行起来。

IOC 控制反转,指将对象的创建权,反转到Spring容器
DI 依赖注入,指Spring创建对象的过程中,将对象依赖属性通过配置进行注入,不能单独存在,需要在IOC的基础上完成操作
依赖注入(DI)和控制反转(IOC)是从不同的角度的描述的同一件事情,通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。

什么是 Spring Bean?

简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。

我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。


<bean id="..." class="...">
   <constructor-arg value="..."/>
bean>

下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。
Java面试知识点概览(持续更新)_第46张图片
org.springframework.beans和 org.springframework.context 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看

将一个类声明为 Bean 的注解有哪些?
  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
@Component 和 @Bean 的区别是什么?
  • @Component 注解作用于类,而@Bean注解作用于方法。
  • @Component通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。
  • @Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。
    @Bean注解使用示例:
@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}

上面的代码相当于下面的 xml 配置

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
beans>

下面这个例子是通过 @Component 无法实现的。

@Bean
public OneService getService(status) {
    case (status)  {
        when 1:
                return new serviceImpl1();
        when 2:
                return new serviceImpl2();
        when 3:
                return new serviceImpl3();
    }
}
注入 Bean 的注解有哪些?

Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。
Java面试知识点概览(持续更新)_第47张图片
@Autowired 和@Resource使用的比较多一些。

@Autowired 和 @Resource 的区别是什么?

Autowired 属于 Spring 内置的注解,默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。

这会有什么问题呢? 当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。

这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。

// smsService 就是我们上面所说的名称
@Autowired
private SmsService smsService;

举个例子,SmsService 接口有两个实现类: SmsServiceImpl1和 SmsServiceImpl2,且它们都已经被 Spring 容器所管理。

// 报错,byName 和 byType 都无法匹配到 bean
@Autowired
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Autowired
private SmsService smsServiceImpl1;
// 正确注入  SmsServiceImpl1 对象对应的 bean
// smsServiceImpl1 就是我们上面所说的名称
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;

我们还是建议通过 @Qualifier 注解来显示指定名称而不是依赖变量的名称。

@Resource属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType。

@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。

public @interface Resource {
    String name() default "";
    Class<?> type() default Object.class;
}

如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定name 和type属性(不建议这么做)则注入方式为byType+byName。

// 报错,byName 和 byType 都无法匹配到 bean
@Resource
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Resource
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;

简单总结一下:

  • @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
  • Autowired 默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为 byName(根据名称进行匹配)。
  • 当一个接口存在多个实现类的情况下,@Autowired 和@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显示指定名称,@Resource可以通过 name 属性来显示指定名称。
Bean 的作用域有哪些?

Spring 中 Bean 的作用域通常有下面几种:

  • singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
  • prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
  • request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
  • session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
  • application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),,该 bean 仅在当前应用启动时间内有效。
  • websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
    如何配置 bean 的作用域呢?

xml 方式:

<bean id="..." class="..." scope="singleton">bean>

注解方式:

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
    return new Person();
}
单例 Bean 的线程安全问题了解吗?

大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。

常见的有两种解决办法:

在 Bean 中尽量避免定义可变的成员变量。
在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。

Bean 的生命周期了解么?

  • Bean 容器找到配置文件中 Spring Bean 的定义。
  • Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。
  • 如果涉及到一些属性值 利用 set()方法设置一些属性值。
  • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的名字。
  • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
  • 如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入 BeanFactory对象的实例。
  • 与上面的类似,如果实现了其他 Aware接口,就调用相应的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
  • 如果 Bean 实现了InitializingBean接口,执行afterPropertiesSet()方法。
  • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法
  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
  • 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。
    Java面试知识点概览(持续更新)_第48张图片

Spring AoP

谈谈自己对于 AOP 的了解

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Aspect Oriented Program 面向切面编程, 在不改变原有逻辑上增加额外的功能

AOP思想把功能分两个部分,分离系统中的各种关注点

核心关注点
业务的主要功能
横切关注点
非核心、额外增加的功能

用户下单为例子
核心关注点:创建订单
横切关注点:记录日志、控制事务
好处
减少代码侵入,解耦
可以统一处理横切逻辑
方便添加和删除横切逻辑

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
Java面试知识点概览(持续更新)_第49张图片
当然你也可以使用 AspectJ !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

AOP 切面编程设计到的一些专业术语:
Java面试知识点概览(持续更新)_第50张图片

AOP里面常见的概念

横切关注点
对哪些方法进行拦截,拦截后怎么处理,这些就叫横切关注点
比如 权限认证、日志、事物

通知 Advice
在特定的切入点上执行的增强处理
做啥? 比如你需要记录日志,控制事务 ,提前编写好通用的模块,需要的地方直接调用

连接点 JointPoint
要用通知的地方,业务流程在运行过程中需要插入切面的具体位置,一般是方法的调用前后,全部方法都可以是连接点

切入点 Pointcut
不能全部方法都是连接点,通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
过滤出相应的 Advice 将要发生的joinpoint地方

切面 Aspect
通常是一个类,里面定义 切入点+通知 , 定义在什么地方; 什么时间点、做什么事情
通知 advice指明了时间和做的事情(前置、后置等)
切入点 pointcut 指定在什么地方干这个事情
web接口设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面,对象和对象,方法和方法之间都是一个个切面

目标 target
目标类,真正的业务逻辑,可以在目标类不知情的条件下,增加新的功能到目标类的链路上

织入 Weaving
把切面(某个类)应用到目标函数的过程称为织入

Java面试知识点概览(持续更新)_第51张图片

Spring常见面试题静态代理和动态代理

能否解释下什么是静态代理
什么是静态代理

  • 由程序创建或特定工具自动生成源代码,在程序运行前,代理类的.class文件就已经存在
  • 通过将目标类与代理类实现同一个接口,让代理类持有真实类对象,然后在代理类方法中调用真实类方法,在调用真实类方法的前后添加我们所需要的功能扩展代码来达到增强的目的
    A -> B -> C

优点

  • 代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可
  • 方便增加功能,拓展业务逻辑

缺点

  • 代理类中出现大量冗余的代码,非常不利于扩展和维护
  • 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度

能否解释下什么是动态代理,spring aop是用什么代理

  • 在程序运行时,运用反射机制动态创建而成,无需手动编写代码
  • Spring AOP 就是要对目标进行代理对象的创建, Spring AOP是基于动态代理的,有动态代理机制: JDK动态代理和CGLIB动态代理
    动态代理:在虚拟机内部,运行的时候,动态生成代理类(运行时生成,runtime生成) ,并不是真正存在的类
静态代理

Count.java

/** 
 * 定义一个账户接口 
 *  
 * @author Administrator 
 *  
 */  
public interface Count {  
    // 查看账户方法  
    public void queryCount();  
  
    // 修改账户方法  
    public void updateCount();  
  
}  

CountImpl.java

/** 
 * 委托类(包含业务逻辑) 
 *  
 * @author Administrator 
 *  
 */  
public class CountImpl implements Count {  
  
    @Override  
    public void queryCount() {  
        System.out.println("查看账户方法...");  
  
    }  
  
    @Override  
    public void updateCount() {  
        System.out.println("修改账户方法...");  
  
    }  
  
}  
  
//CountProxy.java  
package net.battier.dao.impl;  
  
import net.battier.dao.Count;  
  
/** 
 * 这是一个代理类(增强CountImpl实现类) 
 *  
 * @author Administrator 
 *  
 */  
public class CountProxy implements Count {  
    private CountImpl countImpl;  
  
    /** 
     * 覆盖默认构造器 
     *  
     * @param countImpl 
     */  
    public CountProxy(CountImpl countImpl) {  
        this.countImpl = countImpl;  
    }  
  
    @Override  
    public void queryCount() {  
        System.out.println("事务处理之前");  
        // 调用委托类的方法;  
        countImpl.queryCount();  
        System.out.println("事务处理之后");  
    }  
  
    @Override  
    public void updateCount() {  
        System.out.println("事务处理之前");  
        // 调用委托类的方法;  
        countImpl.updateCount();  
        System.out.println("事务处理之后");  
  
    }  
  
}  

TestCount.java

/** 
 *测试Count类 
 *  
 * @author Administrator 
 *  
 */  
public class TestCount {  
    public static void main(String[] args) {  
        CountImpl countImpl = new CountImpl();  
        CountProxy countProxy = new CountProxy(countImpl);  
        countProxy.updateCount();  
        countProxy.queryCount();  
  
    }  
}  

观察代码可以发现每一个代理类只能为一个接口服务,这样一来程序开发中必然会产生过多的代理,而且,所有的代理操作除了调用的方法不一样之外,其他的操作都一样,则此时肯定是重复代码。解决这一问题最好的做法是可以通过一个代理类完成全部的代理功能,那么此时就必须使用动态代理完成。

JDK动态代理

1 JDK动态代理需要一个接口和一个类
1.1 InvocationHandler (调用处理程序)
InvocationHandler 是生成代理实例的类需要实现的接口,然后需要实现接口中的 invoke() 方法,在这个方法中进行对代理实例的处理
1.2 Proxy (代理)
Proxy 是所有代理实例的父类,它提供了创建动态代理实例的静态方法.
2 代码演示一
2.1 创建一个抽象对象

//租房public interface Rend {    public void rend();}

2.2 创建一个 真实对象

public class Homeowner implements Rend {    
@Override    
public void rend(){        
System.out.println("房东出租了房子");    
}}

3.3 创建一个生成代理实例的类(核心)

//这个类是用来生成代理实例的类
public class ProxyInvocationHandle implements InvocationHandler {

    //被代理的接口
    private Rend rend;

    public void setRend(Rend rend) {
        this.rend = rend;
    }
	/**
 * 参数说明: 
 * ClassLoader loader:类加载器 
 * Class[] interfaces:得到全部的接口 
 * InvocationHandler h:得到InvocationHandler接口的子类实例
 */
    //生成得到代理类
    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),rend.getClass().getInterfaces(),this);
    }
        /**
         * 参数说明: 
         * Object proxy:指被代理的对象。 
         * Method method:要调用的方法 
         * Object[] args:方法调用时所需要的参数 
         */
    //处理代理实例,并返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //动态代理的本质,就是使用反射机制实现!
        seeHouse();
        seeHouse();
        Object result = method.invoke(rend, args);
        fare();
        return result;
    }

    public void seeHouse(){
        System.out.println("中介带你看房子");
    }
    public void fare(){
        System.out.println("收中介费");
    }
}

3.4 创建客户类

public class Client {
    public static void main(String[] args) {
        //真实角色
        Homeowner homeowner = new Homeowner();
        //代理角色:现在没有
        ProxyInvocationHandle pih = new ProxyInvocationHandle();
        //通过调用程序处理角色来处理我们要调用的接口对象!
        pih.setRend(homeowner);
        Rend proxy = (Rend) pih.getProxy();//这里的proxy就是动态生成的,我们并没有写
        proxy.rend();
    }
}

但是,JDK的动态代理依靠接口实现,如果有些类并没有实现接口,则不能使用JDK代理,这就要使用cglib动态代理了。

4 总结

  • 动态代理解决了静态代理创建过多的代理类导致开发效率降低的问题
  • 动态代理的角色和静态代理的是相同的
  • 动态代理的代理类是动态生成的 . 静态代理的代理类是我们提前写好的
  • 一个动态代理 , 一般代理某一类业务
  • 一个动态代理可以代理多个类,代理的是接口
  • 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情
  • 公共的业务由代理来完成 . 实现了业务的分工
  • 公共业务发生扩展时变得更加集中和方便
Cglib动态代理

JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

1、BookFacadeCglib.java

public interface BookFacade {  
    public void addBook();  
}  

2、BookCadeImpl1.java

/** 
 * 这个是没有实现接口的实现类 
 *  
 * @author student 
 *  
 */  
public class BookFacadeImpl1 {  
    public void addBook() {  
        System.out.println("增加图书的普通方法...");  
    }  
} 

BookFacadeProxy.java

/** 
 * 使用cglib动态代理 
 *  
 * @author student 
 *  
 */  
public class BookFacadeCglib implements MethodInterceptor {  
    private Object target;  
  
    /** 
     * 创建代理对象 
     *  
     * @param target 
     * @return 
     */  
    public Object getInstance(Object target) {  
        this.target = target;  
        Enhancer enhancer = new Enhancer();  
        enhancer.setSuperclass(this.target.getClass());  
        // 回调方法  
        enhancer.setCallback(this);  
        // 创建代理对象  
        return enhancer.create();  
    }  
  
    @Override  
    // 回调方法  
    public Object intercept(Object obj, Method method, Object[] args,  
            MethodProxy proxy) throws Throwable {  
        System.out.println("事物开始");  
        proxy.invokeSuper(obj, args);  
        System.out.println("事物结束");  
        return null;  
  
  
    }  
  
} 

4、TestCglib.java

public class TestCglib {  
      
    public static void main(String[] args) {  
        BookFacadeCglib cglib=new BookFacadeCglib();  
        BookFacadeImpl1 bookCglib=(BookFacadeImpl1)cglib.getInstance(new BookFacadeImpl1());  
        bookCglib.addBook();  
    }  
}  
JDK动态代理和CGLib动态代理的区别
  • JDK动态代理,要求目标对象实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以用CGLib动态代理
  • JDK动态代理是自带的,CGlib需要引入第三方包
  • CGLib动态代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展
  • CGLib动态代理基于继承来实现代理,所以无法对final类、private方法和static方法实现代理
Spring AOP中的代理使用的默认策略?
  • 如果目标对象实现了接口,则默认采用JDK动态代理
  • 如果目标对象没有实现接口,则采用CgLib进行动态代理
  • 如果目标对象实现了接口,程序里面依旧可以指定使用CGlib动态代理
Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。

AspectJ 定义的通知类型有哪些?
  • Before(前置通知):目标对象的方法调用之前触发
  • After (后置通知):目标对象的方法调用之后触发
  • AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发
  • AfterThrowing(异常通知) :目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
  • Around:(环绕通知)编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法
多个切面的执行顺序如何控制?

1、通常使用@Order 注解直接定义切面顺序

// 值越小优先级越高
@Order(3)
@Component
@Aspect
public class LoggingAspect implements Ordered {

2、实现Ordered 接口重写 getOrder 方法。

@Component
@Aspect
public class LoggingAspect implements Ordered {

    // ....

    @Override
    public int getOrder() {
        // 返回值越小优先级越高
        return 1;
    }
}

@RestController vs @Controller

@Controller 返回⼀个⻚⾯
单独使⽤ @Controller 不加 @ResponseBody 的话⼀般使⽤在要返回⼀个视图的情况,这种情况属于比较传统的Spring MVC 的应⽤,对应于前后端不分离的情况。
Java面试知识点概览(持续更新)_第52张图片

@RestController 返回JSON 或 XML 形式数据
但 @RestController 只返回对象,对象数据直接以 JSON 或 XML 形式写⼊ HTTP 响应
(Response)中,这种情况属于 RESTful Web服务,这也是⽬前⽇常开发所接触的最常⽤的情况
(前后端分离)。
Java面试知识点概览(持续更新)_第53张图片
@Controller +@ResponseBody 返回JSON 或 XML 形式数据
@ResponseBody 注解的作⽤是将 Controller 的⽅法返回的对象通过适当的转换器转换为指定的格式之后,写⼊到HTTP 响应(Response)对象的 body 中,通常⽤来返回 JSON 或者XML 数据,返回 JSON 数据的情况比较多。
Java面试知识点概览(持续更新)_第54张图片

Spring 框架中用到了哪些设计模式?

  • 工厂设计模式 : Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。

Spring 事务

Spring 管理事务的方式有几种?

编程式事务 :在代码中硬编码(不推荐使用) : 通过 TransactionTemplate或者 TransactionManager 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
声明式事务 :在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)

Spring 事务中哪几种事务传播行为?

事务传播行为是为了解决业务层方法之间互相调用的事务问题。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

正确的事务传播行为可能的值如下:

1.TransactionDefinition.PROPAGATION_REQUIRED

使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

2.TransactionDefinition.PROPAGATION_REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

3.TransactionDefinition.PROPAGATION_NESTED

如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

4.TransactionDefinition.PROPAGATION_MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

这个使用的很少。

若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚:

  • TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
Spring 事务中的隔离级别有哪几种?

和事务传播行为这块一样,为了方便使用,Spring 也相应地定义了一个枚举类:Isolation

public enum Isolation {

    DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

    READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

    READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

    REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

    SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

    private final int value;

    Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }

}

下面我依次对每一种事务隔离级别进行介绍:

  • TransactionDefinition.ISOLATION_DEFAULT :使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • TransactionDefinition.ISOLATION_READ_COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
@Transactional(rollbackFor = Exception.class)注解了解吗?

Exception 分为运行时异常 RuntimeException 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。

当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。

在 @Transactional 注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚,加上 rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。

解释JDBC抽象和DAOm欧快

通过使用JDBC抽象和DAO模块,保证数据库代码的简介,并能避免数据库资源错误关闭导致的问题,它在各种不同的数据库的错误信息之上,提供了一个统一的异常访问层,它还利用Spring的AOP模块给Spring应用中的对象提供事务管理服务

Spring常用的注入方式有哪些?

setter属性注入
构造方法注入
注解方式注入

Sping配置文件作用

Spring配置文件是个XML文件,这个文件包含了类信息,描述了如何配置它们,已经如何相互调用

什么是SpingBeans?

SpringBeans是那些形成Spring应用的主干的java对象,它们被SpringIOC容器初始化,装配和管理,这些beans通过容器中配置的元数据创建,比如,以xml文件中的形式定义
Spring’框架定义的beans都是单间beans,在bean tag中有个属性"singleton",如果它被赋为True,bean就是单件,否则就是一个prototype bean,默认是true,所以所有在spring框架中的beans缺省都是单件

一个SpringBean定义包含什么?

一个SpringBean的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖

Spring中的bean是线程安全的吗?

spring中的bean默认是单例模式,spring框架并没有对单例bean进行多线程的封装处理
实际上大部分时候springbean无状态的(比如果dao类),所以某种程度上来说bean也是安全的,但是如果bean有状态的话(比如view model对象),那就要开发者自己去保证线程安全了,最简单的就是改变bean的作用域,把"singleton"变更为"prototype",这样请求bean相当于new Bean()了,所以就可以保证线程安全了

  • 有状态就是有数据存储功能
  • 无状态就是不会保存数据

Spring自动装配bean有哪些方式?

  • no:默认值,表示没有自动装配,应使用显示bean引用进行装配
  • byName:它根据bean的名称注入对象依赖项
  • byType:它根据类型注入对象依赖项
  • 构造函数:通过构造函数来注入依赖项,需要设置大量的参数
  • autodetect:容器首先通过构造函数使用autowired装配,如果不能,则通过byType自动装配

什么是基于java的spring注解配置?给一些注解的例子

基于java的配置,允许你在少量的java注解的帮助下,进行你的大部分Spring配置而非通过xml文件
以@Configuration注解为例,它用来标记类可以当做一个bean的定义,被SpringIOC容器使用,另一个例子是@Bean注解,它表示次方法将要返回一个对象,作为一个bean注册进Spring应用上下文

什么是基于注解的容器配置?

相对于xml文件,注解型的配置依赖于通过字节码元数据装配组件,而非尖括号的声明
开发者通过在相应的类,方法或属性上使用注解的方式,直接组件类中进行配置,而不使用xml表述bean的装配关系

@Required注解

这个注解表明bean的属性必须在配置的时候设置,通过一个bean定义的显示的属性值或通过自动装配,若@Required注解的bean属性未被设置,容器将抛出BeanInitializatioonException

@Autowired注解

@Autowired注解提供了一种更细粒度的控制,包括在何处以及如何完成自动装配,它的用法和@Required一样.修饰setter方法,构造器,属性或者具有任意名称和多个参数的方法

@Qualifier注解

当有多个相同类型的bean却只有一个需要自动装配时候,将@Qualifier注解和@Autowired注解结合使用以消除这种混淆,指定需要装配的确切的bean

在Spring框架中如何更有效的使用jdbc?

使用SpringJDBC框架,资源管理和错误处理的代价将会被减轻,所有开发者只需要写statements和querues从数据存取数据,jdbc也可以在spring框架提供的模板类的帮助下更有效的被使用,这个模板叫做jdbcTemplate

jdbcTemplate

jdbcTemplate类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理

Spring MVC

说说自己对于 Spring MVC 了解?

MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
Java面试知识点概览(持续更新)_第55张图片
Model 1 时代

很多学 Java 后端比较晚的朋友可能并没有接触过 Model 1 时代下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。

这个模式下 JSP 即是控制层(Controller)又是表现层(View)。显而易见,这种模式存在很多问题。比如控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;再比如前端和后端相互依赖,难以进行测试维护并且开发效率极低。

Model 2 时代

学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。

Model:系统涉及的数据,也就是 dao 和 bean。
View:展示模型中的数据,只是用来展示。
Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。
Model2 模式下还存在很多问题,Model2 的抽象和封装程度还远远不够,使用 Model2 进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。

于是,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。

Spring MVC 时代

随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。

MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。

Spring MVC 的核心组件有哪些?

记住了下面这些组件,也就记住了 SpringMVC 的工作原理。

  • DispatcherServlet :核心的中央处理器,负责接收请求、分发,并给予客户端响应。
  • HandlerMapping :处理器映射器,根据 uri 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。
  • HandlerAdapter :处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler;
  • Handler :请求处理器,处理实际请求的处理器。
  • ViewResolver :视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端

SpringMVC 工作原理了解吗?

Spring MVC 原理如下图所示:
Java面试知识点概览(持续更新)_第56张图片
流程说明(重要):

1.客户端(浏览器)发送请求, DispatcherServlet拦截请求。
2.DispatcherServlet 根据请求信息调用 HandlerMapping 。HandlerMapping 根据 uri 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
3.DispatcherServlet 调用 HandlerAdapter适配执行 Handler 。
4.Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServlet,ModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
5.ViewResolver 会根据逻辑 View 查找实际的 View。
6.DispaterServlet 把返回的 Model 传给 View(视图渲染)。
7.把 View 返回给请求者(浏览器)

统一异常处理怎么做?

推荐使用注解的方式统一异常处理,具体会使用到 @ControllerAdvice + @ExceptionHandler 这两个注解 。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(BaseException.class)
    public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
      //......
    }

    @ExceptionHandler(value = ResourceNotFoundException.class)
    public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
      //......
    }
}

这种异常处理方式下,会给所有或者指定的 Controller 织入异常处理的逻辑(AOP),当 Controller 中的方法抛出异常的时候,由被@ExceptionHandler 注解修饰的方法进行处理。

ExceptionHandlerMethodResolver 中 getMappedMethod 方法决定了异常具体被哪个被 @ExceptionHandler 注解修饰的方法处理异常。

@Nullable
 private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
  List<Class<? extends Throwable>> matches = new ArrayList<>();
    //找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系
  for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
   if (mappedException.isAssignableFrom(exceptionType)) {
    matches.add(mappedException);
   }
  }
    // 不为空说明有方法处理异常
  if (!matches.isEmpty()) {
      // 按照匹配程度从小到大排序
   matches.sort(new ExceptionDepthComparator(exceptionType));
      // 返回处理异常的方法
   return this.mappedMethods.get(matches.get(0));
  }
  else {
   return null;
  }
 }

从源代码看出:getMappedMethod()会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。

Spring Boot

SpringBoot核心配置文件是什么?

  • bootstrap(.yam或者.properties):bootstrap由父ApplicationContext加载的,比application优先加载,且boostrap里面的的属性不能被覆盖
  • application(.yam或者.properties):用于SpringBoot项目的自动化配置

SpringBoot有哪些方式可以实现热部署?

使用devtools启动热部署,添加devtools库,在配置文件中把spring.devtools.restart.enable设置为true
使用IDEA编辑器,勾上自动编译或者手动重新编译

SpringBoot中的监视器是什么?

SpringBootActuator是Spring启动框架中的重要功能之一,SpringBoot监视器可帮助你访问生产环境中正在运行的应用程序的当前状态,有几个指标必须在生产环境中进行检查和监控,集市一些外部应用程序可以正在使用这些服务来向相关人员触发警报消息,监视器模块公开了一组可直接作为HTTP url 访问的rest端点来检查状态

如何在SpringBoot中禁用Actuator端点安全性?

默认情况下,所有敏感的http端点都是安全的,只有具有Actuator角色的用户才能访问它们,安全性是使用标准的HttpServletRequest.isUserInRole方法实施的
我们可以使用management.security.enabled=false来禁用安全性
只有在执行机构端点在防火墙后访问时,才建议禁用安全性

如何使用SringBoot实现异常处理?

Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法,我们可以通过实现一个ControllerAdvice类,来处理控制器类抛出的所有异常

什么是WebSockets?

WebSocket是一种计算机通信协议,通过单个tcp连接提供全双工通信信道
WebSocket是双向的,使用WebSocket客户端或服务器可以发起消息发送
WebSocket是全双工的,客户端和服务器通信是相互独立的
单个tcp连接,初始连接使用http,然后将此连接升级到基于套接字的连接,然后这个单一连接用于所有未来的通信Light,与http相比,WebSocket消息i数据交换要轻得多

分布式事务的两阶段提交

第一阶段:准备阶段;第二阶段:提交阶段。

准备阶段

事务协调者(事务管理器)给每个参与者(资源管理器)发送 Prepare 消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的 redo 和 undo 日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

提交阶段:

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

将提交分成两阶段进行的目的很明确,就是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作。

@Mapper和@Repository区别

相同点:

  • @Mapper和@Repository都是作用在dao层接口,使得其生成代理对象bean,交给spring 容器管理
  • 对于mybatis来说,都可以不用写mapper.xml文件

不同点:

  • @Mapper不需要配置扫描地址,可以单独使用,如果有多个mapper文件的话,可以在项目启动类中加入@MapperScan(“mapper文件所在包”)
  • @Repository不可以单独使用,否则会报错误,要想用,必须配置扫描地址(@MapperScannerConfigurer)

@PathVariable

在路由中定义变量规则后,通常我们需要在处理方法(也就是@RequestMapping注解的方法)中获取这个URL的具体值,并根据这个值(例如用户名)做相应的操作,SpringMVC提供的@PathVariable可以帮助我们:
@RequestMapping(value=“/user/{username}”)
public String userProfile(@PathVariable(value=“username”) String username) {
return “user”+username;
}
在上面的例子中,当@Controller处理HTTP请求时,userProfile的参数username会自动设置为URL中对应变量username(同名赋值)的值。

@RequestParam

在SpringMVC框架中,可以通过定义@RequestMapping来处理URL请求。和@PathVariable一样,需要在处理URL的函数中获取URL中的参数,也就是?key1=value1&key2=value2这样的参数列表。通过注解@RequestParam可以轻松地将URL中的参数绑定到处理函数方法的变量中:一旦我们在方法中定义了@RequestParam变量,如果访问的URL中不带有相应的参数,就会抛出异常——这是显然的,Spring尝试帮我们进行绑定,然而没有成功。但有的时候,参数确实不一定永远都存在,这时我们可以通过定义required属性:@RequestParam(value = “username”,required = false)

@RequestParam和@PathVariable的相同点和区别

@RequestParam和@PathVariable都能够完成类似的功能——因为本质上,它们都是用户的输入,只不过输入的部分不同,一个在URL路径部分,另一个在参数部分。要访问一篇博客文章,这两种URL设计都是可以的:
通过@PathVariable,例如/blogs/1
通过@RequestParam,例如blogs?blogId=1
那么究竟应该选择哪一种呢?建议:
1、当URL指向的是某一具体业务资源(或资源列表),例如博客,用户时,使用@PathVariable
2、当URL需要对资源或者资源列表进行过滤,筛选时,用@RequestParam

@RequestPart和@RequestBody

@RequestPart 接收文件以及其他更为复杂的数据类型
比如 XXX(@RequestPart(“file”) MultipartFile file, @RequestPart(“userVO”) UserVO userVO) 复杂协议

@RequestBody
主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);而最常用的使用请求体传参的无疑是POST请求了,
所以使用@RequestBody接收数据时,一般都用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()
可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。
RequestBody 接收的是请求体里面的数据;而RequestParam接收的是key-value里面的参数

@JsonProperty

注解主要用于实体类的属性上,作用可以简单的理解为在反序列化的时候给属性重命名(多一个名字来识别)

Mybatis

JDBC连接数据库的开发步骤

  1. 加载数据库驱动
  2. 获取数据连接对象
  3. 获取语句对象
    会话对象有两种Statement和PreparedStatement执行语句,他们区别是?
    PreparedStatement在执行之前会进行预编译
    效率高于Statement,且能够有效防止SQL注入
    PreparedStatement支持?占位符而不是直接拼接,提高可读性
  4. 处理结果集
  5. 关闭资源
    rs.close()、st.close()、conn.close() 注意关闭顺序以及处理异常

Mybatis加载的流程

  • 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心
  • SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得
  • SqlSessionFactoryBuilder 可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出SqlSessionFactory 实例
  • 工厂设计模式里面 需要获取SqlSession ,里面提供了在数据库执行 SQL 命令所需的所有方法

Dao 接口

通常⼀个 Xml 映射⽂件,都会写⼀个 Dao 接⼝与之对应,请问,这个 Dao 接⼝的⼯作原理是什么?Dao 接⼝⾥的⽅法,参数不同时,⽅法能重载吗?
Dao 接⼝,就是⼈们常说的 Mapper 接⼝,接⼝的全限名,就是映射⽂件中的 namespace的值,接⼝的⽅法名,就是映射⽂件中 MappedStatement 的 id 值,接⼝⽅法内的参数,就是传递给 sql 的参数。 Mapper 接⼝是没有实现类的,当调⽤接⼝⽅法时,接⼝全限名+⽅法名拼接字符串作为 key 值,可唯⼀定位⼀个 MappedStatement
举例: com.mybatis3.mappers.StudentDao.findStudentById ,可以唯⼀找到 namespace
为 com.mybatis3.mappers.StudentDao 下⾯ id = findStudentById 的 MappedStatement 。在 Mybatis
中,每⼀个 、 、 、 标签,都会被解析为⼀个 MappedStatement 对象。
Dao 接⼝⾥的⽅法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
Dao 接⼝的⼯作原理是 JDK 动态代理,Mybatis 运⾏时会使⽤ JDK 动态代理为 Dao 接⼝⽣成代理 proxy 对象,代理对象 proxy 会拦截接⼝⽅法,转⽽执⾏ MappedStatement 所代表的 sql,然后将 sql 执⾏结果返回。

mybatis3.x 防止sql注入

  • { }和${}的区别是什么?

    #{}是预编译处理,KaTeX parse error: Expected 'EOF', got '#' at position 23: …换。 Mybatis在处理#̲{}时,会将sql中的#{}替…{}时,就是把${}替换成变量的值。
    使用#{}可以有效的防止SQL注入,提高系统安全性

XML映射文件标签

select|insert|updae|delete 标签之外,还有哪些标签?
、 、 、 、 ,加上动态 sql 的 9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind 等,其中为 sql ⽚段标签,通过 标签引⼊ sql ⽚段, 为不⽀持⾃增的主键⽣成策略标签。

动态sql

Mybatis 动态 sql 可以让我们在 Xml 映射⽂件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能,Mybatis 提供了 9 种动态 sql 标签trim|where|set|foreach|if|choose|when|otherwise|bind 。

Myabtis多级缓存

有没用过Mybatis一级缓存,能否介绍下
一级缓存的作用域是SQLSession,同一个SqlSession中执行相同的SQL查询(相同的SQL和参数),第一次会去查询数据库并写在缓存中,第二次会直接从缓存中取
基于PerpetualCache 的 HashMap本地缓存,默认开启一级缓存
失效策略:当执行SQL时候两次查询中间发生了增删改的操作,即insert、update、delete等操作commit后会清空该SQLSession缓存; 比如sqlsession关闭,或者清空等

有没用过Mybatis二级缓存,能否介绍下
二级缓存是namespace级别的,多个SqlSession去操作同一个namespace下的Mapper的sql语句,多个SqlSession可以共用二级缓存,如果两个mapper的namespace相同,(即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中,但是最后是每个Mapper单独的名称空间)

基于PerpetualCache 的 HashMap本地缓存,可自定义存储源,如 Ehcache/Redis等

默认是没有开启二级缓存

操作流程:
第一次调用某个namespace下的SQL去查询信息,查询到的信息会存放该mapper对应的二级缓存区域。
第二次调用同个namespace下的mapper映射文件中,相同的sql去查询信息,会去对应的二级缓存内取结果
失效策略:执行同个namespace下的mapepr映射文件中增删改sql,并执行了commit操作,会清空该二级缓存

注意:实现二级缓存的时候,MyBatis建议返回的POJO是可序列化的, 也就是建议实现Serializable接口
缓存淘汰策略:会使用默认的 LRU 算法来收回(最近最少使用的)

一级缓存和二级缓存同时启用,查询顺序是怎样的?
优先查询二级缓存-》查询一级缓存-》数据库

Mybatis3.X 懒加载

什么是Mybatis3.X的懒加载?

  • 按需加载,先从单表查询,需要时再从关联表去关联查询,能大大提高数据库性能, 并不是所有场景下使用懒加载都能提高效率

哪些查询配置支持懒加载

  • resultMap里面的association、collection有延迟加载功能
<resultMap id="VideoOrderResultMapLazy" type="VideoOrder">
        <id column="id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="out_trade_no" property="outTradeNo"/>
        <result column="create_time" property="createTime"/>
        <result column="state" property="state"/>
        <result column="total_fee" property="totalFee"/>
        <result column="video_id" property="videoId"/>
        <result column="video_title" property="videoTitle"/>
        <result column="video_img" property="videoImg"/>


        <association property="user" javaType="User" column="user_id" select="findUserByUserId"/>


resultMap>

    
<select id="queryVideoOrderListLazy" resultMap="VideoOrderResultMapLazy">

        select
         o.id id,
         o.user_id ,
         o.out_trade_no,
         o.create_time,
         o.state,
         o.total_fee,
         o.video_id,
         o.video_title,
         o.video_img
         from video_order o

select>


<select id="findUserByUserId" resultType="User">

       select  * from user where id=#{id}

select>

parameterType

parameterType为输入参数,在配置的时候,配置相应的输入参数类型即可。parameterType有基本数据类型和复杂的数据类型配置。
1.基本数据类型,如输入参数只有一个,其数据类型可以是基本的数据类型,也可以是自己定的类类型。包括int,String,Integer,Date,如下:

select from user where id = #{id}

JPA

Netty

认证权限

Cookie、Session

说下Cookie和Session的区别和联系
cookie数据保存在客户端,session数据保存在服务端
cookie不是很安全,容易泄露,不能直接明文存储信息
Cookie大小和数量存储有限制

你们公司C端业务登录的是怎样做的(业务量大,集群部署)
部分业务是采用redis替代本身的tomcat单机session (业务需要高度可控)
还有其他业务是使用JSON Web token (C端普通业务)

JWT

JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名

JWT格式组成 头部、负载、签名

header+payload+signature
头部:主要是描述签名算法
负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
简单来说: 就是通过一定规范来生成token,然后可以通过解密算法逆向解密token,这样就可以获取用户信息
为啥使用这个呢,有什么优缺点
优点

  • 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
  • 存储在客户端,不占用服务端的内存资源,使用加解密的方式进行校验,在分布式业务中能较好的提高性能和节省空间

缺点

  • token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,如用户权限,密码等
  • 如果没有服务端存储,则不能做登录失效处理,除非服务端改秘钥

生成的token,在客户端或者浏览器是怎么存储的
可以存储在cookie,localstorage和sessionStorage里面

JWT令牌刷新机制

问题来源
JWT令牌保存在客户端,会存在过期时间,那么如果令牌一直没有变化,那么过期时间也不会发生变化。假设一个JWT令牌的过期时间是5天,
但是用户在这5天内一直在使用本系统,那么理论上当到了第五天的时候就应该是自动对这个令牌进行续期操作,而不是让用户重新登录。
解决办法
双令牌机制
设置长短日期的两个令牌,两个令牌都传给客户端,客户端每次携带两个令牌请求
当两个令牌都没有过期的时候,服务端正常验证逻辑
如果短令牌过期,长令牌没有过期,那么服务端重新生成两个新的令牌返回给客户端,客户端下次就带着新的令牌请求,完成了令牌的自动刷新。
缓存令牌机制
服务端不仅将令牌返回给客户端,同时将令牌缓存到Redis中,缓存时间是客户端令牌的过期时间的一倍
如果客户端令牌过期了,但是Redis中的没有过期,那么就生成一个新的令牌返回给客户端,完成自动的令牌续期
如果两者都过期了,那么就让用户重新登录。

SSO

SSO(Single Sign On)即单点登录说的是⽤户登陆多个⼦系统的其中⼀个就有权访问与其相关的其他系统。举个例⼦我们在登陆了京东⾦融之后,我们同时也成功登陆京东的京东超市、京东家电等⼦系统

OAuth2

OAuth 是⼀个⾏业的标准授权协议,主要⽤来授权第三⽅应⽤获取有限的权限。⽽ OAuth 2.0是 对 OAuth 1.0 的完全重新设计,OAuth 2.0更快,更容易实现,OAuth 1.0 已经被废弃。实际上它就是⼀种授权机制,它的最终⽬的是为第三⽅应⽤颁发⼀个有时效性的令牌 token,使得第三⽅应⽤能够通过该令牌获取相关的资源。
OAuth 2.0 比较常⽤的场景就是第三⽅登录,当你的⽹站接⼊了第三⽅登录的时候⼀般就是使⽤的 OAuth 2.0 协议。另外,现在OAuth 2.0也常⻅于⽀付场景(微信⽀付、⽀付宝⽀付)和开发平台(微信开放平
台、阿⾥开放平台等等)。

分布式

Kafka

kafka可以脱离zookeeper单独使用吗?为什么?

Kafka不能脱离zookeeper单独使用,因为kafka使用zookeeper管理和协调kafka的节点服务器(3.0可以脱离zookeeper)

kafka有几种数据保留的策略?

kafka有两种数据保存策略:按照过期时间保留和按照存储的消息大小保留

kafka同时设置了七天和10G清除数据,到达第五天的时候消息到达了101G,这个时候kafka将如何处理?

这个时候kafka会执行数据清除工作,时间和大小不论那个满足条件,都会清空数据

什么情况会导致kafka运行变慢?

  • cpu性能瓶颈
  • 磁盘读写瓶颈
  • 网络瓶颈

使用kafka集群需要注意什么?

  • 集群的数量不是越多越好,最好不要超过7个,因为节点越多,消息复制需要的时间就越长,整个集群的吞吐量就越低
  • 集群数量最好是单数,因为超过一半故障集群就不能用了,设置为单数容错率更高

kafka的设计是什么样的呢?

kafka将消息以topic为单位进行归纳
将向kafka topic发布消息的程序称为producers
将预定topics并消费消息的程序称为consumer
kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker
producers通过网络将消息发送到kafka集群,集群向消费者提供消息

数据传输的事务定义为哪三种?

数据传输的事务定义通常有以下三种级别:

  • 最多一次:消息不会被重复发送,最多被传输一次,但也有可能一次不传输
  • 最少一次:消息不会被漏发送,最少被传输一次,但也有可能被重复发送
  • 精确的一次(Exactly once):不会漏传输也不会重复传输,每个消息都被传输一次而且仅仅被传输一次,这是大家所期望的

kafka判断一个节点是否还存活有那两个条件?

  • 节点必须可以维护和zookeeper的连接,zookeeper通过心跳机制检查每个节点的连接
  • 如果节点是个follower,它必须能及时得同步leader的写操作,延时不能太久

producer是否直接将数据发送到broker的leader(主节点)?

producer直接将数据发送到broker的leader(主节点),不需要再多个节点进行分发,为了帮助prodecer做到这点,所有kafka节点都可以及时的告知,那些节点是活动的,目标topic目标分区的leader在哪,这样producer就可以直接将消息发送到目的地了

kafka consumer是否可以消费指定分区消息?

kafka consumer消费消息时,向broker发出"fetch"请求去消费特定分许的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,consumer拥有了offset的控制权,可以向后回滚去重新消费之前的消息

kafka消息是采用pull模式,还是push模式

kafka最初考虑的问题是,consumer应该从broker拉去消息还是broker将消息推送到consumer,也就是pull还是push,在这方面,kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息

一些消息系统比如scribe和 apache flume 采用了push模式,将消息推送到下游的consumer,这样做有好处也有坏处,由broker决定消息的速率,对于不同消费塑料的consumer就不太好处理了,消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是.push模式下,当broker推送的速率远大于comsumer消费的速率时,consumer恐怕就要崩溃了,最终kafka还是选取了传统的pull模式

pull模式的另外个好处是consumer可自主决定是否批量的从broker拉去数据,push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送,如果为了避免consumer奔溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费,pull模式下,consumer就可以根据自己的消费能力去决定这些策略

pull有个缺点是,如果broker没有可供消费的消息,将大道至consumer不断在循环中轮询,知道新消息到达,为了避免这一些,kafka有个参数可以让consumer阻塞直到新的消息到达(当然也可以阻塞直到消息的数量达到某个特定的量这样就可以批量发)

kafka存储在硬盘上的消息格式是什么?

消息由一个固定长度的头部和可变长度的字节数组组成,头部包含了一个版本号和CRC32校验码
消息长度:4bytes
版本号:1byte
CRC校验码:4bytes
具体的消息:n bytes

kafka高效文件存储设计特点:

  1. kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用
  2. 通过索引信息可以快速定位message和确定response的最大大小
  3. 通过index元数据全部映射到memory,可以避免segment file 的IO磁盘操作
  4. 通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小

kafka与传统消息系统之间有三个关键区别

  1. kafka持久化文件,这些日志可以被重复读取和无限期保留
  2. kafka是一个分布式系统,它可以以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性
  3. kafka支持实时的流式处理

partition的数据如何保存到硬盘

topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增,且消息有序
parition文件下有多个segment(xxx.index,xxx.log)
segment文件里的大小和配置文件大小一致可以根据要求修改,默认为1g
如果大小大于1g时,会滚动一个新的segemtn并且以上一个segmen最后一条消息的偏移量命名

kafka的ack机制

request.required.acks有三个值0,1,-1
0:生产者不会等带broker的ack,这个延迟最低但是存储的保证最弱,当server挂掉的时候就会丢数据
1:服务端会等待ack值leader副本确认接收到消息会发送ack但是如果leader挂掉后它不确保是否复制完成新laader也会导致数据丢失
01:同样在1的基础上,服务端会等所有follower的副本收到数据后才会收到leader发出的ack,这样数据不会丢失

kafka的消费者如何消费数据

消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置等到下次消费时,它会接着上次位置继续消费

消费者负载均衡策略

一个消费者组中的一个分片对应一个消费者成员,它能保证消费者成员都能访问,如果组中成员太多会有空闲的成员

数据有序

一个消费者组里它的内部是有序的,消费者组与消费者之间是无序的

kafka生产数据时数据的分组策略

生产者决定数据产生到集群的哪一个pariton中,每个消息都是以key value格式,key是由生产者发送数据传入,所以生产者(key)决定了数据产生到集群的那个parition

kafka新建的分区会在那个目录下创建

在启动kafka集群之前,我们需要配置好log.dirs参数,其值是kafka数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分割,通过这些目录是分布在不同的磁盘上用于提高读写性能
当然我们也可以配置log,dir参数,含义一样,只需要设置设其中一个即可
如果log.dirs参数只配置了一个目录,那么分配到各个broker上的分区肯定只能在这个目录下创建文件夹用于存放数据
但是如果log,dirs参数配置了多个目录,那么kafka会在那个文件夹中创建目录呢?
答案是:kafka会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为Topic名+分区ID,注意,是分区文件夹最少的目录,而不会磁盘使用量最少的目录,也就是说,如果你给log,dirs参数新增了一个新的磁盘,新的分区目录肯定实先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少的为之

Elasticsearch

RPC

Dubbo

Dubbo的负载均衡策略有哪些?

主要有random(随机,这种是默认的负载均衡策略)、RoundRobin (轮询)、LeastActive (最少活跃数)、ConsistentHash(一致性hash)可以在暴露服务的时候使用loadbalance进行指定。
随机:在一个截面上碰撞的概率高,调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
轮询:存在慢的提供者累积请求的问题,一台机器很慢,但没挂,当请求轮询到那台机子就卡在那,久而久之,所有请求都卡在那台服务器上。
最少活跃数:使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
一致性hash:当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。可以方便节点的增加及移除

Dubbo服务调用超时怎么办?

dubbo在调用服务不成功时,默认是会重试两次的。这样在服务端的处理时间超过了设定的超时时间时,就会有重复请求,此时在接口设计的时候,需要考虑接口的幂等性,避免重复调用导致出现脏数据。

说说Dubbo的运行机制、整体架构

Dubbo基于生产者、消费者的模式,
首先服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

消息队列

你用过消息队列,引入队列有啥优缺点,对比其他消息中间产品,选择这款的原因是啥?

优点:解耦系统、异步化、削峰
缺点: 系统可用性降低、复杂度增高、维护成本增高
主流消息队列Apache ActiveMQ、Kafka、RabbitMQ、RocketMQ

  • ActiveMQ:http://activemq.apache.org/
    Apache出品,历史悠久,支持多种语言的客户端和协议,支持多种语言Java, .NET, C++ 等,基于JMS Provider的实现
    缺点:吞吐量不高,多队列的时候性能下降,存在消息丢失的情况,比较少大规模使用

  • Kafka:http://kafka.apache.org/
    是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理大规模的网站中的所有动作流数据(网页浏览,搜索和其他用户的行动),副本集机制,实现数据冗余,保障数据尽量不丢失;支持多个生产者和消费者
    缺点:不支持批量和广播消息,运维难度大,文档比较少, 需要掌握Scala

  • RabbitMQ:http://www.rabbitmq.com/
    是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不错
    缺点:使用Erlang开发,阅读和修改源码难度大

  • RocketMQ:http://rocketmq.apache.org/
    阿里开源的一款的消息中间件, 纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点, 性能强劲(零拷贝技术),支持海量堆积, 支持指定次数和时间间隔的失败消息重发,支持consumer端tag过滤、延迟消息等,在阿里内部进行大规模使用,适合在电商,互联网金融等领域使用
    缺点:成熟的资料相对不多,社区处于新生状态但是热度高

消息队列三个场景:解耦、异步、削峰

解耦

A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E系统也要这个数据呢?那如果 C系统现在不需要了呢?A 系统负责崩溃在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来? 如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护 这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。

异步

再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200= 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快!

削峰

一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟
最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。

消息发送方式

发送方式一般分三种

  • SYNC 同步发送
    应用场景:重要通知邮件、报名短信通知、营销短信系统等
  • ASYNC 异步发送
    应用场景:对RT时间敏感,可以支持更高的并发,回调成功触发相对应的业务,比如注册成功后通知积分系统发放优惠券
  • ONEWAY 无需要等待响应
    应用场景:主要是日志收集,适用于某些耗时非常短,但对可靠性要求并不高的场景, 也就是LogServer, 只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求 不等待应答
    发送方式汇总对比
    发送方式 发送 TPS 发送结果反馈 可靠性
    同步发送 快 有 不丢失
    异步发送 快 有 不丢失
    单向发送 最快 无 可能丢失

什么是延迟消息?

Producer 将消息发送到消息队列broker服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费
使用场景一:通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息
使用场景二:消息生产和消费有时间窗口要求,定时关闭订单。比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条 延时消息。这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略

你用的队列是否支持顺序消息,是怎么实现顺序消息的?

什么是顺序消息:
消息的生产和消费顺序一致
全局顺序:topic下面全部消息都要有序(少用),性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景,并行度成为消息系统的瓶颈, 吞吐量不够
使用场景:在证券处理中,以人民币兑换美元为例子,在价格相同的情况下,先出价者优先处理,则可以通过全局顺序的方式按照 FIFO 的方式进行发布和消费
局部顺序:只要保证一组消息被顺序消费即可,性能要求高
使用场景:电商的订单创建,同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息、订单交易成功消息 都会按照先后顺序来发布和消费(阿里巴巴集团内部电商系统均使用局部顺序消息,既保证业务的顺序,同时又能保证业务的高性能)
下面是用RocketMQ举例(用kafka或rabbitmq类似)
一个topic下面有多个queue
顺序发布:对于指定的一个 Topic,客户端将按照一定的先后顺序发送消息
举例:订单的顺序流程是:创建、付款、物流、完成,订单号相同的消息会被先后发送到同一个队列中,
根据MessageQueueSelector里面自定义策略,根据同个业务id放置到同个queue里面,如订单号取模运算再放到selector中,同一个模的值都会投递到同一条queue

   public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
            //如果是订单号是字符串,则进行hash,得到一个hash值
          Long id = (Long) arg;
          long index = id % mqs.size();
          return mqs.get((int)index);
   }

顺序消费:对于指定的一个 Topic,按照一定的先后顺序接收消息,即先发送的消息一定会先被客户端接收到。
举例:消费端要在保证消费同个topic里的同个队列,不应该用MessageListenerConcurrently,
应该使用MessageListenerOrderly,自带单线程消费消息,不能在Consumer端再使用多线程去消费,消费端分配到的queue数量是固定的,集群消费会锁住当前正在消费的队列集合的消息,所以会保证顺序消费。
注意:
顺序消息暂不支持广播模式
顺序消息不支持异步发送方式,否则将无法严格保证顺序
不能再Consumer端再使用多线程去消费

你的业务系统有没做消息的重复消费处理,是怎么做的

幂等性:一个请求,不管重复来多少次,结果是不会改变的。
RabbitMQ、RocketMQ、Kafka等任何队列不保证消息不重复,如果业务需要消息不重复消费,则需要消费端处理业务消息要保持幂等性

  • 方式一:Redis的setNX() , 做消息id去重 java版本目前不支持设置过期时间
//Redis中操作,判断是否已经操作过 TODO
boolean flag =  jedis.setNX(key);
if(flag){
        //消费
}else{
        //忽略,重复消费
}
  • 方式二:redis的 Incr 原子操作:key自增,大于0 返回值大于0则说明消费过,(key可以是消息的md5取值, 或者如果消息id设计合理直接用id做key)
int num =  jedis.incr(key);
if(num == 1){
    //消费
}else{
    //忽略,重复消费
}
  • 方式三:数据库去重表
    设计一个去重表,某个字段使用Message的key做唯一索引,因为存在唯一索引,所以重复消费会失败
    CREATE TABLE message_record ( id int(11) unsigned NOT NULL AUTO_INCREMENT, key varchar(128) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY key (key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

消息队列如何保证消息的可靠性

消息可靠性传输,是非常重要,消息如果丢失,可能带来严重后果,一般从三个角度去分析

  1. producer端:
  • 不采用oneway发送,使用同步或者异步方式发送
  • 做好重试,但是重试的Message key必须唯一,投递的日志需要保存,关键字段,投递时间、投递状态、重试次数、请求体、响应体
  1. broker端:
  • 多主多从架构,需要多机房
  • 同步双写、异步刷盘 (同步刷盘则可靠性更高,但是性能差点,根据业务选择)
  • 机器断电重启:异步刷盘,消息丢失;同步刷盘消息不丢失
  • 硬件故障:可能存在丢失,看队列架构
  1. consumer端
  • 必须手动ack,消息队列一般都提供的ack机制,发送者为了保证消息肯定消费成功,只有消费者明确表示消费成功,队列才会认为消息消费成功,中途断电、抛出异常等都不会认为成功——即都会重新投递,每次在确保处理完这个消息之后,在代码里调用ack,告诉消息队列消费成功
  • 消费端务必做好幂等性处理
  • 消息消费务必保留日志,即消息的元数据和消息体,

消息堆积了10小时,有几千万条消息待处理,现在怎么办?

修复consumer, 然后慢慢消费?也需要几小时才可以消费完成,新的消息怎么办?
核心思想:紧急临时扩容,更快的速度去消费数据

  • 修复Consumer不消费问题,使其恢复正常消费,根据业务需要看是否要暂停
  • 临时topic队列扩容,并提高消费者能力,但是如果增加Consumer数量,但是堆积的topic里面的message queue数量固定,过多的consumer不能分配到message queue
  • 编写临时处理分发程序,从旧topic快速读取到临时新topic中,新topic的queue数量扩容多倍,然后再启动更多consumer进行在临时新的topic里消费
  • 直到堆积的消息处理完成,再还原到正常的机器数量

API网关

数据库扩展:分库分表、读写分离

分布式id

分布式接口幂等性

分布式限流

微服务

SpringCloud

设计模式

单例模式

使用场景:单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象(比如数据源,session工厂),使用单例模式可以提高系统性能

单例设计模式八种方式:
饿汉式(静态常量):基于类加载机制,避免了多线程的同步问题,但是也导致了没有达到懒加载效果,从而造成性能浪费
饿汉式(静态代码块):同上

懒汉式(线程不安全):起到了懒加载的目的,但是不能保证多个线程同时进入if判断,所以线程不安全,不要使用这种方式

if(instance==null){
	instance = new Singleton();
}

懒汉式(线程安全,同步方法):解决了线程安全问题,不过由于对静态方法加锁实际上是对整个类加锁,效率不高
懒汉式(同步代码块):不推荐使用
双重检查:延迟加载效率高,推荐使用
静态内部类:采用了类加载机制来保证实例时只有一个线程,JVM保证了线程的安全性
枚举:不仅避免多线程同步问题,而且还能防止反序列化重新创建新的对象

你可能感兴趣的:(java,面试,开发语言)