**
从本地上传到csdn后,有些图片不能显示,如果想要完整版,关注我就自动发了。
**
1、语言特性
Q、Java语言的优点
① 平台无关性( Java 虚拟机实现平台无关性);
② ⾯向对象(封装,继承,多态);
③ 安全的内存管理和访问机制,避免大部分内存泄漏和指针越界;
④ 完善的应用程序接口,支持第三方类库;
⑤ ⽀持多线程( C++ 语⾔没有内置的多线程机制);
⑥ 支持网络编程(提供了java.net包,以编程方式访问Web服务的功能);
Q、Java平台无关与JVM、JDK、JRE
· Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机,编译器(javac.exe)生成与计算机体系结构无关的十六进制值字节码(即扩展名为 .class的文件),由于字节码并不针对⼀种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运⾏。
· JDK: Java Development Kit,开发工具包。提供了编译运行 Java 程序的各种工具,包括编译器、JRE及常用类库,是 JAVA 核心。
· JRE: Java Runtime Environment,运行时环境,运行 Java 程序的必要环境,包括 JVM、核心类库等。
Q、什么是字节码?采用字节码的好处是什么?
Q、HotSpot
· HotSpot是较新的Java虚拟机,用来代替JIT(Just in Time),可以大大提高Java运行的性能。
· Java原先是把源代码编译为字节码在虚拟机执行,这样执行速度较慢。而HotSpot将常用的部分代码编译为本地代码(原生,native),这样显着提高了性能。
· HotSpot包括一个解释器和两个编译器(client 和 server,二选一的),解释与编译混合执行模式,默认启动解释执行。
(栈溢出)
Q、为什么说Java语言“编译与解释共存”?
Q、动态编译 与 静态编译
动态编译:在运行时编译Java源代码,然后再通过类加载器将编译好的类加载进JVM,这种在运行时编译代码的操作就叫做动态编译,是按需编译。
静态编译:编译时就把所有用到的Java代码全都编译成字节码,是一次性编译。
Q、成员变量与局部变量的区别?
成员变量 局部变量
语法形式 ①属于类;
②可以被访问修饰符和static修饰;
③可以被final修饰。 ①属于代码块或方法;
②不能被访问修饰符和static修饰;
③可以被final修饰。
存储方式 成员变量被static修饰,属于类,没有static修饰,属于实例,存放在堆中 局部变量存放在栈中
生存时间 成员变量属于对象,随着对象的创建而存在 局部变量随着方法的调用而生成,随着方法的调用结束而消亡
默认值 成员变量不赋初值,有默认值 局部变量不赋初值,无默认值
Q、Oracle JDK 和 OpenJDK 的对⽐
① OpenJDK 是⼀个参考模型并且是完全开源的,⽽ Oracle JDK 是 OpenJDK 的⼀个实现,并
不是完全开源的;
② Oracle JDK ⽐ OpenJDK 更稳定。
③ 在响应性和 JVM 性能⽅⾯,Oracle JDK 提供了更好的性能;
④ Oracle JDK 根据⼆进制代码许可协议获得许可,⽽ OpenJDK 根据 GPL v2 许可获得许可
Q、关键字、修饰符、运算符、数据类型
➢访问修饰符关键字
public protected private
➢修饰方法、类.属性和变量
static final super this native strictfp synchronized transient volatile
➢定义类、接口、抽象类和实现接口、继承类、实例化对象
class interface abstract implements extends new
➢包的关键字
import package
➢数据类型关键字
byte char boolean short int float long double void null true false
➢条件循环(流程控制)关键字
if else switch case default while for do
break continue return instanceof
➢异常处理关键字
try catch finally throw throws
➢保留字
const goto
➢关键字个数
51 + 2个保留字= 53个关键字
➢注意
java的关键字都是小写的! ! !
➢三目运算符
条件运算符也被称为三元运算符。该运算符有3个操作数,并且需要判断布尔表达式的值。该运算符主要是决定哪个值应该赋值给变量。
表达式:
variable x = (expression) ? value if true : value if false
三目运算符的规则是,先对逻辑表达式expression进行求值,如果逻辑表达式返回true,则返回第二个操作数的值,如果逻辑表达式返回false ,则返回第三个操作数值。
➢数据类型转换
由低到高转为自动类型转换——无信息丢失的数据类型转换
由高到低转换为强制类型转换——可能在转换时,出现精度丢失
char byte short int long float double object
➢拆箱、装箱
装箱:就是将基本数据类型转换为包装器类型
Number
Boolean
Character
拆箱:就是将包装器类型转换为基本数据类型
int (4字节) Integer
byte (1字节) Byte
short (2字节) Short
long (8字节) Long
float (4字节) Float
double (8字节) Double
char (2字节) Character
boolean (未定) Boolean
Q、值调用和引用调用
· 按值调用指方法接收调用者提供的值;
· 引用调用指方法接收调用者提供的变量地址。
Java 总是按值调用,方法得到的是所有参数值的副本;
按引用调用会改变数组的内容会影响实参;
Q、浅拷贝和深拷贝
· 浅拷贝:只复制当前对象的基本数据类型及引用变量,没有复制引用变量指向的实际对象。修改克隆对象的引用数据类型属性可能影响原对象,不安全。
· 深拷贝:完全拷贝基本数据类型和引用数据类型,修改克隆对象不会影响原对象,是安全的。
Q、反射
在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。
优点:运行时能够动态获取类的实例,提高灵活性;可与动态编译结合
缺点:使用反射性能较低(涉及动态解析),破坏了封装性(忽略权限检查)以及内部曝光(会访问私有字段和方法)。(Inspur)
Q、为什么需要反射?
反射赋予了JVM动态编译的能力。动态编译可以最大限度的体现Java的灵活性(多态)。否则类的元信息只能通过静态编译的形式实现(在编译期确定类型,绑定对象),而不能实现动态编译(在运行期确定类型,绑定对象)。也就是说在编译以后,程序在运行时的行为就是固定的了,如果要在运行时改变程序的行为,就需要动态编译,在Java中就需要反射机制。
情景一:有的类是我们在编写程序的时候无法使用new一个对象来实例化对象的。例如:
① 调用的是来自网络的二进制 .class文件,而没有其 .java代码;
② 注解 - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。
情景二:动态加载(可以最大限度的体现Java的灵活性,并降低类的耦合性:多态),有的类可以在用到时再动 态加载到JVM中,这样可以减少JVM的启动时间,同时更重要的是可以动态的加载需要的对象(多态)。例 如:
① 动态代理 - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反 射技术来实现了。
情景三:避免将程序写死到代码里
因为java代码是先通过编译器将.java文件编译成.class的二进制字节码文件,因此如果我们使用new Person() 来实例化对象person会出现的问题就是如果我们希望更换person的实例对象,就要在源代码种更改然后重 新编译再运行,但是如果我们将person的实例对象类名等信息编写在配置文件中,利用反射的 Class.forName(className)方法来实例化java对象(因为实例化java对象都是根据全限定名查找到JVM内 存中的class对象,并根据class对象中的类信息实例化得到java对象,因此xml文件中只要包含了全限定类 名就可以通过反射实例化java对象),那么我们就可以更改配置文件,无需重新编译。例如:
① 开发通用框架 - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比 如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不 同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。
Q、关于java.lang.Class类的理解
Java源代码通过javac.exe以后,会生成.class结尾的字节码文件,然后对此文件使用java.exe命令进行解释运行,相当于将某个字节码文件加载到内存中,这个过程称为类的加载。加载到内存中的类就成为运行时类,此运行时类就作为Class的一个实例。
Class实例对应着加载到内存中的一个运行时类。通过Class实例调用相关方法,可以调取对应运行时类的结构。
Q:如何获取反射中的Class对象?
Class.forName(“类的路径”);当知道该类的全路径名时,可以使用该方法获取 Class 类对象;
运行时类的属性 类名.class。这种方法只适合在编译前就知道操作的 Class;
运行时类的对象名.getClass()。
4.如果是基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象。
5.使用类的加载器:ClassLoader。
Q、Java反射API有几类?
反射 API 用来生成 JVM 中的类、接口或对象的信息。
· Class类:反射的核心类,可以获取类的属性,方法等信息。
· Field类:Java.lang.reflect 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
· Method类:Java.lang.reflect 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
· Constructor类:Java.lang.reflect 包中的类,表示类的构造方法。
· Annotation类:类的注解。
Q、反射机制的应用
第一种:JDBC 的数据库的连接
在JDBC 的操作中,如果要想进行数据库的连接,则必须按照以上的几步完成
第二种:Spring 框架的使用,最经典的就是xml的配置模式。
Spring 通过 XML 配置模式装载 Bean 的过程:
Q、Java的反射机制、类的调用
· 反射机制
· 类的调用:
① 在调用类中先进行被调用类实例化,然后通过实例化出的对象进行访问。
② 将该类中需要被调用的方法设置为静态(static),加了static后,就可以用类名直接调用。
调用格式为:类名.方法名(参数表)Method.test()。
Q、Class 类
在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。
· 获取 Class 对象:① 类名.class 。② 对象的 getClass方法。③ Class.forName(类的全限定名) 。
Q、注解
注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override
标识一个方法是重写方法。
元注解是自定义注解的注解,例如:
@Target :约束作用位置;
@Rentention :约束生命周期;
@Documented :表明这个注解应该被 javadoc 记录。
Q、泛型
泛型本质是参数化类型,解决不确定对象的具体类型的问题。泛型在定义处为 Object ,在编译时才确定具体的参数类型。
泛型的好处:
① 类型安全,放置什么类型,取出来就是什么类型,不存在 ClassCastException 类型转换异常。
② 便于代码重用,合并了同类型的代码。
③ 提升可读性,编码时就可以知道要处理的对象类型。
泛型擦除
泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。例如定义 List 或 List,在编译后都会变成 List 。
Q、JDK8 新特性
· lambda 表达式:允许把函数作为参数传递到方法,简化匿名内部类代码。
· 函数式接口:使用@FunctionalInterface标识,有且仅有一个抽象方法,可被隐式转换为lambda表达式。
· 方法引用:可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。
· 接口:接口可以定义 default 修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。
· 注解:引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。
· 类型推测:加强了类型推测机制,使代码更加简洁。
· Optional 类:处理空指针异常,提高代码可读性。
· Stream 类:引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach 遍历、count 统计个数、 filter 按条件过滤、 limit 取前 n 个元素、 skip 跳过前 n 个元素、 map 映射加工、 concat 合并 stream 流等。
· 日期:增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。
· JavaScript:提供了一个新的 JavaScript 引擎,允许在 JVM上运行特定 JavaScript 应用。
Q、异常
所有异常都是java.lang 包中 Throwable 的子类,有两个重要的子类为 Error 和 Exception。
Error 是程序无法处理的错误,没办法通过catch来进行捕获。例如OutOfMemoryError和 StackOverFlowError。
Exception 是程序本身可以处理的异常,可以通过 catch 来进行捕获。
· 非受检异常:运行时异常,包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。例如: NullPointException(空指针) 、IndexOutOfBoundsException(数组越界)、ClassCastException(类转换异常) 等。
· 受检异常:是Exception中除 RuntimeException 及其子类之外的异常,需要在代码中显式处理,否则会编译出错,报红。例如:IO相关的异常、ClassNotFoundException、SQLException等。
区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常。
Q、Java 和 C++的区别
· 都是⾯向对象的语⾔,都⽀持封装、继承和多态;
· Java 没有指针概念,程序内存更加安全;
· Java 是单继承的,C++⽀持多重继承;但是Java 接⼝可以多继承;
· Java 有⾃动内存管理机制,不需要⼿动释放⽆⽤内存;
· 在C语⾔中,字符串或字符数组最后都会有⼀个额外的字符‘\0’来表示结束。但是,Java语⾔中没有结束符这⼀概念。
Q、final、finally、finalize的区别?
final 用于修饰变量、方法和类。
· final 变量:被修饰的变量不可变,不可变分为 引用不可变 和 对象不可变 ,final 指的是 引用不可变 , final 修饰的变量必须初始化,通常称被修饰的变量为 常量 。
· final 方法:被修饰的方法不允许任何子类重写,子类可以使用该方法。
· final 类:被修饰的类不能被继承,所有方法不能被重写。
finally 作为异常处理的一部分,它只能在 try/catch 语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit (0) 可以阻断finally 执行。
finalize() 是在 java.lang.Object 里定义的方法,也就是说每一个对象都有这么个方法,这个方法在gc 启动,该对象被回收的时候被调用。
一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。
Q、break ,continue ,return 的区别及作用?
break: 结束当前的循环体,不再执行循环;
continue: 结束正在执行的循环,继续执行下次循环;
return: 结束当前的方法,不再执行下面的代码。
Q、线程池
https://blog.csdn.net/l18848956739/article/details/88636885
Q、常见5种运行时异常
https://blog.csdn.net/iblade/article/details/50523263
Q、Exception和Error的区别
Q、throw和throws的区别
①throws跟在方法声明后面,后面接的是异常类名;throw用在方法体内,后面接的是异常类对象名;
②throws后面可以跟多个异常类名,用逗号隔开;throw后面只能抛出一个异常对象名;
③throws表示抛出异常,由该方法的调用者来处理;throw表示抛出异常,由方法体内的语句来处理;
④throws表示有出现异常的可能性,未必会出现异常;throw则是抛出了异常,执行throw一定会出现异常。
Q、try-catch-finally如何使用?
Q、finally块中的代码一定会执行么?
不一定。
Q、final、finally、finalize的区别?
1、final用于修饰变量、方法和类。
①final修饰变量:被修饰的变量不可变,不可变分为 引用不可变 和 对象不可变 ,final 指的是 引用不可变 ,final修饰的变量必须初始化,通常称被修饰的变量为常量 。
②final修饰方法:被修饰的方法不允许任何子类重写,子类可以使用该方法。
③final修饰类:被修饰的类不能被继承,所有方法不能被重写。
2、finally 作为异常处理的一部分,它只能在 try/catch 语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit (0) 可以阻断finally 执行。
3、finalize() 是在 java.lang.Object 里定义的方法,也就是说每一个对象都有这么个方法,这个方法在gc 启动,该对象被回收的时候被调用。
一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。
补充:final修饰变量的情况
①final修饰成员变量,初始化的方法:定义时直接赋值;构造器中赋值(每个构造器中都得赋初始值,因为不知道会调用哪个构造);static静态代码块中赋值;
②final修饰静态成员变量,初始化的方法:定义的时候就必须赋初始值;
③final修饰引用类型变量,引用不可变,不变指的是引用不变,而不是这个引用指向的对象内容不变(即引用的地址不能变,至于地址存的内容没有要求)。
Q、如何使用try-with-resources代替try-catch-finally?
Q、异常使用的注意事项
Q、面向对象 && 面向过程
· 面向过程:让计算机有顺序的、按步骤做一件事,强调过程的先后顺序。性能高,但是软件复用和维护困难,模块之间耦合严重。
· 面向对象:把事物看成一个整体,使用类和对象来设计程序,不是为了完成某一个步骤,而是描述某个事物的一种行为,将步骤通过行为模块化。易复用、易维护、易扩展、低耦合;性能比面向过程低。
面相过程的思维方式,它更加注重这个事情的每一个步骤以及顺序。比较直接高效,需要做什么可以直接开始干。
面向对象的思维方式,它更加注重事情有哪些参与者,需求里面有哪些对象,这些对象各自需要做些什么事情。将其拆解成一个个模块和对象,这样会更易于维护和拓展。
Q、封装、继承、多态
· 封装:把客观事物封装成抽象的类(将代码及其处理的数据绑定(或包装)在一起),并且类可以把自己内部的信息(数据和方法)只让可信的类或者对象操作,对不可信的进行信息隐藏。
封装降低了代码耦合度,方便维护。
· 继承:一个类使用现有类的所有功能,并可以对这些功能进行扩展。
子类<->派生类,父类<->超类<->基类。
它提供代码可重用性,它用于实现运行时多态性。Java 只支持单继承。
· 多态:父类中定义的属性和方法被子类继承后,可以具有不同的属性或表现方式。使得同一个属性或方法在父类及其各个子类中表现出不同的行为。常用于将父类的引用指向子类的对象。这样引用既可以调用父类的方法,又能调用子类的方法。
抽象:提取现实世界中某事物的关键特性,为该事物构建模型的过程。这个抽象模型我们称之为类。对类进行实例化得到对象。
Q、Java实现多态有 3 个必要条件
· 继承:在多态中必须存在有继承关系的子类和父类。
· 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
· 向上转型:在多态中需要将父类引用指向子类对象,只有这样该引用才既可以调用父类的方法,又能调用子类的方法。Person per = new Student();
//向上转型 父类 父类引用 = new 子类对象
/*1. 父类有的方法,都可以调用,如果被子类重写了,则会调用子类的方法。
*2. 父类没有的方法,而子类存在,则不能调用。
3. 向上转型只对方法有影响,对属性没影响。属性不存在重写。/
对于父类中定义的方法,如果子类中重写了该方法,那么父类的引用将会调用子类中的这个方法,这就是动态连接。
Q、Java语言是如何实现多态的
· 本质上多态分两种:
1、编译时多态(又称静态多态)
重载(overload)就属于编译时多态,编译时多态在编译时就已经确定,运行时调用的是确定的方法。
2、运行时多态(又称动态多态)
通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直等到运行时才能确定。
(如子类继承父类后实现的方法)
Q、多态在什么时候使用
· 方法参数列表:代码量大了之后,一些逻辑可以提取出来成为公共的部分,剩余的部分就自然而然地成为多态了。
如:定义方法参数列表时定义为父类类型,这样就可以传递任意子类类型的对象,从而根据传入不同的子类对象来实现不同子类的方法。
· 向上转型:在多态中需要将父类的引用指向子类的对象,只有这样该引用才既可以调用父类的方法,又能调用子类的方法。Person per = new Student();
Q、对象实体和对象引用的区别?
对象实体在堆内存中;对象引用指向对象实例,对象引用存放在栈内存中。
一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。
Q、对象相等和引用相等
对象相等:比较的是内存中存放的内容是否相等;
引用相等:比较的是它们指向的内存地址是否相等。
Q、构造方法的特点?是否可以被override?
Q、接口的特点
接口是功能的集合,只描述所应该具备的方法,并没有具体实现,具体的实现由接口的实现类(相当于接口的子类)来完成。这样将功能的定义与实现分离,优化了程序设计。
1、接口中的成员不能有任何实现(“光说不做”,只是定义了一组未实现的成员);
2、为了多态。接口不能被实例化。也就是说,接口不能new(不能创建对象);
3、只要一个类继承了一个接口,这个类就必须实现这个接口中所有的成员;
4、接口中的成员不能加“访问修饰符”,接口中的成员访问修饰符默认为public,且不能修改;
5、接口中只能有方法、属性、索引器、事件,不能有“字段”和构造函数;
6、接口与接口之间可以继承,并且可以多继承;
7、接口并不能去继承一个类,而类可以继承接口 (接口只能继承于接口,而类既可以继承接口,也可以继承类);
8、一个类可以同时继承一个类并实现多个接口,如果一个子类同时继承了父类A,并实现了接口IA,那么语法上A必须写在IA的前面。
Q、Java创建对象的方式
① new、② 反射、③ 实现cloneable接口重写clone()方法、④ 通过序列化机制。
① 使用new关键字创建对象;
② 利用java的反射机制,通过反射获取类的有参构造函数,通过有参构造函数实例化对象(通过java.lang.Class或者java.lang.reflect.Constructor创建对象);
③ 实现Cloneable接口,然后重写Object.clone()方法创建对象;
④ 实现序列化serialiable接口,通过反序列化,ObjectInputStream的readObject()方法创建对象;
⑤ String str=“abc” 直接由JVM创建 或者使用 字符串操作符 “+”:String str1 = “a”+"bc"由JVM创建。
Q、重载和重写
· 重载:指方法名称相同,参数类型、个数不同。(编译时多态)
对编译器来说,方法名称和参数列表组成了一个唯一键,称为方法签名,JVM 通过方法签名决定调用哪种重载方法,属于静态绑定。
JVM 在重载方法中选择合适方法的顺序:
① 精确匹配。
② 基本数据类型自动转换成更大表示范围。
③ 自动拆箱与装箱。
④ 子类向上转型。
⑤ 可变参数。
· 重写:子类继承父类后,对父类中同名同参数的方法进行的覆盖操作。(运行时多态)
· “两同”即方法名相同、形参列表相同;(不能用返回值类型区分,因为编译器不知道调用哪个)
· “两小”指的是⼦类⽅法返回值类型应⽐⽗类⽅法返回值类型更⼩或相等,⼦类⽅法声明抛出的异常类型应⽐⽗类⽅法声明抛出的异常类型更小或相等;(如果子类抛出的异常>父类,那么父类无法接收,则无法实现多态)
· “一大”指的是⼦类⽅法的访问权限应⽐⽗类⽅法的访问权限更⼤或相等。
Q、构造器 Constructor 不能被 override
首先,构造器是不能被继承的,因为每个类的类名都不相同,而构造器名称与类名相同,所以根本谈不上继承。
又由于构造器不能继承,所以就不能被重写。
但是,在同一个类中,构造器是可以被重载的,所以可以看到⼀个类中有多个构造函数的情况。
Q、Object 类
· equals:检测对象是否相等,默认使用 == 比较对象引用,可以重写 equals 方法自定义比较规则。
对于任何非空引用 x,x.equals(null) 返回 false。
· hashCode:散列码是由对象导出的一个整型值,没有规律,每个对象都有默认散列码,值由对象地址得出。为了在集合中正确使用,一般需要同时重写equals 和 hashCode,要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同,因此 hashCode 是对象相等的必要不充分条件。
· toString:打印对象时默认的方法,如果没有重写打印的是表示对象值的一个字符串。
· clone:clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方法会抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现Cloneable 接口并指定访问修饰符为public。
· finalize:确定一个对象死亡至少要经过两次标记,如果对象在可达性分析后发现没有与 GC Roots 连接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行 finalize 方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。如果有必要执行,对象会被放置在 F Queue 队列,由一条低调度优先级的 Finalizer 线程去执行。
· getClass:返回包含对象信息的类对象。
· wait / notify / notifyAll:阻塞或唤醒持有该对象锁的线程。
Q、内部类
· 内部类:就是在一个类的内部嵌套定义的类,可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括 private 数据。
1.class OuterClass { // 外部类
2. // …
3. class NestedClass { // 嵌套类,或称为内部类
4. // …
5. }
6.}
· 静态内部类:可以使用 static 关键字定义,静态内部类不需要创建外部类来访问,可以直接访问它:
1.class OuterClass {
2. int x = 10;
3.
4. static class InnerClass {
5. int y = 5;
6. }
7.}
8.
9.public class MyMainClass {
10. public static void main(String[] args) {
11. OuterClass.InnerClass myInner = new OuterClass.InnerClass();
12. System.out.println(myInner.y);
13. }
14.}
输出:5
ArrayList 的 SubList 都是静态内部类。内部类中还可以定义内部类,如 ThreadLoacl 静态内部类ThreadLoaclMap 中定义了内部类 Entry。
· 成员内部类: 定义在外部类中的成员位置,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容,包括私有的。
· 局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,只在当前方法中有效,在局部内部类中可以访问外部类的所有成员。
如果方法中的成员与外部类中的成员同名,则可以使用 .this.
1.public class Test {
2. public void method() {
3. class Inner {
4. // 局部内部类
5. }
6. }
7.}
· 匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。
Q、Static关键字
“static”关键字表明一个成员变量或者是成员方法可以在没有所属类的实例变量的情况下被访问。
(实例变量:对象变量、类的成员变量(属性))
· 当创建了类的多个对象时,多个对象共享同一个静态变量,当修改其中一个静态变量时,会导致其他对象的静态变量发生同样改变。
· 静态变量随着类的加载而加载,静态变量的加载要早于对象的创建。静态变量在内存中只有一份。
· 在静态方法中不能访问非静态成员方法/成员变量(JVM载入的时候还没有被创建),但是在非静态成员方法中是可以访问静态成员方法/成员变量的。
· Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定
的。static方法跟类的任何实例都不相关,所以概念上不适用。
代码块执行顺序: 继承中代码块执行顺序:
静态代码块 父类—静态代码块
构造代码块 子类—静态代码块
构造函数 父类—构造代码块
普通代码块(方法实现、继承的) 父类—构造函数
子类—构造代码块
子类—构造函数
子类—普通代码块(方法实现、继承的)
作用:
方便在没有创建对象的情况下来进行调用(方法/变量)。被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
static修饰代码块
static关键字还有一个比较重要的作用就是用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来依次执行每个static块,并且只会执行一次。
static块可以优化程序性能,是因为它的特性:只会在类被初次加载的时候执行一次。
Q、静态成员变量
· 在Java中声明类的成员变量和方法时,可以使用static关键字把成员声明为静态成员。静态变量也叫类变量,非静态变量也叫实例变量;静态方法也叫类方法,非静态方法也叫实例方法。静态成员最主要的特点是它不属于任何一个类的对象,它不保存在任意一个对象的内存空间中,而是保存在类的公共区域中,所以任何一个对象都可以直接访问该类的静态成员,都能获得相同的数据值,修改时也在类的公共区域进行修改。
· 在Java中,静态成员变量的的初始化要求在静态语句块结束之前必须完成,即Java中静态成员变量的初始化时机有两个,在声明的同时进行初始化或在静态语句中初始化。
Q、静态方法为什么不能调用非静态成员?
Q、访问权限控制符
Q、接口和抽象类
抽象类
一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。
比如说,狗是具体对象,而动物则是抽象概念。
接口
接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义,而没有变量和方法的实现。
接口中的所有成员变量都默认是由public static final修饰的。
接口中的所有方法都默认是由public abstract修饰的。
相同点:
区别
· 接口主要为了实现多态性,实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系;
· ⼀个类只能继承⼀个类,但是可以实现多个接口;
· 接口没有构造方法,抽象类有构造方法;
· 接⼝中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
· 接口是顶级类,抽象类在接口下面的第二层,实现了部分接口。
什么时候使用接口?什么时候使用抽象类?
使用抽象类是为了代码复用,而使用接口是为了实现多态性。
抽象类适合用来定义某个领域的固有属性,接口适合用来定义某个领域的扩展功能。
· 当需要为一些类提供公共的实现代码时,应优先考虑抽象类 ;
· 当注重代码的扩展性和可维护性时,应当优先采用接口。
Q、抽象方法、抽象类
(1)只给出方法定义而不具体实现的方法被称为抽象方法,抽象方法是没有方法体的,在代码的表达上就是没有“{}”。使用 abstract 修饰符来表示抽象方法和抽象类。
(2)abstract修饰符表示所修饰的类没有完全实现,还不能实例化。
如果在类的方法声明中使用abstract修饰符,表明该方法是一个抽象方法,它需要在子类实现。
如果一个类包含抽象方法,则这个类也是抽象类,必须使用abstract修饰符,并且不能实例化。
(3)抽象类除了包含抽象方法外,还可以包含具体的变量和具体的方法。类即使不包含抽象方法,也可以被声明为抽象类,防止被实例化。
为什么使用抽象类?
由于父类方法的不确定性,所以用抽象类来把父类的设计的抽象,以至于它都没有任何具体的事例,所以没有必要在父类里写,可以精简代码。
abstract不能和那些关键字共存?
· abstract 和 static
被abstract修饰的方法没有方法体,被static修饰的可以用“类名.”调用,但是“类名.”调用抽象方法是没有意义的。
· abstract 和 final
被abstract修饰的方法强制子类重写,而被final修饰的不让子类重写,所以矛盾。
Q、abstract和final是什么关系?
互斥关系。
abstract定义的抽象类作为模板让子类继承;final定义的类不能被继承。
抽象方法定义通用功能让子类重写;final定义的方法子类不能重写。
Q、abstract和static是什么关系?
abstract修饰的方法是抽象方法,只有定义没有实现。
static修饰的静态方法属于类,可以直接使用”类名.方法名“调用,使用类名调用抽象方法是没有意义的。
Q、子类初始化顺序
① 父类静态代码块和静态变量。
② 子类静态代码块和静态变量。
③ 父类普通代码块和普通变量。
④ 父类构造方法。
⑤ 子类普通代码块和普通变量。
⑥ 子类构造方法。
Q、IO流
Input、Output,输入输出。数据输入到计算机内存的过程为输入,反之输出到外部存储的过程为输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在Java 中分为输入流和输出流,⽽根据数据的处理方式又分为字节流和字符流。
· InputStream / Reader : 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
· OutputStream / Writer : 所有输出流的基类,前者是字节输出流,后者是字符输出流。
字节流一般用于图像或其他文件,字符流一般用于文本文件。
字节输入流转字符输入流通过 InputStreamReader 实现,字节输出流转字符输出流通过 OutputStreamWriter 实现。
字符流与字节流的区别?
· 读写的时候字节流是按字节读写,字符流按字符读写。
· 字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位。字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。
· 在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。
· 只是读写文件,和文件内容无关时,一般选择字节流。
Q、字符串流,文件夹操作流怎么操作
StringReader与StringWriter
StringReader并不常用,因为通常情况下使用String更简单一些。但是在一些需要Reader作为参数的情况下,就需要将String读入到StringReader中来使用了。
文件流
FileInputStream:把一个文件作为输入源,从本地文件系统中读取字节数据,实现对文件的读取操作。
FileReader:与FileInputStream对应,从文件系统中读取字符序列。
其他字节流
ByteArrayInputStream:把内存中的一个缓冲区作为输入源,从内存数组中读取数据字节。
ObjectInputStream:对以前使用过ObjectOuputStream写入的基本数据和对象进行反序列化,用于恢复那些以前序列化的对象,注意这个对象所属的类必须实现Serializable接口。
PipeInputStream:实现了管道的概念,从线程管道中读取线程字节。主要在线程中使用,用于两个线程间通信。
SequenceInputStream:表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。
System.in:从用户控制台读取数据字节,在System类中,in是 InputStream 类的静态导入。
其他字符流
CharArrayReader:与ByteArrayInputStream 对应,从字符数组中读取数据。
PipedReader:与PipedInputStream 对应,从线程管道中读取字符序列。
StringReader:从字符串中读取字符序列。
Q、子类的访问权限
访问权限修饰符:public、protected、默认修饰符、private。
Q、基本数据类型
基本类型boolean默认值为false
包装类Boolean默认值为null
Q、常用Math方法
Math.round是在参数的基础上加0.5然后向下取整,方法内部调用了floor方法;
Q、java中都有哪些引用类型?
· 强引用:当内存不足JVM开始垃圾回收时,不会被回收;
· 软引用:有用但不是必须的对象,内存充足时不回收,内存不足时会被回收;
· 弱引用:有用但不是必须的对象,只要垃圾回收机制一运行,不管内存充足不充足直接回收;
· 虚引用:无法通过虚引用获得对象,在任何时候都可能会被垃圾回收器回收,用PhantomReference实现虚引 用。
Q、什么是包装类?
Java官方提供的一组类,这组类的作用是将基本数据类型的数据封装成引用类型。
包装类是Java提供的一组类,专门用来创建8种基本数据类型对应的对象,一共有8个包装类,存放在java.lang包中,基本数据类型对应的包装类,Byte、Integer、Short、Long、Float、Double、Boolean、Character。
Q、自动装箱/拆箱
· 自动装箱: 将基本数据类型包装为一个包装类对象,例如将 int 元素添加到一个泛型为 Integer 的集合。
· 自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
为什么要有包装类?
Java中的基本数据类型没有方法和属性,但是在特定场景下,必须要利用对象的相关属性,而包装类就是为了让基本数据类型拥有方法和属性,实现对象化交互。
调用的方法
① 装箱:
② 拆箱:
比较两个包装类数值要用 equals 。
Q、基本类型和包装类的区别
Q、包装类的缓存机制
★注意:需要注意Integer包装类的范围是[-128,127],超过这个范围会在堆中创建对象。
Q、自动装箱和自动拆箱
Q、数据结构中数组的分类、数据类型
· 数组分类
1.数组是一种线性表数据结构,用一组连续的内存空间来存储一组具有相同类型的数据。
2.根据维度分为:一维数组和多维数组;根据祖先类可以分为:基本类型数组类(父类为Object类)和引用类型数组类(其所有的祖先类除了Object类外,也包括如A是B的祖先类,A[]也是B[]的祖先类)。
· 数据类型
Q、equals() 和 == 的区别
== 对于基本类型和引⽤类型的作⽤效果是不同的:
对于基本数据类型来说, == 比较的是值。
对于引用数据类型来说, == 比较的是对象的内存地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。 equals() ⽅法存在于 Object 类中,⽽ Object 类是所有类的直接或间接⽗类,因此所有的类都有 equals() ⽅法。
equals() ⽅法存在两种使⽤情况:
· 类没有重写 equals() ⽅法 :通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象(地址),使⽤的默认是 Object 类 equals() ⽅法。
· 类重写了 equals() ⽅法:⼀般都重写 equals() 方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
String 中的 equals 方法是被重写过的,比较的是对象的值。
java语言规范要求equals方法具有以下特性:
· 自反性。对于任意不为null的引用值x,x.equals(x)一定是true。
· 对称性。对于任意不为null的引用值x和y,当且仅当x.equals(y)是true时,y.equals(x)也是true。
· 传递性。对于任意不为null的引用值x、y和z,如果x.equals(y)是true,同时y.equals(z)是true,那
么x.equals(z)一定是true。
· 一致性。对于任意不为null的引用值x和y,如果用于equals比较的对象信息没有被修改的话,多次
调用时x.equals(y)要么一致地返回true要么一致地返回false。
Q、String 不可变性
String 类中使⽤ final 关键字修饰字符数组来保存字符串。
对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象。只是修改 String 变量引用的对象,没有修改原String 对象的内容。
Q、字符串拼接
① 直接用 + ,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 + 拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。
② 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 + 。
③ 使用 StringBuilder 或 StringBuffer,两者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 确定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用synchronized 保证线程安全。
Q、String、StringBuffer、StringBuilder的区别?
Q、String为什么是不可变的?
Q、字符串拼接用“+”还是StringBuilder?
建议使用StringBuilder,因为使用“+”实际上还是调用StringBuilder来拼接字符串的。
Q、String#equals() 和 Object#equals() 有何区别?
Q、字符串常量池
Q、intern方法有什么作用?
Q、成员变量和局部变量
成员变量:在类体的变量部分中定义的变量,也称为属性。
1、相同点:
1.1 定义变量的格式:数据类型 变量名 = 变量值;
1.2 都是先声明,再使用;
1.3 都有其对应的作用域。
2、不同点
2.1 在类中声明的位置不同
属性:直接定义在类的一对{}内;
局部变量:定义在方法内、方法形参、代码块内、构造器形参、构造器内部等。
2.2 关于权限修饰符的不同
属性:可以使用常用的private、public、默认、protected权限修饰符;
局部变量:不可以使用权限修饰符。
2.3 默认初始化值的不同
属性:根据类型的不同,都有默认初始化值:整型0、浮点型0.0、字符型0、布尔型false、引用类型null;
局部变量:没有默认初始化值,必须初始化。
2.4 存放在内存中的位置不同
属性:堆(非static的在堆,static的在方法区);
局部变量:栈。
Q、日期时间API
· JDK1.8之前:
① System静态方法 ② Date类 ③ Calendar类 ④ SimpleDateFormat类
· JDK1.8之后:
① LocalDate、LocalTime、LocalDateTime ② Instance ③ DateTimeFormatter ④其他类
Q、java 对象类型与基本数据类型传参的比较
· 基本数据类型,传递的是值(值拷贝),形参的任何改变不影响实参。比如main函数下定义a和b是实参,而在某个方法下定义的a和b是形参。
· 引用类型的方法传参传的是地址,方法中指向地址值改变也会影响主方法,即形参的改变会影响实参,因为两者都指向堆中的同一个地址。
Q、内存泄漏的场景
OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,意思就是说,当JVM因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。
1、静态集合类
如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
2、资源未关闭或未释放
如数据库连接、IO流等。在这些连接之后没有使用close方法来释放连接或者关闭流操作等,将会造成大量的对象无法被回收,从而引起内存泄漏。
3、变量不合理的作用域
一般而言,一个变量的定义的作用范围 > 其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
Q、空指针问题
当一个变量的值为 null 时,在 Java 里面表示一个不存在的空对象,没有实际内容,没有给它分配内存,没有进行初始化操作,这时候,如果调用这个对象的方法或者变量,就会出现空指针异常。
空指针属于运行时异常 RuntimeException 的子类,它不是捕获型的,只有在程序运行时才可能报出来。
可能出现的场景:
1、字符串常量未初始化,equals比较时导致空指针异常;
2、接口类型的对象(list等)没有使用具体的类进行初始化,调用本身方法时导致空指针异常;
3、对象为空,但未进行判空导致空指针异常;(User user = null; user.getName(); )
4、Hashtable 不允许null值,当put为null时,导致空指针。
Q、枚举
枚举 Enum,是一种有确定值区间的数据类型,本质上就是一个类,具有简洁、安全、方便等特点。
枚举的值被约束到一个特定的范围内,只能从这个范围以内取值。
为什么要有枚举?
因为描述某些对象的属性时,该属性的值不能随便定义,必须在某个特定的区间内取值,如星期、月份等。
出于对数据安全的考虑,类似这种有特定取值范围的数据我们就可以使用枚举来描述。
枚举指由一组常量组成的类型,指定一个取值区间,我们只能从区间中取值。
Q、日期类
Q1、java.util.Date
Date表示当前的系统时间
SimpleDateFormat转换时间格式(“yyyy-MM-dd hh:mm:ss”)(M —— 月份,m —— 分钟,H —— 24小时制,h —— 12小时制)
Q2、java.util.Calendar
Calendar用来完成日期数据的逻辑运算
运算思路:(op + com + t)
①将日期数据传给Calendar (Calendar 提供了很多静态常量,专门用来记录日期数据)
②调用相关方法进行运算
Java 集合,也叫作容器,主要是由两大接口派生而来:
· Collection 接口:主要用于存放单⼀元素;
· Map 接口:主要用于存放键值对。
对于 Collection 接口,下面又有三个主要的子接口: List、Set 和 Queue。
Q、HashMap和HashSet的区别?
Q、Collection和Collections有什么区别?
· java.util.Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法。List,Set,Queue接口都继承Collection,主要用于存放单⼀元素。
· java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态方法(对集合的查找、排序、反转、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
Q、集合 Arraylist和Linkedlist
· ArrayList和LinkedList都是不同步的,也就是不保证线程安全;
· ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构;
· ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响;LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响。
· ArrayList 支持根据元素的序号快速获取元素(get(int index)方法),LinkedList不支持。
· ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空间;而 LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
Q、HashMap、HashSet的区别和特点
· HashMap 实现了Map接口;HashSet 实现了Set接口;
· HashMap 存储键值对;HashSet 仅存储对象;
· HashMap 使用 put() 方法添加元素;HashSet 使用 add() 方法添加元素;
· HashMap 使用 键(key) 来计算hashcode值;HashSet 使用对象来计算hashcode值(equals());
· HashMap 查找比较快,因为使用唯一的键来获取对象;HashSet 查找较慢;
· HashMap不能包含重复key,但可以包含重复的value;HashSet 不允许集合中有重复的值;
· HashMap继承自AbstractMap,HashSet继承自AbstractSet。
Q、HashMap 和 Hashtable 的区别
· HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过 synchronized 修饰。
· 因为线程安全的问题, HashMap 效率高⼀点。(Hashtable 基本被淘汰了)
· HashMap 允许null值;Hashtable 不允许null值;两者不能包含重复key,但可以包含重复的value。
· HashMap使用 键(key) 来计算hashcode值;Hashtable直接使用对象的hashcode;
· HashMap初始容量为16,填充因子默认都是0.75,扩容时是2n;Hashtable初始容量为11,扩容时是2n+1。
· 底层都是以数组+链表的形式来存储,但是在JDK1.8后,HashMap当链表长度大于阈值(默认为 8)时,将链表转化为红黑树。
· HashMap继承自AbstractMap,Hashtable继承自Dictionary。
Q1、HashMap 和 TreeMap 区别
HashMap 和 TreeMap 都继承自 AbstractMap。TreeMap 它还实现了NavigableMap 接口和 SortedMap 接口,使TreeMap 有了对集合内元素的搜索和根据键排序的能力。
Q、ArrayList和Vector的区别
① ArrayList是基于数组的实现,是非线程安全的,效率高,所有的方法都没有synchronized修饰。
② Vector是线程安全的,效率低,实现线程安全是直接通过synchroized修饰方法来完成的。
Q、Vector、ArrayList、LinkedList的区别
1、Vector、ArrayList都是以类似数组的形式存储在内存中,LinkedList则以链表的形式进行存储。
2、List中的元素有序、允许有重复的元素,Set中的元素无序、不允许有重复元素。
3、Vector线程同步,ArrayList、LinkedList线程不同步。
4、LinkedList适合指定位置插入、删除操作,不适合查找;ArrayList、Vector适合查找,不适合指定位置的插入、删除操作。
5、ArrayList在元素填满容器时会自动扩充容器大小的50%,而Vector则是100%,因此ArrayList更节省空间。
Q、ConcurrentHashMap的加锁
· HashMap不支持并发操作,没有同步方法。(HashMap的主干是一个Entry数组,Entry
· ConcurrentHashMap支持并发操作,通过继承(JDK1.7重入锁)ReentrantLock / (JDK1.8内置锁) CAS和synchronized 来进行加锁(分段锁),每次需要加锁的操作锁住的是一个 segment(HashEntry数组+链表),这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
· JDK1.8之前HashMap的结构为数组+链表,之后HashMap的结构为数组+链表+红黑树;
· JDK1.8之前ConcurrentHashMap的结构为segment数组+数组+链表,之后为Node数组+链表+红黑树。
· ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表.
· Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据,
Node就是一个链表,但是只允许查,不允许改
· Re-Entrant-Lock:即表示可重新反复进入的锁,但仅限于当前线程;
Q、Set集合
Q、Queue
Q、Map接口
(1)HashMap 和 HashTable 的区别
HashMap HashTable
是否线程安全 否(ConcurrentHashMap 线程安全) 是(synchronized修饰)
效率 高 低(基本被淘汰)
对Null key 和 Null value 的支持 可以存储null的key和value,但null作为键只能有一个,null作为值可以有多个 不允许存储null的key和value,否则会抛出 NullPointerException
初始容量大小 不指定默认为16;指定大小时扩容为2的幂次方大小 不指定默认为11;指定大小时直接使用给定大小
扩容容量大小 变为原来的2倍 变为原来的2n+1
底层数据结构 1.8之前,数组+链表
1.8之后,数组+链表+红黑树
Q、HashSet 如何检查重复?
补充:什么是HashCode?
将对象的内部信息(内存地址、属性值等),通过某种特定规则转换成一个散列值,就是该对象的hashCode。
两个不同对象的hashCode值可能相等。
hashCode值不相等的两个对象一定不是一个对象。
集合在判断两个对象是否相等的时候,会①先比较他们的hashCode值,如果hashCode不相等,则认为不是同一个对象,可以添加。
如果②hashCode值相等,还不能认为两个对象是相等的,需要通过equals方法进行进一步的判断,equals相等,则两个对象相等,否则两个对象不相等。
hashCode相等,未必相等;hashCode不相等,则一定不相等。
Q、HashMap 的底层实现(重要)
参见HashMap源码&底层数据结构分析
Q、HashMap 的长度为什么是2的幂次方?
Q、集合使用注意事项
集合判空 判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式。
集合转Map 在使用
java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。
集合遍历 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
集合去重 可以利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List的 contains() 进行遍历去重或者判断包含操作。
集合转数组 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
数组转集合 使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。(转完不支持修改操作)
Q、内存解析
栈:局部变量;
堆:new出来的结构:对象、数组;
方法区:常量池(常量)、静态域(静态变量)。
其实String类型是放在字符串常量池中的。
Q、JVM内存结构、JVM组成
定义:
JVM包括 类加载子系统、堆、方法区、栈、本地方法栈、程序计数器、直接内存、垃圾回收器、执行引擎。
· 类加载子系统:类加载子系统负责加载类信息,加载的类信息存放于方法区中。
· 直接内存:直接内存是在Java堆外的、直接向系统申请的内存空间。访问直接内存的速度会优于Java堆。出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。
· 垃圾回收器:垃圾回收器可以对堆、方法区、直接内存进行回收。
· 执行引擎:执行引擎负责执行虚拟机的字节码,虚拟机 会使用即时编译技术将方法编译成机器码后再执行。
运行时数据区
包括:堆、方法区、栈、本地方法栈、程序计数器。
· 堆:(new出来的结构:对象、数组)java堆是虚拟机所管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建。几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
堆既可以被实现成固定大小,也可以是可扩展的,可通过 -Xms 和 -Xmx 设置堆的最小和最大容量,当前主流 JVM 都按照可扩展实现。如果堆没有内存完成实例分配也无法扩展,抛出OutOfMemoryError。
· 方法区:可以认为是堆的一部分,用于存储已被虚拟机加载的信息,常量、静态变量、即时编译器编译后的代码。
在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分,1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;
· Java虚拟机栈:线程私有的,用于存储局部变量表、操作数、动态链接和方法返回等信息;
每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和方法出口等信息。每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程;
有两类异常:① 当线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError。
② 如果 JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出 OutOfMemoryError(HotSpot不可动态扩展,不存在此问题)。
· 本地方法栈:线程私有的,保存的是本地方法的信息,当一个jvm创建的线程调用本地方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用指定的本地方法;
本地方法栈在栈深度异常和栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError。
· 程序计数器:是一块较小的内存空间,存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
Q、Java内存分配方式
Q、JVM中对象及常量、局部变量、全局变量的存储位置
· 局部变量
基本数据类型:变量名和变量值存储在 方法栈 中。
引用数据类型:变量值存储在 方法栈 中(存储的是堆中对象的内存地址),所指向的对象是存储在堆内存中(如new出来的对象)。
· 全局变量
基本数据类型:变量名和变量值存储在 堆 内存中。
引用数据类型:变量名存储的是所引用对象的内存地址,变量名和变量值存储在 堆 内存中。
Q、JVM内存模型
Q1、定义:
Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。
这一组规则被称为 Happens-Before,JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系:
· 单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作;
· 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作;
· volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作;
· 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作;
· 线程join规则:一个线程内的所有动作happens-before任意其他线程在该线程join()成功返回之前;
· 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C。
Q2、JVM自带三种类加载器:
启动类加载器。
扩展类加载器。
应用程序类加载器
Q、JVM生命周期
Q1、诞生
当一个程序启动,伴随的就是一个JVM实例的诞生,当这个程序关闭退出,这个JVM实例就随之消亡。如果在同一台机器上运行多个程序,将诞生相应数量的JVM实例,每个程序都有一个与之对应的JVM实例供其运行。
Q2、运行
main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。java程序的初始线程只就是运行main()的线程,这个线程是非守护线程,只要还有任何非守护线程还在运行,那么JVM就存活着。
Q3、消亡
当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。
Q、类加载的执行过程
一个java类的完整的生命周期会经历加载、连接、初始化、使用和卸载五个阶段。
一个Java文件从编码完成到最终执行,一般主要包括两个过程:编译 和 运行;
编译:即把写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
运行:则是把编译生成的.class文件交给Java虚拟机(JVM)执行。而所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
类加载的过程主要分为三个部分:加载、连接、初始化;其中连接又可以细分为三个小部分:验证、准备、解析;
加载:简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
这里有两个重点:
· 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络, 以及动态代理实时编译
· 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
验证:
· 主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
· 包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
· 对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不 合理的重载?
· 对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
· 对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
准备
· 主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
· 特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
· 比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456
解析:将常量池内的符号引用替换为直接引用的过程。
· 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
· 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
初始化
· 这个阶段主要是对类变量初始化,是执行类构造器的过程。
· 换句话说,只对static修饰的变量或语句进行初始化。
· 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
· 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
Q、类加载器详解
Q1、Java中的类加载器
JVM类加载器分为两类:JVM自带的类加载器和自定义类加载器。
Q2、为什么要自定义类加载器?
① 隔离加载类;② 修改类加载的方式;③ 扩展加载源;④ 防止源码泄漏。
· 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密, 然后再通过实现自己的自定义类加载器进行解密,最后再加载。
· 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源 进 行加载。
Q3、如何实现自定义类加载器?
继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器。
Q、怎么判断对象是否可以被回收?
1、引用计数算法(已被淘汰的算法):如果被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。原理简单,效率高;
目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象之间相互循环引用的问题。尽管该算法执行效率很高。
2、可达性分析算法:主流语言的内存管理都使用可达性分析判断对象是否存活。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
Q、垃圾回收算法 — 老年代、新生代
标记-清除:
采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
标记-复制:
为了解决内存碎片问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这
块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于进行新生代。
实现简单、运行高效,解决了内存碎片问题。 代价是可用内存缩小为原来的一半,浪费空间。
标记-整理:
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,然后清理掉边界以外的内存。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,而且移动必须全程暂停用户线程,因此成本更高,但是却解决了内存碎片的问题。
分代收集算法:
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。
老年代的特点是每次垃圾回收时只有少量对象需要被回收;
而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,
那么就可以根据不同代的特点采取最适合的收集算法。
Q、JVM老年代存什么对象
存放生命周期较长的对象,所以以下对象会被存放在老年代中:
1、长期存活的对象。
2、Survivor区放不下的对象。
3、新生成的大对象(字符串与数组),即超过了设定的值的对象,直接在老年代中分配。
一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到老年代中。
Q、垃圾回收器
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代
· Serial收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级 别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
· ParNew收集器(停止-复制算法):新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环 境下有着比Serial更好的表现。
· Parallel Scavenge收集器(停止-复制算法):并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为 99%,吞吐量=用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。 是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4 来指定线程数。
老年代
· Serial Old收集器(标记-整理算法):老年代单线程收集器,Serial收集器的老年代版本。
· Parallel Old收集器(停止-复制算法):Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
· CMS(Concurrent Mark Sweep)收集器(标记-清理算法):高并发、低停顿,追求最短GC回收停顿时间, cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
如果对象在初生代内存活超过一定次数之后,就可以晋升到老年代中,而CMS垃圾收集器就是专门用来对老 年代做收集。
区别:
· 新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;
· 老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
Q、JVM调优工具
JConsole:jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。
JProfiler:商业软件,需要付费。功能强大。
Visual VM:JDK自带,功能强大,与JProfiler类似。推荐。
Q、线程
Q1、线程的生命周期
· new:新建状态,线程被创建且未启动,此时还未调用 start 方法。
· runnable:Java 将操作系统中的就绪和运行两种状态统称为 RUNNABLE,此时线程有可能在等待时间片,也有可能在执行。
· blocked:阻塞状态,可能由于锁被其他线程占用、调用了 sleep 或 join 方法、执行了 wait 方法等。
· waiting:等待状态,该状态线程不会被分配 CPU 时间片,需要其他线程通知或中断。可能由于调用了无参的 wait 和 join 方法。
· time_waiting:限期等待状态,可以在指定时间内自行返回。导可能由于调用了带参的 wait 和join 方法。
· terminated:终止状态,表示当前线程已执行完毕或异常退出。
Q2、线程的创建方式
① 继承 Thread 类并重写 run 方法。实现简单,但不符合里氏替换原则,不可以继承其他类。
② 实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
③ 实现 Callable 接口并重写 call 方法。可以获取线程执行结果的返回值,并且可以抛出异常。
Q2、守护线程和非守护线程
· 守护线程是一种支持型线程,是运行在后台的一种特殊进程。它具有陪伴的含义。当进程中不存在非守护线程了,则守护线程自动销毁。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
· 非守护线程,就是程序中前端运行的线程。
· 可以通过 setDaemon(true) 将线程设置为守护线程,但必须在线程启动前设置。
· 守护线程被用于完成支持性工作,但在 JVM 退出时守护线程中的 finally 块不一定执行,因为 JVM 中没有非守护线程时需要立即退出,所有守护线程都将立即终止,不能靠在守护线程使用 finally 确保关闭资源。
· 守护线程拥有自动结束自己生命周期的特性,非守护线程却没有。
Q、双亲委派模型
定义:
当需要加载一个类的时候,子类加载器并不会马上去加载,而是依次去请求父类加载器加载,一直往上请求到最高类加载器:启动类加载器。当启动类加载器加载不了的时候,依次往下让子类加载器进行加载。当达到最底下的时候,如果还是加载不到该类,就会出现ClassNotFound的情况。
好处:
保证了程序的安全性、稳定性,可以避免类的重复加载。例子:比如我们重新写了一个String类,加载的时候并不会去加载到我们自己写的String类,因为当请求上到最高层的时候,启动类加载器发现自己能够加载String类,因此就不会加载到我们自己写的String类了。
注:
自定义类加载器,需要继承java.lang.ClassLoader。
Q、volatile、final 和 synchronized
旨在帮助程序员向编译器描述程序的并发要求,其中:
volatile: 保证可见性和有序性;
synchronized: 保证可见性和有序性; 通过管程(Monitor)保证一组动作的原子性;
final: 通过禁止在构造函数初始化和给 final 字段赋值这两个动作的重排序,保证可见性(如果this 引用逃逸就不好说可见性了)
Q、synchronized关键字
1、修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁。
2、修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法, 是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
3、修饰代码块:指定加锁对象,对给定对象/类加锁。 synchronized(this/object) 表示进入同步代码库前要获得 给定对象的锁。 synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁。
总结:
①synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是给 Class 类上锁。
②synchronized 关键字加到实例方法上是给 对象实例上锁。
注意:静态方法调用 —— 既可以使用对象调用也可以使用类调用;
实例方法调用 —— 只可以使用对象调用;
在静态方法中不可以使用 this 关键字。this 指的是当前对象。
补充:snchronized底层实现原理
Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成。每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码,当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权,过程:①如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
②如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
③如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
synchronized是可以通过反汇编指令javap命令,查看相应的字节码文件。
Q、volatile
volitile关键字的作用是可以 使内存中的数据对象线程可见。主内存对线程是不可见的,添加 volitile 关键字后,主内存对线程可见。(每次读取数据都是直接从主内存中读取,而不是从本地内存处读取)
补充:Java内存模型
在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile ,这就 指示 JVM,这个变量是共享且不稳定的, 每次使用它都到主存中进行读取。 所以, volatile 关键字除了①防止 JVM 的指令重排 ,还有一个重要的作用就是②保证变量的可见性。
Q、synchronized vs volatile 的区别
· synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
· volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 关键字要好。但是 volatile 关键字只能用于变量,而 synchronized 关键字可以修饰方法以及代码块。
volatile 关键字能保证数据的可见性(√),但不能保证数据的原子性(×)。synchronized 关键字两者都能保证。
volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
即 synchronized 是线程安全的,而 volatile 是非线程安全的。
Q、ReentrantLock与 synchrnized 的区别
ReentrantLock是一个类,synchronized是一个关键字;
ReentrantLock是JDK实现,synchronized是JVM实现;
ReentrantLock需要手动释放,synchronized可以自动释放锁。
ReentrantLock是 Lock 接口的实现类。
Q、synchronized 和 Lock 的区别
synchronized 自动上锁,自动释放锁;Lock 手动上锁,手动释放锁。
synchronized 无法判断是否获取到了锁;Lock 可以判断是否拿到了锁。
synchronized 拿不到锁就会一直等待;Lock 不一定会一直等待。
synchronized 是 java 关键字;Lock 是 java 接口。
synchronized 是非公平锁;Lock 可以设置是否为公平锁。
Q、内存溢出
内存溢出OutOfMemory:指程序在申请内存时,没有足够的内存空间供其使用。
内存泄露Memory Leak:指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。
Q、内存泄漏的场景
00M,全称“Out Of Memory”,翻译成中文就是“内存用完了”,意思就是说,当JVM因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。
1、静态集合类
如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
2、资源未关闭或未释放
如数据库连接、IO流等。在这些连接之后没有使用close方法来释放连接或者关闭流操作等,将会造成大量的对象无法被回收,从而引起内存泄漏。
3、变量不合理的作用域
一般而言,一个变量的定义的作用范围 > 其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
Q、空指针问题
当一个变量的值为null时,在Java里面表示一个不存在的空对象,没有实际内容,没有给它分配内存,没有进行初始化操作,这时候,如果调用这个对象的方法或者变量,就会出现空指针异常。
空指针属于运行时异常RuntimeException的子类,它不是捕获型的,只有在程序运行时才可能报出来。
可能出现的场景:
①字符串常量未初始化,equals比较时导致空指针异常;
②接口类型的对象(list等)没有使用具体的类进行初始化,调用本身方法时导致空指针异常;
③对象为空,但未进行判空导致空指针异常;(User user = null; user.getName(); )
④Hashtable不允许null值,当put为null时,导致空指针。
Q、堆溢出
· 堆用于存储对象实例,不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及堆最大容量后就会 OOM,例如在 while 死循环中一直 new 创建实例。
· 堆 OOM 是实际应用中最常见的 OOM,处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存溢出还是内存泄漏。
· 如果是内存泄漏,通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联才导致无法回收,一般可以准确定位到产生内存泄漏代码的具体位置。
· 如果不是内存泄漏,即内存中对象都必须存活,应当检查 JVM 堆参数,与机器内存相比是否还有向上调整的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
Q、栈溢出
由于 HotSpot 不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss 参数来设定,存在两种异常:
· StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError;
例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。
· OutOfMemoryError: 如果JVM栈可以动态扩展,当扩展无法申请到足够内存时会抛出OutOfMemoryError。HotSpot 不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OOM,否则在线程运行时是不会因为扩展而导致溢出的。
Q、运行时常量池溢出
String 的 intern 方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此 String 对象的字符串,则返回池中这个字符串的 String 对象的引用,否则将此 String 对象包含的字符串添加到常量池并返回此 String 对象的引用。
在 JDK6 及之前常量池分配在永久代,因此可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制永久代大小,间接限制常量池。在 while 死循环中调用 intern 方法导致运行时常量池溢出。在 JDK7 后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中。
Q、方法区溢出
方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用 JDK 反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib 这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。
JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施,例如 - XX:MetaspaceSize 指定元空间初始大小,达到该值会触发 GC 进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。
Q、常见的排序算法
P1:排序分类
· 排序算法可以分为内部排序和外部排序,在内存中进行的排序称为内部排序,当要排序的数据量很大时无法全部拷贝到内存,这时需要使用外存进行排序,这种排序称为外部排序。
· 内部排序:比较排序、非比较排序;
比较排序:插入排序、选择排序、交换排序和归并排序;
非比较排序:计数排序、基数排序和桶排序。
· 其中插入排序:直接插入排序和希尔排序;
选择排序:直接选择排序、堆排序;
交换排序:冒泡排序、快速排序。
P2:直接插入排序(插入到已排好序的一组数据上)
· 每一趟将一个待排序记录按其关键字的大小插入到已排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
· 是一种稳定的排序,时间复杂度 O(n²),当元素基本有序时最好时间复杂度为 O(n),空间复杂度为 O(1)。
适用场景:待排序记录较少或基本有序的情况。
1.public void insertionSort(int[] nums) {
2. for (int i = 1; i < nums.length; i++) {
3. int insertNum = nums[i];
4. int insertIndex;
5. for (insertIndex = i - 1; insertIndex >= 0 && nums[insertIndex] > insertNum; insertIndex–) {
6. nums[insertIndex + 1] = nums[insertIndex];
7. }
8. nums[insertIndex + 1] = insertNum;
9. }
10.}
优化:直接插入没有利用到要插入的序列已有序的特点,插入第 i 个元素时可以通过二分查找找到插入位置 insertIndex,再把 i~insertIndex 之间的所有元素后移一位,把第 i 个元素放在插入位置上。
1.public void binaryInsertionSort(int[] nums) {
2. for (int i = 1; i < nums.length; i++) {
3. int insertNum = nums[i];
4. int insertIndex = -1;
5. int start = 0;
6. int end = i - 1;
7. while (start <= end) {
8. int mid = start + (end - start) / 2;
9. if (insertNum > nums[mid])
10. start = mid + 1;
11. else if (insertNum < nums[mid])
12. end = mid - 1;
13. else {
14. insertIndex = mid + 1;
15. break;
16. }
17. }
18. if (insertIndex == -1)
19. insertIndex = start;
20. if (i - insertIndex >= 0)
21. System.arraycopy(nums, insertIndex, nums, insertIndex + 1, i - insertIndex);
22. nums[insertIndex] = insertNum;
23. }
24.}
P3:希尔排序(按下标进行增量分组)
基本原理:把记录按下标的一定增量分组,对每组进行直接插入排序,每次排序后减小增量,当增量减至 1 时排序完毕。
属于插入排序,又称缩小增量排序,是对直接插入排序的一种改进,并且是一种不稳定的排序,平均时间复杂度为O(n1.3),最差 时O(n²),最好 时O(n),空O(1)。
适用场景:中等规模的数据量,对规模很大的数据量不是最佳选择。
1.public void shellSort(int[] nums) {
2. for (int d = nums.length / 2; d > 0 ; d /= 2) {
3. for (int i = d; i < nums.length; i++) {
4. int insertNum = nums[i];
5. int insertIndex;
6. for (insertIndex = i - d; insertIndex >= 0 && nums[insertIndex] > insertNum; insertIndex -= d) {
7. nums[insertIndex + d] = nums[insertIndex];
8. }
9. nums[insertIndex + d] = insertNum;
10. }
11. }
12.}
P4:直接选择排序(一直选剩下最小的min放在未排序的最首位)
属于选择排序,是一种不稳定的排序,任何情况下 时 O(n²),空 O(1)。
基本原理:每次在未排序序列中找到最小元素,和未排序序列的第一个元素交换位置,再在剩余未排序序列中重复该操作直到所有元素排序完毕。
适用场景:数据量较小的情况,比直接插入排序稍快。
1.public void selectSort(int[] nums) {
2. int minIndex;
3. for (int index = 0; index < nums.length - 1; index++){
4. minIndex = index;
5. for (int i = index + 1;i < nums.length; i++){
6. if(nums[i] < nums[minIndex])
7. minIndex = i;
8. }
9. if (index != minIndex){
10. swap(nums, index, minIndex);
11. }
12. }
13.}
P5:堆排序
属于选择排序,是对直接选择排序的改进,不稳定的排序,任时O(nlogn),空O(1)。
基本原理:将待排序记录看作完全二叉树,可以建立大根堆或小根堆,大根堆中每个节点的值都不小于它的子节点值,小根堆中每个节点的值都不大于它的子节点值。
以大根堆为例,在建堆时首先将最后一个节点作为当前节点,如果当前节点存在父节点且值>父节点,就将当前节点和父节点交换。在移出时首先暂存根节点的值,然后用最后一个节点代替根节点并作为当前节点,如果当前节点存在子节点且值小于子节点,就将其与值较大的子节点进行交换,调整完堆后返回暂存的值。
适用场景:数据量较大的情况。
P6、冒泡排序
稳定,平均/最坏时间复杂度 O(n²),元素基本有序时最好时间复杂度 O(n),空间复杂度 O(1)。
基本原理:比较相邻的元素,如果第一个比第二个大就进行交换,对每一对相邻元素做同样的工作,从开始第一对
到结尾的最后一对,每一轮排序后末尾元素都是有序的,针对 n 个元素重复以上步骤 n -1 次排序完毕。
1.public void bubbleSort(int[] nums) {
2. for (int i = 0; i < nums.length - 1; i++) {
3. for (int j = 0; j < nums.length - 1 - i; j++) {
4. if (nums[j] > nums[j + 1]) {
5. swap(nums, j, j + 1);
6. }
7. }
8. }
9.}
P7、快速排序
不稳定,平均/最好时间复杂度 O(nlogn),元素基本有序时最坏时间复杂度O(n²),空间复杂度 O(logn)。
基本原理:首先选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全部小于等于基准
元素,一部分全部大于等于基准元素,再按此方法递归对这两部分数据进行快速排序。
快速排序的一次划分从两头交替搜索,直到 low 和 high 指针重合,一趟时间复杂度 O(n),整个算法的时间复杂度与划分趟数有关。
最好情况是每次划分选择的中间数恰好将当前序列等分,经过 log(n) 趟划分便可得到长度为 1 的子表,这样时间复杂度 O(nlogn)。
最坏情况是每次所选中间数是当前序列中的最大或最小元素,这使每次划分所得子表其中一个为空表,这样长度为 n 的数据表需要 n 趟划分,整个排序时间复杂度 O(n²)。
1.public void quickSort(int[] nums, int start, int end) {
2. if (start < end) {
3. int pivotIndex = getPivotIndex(nums, start, end);
4. quickSort(nums, start, pivotIndex - 1);
5. quickSort(nums, pivotIndex + 1, end);
6. }
7.}
8.
9.public int getPivotIndex(int[] nums, int start, int end) {
10. int temp = nums[start];
11. int low = start;
12. int high = end;
13. while (low < high) {
14. while (low <= high && nums[low] <= temp) low++;
15. while (low <= high && nums[high] > temp) high–;
16. if (low < high) {
17. swap(nums, low, high);
18. }
19. }
20. swap(nums, start, high);
21. return high;
22.}
P8、归并排序
归并排序基于归并操作,是一种稳定的排序算法,任何情况时间复杂度都为 O(nlogn),空间复杂度为O(n)。
基本原理:应用分治法将待排序序列分成两部分,然后对两部分分别递归排序,最后进行合并,使用一个辅助空间并设定两个指针分别指向两个有序序列的起始元素,将指针对应的较小元素添加到辅助空间,重复该步骤到某一序列到达末尾,然后将另一序列剩余元素合并到辅助空间末尾。
适用场景:数据量大且对稳定性有要求的情况。
1.int[] help;
2.
3.public void mergeSort(int[] arr) {
4. int[] help = new int[arr.length];
5. sort(arr, 0, arr.length - 1);
6.}
7.
8.public void sort(int[] arr, int start, int end) {
9. if (start == end) return;
10. int mid = start + (end - start) / 2;
11. sort(arr, start, mid);
12. sort(arr, mid + 1, end);
13. merge(arr, start, mid, end);
14.}
15.
16.public void merge(int[] arr, int start, int mid, int end) {
17. if (end + 1 - start >= 0) System.arraycopy(arr, start, help, start, end + 1 - start);
18. int p = start;
19. int q = mid + 1;
20. int index = start;
21. while (p <= mid && q <= end) {
22. if (help[p] < help[q]) arr[index++] = help[p++];
23. else
24. arr[index++] = help[q++];
25. }
26. while (p <= mid) arr[index++] = help[p++];
27. while (q <= end) arr[index++] = help[q++];
28.}
Q、Java中常见的锁
一、悲观锁,乐观锁
1、悲观锁
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有其他线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被其他线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
2、乐观锁
在并发操作时,认为不会有其他的线程来修改数据,所以不会加锁。在更新数据的时候会判断有没有其他线程更新这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果数据已经被其他线程更新,则会根据不同的实现方式进行不同的操作(比如:报错或者自动重试)
悲观锁适合写多的操作,先加锁可以确保数据写操作时的准确性
乐观锁适合读多的操作,不加锁的特点能够使读操作的性能得到提升
Q、公平锁和非公平锁的区别
公平锁:线程同步时,多个线程排队时,顺序执行。
非公平锁:线程同步时,多个线程排队时,可以插队。
Q、Java中常见的锁
乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新), 如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS(比较交换) 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁 悲观锁就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞直到拿到锁。
java中的悲观锁就是synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
自旋锁
(获取不到锁的时候等待一会,看能否获取到锁,主要是为了减少线程上下文切换带来的开销) 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换! 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;
自旋锁时间阈值(1.6引入了适应性自旋锁)
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
JVM对于自旋周期的选择,jdk1.5这个限度是一定的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM还针对当前CPU的负荷情况做了较多的优化,如果平均负载小于CPUs则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果CPU处于节电模式则停止自旋,自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。
自旋锁的开启
JDK1.6中-XX:+UseSpinning开启;-XX:PreBlockSpin=10为自旋次数;
JDK1.7后,去掉此参数,由jvm控制;
Synchronized 同步锁 synchronized它可以把任意一个非NULL的对象当作锁。它属于独占式的悲观锁,同时属于可重入锁。
Synchronized 作用范围:
1.作用于实例方法时,锁住的是当前对象的实例(this);
2.作用于静态方法时,锁住的是Class类;
3.作用于代码块,根据锁定的对象判定锁定什么。
独占锁 独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
共享锁 共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
1.AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别标识AQS队列中等待线程的锁获取模式。
2.java的并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。
重量级锁 依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。
轻量级锁 轻量级锁总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
偏向锁 偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级 锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换 ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
Q、什么是CAS?
Q1、概念及特性
CAS(Compare And Swap/Set)比较并交换,无锁优化,乐观锁机制,锁自旋。位于java.util.concurrent.atomic.xxx 包下,属于原子操作。
CAS算法过程:它包含 3 个参数 CAS(V,E,N)。V (variate)表示要更新的变量(内存值),E (expect)表示预期值(旧的),N (new)表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。(看要更新的值是否等于期望值)
CAS 操作是基于乐观锁机制,认为操作可以成功完成。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理, CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
Q2、原子包 java.util.concurrent.atomic(锁自旋)
JDK1.5 的原子包:java.util.concurrent.atomic 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。相对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现。
Q3、ABA问题
CAS 会导致“ABA问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下一时刻比较并替换,那么这个时间差中可能会存在数据变化。比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。 部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。
Q4、CAS会带来什么问题?
· ABA问题
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
· 循环时间长开销大
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
· 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
Q、什么是AQS(抽象的队列同步器)
AbstractQueuedSynchronizer ,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的 ReentrantLock/Semaphore/CountDownLatch。
维护一个volatile int state共享资源和一个FIFO线程等待队列(多线程争抢资源时会进入该阻塞队列等待资源)。state 的访问方式有三种: getState()、setState()、compareAndSetState()。
Q1、AQS 定义两种资源共享方式
1、Exclusive 独占资源-ReentrantLock
Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
2、Share 共享资源-Semaphore/CountDownLatch
Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。
AQS 只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)之所以没有定义成 abstract,是因为独占模式下只用实现 tryAcquire-tryRelease ,而共享模式下只用实现 tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法: 1.isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
2.tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
3.tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
4.tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余 可用资源;正数表示成功,且有剩余资源。
5.tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。
同步器的实现是 ABS 核心(state 资源状态计数)
同步器的实现是 ABS 核心,以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失 败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意, 获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。 以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后面动作。 ReentrantReadWriteLock 实现独占和共享两种方式。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也
只需要实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器 同时实现独占和共享两种方式,如 ReentrantReadWriteLock。
Q、多线程
多线程是指一个程序中可以同时运行多个不同的线程来执行不同的任务。
何时需要多线程
程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
需要一些后台运行的程序时。
优点
提高程序的响应
提高CPU的利用率
改善程序的结构,将复杂的任务分为多个线程,独立运行。
缺点
线程也是程序,占用内存,线程越多占用内存也是越多的。
多线程是需要协调和管理,所以这也是需要CPU时刻跟踪线程的。
线程之间对共享资源的访问会互相影响,必须解决竞用共享资源的问题。
Q、多线程怎么保证线程安全?
方法
要保证线程安全,就必须保证线程同步。保证线程的可见性,有序性和原子性。
· 线程同步
线程同步的含义和字面意义相反。同步其实是线程“排队”的意思。就是让线程按照一定的顺序执行,每一时刻,只有一个线程,对共享资源进行操作。
· 可见性
一个线程对共享资源的操作,所有的线程都可以看见。
以具体实例来说明。就好比一个线程修改了一个数据,其他线程立马知道该数据被修改了。即就是在线程和主存之间有一个缓存,线程修改数据,是在缓存中修改,还需要在主存修改,而可见性就是立刻在主存中修改,防止其他线程读取时,发生数据错误。
· 有序性
就是代码的执行顺序是有序的,执行的顺序不会发生改变。
· 原子性
顾名思义,原子是一个不可分的整体。就是一个代码块,要么全部执行,要么全部不执行,只要其执行了,就无法被任何事务打断。
具体方法
volatile关键字
作用:保证线程可见性和禁止指令重排序。
保证可见性
实现原理:当一个变量被volatile修饰,一旦其发生改变,那么根据缓存一致性协议,其他线程的缓存都会失效,需要重新从内存中获取数据,就可以保证数据的准确性。就好比这个数据修改了,其他线程缓存失效,就知道了它被修改了,就要重新获取,即可见的含义。
禁止重排序
实现原理:通过jvm给指令的前后加上内存屏障,屏障两边的指令不可以重排序,保证有序。
synchronized关键字
作用:利用线程互斥来实现线程同步,即通过同一时刻只有一个线程可以访问(互斥),来实现线程的原子性,全部执行完,才能换线程执行,线程顺序执行(同步)。
synchronized最主要的就是保持原子性,保持原子性,最主要的就是线程同步,同步最基本的方法就是加锁,加锁最直接的就是加synchronized关键字。
效率:synchronized在早期是一把重量级锁,但是随着java发展,如今的效率已经很高。例如i++不是原子操作,它分为三步:1.获取i的值 2.修改i的值 3.将修改的值赋予i 。如果在其外面加入synchronized关键字,保证了每次只有一个线程可以修改i,那么就保证了i++在并发环境下的安全性。
保证原子性
上面的双重检测锁也使用了synchronized关键字,加同一个锁的线程同步互斥,里面的代码块在同一时刻,只有一个线程可以访问,所以保证了唯一实例。
Q、Java中实现多线程的方式?
1、继承 Thread 类
2、实现 Runnable 接口
3、实现 Callable 接口
Callable 接口和Runnable 接口的区别在于:Runnable 的 run() 方法没有返回值;Callable 的 call() 方法有返回值。
Q、JUC工具类(三种计数方法)
CountDownLatch减法计数器:
可以用来倒计时,当两个线程同时执行时,如果要确保一个线程先执行,可以使用计数器,当计数器清零的时候,再让另一个线程执行。
CyclicBarrier加法计数器:
可以用来计时,当执行的次数达到 CyclicBarrier 设置的值时,就会执行 CyclicBarrier 参数中的接口中实现的代码,达到一次条件就会执行一次 CyclicBarrier 中的接口方法。
semaphore计数信号量:
实际开发时可以使用它来完成限流操作,限制可以访问某些资源的线程数量。
Q、线程池核心参数
corePoolSize核心池大小,初始化的线程数量。
maximumPoolSize线程池最大线程数,它决定了线程池容量的上限。
corePoolSize就是线程池的大小,maximumPoolSize 是一种补救措施,任务量突然增大的时候的一种补救措施。
keepAliveTime线程对象的存活时间(在没有任务可执行的情况下),必须是线程池中的数量大于 corePoolSize,才会生效。
unit线程对象存活时间单位
workQueue等待队列,存储等待执行的任务。
threadFactory线程工厂,用来创建线程对象
handler拒绝策略
拒绝策略有4种:
1、AbortPolicy:直接抛出异常
2、DiscardPolicy:放弃任务,不抛出异常
3、DiscardOldestPolicy:尝试与等待队列中最前面的任务去争夺,不抛出异常
4、CallerRunsPolicy:谁调用谁处理
Q、常见的几种线程池
1、单例线程池(newSingleThreadExecutor())
2、固定数量线程池(newFixedThreadPool())
3、带缓存的线程池(newCachedThreadPool())
Q、并发和并行有什么区别
并发 就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器可以
做到并发。比如有两个进程 A 和 B , A 运行一个时间片之后,切换到 B , B 运行一个时间片之后又切换
到 A 。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。
并行 就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条
指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。
Q、死锁–阻塞
在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。
通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。
如何处理死锁问题
常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略(忽略)。
阻塞是由于资源不足引起的排队等待的现象。
Q1、线程死锁的4个必要条件
①互斥条件:该资源任意一个时刻只由一个线程占用。
②请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
③不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
④循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
Q2、如何预防和避免线程死锁?
· 如何预防死锁? 破坏死锁的产生的必要条件即可:
①破坏请求与保持条件:一次性申请所有的资源。
②破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
③破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
· 如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3…Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称
Q、Synchronized’ 有几种用法?
修饰实例方法 (锁当前对象实例)
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁。
修饰静态方法 (锁当前类)
给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何⼀个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
静态 synchronized 用法和非静态 synchronized ⽅法之间的调用互斥么?不互斥!如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized 方法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized 方法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized ⽅法占用的锁是当前实例对象锁。
Q、ThreadLocal和Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
① Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
② Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
Q、线程生命周期/状态
线程在生命周期中会经历新建(New)、就绪(Runnable)、运行(Running)、阻塞 (Blocked)和终止(Terminated)5种状态。
新建状态:使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。
就绪状态:当线程对象调用了 start() 方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
运行状态:如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞状态:阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu 时间片,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu 时间片转到运行(running)状态。阻塞的情况分3种:
①等待阻塞(wait -> 等待队列)
运行(running)的线程执行 wait()方法,JVM 会把该线程放入等待队列(waitting queue) 中。
②同步阻塞(lock -> 锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
③其他阻塞(sleep/join/yield) 运行(running)的线程执行 Thread.sleep(long ms)或 join() 或 yield() 方法,或者发出了 I/O 请求时, JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行(runnable)状态。
终止状态:线程运行完毕或因异常导致线程终止运行。
Q、终止线程的4种方式
1、线程正常运行结束。
2、使用退出标志退出线程。
3、调用interrupt方法结束线程。
①线程处于阻塞状态:如使用了sleep,同步锁的wait,socket中的 receiver,accept 等方法时, 会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。 阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的,一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。(线程处于阻塞状态,只有捕获了InterruptedException 异常之后,才能正常终止线程)
②线程未处于阻塞状态:使用 isInterrupted() 判断线程的中断标志来退出循环。当使用 interrupt()方法时,中断标志就会置 true,线程可以终止。
4、调用stop方法终止线程(线程不安全)。
Q、sleep和wait的区别
sleep wait
共同点 wait(),wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃CPU的使用权,进阻塞状态。
方法归属不同 sleep是Thread类的静态方法;
wait是Object的成员方法,每个对象都有。
醒来时机不同 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来;
wait(long) 和 wait() 还可以被notify唤醒,wait() 如果不唤醒就一直等下去
锁特性不同 wait方法的调用必须先获取wait对象的锁,而sleep 则无此限制。
wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃,但你们还可以用)。
而sleep如果在synchronized 代码块中执行,并不会释放对象锁(我放弃,你们也用不了)。
Q、设计模式
设计模式原则
· 开闭原则:OOP 中最基础的原则,指一个软件实体(类、模块、方法等)应该对扩展开放,对修改关闭。强调用抽象构建框架,用实现扩展细节,提高代码的可复用性和可维护性。
· 单一职责原则:一个类、接口或方法只负责一个职责,降低代码复杂度以及变更引起的风险。
· 依赖倒置原则:程序应该依赖于抽象类或接口,而不是具体的实现类。
· 接口隔离原则:将不同功能定义在不同接口中实现接口隔离,避免了类依赖它不需要的接口,减少了接口之间依赖的冗余性和复杂性。
· 里氏替换原则:开闭原则的补充,规定了任何父类可以出现的地方子类都一定可以出现,可以约束继承泛滥,加强程序健壮性。
· 迪米特原则:也叫最少知道原则,模块之间都要尽可能少地了解和依赖,降低代码耦合度。
· 合成/聚合原则:尽量使用组合(has-a)/聚合(contains-a)而不是继承(is-a)达到软件复用的目的,避免滥用继承带来的方法污染和方法爆炸。
方法污染指父类的行为通过继承传递给子类,但子类并不具备执行此行为的能力;
方法爆炸指继承树不断扩大,底层类拥有的方法过于繁杂,导致很容易选择错误。
Q、简单工厂模式 – 只需传参
· 简单工厂模式指由一个工厂对象来创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。适用于工厂类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则,且产品多的话会使工厂类比较复杂。
· Calendar 抽象类的 getInstance 方法,调用 createCalendar 方法根据不同的地区参数创建不同的日历对象。
Spring 中的 BeanFactory 使用简单工厂模式,根据传入一个唯一的标识来获得 Bean 对象。
Q、工厂方法模式 – 让接口的实现类决定创建哪种对象
· 工厂方法模式指定义一个创建对象的接口,让接口的实现类决定创建哪种对象,让类的实例化推迟到子类中进行。
· 客户端只需关心对应工厂而无需关心创建细节,主要解决了产品扩展的问题,在简单工厂模式中如果产品种类变多,工厂的职责会越来越多,不便于维护。
· Collection 接口这个抽象工厂中定义了一个抽象的 iterator 工厂方法,返回一个 Iterator 类的抽象产品。该方法通过 ArrayList 、HashMap 等具体工厂实现,返回 Itr、KeyIterator 等具体产品。
Spring 的 FactoryBean 接口的 getObject 方法也是工厂方法。
Q、抽象工厂模式 – 创建一个无需指定具体类的接口
· 抽象工厂模式指提供一个创建一系列相关或相互依赖对象的接口,无需指定它们的具体类。
· 客户端不依赖于产品类实例如何被创建和实现的细节,主要用于系统的产品有多于一个的产品族,而系统只消费其中某一个产品族产品的情况。抽象工厂模式的缺点是不方便扩展产品族,并且增加了系统的抽象性和理解难度。
· java.sql.Connection 接口就是一个抽象工厂,其中包括很多抽象产品如 Statement、Blob、Savepoint
等
Q、单例模式 – 饿汉式 & 懒汉式
· 单例模式属于创建型模式,一个类只允许创建一个对象或实例,构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。
· 优点:内存中只有一个实例,减少了开销,尤其是频繁创建和销毁实例的情况下并且可以避免对资源的多重占用。
· 缺点:①没有抽象层,难以扩展,与单一职责原则冲突。
②单例类的职责过重, 在一定程度上违背了“单一职责原则”。 因为单例类既充当了工厂角色, 提供了工厂方法, 同时又充当了产品角色, 包含一些业务方法, 将产品的创建和产品的本身的功能融合到起。
③单例模式实例化的共享对象如果长时间不被利用可能会被系统认定为垃圾,从而被自动销毁并回收。下次 使用时又将重新实例化,这将导致共享的单例对象状态的丢失。
· Spring 的 ApplicationContext 创建的 Bean 实例都是单例对象,还有 ServletContext、数据库连接池等也都是单例模式。
使用场景
1.处理资源访问冲突,多线程同时访问;
2.表示全局唯一类,从业务上来讲,如果有些数据在系统中只保存一份,适合设计成单例类。
饿汉式:在类加载时就初始化创建单例对象。
优点:线程安全;
缺点:对象加载时间过长,一直占用资源;
不管是否使用都创建对象可能会浪费内存。
懒汉式:在外部调用时才会加载。
优点:延迟对象的创建;
确定:线程不安全,
可以加锁保证线程安全但效率低
单例模式的实现方式
饿汉单例
线程安全,安全是由类加载机制保证。
1./**
2. * 饿汉式:会一直占用着资源
3. * 说法1:如果实例占用资源多或者初始化耗时长,提前初始化实例是一种浪费资源的行为,应该在用到时再去初始化。
4. *
5. * 说法2:1.如果初始化耗时长,在用到时再去初始化的话,会影响到系统性能。比如在请求接口时,做初始化操作,会导致请求的响应时间变长。耗时的初始化操作提前到启动时完成,能避免程序运行时导致的性能问题。
6. * 2.如果实例占用资源多。我们在程序启动时就能触发报错,我们可以立即修复,能避免程序运行一段时间后,突然初始化实例占用资源多,导致系统崩溃,影响系统性能。
7. *
8. * 如果耗时短,占用资源少的话,在什么时候都可以。
9. */
10.public class Singleton {
11. private static final Singleton instance = new Singleton();
12.
13. private Singleton(){}
14.
15. public static Singleton getInstance(){
16. return instance;
17. }
18.
19. public static void main(String[] args){
20. Singleton singleton = Singleton.getInstance();
21. }
22.}
懒汉单例
懒汉单例模式和饿汉单例模式的区别就是:懒汉创建了延迟对象,同时饿汉式的单例对象是被修饰为final类型。
优点:尽最大可能节省内存空间
缺点:在多线程编程中,使用懒汉式可能造成类的对象在内存中不唯一,虽然用过修改代码可以改正这些问题,但是降低了效率,线程不安全。
1./**
2. * 懒汉式:支持延迟加载
3. * getInstance()函数加锁操作,导致函数并发度很低,并发度为1,相当于串行操作。如果函数被频繁用到的话,
4. * 频繁的加锁、释放锁及并发度低等问题,会导致性能瓶颈。
5. */
6.public class Singleton {
7. private static Singleton instance = null;
8.
9. private Singleton(){}
10.
11. public static Singleton getInstance(){
12. if(instance==null)
13. instance = new Singleton();
14. return instance;
15. }
16.}
全局锁式
线程安全,线程同步时效率不高(synchronized)
1.public class Singleton {
2. private static Singleton instance;
3.
4. private Singleton(){}
5.
6. //synchronized修饰的是静态方法,锁住类对象
7. public synchronized static Singleton getInstance(){
8. if(instance == null)
9. instance = new Singleton();
10. return instance;
11. }
12.}
静态代码块
线程安全,类主动加载时才初始化实例,实现了懒加载策略,且线程安全。
1.public class Singleton {
2. private final static Singleton instance;
3. private Singleton(){}
4.
5. static {
6. instance = new Singleton();
7. }
8.
9. public static Singleton getInstance(){
10. //使用前将singleton属性通过静态代码块实现
11. return instance;
12. }
13.
14. public static void main(String[] args){
15. //第一次调用Singleton,JVM需要负责将其加载到内存中,在加载过程处理静态代码块
16. Singleton.getInstance();
17. //第二次调用Singleton,JVM中已经存在Singleton,直接使用getInstance
18. Singleton.getInstance();
19. }
20.}
双重校验锁式
线程安全,且实现了懒加载策略,同时保证了线程同步时的效率。但是volatile强制当前线程每次读操作进行时,保证其他所有线程写操作已完成。volatile使得JVM内部的编译器舍弃了编译时优化,对于性能有一定的影响
1./**
2. * 双重检测:既支持延迟加载,也支持高并发。
3. *
4. * 在instance创建之后,调用getInstance()函数,不会进入加锁逻辑,解决了懒汉式并发度低的问题。
5. * 如果不用volatile修饰的话,因为指令重排序,可能会导致对象被new出来复制给instance之后,还没来得及执行构造函数中的代码逻辑,就被另一个线程使用了。
6. * volatile关键字可以禁止指令重排序。在低版本java中才会出现指令重排序,高版本的java已经在JDK中解决了这个问题,只要把对象new操作和初始化操作设计成原子操作,就能禁止指令重排序
7. */
8.public class Singleton {
9. //volatile关键词确保:在instance变量被初始化Singleton实例时,多个线程正确的处理instance变量
10. private volatile static Singleton instance;
11. private Singleton(){}
12.
13. public static Singleton getInstance() {
14. if (instance == null) {
15. synchronized (Singleton.class) {
16. if (instance == null) {
17. //内层if判断使用的时间(起作用时机)
18. //第一次两线程同时调用getInstance,都会进入外层if判断
19. //内层if判断是针对第二个人进入synchronized代码块线程,此时第一个线程已经创建出对象
20. //第二个线程无需创建
21. instance = new Singleton();
22. }
23. }
24. }
25. return instance;
26. }
27.}
静态内部类式(推荐)
线程安全,不存在线程同步问题,且单例对象在程序第一次getInstance()时主动加载SingletonHolder 和 静态成员INSTANCE
1./**
2. * 静态内部类
3. * SingletonHolder是一个静态内部类,当外部类Singleton4被加载的时候,并不会
4. * 创建SingletonHolder实例对象,只有当调用getInstance()函数时,SingletoneHolder菜
5. * 会被加载,此时才会创建instance.instance的唯一性、创建过程的线程安全性,都由JVM来
6. * 保证。这种实现方式既保证线程安全,又能做到延迟加载。
7. */
8.public class Singleton {
9. private Singleton(){}
10.
11. private static class SingletonHolder{
12. private static final Singleton instance = new Singleton();
13. }
14.
15. public static Singleton getInstance(){
16. return SingletonHolder.instance;
17. }
18.}
枚举式
线程安全,不存在线程同步问题,且单例对象在枚举类型INSTANCE,第一次引用时通过枚举的 构造函数 初始化
这种方式是Effective Java 书籍提倡的方式。它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
1./**
2. * 枚举
3. * 通过java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
4. */
5.public class Singleton {
6. private Singleton(){}
7.
8. enum SingletonEnum{
9. INSANCE;
10.
11. private final Singleton singleton;
12.
13. private SingletonEnum(){
14. instance = new Singleton();
15. }
16. }
17.
18. public static Singleton getInstance(){
19. return SingletonEnum.INSANCE.instance;
20. }
21.}
单例是如何保证只创建一个对象的?
双重检验锁方式
确保类的构造方法是私有的,使用 static final 的私有成员变量存放这个唯一实例。
还要提供一个获取实例的接口(由于我们要通过类名获取实例,所以接口方法是 static 的)。
Q、代理模式
代理模式的原理:为其他对象提供一种代理以控制对这个对象的访问。
任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
优点:可以增强目标对象的功能,降低代码耦合度,扩展性好;
缺点:在客户端和目标对象之间增加代理对象会导致请求处理速度变慢,增加系统复杂度。
Spring 利用动态代理实现 AOP,如果 Bean 实现了接口就使用 JDK 代理,否则使用 CGLib 代理。
Q、静态代理与动态代理
静态代理:代理对象持有被代理对象的引用,调用代理对象方法时也会调用被代理对象的方法,但是会在被代理对象方法的前后增加其他逻辑。代理类在运行前就创建好了,是手动创建的类。缺点是一个代理类只能为一个目标服务,如果要服务多种类型会增加工作量。
动态代理:在程序运行时通过反射创建具体的代理类,代理类和被代理类的关系在运行前是不确定的。主要分为 JDK 动态代理和 CGLib 动态代理。
Q、JDK动态代理 – 实现接口
通过反射来接收被代理的类,在不改变原来目标方法功能的前提下,在代理中增强自己的功能代码。并且要求被代理的类必须实现一个接口。
JDK动态代理的核心是InvocationHandler接口和Proxy类。
问题1:为什么 JDK 动态代理要基于接口实现?而不是基于继承来实现?
解答:因为 JDK 动态代理生成的对象默认是继承 Proxy ,Java 不支持多继承,所以 JDK 动态代理要基于接口来实现。
问题2:JDK 动态代理中,目标对象调用自己的另一个方法,会经过代理对象吗?
解答:不会,内部调用方法使用的对象是目标对象本身,被调用的方法不会经过代理对象。
问题3:为什么不使用静态代理而使用动态代理?
静态代理是代理类在编译期间就创建好了,在编译时就已经将接口,被代理类,代理类等确定下来。如果有很多类很多接口需要代理,那么就只能使用代码提前写死,不够灵活;
使用了动态代理之后,不需要手动创建代理类,全部交给代理去完成对代理类的创建,实现无侵入式的代码扩展,这也是符合面向对象编程原则的操作。
Q、CGLib 动态代理 – 继承
JDK 动态代理要求实现被代理对象的接口,而 CGLib 要求继承被代理对象。CGLib(Code Generation Library)是一个代码生成的类库,可以在运行时动态的生成某个类的子类。CGLib是通过继承的方式做的动态代理,因此,如果一个类是 final 类则不能使用 CGLib 代理。
Spring 的代理方式有哪两种?
JDK 动态代理和 CGLIB 动态代理。
Q、观察者模式
观察者模式属于行为型模式,也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
缺点是如果被观察者对象有很多的直接和间接观察者的话通知很耗时, 如果存在循环依赖的话可能导致系统崩溃,另外观察者无法知道目标对象具体是怎么发生变化的。
ServletContextListener 能够监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用。当Servlet 容器启动 Web 应用时调用 contextInitialized 方法,终止时调用 contextDestroyed 方法。
Spring 事件驱动模型就是观察者模式很经典的一个应用。
Q、装饰器模式
· 装饰器模式属于结构型模式,在不改变原有对象的基础上将功能附加到对象,相比继承可以更加灵活地扩展原有对象的功能。
· 装饰器模式适合的场景:在不想增加很多子类的前提下扩展一个类的功能。
· 装饰器模式的关注点在于给对象动态添加方法,而动态代理更注重对象的访问控制。动态代理通常会在代理类中创建被代理对象的实例,而装饰器模式会将装饰者作为构造方法的参数。
· java.io 包中,InputStream 字节输入流通过装饰器 BufferedInputStream 增强为缓冲字节输入流。
· 装饰器模式具有层级关系,装饰器与被装饰者实现同一个接口,满足 is-a 的关系,注重覆盖和扩展,是一种前置考虑。
Q、适配器模式
适配器模式属于结构型模式,它作为两个不兼容接口之间的桥梁,使得原本由于接口不兼容而不能一起工作的类可以一起工作。
缺点是过多使用适配器会让系统非常混乱,不易整体把握。
· java.io 包中,InputStream 字节输入流通过适配器 InputStreamReader 转换为 Reader 字符输入流。
· Arrays.asList 方法,将数组转换为对应的集合(注意不能使用修改集合的方法,因为返回的 ArrayList是 Arrays 的一个内部类)。
· 适配器模式没有层级关系,适配器和被适配者没有必然连续,满足 has-a 的关系,解决不兼容的问题,是一种后置考虑。
· 适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
Q、策略模式
策略模式属于行为型模式,定义了一系列算法并封装起来,之间可以互相替换。策略模式主要解决在有多种算法相似的情况下,使用 if/else 所带来的复杂和难以维护问题。
优点是算法可以自由切换,可以避免使用多重条件判断并且扩展性良好;
缺点是策略类会增多并且所有策略类都需要对外暴露。
· 在集合框架中,经常需要通过构造方法传入一个比较器 Comparator 进行比较排序。Comparator 就是一个抽象策略,一个类通过实现该接口并重写 compare 方法成为具体策略类。
· 创建线程池时,需要传入拒绝策略,当创建新线程使当前运行的线程数超过 maximumPoolSize 时会使用相应的拒绝策略处理。
Q、模板模式
模板模式属于行为型模式,使子类可以在不改变算法结构的情况下重新定义算法的某些步骤,适用于抽取子类重复代码到公共父类。
优点是可以封装固定不变的部分,扩展可变的部分;
缺点是每一个不同实现都需要一个子类维护,会增加类的数量。
· 为防止恶意操作,一般模板方法都以 final 修饰。
· HttpServlet 定义了一套处理 HTTP 请求的模板,service 方法为模板方法,定义了处理HTTP请求的基本流程,doXXX 等方法为基本方法,根据请求方法的类型做相应的处理,子类可重写这些方法。
Q、字节流和字符流的区别?
①读写的时候字节流是按字节读写,字符流按字符读写。
②字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位。字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。
③在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。
④只是读写文件,和文件内容无关时,一般选择字节流。
Q、序列化和反序列化
Q1、如果不想进行序列化怎么办?
Q2、使用到序列化和反序列化的场景?
Q、泛型
Q1、泛型通配符
? 表示可以任意类型的泛型对象。
Q2、泛型上限和下限
1、泛型上限(最大类型,小于等于上限才可以):表示实例化时具体的数据类型,可以是上限类型的子类或者时上限类型本身,用 extends 表示。
使用:类名 <泛型标识 extends 上限类名>
2、泛型下限(最小类型,大于等于下限都可以):表示实例化时具体的数据类型,可以是下限类型的父类或者是下限类型本身,用 super 表示。
使用:类名 <泛型标识 super 下限类名>
Q、Java问题诊断排查工具
jps:查看本机java进程信息
jstack:打印线程的栈信息,制作线程dump文件
jmap:打印内存映射信息,制作堆dump文件
jstat:性能监控工具
jhat:内存分析工具,用于解析堆dump文件并以适合人阅读的方式展示出来
jConsole:简易的JVM可视化工具
jVisualVM:功能更强大的JVM可视化工具
javap:查看字节码
Q、notify() 和 notifyAll() 有什么区别?
notify() 方法随机唤醒对象的等待池中的一个线程,进入锁池;
notifyAll() 唤醒对象的等待池中的所有线程,进入锁池。
· 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池,等待池中的线程不会去竞争该对象的锁。
· 锁池:只有获取了对象的锁,线程才能执行对象的 synchronized 代码,对象的锁每次只有一个线程可以获得,其他线程只能在锁池中等待
Q、队列和栈是什么?有什么区别?
· 队列和栈的区别
队列(Queue):限定只能在表的一端进行插入和在另一端进行删除操作的线性表;
栈(Stack):是限定只能在表的一端进行插入和删除操作的线性表。
(1)操作的名称不同:
队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
(2)操作的限定不同:
队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行 操作。
(3)操作的规则不同:
队列是先进先出(FIFO),而栈为后进先出(LIFO)。
(4)遍历数据速度不同:
队列是基于地址指针进行遍历,而且可以从头部或者尾部进行遍历,但不能同时遍历,无需开辟空间。
栈是只能从顶部取数据,而且在遍历数据的同时需要为数据开辟临时空间,保持数据在遍历前的一致性。
(5)接口实现:
队列和栈由Collcetion接口实现,队列由Queue接口实现,栈由List接口实现。
(6)遍历数据速度不同
队列:基于地址指针进行遍历,而且可以从头部或者尾部进行遍历,但不能同时遍历,无需开辟空间,遍历速 度要快;
栈:只能从顶部取数据,也就是说最先进入栈底的,需要遍历整个栈才能取出来,而且在遍历数据的同时 需要为数据开辟临时空间,保持数据在遍历前后的一致性。
Q、树
1、概念
· 树的定义:树是由(n>=1)个有限节点组成的一个具有层次关系的集合。
· 树的特点
①每个结点具有零个或多个子结点。
②没有父节点的结点为根结点。
③每个非根结点只有一个父结点。
④每个结点及其后代结点整体上可以看作是一棵树,称为当前结点的父结点的一个子树。
· 树的相关术语
结点的度:一个结点 含有的子树的个数 称为该结点的度。
叶子结点:度为0的结点 称为叶结点,也可以叫做终端结点。
分支结点:度不为0的结点 称为分支结点,也可以叫做非终端结点。
结点的层次:从根结点开始,根结点的层次为1,根的直接后继层次为2,以此类推。
结点的层序编号:将树中的结点,按照从上层到下层,同层从左到右的次序排成一个线性序列,把他们编成连续的自然数。
树的度:树中 所有结点的度的最大值。
树的高度(深度):树中 结点的最大层次。
森林:m(m>=0)个互不相交的树的集合,将一颗非空树的根结点删去,树就变成一个森林;给森林增加一个统一的根结点,森林就变成—棵树。
孩子结点:—个结点的直接后继结点称为该结点的孩子结点。
双亲结点(父结点):一个结点的直接前驱称为该结点的双亲结点。
兄弟结点:同一双亲结点的孩子结点间互称兄弟结点。
2、二叉树
二叉树就是度不超过2的树(每个结点最多有两个子节点)。
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
· 二叉树的遍历方式
①前序遍历:先访问根结点,然后再访问左子树,最后访问右子树。
②中序遍历:先访问左子树,中间访问根节点,最后访问右子树。
③后序遍历:先访问左子树,再访问右子树,最后访问根节点。
④层序遍历:从根节点(第一层)开始,依次向下,获取每一层所有结点的值(广度优先遍历)。
3、平衡树
1、2-3查找树
为了保证查找树的平衡性,我们需要一些灵活性,因此在这里我们允许树中的一个结点保存多个键。确切的说,我们将一棵标准的二叉查找树中的结点称为2-结点(含有一个键和两条链),而现在我们引入3-结点,它含有两个键和三条链。2-结点和3-结点中的每条链都对应着其中保存的键所分割产生的一个区间。
· 2-3树满足的条件
2-结点:含有一个键(及其对应的值)和两条链,左链接指向2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
3-结点:含有两个键(及其对应的值)和三条链,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。
· 2-3树的性质
通过对2-3树插入操作的分析,我们发现在插入的时候,2-3树需要做一些局部的变换来保持2-3树的平衡。
—棵完全平衡的2-3树具有以下性质:
①任意空链接到根结点的路径长度都是相等的。
②4-结点变换为3-结点时,树的高度不会发生变化,只有当根结点是临时的4-结点,分解根结点时,树高+1。
③2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长,而2-3树是自底向上生长。
4、红黑树
红黑树主要是对2-3树进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:
红链接:将两个2-结点连接起来构成一个3-结点。
黑链接:则是2-3树中的普通链接。
确切的说,我们将3-结点表示为由由一条左斜的红色链接(两个2-结点其中之一是另一个的左子结点)相连的两个2-结点。
· 红黑树的定义
红黑树是含有红黑链接并满足下列条件的二叉查找树:
①红链接均为左链接。
②没有任何一个结点同时和两条红链接相连。
③该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同。
一种自平衡的二叉查找树(左中右↑),除了满足二叉查找树的性质外,还需要满足如下五个条件:
① 每个节点非黑即红;
② 根节点总是黑色的;
③ 每个叶子节点都是黑色的空节点(NIL节点);
④ 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
⑤ 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
应用:TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。
为什么要用?是为了解决二叉查找树在某些情况下会退化成一个线性结构的问题(如:形成二叉查找树的时候插入的元素是有序时,查找性能变成线性)。
· 红黑树平衡化
左旋:当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋。
右旋:当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋。
5、B树
B树是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等操作。
B树中允许一个结点中包含多个key。M阶的B树,具有如下特点:
①每个结点最多有M-1个key,并且以升序排列。
②每个结点最多能有M个子结点。
③根结点至少有两个子结点。
6、B+树
B+树是对B树的一种变形树,它与B树的差异在于:
①非叶结点仅具有索引作用,也就是说,非叶子结点只存储key,不存储value。
②树的所有叶结点构成一个有序链表,可以按照key排序的次序遍历全部数据。
· B+树和B树的区别
B+树的优点在于:
①由于B+树在非叶子结点上不包含真正的数据,只当做索引使用,因此在内存相同的情况下,能够存放更多的key。
②B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。
B树的优点在于:
由于B树的每一个节点都包含key和value,因此我们根据key查找value时,只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找,都必须一次一次,一直找到树的最大深度处,也就是叶子结点的深度,才能找到value。
7、堆
堆是计算机科学中一类特殊的数据结构的统称,堆通常可以被看做是一棵完全二叉树的数组对象。
· 堆的特性
①它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。
②通常使用数组来实现。
8、优先队列
最大优先队列:可以获取并删除队列中最大的值。
最小优先队列:可以获取并删除队列中最小的值。
9、图
图是由一组顶点和一组能够将两个顶点相连的边组成的。
自环:即一条连接一个顶点和其自身的边。
平行边:连接同一对顶点的两条边。
1、图的分类
按照连接两个顶点的边的不同,可以把图分为以下两种:
无向图:边仅仅连接两个顶点,没有其他含义。
有向图:边不仅连接两个顶点,并且具有方向。
2、无向图的相关术语
相邻顶点:当两个顶点通过—条边相连时,我们称这两个顶点是相邻的,并且称这条边依附于这两个顶点。
度:某个顶点的度就是依附于该顶点的边的个数。
子图:是一幅图的所有边的子集(包含这些边依附的顶点)组成的图。
路径:是由边顺序连接的一系列的顶点组成。
环:是—条至少含有一条边且终点和起点相同的路径。
连通图:如果图中任意一个顶点都存在一条路径到达另外一个顶点,那么这幅图就称之为连通图。
连通子图:一个非连通图由若干连通的部分组成,每一个连通的部分都可以称为该图的连通子图。
3、无向图的搜索算法
深度优先搜索和广度优先搜索。
4、有向图的相关术语
定义:有向图是一副具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点。
出度:由 某个顶点指出的边的个数 称为该顶点的出度。
入度:指向某个顶点的边的个数 称为该顶点的入度。
有向路径:由—系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点。
有向环:—条至少含有一条边,且起点和终点相同的有向路径。
Q、红黑树
一种自平衡的二叉查找树(左中右↑),除了满足二叉查找树的性质外,还需要满足如下五个条件:
① 每个节点非黑即红;
② 根节点总是黑色的;
③ 每个叶子节点都是黑色的空节点(NIL节点);
④ 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
⑤ 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
· 应用 :TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。
· 为什么要用? 是为了解决二叉查找树在某些情况下会退化成一个线性结构的问题(如:形成二叉查找树的时候插入的元素是有序时,查找性能变成线性)。
网页解释
Q、二叉树前中后序遍历的应用
(1)前序遍历:可以用来实现目录结构的显示。
(2)中序遍历:可以用来做表达式树,在编译器底层实现的时候用户可以实现基本的加减乘除,比如 a*b+c。
(3)后序遍历:可以用来实现计算目录内的文件占用的内存大小。
Q、HashCode()的作用?
Q、为什么要有hashCode?
Q、为什么重写equals()时必须重写hashCode()方法?
Q、关系型数据库和非关系型数据库
· 关系型数据库:MySql、Oracle、DB2、SQLServer
最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组。
优点:
1、格式一致,易于维护;
2、通用的SQL语言,使用方便;
3、支持SQL的复杂操作(多表)。
缺点:
1、读写性能较差;
2、表结构较为固定,灵活性较差;
3、应对高并发读写较差。
· 非关系型数据库:Redis、Mongo db、MemCached
一种数据结构化存储方法的集合,可以是文档或者键值对等。
优点:
1、格式灵活,可以使key-value形式、文档形式、图片形式等;(关系型数据库只支持基础类型)
2、速度快,可以使用硬盘或者随机存储器作为载体;(关系型数据库只能使用硬盘)
3、成本低,部署简单,大多都是开源软件。
缺点:
1、数据结构相对复杂;
2、没有事务处理机制。
非关系型数据库的分类
1、文档型
2、key-value型
3、图形数据库
4、列式数据库
Q、基本概念
数据库(DataBase,DB):存储数据的仓库,数据是有组织的进行存储。
数据库管理系统(DataBase Management System,DBMS):管理数据库的大型软件。
SQL(Structured Query Lanagement):结构化查询语言,操作关系型数据库的编程语言,定义了所有关系型数据库的统一标准。
Q、MySQL支持的数据类型有?
数值型、日期型、字符串型
Q、MySQL中的约束有?
非空约束、唯一约束、主键约束、默认约束、检查约束、外键约束
注意:MySQL中不支持检查约束。
非空约束:保证列中所有数据不能有null值(not null)。
唯一约束:保证列中所有数据各不相同(unique)。
主键约束:主键是一行数据的唯一标识,要求非空且唯一(primary key),一张表只能有一个主键。
检查约束:保证列中的值满足某—条件(check)。
默认约束:保存数据时,未指定值则采用默认值(default)。
外键约束:外键用来让两个表的数据之间建立链接,保证数据的一致性和完整性(foreign key)。
Q、多表查询
笛卡尔积:取A,B集合所有组合情况。
多表查询:从多张表查询数据
√ 连接查询
· 内连接:相当于查询AB交集数据。
· 外连接:
· 左外连接:相当于查询A表所有数据和交集部分数据。
· 右外连接:相当于查询B表所有数据和交集部分数据。
√ 子查询
Q、内连接(查询AB交集)
① 隐式内连接
select 字段列表 from 表1,表2 … where 条件;
② 显式内连接
select 字段列表 from 表1 [inner] join 表2 on 条件;
Q、外连接
① 左外连接(查询A表所有数据和交集部分数据)
select 字段列表 from 表1 left [outer] join 表2 on 条件;
② 右外连接(查询B表所有数据和交集部分数据)
select 字段列表 from 表1 right [outer] join 表2 on 条件;
Q、子查询(查询中嵌套查询)
子查询根据查询结果不同,作用不同:
· 单行单列:作为条件值,使用 =!= >< 等进行条件判断
select 字段列表 from 表 where 字段名 = (子查询);
· 多行单列:作为条件值,使用in等关键字进行条件判断
select 字段列表 from 表 where 字段名 in (子查询);
· 多行多列:作为虚拟表
select 字段列表 from (子查询) where 条件;
Q、临时表
定义
MySQL 临时表在需要保存一些临时数据时是非常有用的。临时表只在当前连接可见,当关闭连接时,Mysql会自动删除表并释放所有空间。
分类
临时表分为本地临时表和全局临时表,它们在名称、可见性以及可用性上有区别。
特点
本地临时表就是用户在创建表的时候添加了"#“前缀的表,其特点是根据数据库连接独立。只有创建本地临时表的数据库连接有表的访问权限,其它连接不能访问该表;
不同的数据库连接中,创建的本地临时表虽然"名字"相同,但是这些表之间相互并不存在任何关系;在SQLSERVER中,通过特别的命名机制保证本地临时表在数据库连接上的独立性,意思是你可以在不同的连接里使用相同的本地临时表名称。
全局临时表是用户在创建表的时候添加”##"前缀的表,其特点是所以数据库连接均可使用该全局临时表,当所有引用该临时表的数据库连接断开后自动删除。
全局临时表相比本地临时表,命名上就需要注意了,与本地临时表不同的是,全局临时表名不能重复。
临时表利用了数据库临时表空间,由数据库系统自动进行维护,因此节省了物理表空间。并且由于临时表空间一般利用虚拟内存,大大减少了硬盘的I/O次数,因此也提高了系统效率。
临时表在事务完毕或会话完毕数据库会自动清空,不必记得用完后删除数据。
Q、MySQL操作指令
DDL
数据库操作指令 查询数据库 show databases;
创建数据库
create database 数据库名称;(create database if not exists 数据库名称;—— 创建数据库,判断,如果不存在创建)
删除数据库 drop database 数据库名称;(drop database if not exists 数据库名称;—— 删除数据库,判断,如果存在删除)
使用数据库
查看当前使用的数据库:select database();
使用数据库:use 数据库名称;
表操作指令 查询表 查询当前数据库下所有表名称:show tables;
查询表结构:(describle)desc 表名称;/ show create table 表名;
创建表 create table 表名{
字段名1 数据类型1,
字段名2 数据类型2,
…
};
删除表 drop table 表名;(drop table if not exists 表名;—— 删除表,判断,如果存在删除)
修改表 修改表名:alter table 表名 rename to 新的表名;
添加一列:alter table 表名 add 列名 数据类型;
修改数据类型:alter table 表名 modify 列名 新数据类型;
修改列名和数据类型:alter table 表名 change 列名 新列名 新数据类型;
删除列:alter table 表名 drop 列名;
DML语句
添加数据 给指定列添加数据:insert into 表名(列名1,列名2,…) values(值1,值2,…);
给全部列添加数据:insert into 表名 values(值1,值2,…);
批量添加数据:insert into 表名(列名1,列名2,…) values(值1,值2,…),(值1,值2,…),(值1,值2,…)…;
insert into 表名 values(值1,值2,…),(值1,值2,…),(值1,值2,…)…;
修改数据 update 表名 set 列名1=值1,列名2=值2,… [where 条件];
注意:修改语句中不加条件,将修改所有数据。
删除数据 drop from 表名 [where 条件];
注意:删除语句中不加条件,将删除所有数据。
DQL语句
select 字段列表 from 表名列表 where 条件列表 group by 分组字段 having 分组后条件 order by 排序字段 limit 分页限定;
基础查询 查询多个字段:select 字段列表 from 表名;/select * from 表名(查询所有数据);
去除重复记录:select distinct字段列表 from 表名;
起别名:as (也可省略)
条件查询 select 字段列表 from 表名列表 where 条件列表;
排序查询 select 字段列表 from 表名列表 order by 排序字段名1 [排序方式1],排序字段名1 [排序方式1] …;
排序方式:ASC(升序排列,默认),DESC(降序排序)
注意:如果有多个排序条件,当前边的条件值一样时,才会根据第二条件进行排序。
聚合函数 概念:将一列数据作为一个整体,进行纵向计算。
聚合函数分类:
count(列名) 统计数量(不能统计值为null的数据)
max(列名) 最大值
min(列名) 最小值
sum(列名) 求和
avg(列名) 平均值
聚合函数语法:
select 聚合函数名(列名) from 表;
注意:null值不参与所有聚合函数运算。
分组查询 select 字段列表 from 表名列表 [where 分组前条件限定] group by 分组字段名 [having 分组后条件过滤] ;
注意:分组之后,查询的字段为聚合函数和分组字段,查询其他字段无任何意义 。
分页查询 select 字段列表 from 表名列表 limit 起始索引,查询条目数;
· 起始索引:从0开始
计算公式:起始索引 = (当前页码-1)* 每页显示的条数
事务相关
查看事务的默认提交方式 select @@autocommit; – 1 自动提交,0 手动提交
修改事务提交方式 set @@autocommit = 0;
注意:
①条件查询的条件列表
②where和having区别
· 执行时机不一样:where是分组之前进行限定,不满足where条件,则不参与分组,而having是分组之后对结果进行过滤。
· 可判断的条件不一样:where 不能对聚合函数进行判断,having可以。
执行顺序:where > 聚合函数 > having
Q、MyISAM 和 InnoDB 的区别?
InnoDB MyISAM Memory
支持事务 支持 不支持 不支持
支持外键 支持 不支持 不支持
支持行级锁 支持 不支持,支持表锁 不支持,支持表锁
Q、MySQL事务
Q1、什么是事务?
事务就是逻辑上的一组操作,要么都执行,要么都不执行。
Q2、事务的四大特性ACID
Q3、并发事务带来了哪些问题?
Q4、SQL的事务隔离级别有哪些?
Q、MySQL锁
Q1、InnoDB有哪几种行锁?
Q、MySQL性能优化
Q、MySQL索引
Q、索引类型
Q、日志
Q1、redo log 日志
Q2、binlog日志
Q3、两阶段提交
Q4、undo log 日志
Q、数据库:工作中用到的数据库你复述一下主要的列,如何使用的
emp.sql
B-TREE索引与HASH索引。
为什么要用索引?
· 使用索引后减少了存储引擎需要扫描的数据量,加快查询速度;
· 索引可以把随机I/O变为顺序I/O;
· 索引可以对所搜结果进行排序以避免使用磁盘临时表。
B-Tree更加适合范围查找的Sql语句;而HASH索引检索效率非常高,检索一次就可以定位,但不适用于区分度小的列上,如性别字段。
Q、数据库的左连接和右连接区别
Left Join左连接where只影响右表,Right Join右连接where只影响到左表。
select * from user1 Left Join user2 where user1.id = user2.id
左连接后的检索结果是显示user1的所有数据和user2中满足where 条件的数据。
内连接inner join…on :
inner join…on 是 SQL 中最重要、最常用的表连接形式,用于连接两个或者多个表中都存在满足条件的记录。
where子句中使用的连接语句,在数据库语言中,被称为隐式内连接。
inner join…on子句产生的连接称为显式内连接。(其他join参数也是显性连接)
但是隐式内连接where随着数据库语言的规范和发展,已经逐渐被淘汰,比较新的数据库语言基本上已经抛弃了隐性连接,全部采用显性连接了。
Q、内连接与外连接
内连接(inner join):也称等值连接
取出两张表中匹配到的数据,匹配不到的不保留。
外连接(outer join):
取出连接表中匹配到的数据,匹配不到的也会保留,其值用NULL替代。
Q、数据库搜索引擎介绍Innodb
ISAM
ISAM是一个数据表格管理方法,执行读取操作的速度很快,而且不占用大量的内存和存储资源。但是它不支持事务处理,也不能够容错。
MyISAM
MyISAM是MySQL的ISAM扩展格式和缺省的数据库引擎。MyISAM还使用一种表格锁定的机制,来优化多个并发的读写操作。
InnoDB(和Berkley DB)
InnoDB数据库引擎都是造就MySQL灵活性的技术的直接产品。尽管要比ISAM和MyISAM引擎慢很多,但是InnoDB包括了对事务处理和外来键的支持。
MyISAM与InnoDB的区别
InnoDB和MyISAM是许多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣,视具体应用而定。基本的差别为:MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持。MyISAM类型的表强调的是性能,其执行速度比InnoDB类型更快,但是不提供事务支持,而InnoDB提供事务支持以及外部键等高级数据库功能。
读多写少不需要事务,选MyISAM;要求可靠性、事务,选InnoDB。
Q、数据库事务的四大特性–ACID
事务:是一组操作的集合,这组操作,要么全部执行成功,要门全部执行失败。
原子性Atomicity:事务是最小的执行单位,要么全部执行,要么全部不执行;
一致性Consistency:执行事务前后,数据保持一致,多个事务读取的结果是相同的;
隔离性Isolation:并发访问数据时,一个用户事务不会被其他事务所干扰;
持久性Durability:事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
Q、脏读、重复读、幻读
脏读:一个事务读到另外一个事务还没有提交的数据。
不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。
幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了“幻影”。
Q1、解决幻读
MVCC加上间隙锁的方式
(1)在快照读读情况下,mysql通过mvcc来避免幻读。
(2)在当前读读情况下,mysql通过next-key来避免幻读。锁住某个条件下的数据不能更改。
Q2、mvcc是什么
MVCC(Mutil-Version Concurrency Control),就是多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
在Mysql的InnoDB引擎中就是指在已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于SELECT操作会访问版本链中的记录的过程。
这就使得别的事务可以修改这条记录,反正每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。
Q3、隔离级别
–查看事务隔离级别:
select @@transaction_isolation;
–设置事务隔离级别:
set [session | global] transaction isolation level {read uncommitted | read committed | repeatable read | serializable}
注:事务隔离级别越高,数据越安全,但是性能越低。
Q、CRUD基本操作
增C(create):insert into 表名[字段1,字段2,…] values(值1,值2,…);
如:insert into employee(id,name,gender) values(1,‘小明’,‘男’),(2,‘小红’,‘女’);
查R(retrieve):①select 字段1,字段2,… ()from 表名 where条件;
② select * from 表名 [where条件] group by 分组字段名 [having 分组过滤后条件];
如:select workaddress, count() address_count from emp where age<45 group by
workaddress having address_count>=3;
(where是分组之前进行过滤,不满足where不参与分组,having是分组之后对结果过滤)
③ 排序:select * from emp order by age asc,entrydate desc;(asc升序、默认,desc降序)
④ 分页:select * from emp limit 起始索引,查询记录数;((查询页码-1)*每页显示数 )
改U(update):update 表名 set 字段名1=值1,字段名2=值2,…,[where 条件];
如:update employee set name=‘小王’,gender=‘女’ where id = 1;
update employee set department = ‘开发’;
删D(delete):delete from 表名 [where 条件];
如:delete from employee where gender = ‘女’;(按条件删除)
delete from employee; (删除所有)
Q、存储过程
· 存储过程(Stored Procedure)是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。
· 存储过程是为了完成特定功能的SQL语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(需要时)来调用执行。
· 存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。
优点
· 存储过程可封装,并隐藏复杂的商业逻辑。
· 存储过程可以回传值,并可以接受参数。
· 存储过程无法使用 SELECT 指令来运行,因为它是子程序,与查看表,数据表或用户定义函数不同。
· 存储过程可以用在数据检验,强制实行商业逻辑等。
缺点
· 存储过程,往往定制化于特定的数据库上,因为支持的编程语言不同。当切换到其他厂商的数据库系统时,需要重写原有的存储过程。
· 存储过程的性能调校与撰写,受限于各种数据库系统。
Q、sql数据库中的 delete 与drop的区别
delete:
1、delete是DML,执行delete操作时,每次从表中删除一行,并且同时将该行的的删除操作记录在redo和undo表空间中以便进行回滚(rollback)和重做操作,但要注意表空间要足够大,需要手动提交(commit)操作才能生效,可以通过rollback撤消操作。
2、delete可根据条件删除表中满足条件的数据,如果不指定where子句,那么删除表中所有记录。
3、delete语句不影响表所占用的extent,高水线(high watermark)保持原位置不变。
drop:
1、drop是DDL,会隐式提交,所以,不能回滚,不会触发触发器。
2、drop语句删除表结构及所有数据,并将表所占用的空间全部释放。
3、drop语句将删除表的结构所依赖的约束,触发器,索引,依赖于该表的存储过程/函数将保留,但是变为invalid状态。
Q、数据库视图和表的区别是什么
1、视图是已经编译好的sql语句,表不是;
2、视图没有实际的物理记录,表有;
3、表是内容,视图是窗口;
4、表占用物理空间,视图不占用物理空间;
5、表是概念模式,视图是外模式;
6、表属于全局模式中的表,视图属于局部模式的表等等。
Q、触发器
定义
数据库触发器是一个与表相关联的、存储的PL/SQL程序。
应用场景
a. 复杂的安全检查
b. 数据的确认
c. 数据库的审计
d. 数据库的备份与同步
分类
a. 语句级触发器:针对的是表
b. 行级触发器:针对的是行【for each row】
Q、聚合函数有哪些
聚合函数是对一组值执行计算并返回单一的值的函数,它经常与SELECT语句的GROUP BY子句一同使用
Q、如何定位并优化慢查询 SQL?
慢查询,顾名思义就是很慢的查询。MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
方法:
1.根据慢日志定位慢查询 sql
2.使用 explain 等工具分析 sql 执行计划
3.修改 sql 或者尽量让 sql 走索引
Q、查询语句的执行顺序
Q、Redis 数据库
Redis是一个基于内存的key-value结构数据库。
Q1、Redis基础
Redis是一个开源的内存中的键值对(key-value)数据结构存储系统,可以用作数据库、缓存和消息中间件。Redis属于NoSql数据库。
Q2、Redis应用场景
①缓存;②任务队列;③消息队列;④分布式锁。
Q3、Redis数据类型
①字符串String;②哈希Hash;③列表List(常用于任务队列);④集合Set;⑤有序集合sorted Set。
Redis中所说的数据类型主要是针对值value数据类型的,键key的类型都是字符串类型。
Q4、Redis常用命令
具体参见链接Redis中文网
字符串相关 SET key value 设置指定key的值
GET key 获取指定key的值
SETEX key seconds value 设置指定key的值,并将key的过期时间设置为seconds秒(常用于验证码)
SETNX key value 只有在key不存在时设置key的值(常用于分布式锁)
哈希相关 HSET key field value 将哈希表key中的字段field的值设为value
HGET key field 获取存储在哈希表中指定字段的值
HDEL key field 删除存储在哈希表中的指定字段
HKEYS key 获取哈希表中所有字段
HVALS key 获取哈希表中所有值
HGETALL key 获取在哈希表中指定key的所有字段和值
列表相关(插入在头部,获取在尾部)
LPUSH key value1 [value2] 将一个或多个值插入到列表头部
LRANGE key start stop 获取列表指定范围内的元素
RPOP key 移除并获取列表最后一个元素
LLEN key 获取列表长度
BRPOP key1 [key2] timeout 移出并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
无序集合 SADD key member1 [member2] 向集合中添加一个或多个成员
SMEMBERS key 返回集合中的所有成员
SCARD key 获取集合的成员数
SINTER key1 [key2] 返回所有给定集合的交集
SUNION key1 [key2] 返回所有给定集合的并集
SDIFF key1 [key2] 返回所有给定集合的差集
SREM key member1 [member2] 移除集合中一个或多个成员
有序集合 ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
ZINCRBY key increment member 有序集合中对指定成员的分数加上增量increment
ZREM key member1 [member2] 移除有序集合中的一个或多个成员
通用命令 KEYS pattern 查找所有符合给定模式(pattern)的key
EXISTS key 检查给定key是否存在
TYPE key 返回key所存储的值的类型
TTL key 返回给定key的剩余生存时间(TTL,time to live),以秒为单位
DEL key 该命令用于在key存在时删除key
Q5、Redis为什么速度快?
①Redis 基于内存,内存的访问速度是磁盘的上千倍;
②Redis基于Reactor模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和IO多路复用;
③Redis内置了多种优化过后的数据结构实现,性能非常高。
Q6、Redis 和 Memcached 的区别和共同点
共同点:
①都是基于内存的数据库,一般都用来当做缓存使用。
②都有过期策略。
③两者的性能都非常高。
区别:
①Redis支持更丰富的数据类型(支持更复杂的应用场景)。Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储;Memcached 只支持最简单的 k/v 数据类型。
②Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memcached把数据全部存在内存之中。
③Redis有灾难恢复机制。因为可以把缓存中的数据持久化到磁盘上。
④Redis在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached在服务器内存使用完之后,就会直接报异常。
⑤Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是Redis目前是原生支持cluster模式的。
⑥Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。(Redis 6.0引入了多线程IO)
⑦Redis支持发布订阅模型、Lua 脚本、事务等功能,而Memcached不支持。并且,Redis支持更多的编程语言。
⑧Memcached过期数据的删除策略只用了惰性删除,而Redis同时使用了惰性删除与定期删除。
Q、网络概念
· 计算机网络体系结构是指计算机网络层次结构和各层协议的集合。
· 物理层:在物理媒体上为数据端设备透明的传输原始比特流,为数据链路层提供数据传输服务。
· 数据链路层:采用差错控制与流量控制方法,使有差错的物理线路变成无差错的数据链路。(帧同步、寻址)
· 网络层:对分组进行路由选择,并实现流量控制、拥塞控制、差错控制和网际互联等。
· 传输层:为端到端连接提供可靠的传输服务,并提供流量控制、差错控制、数据传输管理等服务。
· 会话层:负责维护两个对话主机之间连接的建立、管理和终止,以及数据的交换。
· 表示层:负责通信系统之间的数据格式变换、数据加密与解密、数据压缩与恢复。
· 应用层:实现协同工作的应用程序之间的通信过程,通过不同应用软件提供多种服务。
Q、HTTP协议,TCP协议,UDP协议的区别跟特点,还有不足
· HTTP 协议:Hyper Text Transfer Protocol(超文本传输协议),是用于从万维网(WWW:World Wide Web)服务器传输超文本到本地浏览器的传送协议;(万维网服务器->本地浏览器)
HTTP是基于TCP的可靠通信,应用层,基于客户端与服务端的通信。协议对于事务处理没有记忆能力,缺少状态意味着如果后续处理需要前面的信息,则它必须重传。
TCP协议:Transmission Control Protocol(传输控制协议),是一种面向连接的、可靠的传输层协议,并且支持全双工通信;传输速度慢。
UDP 协议:User Datagram Protocol(用户数据报协议),一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务;传输速度快。
Q、网络协议
· ARP
地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。
主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
Q、计网中路由器和交换机的区别
· 交换机:过滤,转发(依靠 MAC 地址);路由器:寻址,转发(依靠 IP 地址)。
· 交换机用于连接局域网,数据包在局域网内网转发;路由器用于连接局域网和外网,数据包可以在不同局域网转发。
· 交换机工作于数据链路层;路由器工作于网络层。
· 交换机负责具体的数据包传输,路由器不负责包的实际传输,路由器只封装好要传输的数据包,然后交给交换机去传输(不一定是交换机,可能是其他传输技术)。
· 交换机没有MAC地址和IP地址,路由器有MAC地址和IP地址(指纯碎的交换机和路由器,三层交换机是可以有IP地址的,路由器也有内置交换机功能的)
路由器内有一份路由表,里面有它的寻址信息(就像是一张地图),它收到网络层的数据报后,会根据路由表和选路算法将数据报转发到下一站(可能是路由器、交换机、目的主机)
交换机内有一张MAC表,里面存放着和它相连的所有设备的MAC地址,它会根据收到的数据帧的首部信息内的目的MAC地址在自己的表中查找,如果有就转发,如果没有就放弃
其他:
集线器和交换机的区别
集线器和交换机都是工作在TCP/IP协议的最后一层,数据链路(物理层),都是连接多个设备形成局域网的。
集线器会把接收到的数据包每次都广播给局域网局域网的所有计算机,而交换机只有首次在MAC地址表找不到记录才广播,其他时候是直接单独发送给对应MAC地址的计算机。交换机可以说是集线器的升级改良版,在集线器的基础上多了MAC地址表,可以分割冲突域,更加智能化。
集线器的数据传输方式是广播方式,而交换机的数据传输是有目的的,数据只对目的节点发送,只是在自己的MAC地址表中找不到的情况下第一次使用广播方式发送,然后因为交换机具有MAC地址学习功能,第二次以后就不再是广播发送了,又是有目的的发送。这样的好处是数据传输效率提高,不会出现广播风暴,在安全性方面也不会出现其它节点侦听的现象
网桥和交换机的区别
交换机工作时,实际上允许许多组端口间的通道同时工作。所以,交换机的功能体现出不仅仅是一个网桥的功能,而是多个网桥功能的集合。即网桥一般分有两个输出输入端口,而交换机具有高密度的端口。所以一般的交换机,网桥就有桥接作用。
网桥主要由软件实现,交换机主要由硬件实现
Q、TCP和UDP主要有何区别?适用的场合?
· TCP(传输控制协议)特点:TCP协议是一种面向连接的、可靠的传输层协议,并且支持全双工通信,支持流传输,还有流量控制和拥塞控制。传输速度慢。
· 适用场合:适用于注重准确交付的场合。
· UDP(用户数据报协议)特点:UDP协议是一种无连接的、不可靠的传输层协议,提供有限的差错检验功能,传输数据时不需要跟目的节点建立连接,不支持发送数据流。传输速度快。
· 适用场合:适用于注重快速交付的场合。
Q、TCP如何保证传输的可靠性?(★)
Q、TCP如何实现流量控制
Q、TCP的拥塞控制是怎么实现的
Q、什么是粘包/拆包
一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。
TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。
Q1、解决办法
1、消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
3、将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段
3、自定义协议,来实现TCP的粘包/拆包问题
分别画出OSI和TCP/IP的分层体系结构,并进行简单的对比说明。
相同点:
都采取分层的体系结构,都是基于独立的协议栈的概念,都可以解决异构网络的互联。(以传输层为界,其上层都依赖传输层提供端到端的传输服务)(异构网络是由不同制造商生产的计算机,网络设备和系统组成的网络)
不同点:
(1)OSI精确定义了服务、协议和接口的概念,TCP/IP在概念上没有明确区别;
(2)OSI模型发明早于协议出现,通用性良好,但TCP/IP协议早于模型出现;
(3)在通信上,OSI非常重视连接通信,而TCP/IP一开始就重视数据报通信。
说明TCP/IP 参考模型与OSI/RM 相比有何优点和不足。
优点:
1)简单、灵活、易于实现;
2)充分考虑不同用户的需求。
缺点:
1)没有明显地区分出服务、协议和接口的概念;
2)网络接口层只是个接口,不区分物理层和数据链路层。
Q、从浏览器输入URL到页面展示发生了什么?(★★★★★)
我们在浏览器输入百度网址后,①首先使用DNS域名解析解析输入域名,将其转换为对应的IP地址(首先会查找DNS缓存中是否存在搜索网址对应的IP地址,存在直接返回,反之继续解析);②浏览器获取到IP地址后,使用一个随机端口向服务器端口发起TCP连接请求,通过TCP 3次握手,建立TCP连接;③连接建立后,浏览器发送HTTP GET 请求(请求HTML文件);④服务器收到请求后,返回一个HTTP响应报文;⑤浏览器接收到服务器发送的HTTP响应报文后(实际为一个HTML文件),开始显示HTML;⑥TCP 4次挥手断开连接。
Q11、HTTP三次握手和四次挥手
· 三次握手:
三次握手其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。
第一次:客户端向服务器的TCP发送一个连接请求报文段;
第二次:服务器的TCP收到连接请求报文段后,如同意连接则向客户端发回确认,并为该TCP连接分配TCP缓存和变量。
第三次:当客户端收到确认报文段后,还要向服务器给出确认,并且也要给该连接分配缓存和变量。
· 四次挥手:
关闭连接时
第一次:客户端首先发出关闭连接请求;
第二次:当服务端收到FIN报文时,发出ACK确认报文,服务端进入CLOSE_WAIT(关闭等待)状态;
第三次:服务端发送完毕后,也要给客户端发送断开连接请求;
第四次:客户端收到FIN报文后,发送ACK确认报文,客户端进入TIME_WAIT(时间等待)状态。。
为什么TCP连接要用三次握手与四次挥手?
因为 tcp 是全双工。三次握手 是为了建立可靠的连接。这主要是为了防止两次握手情况下,已失效的连接请求报文段突然又传到服务器端而产生错误。
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
四次挥手
TCP是全双工的通信机制,每个方向必须单独进行关闭。当一端完成它的数据发送任务后就可以发送一个FIN报文来终止这个方向的数据发送;当另一端收到这个FIN报文后,释放报文段并发出确认。
Q、WebSocket的介绍
Java中Socket是什么?
· Socket 也称作"套接字",用于描述 IP 地址和端口,是一个通信链的句柄,是应用层与传输层之间的桥梁;
· 应用程序可以通过 Socket 向网络发出请求或应答网络请求;
· 网络应用程序位于应用层,TCP 和 UDP 属于传输层协议,在应用层和传输层之间,使用 Socket 来进行连接。
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 可以让客户端和服务端进行双向通信,允许服务端主动向客户端推送数据。
Q、全双工
通信允许数据在两个方向上同时传输,在能力上相当于两个单工通信方式的结合。全双工指可以同时进行信号的双向传输。
全双工:例如使用的手机就是全双工,在同一时刻两个用户可以同时给对方传送数据
半双工:例如使用的对讲机,当A方按住通话按钮才可以向B方传送数据,B方也是,在同一时刻只有一个用户能够传送数据(A/用户都可以传递信息,但是不能够同时传递)
单工:例如看电视时,只能接收对方发送的信息,不能够给对方传递信息;
Q、HTTP 状态码有哪些?
Q、HTTP 和 HTTPS 有什么区别?
HTTP HTTPS
端口号 80 443
URL前缀 http:// https://
安全性 运行在TCP上,明文传输 运行在TCP和SSL/TLS上,加密传输(对称加密)
耗费资源 低 高
Q1、HTTP 1.0 和 HTTP 1.1 的区别?
短连接:客户端和服务端通信一次,建立一次连接,用完就断开;
长连接:客户端和服务端通信完不断开,继续使用建立的连接。
HTTP 1.0 HTTP 1.1
连接方式 短连接 长连接
状态响应码 种类少 种类多
缓存处理 Header作为缓存判断标准 有更多缓存控制策略
带宽和网络连接 不允许只请求资源的一部分 加入range域,允许请求资源的一部分
Host头处理 无 加入了Host字段
Q2、HTTP 是不保存状态的协议,如何保存用户状态?
Q、URI 和 URL 的区别是什么?
URI:人的身份证号,唯一标识一个人;
URL:人的家庭住址,通过URL可以找到这个人。
Q、Linux:文件复制、目录复制命令
文件复制:cp + 文件名 + 目标路径
目录复制:cp -r + 源路径 + 目标路径
Q、Linux目录结构
bin(binaries):存放二进制可执行文件。
sbin(super user binaries):存放二进制可执行文件,只有root才能访问etc(etcetera存放系统配置文件)。
usr(unix shared resources):用于存放共享的系统资源,系统应用程序。
home:存放用户文件的根目录。
root:超级用户目录。
dev(devices):用于存放设备文件。
library:存放跟文件系统中的程序运行所需要的共享库及内核模块。
mnt (mount):系统管理员安装临时坟文件系统的安装点。
boot:存放用于系统引导时使用的各种文件。
tmp(temporary):用于存放各种临时文件。
var(variable):用于存放运行时需要改变数据的文件。
Q、Linux常用指令
Linux下查看ip地址:ifconfig -a (Windows下:ipconfig)
ps Linux下的进程查看命令(类似Windows下的任务管理器)
ps -ef:可以查看当前运行进程的详细信息
指令 pwd 查看当前所在的目录
touch [fileName] 如果文件不存在,创建文件
文件目录操作 ls [-al] [dir]
(ls -l 可简写为 ll) 用于显示指定目录下的内容
-a:显示所有文件及目录(.开头的隐藏文件也会列出)
-l:除文件名称外,同时将文件型态、权限、拥有者、文件大小等信息详细列出
cd [dirName] 用于切换当前工作目录,及进入指定目录
~表示用户的home目录
.表示目前所在的目录
…表示目前目录位置的上级目录
cat [-n] fileName 用于显示文件内容
-n:由1开始对所有输出的行数编号
more fileName 以分页的形式显示文件内容
回车键:向下滚动一行
空格键:向下滚动一屏
b:返回上一屏
q或Ctrl + C:退出more
tail [-f] fileName 查看文件末尾的内容
-f:动态读取文件末尾内容并显示,通常用于日志文件的内容输出
mkdir [-p] dirName 用于创建目录
-p:确保目录名称存在,不存在的就创建一个。通过该选项,可以实现多层目录同时创建。
rmdir [-p] dirName 用于删除空目录
-p:当子目录被删除后使父目录为空目录的话,则一并删除
rm [-rf] name 用于删除文件或目录
-r:将目录及目录中所有的文件(目录)逐一删除
-f:无需确认,直接删除
拷贝移动 cp [-r] source dest 用于复制文件或目录
-r:如果复制的是目录需要使用此选项,此时将复制该目录下所有的子目录和文件
mv source dest 用于为文件或目录改名、或将文件或目录移动到其他位置
打包压缩 tar [-zcxvf] fileName [files]
用于对文件进行打包、解包、压缩、解压
-z:z代表的是gzip,通过gzip命令处理文件,gzip可以对文件压缩或者解压
-c:c代表的是create,即创建新的包文件
-x:x代表的是extract,实现从包文件中还原文件
-v:v代表的是verbose,显示命令的执行过程
-f:f代表的是file,用于指定包文件的名称
包文件后缀为 .tar 表示只是完成了打包,并没有压缩
包文件后缀为 .tar.gz 表示打包的同时还进行了压缩
如:
tar -cvf 文件名:仅仅打包文件
tar -zcvf 文件名:打包的同时压缩文件
tar -xvf 文件名:解包对应的文件
tar -zxvf 文件名:解压
文本编辑 vi/vim
vi fileName vi命令是Linux系统提供的一个文本编辑工具,可以对文件内容进行编辑
说明:vim是从vi发展来的,可以在编辑文件时对文本内容进行着色;使用vim命令,需要安装对应的包(yum install vim)。
Vim fileName 使用vim命令编辑文件,如果指定的文件存在,则直接打开;反之新建文件。
vim进行文本编辑时分3种模式:命令模式、插入模式、底行模式。
命令模式:vim打开文件,默认为命令模式,另两种模式需要先进入命令模式,才能切换。
插入模式:命令模式下按(i,a,o)任一个,进入插入模式;插入模式下按ESC回到命令模式。
底行模式:输入wq(保存并退出)、q!(不保存退出)、set nu(显示行号)
查找 find dirName -option fileName 在指定目录查找文件
grep word fileName 从指定文件中查找指定的文本内容
其他 netstat -anp | grep 8080 查看端口占用情况
netstat -anp | grep 8080 查看指定端口
netstat -anp 或 netstat -tln 查看所有端口
ip addr 查看ip地址
set nu linux 命令行模式下输入,可以显示行号
1、项目
Q、软件开发流程
Q、角色分工
Q、项目中如何防止宕机
运维可以借助管理软件,对服务器进行日常维护及优化工作,有效降低宕机概率;
也可以通过宕机监控和警报等系统有效降低宕机所带来的不利影响。
运维可以通过对硬件、软件进行排查,提前规避可能出现的问题。
服务器宕机原因:① 运行环境出现问题 ②服务器无法承受负载 ③性能问题。
2、框架相关
Q、SSM中的基本层
Q1、entity层 = domain层 = model层 = POJO层
实体层,用于存放实体类,与数据库中的属性值基本保持一致,实现set和get、toString()等。
区别:
【entity】实体类字段必须和数据库字段一样,
【domain】表示一个对象模块,字段对应即可,命名可以不一样
【Model】是为页面提供数据和数据校验的
【POJO】Plain Ordinary Java Object,通指没有使用Entity Beans的普通java对象,可以把POJO作为支持业务逻辑的协助类。
【JavaBean】更多的是一种规范,也即包含一组set和get方法的Java对象。
Q2、dao层(interface)CRUD ↓↓
数据访问对象层,Data Access Object, 对数据库进行数据持久化操作,对某个表进行增删查改操作,和数据库中的某一张表对应,而且其中也只是封装了增删查改的方法
Q3、service层 ==>业务层
service层即为业务逻辑层,主要是针对具体问题的操作,间接与数据库打交道(提供操作数据库的方法)。要做这一层的话,要先设计接口,再实现类。
Q4、controller层
控制层,负责具体模块的业务流程控制、负责请求转发,接收前端页面传过来的参数,传给service处理进行业务操作,再将处理结果返回到前端。
Q5、dto层
数据传输对象层,Data Transfer Object,用于传输对象,在前后端(界面,数据库)之间进行数据传递。
1、临时存储界面提交的数据,并将数据通过jdbc加入到数据库中
2、取出数据库中数据,临时存储到对象,并传输到界面展示。
Q6、Mapper
Mapper 可以极大的方便开发人员进行ORM(对象关系映射,Object Relational Mapping),提供极其方便的单表增删改查,让mybatis的开发更方便。
只需自己在PersonMapper.java接口添加方法,对应在方法中写约束条件就可以了。
Q、Spring Boot
Q1、说说对Spring Boot的理解
· Spring Boot于 2013 年伴随Spring 4.0而生,是用来简化新Spring应用的配置以及开发过程。 可以自动配置 Spring 的各种组件,不依赖代码生成和 XML 配置文件。
· Spring Boot默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了很多的框架,同时可以容易地与SpringJDBC、SpringORM等Spring生态系统集成。
· 其实就是简单、快速、方便,一个application.xml就可以代替之前的*.properties和*.xml配置文件,仅仅只需要非常少的几个配置就可以迅速方便的搭建起来一套web项目或者是构建一个微服务!
不足:
Spring Boot作为一个入门级的微框架,缺少服务注册等外围方案,没有配套的安全管控方案等。(缺少一些细节)。
Q2、为什么要用SpringBoot?
在使用Spring框架进行开发的过程中,需要配置很多Spring框架包的依赖,如spring-core、spring
bean、spring-context等,而这些配置通常都是重复添加的,而且需要做很多框架使用及环境参数的重
复配置,如开启注解、配置日志等。Spring Boot致力于弱化这些不必要的操作,提供默认配置,当然这
些默认配置是可以按需修改的,快速搭建、开发和运行Spring应用。
以下是使用SpringBoot的一些好处:
自动配置,使用基于类路径和应用程序上下文的智能默认值,当然也可以根据需要重写它们以满足
开发人员的需求。
创建Spring Boot Starter 项目时,可以选择选择需要的功能,Spring Boot将为你管理依赖关系。
SpringBoot项目可以打包成jar文件。可以使用Java-jar命令从命令行将应用程序作为独立的Java应
用程序运行。
在开发web应用程序时,springboot会配置一个嵌入式Tomcat服务器,以便它可以作为独立的应
用程序运行。(Tomcat是默认的,当然你也可以配置Jetty或Undertow)
SpringBoot包括许多有用的非功能特性(例如安全和健康检查)。
Q3、Spring Boot 的核心注解是哪个?它主要由哪几个注解
组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下
3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源
自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
Q、Spring
Q1、说说对Spring的理解
Spring包括如SpringFramework、SpringData、SpringBoot、SpringCloud等很多项目,其中SpringFramework这个项目,是一个开源的Java/JavaEE全功能栈(?)的应用程序,SpringFramework提供了一个简易的开发方式,这种开发方式可以避免使用那些可能致使底层代码变得繁杂混乱的大量的属性文件和帮助类。
Spring功能:
简化开发:降低开发的复杂性;
框架整合:高效整合其他技术,提高开发与运行效率。
Q2、Spring的核心概念:IoC、AOP
① IoC控制反转 (Inversion of Control) :充分解耦,使用对象时,由主动new产生出对象转换为由spring容器提供对象,此过程中,对象创建控制权由程序转移到外部。在 Spring 中管理对象及其依赖关系是通过 Spring 的 IoC 容器实现的。
② Bean管理:IoC容器负责对象的创建、初始化等操作,被创建的或者被管理的对象在IoC容器中统称为Bean。
获取bean的方式(getBean()):使用bean id名称获取;使用bean类型获取;使用bean id名称和类型获取。
③ DI依赖注入(Dependency Injection):在容器中建立bean与bean之间的依赖关系的过程称为依赖注入。
④ AOP面向切面编程(Aspect Oriented Programming):一种编程范式,是将代码中重复的部分抽取出来,在需要执行的时候使用动态代理技术,在不修改源码的基础上对方法进行增强,是Spring的一种无侵入式的理念。
Q、AOP通知类型
①前置通知
②后置通知
③环绕通知
④返回后通知
⑤抛出异常后通知
Q、Spring的自动装配方式?
Q1、什么是自动装配?
IOC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配。
Q2、Spring提供了哪几种自动装配类型?
· byName:根据名称进行自动匹配。
· byType:根据类型进行自动匹配。
· constructor:与 byType 类似, 只不过它是针对构造函数注入而言的。
· autodetect:根据 Bean 的自省机制决定采用 byType 还是 constructor 进行自动装配,如果 Bean 提供了默认的构造函数,则采用 byType,否则采用 constructor。
注意:自动装配bean的优先级是最低的。
Q、Spring中的单例bean会存在线程安全问题么?
Spring 中的单例 Bean不是线程安全的。
因为单例 Bean,是全局只有一个 Bean,所有线程共享。如果说单例 Bean,是一个无状态的,也就是线程中的操作不会对 Bean 中的成员变量执行查询以外的操作,那么这个单例 Bean 是线程安全的。比如 Spring mvc 的 Controller、Service、Dao 等,这些 Bean 大多是无状态的,只关注于方法本身。
假如这个 Bean 是有状态的,也就是会对 Bean 中的成员变量进行写操作,那么可能就存在线程安全的问题。
Q1、单例 Bean 线程安全问题怎么解决呢?
①将 Bean 定义为多例
这样每一个线程请求过来都会创建一个新的 Bean,但是不利于容器管理 Bean。
②在 Bean 对象中尽量避免定义可变的成员变量
③将 Bean 中的成员变量保存在 ThreadLocal 中 ★。
我们知道 ThredLoca 能保证多线程下变量的隔离,可以在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 里,这是推荐的一种方式。
Q、为什么要用三级缓存?二级缓存行不行?
不行,主要是为了生成代理对象。如果是没有代理的情况下,使用二级缓存解决循环依赖也是 OK 的。但是如果存在代理,三级没有问题,二级就不行了。
因为三级缓存中放的是生成具体对象的匿名内部类,获取 Object 的时候,它可以生成代理对象,也可以返回普通对象。使用三级缓存主要是为了保证不管什么时候使用的都是一个对象。
假设只有二级缓存的情况,往二级缓存中放的显示一个普通的 Bean 对象,Bean 初始化过程中,通过 BeanPostProcessor 去生成代理对象之后,覆盖掉二级缓存中的普通 Bean 对象,那么可能就导致取到的 Bean 对象不一致了。
Q、@Autowired的实现原理?
实现@Autowired 的关键是:AutowiredAnnotationBeanPostProcessor
在 Bean 的初始化阶段,会通过 Bean 后置处理器来进行一些前置和后置的处理。
实现 @Autowired 的功能,也是通过后置处理器来完成的。这个后置处理器就是 AutowiredAnnotationBeanPostProcessor。
Spring 在创建 bean 的过程中,最终会调用到 doCreateBean()方法,在 doCreateBean()方法中会调用 populateBean()方法,来为 bean 进行属性填充,完成自动装配等工作。
在 populateBean()方法中一共调用了两次后置处理器,第一次是为了判断是否需要属性填充,如果不需要进行属性填充,那么就会直接进行 return,如果需要进行属性填充,那么方法就会继续向下执行,后面会进行第二次后置处理器的调用,这个时候,就会调用到 AutowiredAnnotationBeanPostProcessor 的 postProcessPropertyValues()方法,在该方法中就会进行@Autowired 注解的解析,然后实现自动装配。
Q、Spring MVC 执行流程
SpringMVC 的执行流程如下
1、用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 DispatcherServlet(前端控制器);
2、由 DispatcherServlet 请求一个或多个 HandlerMapping(处理器映射器),并返回一个执行链(HandlerExecutionChain)。
3、DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器);
4、HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller);
5、Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC的底层对象,包括 Model 数据模型和 View 视图信息);
6、HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet ;
7、DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析;
8、ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet;
9、DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图);
10、视图负责将结果显示到浏览器(客户端)。
Q、SpringBoot与SpringMVC的区别
形式上:SpringBoot是一个自动化配置的工具;SpringMVC是一个web框架
在搭建项目时:SpringMVC需要手动配置xml文件,同时需要配置Tomcat服务器。而SpringBoot采用约定大于配置的方式,进行自动装配,同时内置服务器,打开就可以直接用。
Q、依赖注入
· 在容器中建立bean与bean之间依赖关系的过程称为依赖注入,在容器实例化对象时主动将对象依赖的类注入给它。
· IoC的实现方式主要就是依赖注入:假设一个 Car 类需要一个 Engine 的对象,那么一般需要手动 new 一个 Engine,利用 IoC 就只需要定义一个私有的 Engine 类型的成员变量,容器会在运行时自动创建一个 Engine 的实例对象并将引用自动注入给成员变量。
Q1、依赖注入的相关注解
@Autowired:自动按类型注入,如果有多个匹配则按照指定 Bean 的 id 查找,查找不到会报错;
@Qualifier:在自动按照类型注入的基础上再按照 Bean 的 id 注入,给变量注入时必须搭配@Autowired,给方法注入时可单独使用;
@Resource:直接按照 Bean 的 id 注入,只能注入 Bean 类型;
@Value:用于注入基本数据类型和 String 类型。
Q2、依赖注入的实现方法
· 构造方法注入: IoC Service Provider 会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。这种方法的优点是在对象构造完成后就处于就绪状态,可以马上使用。缺点是当依赖对象较多时,构造方法的参数列表会比较长,构造方法无法被继承,无法设置默认值。对于非必需的依赖处理可能需要引入多个构造方法,参数数量的变动可能会造成维护的困难。
· setter 方法注入: 当前对象只需要为其依赖对象对应的属性添加 setter 方法,就可以通过 setter 方法将依赖对象注入到被依赖对象中。setter 方法注入在描述性上要比构造方法注入强,并且可以被继承,允许设置默认值。缺点是无法在对象构造完成后马上进入就绪状态。
· 接口注入: 必须实现某个接口,接口提供方法来为其注入依赖对象。使用少,因为它强制要求被注入对象实现不必要接口,侵入性强。
向一个类中传递数据的方式有几种?
① 普通方法:在bean中定义引用类型属性并提供set方法,配置中使用property标签的value属性注入简单类型数据,使用ref属性注入引用类型对象。
② 构造方法。
Q、Bean相关
Q1、bean实例化的4种方式?
①构造函数(默认,使用反射机制)
②静态工厂
③实例工厂
④工厂对象(实现FactoryBean接口,指定泛型)
Q2、Bean 的生命周期
· 初始化:在 IoC 容器的初始化过程中会对 Bean 定义,加载配置并解析,最后将解析的 Bean 信息放在一 个 HashMap 集合中。
· 使用:当 IoC 容器初始化完成后,会对 Bean 实例进行创建和依赖注入,注入对象依赖的各种属性值。经过这 一系列初始化操作后 Bean 达到可用状态,接下来就可以使用 Bean 了。
· 销毁:当使用完成后会调用 destroy 方法进行销毁,此时也可以指定自定义的销毁方法,最终 Bean 被销毁且 从容器中移除。
XML 方式通过配置 bean 标签中的 init-Method 和 destory-Method 指定自定义初始化和销毁方法。
注解方式通过配置 @Bean 注解中的 init-Method 和 destory-Method 指定自定义初始化和销毁方法。
Q3、获取bean的方式
①使用bean名称
②使用bean名称,同时指定类型
③使用bean类型
Q、ApplicationContext 接口和BeanFactory 接口有什么区别 ?
① ApplicationContext 接口继承BeanFactory接口,Spring核心工厂是BeanFactory,BeanFactory采取延迟加载,第一次getBean时才会初始化Bean,ApplicationContext是会在加载配置文件时初始化Bean;
② ApplicationContext是对BeanFactory扩展,它可以进行国际化处理、事件传递和bean自动装配以及各种不同应用层的Context实现 ;
开发中基本都在使用ApplicationContext,web项目使用WebApplicationContext,很少用到BeanFactory。
Q、SpringMVC
Q、Mybatis
一款优秀的持久层框架, 用于简化JDBC开发。
持久层:负责将数据保存到数据库的代码。(表现层(页面展示)、业务层(逻辑处理)、持久层(数据持久化))
Q1、Mybatis 的优缺点
优点
相比 JDBC 减少了大量代码量,减少冗余代码。
使用灵活,SQL 语句写在 XML 里,从程序代码中彻底分离,降低了耦合度,便于管理。
提供 XML 标签,支持编写动态 SQL 语句。
提供映射标签,支持对象与数据库的 ORM 字段映射关系。
缺点
SQL 语句编写工作量较大,尤其是字段和关联表多时。
SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库
Q2、MyBatis和JDBC的区别
JDBC:硬编码(注册驱动,获取连接;SQL语句)、操作繁琐(手动设置参数、手动封装结果集)
MyBatis:使用配置文件解决硬编码,参数和结果集封装自动完成。
MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。
Q3、#{} 和 ${} 的区别
① #{}是SQL中的参数占位符,MyBatis会将其替换为 ?,可以防止SQL注入;
② KaTeX parse error: Expected 'EOF', got '#' at position 66: …使用时机:参数传递的时候使用:#̲{};表名或者列名不固定的情况…{}会存在SQL注入问题。
Q4、动态SQL?
SQL语句会随着用户的输入或外部条件的变化而变化,我们称为动态SQL。
动态SQL可以使用if、choose(when、otherwise)、trim(where、set)、foreach等语句。
Q、Mybatis Plus
Mybatis-Plus是一个Mybatis的增强工具,只是在Mybatis的基础上做了增强却不做改变,MyBatis-Plus支持所有Mybatis原生的特性,所以引入Mybatis-Plus不会对现有的Mybatis构架产生任何影响。
优点
1、依赖少:仅仅依赖 Mybatis 以及 Mybatis-Spring;
2、损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作;
3、预防Sql注入:内置 Sql 注入剥离器,有效预防Sql注入攻击;
4、通用的CRUD操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操 作,更有强大的条件构造器,满足各类使用需求;
5、多种主键策略:支持多达4种主键策略(内含分布式唯一ID生成器),可自由配置,完美解决主键问题;
6、内置分页插件:基于 Mybatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普 通List查询。
7、内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能有效解决慢查询 。
Q、Mybatis 和 Mybatis Plus 的区别
MyBatis:
· 所有SQL语句全部自己写。
· 手动解析实体关系映射转换为MyBatis内部对象注入容器。
· 不支持Lambda形式调用。
Mybatis Plus:
· 强大的条件构造器,满足各类使用需求。
· 内置的Mapper,通用的Service,少量配置即可实现单表大部分CRUD操作。
· 支持Lambda形式调用。
· 提供了基本的CRUD功能,不用编写SQL语句。
· 自动解析实体关系映射转换为MyBatis内部对象注入容器。
Q、Spring Cache
Q1、Spring Cache介绍
Spring Cache是一个框架,实现了基于注解的缓存功能,实现时需要实现CacheManager接口。CacheManager是Spring提供的各种缓存技术抽象接口。针对不同的缓存技术需要实现不同的CacheManager:
Q2、Spring Cache注解
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
Q、Sharding-JDBC
Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供额外的服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。在程序中用来实现数据库读写分离。
Q、Redis
Redis 就是⼀个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被⼴泛应用于缓存方向。
特点
1、基于键值对的数据结构服务器
值不仅可以是字符串,还可以是其他数据结构,可以应用于多种场景,也可以提高效率。主要提供五种数据结 构:字符串string、哈希hash、列表list、集合set、有序集合zset,在字符串基础上演变出 Bitmaps 和 HyperLogLog,Redis 3.2加入了有关 GEO 地理信息定位的功能。
2、简单稳定
① 源码很少,早期只有 2 万行,在 3.0 版本添加了集群特性,增加到了 5 万行,相对于 NoSQL 数据库
代码量少很多。② 单线程模型,服务端处理模型更简单,客户端开发更简单。③ 不依赖底层操作系统的类库, 自己实现了事件处理的相关功能。虽然简单,但也稳定。
3、客户端语言多
Java、PHP、Python、C、C++ 等。
4、数据持久化
数据放在内存中不安全,一旦发生断电或故障就可能丢失,Redis 提供了两种持久化方式 RDB 和 AOF
将内存的数据保存到硬盘。
RDB:以快照的形式保存到磁盘上;
AOF 持久化:采用日志的形式来记录每个写操作,追加到文件中。
Q、GET和POST两种基本请求方法的区别
· GET参数通过URL传递,POST放在Request body中;
· GET比POST更不安全,因为参数直接暴露在URL上,所以最好不要用来传递敏感信息;
· GET产生一个TCP数据包,POST产生两个TCP数据包。
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据);
· GET请求只能进行url编码,而POST支持多种编码方式;
· GET请求在URL中传送的参数是有长度限制的,而POST没有。
HttpServletRequest:从客户端获取数据
继承自ServletRequest,所携带的信息包括请求的地址(URL),请求的参数(Request headers)和实体数据(Entity body),
HttpServletResponse:向客户端发送数据
继承自ServletResponse接口,可以向客户端发送三种类型的数据:响应头(Response headers)、状态码、实体数据(Entity body)
Q、对缓存机制的认识;了解Redis的注解嘛?
· 缓存,就是数据交换的缓冲区。可以分为:内存数据缓存,数据库缓存,文件缓存。
每次想获取数据的时候,先检测内存中有无缓存,再检测本地有无缓存(数据库\文件),最终发送网络请求,将服务器返回的网络数据进行缓存,以便下次读取。
· Redis,全称是Remote Dictionary Server(远程字典服务),一个基于内存实现的键值型非关系(NoSQL)数据库。Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作,因此Redis被广泛应用于缓存。
Redis有以下这五种基本类型:
String(字符串) Hash(哈希) List(列表) Set(集合) zset(有序集合)
它还有三种特殊的数据结构类型:
Geospatial Hyperloglog Bitmap
Q.1、Redis 的持久化机制
为了避免数据丢失,Redis提供了持久化,即把数据保存到磁盘。
· RDB:就是把内存数据以快照的形式保存到磁盘上。
优点:适合大规模的数据恢复场景;
缺点:没办法做到实时持久化/秒级持久化,新老版本存在格式兼容问题。
· AOF(append only file) 持久化:采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。
优点:数据的完整性更高,实时;
缺点:AOF记录的内容越多,文件越大,数据恢复变慢。
注解:
@Cacheable 表示方法的返回值将被缓存;若加载类上,则所有方法返回值将被缓存;
@CachePut 更新缓存;
@CacheEvict 根据一定的条件对缓存进行清空;
Q、为什么service层要写接口和实现类
不用接口的话,假如修改了dao中的代码,因为service引用了dao中的类,那么也要改变service里面的代码,改完之后要重新编译运行,当项目比较大的时候,编译和运行会很浪费时间,到最后就变成了牵一发而动全身。如果使用Imp实现类,后期维护的时候如果要修改功能只需要修改实现类里面的代码,而不需要修改其他包的代码。接口是个规范,有利于项目模块化。
如: Comparable 接口中,a、b 两个对象都满足 Comparable 接口,也就是说他们是可以进行比较的。具体怎么比较,这段程序不需要知道。Comparable 接口有一个方法,叫 compareTo,这个方法就是用来取代 <、> 这样的运算符,用实现类去作比较就很方便了。
Q、正向工程、逆向工程
· 正向工程(Java 代码–>DB表):先创建Java实体类,由框架负责根据实体类生成数据库表。
· 逆向工程(DB表–>Java 代码):先创建数据库表,由框架负责根据数据库表,反向生成Java实体类、Mapper接口、配置文件等。
Q、MVC
· Model层:数据模型,负责系统的业务逻辑判断、数据库存取等;
· View层:视图,主要是负责页面呈现,显示的页面可能是HTML,XML或则JSON;
· Controller层:控制器,主要是负责接收用户的输入并调用模型和视图去完成用户的需求。
MVC是一种设计规范,只是三层架构中的展现层。
优点:① 耦合性低 ② 重用性高 ③ 部署快,生命周期成本低 ④ 可维护性高
缺点:① 调试困难 ② 不适合中小规模的应用程序 ③ 增加系统结构和实现的复杂性
Q、Vue
· 是一套前端框架,免除原生JavaScript中的DOM操作(使用document获取表单数据,再设置数据),简化书写;
· 基于MVVM(Model-View-ViewModel)思想,实现数据的双向绑定,将编程的关注点放在数据上。
v-bind:为html标签绑定属性值href:url;
v-model:在表单元素上创建双向数据绑定;
v-on:为html标签绑定事件;
Q、Nginx
Nginx的定义
Nginx (engine x) 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。
Nginx的作用:
1、作为 Web 服务器:相比 Apache,Nginx 使用更少的资源,支持更多的并发连接,体现更高的效率,这点使 Nginx 尤其受到虚拟主机提供商的欢迎。能够支持高达 50,000 个并发连接数的响应,感谢 Nginx 为我们选择了 epoll and kqueue 作为开发模型
2、作为负载均衡服务器:Nginx 既可以在内部直接支持 Rails 和 PHP,也可以支持作为 HTTP代理服务器 对外进行服务。Nginx 用 C 编写, 不论是系统资源开销还是 CPU 使用效率都比 Perlbal 要好的多。
3、作为邮件代理服务器: Nginx 同时也是一个非常优秀的邮件代理服务器(最早开发这个产品的目的之一也是作为邮件代理服务器),Last.fm 描述了成功并且美妙的使用经验。
4、Nginx 安装非常的简单,配置文件 非常简洁(还能够支持perl语法),Bugs非常少的服务器: Nginx 启动特别容易,并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动。
Q、Hibernate
Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的ORM(Object-Relative Database Mapping)框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。
Q、JDBC
DBC是低级别的数据库访问方式,JDBC并不支持面向对象的数据库表示。JDBC数据库表示完全围绕关系数据库模型。在大型应用程序的DAO中书写这样的代码,维护量是非常大的。
Q、java中的Session会话机制
Session 称为“会话控制”,Session 对象存储特定用户会话所需的属性及配置信息。
① HttpSession:也叫用户Session ,主要用在JavaWeb开发中,Session 共享,Session 超时设置之类所提的Session 就是HttpSession。
② hibernate中的Session :是用于数据库持久层操作的Session 有了它可以方便的操作hibernate框架获取各种API。
③ Apache组织开发的javax.jms.session:这个是用于消息队列发送时用到的Session ,通过Session 可以获取到队列发送器,发送消息。
Session删除的时间
1)Session超时:超时指的是连续一定时间服务器没有收到该Session所对应客户端的请求,并且这个时间超过了服务器设置的Session超时的最大时间。
2)程序调用HttpSession.invalidate();
3)服务器关闭或服务停止。
session位置
服务器端的内存中(服务器崩掉所有session就没了)。不过session可以通过特殊的方式做持久化管理(将session放在磁盘中)。
Q、Lombok
是一个Java库,可以通过添加注解的方式,简化Java开发。其中常用注解有:@Data、@Slf4j
@Data注解在类上,会为类的所有属性自动生成setter/getter、equals、hashCode、toString等方法,如为final属性,则不会为该属性生成setter方法。
@Slf4j 注解在类上(Simple Logging Facade for Java),常用log.info()输出日志信息,private static final Logger log = LoggerFactory.getLogger(UserController.class);
Q、Mybatis使用注解开发
使用注解开发会比XML配置文件(需要写statement,sql语句)开发更加方便;但是,注解用于完成简单功能,配置文件完成复杂功能。
Q、Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10
Q、框架常用注解
Q1、JDK注解作用总结
@Override:表示当前方法覆盖了父类的方法
@Deprecation:表示方法已经过时,方法上有横线,使用时会有警告。
@SuppviseWarnings:表示关闭一些警告信息(通知java编译器忽略特定的编译警告)
Q2、Spring注解
@Bean:用在方法上,产生一个bean,交给spring管理
@Configuration:把一个类作为一个IoC容器;
@Service:用在service层上,表示这是一个业务逻辑层bean;
@GetMapping:处理get请求,如page分页管理,getById;
@PostMapping:处理post请求,login、logout、save;
@PutMapping:向服务器提交信息,常用于更新update;
@DeleteMapping:删除;
@Transactional:事务回滚,用于service层;
@EnableTransactionManagement:开启注解式事务驱动;
@Value(“${xxxx}”)注解从配置文件读取值的用法
@Component:标注Spring管理的Bean,使用@Component注解在一个类上,表示将此类标记为Spring容器中的一个Bean;
@ComponentScan:默认会扫描该类所在的包下所有的配置类;
@After:在方法执行之后执行(方法上);
@Before:在方法执行之前执行(方法上);
@Around:在方法执行之前与之后执行(方法上);
@Repository:用在dao层上,用于标注数据访问组件;
@Resource:默认按照名称方式进行bean匹配;
@Autowired:默认按照类型方式进行bean匹配;
@RunWith(SpringJUnit4ClassRunner.class):设置Spring测试环境;
@EnableAspectJAutoProxy:第三方配置类开启切面
@Aspect:声明切面类
@RunWith
@ContextConfiguration
@ContextConfiguration(classes = SpringConfig.class) 引入支持测试第三方配置类或文件
@ContextConfiguration(Locations=“classpath:applicationContext.xml”)
Q3、SpringMVC注解
@Controller:控制层,与页面打交道
@RestController:设置当前控制器类为RESTful风格,等同于@Controller与@ResponseBody两个注解的功能组合;
@EnableWebMvc: 开启Web MVC的配置支持
@RequestMapping:设置请求响应路径@RequestMapping(“/hello/{id}”) :url参数绑定
@RequestParam:取出URI模板中的变量作为参数;
@RequestBody:将请求体中的JSON字符串绑定到相应的bean上;
@PathVariable:@PathVariable(“id”): Long id;
@ResponseBody:将方法的返回值以特定的格式写入到response的body区域,进而将数据返回给客户端;
@DateTimeFormat:设定日期时间型数据格式;
Q4、Springboot注解
@SpringBootApplication:启动类
@ServletComponentScan:Filter可以直接通过@WebFilter注解自动注册、Servlet→@WebServlet注解自动注册;
@EnableAutoConfiguration:自动配置。
@ConfigurationProperties:配置属性绑定把全局/主配yml/properties的配置信息注入到POJO(全局 > application.yml/properties)
@CrossOrigin:跨域支持
Q5、Mybatis注解
1、SQL语句映射
@Insert:插入,实现新增功能;
@Select:查询
@Update:更新
@Delete:删除
2、结果集映射:
@Result:结果,代表一个字段的映射关系;
@Results:结果集合;
@ResultMap:引用映射结果集;
3、关系映射
@One:用于一对一关系映射;
@Many:用来使用嵌套Select语句用于一对多关系映射;
Q6、Lombok注解
@Data:包含get、set、tostring方法
@Slf4j:注解在类上(Simple Logging Facade for Java)
@NoArgsConstructor:空参构造
@AllArgsConstructor:满参构造
@Accessors(chain = true):链式结构
Q7、Mybatis Plus注解
@Data:包含get、set、tostring方法;
@Slf4j:注解在类上,常用log.info()输出日志信息;
@TableField:设置当前属性对应的数据库表中的字段关系;
@TableName:设置当前类对应与数据库表关系;
@TableId:设置当前类中主键属性的生成策略;
@Version:实体类中添加对应字段,并设定当前字段为“锁”标记字段;
Q、@SpringBootApplication注解
一般放在项目的启动类上,用来把启动类注入到容器中,用来定义容器扫描的范围,用来加载一些bean。
@SpringBootApplication注解主要包装了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan注解
· @SpringBootConfiguration(封装了)>@Configuration>@Component注解,@Component注解主要用来把一个bean注入到容器中;
· @EnableAutoConfiguration将基于添加的jar依赖项来自动配置;
· @ComponentScan注解主要用来指定扫描容器的范围。
Q、@Resource 和 @Autowired 的区别
· @Resource是Java自己的注解,有两个属性比较重要,分别是name和type;Spring将@Resource注解的name属性 解析为bean的名字,而type属性则解析为bean的类型。所以如果i) 使用name属性,则使用byName的自动注入策略,而ii)使用type属性时则使用byType自动注入策略。默认按name进行注入。
· @AutoWired是spring的注解,Autowired只根据type进行注入,不会去匹配name。如果涉及到type无法辨别注入对象时,那需要依赖@Qualifier或@Primary注解一起来修饰。使用@Autowired方式最好使用构造函数的方式注入。
· @Resource默认按名称方式进行bean匹配,@Autowired默认按类型方式进行bean匹配。
Q、@bean 和 @Component 的区别
@Component 和 @Bean是两种使用注解来定义bean的方式,都可以使用@Autowired或者@Resource注解注入。
@Component(和@Service和@Repository)用于自动检测和使用类路径扫描自动配置bean。注释类和bean之间存在隐式的一对一映射(即每个类一个bean)。@Component注解表明一个类会作为组件类,并告知Spring要为这个类创建bean。
@Bean用于显式声明单个bean,而不是让Spring像上面那样自动执行它。它将bean的声明与类定义分离,并允许您精确地创建和配置bean。@Bean常和@Configuration注解搭配使用。@Bean注解告诉Spring这个方法将会返回一个对象,这个对象要注册为Spring应用上下文中的bean。
Q、为什么有了@Component,还需要@Bean?
如果想将第三方的类变成组件,你又没有没有源代码,也就没办法使用@Component进行自动配置,这种时候使用@Bean就比较合适了。不过同样的也可以通过xml方式来定义。
另外@Bean注解的方法返回值是对象,可以在方法中为对象设置属性。
此外Spring的Starter机制,就是通过@Bean注解来定义bean。
可以搭配@ConditionalOnMissingBean注解 @ConditionalOnMissingClass注解,如果本项目中没有定义该类型的bean则会生效。避免在某个项目中定义或者通过congfig注解来声明大量重复的bean。
Q、拦截器的实现
定义:拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行。
作用:在指定的方法调用前后执行预设的代码;
阻止原始方法的执行。
① 定义项目拦截器ProjectInterceptor类,实现HandlerInterceptor接口,重写其中的preHandle()、postHandle()、afterCompletion()方法。在类上添加@Component注解,让SpringMVC(容器)环境加载它。
② 在SpringMvcConfig类中,实现WebMvcConfigurer接口,通过重写addInterceptors()配置拦截器(还可以通过addResourceHandlers()过滤静态资源),注入刚才加载到容器中的拦截器ProjectInterceptor,并指定要拦截的路径。(侵入性较强)
② 定义一个SpringMvcSupport类,继承WebMvcConfigurationSupport,通过重写addInterceptors()配置拦截器(还可以通过addResourceHandlers()过滤静态资源),注入刚才加载到容器中的拦截器ProjectInterceptor,并指定要拦截的路径。最后添加@Configuration注解让容器扫描。
Q1、过滤器与拦截器的区别
· 过滤器可以简单的理解为“取”,关注的是web请求,基于函数回调来实现的,属于Servlet技术;
· 拦截器可以简单的理解为“拒”,关注的是方法调用,基于java反射机制来实现的,属于SpringMVC技术。
· 过滤器依赖于servlet容器,拦截器不依赖servlet容器。
· 过滤器可以对所有请求起作用,拦截器只对SpringMVC的访问起作用。
· 在Action的生命周期中,过滤器只能在容器初始化时调用一次,而拦截器可以多次调用。
Q、Maven
Q1、Maven介绍
Apache Maven是一个用于管理和构建Java项目的工具,它基于项目对象模型(POM)的概念,通过一小段描述信息来管理项目的构建、报告和文档。
官网:http://maven.apache.org/
主要功能有:
-> 提供了一套标准化的项目结构
-> 提供了一套标准化的构建流程(编译,测试,打包,发布…)
-> 提供了一套依赖管理机制
Q2、Maven中仓库的分类
本地仓库:自己计算机上的一个目录。
中央仓库:由Maven团队维护的全球唯一的仓库。(https://repo1.maven.org/maven2/)
远程仓库(私服):一般由公司团队搭建的私有仓库。
jar包的查找顺序:本地仓库 --> 远程仓库 --> 中央仓库
Q3、Maven坐标
· 什么是坐标?
Maven中的坐标是资源的唯一标识。
使用坐标来定义项目或引入项目中需要的依赖。
· Maven坐标主要组成
groupld:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.itheima)。
artifactld:定义当前Maven项目名称(通常是模块名称,例如order-service、goods-service)。
version:定义当前项目版本号。
scope:设置坐标的依赖范围,如编译环境、测试环境、运行环境。
Q、Java Servlet
在Web服务器端加载并运行的Java应用程序具体运行在Servlet引擎管理的JVM上。Servlet容器负责Servlet和用户的通信以及调用Servlet的方法。Servlet和用户的通信采用请求/响应模式。用于以动态响应客户机请求形式扩展Web服务器(Web Container)的功能。Servlet是开发服务器端应用程序的一个很好选择, Servlet与JSP结合使用,能提供更强大的服务器端功能。
一个servlet就是Java编程语言中的一个类,它被用来扩展服务器的性能,服务器上驻留着可以通过“请求-响应”编程模型来访问的应用程序。
Q、消息队列
理解如下问题:
1、消息队列为什么会出现?
2、消息队列能用来做什么?
3、使用消息队列存在的问题?
4、如何解决重复消费消息的问题?
5、如何解决消息的顺序消费问题?
6、如何解决分布式事务问题?
7、如何解决消息堆积问题?
5.1.1 消息队列常见面试题
1、什么是消息队列?
我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。(注意:消费消息是按照顺序来消费的。)
消息队列是分布式系统中重要的组件之一。使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。
2、为什么要用消息队列?
① 通过异步处理提高系统性能(减少响应所需时间)。
② 削峰/限流。
③ 降低系统耦合性。
3、使用消息队列存在的问题
① 系统可用性降低:系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或者说MQ挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了!(需要考虑消息是否丢失或挂掉)
② 系统复杂性提高:加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!(需要保证消息不被重复消费、丢失、传递的顺序前后一致等问题)
③ 一致性问题:我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!(数据保存到MQ中,消息可能未被消费者正确消费,导致数据不一致)
4、JMS vs AMQP
1、JMS
什么是JMS?
JMS(Java Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。JMS(Java Message Service,Java消息服务)API是一个消息服务的标准或者说是规范,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。
ActiveMQ就是基于JMS规范实现的。
JMS两种消息模型
①点对点(P2P)模型
使用队列(Queue)作为消息通信载体;满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送100条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。)
②发布/订阅(Pub/Sub)模型
发布订阅模型(Pub/Sub)使用主题(Topic)作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。
JMS五种不同消息的正文格式
JMS 定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收一些不同形式的数据,提供现有消息格式的一些级别的兼容性。
①StreamMessage — Java原始值的数据流
②MapMessage — 一套名称-值对
③TextMessage — 一个字符串对象
④ObjectMessage — 一个序列化的 Java 对象
⑤BytesMessage — 一个字节的数据流
2、AMQP
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 高级消息队列协议(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。
RabbitMQ就是基于AMQP协议实现的。
3、JMS vs AMQP
对比方面 JMS AMQP
定义 Java Api 协议
跨语言 否 是
跨平台 否 是
支持消息类型 ①点对点;②发布订阅; 五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。
支持消息类型 支持多种,如上面提到的 byte[](二进制)
总结:
①AMQP为消息定义了应用层(wire-level protocol)的协议,而JMS所定义的是API规范。在Java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而AMQP天然具有跨平台、跨语言特性。
②JMS支持TextMessage、MapMessage等复杂的消息类型;而AMQP仅支持 byte[]消息类型(复杂的类型可序列化后发送)。
③由于Exchange提供的路由算法,AMQP可以提供多样化的路由方式来传递消息到消息队列,而JMS仅支持 队列 和 主题/订阅 方式两种。
5、常见消息队列的对比
对比方向 概要
吞吐量 万级:ActiveMQ和RabbitMQ(ActiveMQ 的性能最差)
十万级甚至是百万级:RocketMQ和Kafka
可用性 都可以实现高可用。ActiveMQ和RabbitMQ都是基于主从架构实现高可用性。RocketMQ基于分布式架构。kafka也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
时效性 RabbitMQ基于erlang开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是ms级。
功能支持 除了Kafka,其他三个功能都较为完备。Kafka功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准
消息丢失 ActiveMQ和 RabbitMQ丢失的可能性非常低,RocketMQ和 Kafka理论上不会丢失。
总结:
①ActiveMQ的性能比较差,版本迭代很慢,不推荐使用。
②RabbitMQ在吞吐量方面虽然稍逊于Kafka和RocketMQ,但是由于它基于 erlang开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用Kafka。
③RocketMQ阿里出品,Java系开源项目,但接口这块不是按照标准JMS规范走的,有些系统要迁移需要修改大量代码。
④Kafka仅仅提供较少的核心功能,却可以实现超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量。kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。
Q、RabiitMQ
1、RabbitMQ介绍
2、Rabbit核心概念
3、AMQP是什么?
4、什么是生产者和消费者?
生产者:消息生产者,就是投递消息的一方。消息一般包含两个部分:消息体(payload)和标签(Label)。
消费者:消费消息,也就是接收消息的一方。消费者连接到 RabbitMQ 服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
5、说说Broker服务节点、Queue队列、Exchange交换器?
Broker:可以看做RabbitMQ的服务节点。
Queue:RabbitMQ的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
Exchange:交换器,生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。
6、什么是死信队列?如何导致的?
DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,它能被重新发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列就称之为死信队列。
导致的死信的几种原因:
①消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
②消息TTL过期。
③队列满了,无法再添加。
7、什么是延迟队列?RabbitMQ怎么实现延迟队列?
延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。(发送到队列后,消费者不能立马消费,得等待一定时间才能消费,该队列即为延迟队列)
RabbitMQ自身没有延迟队列的,那么如何实现延迟队列?
①通过RabbitMQ本身队列的特性来实现,需要使用RabbitMQ的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)。
②在RabbitMQ3.5.7及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时,插件依赖Erlang/OPT 18.0及以上。
也就是说,AMQP协议以及RabbitMQ本身没有直接支持延迟队列的功能,但是可以通过TTL和DLX模拟出延迟队列的功能。
8、什么是优先级队列?
RabbitMQ自V3.5.0有优先级队列实现,优先级高的队列会先被消费。
可以通过x-max-priority参数来实现优先级队列。不过,当消费速度大于生产速度且Broker没有堆积的情况下,优先级显得没有意义。
9、RabbitMQ有哪些工作模式?
①简单模式;②work工作模式;③pub/sub发布订阅模式;④Routing路由模式;⑤Topic主题模式。
10、RabbitMQ消息怎么传输?
由于TCP链接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈,所以RabbitMQ使用信道的方式来传输数据。信道(Channel)是生产者、消费者与RabbitMQ通信的渠道,信道是建立在TCP链接上的虚拟链接,且每条TCP链接上的信道数量没有限制。就是说RabbitMQ在一条TCP链接上建立成百上千个信道来达到多个线程处理,这个TCP被多个线程共享,每个信道在RabbitMQ都有唯一的ID,保证了信道私有性,每个信道对应一个线程使用。
11、如何保证消息的可靠性?
消息可能丢失的情况:①消息到MQ的过程中搞丢;②MQ自己搞丢;③MQ到消费过程中搞丢。
①生产者到RabbitMQ:事务机制和Confirm机制,注意:事务机制和Confirm机制是互斥的,两者不能共存,会导致RabbitMQ报错。
②RabbitMQ自身:持久化、集群、普通模式、镜像模式。
③RabbitMQ到消费者:basicAck机制、死信队列、消息补偿机制。
12、如何保证RabbitMQ消息的顺序性?
①拆分多个queue(消息队列),每个queue(消息队列)一个consumer(消费者),就是多一些queue(消息队列)而已,确实是麻烦点;(一个消费者一个队列)
②一个queue(消息队列)但是对应一个consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的worker来处理。(就一个队列一个消费者,内部分任务)
13、如何保证RabbitMQ高可用的?
RabbitMQ是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以RabbitMQ为例子讲解第一种MQ的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。
单机模式
普通集群模式:多台机器上启动多个RabbitMQ实例,每个机器启动一个。
镜像集群模式:即RabbitMQ的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,就是说,每个RabbitMQ节点都有这个queue的一个完整镜像,包含 queue 的全部数据的意思。
14、如何解决消息挤压问题?
临时紧急扩容。这种做法相当于是临时将 queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据。等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
15、如何解决消息队列的延时以及过期失效问题?
RabbtiMQ是可以设置过期时间的,也就是TTL。如果消息在queue中积压超过一定的时间就会被RabbitMQ给清理掉,这个数据就没了。那怎么将有用的数据找回呢?批量重导,写程序查出丢失程序,再写回mq中。
Q、RocketMQ
1、RocketMQ是什么?
RocketMQ 是一个 队列模型 的消息中间件,具有高性能、高可靠、高实时、分布式的特点。它是一个采用Java语言开发的分布式的消息系统,由阿里巴巴团队开发。
2、发布者、订阅者、主题、Broker
发布者(Publisher):消息的生产者
订阅者(Subscriber):消息的消费者
主题(Topic):存放消息的容器
Broker(消息存储服务器)
注意:
①一个主题包含多个队列,分布在不同的Broker(消息存储服务器)上。
②一个消费者集群共同消费一个topic(主题)内的多个队列。
③一个队列只能被一个消费者消费。
3、队列模型和主题模型
4、RocketMQ中的消息模型
5、RocketMQ的架构图
Q、读写分离&分库分表
1、读写分离
1、何为读写分离
读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。
一般情况下,我们都会选择一主多从,也就是一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据的准确性。这样的架构实现起来比较简单,并且也符合系统的写少读多的特点。
2、读写分离会带来什么问题?如何解决?
3、如何实现读写分离?
4、主从复制原理
MySQL binlog(binary log即二进制日志文件)主要记录了MySQL数据库中数据的所有变化(数据库执行的所有DDL和DML语句)。因此,我们根据主库的MySQL binlog日志就能够将主库的数据同步到从库中。
Q、分库分表
1、为什么要进行分库分表?
当MySQL中一张表的数据量过大的时候,不便于存储,因此我们需要分库分表。
2、何为分库?
分库就是将数据库中的数据分散到不同的数据库上,可以是垂直拆分也可以是水平拆分。
下面这些操作都涉及到了分库:
①你将数据库中的用户表和用户订单表分别放在两个不同的数据库。
②由于用户表数据量太大,你对用户表进行了水平切分,然后将切分后的2张用户表分别放在两个不同的数据库。
3、何为分表?
分表就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。
★4、垂直和水平拆分?
1、数据库的垂直&水平拆分?
垂直分库:以表为依据,不同表到不同库,每个库的表结构和数据都不一样,合起来是全量数据。(如用户表和用户订单表放到不同数据库)
水平分库:以字段为依据,一个库的数据拆分到多个库,每个库的表结构一致,数据不一致,合起来是全量数据。(如用户表的1-100000条记录在1号数据库,100001-200000条记录在2号数据库)
2、表的垂直&水平拆分?
垂直分表:以字段为依据(对列拆分),不同字段分到不同表,每个表结构和数据不一致,合起来是全量数据。(如将用户信息表的1-10列抽取出来作为一张表,10-20列抽取出来作为另一张表)
水平分表:以字段为依据(对行拆分),一张表数据拆分到多张表,每个表结构一致,数据不一致,合起来是全量数据。(如将用户信息表的1-100000条记录作为表1,100001-200000条记录作为表2,这样就可以避免单一表数据量过大对性能造成影响)
5、什么时候需要分库分表?
遇到下面几种场景可以考虑分库分表:
①单表的数据达到千万级别以上,数据库读写速度比较缓慢(分表)。
②数据库中的数据占用的空间越来越大,备份时间越来越长(分库)。
③应用的并发量太大(分库)。
6、分库分表会带来什么问题?
引入分库分表之后,会给系统带来什么挑战呢?
① join操作:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。(不同数据库的表无法使用join操作,需要手动封装数据)
② 事务问题:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。(不同数据库表操作,无法保证事务)
③ 分布式id:分库之后,数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式id了。(不同数据库表操作,无法使用数据表的自增主键,得使用分布式id)
7、分库分表使用什么?
ShardingSphere 项目(包括Sharding-JDBC、Sharding-Proxy和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。
8、分库分表后,数据如何迁移?
停机迁移,将老库的数据利用脚本文件更新到新库中。
双写方案,对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。这样就能保证,咱们新库里的数据是最新的。
在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。重复上一步的操作,直到老库和新库的数据一致为止。
9、分库分表后,ID键如何处理?
分库分表后不能每个表的ID都是从1开始,所以需要一个全局ID,设置全局ID主要有以下几种方法:
(1)UUID:
优点:本地生成ID,不需要远程调用,全局唯一不重复。
缺点:占用空间大,不适合作为索引。
(2)数据库自增ID:
在分库分表表后使用数据库自增ID,需要一个专门用于生成主键的库,每次服务接收到请求,先向这个库中插入一条没有意义的数据,获取一个数据库自增的ID,利用这个ID去分库分表中写数据。
优点:简单易实现。
缺点:在高并发下存在瓶颈。
(3)Redis生成ID:
优点:不依赖数据库,性能比较好。
缺点:引入新的组件会使得系统复杂度增加。
(4)Twitter的snowflake算法:是一个64位的long型的ID,其中有1bit是不用的,41bit作为毫秒数,10bit作为工作机器ID,12bit作为序列号。
1bit:第一个bit默认为0,因为二进制中第一个bit为1的话为负数,但是ID不能为负数。
41bit:表示的是时间戳,单位是毫秒。
10bit:记录工作机器ID,其中5个bit表示机房ID,5个bit表示机器ID。
12bit:用来记录同一毫秒内产生的不同ID。
Q、如何学习的java
应用框架看官方文档,许多博客都是翻译加工的官方文档,敲一敲官方的小demo,也可以去GitHub上找项目,遇到不懂的问题还可以百度或者查Stack Overflow。
比如,学习Mybatis时,第一步配置数据库的属性等properties可以去Mybatis的官网复制部分代码,然后把相应的属性改了就可以用了。
Q、读研之后发现和本科有什么差别
读研给我的感受就是学术能力上的提高,通过论文搜索网站(archive,Paper with code)找到合适的论文,可以快速了解一门技术,一个方向目前的研究进展;而且本科时期遇到问题基本都是看书、中文博客网站,但是研究生学会了利用官方文档、学会去Stack Overflow上找解决方法。
Q、python为什么不支持多线程
python的线程实质是操作系统原生的线程,而每个线程要执行python代码的话,需要获得对应代码解释器的锁GIL。一般我们运行python程序都只有一个解释器,这样不同线程需要获得同一个锁才能执行各自的代码,互斥了,于是代码就不能同时运行了。
Q、jsp页面js和java语言哪个先运行
java是在服务器端运行的代码,jsp在服务器的servlet里运行,而 JavaScript和html都是在浏览器端运行的代码。所以服务器端先执行,执行后将信息传给客户端。
因此加载jsp页面的执行顺序是java -> jsp ->js,
Q、多人git提交版本如何避免冲突
首先需要在仓库上边创建分支,简单一点的话n个人协同开发就创建n个分支,分支名自己定,便于区分记忆就好