对于 Object 来说,equals 是用 == 实现的,所以二者是相同的,都是用来比较两个对象的引用是否相同的,但 Java 中的其他类,都会重写 equals 让其变为值比较,而非引用比较,如 Integer 和 String 都是这样。
重载:在同一类中
1.方法名必须相同
2.参数类型不同
3.参数数量不同
4.参数顺序不同
5.方法返回值和访问修饰符可以不同
重写:是子类对父类允许访问的方法进行重新编写
1.方法名必须相同
2.参数列表必须相同
3.子类返回值范围应比父类的更小或相等
4.访问修饰符范围应该大于或等于父类
1.定义的关键字不同:抽象类为abstract,接口为interface
2.方法:抽象类可以包含抽象方法和具体方法,接口只能包含方法的声明(抽象方法)
3.方法的访问修饰符:抽象类无限制,只是里面的抽象方法不能用private
接口有限制,默认的public方法
4.实现:一个类只能继承一个抽象类但可以实现多个接口
5.变量:抽象类可以包含实例变量和静态变量,接口只能包含常量
6.构造函数:抽象类可以有构造函数,接口不能有构造函数
Integer会缓存-128到127的对象到常量池
之后不论是new还是什么,Integer只要在这个范围内只要值相同,他们都是同一个对象
比如:
Integer a=Integer.valueOf(127);
Integer b=Integer.valueOf(127);
a==b为true
Float和Double没有缓存机制
final:
是个修饰符,可以用于修饰类,方法和变量
修饰类时,该类不能被继承,即为最终类
修饰方法时,该方法不能被子类重写
修饰变量时,表示该变量是一个常量,其值不能被修改
finally:
是一个关键字,用于定义一个代码块,通常与try-catch结构一起使用
finally块中的代码无论是否抛出异常,都会被执行
finally块通常用于释放资源、关闭连接或执行必要的清理操作
finalize:
finalize是Object类中的一个方法,被用于垃圾回收机制
finalize方法在对象被垃圾回收之前被调用,用于进行资源释放或其他清理操作
通常情况下,我们不需要显式地调用finalize方法,而是交由垃圾回收器自动调用
finally中的方法一定会执行吗?
正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到 System.exit() 方法或 Runtime.getRuntime().halt() 方法,或者是 try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题,finally 中的代码是不会执行的。而 exit() 方法会执行 JVM 关闭钩子方法或终结器,但 halt() 方法并不会执行钩子方法或终结器。
finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行
另外,在以下 2 种特殊情况下,finally
块的代码也不会被执行:
不会执行的情况:
1.System.exit()
2.Runtime.getRuntime().halt()
3.try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题
什么是多态?
多态是面向对象编程中的一个重要概念,它允许通过父类类型的引用变量来引用子类对象,并在运行时根据实际对象的类型来确定调用哪个方法。换句话说,一个对象可以根据不同的情况表现出多种形态。
通过多态,我们可以利用父类类型的引用变量来指向子类对象,并根据实际对象的类型调用对应的方法。这样可以在不修改现有代码的情况下,动态地切换和扩展对象的行为。
特点和优势:
实现原理:
多态的实现原理主要是依靠“动态绑定”和“虚拟方法调用”,它的实现流程如下:
动态绑定:
动态绑定(Dynamic Binding):指的是在编译时,Java 编译器只能知道变量的声明类型,而无法确定其实际的对象类型。而在运行时,Java 虚拟机(JVM)会通过动态绑定来解析实际对象的类型。这意味着,编译器会推迟方法的绑定(即方法的具体调用)到运行时。正是这种动态绑定机制,使得多态成为可能。
虚拟方法调用:
虚拟方法调用(Virtual Method Invocation):在 Java 中,所有的非私有、非静态和非 final 方法都是被隐式地指定为虚拟方法。虚拟方法调用是在运行时根据实际对象的类型来确定要调用的方法的机制。当通过父类类型的引用变量调用被子类重写的方法时,虚拟机会根据实际对象的类型来确定要调用的方法版本,而不是根据引用变量的声明类型。
实例化:普通类可以直接实例化,而抽象类不能直接实例化。
方法:抽象类中既包含抽象方法又可以包含具体的方法,而普通类只能包含普通方法。
实现:普通类实现接口需要重写接口中的方法,而抽象类可以实现接口方法也可以不实现。
可变性:
线程安全性:
性能:
总结:
综上,如果在单线程环境下进行字符串操作,且不需要频繁修改字符串,推荐使用String;如果在多线程环境下进行字符串操作,或者需要频繁修改字符串,优先考虑使用StringBuffer;如果在单线程环境下进行频繁的字符串拼接和修改,推荐使用StringBuilder以获取更好的性能。
形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。
ArrayList内部基于动态数组(Object数组)实现,比 Array(静态数组) 使用起来更加灵活
ArrayList只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。
ArrayList是有序的。
ArrayList是线程不安全的,但是效率高。
ArrayList如果使用无参构造器,初始容量是0,当第一次添加时,容量变为10,当需要扩容时1.5倍扩容。
如果使用指定大小构造器,则初始容量为指定大小,需要扩容时直接1.5倍扩容。
ArrayList与Vector区别?
答:Vector是线程安全的,ArrayList是线程不安全的。
Vector无参构造器时默认是10容量,需要扩容时2倍扩容,指定容量有参构造的话直接为指定大小,2倍扩容
时间复杂度:
ArrayList查询O(1) 插入和删除O(n)
Linkedlist查询O(n) 插入和删除O(1)
在 JDK 1.7 时,HashMap 底层是通过数组 + 链表实现的;
而在 JDK 1.8 时,HashMap 底层是通过数组 + 链表或红黑树实现的。
链表升级为红黑树:
在 JDK 1.8 之后,HashMap 默认是先使用数组 + 链表存储数据,但当满足以下两个条件时:
为了(查询)的性能考虑会将链表升级为红黑树进行存储,具体执行流程如下:
红黑树退化为链表:
当进行了删除操作,导致**红黑树的节点小于等于 6 时,会发生退化**,将红黑树转换为链表。这是因为当节点数量较少时,红黑树对性能的提升并不明显,反而占用了更多的内存空间。具体执行流程如下:
扩容机制:
第一次添加时table数组扩容到16,当数组元素到达16*负载因子0.75=12(临界值)时便会对数组进行扩容
扩容是**2倍扩容**
hashset比较添加的元素是否重复是判断**hash()相同且equals()**相同
对于hashmap中链表添加节点是使用头插法,这导致每次扩容后链表里的元素顺序会反转
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
HashSet 实现了 Set 接口,只存储对象;HashMap 实现了 Map 接口,用于存储键值对。
HashSet 底层是用 HashMap 存储的,HashSet 封装了一系列 HashMap 的方法,HashSet 将(自己的)值保存到 HashMap 的 Key 里面了。
HashSet 不允许集合中有重复的值(如果有重复的值,会插入失败),而 HashMap 键不能重复,值可以重复(如果键重复会覆盖原来的值
在 Java 中,反射是指在运行时检查和操作类、接口、字段、方法等程序结构的能力。通过反射,可以在运行时获取类的信息,创建类的实例,调用类的方法,访问和修改类的字段等。
反射使用场景:
编程开发工具的代码提示,如 IDEA 或 Eclipse 等,在写代码时会有代码(属性或方法名)提示,这就是通过反射实现的。
很多知名的框架如 Spring,为了让程序更简洁、更优雅,以及功能更丰富,也会使用到反射,比如 Spring 中的依赖注入就是通过反射实现的。
数据库连接框架也会使用反射来实现调用不同类型的数据库(驱动)。
反射的关键实现方法有以下几个:
得到类:Class.forName("类名")
得到所有字段:getDeclaredFields()
得到所有方法:getDeclaredMethods()
得到构造方法:getDeclaredConstructor()
得到实例:newInstance()
调用方法:invoke()
深克隆的实现方法有很多,比如以下几个:
语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及 static
所修饰;但是,成员变量和局部变量都能被 final
所修饰。
存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static
修饰的,那么这个成员变量是属于类的,如果没有使用 static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值,局部变量如果我们不手动赋初始值会报错。
1.面向对象(封装 继承 多态)
2.平台无关性(java虚拟机实现平台无关性)
3.支持多线程
4.可靠性(具备异常处理和自动内存管理机制)
5.安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源)、
6.解释与编译并存(编译阶段:java文件编译成.class字节码文件 解释阶段:JVM加载字节码文件将其解释为机器码,然后在计算机上执行)
JIT:即时编译 有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用
JDK:java开发工具包
包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等
JRE: java 运行时环境
主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)
JVM:java虚拟机
执行Java字节码,将其解释成底层的机器码,然后在宿主机上运行
JIT:
JIT(Just-In-Time)编译器是一种在程序运行时将字节码编译成本地机器码的技术。它是Java虚拟机(JVM)的一部分,用来提高Java程序的执行效率。 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 标识符 。简单来说, 标识符就是一个名字 。
有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 关键字 。简单来说,关键字是被赋予特殊含义的标识符 。
8种。
6种数字类型:
4种整数型:byte,shot,int,long(默认值0 0 0 0L)
2中浮点型:float,double(默认值 0.0f 0.0d)
1中字符类型:char(默认值\u0000)
1中布尔类型:boolean(默认值false)
null
,而基本类型有默认值且不是 null
==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals()
方法。静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而==非静态成员属于实例对象==,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
面向对象的优点:易维护,易复用,易扩展
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。
构造方法具有以下特点:
void
声明。构造方法不能被重写(override),但可以被重载(overload)。因此,一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String s1 = new String("abc");这句话创建了几个字符串对象?
答:会创建 1 或 2 个字符串对象。
Exception
:程序本身可以处理的异常,可以通过 catch
来进行捕获。
Error
:Error
属于程序无法处理的错误。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译。
RuntimeException
及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)String getMessage()
: 返回异常发生时的简要描述
String toString()
: 返回异常发生时的详细信息
String getLocalizedMessage()
: 返回异常对象的本地化信息。使用 Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()
返回的结果相同
void printStackTrace()
: 在控制台上打印 Throwable
对象封装的异常信息
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList
对象只能传入 Person
对象,如果传入其他类型的对象就会报错。
Annotation
(注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了Annotation
的特殊接口:
JDK 提供了很多内置的注解(比如 @Override
、@Deprecated
),同时,我们还可以自定义注解。
注解只有被解析之后才会生效,常见的解析方法有两种:
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。@Value
、@Component
)都是通过反射来进行处理的。Java 中将实参传递给方法(或函数)的方式是 值传递:
java不引入引用传递是因为:
List
: 存储的元素是有序的、可重复的。
Set
: 存储的元素不可重复的。
Queue
: 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
Map
: 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
HashMap 线程不安全主要体现在以下两方面:
JDK1.7 的 ConcurrentHashMap:
JDK1.8 的 ConcurrentHashMap:
JDK1.8 的 ConcurrentHashMap
不再是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。
JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
Segment
分段锁来保证安全, Segment
是继承自 ReentrantLock
。JDK1.8 放弃了 Segment
分段锁的设计,采用 Node + CAS + synchronized
保证线程安全,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点。在 Java 中,异常(Exception)主要分为两大类:
受检异常:
描述: 受检异常是编译时异常,必须在代码中显式处理(通过 try-catch
块或通过 throws
子句声明抛出)。这些异常通常是由于外部因素引起的,如文件未找到、数据库连接失败等,程序需要对这些异常进行处理,以确保程序的健壮性。
常见的例子
:
IOException
SQLException
ClassNotFoundException
运行时异常:
描述: 运行时异常是未受检异常,通常是由编程错误引起的,例如除零错误、空指针引用、数组越界等。这类异常在编译时不强制要求处理,可以选择不捕获或抛出。运行时异常一般表示程序员的逻辑错误,因此往往需要通过调试或重构代码来解决。
常见的例子:
NullPointerException
ArrayIndexOutOfBoundsException
ArithmeticException
ConcurrentModificationException
并发修改异常封装 继承 多态
封装:
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
继承:
不同类型的对象,相互之间经常有一定数量的共同点。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
关于继承如下 3 点请记住:
多态:
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
多态的特点:
如果仅使用 hashCode
的低位来计算数组的索引,容易发生冲突(即不同的键映射到同一个索引)。这种冲突会导致多个键值对被放在同一个桶中,从而退化为链表或红黑树,降低性能。
通过右移16位,将 hashCode
的高位和低位混合在一起,可以使哈希值更加均匀分布,减少冲突的概率
进程和线程的区别也是很明显的,进程是==资源分配的最小单位==,线程是 CPU 调度的最小单位。具体来说:
**进程(Process)是指正在运行的一个程序的实例。**每个进程都拥有的资源:堆、栈、虚存空间(页表)、文件描述符等信息。在 Java 中,每个进程都由一个主线程(main thread)启动。当进程运行时,操作系统会为其分配一个进程号,并将其作为一个独立的实体来进行管理。
**线程(Thread)是指进程中的一个执行单元,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。**在 Java 中,每个线程都拥有自己的栈空间和程序计数器,并且可以访问共享的堆内存。
程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。
我们上面提到了用户线程和内核线程,考虑到很多读者不太了解二者的区别,这里简单介绍一下:
顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。
一句话概括 Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程。
答:通过线程池来管理线程资源,如果是对外提供服务的接口不使用线程池来创建,有可能在高并发的场景下创建大量的线程从而导致过度的消耗系统和资源,甚至拉垮系统。使用线程池的好处可以减少线程的重复创建与销毁,进而节省系统的资源开销。
6种。
NEW: 初始状态,线程被创建出来但没有被调用 start()
。
RUNNABLE: 运行状态,线程被调用了 start()
等待运行的状态。
BLOCKED:阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
当线程执行 wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
当线程进入 synchronized
方法/块或者调用 wait
后(被 notify
)重新进入 synchronized
方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。
线程在执行完了 run()
方法之后将会进入到 TERMINATED(终止) 状态。
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
sleep()
, wait()
等。这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
共同点:两者都可以暂停线程的执行。
区别:
sleep()
方法没有释放锁,而 wait()
方法释放了锁 。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()
或者 notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout)
超时后线程会自动苏醒。sleep()
是 Thread
类的静态本地方法,wait()
则是 Object
类的本地方法。为什么这样设计呢?下一个问题就会聊到。可中断锁和不可中断锁有什么区别?
ReentrantLock
就属于是可中断锁。synchronized
就属于是不可中断锁。lock的3个方法 lock.lock()
上锁 ,lock.unlock()
解锁和lock.trylock()
尝试获取锁,而不会一直阻塞
Lock默认是非公平锁,我们在构造时参数为true可以设置lock为公平锁
公平锁:非常公平,先来后到排队,后来的线程肯定后执行
非公平锁:即时前面的线程排了很久队,后来了一个线程一样和他们竞争抢锁
Lock用法:
Lock lock = new ReentrantLock();
lock.lock();// 加锁
try {
// 业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了" +
(50 - (--number)) + "张票,剩余:" + number + "张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();// 解锁
}
synchronized的锁对象是方法的调用者
class Test1{
public synchronized void sendMsg(){
System.out.println("发短信");
}
public synchronized void callPhone(){
System.out.println("打电话");
}
}
class Main1{
Test1 test=new Test1();
new Thread(()->{
test.sendMsg();
},"t1").start();
new Thread(()->{
test.callPhone();
},"t2").start();
}
上面两个方法用的是同一个锁,谁先拿到执行谁,先拿到的没执行完,别的线程调另一方法不会执行,会一直等待
每个对象都有对象头 对象头里有锁的状态 用数字区别
静态方法加synchronized的话锁的就是Class类模板
一些集合类如arrayList在并发下是不安全的,如果我们多线程来向一个list中add元素会抛出ConcurrentModificationException并发修改异常
解决方案:
1.Vector
2.Collections.synchronizedList(new ArrayList<>())
3.CopyOnWriteArrayList
CopyOnWriteArrayList:
写入时复制
CopyOnWriteArrayList比Vector强在哪里?
Vector使用的synchronized关键字,效率低很多,CopyOnWriteArrayList使用的lock锁
CopyOnWriteArrayList适用于读多写少
CountDownLatch:
import java.util.concurrent.CountDownLatch;
// 计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6,必须要执行任务的时候,再使用!
CountDownLatch countDownLatch = new CountDownLatch(6);//1.创建countDownLatch初始化
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()
+" Go out");
countDownLatch.countDown(); //2.减一操作
},String.valueOf(i)).start();
}
countDownLatch.await(); // 3.等待计数器归零,然后再向下执行
System.out.println("Close Door");
}
CyclicBarrier:循环栅栏
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
/*
* 集齐7颗龙珠召唤神龙
*/
// 召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功!");//等七个线程都执行到cyclicBarrier.await()再执行这句
});
for (int i = 1; i <=7 ; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
try {
cyclicBarrier.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore:信号量
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位! 限流!、
// 如果已有3个线程执行(3个车位已满),则其他线程需要等待‘车位’释放后,才能执行!
Semaphore semaphore = new Semaphore(3);//信号量为3
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
try {
semaphore.acquire();// acquire() 得到
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // release() 释放
}
},String.valueOf(i)).start();
}
}
}
使用:
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//创建
readWriteLock.writeLock().lock();//写锁
readWriteLock.writeLock().unlock();
readWriteLock.readLock().lock();//读锁
readWriteLock.readLock().unlock();
写锁只能一个一个线程依次执行,读锁可以多个线程同时执行
疑问:那读的时候为什么还要加读锁呢,不加锁不也是多个线程同时读吗?
答:加了读锁就会防止读的时候有写入操作导致幻读
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
SynchronousQueue 同步队列:
BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队列
没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
put、take
从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。
单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程:
线程池(new ThreadPoolExecutor(7个参数))---最大的线程如何去设置:
io密集型:判断程序中十分耗io的线程数,设置最大线程数大于这个数(通常是两倍)
cpu密集型:cpu几核就设置最大线程为几,可以保持cpu效率最高
Runtime.getRuntime().availableProcessors()//获取cpu核数
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
死锁的四个必要条件:
破坏四个条件中的任意一个都能破解死锁
如何发现死锁?
1、使用jsp -l定位进程号
2、使用jstack 进程号找到死锁问题
3大方法:
Executors.newSingleThreadExecutor();// 创建单个线程的线程池
Executors.newFixedThreadPool(5);// 创建一个固定大小的线程池
Executors.newCachedThreadPool();// 创建一个可伸缩的线程池
☆但是建议使用底层方法==new ThreadPoolExecutor(7个参数)==来创建线程池,防止oom
在Java中,使用Executors
类提供的静态方法创建线程池时,可能会导致OOM(OutOfMemoryError)的问题,主要是由于其默认的线程池配置可能不够灵活,导致资源管理不当。以下是一些可能导致OOM的原因:
Executors
类提供的一些静态方法,例如Executors.newFixedThreadPool()
创建的线程池是固定大小的,如果提交的任务量超过了线程池的容量,那么任务会被放入无界队列中,导致内存消耗过多。如果持续提交任务,队列可能会无限增长,最终导致内存耗尽。Executors
类创建的线程池通常使用无界队列,例如LinkedBlockingQueue
,这意味着队列可以无限增长。如果生产者速度快于消费者速度,队列会持续增长,最终导致内存耗尽。Executors.newCachedThreadPool()
创建的线程池是一个可缓存的线程池,它会根据需求动态调整线程数量。但是,如果任务提交速度过快,导致线程数持续增长,最终可能导致内存耗尽。相比之下,使用ThreadPoolExecutor
类直接创建线程池能够提供更灵活的配置选项,例如指定核心线程数、最大线程数、队列类型以及拒绝策略等。通过合理地配置这些参数,可以更好地控制线程池的行为,避免OOM问题。
7大参数:
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue,// 阻塞队列
ThreadFactory threadFactory,// 线程工厂:创建线程的,一般 不用动
RejectedExecutionHandler handle )// 拒绝策略
举例:
//手动创建一个线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
5,
3,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());//七个参数
}
4种拒绝策略:
new ThreadPoolExecutor.AbortPolicy()
队列满了,还有任务进来,不处理该任务,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy()
哪来的去哪,比如是main来的任务,队列满了,交回给让main处理
new ThreadPoolExecutor.DiscardPolicy()
队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy()
队列满了,尝试去和最早的竞争,也不会抛出异常!
没有返回值的 runAsync 异步回调
// 没有返回值的 runAsync 异步回调
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName()+"runAsync=>Void");
});
System.out.println("1111");
completableFuture.get(); // 获取阻塞执行结果
有返回值的 supplyAsync 异步回调
completableFuture.whenComplete()获取成功的返回结果
completableFuture.exceptionally()获取到错误的返回结果
// 有返回值的 supplyAsync 异步回调
// ajax,成功和失败的回调
// 返回的是错误信息;
CompletableFuture<Integer> completableFuture =
CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()
+"supplyAsync=>Integer");
int i = 10/0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t=>" + t); // 正常的返回结果
System.out.println("u=>" + u);
// 错误信息:
// java.util.concurrent.CompletionException:
// java.lang.ArithmeticException: / by zero
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return 233; // 可以获取到错误的返回结果
}).get());
JMM是个概念,是不存在的东西,是一种约定
屏蔽各种硬件或系统内存的访问差异,实现java程序在各平台达到一致的内存访问效果
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁。
线程 工作内存 、主内存
8种操作:
1.可见性:如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
2.不保证原子性:虽然 volatile
变量保证了对它的操作是立即可见的,但它并不能保证对该变量的操作是原子的。如果一个变量的操作需要保证原子性,通常需要使用 synchronized 关键字(或者lock锁)或者 java.util.concurrent.atomic
包中的原子类
3.禁止指令重排:
什么是指令重排?答:我们写的程序,计算机并不是按照你写的那样去执行的,源代码 —> 编译器优化的重排 —> 指令并行也可能会重排 —> 内存系统也会重排 ——> 执行
volatile 可以避免指令重排:
内存屏障。CPU指令。作用:
所以本题答案可回答:volatile 是可以保证可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
volatile 关键字在底层的实现主要是通过内存屏障(memory barrier)来实现的。内存屏障是一种 CPU 指令,用于强制执行 CPU 的内部缓存与主内存之间的数据同步。
在 Java 中,当线程读取一个 volatile 变量时,会从主内存中读取变量的最新值,并把它存储到线程的工作内存中。当线程写入一个 volatile 变量时,会把变量的值写入到线程的工作内存中,并强制将这个值刷新到主内存中。这样就保证了 volatile 变量的可见性和有序性。
CAS(Compare and Swap 比较和交换)是一种用于实现多线程同步的原子操作。它是一种乐观锁的实现方式,在并发编程中广泛应用。
实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是Unsafe。
CAS是CPU的并发原语!java无法操作内存,但是java可以调用c++让c++操作内存 unsafe类就是java的后门,可以通过unsafe类操作内存
自带原子性
缺点:1.循环会耗时 2.一次性只能保证一个共享变量的原子性 3.会存在ABA问题
尽管自旋会导致CPU循环等待,但它不会导致线程进入阻塞状态,而是占用CPU资源进行忙等待。
CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了AtomicReference(原子引用)类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用AtomicReference
来执行 CAS 操作。
除了 AtomicReference
这种方式之外,还可以利用加锁来保证。
ABA问题(狸猫换太子)
ABA 问题是在使用 CAS(Compare and Swap)操作时可能遇到的一种情况,它主要涉及到原子操作的可见性和一致性问题。
具体来说,ABA 问题指的是在一个线程读取一个变量的值 A,然后另一个线程修改该变量的值为 B,再次修改回 A,此时第一个线程再次读取该变量的值时,由于值仍然是 A,所以它会错误地认为该变量的值并未被修改过。这样一来,尽管变量的值发生了变化,但是由于在修改前后的值都是 A,因此 CAS 操作可能会误认为没有其他线程修改过该变量。
ABA 问题可能导致一些意外的行为和数据不一致性。例如,在使用 CAS 进行某种状态判断时,可能会出现误判,从而导致程序的行为不符合预期。这对于需要保证数据的一致性和正确性的场景来说是一个潜在的风险。
为了解决 ABA 问题,可以使用版本号或者时间戳等机制来确保 CAS 操作的原子性和一致性。通过引入一个额外的变量,记录每次变量的修改次数或者修改时间戳,可以在进行 CAS 操作时检查这个变量,从而避免 ABA 问题。
在 Java 中,引入==原子引用==,AtomicStampedReference
类提供了一种解决 ABA 问题的方案,它可以在 CAS 操作时同时比较引用和版本号,从而避免了 ABA 问题的发生。
如何预防死锁? 破坏死锁的产生的必要条件即可:
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
public class Singleton {
private volatile static Singleton uniqueInstance;//volatile防止指令重排
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
volatile防止指令重排:
因为对于uniqueInstance = new Singleton();//这句代码可以分为3步
/*
1.申请内存空间
2.创建对象
3.将内存空间指向对象
正常顺序是123 但是指令重排可能会变为132,这样会导致第一个线程执行了13没执行2,这时第二个线程进来认为uniqueInstance != null直接返回该对象了,但这时该对象还没创建,是一片虚无
*/
1.悲观锁:
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
像 Java 中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。
2.乐观锁:
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
在 Java 中java.util.concurrent.atomic
包下面的原子变量类(比如AtomicInteger
、LongAdder
)就是使用了乐观锁的一种实现方式 CAS 实现的。
各自比较:
悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder
),也是可以考虑使用乐观锁的,要视实际情况而定。
乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic
包下面的原子变量类)。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
1.无锁状态 当前无任何线程获取它,所以此过程不需要加锁,是无锁状态。当一个线程访问一个同步块时,如果该同步块没有被其他线程占用,那么该线程就可以直接进入同步块,并且将同步块标记为偏向锁状态。
2.偏向锁状态 在偏向锁状态下,同步块已经被一个线程占用,其他线程访问该同步块时,只需要判断该同步块是否被当前线程占用,如果是,则直接进入同步块。这个过程不需要进行任何加锁操作,仍然属于乐观锁状态。
3.轻量级锁状态 如果在偏向锁状态下,有多个线程竞争同一个同步块,那么该同步块就会升级为轻量级锁状态。此时,每个线程都会在自己的 CPU 缓存中保存该同步块的副本,并通过 CAS(Compare and Swap)操作来对同步块进行加锁和解锁。这个过程需要进行加锁操作,但相对于传统的 mutex 锁,轻量级锁的效率要高很多。
4.重量级锁状态 轻量级锁之后会通过自旋来获取锁,自旋执行一定次数之后还未成功获取到锁,此时就会升级为重量级锁,并且进入阻塞状态。
volatile
关键字是线程同步的轻量级实现,所以 volatile
性能肯定比synchronized
关键字要好 。但是 volatile
关键字只能用于变量而 synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而 synchronized
关键字解决的是多个线程之间访问资源的同步性。是一个抽象类,主要用来构建锁和同步器(提供了线程同步的底层实现机制)
AQS为构建锁和同步器提供了一些通用功能的实现
AQS思想:
如果被请求的共享资源空闲,便将当前线程设为有效线程并将共享资源锁定。
如果共享资源被占用,就需要一套线程阻塞等待和被唤醒时锁分配机制,该机制AQS是基于CLH锁(是对自旋锁的改进,虚拟双向队列)实现的
基于AQS的常见工具类:
AQS提供了两种锁:共享锁和排他锁
Lock中的ReetrantLock用到了排他锁功能
Semaphore 和CountDownLatch 用到了共享锁
用互斥变量state记录锁竞争的状态,0为无锁,1为有锁。state
变量由 volatile
修饰,用于展示当前临界资源的获锁情况。
未获得锁的线程会阻塞到一个双向链表,遵循FIFO原则
以可重入的互斥锁 ReentrantLock 为例,它的内部维护了一个 state 变量,用来表示锁的占用状态。state 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 lock() 方法时,会尝试通过 tryAcquire() 方法独占该锁,并让 state 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加)。
这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。
不能。不过可以在构造方法内部使用synchronized代码块
构造方法本身是线程安全的,但如果在构造方法中涉及共享资源的操作,就需要采取适当的措施来保证整个构造过程的线程安全。
synchronized底层原理属于JVM层面的东西
查看字节码文件发现:
可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
JDK 提供的所有现成的 Lock
实现类,包括 synchronized
关键字锁都是可重入的。
写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。
另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。
通过ThreadLocal可以使每一个线程都有自己的专属本地变量
原理:
每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后最好手动调用remove()
方法
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。
ThreadPoolExecutor
默认不会回收核心线程,即使它们已经空闲了。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。但是,如果线程池是被用于周期性使用的场景,且频率不高(周期之间有明显的空闲时间),可以考虑将 allowCoreThreadTimeOut(boolean value)
方法的参数设置为 true
,这样就会回收空闲(时间间隔由 keepAliveTime
指定)的核心线程了。
线程通讯指的是多个线程之间通过共享内存或消息传递等方式来协调和同步它们的执行。
如何通讯:
实现方法:
1.wait() 和 notify()
2.Semaphore 类
3.CyclicBarrier 类
4.Lock 接口和 Condition 接口来实现
死锁的常用解决方案有以下两个:
主要用来解决==多线程环境下的协调与同步问题==
public class Duoxiancheng {
public static void main(String[] args) {
Mark mark = new Mark();
//消费者线程
new Thread(()->{
int i=0;
while(i<20){
try {
mark.add();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
i++;
}
}).start();
//生产者线程
new Thread(()->{
int j=0;
while (j<20){
try {
mark.remove();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
j++;
}
}).start();
}
}
class Mark{
int data=0;
public synchronized void add() throws InterruptedException {
while(data!=0){
this.wait();
}
data++;
System.out.println("消费者消费了,现在为0");
this.notifyAll();
}
public synchronized void remove() throws InterruptedException {
while(data==0){
this.wait();
}
data--;
System.out.println("生产者生产了,现在为1");
this.notifyAll();
}
}
Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。
我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,Spring 提供的核心功能主要是 IoC 和 AOP。
Spring 包含了多个功能模块(上面刚刚提到过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。
spring的核心特点:
ioc(inversion of control)控制反转,是spring框架的核心概念之一
是一种设计思想而不是一种具体的技术实现,ioc思想就是将原本在程序中手动创建对象的控制权,交由spring框架来管理。
在传统编程模式下,对象之间的创建、组装和管理都是由开发人员手动完成的,而在ioc模式下,这些责任都委托给了ioc容器来管理。
在ioc模式中,本该由开发人员手动控制对象的依赖关系变为了由容器自动注入。这样反转的控制1.使得各模块解耦 2.提高代码灵活性、可维护性和可测试性。
ioc的实现依赖于ioc容器这个组件。ioc容器负责创建和管理对象,以及解决对象之间的依赖关系。
IoC 容器通过以下两种主要的方式来实现控制反转:
ioc的优点?
总之ioc是Spring框架的基石,是Spring框架众多特性的基础。
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制、缓存控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
AOP 的实现依赖于以下几个概念:
优点:
AOP的实现依靠动态代理技术(JDK 动态代理和 CGLIB动态代理):
对于基于接口的目标类,Spring 使用 JDK 动态代理;
对于没有实现接口的目标类,Spring 使用 CGLIB 生成子类来实现代理。
SpringAOP和AspectJ:
SpringAOP和AspectJ是AOP面向切面编程的两种实现方式,两者并没有特别强的关系。
AspectJ是基于编译器实现的,当一个类编译成字节码文件的时候,会将定义的一些额外逻辑织入字节码文件中;
SpringAOP是基于动态代理机制实现的,使用动态代理生成代理对象后,当执行某些方法时,会执行一些额外定义的切面逻辑。
依赖注入和依赖查找的区别在于,依赖注入是将依赖关系委托给容器,由容器来管理对象之间的依赖关系;而依赖查找是由对象自己来查找它所依赖的对象,容器只负责管理对象的生命周期。
Bean 代指的就是那些被 IoC 容器所管理的对象。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
Bean有哪几种配置方式?
XML配置:使用XML文件来配置Bean,通过
注解配置:使用注解来配置Bean,通过在Bean类上添加注解,如@Component、@Service、@Repository等,来标识Bean的角色和作用。
JavaConfig方式:使用Java类来配置Bean,通过编写一个配置类,使用@Configuration注解标识,然后在方法上使用@Bean注解来定义Bean。
@Import:@Import注解可以用于导入其他配置类,也可以用于导入其他普通类。当导入的是配置类时,被导入的配置类中定义的Bean会被纳入到当前配置类的上下文中;当导入的是普通类时,被导入的类本身会被当作一个Bean进行注册。
这4种是最常用的,另外还有两种一些冷门的:
Groovy配置:使用Groovy脚本来配置Bean,通过编写一个Groovy脚本文件,使用Spring的DSL(Domain Specific Language)来定义Bean。
JSR-330:Spring提供了对JSR-330标准注释(依赖注入)的支持,可以使用@Named 或 @ManagedBean替代@Component,但是需要javax.inject依赖。
@Component
:通用的注解,可标注任意类为 Spring
组件。如果一个 Bean 不知道属于哪个层,可以使用@Component
注解标注。
@Repository
: 对应持久层即 Dao 层,主要用于数据库相关操作。
@Service
: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
@Controller
: 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service
层返回数据给前端页面。
@Component
注解作用于类,而@Bean
注解作用于方法。@Component
通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 @ComponentScan
注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean
告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。@Bean
注解比 @Component
注解的自定义性更强,而且很多地方我们只能通过 @Bean
注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring
容器时,则只能通过 @Bean
来实现。Spring 内置的 @Autowired
以及 JDK 内置的 @Resource
和 @Inject
都可以用于注入 Bean。
@Autowired:
Autowired
属于 Spring 内置的注解,
默认的注入方式为byType
(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。
这个注解中,有一个required属性,默认值为true,表示必须实例化一个注入的Bean。如果找不到对应类型的Bean,会在应用启动时报错。
存在的问题: 当一个接口存在多个实现类的话,byType
这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。
这种情况下,注入方式会变为 byName
(根据名称进行匹配),这个名称通常就是类名(首字母小写)。
@Resource:
@Resource
属于 JDK 提供的注解,默认注入方式为 byName
。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType
。
@Resource
有两个比较重要且日常开发常用的属性:name
(名称)、type
(类型)
总结:
@Autowired默认根据接口类型匹配注入,如果有多个实现类则变为根据名称匹配(首字母小写)
@Resource默认根据名称匹配注入,如果根据名称找不到则变为根据类型匹配,还可以手动指定属性name和type
getBean()
两次,得到的是不同的 Bean 实例。如何配置 bean 的作用域呢?:
xml方式:
<bean id="..." class="..." scope="singleton"></bean>
@Scope注解方式:
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
取决于Bean的作用域和状态。
prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。
singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。
不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
对于有状态单例 Bean 的线程安全问题,常见的解决办法:
ThreadLocal
成员变量,将需要的可变成员变量保存在 ThreadLocal
中(推荐的一种方式)。执行顺序:
InitializingBean
接口或者在配置文件中配置了初始化方法的Bean,容器会调用其初始化方法进行一些额外的初始化工作。首先Spring是一个生态:可以构建企业级应用程序所需的一切基础设施
通常Spring指的就是Spring Framework,它有两大核心:
1.IOC 和 DI 的支持
Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring 工厂用于生成 Bean,并且管理 Bean 的生命周期,实现高内聚低耦合的设计理念。
2.AOP 编程的支持
Spring 提供了面向切面编程,面向切面编程允许我们将横切关注点从核心业务逻辑中分离出来,实现代码的模块化和重用。可以方便的实现对程序进行权限拦截、运行监控、日志记录等切面功能。
总结一句话:它是一个轻量级、非入侵式的控制反转 (IoC) 和面向切面 (AOP) 的容器框架。
答:完全没有关系。
BeanFactory :
BeanFactory 是 Spring 框架的基本容器,负责管理和创建应用程序中的对象。BeanFactory 的主要职责是实例化 Bean、处理 Bean 之间的依赖关系、注入属性以及在需要时销毁 Bean。
// 得到 BeanFactory 对象
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));
// 使用 BeanFactory 得到 User 对象
beanFactory.getBean("user");
FactoryBean:
FactoryBean 是一个特殊的 Bean,它实现了 Spring 的 FactoryBean 接口。
它是一种更加灵活和可扩展的机制,可以通过编程的方式动态地创建和配置Bean。
FactoryBean 的实现类可以自定义创建和管理 Bean 的逻辑。例如,它可以根据条件选择性地创建不同的实例,或者在创建 Bean 之前进行一些初始化操作。FactoryBean 通常在 Spring 配置文件中配置,并由 BeanFactory 负责实例化和管理。
import org.springframework.beans.factory.FactoryBean;
public class MyBeanFactory implements FactoryBean<MyBean> {
@Override
public MyBean getObject() throws Exception {
// 在这里定义创建 Bean 的逻辑
MyBean myBean = new MyBean();
// 进行一些初始化操作
myBean.setName("Example Bean");
// 返回创建的 Bean 实例
return myBean;
}
@Override
public Class<?> getObjectType() {
// 返回创建的 Bean 的类型
return MyBean.class;
}
@Override
public boolean isSingleton() {
// 指示创建的 Bean 是否是单例
return true;
}
}
FactoryBean接口定义了两个方法:getObject()和getObjectType()。
getObjectType()
方法用于返回创建的Bean对象的类型。
getObject()
方法用于返回创建的Bean对象,最终该Bean对象会进行注入
总结:
BeanFactory 是 Spring 的基本容器,用于创建和管理 Bean 实例,而 FactoryBean 是一个特殊的 Bean,用于创建其他 Bean 实例,并提供一些初始化 Bean 的设置。
1.方法是private也会失效,解决:改成public: Spring的事务代理通常是通过Java动态代理或CGLIB动态代理生成的,这些代理要求目标方法是公开可访问的(public)。私有方法无法被代理,因此事务将无效。解决方法是将目标方法改为public或protected。
2.目标类没有配置为Bean也会失效,解决:配置为Bean: Spring的事务管理需要在Spring容器中配置的Bean上才能生效。如果目标类没有被配置为Spring Bean,那么事务将无法被应用。解决方法是确保目标类被正确配置为Spring Bean。
3.自己捕获了异常,解决:不要捕获处理: Spring事务管理通常依赖于抛出未捕获的运行时异常来触发事务回滚。如果您在方法内部捕获了异常并处理了它,事务将不会回滚。解决方法是让异常在方法内部被抛出,以触发事务回滚。
4.同一个类中方法调用,导致 @Transactional 失效,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这是由于使用 Spring AOP
代理造成的,因为 只有当事务方法被 当前类以外的代码 调用时,才会由Spring
生成的代理对象来管理。
5.@Transactional 注解属性 rollbackFor 设置错误,Spring默认抛出了未检查unchecked
异常(继承自 RuntimeException
的异常)或者 Error
才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor 属性,如果未指定 rollbackFor 属性则事务不会回滚。
6.事务的传播行为设置错误,@Transactional 注解属性 propagation设置错误
若是错误的配置以下三种 propagation,事务将不会发生回滚。
TransactionDefinition.PROPAGATION_SUPPORTS
:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER
:以非事务方式运行,如果当前存在事务,则抛出异常。
在多线程环境下,Spring事务管理默认情况下无法保证全局事务的一致性。这是因为Spring的本地事务管理是基于线程的,每个线程都有自己的独立事务。
Spring的事务管理通常将事务信息存储在ThreadLocal中,这意味着每个线程只能拥有一个事务。这确保了在单个线程内的数据库操作处于同一个事务中,保证了原子性。
可以通过如下方案进行解决:
1.非Spring管理的对象
Spring的AOP只能拦截由Spring容器管理的Bean对象。如果您使用了非受Spring管理的对象,则AOP将无法对其进行拦截。
2.同一个Bean内部方法调用
如果一个Bean内部的方法直接调用同一个Bean内部的另一个方法,AOP将无法拦截这个内部方法调用。因为AOP是基于代理的,只有通过代理对象才能触发AOP拦截。
3.静态方法
Spring的AOP只能拦截非静态方法。如果您尝试拦截静态方法,AOP将无法生效。
4.final方法
AOP无法拦截final方法。final方法是不可重写的,因此AOP无法生成代理对象来拦截这些方法。
直接在对象内部调用方法:如果您直接在对象内部调用方法而不通过代理对象,AOP将无法拦截。因此,建议始终通过代理对象调用方法以确保AOP的生效。
5.使用private修饰方法
动态代理失效
BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。
BeanFactory:是Spring框架的核心接口之一, 我们可以称之为 “低级容器”。为什么叫低级容器呢?因为Bean的生产过程分为【配置的解析】和【Bean的创建】,而BeanFactory只有Bean的创建功能,但也说明它内存占用更小,在早期会在一些内存受限的可穿戴设备中作为spring容器使用。
ApplicationContext 可以称之为 “高级容器”。因为他比 BeanFactory 多了更多的功能。他继承了多个接口,因此具备了更多的功能。例如配置的读取、解析、扫描等,还加入了 如 事件事件监听机制,及后置处理器让Spring提升了扩展性。所以你看他的名字,已经不是 BeanFactory 之类的工厂了,而是 “应用上下文”, 代表着整个大容器的所有功能。该接口定义了一个 refresh 方法,此方法是所有阅读 Spring 源码的人的最熟悉的方法,用于刷新整个容器,即重新加载/刷新所有的 bean。
其他说法:
1,可以清晰的定义每个容器的职责
2,限制组件之间的依赖关系
3,子容器可以访问父容器中的组件
4,父容器的Bean可以在整个应用程序范围内共享
5,有助于管理和组织应用程序的各个部分
1.简单工厂:
BeanFactory:Spring的BeanFactory充当工厂,负责根据配置信息创建Bean实例。它是一种工厂模式的应用,根据指定的类名或ID创建Bean对象。
2.工厂方法:
FactoryBean:FactoryBean接口允许用户自定义Bean的创建逻辑,实现了工厂方法模式。开发人员可以使用FactoryBean来创建复杂的Bean实例。
3.单例模式:
Bean实例:Spring默认将Bean配置为单例,确保在容器中只有一个共享的实例,这有助于节省资源和提高性能。
4.适配器模式:
SpringMVC中的HandlerAdapter:SpringMVC的HandlerAdapter允许不同类型的处理器适配到处理器接口,以实现统一的处理器调用。这是适配器模式的应用。 5.装饰器模式:
BeanWrapper:Spring的BeanWrapper允许在不修改原始Bean类的情况下添加额外的功能,这是装饰器模式的实际应用。
6.代理模式:
AOP底层:Spring的AOP(面向切面编程)底层通过代理模式来实现切面功能,包括JDK动态代理和CGLIB代理。
7.观察者模式:
Spring的事件监听:Spring的事件监听机制是观察者模式的应用,它允许组件监听和响应特定类型的事件,实现了松耦合的组件通信。
8.策略模式:
excludeFilters、includeFilters:Spring允许使用策略模式来定义包扫描时的过滤策略,如在@ComponentScan注解中使用的excludeFilters和includeFilters。
9.模板方法模式:
Spring几乎所有的外接扩展:Spring框架的许多模块和外部扩展都采用模板方法模式,例如JdbcTemplate、HibernateTemplate等。
10.责任链模式:
AOP的方法调用:Spring AOP通过责任链模式实现通知(Advice)的调用,确保通知按顺序执行。
观察者模式: 它允许一个对象(称为主题或被观察者)维护一组依赖于它的对象(称为观察者),并在主题状态发生变化时通知观察者。
它包含三个核心:
1.事件: 事件是观察者模式中的主题状态变化的具体表示,它封装了事件发生时的信息。在Spring中,事件通常是普通的Java对象,用于传递数据或上下文信息。
2.事件发布者: 在Spring中,事件发布者充当主题的角色,负责触发并发布事件。它通常实现了ApplicationEventPublisher接口或使用注解@Autowired来获得事件发布功能。
3.事件监听器: 事件监听器充当观察者的角色,负责监听并响应事件的发生。它实现了ApplicationListener接口,通过onApplicationEvent()方法来处理事件。
总之,Spring事件监听机制的核心机制是观察者模式,通过事件、事件发布者和事件监听器的协作,实现了松耦合的组件通信,使得应用程序更加灵活和可维护。
因为它是一个 可执行的 jar 文件,即包含了一个内嵌的 Web 服务器和所有应用运行所需的依赖。
Tomcat 为 Spring Boot 框架默认的 Web 容器。
默认情况下 Spring Boot 能够同时处理的请求数=最大连接数(8192)+最大等待数(100),结果为 8292 个。
当然,这两个值是可以在 Spring Boot 配置文件中修改的,如下配置所示:
server:
tomcat:
max-connections: 2000 # 最大连接数
accept-count: 200 # 最大等待数
Propagation.REQUIRED:表示如果当前存在事务,则在当前事务中执行;如果当前没有事务,则创建一个新的事务并在其中执行。即,方法被调用时会尝试加入当前的事务,如果不存在事务,则创建一个新的事务。如果外部事务回滚,那么内部事务也会被回滚。
Propagation.NESTED:表示如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务并在其中执行。嵌套事务是独立于外部事务的子事务,它具有自己的保存点,并且可以独立于外部事务进行回滚。如果嵌套事务发生异常并回滚,它将会回滚到自己的保存点,而不影响外部事务。
1.内置Starter和自动配置:Spring Boot提供了丰富的内置Starter,这些Starter是预定义的依赖集合,可以轻松集成各种主流框架和技术。同时,Spring Boot通过自动配置大大减少了繁琐的配置工作,让开发人员可以直接开箱即用。
2.零XML配置:Spring Boot采用JavaConfig的方式进行开发,不需要编写大量的XML配置文件。这种零XML的开发方式让开发更加简洁和可读,同时提高了可维护性。
3.内置Web容器:Spring Boot内置了多个Web容器,如Tomcat、Jetty、Undertow等,无需外部Web服务器。这意味着您可以将应用程序打包成可执行的JAR文件,直接运行而不需要额外的容器配置,从而简化了部署过程。
4.微服务支持:Spring Boot与Spring Cloud结合使用,可以轻松快速构建和部署微服务架构。
5.依赖版本管理:Spring Boot帮助开发人员管理了常用第三方依赖的版本,防止出现版本冲突问题。这样,您可以更专注于业务逻辑,而不用担心依赖的版本兼容性。
6.监控和管理: Spring Boot自带了监控功能,包括应用程序运行状况监控、内存使用情况、线程池状态、HTTP请求统计等。此外,Spring Boot还提供了优雅关闭应用程序的方式,使得应用程序的管理更加便捷。
他们的关系是:
Spring是框架,Spring Boot是个脚手架: Spring是一个全功能的Java应用程序框架,旨在帮助开发人员构建各种类型的应用程序,包括Web应用、企业级应用、批处理应用等。Spring提供了大量的组件和功能,但需要开发人员进行详细的配置和集成。Spring Boot则是一个脚手架工具,它基于Spring框架,旨在简化Spring应用程序的初始配置和开发过程,提供了自动化配置和约定优于配置的特性。
Spring Boot构建在Spring之上: Spring Boot不是一个独立的框架,而是建立在Spring框架之上的工具。它使用了Spring的核心功能,如依赖注入、面向切面编程、事务管理等。因此,Spring Boot项目可以充分利用Spring框架的功能,但更容易启动和运行。
他们的区别是:
引入starter
@SpringBootApplication:
1.@SpringBootConfiguration--->@Configuration
2.@EnableAutoConfiguration--->@Import(importselector的实现类)importselector的实现类实现了selectimport方法,返回值是string数组,里面包含了要自动配置的所有类的全类名,该方法写导入了META-INFO/spring.factories和META-INFO/spring/AutoConfiguration.imports两个文件,文件里就是要自动配置 的类的全类名(META-INFO/spring/AutoConfiguration.imports是springboot2.7.x之后新增的,在springboot3之后就彻底移除spring.factories)
3.@ComponentScan:组件扫描,默认扫描当前包及其子包
用@Condition来过滤掉无效的自动配置类
1.使用@EnableAutoConfiguration注解开启自动配置特性。
2.使用@SpringBootApplication注解开启SpringBoot应用程序。
3.使用@Configuration注解和@Import注解手动导入需要的配置类。
4.继承spring-boot-starter-parent项目。
数据库范式有 3 种:
CHAR 和 VARCHAR 是最常用到的字符串类型,两者的主要区别在于:CHAR 是定长字符串,VARCHAR 是变长字符串。
CHAR 在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;VARCHAR 在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。
CHAR 更适合存储长度较短或者长度都差不多的字符串,例如 Bcrypt 算法、MD5 算法加密后的密码、身份证号码。VARCHAR 类型适合存储长度不确定或者差异较大的字符串,例如用户昵称、文章标题等。
CHAR(M) 和 VARCHAR(M) 的 M 都代表能够保存的字符数的最大值,无论是字母、数字还是中文,每个都只占用一个字符。
VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,不过,VARCHAR(100) 会消耗更多的内存。
UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。
对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用。
DECIMAL 和 FLOAT 的区别是:DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。
DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。
在 Java 中,MySQL 的 DECIMAL 类型对应的是 Java 类 java.math.BigDecimal
。
DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。
TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。
NULL
和‘’
的区别是什么? NULL
跟 ''
(空字符串)是两个完全不一样的值,区别如下:
NULL
代表一个不确定的值,就算是两个 NULL
,它俩也不一定相等。例如,SELECT NULL=NULL
的结果为 false,但是在我们使用DISTINCT
,GROUP BY
,ORDER BY
时,NULL
又被认为是相等的。''
的长度是 0,是不占用空间的,而NULL
是需要占用空间的。NULL
会影响聚合函数的结果。例如,SUM
、AVG
、MIN
、MAX
等聚合函数会忽略 NULL
值。 COUNT
的处理方式取决于参数的类型。如果参数是 *
(COUNT(*)
),则会统计所有的记录数,包括 NULL
值;如果参数是某个字段名(COUNT(列名)
),则会忽略 NULL
值,只统计非空值的个数。NULL
值时,必须使用 IS NULL
或 IS NOT NULLl
来判断,而不能使用 =、!=、 <、> 之类的比较运算符。而''
是可以使用这些比较运算符的。看了上面的介绍之后,相信你对另外一个高频面试题:“为什么 MySQL 不建议使用 NULL
作为列默认值?”也有了答案。
MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。
MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。
1.存储方式:
MyISAM使用非聚簇索引,索引文件和数据文件是分开的;而InnoDB使用聚簇索引,将索引和数据一起存储在同一个文件中。
2.锁机制:
MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
3.是否支持事务:
MyISAM 不提供事务支持。
InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。并且,InnoDB 默认使用的 REPEATABLE-READ(可重读)隔离级别是可以解决幻读问题发生的(基于 MVCC 和 Next-Key Lock)。
4.是否支持外键:
MyISAM 不支持,而 InnoDB 支持。
5.是否支持数据库异常崩溃后的安全恢复:
MyISAM 不支持,而 InnoDB 支持。
使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 redo log
。
6.性能:
InnoDB 的性能比 MyISAM 更强大。
总结:
innoDB:支持外键,事务,崩溃修复(redo log)。支持行锁,占用空间较大,默认使用B+数索引,读效率低,支持MVCC
MyISAM:不支持外键和事务,只有表锁,占用空间小,写入效率低,读效率高。B数索引
主键索引:主键索引是一种特殊的索引类型,它是用于唯一标识每一行数据的索引,每个表只能有一个主键索引。
唯一索引:唯一索引是用来保证列的唯一性的索引,一个表可以有多个唯一索引。
普通索引:普通索引也叫非唯一索引,它是最常见的一种索引类型,可以加速查询和排序操作。
全文索引:全文索引是一种用于全文搜索的索引类型,能够对文本数据进行快速的模糊搜索和关键字搜索。
复合索引:复合索引也叫多列索引或联合索引,它是包含多个列的索引类型,能够加速多列查询和排序操作。
哈希索引:哈希索引是基于哈希表实现的索引类型,能够对等值查询进行高效的处理,但不支持范围查询和排序,MySQL 中 Memory 引擎中支持哈希索引。
什么是索引:
索引是数据库中用于提高数据检索性能的排好序的数据结构。它类似于书籍的目录,通过建立特定的数据结构将列或多个列的值与它们在数据表中对应的行关联起来,以加快查询速度。
优点:
缺点:
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B+Tree 索引。
数据存储方式:在B树中,每个节点都包含键和对应的值,叶子节点存储了实际的数据记录;而B+树中,只有叶子节点存储了实际的数据记录,非叶子节点只包含键信息和子节点的指针。
数据检索方式:在B树中,由于非叶子节点也存储了数据,所以查询时可以直接在非叶子节点找到对应的数据,具有更短的查询路径;而B+树的所有数据都存储在叶子节点上,只有通过叶子节点才能获取到完整的数据。
范围查询效率:由于B+树的所有数据都存储在叶子节点上,并且叶子节点之间使用链表连接,所以范围查询的效率较高。而在B树中,范围查询需要通过遍历多个层级的节点,效率相对较低。
适用场景:B树适合进行随机读写操作,因为每个节点都包含了数据;而B+树适合进行范围查询和顺序访问,因为数据都存储在叶子节点上,并且叶子节点之间使用链表连接,有利于顺序遍历。
如果我用 product_no 二级索引查询商品,如下查询语句:
select * from product where product_no = '0002';
会先检二级索引中的 B+Tree 的索引值(商品编码,product_no),找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。这个过程叫「回表」,也就是说要查两个 B+Tree 才能查到数据。
它指的是在使用复合索引时,索引的最左边的连续几个列会被用于查询过滤条件的匹配。
比如,如果创建了一个 (a, b, c)
联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:
需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。
但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
上面这些查询条件之所以会失效,是因为(a, b, c)
联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。
遵循最左前缀原则的好处包括:
覆盖索引是指一个索引包含了查询所需的所有列,而无需访问表的实际数据页。无需回表操作,减少磁盘IO,提高响应速度。
覆盖索引通常适用于以下场景:
musql5.6针对扫描二级索引的优化改进,把索引的过滤条件下推到存储引擎,适用于myisam和innoDB。减少回表操作。
具体:
可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
假设有一个包含许多列的表 products,并为复合索引 (category_id, price) 创建以下查询:
SELECT * FROM products WHERE category_id = 5 AND price < 100;
如果复合索引 (category_id, price) 存在:
无索引下推:
MySQL根据category_id = 5条件使用索引检索数据,然后访问主表的每一行以评估price < 100条件。这可能导致访问许多不满足价格条件的行。
有索引下推:
MySQL在索引层面直接评估price < 100的条件,只需将符合条件的行从表中提取。这减少了对表的I/O操作。
适用场景:
索引下推非常适合于查询条件中涉及多个列的过滤,且这些列存在复合索引的情况下。它可以在索引扫描过程中应用额外的过滤条件来减少所需的行访问。
幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。
1. 合理使用索引:
前缀匹配:使用LIKE 'prefix%'的形式,这种情况MySQL能够利用索引,比如:
SELECT * FROM users WHERE username LIKE 'John%';
如果username字段有索引,前缀匹配会用到索引。
2.使用反向索引:
对于需要匹配后缀的情况(即LIKE '%suffix'),可以创建一个辅助列存储反转字符串,并基于此列进行前缀匹配。
创建反向字符串:
ALTER TABLE users ADD reversed_username VARCHAR(255);
UPDATE users SET reversed_username = REVERSE(username);
CREATE INDEX idx_reversed_username ON users(reversed_username);
3. 限制扫描范围:
在LIKE查询中,如果可以通过其他条件进一步缩小搜索范围,请尽量加入这些条件。
SELECT * FROM users WHERE created_at >= '2023-01-01' AND username LIKE 'John%';
4. 使用缓存
5. 使用专业工具:
对于非常大的数据集或者需要复杂文本处理和搜索功能,可以使用外部全文搜索引擎如Elasticsearch、Solr或者Sphinx来代替MySQL的LIKE操作。
count(1)和count(*)几乎一样,是统计表中所有行的数量,值是否为 NULL。
count(列名) 统计指定列中非 NULL 值的行数。
性能对比
select *
,只查询需要的字段。A加行锁,B之后要加表锁
1.无意向锁的话,B会成功,A的行锁失效
2.有意向锁的话,B会失败
数据库事务是指一系列数据库操作的逻辑单元,这些操作要么全部成功执行,要么全部回滚。它的目的是确保数据的一致性和完整性。
事务具备4大特性,即原子性、一致性、隔离性和持久性:(ACID)
原子性:事务中的所有操作要么全部执行成功,要么全部回滚到事务开始前的状态。如果在事务执行期间发生错误,系统将回滚所有已执行的操作,以保持数据的一致性。
一致性:事务的执行不会破坏数据库的完整性约束。在事务开始和结束时,数据库必须处于一致的状态。
如小李转账100元给小白,不管操作是否成功,小李和小白的账户总额是不变的。
隔离性:事务的执行是相互隔离的,即每个事务对其他事务是透明的。并发执行的多个事务不会相互干扰,每个事务感知不到其他事务的存在。
持久性:一旦事务提交成功,事务中的所有操作都必须持久化到数据库中。
二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
redo log:
redo log是innoDB独有的,让mysql拥有了崩溃恢复能力。实现了事务中的**持久性,主要用于掉电等故障恢复**;
undo log:
undo log是 Innodb 存储引擎层生成的日志,实现了事务中的**原子性,主要用于事务回滚和 MVCC**。
是逻辑日志,记录相反的操作记录
一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:
bin log:
二进制日志,记录了数据库中所有数据的更改操作。
是 Server 层生成的日志,主要==用于数据备份和主从复制==;
写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。
磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。
产生的 redo log 是直接写入磁盘的吗?:
实际上, 执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,因为这样会产生大量的 I/O 操作,而且磁盘的运行速度远慢于内存。
所以,redo log 也有自己的缓存—— redo log buffer,每当产生一条 redo log 时,会先写入到 redo log buffer,后续在持久化到磁盘
不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。
因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。
binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。
MVCC是多版本并发控制,维护一个数据的多个版本,使读写无冲突。
是事务隔离级别无锁的实现方式,用于提高事务的并发性能。
MVCC实现原理:
当前读:读的是记录的最新版本,会对记录加锁
快照读:简单的select就是快照读,读的是数据的可见版本,有可能是历史数据,不加锁
读已提交:每次select都生成快照读,每次快照读都生成一个新的ReadView
可重复度:事务中第一个select才是快照读的地方,事务中第一次快照读时才生成ReadView,后续复用该ReadView
串行化:快照读会退化成当前读
在 Read View 中包含了以下 4 个主要的字段:
虽然说 MySQL 的数据是存储在磁盘里的,但是也不能每次都从磁盘里面读取数据,这样性能是极差的。
要想提升查询性能,加个缓存就行了嘛。所以,当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存中读取。
为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
行锁是针对单条记录的锁,也称为记录锁。行锁用于精确锁定表中的某一行数据,确保在同一时刻,只有一个事务能够对该行数据进行修改。
适用场景:行锁通常用于SELECT FOR UPDATE
、UPDATE
、DELETE
语句,这些语句会锁定相关的行,防止其他事务同时修改。
优点:行锁粒度小,锁定范围有限,因此并发性能较好。
间隙锁是指锁住两个索引记录之间的间隙,防止其他事务在该间隙中插入新记录。
当事务执行范围查询时,InnoDB 会在需要时对两个记录之间的空隙加锁,防止其他事务插入记录。间隙锁通常与临键锁一起使用。
应用:间隙锁主要用于范围查询(如 SELECT ... FOR UPDATE
)中,确保查询的范围在事务期间不发生变化。
优点:避免幻读
它结合了行锁和间隙锁。临键锁不仅锁住查询命中的记录,还会锁住这些记录前面的间隙,以防止其他事务插入新的记录。
应用:范围查询
优点:避免幻读
主要还是缓存和分布式锁
五种基础数据类型:
还有三种特殊数据类型:
HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
地理位置Geospatial
geo 的数据类型为 zset。
GEO 的数据结构总共有六个常用命令:geoadd、geopos、geodist、georadius、georadiusbymember、gethash
GEOADD:将指定的经度和纬度信息添加到指定的键中。其语法如下:
GEOPOS:获取指定地点的经度和纬度信息。其语法如下:
GEODIST:计算两个地点之间的距离。其语法如下:
GEORADIUS:根据指定的中心地点和半径范围,获取在这个范围内的地点信息。其语法如下:
GEORADIUSBYMEMBER:根据指定的地点和半径范围,获取在这个范围内的地点信息。其语法类似于 GEORADIUS
命令,但是它使用地点名称或标识作为参数,而不是经纬度。
GEOHASH:获取指定地点的 Geohash 值。Geohash 是地理位置编码系统,用于将地理位置信息编码为一串字符串。在 Redis 中,GEOHASH
命令不是原生的命令,但可以通过使用 GEOPOS
命令获取经纬度信息,然后使用其他编程语言中的 Geohash 库进行编码。
基数统计 HyperLogLog
什么是基数
比如数据集 A {1,3,5,7,5,7,8},那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数(不重复元素)为 5。基数估计就是在误差可接受的范围内,快速计算基数。
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。HyperLogLog 则是一种算法,它提供了不精确的去重计数方案。
[PFADD key element [element ...]添加元素
[PFCOUNT key [key ...]查看基数数量
[PFMERGE destkey sourcekey [sourcekey ...]合并基数
位图Bitmap
位存储
在开发中,可能会遇到这种情况:需要统计用户的某些信息,如活跃或不活跃,登录或者不登录;又如需要记录用户一年的打卡情况,打卡了是 1, 没有打卡是 0,如果使用普通的 key-value 存储,则要记录 365 条记录,如果用户量很大,需要的空间也会很大,所以 Redis 提供了 Bitmap 位图这种数据结构。
Bitmap 就是通过操作二进制位来进行记录,即为 0 和 1;如果要记录 365 天的打卡情况,使用 Bitmap表示的形式大概如下:0101000111000111...........................,这样有什么好处呢?当然就是节约内存了,365 天相当于 365 bit,又 1 字节 = 8 bit , 所以相当于使用 46 个字节即可。
BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态,其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现。Redis 从 2.2 版本之后新增了 setbit,getbit, bitcount 等几个 bitmap 相关命令。
添加元素:
SETBIT key offset value : 设置 key 的第 offset 位为value (1或0)
获取元素:
GETBIT key offset 获取offset设置的值,未设置过默认返回0
统计元素:
bitcount key [start, end] 统计 key 上位为1的个数
在绝大部分情况,我们建议使用 String 来存储对象数据即可!
有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表) 组成的。
当数据比较少时,有序集合是压缩列表 ziplist 存储的,反之则为跳跃表 skiplist 存储,使用压缩列表存储必满足以下两个条件:
如果不能满足以上两个条件中的任意一个,有序集合将会使用跳跃表 skiplist 结构进行存储。
缓存穿透是指在缓存系统中,大量的请求查询不存在于缓存和数据库中的数据,导致这些请求直接访问数据库,占用数据库资源,而缓存无法发挥作用的现象。
解决:
缓存击穿是指在缓存系统中,某个热点数据过期或失效时,同时有大量的请求访问该数据,导致请求直接访问数据库或后端服务,给数据库或后端服务造成巨大压力,导致系统性能下降甚至崩溃的现象。
解决:
缓存雪崩是指在某一时刻,大量缓存数据共同过期或者redis宕机,导致瞬时大流量涌向数据库。
解决:
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。它基于位数组和多个哈希函数的原理,可以高效地进行元素的查询,而且占用的空间相对较小,如下图所示:
布隆过滤器说一个元素不在集合中,那么它一定不在这个集合中;但如果它说一个元素在集合中,则有可能是不存在的(存在误差)。
特点:
布隆过滤器执行过程:
适用场景:
底层原理:
bitmap位图
存储一个字符串比如lmj,就需要hsah("lmj")%n(n为bitmap长度)取模的值就是存储的bitmap的位置,但是可能会有hash碰撞,所以布隆过滤器对一个字符串使用多个hash算法给数据存到每个取模得到的位置上,判断的时候要所有位置都是1才能证明数据存在,只要有一个位置是0就说明数据不存在,这样能有效地减少误判的几率,但误判还是会存在的。(不存在一定不存在,存在不一定存在)
如何减小误判的几率:
1.增加bitmap的长度
2.增加hash个数(但会让我们的布隆过滤器性能有所降低)
redission提供了布隆过滤器
RBloomFilter
bloomFilter.tryInit(估算的bitmap位数,容错率)
之后就可以bloomFilter.add(元素)了
用bloomFilter.contains(元素)来判断元素存不存在
Redis 定期删除流程如下:
同时为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
volatile-random:随机淘汰设置了过期时间的任意键值。这种策略适用于数据集大小较大,需要保持数据随机性的情况。
volatile-ttl:优先淘汰更早过期的键值。这种策略适用于数据集大小较小,需要保持数据及时更新的情况。
volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值。这是Redis 3.0之前默认的内存淘汰策略,适用于数据集大小较小,需要保持数据的访问频率的情况。
volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值。这是Redis 4.0后新增的内存淘汰策略,适用于数据集大小较小,需要保持数据的访问频率的情况。
在所有数据范围内进行淘汰:
allkeys-random:随机淘汰任意键值。这种策略适用于数据集大小较大,需要保持数据随机性的情况。
allkeys-lru:淘汰整个键值中最久未使用的键值。这种策略适用于数据集大小非常大的情况。
allkeys-lfu:淘汰整个键值中最少使用的键值。这是Redis 4.0后新增的内存淘汰策略,适用于数据集大小非常大的情况。
RDB和AOF。
RDB(Redis DataBase)
rdb保存的文件是dump.rdb
RDB是一种快照持久化的方式,它会将Redis在某个时间点的数据状态以二进制的方式保存到硬盘上的一个文件中。RDB持久化可以通过配置定时或手动触发,也可以设置自动触发的条件。RDB的优点是生成的文件比AOF文件更小,恢复速度也更快,适合用于备份和灾难恢复。
优点:
1.适合大规模的数据恢复
2.对数据的完整性要求不高
缺点:
1.需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了。
2.fork进程的时候,会占用一定的内容空间。
AOF(Append Only File)
aof保存的文件是appendonly.aof
快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
AOF以日志的形式来记录每个写的操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
触发条件:AOF 文件的更新可以由多种条件触发:
优点:
AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。
AOF 文件使用 Redis 命令追加的形式来构造,因此,即使 Redis 只能向 AOF 文件写入命令的片断,使用 redis-check-aof 工具也很容易修正 AOF 文件。
AOF 文件的格式可读性较强,这也为使用者提供了更灵活的处理方式。例如,如果我们不小心错用了 FLUSHALL 命令,在重写还没进行时,我们可以手工将最 后的 FLUSHALL 命令去掉,然后再使用 AOF 来恢复数据。
缺点:
AOF和RDB混合持久化:
虽然跟 AOF 相比,RDB 快照的恢复速度快,但快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销,那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?
在 Redis4.0 提出了混合使用 AOF 和 RDB 快照的方法,也就是两次 RDB 快照期间的所有命令操作由 AOF 日志文件进行记录。这样的好处是 RDB 快照不需要很频繁的执行,可以避免频繁 fork 对主线程的影响,而且 AOF 日志也只记录两次快照期间的操作,不用记录所有操作,也不会出现文件过大的情况,避免了重写开销。
都是基于redis.conf文件
关闭RDB持久化
save "" # 将 save 参数列表清空,表示不进行任何条件下的数据保存
关闭AOF持久化
appendonly no # 设置为 no,表示关闭 AOF 持久化
关闭混合持久化
rdb-aof-use-rdb-preamble no # no 表示关闭混合持久化
SETNX 和 EXPIRE 命令来实现
SETNX 是 "SET if Not eXists" 的缩写
SETNX 用于在指定的 key 不存在时设置 key 的值。如果 key 已经存在,SETNX 操作将不做任何事情,返回失败;如果 key 不存在,SETNX 操作会设置 key 的值,并返回成功。而 EXPIRE 是设置锁的过期时间的,主要为了防止死锁的发生。
存在的误删问题:
比如线程 1 设置的过期时间为 5s,而线程 1 执行了 7s,那么在第 5s 之后锁过期了,那么其他线程就可以拥有这把锁了,之后线程 1 执行完业务,又执行了锁删除操作,那么此时锁就被误删了。
解决方案:
此时可以每个锁的 value 中添加拥有者的标识,删除之前先判断是否是自己的锁,如果是则删除,否则不删除。但是判断和删除之间不是原子性操作,所以依然有问题。此时可以使用 lua 脚本来判断并删除锁,lua 脚本可以保证 redis 中多条语句执行的原子性,所以就可以解决此问题了。
所有存在的问题:
死锁问题:SETNX 如未设置过期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的情况。
锁误删问题:SETNX 设置了超时时间,但因为执行时间太长,所以在超时时间之内锁已经被自动释放了,但线程不知道,因此在线程执行结束之后,会把其他线程的锁误删的问题。
不可重入问题:也就是说同一线程在已经获取了某个锁的情况下,如果再次请求获取该锁,则请求会失败(因为只有在第一次能加锁成功)。也就是说,一个线程不能对自己已持有的锁进行重复锁定。
无法自动续期:线程在持有锁期间,任务未能执行完成,锁可能会因为超时而自动释放。SETNX 无法自动根据任务的执行情况,设置新的超时实现,以延长锁的时间。
Redission分布式锁
redission的分布式锁相比于setnx的锁的优点:
如果一个线程获取锁后,运行程序到释放锁所花费的时间大于锁自动释放时间(也就是看门狗机制提供的超时时间30s),那么Redission会自动给redis中的目标锁延长超时时间。看门狗每隔 10秒 检查一次,如果发现客户端仍然持有锁,就会自动续约,将锁的过期时间重置为 30秒。
multiLock解决分布式锁主从一致性问题:不设置一台redis主机多台从机,而是设置多台redis主机,我们获取锁需要向每个主机都获取锁,multiLock就是把我们每个redis主机的锁一起拿来做成联锁。
内部原理就是把多个主机锁放入一个集合之中,遍历该集合,一个一个的获取每个锁,把获取成功的锁放入一个成功获取的锁的集合,当遍历过程中有一个锁没获取成功,我们就重新再来遍历锁集合,一个一个重新获取每个锁,直到要么所有锁都获取到了,要么就获取锁超时了退出。最后如果每个锁都获取成功的话,我们最后还要遍历成功获取的锁的集合,把每个锁的超时时间重置一下,因为第一个获取到的和最后一个获取到的锁的过期时间肯定不一致,最后这一步就是为了保证所以锁的过期时间一致(当然如果我们没有设置过期时间,默认为-1,就是开启看门狗机制就不需要最后这步重置过期时间操作,因为看门狗机制会无限自动续期锁的过期时间)。
RedLock 算法旨在解决单个 Redis 实例作为分布式锁时可能出现的单点故障问题,通过在多个独立运行的 Redis 实例上同时获取锁的方式来提高锁服务的可用性和安全性。
RedLock 是对集群的每个节点进行加锁,如果大多数节点(N/2+1)加锁成功,则才会认为加锁成功。 这样即使集群中有某个节点挂掉了,因为大部分集群节点都加锁成功了,所以分布式锁还是可以继续使用的。
Redisson 中的 RedLock 是基于 RedissonMultiLock(联锁)实现的。
RedissonMultiLock 是 Redisson 提供的一种分布式锁类型,它可以同时操作多个锁,以达到对多个锁进行统一管理的目的。联锁的操作是原子性的,即要么全部锁住,要么全部解锁。这样可以保证多个锁的一致性。
(自动选举老大的模式)
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
哨兵模式专注于对 Redis 实例(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主及主从切换,实现故障自动转移和恢复,确保整个 Redis 系统的可用性,所以哨兵模式相比于主从同步多个一个自动容灾恢复的优点。
如果master节点断开了,这个时候就会从从机中随机选择一个服务器(这里面是一个投票算法:
num(total_sentinels)/2+1
即半数(所有哨兵数的半数,无论哨兵死活)
以上的选票即可成为哨兵中的leader
,这就是著名的Raft
算法quorum
)
如果断开的主机回来了也只能当新选出来的主机的从机
大key 通常指的是一个键包含了大量的数据,使得该键对应值的占用的内存超出了正常范围。
影响:
大key
的操作进而会阻塞其他请求的处理,从而影响系统性能。AOF
与RDB
都会因为该 大key
耗费更多的时间,从而延迟持久化时间,分布式环境下甚至会造成缓存不一致。大key
在进行网络传输时会增加网络传输的延迟,在分布式环境下进行数据同步时可能会造成数据的不一致。解决:
大key
进行拆分为多个 小key
大key
进行清理Hibernate 是一个全自动的 ORM 框架,不需要手动编写 SQL 语句,使用api进行数据库查询,学习成本相对较高。
MyBatis 是一个半自动的 ORM 框架,需要开发者手动编写和管理 SQL 语句,使用原生sql语句查询,可以更灵活地控制和优化 SQL 语句。
优点:
缺点:
#{}
替换为实际的参数值,并将 ${}
替换为实际的 SQL 语句。#{}
和${}
有什么区别? #{}
用于预编译,提供参数安全性,适合大多数情况。${}
用于字符串替换,潜在安全风险较高,仅在特定情况下使用,确保参数值安全。类加载器(Class Loader)是 Java 虚拟机(JVM)的重要组成部分,负责将字节码文件加载到内存中并转换为可执行的类。
类加载器使用一种称为双亲委派模型(Parent Delegation Model)的机制来加载类。
类加载器主要有以下几种类型:
启动类加载器(Bootstrap ClassLoader):
这是虚拟机自身的类加载器,负责加载JVM运行时环境所需的类库,如rt.jar等。它是C/C++实现的,因此在Java中无法直接获取到。
扩展类加载器(Extension ClassLoader):
这个类加载器用来加载Java的扩展库,如$JAVA_HOME/lib/ext目录下的jar包。它是由Java编写的,在java.lang.ClassLoader的直接子类。
应用程序类加载器(Application ClassLoader):
也称为系统类加载器,负责加载应用程序classpath下的类库。它是由Java编写的,在java.lang.ClassLoader的直接子类。
自定义类加载器(Custom ClassLoader):
可以根据需求自定义的类加载器,继承自java.lang.ClassLoader。通过这种方式,可以实现一些特殊需求,如加载加密的类文件、从网络上动态下载类等。
双亲委派机制(Parent Delegation Mechanism)是Java中的一种类加载机制。在Java中,类加载器负责加载类的字节码并创建对应的Class对象。双亲委派机制是指当一个类加载器收到类加载请求时,它会先将该请求委派给它的父类加载器去尝试加载。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。
这种机制的设计目的是为了保证类的加载是有序的,避免重复加载同一个类。Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,它负责加载Java核心类库。其他加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。通过双亲委派机制,可以确保类在被加载时,先从上层的加载器开始查找,逐级向下,直到找到所需的类或者无法找到为止。
这种机制的好处是可以避免类的重复加载,提高了类加载的效率和安全性。同时,它也为Java提供了一种扩展机制,允许开发人员自定义类加载器,实现特定的加载策略。
优点:
缺点:
凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层C语言的库。
被native关键字修饰的方法叫做本地方法,本地方法和其它方法不一样,本地方法意味着和平台有关,因此使用了native的程序可移植性都不太高。另外native方法在JVM中运行时数据区也和其它方法不一样,它有专门的本地方法栈。native方法主要用于加载文件和动态链接库,由于Java语言无法访问操作系统底层信息(比如:底层硬件设备等),这时候就需要借助C语言来完成了。被native修饰的方法可以被C语言重写。
JNI:Java Native Interface (java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈,其他的就是Java栈;
Native Interface 本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷及时融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是在 Native Method Stack 中登记native方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java管理系统生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service 等等,不多做介绍!
Native Method Stack
它的具体做法是Native Method Stack 中登记native方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies。
通常所说的 JVM 内存布局,一般指的是 JVM 运行时数据区(Runtime Data Area),也就是当字节码被类加载器加载之后的执行区域划分。
《Java虚拟机规范》中将 JVM 运行时数据区域划分为以下 5 部分:
程序计数器(Program Counter Register):用于记录当前线程执行的字节码指令地址,是线程私有的,线程切换不会影响程序计数器的值。
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息,也是线程私有的。每个方法在执行时都会创建一个栈帧,栈帧包含了方法的局部变量表、操作数栈等信息。
本地方法栈(Native Method Stack):与 Java 虚拟机栈类似,用于存储本地方法的信息。
Java 堆(Java Heap):用于存储对象实例和数组,是 JVM 中最大的一块内存区域,它是所有线程共享的。堆通常被划分为年轻代和老年代,以支持垃圾回收机制。
年轻代(Young Generation):用于存放新创建的对象。年轻代又分为 Eden 区和两个 Survivor 区(通常是一个 From 区和一个 To 区),对象首先被分配在 Eden 区,经过垃圾回收后存活的对象会被移到 Survivor 区,经过多次回收后仍然存活的对象会晋升到老年代。
老年代(Old Generation):用于存放存活时间较长的对象。老年代主要存放长时间存活的对象或从年轻代晋升过来的对象。
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也是所有线程共享的。
PC 寄存器的作用包括:
Method Area 方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
jdk7之前常量池存在方法区中,jdk7之后常量池存在堆中
static、final、Class、常量池
栈是线程级的
栈:栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就over
栈:八大基本类型、对象引用、局部变量、实例的方法
栈运行原理:栈帧
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
堆内存中还要细分为三个区域:
GC 垃圾回收,主要是在新生区和老年区,
假设内存满了,OOM,堆内存不够! java.lang.OutOfMemoryError:Java heap space
在JDK8以后,永久存储区改了个名字(元空间,元空间在计算机的内存中,不在JVM中)
新生区:
老年区:
新生区一个对象经历了15次轻GC都活下来了,就会进入年老区
新生区中的伊甸园区,幸存区0,1都满了,就会进行重GC,活下来的对象进入老年区
永久区:
这个区域常驻内存的。用来存在JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟就会释放这个区域的内存
一个启动类,加载了大量的第三方jar包。Tomcat 部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM
注意:方法区是一种逻辑概念,永久代和元空间都是对方法区的具体实现
逻辑上存在,物理上不存在
引用计数法和可达性分析法
引用计数法:
可达性分析法:
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
复制算法,标记清除法,标记压缩法。
1.复制算法
复制算法主要是用在新生区
复制算法最佳使用场景:对象存活度较低的时候;新生区~
2.标记清除算法
3.标记压缩法
总结:
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
思考一个问题:难道没有最优算法吗?
答案:没有,没有最好的算法,只有最合适的算法 -------> GC:分代收集算法
年轻代:
老年代:
Full GC(Full Garbage Collection)是指对整个堆内存进行垃圾回收的过程。在进行 Full GC 时,会对年轻代和老年代(以及永久代或元数据区)中的所有对象进行回收。
Full GC 通常发生在以下情况之一:
Full GC 是一种较为耗时的操作,因为它需要扫描和回收整个堆内存。在 Full GC 过程中,应用程序的执行通常会暂停,这可能会导致较长的停顿时间(长时间的停顿会影响应用程序的响应性能)。 为了避免频繁的 Full GC,通常采取一些优化措施,如合理设置堆大小、调优垃圾回收参数、减少对象的创建和存活时间等。
JVM 调优是一个很大的话题,在回答“如何进行 JVM 调优?”之前,首先我们要回答一个更为关键的问题,那就是,我们为什么要进行 JVM 调优?
只有知道了为什么要进行 JVM 调优之后,你才能准确的回答出来如何进行 JVM 调优?
要进行 JVM 调优无非就是以下两种情况:
所以,针对不同的 JVM 调优的手段和侧重点也是不同的。
总的来说,JVM 进行调优的流程如下:
1.确定JVM调优原因
先确定是目标驱动型的 JVM 调优,还是问题驱动型的 JVM 调优。
如果是目标性的 JVM 调优,那么 JVM 调优实现思路就比较简单了,如:
如果是以问题驱动的 JVM 调优,那就要先分析问题是什么,然后再进行下一步的调优了。
2.分析JVM运行情况
我们可以借助于目前主流的监控工具 Prometheus + Grafana 和 JDK 自带的命令行工具,如 jps、jstat、jinfo、jstack 等进行 JVM 运行情况的分析。
主要分析的点是 Young GC 和 Full GC 的频率,以及垃圾回收的执行时间。
3.设置JVM调优参数
常见的 JVM 调优参数有以下几个:
4.压测观测调优后的效果
JVM 参数调整之后,我们要通过压力测试来观察 JVM 参数调整前和调整后的差别,以确认调整后的效果。
5.应用调优后的配置
在确认了 JVM 参数调整后的效果满足需求之后,就可以将 JVM 的参数配置应用与生产环境了。
内存溢出(Memory Overflow)和内存泄漏(Memory Leak)是两个与内存管理相关的问题,它们有以下区别:
相比于永久代,元空间具有更好的灵活性和扩展性,可以更好地满足不同应用程序的需求。 永久代的大小是固定的,当加载的类信息、常量池等数据超过了永久代的大小时,就会导致内存溢出。而元空间的大小可以根据需要进行调整,不再受到固定大小的限制。同时,元空间的数据可以存储在本地内存中,不再受到 Java 堆大小的限制。因此,使用元空间替代永久代可以提高程序的灵活性和稳定性。 所以,元空间的优势主要体现在以下两点:
RabbitMQ 是一个开源的消息中间件,使用 Erlang 语言开发。这种语言天生非常适合分布式场景,RabbitMQ 也就非常适用于在分布式应用程序之间传递消息。
特点:
AMQP: AMQP 不是一个具体的消息中间件产品,而是一个协议规范。他是一个开放的消息产地协议,是一种应用层的标准协议,为面向消息的中间件设计。AMQP 提供了一种统一的消息服务,使得不同程序之间可以通过消息队列进行通信。 SpringBoot 框架默认就提供了对 AMQP 协议的支持springAMQP。
RabbitMQ:RabbitMQ则是一个开源的消息中间件,是一个具体的软件产品。RabbitMQ 使用 AMQP 协议来实现消息传递的标准,但其实他也支持其他消息传递协议,如 STOMP 和 MQTT。RabbitMQ 基于 AMQP 协议定义的消息格式和交互流程,实现了消息在生产者、交换机、队列之间的传递和处理。
SpringAMQP的三个功能:
Fanout广播
Direct订阅
Topic通配符订阅
Headers头匹配
交换机主要作用:
topic模式消费者代码演示:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg){
System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg){
System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}
不管是生产者还是消费者,都需要配置MQ的基本信息。分为两步:
<!--消息发送-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: 192.168.150.101 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 123 # 密码
生产者可靠性:
①生产者重试机制:
如网络故障情况,SpringAMQP提供的消息发送时的重试机制,当RabbitTemplate
与MQ连接超时后,多次重试。但是该机制是阻塞式的重试,所以大多禁用该机制。
如何开启?:
修改publisher
模块的application.yaml
文件,添加下面的内容:
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
②生产者确认机制:
在publisher模块的application.yaml
中添加配置:
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
publisher-confirm-type参数:
none:关闭确认机制
simple:同步阻塞等mq回执
correlated:异步回调返回回执
触发确认的几种情况:
MQ可靠性
①数据持久化:
交换机和队列持久化开启方式:
durable:true
消息持久化开启方式:
deliveryMode=2
②LazyQueue:
开启:
x-queue-mode=lazy
消费者可靠性
①消费者确认机制
acknowledge-mode=auto
②失败重试机制
消息会不断入队
③失败处理策略
④业务幂等性
设唯一消息id
当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
basic.reject
或 basic.nack
声明消费失败,并且消息的requeue
参数设置为false(消息被消费者明确拒绝)如果一个队列中的消息已经成为死信,并且这个队列通过**dead-letter-exchange
属性指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机就称为死信交换机**(Dead Letter Exchange)。而此时加入有队列与死信交换机绑定,则最终死信就会被投递到这个队列中。
OSI七层模型:
TCP/IP 四层模型:
应用层常见协议:
传输层常见协议:
网络成常见协议:
HTTPS就是在HTTP和TCP层之间加了SSL/TLS安全传输层
http://
,HTTPS 的 URL 前缀是 https://
。HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们如何保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。
在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。
Cookie 被禁用怎么办?
最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。
什么是WebSocket?
WebSocket 是一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。
WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebSocket 和 HTTP 有什么区别?
WebSocket的工作过程?
Upgrade: websocket
和 Sec-WebSocket-Key
等字段,表示要求升级协议为 WebSocket;Connection: Upgrade
和 Sec-WebSocket-Accept: xxx
等字段、表示成功升级到 WebSocket 协议。DNS(Domain Name System)域名管理系统。DNS 要解决的是域名和 IP 地址的映射问题。
大致:浏览器缓存——操作系统缓存——hosts文件——本地DNS——根DNS——顶级DNS——权威DNS——本地DNS——客户端
什么时候选TCP什么时候选UDP?
UDP 一般用于即时通信,比如:语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。
TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。
第三次握手是可以携带数据的,前两次握手是不可以携带数据的
三次握手:
一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,等待服务端的确认;
二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端
三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入TCP连接成功状态,完成 TCP 三次握手。
四次挥手:
第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 FIN-WAIT-1 状态。
第二次挥手:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。
第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入超时等待时间,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
保证双方都有发送和接收的能力
HTTP/2.0:使用 TCP 作为传输协议、使用 HPACK 进行头部压缩、依赖 TLS 进行加密。
HTTP/3.0:使用基于 UDP 的 QUIC 协议、使用更高效的 QPACK 进行头部压缩、在 QUIC 中直接集成了 TLS。QUIC 协议具备连接迁移、拥塞控制与避免、流量控制等特性。
通过==滑动窗口==机制实现
通过四个
首部校验和,如果收到的段的校验和有差错,则丢弃该段
对失序数据包重排序以及去重
重传机制
超时重传(基于计时器重传),
快重传(连续收到三个重复确认),
SACK (可以将已收到的数据的信息发送给「发送方」,只重传丢失的数据,
D SACK (使用了 SACK 来告诉「发送方」有哪些数据被重复接收了)
流量控制(滑动窗口机制)
拥塞控制(慢开始,拥塞避免,快重传,快恢复)
1.2步是TLS握手阶段
对称加密和非对称加密结合的”混合加密“
根据ip地址查询相应的MAC地址
怎么查?
答:ARP 协议会在以太网中以广播的形式,对以太网所有的设备喊出:“这个 IP 地址是谁的?请把你的 MAC 地址告诉我”。
然后就会有人回答:“这个 IP 地址是我的,我的 MAC 地址是 XXXX”。
好像每次都要广播获取,这不是很麻烦吗?
答:后续操作系统会把本次查询结果放到一块叫做 ARP 缓存的内存空间留着以后用,不过缓存的时间就几分钟。
网络包只是存放在内存中的一串二进制数字信息,没有办法直接发送给对方。因此,我们需要将**数字信息转换为电信号**,才能在网线上传输。
负责执行这一操作的是网卡,要控制网卡还需要靠网卡驱动程序。
网卡驱动获取网络包之后,会将其复制到网卡内的缓存区中,接着会在其开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列。
FCS
(帧校验序列)用来检查包传输过程是否有损坏交换机里的模块将电信号转换为数字信号。
通过包末尾的 FCS
校验错误,如果没问题则放到缓冲区。
交换机的端口不具有 MAC 地址
交换机的 MAC 地址表主要包含两个信息:
路由器的端口具有 MAC 地址,因此它就能够成为以太网的发送方和接收方;同时还具有 IP 地址
总的来说,路由器的端口都具有 MAC 地址,只接收与自身地址匹配的包,遇到不匹配的包则直接丢弃。
接下来,路由器会根据 MAC 头部后方的 IP
头部中的内容进行包的转发操作。
在网络包传输的过程中,源 IP 和目标 IP 始终是不会变的,一直变化的是 MAC 地址,因为需要 MAC 地址在以太网内进行两个设备之间的包传输。
HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。
HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存。
强制缓存:
强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。
协商缓存:
协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存。
问题场景:
客户端通过浏览器向服务端发起 HTTPS 请求时,被「假基站」转发到了一个「中间人服务器」,于是客户端是和「中间人服务器」完成了 TLS 握手,然后这个「中间人服务器」再与真正的服务端完成 TLS 握手。
但是要发生这种场景是有前提的,前提是用户点击接受了中间人服务器的证书。
中间人服务器与客户端在 TLS 握手过程中,实际上发送了自己伪造的证书给浏览器,而这个伪造的证书是能被浏览器(客户端)识别出是非法的,于是就会提醒用户该证书存在问题。
所以,HTTPS 协议本身到目前为止还是没有任何漏洞的,即使你成功进行中间人攻击,本质上是利用了客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全。
但 HTTP/1.1 还是有性能瓶颈:
Body
的部分;HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。
使用QUIC协议,优点:
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
HPACK 算法主要包含三个组成部分:
客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 ==Huffman 编码==压缩数据,可达到 50%~90% 的高压缩率。
其实,TCP
是70年代出来的协议,而 HTTP
是 90 年代才开始流行的。而直接使用裸 TCP 会有问题,可想而知,这中间这么多年有多少自定义的协议,而这里面就有80年代出来的 RPC
。
所以我们该问的不是既然有 HTTP 协议为什么要有 RPC,而是为什么有 RPC 还要有 HTTP 协议。
HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。
源端口和目的端口
序列号
滑动窗口大小
校验和
首部长度
控制位:SYN ACK FIN RST(出现异常必须强制断开连接)
当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:
文件描述符限制
每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
cat /proc/sys/fs/file-max
查看;cat /etc/security/limits.conf
查看;cat /proc/sys/fs/nr_open
查看;内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。
攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
避免 SYN 攻击方式,可以有以下四种方法:
正常流程:
accpet()
socket 接口,从「 Accept 队列」取出连接对象。客户端出现故障指的是客户端的主机发生了宕机,或者断电的场景。发生这种情况的时候,如果服务端一直不会发送数据给客户端,那么服务端是永远无法感知到客户端宕机这个事件的,也就是服务端的 TCP 连接将一直处于 ESTABLISH
状态,占用着系统资源。
为了避免这种情况,TCP 搞了个保活机制。
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。
处于 Established 状态的服务端,如果收到了客户端的 SYN 报文(注意此时的 SYN 报文其实是乱序的,因为 SYN 报文的初始化序列号其实是一个随机数),会回复一个携带了正确序列号和确认号的 ACK 报文,这个 ACK 被称之为 Challenge ACK。
接着,客户端收到这个 Challenge ACK,发现确认号(ack num)并不是自己期望收到的,于是就会回 RST 报文,服务端收到后,就会释放掉该连接。
TCP 和 UDP 是两种不同的传输层协议,理论上它们是可以同时绑定到相同的端口的。因为 TCP 和 UDP 使用的端口号在操作系统中是相互独立的,两者的套接字由 (IP 地址, 端口号, 协议)
组成。因此,TCP 和 UDP 可以在同一个 IP 地址和端口号上并存,但它们的协议类型不同,这使得操作系统能够区分它们。
依靠ICMP协议,ICMP:用于告知网络包传送过程中产生的错误和各种控制信息
127.0.0.1
是回环地址。localhost
是域名(存在hosts文件中),但默认等于 127.0.0.1
。ping
回环地址和 ping
本机地址,是一样的,走的是lo0 "假网卡",都会经过网络层和数据链路层等逻辑,最后在快要出网卡前狠狠拐了个弯, 将数据插入到一个链表后就软中断通知 ksoftirqd 来进行收数据的逻辑,压根就不出网络。所以断网了也能 ping
通回环地址。