Java常见面试题总结
Java
Java基础概念和知识
Java语言的特点
Java SE(Standard Edition) VS Java EE(Enterprise Edition)
简单来说,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。
JVM vs JDK vs JRE
JVM(Java Virtual Machine) Java虚拟机是运行java字节码的虚拟机,是Java的核心,提供了Java的跨平台特性。
JDK(Java Development Kit) Java开发工具包,是Java的开发工具包,提供了Java的开发环境。它包含了 JRE,同时还包含了编译 Java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等。
JRE(Java Runtime Environment) Java运行时环境,是Java程序的运行环境,包含了JVM和一些核心类库。
总结:JDK 包含了 JRE 和开发工具,JRE 包含了 JVM 和核心类库。
字节码是什么?字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件)
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
Java程序从源码到运行的过程:.java -> javac编译 -> .class(字节码) -> 解释器&JIT编译器 -> 机器码
补充:我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言 。
为什么说 Java 语言“编译与解释并存”?
高级编程语言按照程序的执行方式分为两种:
编译型:编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
解释型:解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
为什么说 Java 语言“编译与解释并存”?
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。
AOT 有什么优点?为什么不全部使用 AOT 呢?
JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation) 。和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。
然而,AOP编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等
Java 和 C++ 的区别?
Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
- C++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
Java基本语法
标识符和关键字的区别是什么?
在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符 。简单来说,标识符就是程序员在开发的时候自己起的一个名字 。
有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 关键字 。简单来说,关键字是被Java语言赋予特殊含义的标识符 。
自增资减运算符
移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。
在 Java 代码里使用 <<、 >> 和>>>转换成的指令码运行起来会更高效些。
Java 中有三种移位运算符:
- << :左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
:带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
:无符号右移,忽略符号位,空位都以 0 补齐。
注意:由于 double,float 在二进制中的表现比较特殊,因此不能来进行移位操作。
如果移位的位数超过数值所占有的位数会怎样?
当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。也就是说左移/右移 32 位相当于不进行移位操作(32%32=0)。左移/右移 42 位相当于左移/右移 10 位(42%32=10)。当 long 类型进行左移/右移操作时,由于 long 对应的二进制是 64 位,因此求余操作的基数也变成了 64。
continue、break 和 return 的区别是什么?
需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词:
- continue:指跳出当前的这一次循环,继续下一次循环。
- break:指跳出整个循环体,继续执行循环下面的语句。
return 用于跳出所在方法,结束该方法的运行。
return 一般有两种用法:
- return;:直接使用 return 结束方法执行,用于没有返回值函数的方法
- return value;:return 一个特定值,用于有返回值函数的方法
基本数据类型
Java中有8种基本数据类型,分别是:
- 6种数字类型:
- 4种整形:byte short int long(位数依次为8 16 32 64)
- 2种浮点型:float double(位数依次为32 64)
- 1种字符型:char
- 1种布尔型:boolean
二进制补码表示法中,最高位是用来表示符号的(0 表示正数,1 表示负数),其余位表示数值部分。
注意:
Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析。
char a = ‘h’char :单引号,String a = “hello” :双引号。这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean 。
基本类型和包装类型的区别?
基本数据类型是”数”,包装类型是”对象”。
- 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
- 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
- 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
- 比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。
所有整型包装类对象之间值的比较,全部使用 equals() 方法。\
⚠️ 注意:基本数据类型存放在栈中是一个常见的误区! 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中。
自动装箱和拆箱
什么是自动拆装箱?
- 装箱:将基本数据类型转换为包装类型。
- 拆箱:将包装类型转换为基本数据类型。
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
为什么浮点数运算的时候会有精度丢失的风险?
计算机是二进制的,在标识一个数字的时候宽度是有限的,所以浮点数的精度是有限的,在表示无限不循环小数到时候,只能被截断。这也就是解释了为什么浮点数没有办法用二进制精确表示。
如何解决浮点数运算的精度丢失问题?
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
超过 long 整型的数据应该如何表示?
BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。
相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。
变量
局部变量和成员变量的区别?
- 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
- 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
静态变量有什么作用?
静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
字符型常量和字符串常量的区别?
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
- ⭐️含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。
方法
静态方法为什么不能调用非静态成员
这个需要结合 JVM 的相关知识,主要原因如下:
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
简言之,静态方法先类加载时就有了,而非静态成员得创建对象实例之后才有,所以静态方法不能直接调用非静态成员。
静态方法和实例方法的区别?
调用方式
在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式(但一般使用前者,因为静态方法是属于类的),而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。访问类成员限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
方法的重载和重写有什么区别?
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理.(同一个方法名,但是有不同输入数据类型)重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法(重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。)重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
什么是可变长参数
所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。
1 | public static void method1(String... args) { |
当存在重载方法时,可变长参数的方法会被当做最后的选择。
面向对象基础
面向对象和面向过程的区别
- 一步步
- 对象执行方法
对象实体与对象引用有何不同?
new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
如果一个类没有声明构造方法,该程序能正确执行吗?
可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。
构造方法是否可被 override?
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
面向对象的三大特征
- 封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息
- 继承
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- 多态:多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
多态的特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类重写的方法,如果子类没有重写父类的方法,执行的是父类的方法。(就近)
接口和抽象类有什么共同点和区别?
共同点:
- 都不能被实例化。
- 能可包含抽象方法
- 都可以有默认实现方法
不同点:
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。(接口是一种规范);抽象类主要用于代码复用,强调的是所属关系。
- 一个类只能继承一个类,但是可以实现多个接口
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点);
不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
总结:会在堆上创建一个新对象,但是对象内部的引用类型的对象不拷贝对象本身,而是只拷贝地址引用。 - 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
Object
Object类是一个特殊的类,是所有类的父类
== 和 equals() 的区别是什么?
== 对于基本类型和引用类型的作用效果是不同的(本质比较的是值):
- 对于基本数据类型来说,== 比较的是值。
- 对于引用数据类型来说,== 比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
equals() 方法存在两种使用情况:
- 类没有重写 equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
- 类重写了 equals()方法:一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
hashCode() 有什么用?
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode?
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。
但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。
如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
本质山, hashCode() 和 equals()都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
hashCode 在一些容器中(如HashSet HashyMap)中的判断元素是否在对应容器中的效率更高
那为什么不只提供 hashCode() 方法呢?
两个对象的hashCode值相等不代表两个对象就一定相等
那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。
总结下来就是:
- 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
- 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
(hashCode相等是两个对象相等的必要不充分条件)
为什么重写 equals() 时必须重写 hashCode() 方法?
为什么重写 equals() 时必须重写 hashCode() 方法?因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
String
String、StringBuffer、StringBuilder 的区别?
可变性:
String是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。线程安全性:
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。性能:
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。对于三者的使用
操作少量的数据: 适用 String
单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
String为什么是不可变的
String 类中使用 final 关键字修饰字符数组
String#equals() 和 Object#equals() 有何区别?
String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的 equals 方法是比较的对象的内存地址。
字符串常量池的作用
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String s1 = new String(“abc”);这句话创建了几个字符串对象?
这句话包含了两个对象 字符串常量对象”abc”和字符串对象s1
字符串对象s1一定会创建在堆里(看见了new则一定会在堆中创建对象)
如果 字符串常量池中 不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
String#intern 方法有什么作用?
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返
异常
Java中exception和error有什么区别
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:
- Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
- Error:Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
受检异常(checked exception)和未受检异常(Unchecked exception)有什么区别
- Checked Exception 即受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException…。 - Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException 及其子类都统称为非受检查异常,常见的有: - NullPointerException(空指针错误)
- IllegalArgumentException(参数错误比如方法入参类型错误)
- NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
- ArrayIndexOutOfBoundsException(数组越界错误)
- ClassCastException(类型转换错误)
- ArithmeticException(算术错误)
- SecurityException (安全错误比如权限不够)
- UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
- …
Throwable 类常用方法有哪些?
- String getMessage(): 返回异常发生时的简要描述
- String toString(): 返回异常发生时的详细信息
- String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
- void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息
try-catch-finally 如何使用?
- try:用于捕获异常。后面可跟一个或多个catch,如果没有catch则必须跟一个finally
- catch:用于处理try捕获的异常
- finally:无论是否捕获或处理异常,finally 块里的语句都会被执行。
当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
⚠️注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
finally 中的代码一定会执行吗?
不一定,finally 之前虚拟机被终止运行的话(例如System.exit(1),finally 中的代码就不会被执行。
另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:
- 程序所在的线程死亡。
- 关闭 CPU。
如何使用 try-with-resources 代替try-catch-finally?
- 适用范围(资源的定义): 任何实现
java.lang.AutoCloseable
或者java.io.Closeable
的对象 - finally 块的执行顺序: 在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行
简单来说,当有类似于InputStream、OutputStream、Scanner、PrintWriter等的资源都需要我们调用close()方法来手动关闭时,可以用try-with-resources来代码让更简短,更清晰,产生的异常对我们也更有用
异常使用有哪些需要注意的地方?
- 每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
- 建议抛出更加具体的异常
⭐️泛型(Generics)
泛型的使用方式有几种?
- 泛型类
- 泛型接口
- 泛型方法
项目中哪里遇到了泛型?
自定义接口通用返回结果
CommonResult<T>
通过参数 T 可根据具体的返回类型动态指定结果的数据类型定义 Excel 处理类
ExcelUtil<T>
用于动态指定 Excel 导出的数据类型构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
……
⭐️反射
什么是反射?
反射赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射的优缺点?
优点:
- 反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
缺点:
- 安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)
- 性能也要稍差点
反射的应用场景?
- 框架中大量使用了动态代理,而动态代理的实现也依赖反射。
- 注解的实现也用到了反射。
注解(Annotation)
注解是什么
可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解的解析方法有哪几种?
- 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value、@Component)都是通过反射来进行处理的。
⭐️SPI(Service Provider Interface)
什么是SPI
专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。
SPI 和 API 有什么区别?
广义上来说它们都属于接口,API是接口存在于实现方,SPI是接口存在于调用方
SPI 的优缺点?
- 通过 SPI 机制能够大大地提高接口设计的灵活性,
但是 SPI 机制也存在一些缺点,比如: - 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个 ServiceLoader 同时 load 时,会有并发问题。
⭐️序列化和反序列化
什么是序列化?什么是反序列化?
简单来说:
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
如果有些字段不想进行序列化怎么办?
使用transient关键字
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
JDK自带的序列化方式
JDK 自带的序列化,只需实现 java.io.Serializable接口即可。
为什么不推荐使用 JDK 自带的序列化?
- 不支持跨语言调用
- 性能差
- 存在安全问题:输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码
常见的序列化协议有哪些?
比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
序列化协议对应于 TCP/IP 4 层模型的哪一层?
OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据,就对应的是序列化和反序列化么
又因为OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
总结
Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用,并且 Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。
像 Protobuf、 ProtoStuff、hessian 这类都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。
IO
Java IO 流了解吗?
数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出
数据传输过程类似于水流,因此称为 IO 流
IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的:
InputStream/Reader:前者是字节输入流,后者是字符输入流。
OutputStream/Writer:前者是字节输出流,后者是字符输出流。
Java IO 中的设计模式有哪些?
- [ ]
BIO、NIO 和 AIO 的区别?
- [ ]
⭐️语法糖
什么是语法糖
语法糖(Syntactic sugar) 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法
Java中常见的语法糖有哪些?
switch 支持 String 与枚举
java中的switch本身支持基本类型,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(ascii 码是整型)以及int。
Java 7 中switch开始支持String。字符串的 switch 是通过equals()和hashCode()方法来实现的(hashCode()方法返回的是int)
自动装箱与拆箱
装箱过程是通过调用包装器的 valueOf 方法实现的,而拆箱过程是通过调用包装器的 xxxValue 方法实现的。
可变长参数
枚举类
枚举类型不能被继承,当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,
内部类
可以把内部类理解为外部类的一个普通成员
内部类之所以是语法糖,是因为踏仅仅是一个编译时的概念,一旦编译成功,就会生成两个完全不同.class文件。
条件编译
让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。
断言assert
在Java中,断言是一种用于在代码中插入检查条件的机制。它允许程序员在代码中插入一些断言语句,用于检查程序的某些假设是否为真。如果断言的条件为假,系统会抛出一个 AssertionError 异常。
例:
1 | public class AssertExample { |
⭐️基础重要知识点
Java中的值传递详解
形参vs实参
参数在程序设计语言中可分为:
- 实参:Arguments,用于传递给函数或方法的参数,必须要有确定的值
- 形参:Parameters,用于定义函数/方法,接受实参
值传递vs引用传递
程序设计语言将实参
传递给方法的方法有两种:
- 引用传递:方法接受的是实参所引用的对象在
堆
中的地址,对形参
的修改会影响到实参(类似linux中的硬连结) - 值传递:方法接受的是实际参数的
拷贝副本
(类似linux中的软链接)
Java中只有值传递
为什么Java中只有值传递?
例1:传递基本类型参数
1 | public static void main(String[] args) { |
输出:
1 | a = 20 |
- 可以看到方法的修改并没有生效,因为swap方法中的a和b是num1和num2的副本,并不是本体,由此可知一个方法不能修改一个基本数据类型的参数
例2:传递引用类型参数
1 | public static void main(String[] args) { |
输出:
1 | 1 |
- 对于引用类型的数据,也是传递的值,但是引用类型(如数组对象)保存的是实参的地址。
例3:传递引用类型参数2
Conclusion
Java 中将实参传递给方法(或函数)的方式是 值传递:
- 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。
代理模式
代理模式是什么?
简单来说就是我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。(结合现实生活中的代理人来理解)
静态代理
静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改),而且麻烦(需要对每个目标类都单独写一个代理类),实际开发中基本不用。
静态代理实现步骤:
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
动态代理
我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。
动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。
JDK动态代理机制
介绍
在 Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。
Proxy 类中使用频率最高的方法是:newProxyInstance() ,这个方法主要用来生成一个代理对象。
1 | public static Object newProxyInstance(ClassLoader loader, |
这个方法一共有 3 个参数:
- loader :类加载器,用于加载代理对象。
- interfaces : 被代理类实现的一些接口;
- h: 实现了 InvocationHandler 接口的对象;
要实现动态代理的话,还必须需要实现InvocationHandler 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。
1 |
|
invoke() 方法有下面三个参数:
- proxy :动态生成的代理类
- method : 与代理类对象调用的方法相对应
- args : 当前 method 方法的参数
JDK 动态代理类使用步骤
- 定义一个接口及其实现类;
- 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;invoke() 方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke() 方法,然后 invoke() 方法代替我们去调用了被代理对象的原生方法。
1 |
|
- 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;
CGLIB 动态代理机制
介绍
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
在 CGLIB 动态代理机制中 MethodInterceptor(拦截被代理类中的方法) 接口和 Enhancer(实现对拦截方法的增强) 类是核心。
JDK动态代理和CGLIB动态代理的对比
JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。
就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
静态代理和动态代理的对比
- 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的
- JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
BigDecimal
介绍:BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。
这是因为浮点数在计算机中的存储方式和精度问题。浮点数采用二进制的形式存储,而在二进制中,一些十进制的小数是无法精确表示的,就像在十进制中无法精确表示 1/3 一样。这会导致在进行浮点数计算时产生一些微小的舍入误差。这些微小的误差可能会在不同的计算中累积,导致我们不能简单地依赖直接相等性比较来判断两个浮点数是否相等。
当我们使用 == 运算符比较两个浮点数时,它会比较它们的存储值,而存储值是有舍入误差的。
而对于包装数据类型,例如 Double、Float 等,它们是对象,使用 equals() 方法进行比较时,默认情况下比较的是对象的引用地址,而不是比较对象所代表的值是否相等。因此,使用 equals() 方法进行包装数据类型的相等性比较可能不会得到正确的结果,除非你重写了 equals() 方法来进行值比较。
BigDecimal等值比较问题
BigDecimal的等值比较应使用compareTo()
方法,而不是equals()
方法。compareTo() 方法可以比较两个 BigDecimal 的值。
Java魔法类Unsafe
Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。
集合
并发编程
ThreadLocal
ThreadLocal有什么用?
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。
如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
DK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
ThreadLocal内存泄露问题是怎么倒置的?
[!TIP]
待施工
线程池
线程池就是管理一系列线程的资源池。
当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
为什么要用线程池?
池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如何创建线程池
法一:通过ThreadPoolExecutor构造函数来创建(推荐)。
法二:通过 Executor 框架的工具类 Executors 来创建。
Executors工具类可以创建多种类型的线程池,包括:
- FixedThreadPool:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出(队列)的顺序执行队列中的任务。
- CachedThreadPool: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
- ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。
为什么不推荐使用内置线程池
《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
- 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,因为Executors
容易导致OOM(out of memory)问题
线程池常见参数
线程池的拒绝策略有哪些?
如果不允许丢弃任务任务,应该选择哪个拒绝策略?
CallerRunsPolicy 拒绝策略有什么风险?如何解决?
线程池常用的阻塞队列有哪些?
线程池处理任务的流程
如何给线程池命名?
如何设定线程池的大小?
如何动态修改线程池的参数?
如何设计一个能够根据任务的优先级来执行的线程池?
[!TIP]
待施工
> IO
JVM
新特性
计算机基础
计算机网络
操作系统
操作系统基础
什么是操作系统?
操作系统本质上是一个运行在计算机上的软件程序 ,主要用于管理计算机硬件和软件资源。 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。
内核(Kernel)和中央处理器(CPU,Central Processing Unit)
- 操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件。
- CPU 主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作。
操作系统主要有哪些功能
- 进程和线程的管理:进程的创建、撤销、阻塞、唤醒,进程间的通信等。
- 存储管理:内存的分配和管理、外存(磁盘等)的分配和管理等。
- 文件管理:文件的读、写、创建及删除等。
- 设备管理:完成设备(输入输出设备和外部存储设备等)的请求或释放,以及设备启动等功能。
- 网络管理:操作系统负责管理计算机网络的使用。网络是计算机系统中连接不同计算机的方式,操作系统需要管理计算机网络的配置、连接、通信和安全等,以提供高效可靠的网络服务。
- 安全管理:用户的身份认证、访问控制、文件加密等,以防止非法用户对系统资源的访问和操作。
用户态和内核态?
主要是访问权限的区别,可以理解为root用户和普通用户
进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
- 用户态(User Mode) : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入内核态。
- 内核态(Kernel Mode):内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
为什么要有用户态和内核态?只有一个内核态不行么?
- 限制危险指令
- 所有程序或进程都必须共享系统资源将导致系统资源的竞争和冲突,从而影响系统性能和效率。
并且,这样也会让系统的安全性降低。
用户态和内核态是如何切换的?
- 系统调用(Trap):用户态进程主动要求切换到内核态的一种方式。我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的内核态级别的子功能咋办呢?那就需要系统调用了。
系统调用是应用程序与操作系统之间进行交互的一种方式,通过系统调用,应用程序可以访问操作系统底层资源例如文件、设备、网络等。
- 中断(Interrupt):用户态执行操作的时候有外围设备干完活了突然插队,那么用户态就切换到内核态。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
- 异常(Exception):用户态运行时发生了事先不可知的某些异常(如缺页异常)会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态
系统调用的过程
- 用户态执行程序时权限不足,中断执行(Trap),发起系统调用
- 发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序。内核程序开始执行,开始处理系统调用。
- 内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。
总结:用户态发起系统调用->Trap->CPU跳转中断处理程序,处理系统调用->Trap->返回用户态
进程和线程
什么是进程和线程
进程(Process) 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。
线程(Thread) 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。
进程和线程的区别是什么
从JVM的角度而言 ,在运行时数据区中,一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
总结:
- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
有了进程为什么还需要线程?
- 进程切换是一个开销很大的操作,线程切换的成本较低。
- 线程更轻量,一个进程可以创建多个线程。
- 多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。
- 同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核。
为什么要使用多线程
从总体上而言:
- 从计算机底层而言:线程可看作轻量级的进程,是程序执行的最小单位线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
从计算机底层而言:
- 单核:当单核运行的时候,如果进程中只有一个线程,那么IO阻塞的时候,整合进程都被阻塞。而如果有多线程,那么就可以让别的线程继续使用cpu,从而提高了对系统资源的利用效率
- 多核:让CPU所有的核心都被不同的线程利用到,提高进程利用多核CPU的能力
线程间同步的方式有哪些
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。
下面是几种常见的线程同步的方式:
- 互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
- 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
3, 信号量(Semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。 - 屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 CyclicBarrier 是这种机制。
- 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
PCB 是什么?包含哪些信息?
PCB(Process Control Block) 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。你可以将 PCB 视为进程的大脑。
当操作系统创建一个新进程时,会为该进程分配一个唯一的进程 ID,并且为该进程创建一个对应的进程控制块。当进程执行时,PCB 中的信息会不断变化,操作系统会根据这些信息来管理和调度进程。
PCB主要包括下面几部分内容:
- 进程的描述信息,包括进程的名称、标识符等等;
- 进程的调度信息,包括进程阻塞原因、进程状态(就绪、运行、阻塞等)、进程优先级(标识进程的重要程度)等等;
- 进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。
- 进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。
- 处理机的状态信息(由处理机的各种寄存器中的内容组成的),包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
- ……
进程有哪几种状态
我们一般把进程大致分为 5 种状态,这一点和线程很像,
- 创建状态(new):进程正在被创建
- 就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行
- 运行状态(running)
- 阻塞状态(waiting):又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态(terminated)
进程间的通信方式有哪些?
- 管道/匿名管道(pipes): 有亲缘关系的父子进程间或者兄弟进程之间的通信
- 有名管道(Named Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 先进先出(First In First Out) 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
- 消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
- 共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
- 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
⭐️常见的进程调度算法有哪些
- 先来先服务(FCFS)
- 短作业优先(SJF, Shortest Job First):从就绪队列中选出一个估计运行时间最短的进程为之分配资源
- 时间片轮转法(RR, Round-Robin)
- ⭐️多级反馈队列调度(MFQ, Multi_leveel Feedback Queue)多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
- 优先级调度算法(Priority):为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。
什么是僵尸进程和孤儿进程
在 Unix/Linux 系统中,子进程通常是通过 fork()系统调用创建的,该调用会创建一个新的进程,该进程是原有进程的一个副本。子进程和父进程的运行是相互独立的,它们各自拥有自己的 PCB,即使父进程结束了,子进程仍然可以继续运行。
当一个进程调用 exit()系统调用结束自己的生命时,内核会释放该进程的所有资源,包括打开的文件、占用的内存等,但是该进程对应的 PCB 依然存在于系统中。这些信息只有在父进程调用 wait()或 waitpid()系统调用时才会被释放,以便让父进程得到子进程的状态信息。这样的设计可以让父进程在子进程结束时得到子进程的状态信息,并且可以防止出现“僵尸进程”(即子进程结束后 PCB 仍然存在但父进程无法得到状态信息的情况)。
这两个都是针对子进程的说法
僵尸进程:
子进程已经终止,
但是其父进程仍在运行,且父进程没有调用 wait()或 waitpid()等系统调用来获取子进程的状态信息,释放子进程占用的资源,导致子进程的 PCB 依然存在于系统中,但无法被进一步使用。
这种情况下,子进程被称为“僵尸进程”。
避免僵尸进程的产生,父进程需要及时调用 wait()或 waitpid()系统调用来回收子进程。孤儿进程:一个进程的父进程已经终止或者不存在,但是该进程仍在运行。这种情况下,该进程就是孤儿进程。
孤儿进程通常是由于父进程意外终止或未及时调用 wait()或 waitpid()等系统调用来回收子进程导致的。为了避免孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为 init 进程(进程号为 1),由 init 进程来回收孤儿进程的资源。
如何查看僵尸进程
Linux 下可以使用 Top 命令查找,zombie 值表示僵尸进程的数量,为 0 则代表没有僵尸进程。
下面这个命令可以定位僵尸进程以及该僵尸进程的父进程:
1 | ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]' |
死锁
什么是死锁
多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
产生死锁的四个必要条件
- 互斥:资源必须处于非共享模式,即一次只有一个进程可以使用a。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
- 占有并等待:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
- 非抢占:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
- 循环等待:有一组等待进程 {P0, P1,…, Pn}, P0 等待的资源被 P1 占有,P1 等待的资源被 P2 占有,……,Pn-1 等待的资源被 Pn 占有,Pn 等待的资源被 P0 占有。
注意 ⚠️:这四个条件是产生死锁的 必要条件 ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
解决死锁的方法
解决死锁的方法可以从多个角度去分析,一般的情况下,有预防,避免,检测和解除四种。
- 预防是采用某种策略,限制并发进程对资源的请求,从而使得死锁的必要条件在系统执行的任何时间上都不满足。
- 避免则是系统在分配资源时,根据资源的使用情况提前做出预测,从而避免死锁的发生
- 检测是指系统设有专门的机构,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。
- 解除是与检测相配套的一种措施,用于将进程从死锁状态下解脱出来。
死锁的预防
死锁四大必要条件上面都已经列出来了,很显然,只要破坏四个必要条件中的任何一个就能够预防死锁的发生。破坏第一个条件 互斥条件:使得资源是可以同时访问的,这是种简单的方法,磁盘就可以用这种方法管理,但是我们要知道,有很多资源 往往是不能同时访问的 ,所以这种做法在大多数的场合是行不通的。破坏第三个条件 非抢占:也就是说可以采用 剥夺式调度算法,但剥夺式调度方法目前一般仅适用于 主存资源 和 处理器资源 的分配,并不适用于所有的资源,会导致 资源利用率下降。
预防死锁的方法,是通过考虑破坏第二个条件和第四个条件。
- 静态分配策略静态分配策略可以破坏死锁产生的第二个条件(占有并等待)。所谓静态分配策略,就是指一个进程必须在执行前就申请到它所需要的全部资源,并且知道它所要的资源都得到满足之后才开始执行。进程要么占有所有的资源然后开始执行,要么不占有资源,不会出现占有一些资源等待一些资源的情况。静态分配策略逻辑简单,实现也很容易,但这种策略 严重地降低了资源利用率,因为在每个进程所占有的资源中,有些资源是在比较靠后的执行时间里采用的,甚至有些资源是在额外的情况下才使用的,这样就可能造成一个进程占有了一些 几乎不用的资源而使其他需要该资源的进程产生等待 的情况。
- 层次分配策略层次分配策略破坏了产生死锁的第四个条件(循环等待)。在层次分配策略下,所有的资源被分成了多个层次,一个进程得到某一次的一个资源后,它只能再申请较高一层的资源;当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源,按这种策略,是不可能出现循环等待链的,因为那样的话,就出现了已经申请了较高层的资源,反而去申请了较低层的资源,不符合层次分配策略
死锁的解除
常用的解除死锁的方法有以下四种:
- 立即结束所有进程的执行,重新启动操作系统:这种方法简单,但以前所在的工作全部作废,损失很大。
- 撤销涉及死锁的所有进程,解除死锁后继续运行:这种方法能彻底打破死锁的循环等待条件,但将付出很大代价,例如有些进程可能已经计算了很长时间,由于被撤销而使产生的部分结果也被消除了,再重新执行时还要再次进行计算。
- 逐个撤销涉及死锁的进程,回收其资源直至死锁解除。
- 抢占资源:从涉及死锁的一个或几个进程中抢占资源,把夺得的资源再分配给涉及死锁的进程直至死锁解除。
内存管理
内存管理主要做了什么?
- 内存的分配与回收:对进程所需的内存进行分配和释放,malloc 函数:申请内存,free 函数:释放内存。
- 地址转换:将程序中的虚拟地址转换成内存中的物理地址。
- 内存扩充:当系统没有足够的内存时,利用虚拟内存技术或自动覆盖技术,从逻辑上扩充内存。
- 内存映射:将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容,速度更快。
- 内存优化:通过调整内存分配策略和回收算法来优化内存使用效率。
- 内存安全:保证进程之间使用内存互不干扰,避免一些恶意程序通过修改内存来破坏系统的安全性。
什么是内存碎片?
由内存的申请和释放产生的,通常分为内部内存碎片和外部内存碎片
内部内存碎片(Internal Memory Fragmentation,简称为内存碎片):已经分配给进程使用但未被使用的内存。(进程用剩下的)
外部内存碎片(External Memory Fragmentation,简称为外部碎片):由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。(太小以致没人要的)
常见的内存管理方式有哪些?
- 连续内存管理:为一个用户程序分配一个连续的内存空间,内存利用率一般不高。容易产生内存碎片
- 非连续内存管理:允许一个程序使用的内存分布在离散或者说不相邻的内存中,相对更加灵活一些。
虚拟内存
虚拟内存(Virtual Memory)本质上来说它只是逻辑存在的,是一个假想出来的内存空间
它允许程序访问比实际物理内存更大的内存空间。
在使用虚拟内存的系统中,每个程序都认为它拥有连续的、私有的内存空间,这被称为虚拟地址空间
虚拟内存的主要主要作用:
隔离进程:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
提升物理内存利用率:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。
简化内存管理:进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。
多个进程共享物理内存:进程在运行过程中,会加载许多操作系统的动态库。这些库对于每个进程而言都是公用的,它们在内存中实际只会加载一份,这部分称为共享内存。
提高内存使用安全性:控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。
提供更大的可使用内存空间:可以让程序拥有超过系统物理内存大小的可用内存空间。
这是因为当物理内存不够用时,可以利用磁盘充当,将物理内存页(通常大小为 4 KB)保存到磁盘文件(会影响读写速度),数据或代码页会根据需要在物理内存与磁盘之间移动。
没有虚拟内存会有什么问题
- 用户程序可以直接访问内存,如果不小信息操作到系统运行所需要的内存,造成操作系统崩溃,严重影响系统的安全。
- 同时运行多个程序容易崩溃。因为不同的程序容易有相同内存地址导致冲突
比如你想同时运行一个微信和一个 QQ 音乐,微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就可能会造成微信这个程序会崩溃。 - 程序运行过程中使用的所有数据或指令都要载入物理内存,但是很大部分都是不会用到的,会浪费内存空间
什么是虚拟地址和物理地址?
物理地址(Physical Address) 是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。
但程序中访问的内存地址不是物理地址,而是虚拟地址(Virtual Address)。
操作系统一般通过 CPU 芯片中的一个重要组件MMU(Memory Management Unit,内存管理单元) 将虚拟地址转换为物理地址,这个过程被称为地址翻译/地址转换(Address Translation) 。
什么是虚拟地址空间和物理地址空间?
- 虚拟地址空间是虚拟地址的集合,是虚拟内存的范围。每一个进程都有一个一致且私有的虚拟地址空间。
- 物理地址空间是物理地址的集合,是物理内存的范围。
虚拟地址与物理内存地址是如何映射的?
MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:
- 分段机制
- 分页机制
- 段页机制
其中,现代操作系统广泛采用分页机制,需要重点关注!
分段机制
分段机制以段的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
段表有什么用?地址翻译过程是怎样的?
分段管理通过段表(Segment Table)映射虚拟地址和物理地址。
分段机制下的虚拟地址由两部分组成:
- 段号:标识着该虚拟地址属于整个虚拟地址空间中的哪一个段。
- 段内偏移量:相对于该段起始地址的偏移量。
具体的地址翻译过程如下:
- MMU 首先解析得到虚拟地址中的段号;
- 通过段号去该应用程序的段表中取出对应的段信息(找到对应的段表项);
- 从段信息中取出该段的起始地址(物理地址)加上虚拟地址中的段内偏移量得到最终的物理地址。
MMU解析虚拟地址的段号->通过段号去段表中找到对应的段表项,从段信息中获取该段的起始地址(物理地址)加上虚拟地址中的偏移量得到最终的物理地址
通过段号一定要找到对应的段表项吗?得到最终的物理地址后对应的物理内存一定存在吗?
不一定,段表项可能并不存在:
- 段表项被删除:软件错误、软件恶意行为等情况可能会导致段表项被删除。
- 段表项还未创建:如果系统内存不足或者无法分配到连续的物理内存块就会导致段表项无法被创建。
分段机制容易出现外部内存碎片
分页机制
分页机制(Paging) 把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。现代操作系统广泛采用分页机制。
注意:这里的页是连续等长的,不同于分段机制下不同长度的段。
在分页机制下,应用程序虚拟地址空间中的任意虚拟页可以被映射到物理内存中的任意物理页上,因此可以实现物理内存资源的离散分配。分页机制按照固定页大小分配物理内存,使得物理内存资源易于管理,可有效避免分段机制中外部内存碎片的问题。
- 页表有什么用?地址翻译过程是怎样的?
分页管理通过 页表(Page Table) 映射虚拟地址和物理地址。
分页机制下的虚拟地址由两部分组成:
- 页号:通过虚拟页号可以从页表中取出对应的物理页号;
- 页内偏移量:物理页起始地址+页内偏移量=物理内存地址。
具体的地址翻译过程如下:
MMU解析虚拟地址的页号->通过页号去段表中找到对应的页表项,从页信息中获取该段的起始地址(物理地址)加上虚拟地址中的偏移量得到最终的物理地址
通过虚拟页号也不一定要找到对应的物理页号,可能会存在页缺失
多级页表属于时间换空间的典型场景,利用增加页表查询的次数减少页表占用的空间。结合数据库理解将一张表拆分为两张表,二级列表按需加载,就可以节省空间
段页机制
在段页式机制下,地址翻译的过程分为两个步骤:
段式地址映射。
页式地址映射。
⭐️操作系统师兄面试题
数据结构
算法
数据库
基础
数据库基础知识总结
基本概念
- 元组(tuple) :元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。
- 码:就是能唯一标识实体的属性,对应表中的列
- 候选码:若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。
- 主码:主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。
- 外码:
- 主属性:候选码中出现过的属性称为主属性
- 非主属性: 不包含在任何一个候选码中的属性称为非主属性。
什么是ER图(Entity Relationship Diagram)
数据库的范式
- 1NF(第一范式):属性不可再分。
- 2NF(第二范式):1NF 的基础之上,消除了非主属性对于码的部分函数依赖。
- 3NF(第三范式):3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计,基本上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。
一些重要概念:
- 函数依赖:X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
- 部分函数依赖:如果 X→Y,且存在X的子集X0→Y,则称 Y 对 X 部分函数依赖
- 完全函数依赖: 在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖
- 传递函数依赖: 在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X
为什么不推荐使用外键和级联?
说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
为什么不要用外键呢?大部分人可能会这样回答:
- 增加了复杂性: a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
- 对分库分表不友好:因为分库分表下外键是无法生效的。……
但是,外键也是有很多好处的,比如:
- 保证了数据库数据的一致性和完整性;
- 级联操作方便,减轻了程序代码量;
……
不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分库分表,并发量不是很高的情况还是可以考虑使用外键的。
什么是存储过程?
一些 SQL 语句的集合,中间加了点逻辑控制语句。
存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。
drop、delete 与 truncate 区别?
用法不同
- drop(丢弃数据): drop table 表名,在删除表的时候使用。
- truncate (清空数据) : truncate table 表名 ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。
- delete(删除数据) : delete from 表名 where 列名=值,删除某一行的数据,如果不加 where 子句和truncate table 表名作用类似。
truncate 和不带 where子句的 delete、以及 drop 都会删除表内的数据,
但是 **truncate 和 delete 只删除数据不删除表的结构(定义)**,
执行 drop 语句,此表的结构也会删除,也就是执行drop 之后对应的表不复存在。
属于不同的数据库语言
- truncate 和 drop 属于 DDL(数据定义语言)语句,操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。
- delete 语句是 DML (数据库操作语言)语句,这个操作会放到 rollback segment 中,事务提交之后才生效。
DML 语句和 DDL 语句区别
- DML 是数据库操作语言(Data Manipulation Language)的缩写,是指对数据库中表记录的操作,主要包括表记录的插入、更新、删除和查询,是开发人员日常使用最频繁的操作。
- DDL (Data Definition Language)是数据定义语言的缩写,简单来说,就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。DDL 语句更多的被数据库管理员(DBA)所使用,一般的开发人员很少使用。另外,由于select不会对表进行破坏,所以有的地方也会把select单独区分开叫做数据库查询语言 DQL(Data Query Language)。
NoSQL基础知识总结
NoSQL是什么
Not Only SQL 泛指非关系型的数据库。
主要针对的是键值、文档以及图形类型数据存储。
并且,NoSQL 数据库天生支持分布式,数据冗余和数据分片等特性,旨在提供可扩展的高可用高性能数据存储解决方案。
NoSQL 数据库有哪些类型?
NoSQL 数据库主要可以分为下面四种类型:
- 键值:键值数据库是一种较简单的数据库,其中每个项目都包含键和值。这是极为灵活的 NoSQL 数据库类型,因为应用可以完全控制 value 字段中存储的内容,没有任何限制。Redis 和 DynanoDB 是两款非常流行的键值数据库。
- 文档:文档数据库中的数据被存储在类似于 JSON(JavaScript 对象表示法)对象的文档中,非常清晰直观。每个文档包含成对的字段和值。这些值通常可以是各种类型,包括字符串、数字、布尔值、数组或对象等,并且它们的结构通常与开发者在代码中使用的对象保持一致。MongoDB 就是一款非常流行的文档数据库。
- 图形:图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。图形数据库的典型使用案例包括社交网络、推荐引擎、欺诈检测和知识图形。Neo4j 和 Giraph 是两款非常流行的图形数据库。’
- 宽列:宽列存储数据库非常适合需要存储大量的数据。
字符集详解
MySQL 字符编码集中有两套 UTF-8 编码实现:utf8 和 utf8mb4。
如果使用 utf8 的话,存储 emoji 符号和一些比较复杂的汉字、繁体字就会出错。
为什么会这样呢?
字符编码是什么?
字符编码是一种将字符集中的字符与计算机中的二进制数据相互转换的方法,可以看作是一种映射规则。也就是说,字符编码的目的是为了让计算机能够存储和传输各种文字信息。
乱码的本质:编码和解码时用了不同或者不兼容的字符集 。
SQL
SQL语法基础知识总结
SQL 分类
- DDL 数据定义语言
DDL 的核心指令是 CREATE、ALTER、DROP。
DDL 的主要功能是定义数据库对象。 - DML 数据操纵语言
DML 的主要功能是 访问数据,因此其语法都是以读写数据库为主。
DML 的核心指令是 INSERT、UPDATE、DELETE、SELECT。这四个指令合称 CRUD(Create, Read, Update, Delete),即增删改查。 - TCL 事务控制语言
TCL 的核心指令是 COMMIT、ROLLBACK。 - DCL 数据控制语言
数据控制语言 (Data Control Language, DCL) 是一种可对数据访问权进行控制的指令,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。
DCL 的核心指令是 GRANT、REVOKE。DCL 以控制用户的访问权限为主,因此其指令作法并不复杂,可利用 DCL 控制的权限有:CONNECT、SELECT、INSERT、UPDATE、DELETE、EXECUTE、USAGE、REFERENCES。
MySQL
整数类型的 UNSIGNED 属性有什么用?
MySQL 中的整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数
CHAR 和 VARCHAR 的区别是什么?
CHAR 是定长字符串,VARCHAR 是变长字符串。
VARCHAR(100)和 VARCHAR(10)的区别是什么?
VARCHAR(100)和 VARCHAR(10)都是变长类型,表示能存储最多 100 个字符和 10 个字符。
DECIMAL 和 FLOAT/DOUBLE 的区别是什么?
DECIMAL 和 FLOAT 的区别是:DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。
DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。
DATETIME 和 TIMESTAMP 的区别是什么?
DATETIME 类型没有时区信息
TIMESTAMP 和时区有关。
NULL 和 ‘’ 的区别是什么?
- NULL 代表一个不确定的值,就算是两个 NULL,它俩也不一定相等。
例如,SELECT NULL=NULL的结果为 false,但是在我们使用DISTINCT,GROUP BY,ORDER BY时,NULL又被认为是相等的 - ‘’的长度是 0,是不占用空间的,而NULL 是需要占用空间的。
- NULL 会影响聚合函数的结果。
- 符。而’’是可以使用这些比较运算符的。
综上,MySQL 不建议使用 NULL 作为列默认值。
Boolean 类型如何表示?
MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。
MySQL基础架构
Redis
常用框架
系统设计
分布式
高性能
高可用
前端
Axios
Axios 是一个流行的基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js 环境。它使得在 JavaScript 中发送 AJAX 请求变得更加简单和便捷。
使用 Axios,你可以轻松地执行各种 HTTP 请求,例如 GET、POST 等,并处理响应数据。
Java中session和cookie的使用
Session简单介绍
[!TIP]
session通常是基于cookie实现的,每一个session都会有一个sessionid保存在浏览器的cookie中
session和cookie都是Java开发中实现会话跟踪的技术。
在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。
因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,为用户服务。
Cookie简单介绍
浏览器与WEB服务器之间是使用HTTP协议进行通信的,当某个用户发出页面请求时,WEB服务器只是简单的进行响应,然后就关闭与该用户的连接。
因此当一个请求发送到WEB服务器时,无论其是否是第一次来访,服务器都会把它当作第一次来对待,这样的不好之处可想而知。为了弥补这个缺陷,Netscape开发出了cookie这个有效的工具来保存某个用户的识别信息,因此人们昵称为“小甜饼”。
cookies是一种WEB服务器通过浏览器在访问者的硬盘上存储信息的手段:Netscape Navigator使用一个名为cookies.txt本地文件保存从所有站点接收的Cookie信息;而IE浏览器把Cookie信息保存在类似于C:\windows\cookies的目录下。当用户再次访问某个站点时,服务端将要求浏览器查找并返回先前发送的Cookie信息,来识别这个用户。
⭐️Session和Cookie的主要区别
Cookie 和 Session 都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。
Cookie一般用来保存用户信息
我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了
一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了
这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可
(为了安全考虑,重新登录一般要将 Token 重写)登录一次网站后访问网站其他页面不需要重新登录。Session 的主要作用就是通过服务端记录用户的状态。
典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。
服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。
Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。
Cookie 存储在客户端中,而 Session 存储在服务器上,相对来说 Session 安全性更高。
如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。
Session实现原理
服务器创建session出来后,会把session的id号,以cookie的形式回写给客户机。
这样,只要客户机的浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户机浏览器带session id过来了,就会使用内存中与之对应的session为之服务。
Session的创建和销毁时机
- session对象的创建
在程序中第一次调用request.getSession()方法时就会创建一个新的Session,可以用isNew()方法来判断Session是不是新创建的
- session对象的销毁
session对象默认30分钟没有使用,则服务器会自动销毁session,在web.xml文件中可以手工配置session的失效时间