Java后端开发八股文
2023-08-29 16:38:16 35 举报
AI智能生成
Java后端开发主要涉及使用Java语言进行服务器端程序的开发,包括设计、编写、测试和优化代码。这需要对Java语言有深入的理解,包括但不限于面向对象编程、异常处理、多线程、集合框架等。同时,还需要熟悉常用的Java开发框架,如Spring、Hibernate等,以及数据库技术,如MySQL、Oracle等。此外,对于网络协议、操作系统、数据结构和算法等也有一定的了解。在实际开发中,需要能够根据需求设计和实现高效、稳定、可扩展的系统。同时,也需要有良好的问题解决能力,能够在遇到问题时迅速定位并解决问题。
作者其他创作
大纲/内容
Java基础
面向对象的特征
抽象 封装 继承 多态
多态
修饰符
访问修饰符
private
被private修饰的属性和方法,不能被其他类访问,子类不能继承也不能访问。只能在所在类内部访问。
缺省
缺省变量或者方法前没有访问修饰符时,可以被所在类访问,可以被同一包内的其他类访问或者继承。但是不能被其他包访问
protected
被protected修饰的方法和属性,在同一包内可被访问和继承。不同包内,子类可继承,非子类不能访问
public
方法和属性前有public修饰,可以被任意包内的类访问。
另外,类要想被其他包导入,必须声明为public。被public修饰的类,类名必须与文件名相同
另外,类要想被其他包导入,必须声明为public。被public修饰的类,类名必须与文件名相同
非访问修饰符
static
用来修饰方法和变量
静态变量(类变量)
static 关键字用来声明独立于对象的静态变量,无论一个类实例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static 变量
静态方法
静态方法内不能访问非静态方法
在静态方法中不能访问非静态成员方法和非静态成员变量
但是在非静态成员方法中是可以访问静态成员方法/变量
但是在非静态成员方法中是可以访问静态成员方法/变量
可以直接通过 类.静态方法 调用,不需要this和对象
可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途
被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。
想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问
被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。
想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问
final
用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的
abstract
用来创建抽象类和抽象方法
synchronized 和 volatile
final
作用在变量,不能被修改
作用在方法,可以被重载,不能重写
作用在类,不能被继承
作用在方法,可以被重载,不能重写
作用在类,不能被继承
final在多线程并发下的应用
由fianl域的读写重排序规则,可以保证正在创建中的对象不能被其他线程访问到
写final域重排序规则:禁止对final域的写 重排序到构造方法之外
编译器会在fianl域写之后,构造方法的return语句之前插入一个storestore屏障
编译器会在fianl域写之后,构造方法的return语句之前插入一个storestore屏障
读final域的重排序规则为:在一个线程中,初次读取对象引用和初次读取该对象包含的final域,
JMM会禁止这两个的重排序操作。 处理器会在这两个操作前面插上LoadLoad内存屏障
JMM会禁止这两个的重排序操作。 处理器会在这两个操作前面插上LoadLoad内存屏障
内部类
根据静态与否划分
静态内部类
内部类的创建不依赖外部类的实例对象
非静态内部类
内部类实例的创建必须依赖外部类的实例对象,所以普通的内部类不允许有静态的成员
根据位置划分
成员内部类
组成:静态的内部类可以有静态成员,非静态的内部类不能有静态成员
访问:静态内部类可以访问外部类的静态变量,不可以访问外部类的非静态变量;非静态内部类可以访问外部类的成员
创建:静态内部类的创建不依赖外部类,非静态内部类的创建必须依赖外部类的实例
局部内部类: 定义和使用只存在于方法内部的内部类
区别
不能用public,private等修饰,只能方法内部使用和访问
局部内部类可以访问外部类的成员变量,但该成员必须声明为 final,并内部不允许修改该变量的值。(这句话并不准确,因为如果不是基本数据类型的时候,只是不允许修改引用指向的对象,而对象本身是可以被就修改的)
匿名内部类: 一般用于函数参数,用于实现一个抽象类或者接口
约束
匿名内部类是没有访问修饰符的
匿名内部类必须继承一个抽象类或者实现一个接口
匿名内部类必须继承一个抽象类或者实现一个接口
匿名内部类是没有构造方法的,因为它没有类名
作用
内部类可以访问该类定义所在作用域中的数据,包括被private修饰的私有数据
内部类可以对同一个包中的其他类隐藏
内部类可以实现java单继承的缺点
当我们想要定义一个回调函数却不想写大量代码的时候我们可以选择使用匿名内部类来实现
Object类
clone()
创建并返回此对象的一个副本
equals(Object obj)
指示某个其他对象是否与此对象“相等”
equals相同,hashcode一定相同
equals不相同,hashcode可能相同
hashcode不同,equals一定不同
equals不相同,hashcode可能相同
hashcode不同,equals一定不同
finalize()
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
getClass()
返回一个对象的运行时类。
hashCode()
返回该对象的哈希码值。
notify()
唤醒在此对象监视器上等待的单个线程
notifyAll()
唤醒在此对象监视器上等待的所有线程
toString()
返回该对象的字符串表示
wait()
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法
wait(long timeout)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量
wait(long timeout, int nanos)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
反射
创建方式
1、forName(全限定路径)
Class classobj1 = Class . forName ("com.fanshe.student") ;
2、类名.class
Class classobj2 = Student.class;
3、对象.getClass()
Student stu = new StudentO ;
Class classobj3 = stu. getClassO;
Class classobj3 = stu. getClassO;
通过反射机制,可以在运行时访问 Java 对象的属性,方法,构造方法等
应用场景:开发通用框架、动态代理(AOP)、注解、可扩展性功能
缺点
破坏封装性
反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题
性能开销
由于反射涉及动态解析的类型,因此无法执行某些jvm优化。反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用且频繁调用的代码段中避免
动态加载类
面试题:如何在Java程序运行时不停机动态加载一个函数进来?
newInstance()和new()的区别
1、创建前提不同:newInstance创建类是这个类必须已经加载过且已经连接,new创建类是则不需要这个类加载过
2、创建对象的方式不同:newInstance是实用类的加载机制,new则是直接创建一个类
3、创建对象类型不同:newInstance: 弱类型(GC回收对象的限制条件很低,容易被回收)、低效率、只能调用无参构造)
new 强类型(GC不会自动回收,只有所有的指向对象的引用被移除是才会被回收,若对象生命周期已经结束,但引用没有被移除,经常会出现内存溢出)
new 强类型(GC不会自动回收,只有所有的指向对象的引用被移除是才会被回收,若对象生命周期已经结束,但引用没有被移除,经常会出现内存溢出)
接口和抽象类
抽象类
抽象类含无实现的方法,不能创建对象
抽象类不必须含有抽象方法
抽象类不能设private,生来就为了继承
子类必须实现父类抽象类方法,否则自己也设抽象类
抽象类不必须含有抽象方法
抽象类不能设private,生来就为了继承
子类必须实现父类抽象类方法,否则自己也设抽象类
接口
default方法:接口方法提供的默认实现,必须使用 default 修饰符标记这个方法
解决默认方法冲突
接口冲突:1.如果一个类实现了A,B两个接口,这两个接口有相同的默认方法,则实现类需要重写这个默认方法
超类优先:2.如果一个类的超类中方法和该类的实现接口默认方法相同,则这个类继承的方法以超类的方法为准
类优先:3.如果一个类扩展了超类的方法,并且该方法与实现接口中默认方法同名同参,则该方法以类优先
静态方法:java8开始,允许接口中包含静态方法
接口中的域(即常量): 自动地设置为 public static final
A对象 instanceof B接口:判断A类是否是B接口的实例
区别
重写:抽象类中的抽象方法必须实现,非抽象方法不用,接口必须实现所有非default方法
成员变量:抽象类可以是各种类型,接口只能是public static final类型
方法:抽象类有方法实现细节,接口只有public abstract方法
静态方法/代码块:抽象类有,接口可以有静态方法(java8的新特性),无静态代码块
继承/实现:只能继承一个抽象类,但可以实现多个接口
抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)
接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。对“接口为何是约束”的理解,我觉得配合泛型食用效果更佳。
深拷贝&浅拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容
区别:对引用数据类型是否需要创建一个新的对象
扩展
基本数据类型
byte,short,int,long,doule,float,boolean,char
方法中定义的基本数据类型局部变量的具体内容是存储在栈中
但基本类型成员变量也是随类一起储存在堆里
但基本类型成员变量也是随类一起储存在堆里
详解
引用数据类型
类(对象)、接口 、数组
引用数据类型变量的具体内容都是存放在堆中的,地址存放在栈中
File类
public boolean createNewFile() throws IOException
创建文件:当且仅当不存在具有此抽象路径名指定的名称的文件时,原子地创建由此抽象路径名指定的一个新的空文件。
创建文件:当且仅当不存在具有此抽象路径名指定的名称的文件时,原子地创建由此抽象路径名指定的一个新的空文件。
public boolean mkdir()
创建目录:创建此抽象路径名指定的目录
创建目录:创建此抽象路径名指定的目录
public boolean delete()
删除此抽象路径名表示的文件或目录
删除此抽象路径名表示的文件或目录
public String getName()
返回由此抽象路径名表示的文件或目录的名称
返回由此抽象路径名表示的文件或目录的名称
public String getParent() / getParentFile()
返回此抽象路径名的父路径名的路径名字符串 / 抽象路径名,如果此路径名没有指定父目录,则返回 null
返回此抽象路径名的父路径名的路径名字符串 / 抽象路径名,如果此路径名没有指定父目录,则返回 null
public boolean exists()
测试此抽象路径名表示的文件或目录是否存在
测试此抽象路径名表示的文件或目录是否存在
public long length()
返回由此抽象路径名表示的文件的长度
返回由此抽象路径名表示的文件的长度
public String toString()
返回此抽象路径名的路径名字符串
返回此抽象路径名的路径名字符串
public boolean equals(Object obj)
测试此抽象路径名与给定对象是否相等
测试此抽象路径名与给定对象是否相等
...
标识符
标识符由数字(0~9)和字母(A~Z 和 a~z)、美元符号($)、下划线(_)
以及 Unicode 字符集中符号大于 0xC0 的所有符号组合构成(各符号之间没有空格)。
标识符的第一个符号为字母、下划线和美元符号,后面可以是任何字母、数字、美元符号或下划线。
另外,Java 区分大小写,因此 myvar 和 MyVar 是两个不同的标识符。
提示:标识符命名时,切记不能以数字开头,也不能使用任何 Java 关键字作为标识符,而且不能赋予标识符任何标准的方法名
以及 Unicode 字符集中符号大于 0xC0 的所有符号组合构成(各符号之间没有空格)。
标识符的第一个符号为字母、下划线和美元符号,后面可以是任何字母、数字、美元符号或下划线。
另外,Java 区分大小写,因此 myvar 和 MyVar 是两个不同的标识符。
提示:标识符命名时,切记不能以数字开头,也不能使用任何 Java 关键字作为标识符,而且不能赋予标识符任何标准的方法名
正则表达式
常用符号
ps:一般\大写字母表示小写字母的反义
. 表示匹配任意的字符
[]代表匹配中括号中其中任一个字符,如[abc]匹配a或b或c
\d 表示数字
\D 表示非数字
\s表示由空字符组成,[ \t\n\r\x\f]
\S表示由非空字符组成,[^\s]
\w表示字母、数字、下划线,[a-zA-Z0-9_]
\W表示不是由字母、数字、下划线组成
?: 表示出现0次或1次
+表示出现1次或多次
{n,m}表示出现n~m次
JAVA调用正则表达式的类是java.util.regex.Matcher 和 java.util.regex.Pattern
重载Overload&重写Override
重写(Override)
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写
重载(Overload)
重载就是同样的一个方法能够根据传入参数的不同,做出不同的处理
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数个数or类型必须不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同
区别
构造器
不能被继承、不能被重写Override、可以被重载
继承(extends)的含义其实是“扩展”,子类完全没必要扩展父类的构造函数,因为反正每次调子类的时候都会“自动运行”它父类的构造函数,如果真的需要子类构造函数特殊的形式,子类直接修改或重载自己的构造函数就好了
1、在子类继承父类的时候,子类必须调用父类的构造函数;
2、在父类有默认构造函数,子类实例化时自动调用,在父类没有默认构造函数,即无形参构造函数,子类构造函数必须通过super调用父类的构造函数;
3、在java的继承当中子类是不可以继承父类的构造函数,只能调用父类的构造函数
2、在父类有默认构造函数,子类实例化时自动调用,在父类没有默认构造函数,即无形参构造函数,子类构造函数必须通过super调用父类的构造函数;
3、在java的继承当中子类是不可以继承父类的构造函数,只能调用父类的构造函数
面试题
说说String&StringBuffer&StringBuilder区别
可变性
String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以String 对象是不可变的
StringBuffer 与 StringBuilder 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
但是没有用 final 关键字修饰,所以这两种对象都是可变的
但是没有用 final 关键字修饰,所以这两种对象都是可变的
StringBuffer是字符串变量,它的对象是可以扩充和修改的。
StringBuilder是一个可变的字符序列
StringBuilder是一个可变的字符序列
线程安全性和性能
String不可变,线程安全;String的修改会生成一个新对象,性能最低
StringBuffer和StringBuilder都是对本身修改,相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,
但却要冒多线程不安全的风险。
但却要冒多线程不安全的风险。
StringBuffer 对方法加了同步锁,线程安全
StringBuilder 没对方法加同步锁,线程不安全
StringBuilder 没对方法加同步锁,线程不安全
为什么String 设计为final?
因为要保证string的不可变性
什么是不可变性
给一个已有字符串"abcd"第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
为什么不可变
string的主要成员字段value是个char[ ]数组,而且是用private final修饰的,再加上string自身的final,为的就是安全性
不可变有什么好处?
安全
当String支持不可变性的时候,它们的值很好确定,不管调用哪个方法,都互不影响
不可变性支持线程安全
在并发场景下,多个线程同时读一个资源,是不会引发竟态条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全。
不可变性支持字符串常量池
例如:字符串 one 和 two 都用字面量 "something" 赋值。它们其实都指向同一个内存地址。
在大量使用字符串的情况下,这样可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了
final 在 Java 中有什么作用?
final 修饰的类叫最终类,该类不能被继承。
final 修饰的方法不能被重写。
final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
Java为什么是值传递
错误概念
错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递
错误理解二:Java是引用传递
错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递
什么是值/引用传递
实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数
当我们调用一个有参函数的时候,会把实际参数传递给形式参数。
但是,在程序语言中,这个传递过程中传递的两种情况,即值传递和引用传递
当我们调用一个有参函数的时候,会把实际参数传递给形式参数。
但是,在程序语言中,这个传递过程中传递的两种情况,即值传递和引用传递
值传递(pass by value):是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数
引用传递(pass by reference):是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
==和equals的区别
==比较基本数据类型时比较的是字面量,比较引用数据类型时比较的是在内存中的地址
两个new的Integer(False)
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的
(因为new生成的是两个对象,其内存地址不同)
(因为new生成的是两个对象,其内存地址不同)
两个非new的Integer(True/False)
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,
如果两个变量的值不在此区间,则比较结果为false
(因为当值不在-128和127间时,非new生成Integer变量时,java API中最终会按照new Integer(i)生成)
如果两个变量的值不在此区间,则比较结果为false
(因为当值不在-128和127间时,非new生成Integer变量时,java API中最终会按照new Integer(i)生成)
非new的Integer和new的Integer(False)
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false,
(因为 ①当变量值在-128~127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;②当变量值在-128~127之间时,非new生成Integer变量时,java API中最终会按照new Integer(i)进行处理,最终两个Interger的地址同样是不相同的)
(因为 ①当变量值在-128~127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;②当变量值在-128~127之间时,非new生成Integer变量时,java API中最终会按照new Integer(i)进行处理,最终两个Interger的地址同样是不相同的)
new的Integer和基本类型int(True)
Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true
(因为包装类Integer和基本数据类型int比较时,java会自动拆箱为int,然后进行比较,实际上就变为两个int变量的比较)
(因为包装类Integer和基本数据类型int比较时,java会自动拆箱为int,然后进行比较,实际上就变为两个int变量的比较)
Object的equals默认是比较对象的内存地址值,但String一般会重写equals,使其变为比较字面量
ps:equals在比较基本类型的时候会自动装箱成
ps:equals在比较基本类型的时候会自动装箱成
不同对象的equals具体举例
为什么重写 equals 时必须重写 hashCode 方法
“如果两个对象相同,那么他们的hashcode应该相等”
重写equals是为了修改equals功能为 只比较字面值 ,如果不重写hashcode方法,
那么在hashset中会出现equals调用结果相同,但hashcode不同导致出现重复元素
那么在hashset中会出现equals调用结果相同,但hashcode不同导致出现重复元素
如何重写hashcode?
new people(1,"lily")
new people(1,'lily')
这个两个对象除了内存地址都是一样的,
所以我们希望存入hashMap里时不会出现类似重复对象
所以我们重写equals方法,改为判断类对象(instance of)
如果不重写hashcode的话,就有可能equals相等,hashcode不相等,
导致hashmap里无法覆盖相同的对象
new people(1,'lily')
这个两个对象除了内存地址都是一样的,
所以我们希望存入hashMap里时不会出现类似重复对象
所以我们重写equals方法,改为判断类对象(instance of)
如果不重写hashcode的话,就有可能equals相等,hashcode不相等,
导致hashmap里无法覆盖相同的对象
首先要明白hashcode的作用是在集合里用来匹配位置查找对象
hashcode和equals的区别
Object类的hashcode方法,底层调用的是native hashcode(),是根据地址转换而来的,String重写了hashcode,会使得不同字符串存在hash冲突
如何解决hashcode冲突
链地址法(Java hashmap就是这么做的)
开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
再哈希法
建立一个公共溢出区
java写一个Demo类执行main函数到打印hello world时都经历了哪些流程
编译期&运行期区别
1、编译
通过javac编译器进行编译,从Java源码 ---> Java 字节码
通过 javac xxx.java 即可以编译该源码,javac编译器位于jdk --> bin -->javac
通过 javac xxx.java 即可以编译该源码,javac编译器位于jdk --> bin -->javac
javac(编译期)具体操作
1、分析和填充符号表
语法&词法分析
把源代码的字符流转为标记(Token)集合,标记是编译阶段的最小单元,字符则是编程阶段里源码的最小单元
比如,int i = 0由4个标记构成分别是「int,i,=,0」编译器只认识这些标记
词法分析过程就是识别每个标记的过程
语法分析则是把生成的标记集合 构成一个语法树,每个节点代表程序代码中的语法结构,如包,类型,修饰符,运算符等等
比如,int i = 0由4个标记构成分别是「int,i,=,0」编译器只认识这些标记
词法分析过程就是识别每个标记的过程
语法分析则是把生成的标记集合 构成一个语法树,每个节点代表程序代码中的语法结构,如包,类型,修饰符,运算符等等
填充符号表
在词法分析之后我们需要把数据存起来,以供后续流程使用,编译器会以key-value的形式存储数据,以符号地址为key符号信息为value,具体形式没做限制可以是树状符号表或者有序符号表等。
在语义分析(第三步)中,根据符号表所登记的内容 语义检查和产生中间代码,在目标代码生成阶段,当对符号表进行地址分配时,该符号表是检查的依据
在语义分析(第三步)中,根据符号表所登记的内容 语义检查和产生中间代码,在目标代码生成阶段,当对符号表进行地址分配时,该符号表是检查的依据
2、注解处理器
注解与普通的Java代码一样在运行期间发挥作用。可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。
如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
换句话说当我们处理注解时如果修改了语法树的话会重新执行分析以及符号填充过程,把注解也填充进来,直到处理完所有注解
如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
换句话说当我们处理注解时如果修改了语法树的话会重新执行分析以及符号填充过程,把注解也填充进来,直到处理完所有注解
3、语义分析
语法分析以及处理注解之后,编译器获得了程序代码的抽象语法树,语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。
即可能出现语法树上的内容单个来说是合法的但是结合到上下文语义则未必是合法的,例如:
即可能出现语法树上的内容单个来说是合法的但是结合到上下文语义则未必是合法的,例如:
4、解语法糖
Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。虚拟机并不支持这些语法,它们在编译阶段就被还原回了简单的基础语法结构,这个过程称为解语法糖。
换句话说,不论是否使用Java的语法糖,最终到jvm哪里的时候都是一样的基础语法结构,jvm不支持语法糖,所以需要编译阶段解语法糖,语法糖的初衷是用来提升开发效率(方便开发者开发),而不是代码性能。
换句话说,不论是否使用Java的语法糖,最终到jvm哪里的时候都是一样的基础语法结构,jvm不支持语法糖,所以需要编译阶段解语法糖,语法糖的初衷是用来提升开发效率(方便开发者开发),而不是代码性能。
5、字节码生成
字节码生成阶段主要工作就是将前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中
字节码生成是Javac编译过程的最后一个阶段,在Javac源码里面由com.sun.tools.javac. jvm.Gen类来完成。
字节码生成是Javac编译过程的最后一个阶段,在Javac源码里面由com.sun.tools.javac. jvm.Gen类来完成。
2、加载并执行
跳转JVM部分
java的new和newInstance()创建对象的区别
创建对象的方式不一样,newInstance是使用类加载机制,new是创建一个新类
因为软件的可伸缩、可扩展和可重用等软件设计思想,所以延申出两种创建方式
因为软件的可伸缩、可扩展和可重用等软件设计思想,所以延申出两种创建方式
待补充
为什么在一个静态方法内调用一个非静态成员为什么是非法的?
这个需要结合 JVM 的相关知识,静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,然后通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
“static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?
“static” 关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量情况下访问。
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用
Java容器
hashmap(线程不安全)
四个构造方法
无参、参为另一个Map、参为容量、参为容量+加载因子
put
1.7
key -> hashCode -> 扰动 -> (n-1)&hash -> 存在则比较key是否相同 -> 相同就覆盖不同就拉链法
1.8
key -> hashCode -> 扰动 -> (n-1)&hash -> 存在则比较key是否相同 -> 相同就覆盖不同就添加到树/链表 -> 可能树化
get 主要讲下要判断树和链表再get
扩容
当HashMap中元素数超过容量*加载因子时,HashMap会进行扩容
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 :容量默认16
若扩容前容量为2的n次方,每个元素的索引由后n-1位决定,扩容后则由后n位决定,多了一位
扩容时进行(n-1)&hash之后,若多的这一位为0则 新索引=原索引,为1则 新索引=原索引+原容量
扩容时进行(n-1)&hash之后,若多的这一位为0则 新索引=原索引,为1则 新索引=原索引+原容量
扩容步骤
1、扩容:创建一个新的Entry空数组,长度是原数组的2倍
2、ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组
hash具体操作:新index = HashCode(Key) & (Length - 1)
ps:%和/ 比 & 慢10倍左右,因此用(n-1)&hash
hash具体操作:新index = HashCode(Key) & (Length - 1)
ps:%和/ 比 & 慢10倍左右,因此用(n-1)&hash
补充
threshold = capacity * loadFactor,当 Size>=threshold的时候就扩容
loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值
loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值
static final float DEFAULT_LOAD_FACTOR = 0.75f; :加载因子默认为0.75
思路:默认容量,负载因子,扩容阈值,红黑树与链表转换阈值,JDK1.7与1.8的区别,为什么引入红黑树
JDK1.7和1.8的区别
创建
JDK7:new HashMap() 时,底层创建 size 为 16 的数组Entry[]
数据的储存位置一般是通过 hash(key.hashCode())%len 获得,也就是元素的 key 的哈希值对数组长度取模得到。
例如12%16=12;28%16=12;108%16=12;140%16=12。所以 12、28、108 以及 140 都存储在数组下标为 12
数据的储存位置一般是通过 hash(key.hashCode())%len 获得,也就是元素的 key 的哈希值对数组长度取模得到。
例如12%16=12;28%16=12;108%16=12;140%16=12。所以 12、28、108 以及 140 都存储在数组下标为 12
JDK8:首次调用 put() 时,底层创建 size 为 16 的数组Node[]。
底层
JDK7:数组+链表
JDK8:数组+链表+红黑树。
当数组某个索引位置的元素以链表形式存在的数据个数 >8 且当前数组的长度 >64 时,该索引位置上的所有数据改为使用红黑树存储。
当链表长度大于8时,由单链表转化为红黑树;而当链表长度小于6时,又由红黑树转化为单链表
当数组某个索引位置的元素以链表形式存在的数据个数 >8 且当前数组的长度 >64 时,该索引位置上的所有数据改为使用红黑树存储。
当链表长度大于8时,由单链表转化为红黑树;而当链表长度小于6时,又由红黑树转化为单链表
储存形式
新节点插入顺序
JDK7:头插法
扩展:头插法
JDK8:尾插法
扩展:尾插法
1.7hashmap因使用头插法,多线程扩容时会死循环,1.8改成尾插法
实际上hashmap还是线程不安全,还存在其他并发问题,例如源码中的put/get方法未加同步锁
无法保证上一秒put的值,下一秒get的时候还是上一秒put的原值(可能在get前被别的线程又put了)
无法保证上一秒put的值,下一秒get的时候还是上一秒put的原值(可能在get前被别的线程又put了)
concurrenthashmap(线程安全)
1.7
储存结构
segment数组,每个segment有一个hashEntry数组,每个hashEntry都是一条链表
采用锁分离技术:分段锁保证读写线程安全
采用锁分离技术:分段锁保证读写线程安全
每一个 Segment 类似于 HashMap 的结构,所以每一个 HashMap 的内部可以进行扩容。
但是 Segment 的个数一旦初始化就不能改变,默认 Segment 的个数是 16 个,意思ConcurrentHashMap默认支持最多 16 个线程并发
但是 Segment 的个数一旦初始化就不能改变,默认 Segment 的个数是 16 个,意思ConcurrentHashMap默认支持最多 16 个线程并发
初始化
初始化 并发级别 concurrencyLevel 大小,无参默认16
初始化 容量大小=concurrencyLevel 之上最近的 2 的幂次方,无参默认16
记录 segmentShift偏移量=28,sshift为【容量 = 2^N】中的 N, 在后面 Put 时计算位置时会用到。默认是 32 - sshift = 28.
记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15
初始化 segments[0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容
put
计算要 put 的 key 的位置,获取指定位置的 Segment,如果指定位置的 Segment 为空,则初始化这个 Segment
初始化segment流程
检查segment是否为空
使用 Segment[0] 的容量和负载因子创建一个 HashEntry 数组
检查segment是否为空
使用创建的 HashEntry 数组初始化这个 Segment
cas检查segment是否为空
自旋tryLock()上锁(segment继承了reentrantLock) -> 计算index -> 找到hashEntry并遍历
不存在:大于阈值就扩容+头插法插入
存在:遍历链表找到一样的key然后覆盖
不存在一样的key:大于阈值就扩容+头插法插入
存在就覆盖并返回旧值
get
先判断count是否等于0,然后遍历链表,最后判断如果value等于null,
就说明发生重排序,就加锁再获取这个值,最后解锁。
就说明发生重排序,就加锁再获取这个值,最后解锁。
扩容(rehash)
segment永远是16个(初始化可以改),只能扩容hashEntry数组,扩容机制和hashmap相同:容量*2,新索引=原索引 或 新索引=原索引+原容量
1.8
储存结构
Node 数组 + 链表 / 红黑树
初始化
自旋+cas。sizeCtl的值决定着当前的初始化状态
sizeCtl
0
代表数组未初始化,容量大小默认为16
n(大于0)
如果数组未初始化,则记录的是数组的初始容量
如果已初始化,则记录的是数组的扩容阈值(初始容量*加载因子)
如果已初始化,则记录的是数组的扩容阈值(初始容量*加载因子)
-1
数组正在进行初始化(调用put方法里的initTable())同时其他线程不能再初始化
-n(小于0,但不是-1)
表示数组正在扩容,-(1+n)表示当前正有n个线程共同完成数组的扩容任务(有争议)
InitTable 初始化数组
put(Key,Value)保证线程并发安全
put(Key,Value)保证线程并发安全
CAS+自旋,没有用到锁
new的时候传入参数
初始化:不是new的时候,而是调用put()方法里的putVal()里的initTable()
无参
DEFAULT_CAPACITY = 16; 容量大小默认为16
1个参数
initialCapacity
tableSizeFor
用于设置容量大小,为传入参数+传入参数右移1位+1后向上取整2的幂次位数
例如32,则位32+16+1=49,然后向上取2的6次方:64
用于设置容量大小,为传入参数+传入参数右移1位+1后向上取整2的幂次位数
例如32,则位32+16+1=49,然后向上取2的6次方:64
2个参数
initialCapacity
但是内部方法实际上会传入三个参数,
第三个为concurrencyLevel,默认传入为1
第三个为concurrencyLevel,默认传入为1
loadFactor
3个参数
initialCapacity (int)
loadFactor (float)
concurrencyLevel (int)
put
key -> hashCode -> cas写入 -> 若hashcode=-1则为正在扩容 -> 失败就用synchronized写 -> 大于阈值转树
get
找位置 -> 若hashcode=-1则为正在扩容 -> 遍历链表/树
面试题
1、jdk7和8的区别
整体结构
1.7:Segment + HashEntry + ReentrantLock(Unsafe)
1.8:移除Segment,使锁的粒度更小,Synchronized + CAS + Node数组 + Unsafe
put()
1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。
1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7)
2、jdk8为什么用cas+synchronized代替分段锁?
2.1、为什么要使用synchronized而不是如ReentranLock这样的可重入锁?
3、jdk8为什么舍弃segment的原因?
segment主要是为了分段锁
1.如果使用reentrantlock则每个节点都会继承AQS获取同步支持,增加内存开销。
而jdk8中只有链表或树根需要同步支持。
2.synchronized是JVM自带的,所以运行时JVM对synchronized有一定的调优
1.如果使用reentrantlock则每个节点都会继承AQS获取同步支持,增加内存开销。
而jdk8中只有链表或树根需要同步支持。
2.synchronized是JVM自带的,所以运行时JVM对synchronized有一定的调优
ArrayList(并发版CopyOnWriteArrayList
对比
与Vector
都实现了list
Vector古老、线程安全。ArrayList常用、线程不安全
与LinkedList(双向链表)(并发版ConcurrentLinkedQueue
都实现了list,都线程不安全
数组和链表的区别
构造方法
无参、参为容量、参为另一个Collection
初始化
无参构造时是个空数组,向其中加入第一个元素时,才扩容为10
1.7中类似饿汉式初始化数组,1.8类似懒汉式延迟加载
扩容
当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法。
当 add 第 2 个元素时,minCapacity 为 2,此时 e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。
添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。
直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容
grow方法
int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)
Set
hashset为什么要用present,
present是Object
present是Object
set的底层结构是hashmap,
它add()里的Put()方法使用的是hashmap的put,
所以是当key值相同时,覆盖旧值返回之前的对象值present!=null返回false
用空间换取一些信息
它add()里的Put()方法使用的是hashmap的put,
所以是当key值相同时,覆盖旧值返回之前的对象值present!=null返回false
用空间换取一些信息
JVM
字节码文件(.class文件)
class文件基本组织结构
图示
Class文件中的常量池详解(上)
Class文件中的常量池详解(下)
补充
1.常量池中,小于等于32767的int型常量不会保存,为1的long型数据不出现在常量池
类加载过程
加载(loading):将class字节码文件存放到方法区,在堆中创建Class对象
数组类型不通过类加载器创建,它由JVM直接创建
链接(linking)
验证(verification):校验字节码文件的正确性,文件格式是否符合class文件规范;元数据验证,符号引用验证
准备(preparation):为类的静态类变量分配内存,并初始化为默认值0值(不是指定值)
而实例变量会随着new了个实例对象后,在堆内分配空间
特例:加了final static的会在这个阶段直接赋指定值
而实例变量会随着new了个实例对象后,在堆内分配空间
特例:加了final static的会在这个阶段直接赋指定值
静态变量=类变量
实例变量=普通变量=非static变量
实例变量=普通变量=非static变量
解析(resolution):把类中的符号引用转换成直接引用
符号引用和直接引用的区别
初始化(initialization):对类的静态变量初始化赋值,并执行静态代码块
参考文献
类加载的过程(加载、验证、准备、解析、初始化)
Java中Class对象详解
类加载器
类加载底层调用流程(所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存)
双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这样避免了在lib目录下的文件被替换
作用
保证加载类的唯一性:当父加载器已经加载了该类时,就没有必要子加载器再加载一次
沙箱安全机制:自己写的java.lang.String.class不会被加载,防止核心api库被篡改
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略
所有的Java程序运行都可以指定沙箱,可以定制安全策略
BootstrapClassLoader用c++写的
userDefinedClassLoader自定义类加载器
负责加载用户自定义路径下的jar包和类
在双亲委派机制下自定义类加载器
不打破双亲委派模型,继承ClassLoader类+重写ClassLoader类中的findClass()方法,无法被父加载器加载的类最终会通过这个方法被加载
想打破双亲委派模型,继承ClassLoader类+ 重写findClass方法+重写loadClass方法或者使用上下文加载器
不打破双亲委派模型,继承ClassLoader类+重写ClassLoader类中的findClass()方法,无法被父加载器加载的类最终会通过这个方法被加载
想打破双亲委派模型,继承ClassLoader类+ 重写findClass方法+重写loadClass方法或者使用上下文加载器
自定义类加载器有什么用?
jvm自带的三个加载器只能加载指定路径下的类字节码
自定义类加载器可以加载本机或网络上的某个类文件
jvm自带的三个加载器只能加载指定路径下的类字节码
自定义类加载器可以加载本机或网络上的某个类文件
打破双亲委派机制的场景?
Tomcat。一个tomcat容器可能运行多个应用程序,每个应用需要同一种类库的不同版本,因此应用之间需要独立和隔离
Tomcat。一个tomcat容器可能运行多个应用程序,每个应用需要同一种类库的不同版本,因此应用之间需要独立和隔离
扩展类、应用类和自定义类加载器都是java.lang.ClassLoader的子类实例
自定义类加载器直接继承java.lang.ClassLoader
自定义类加载器直接继承java.lang.ClassLoader
AppClassLoader的父类加载器输出为ExtClassLoader
ExtClassLoader的父类加载器输出为null
null并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader
ExtClassLoader的父类加载器输出为null
null并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader
由于应用类加载器和扩展类加载器都继承了ClassLoader,而ClassLoader类中有parent变量,用于指示向上委托的方向,而不是通过下层继承上层实现
Java 类加载器怎么实现将同一个对象加载两次?
创建一个classloader,parent设置成null
沙箱安全机制
Java安全模型
运行时数据区
(java内存结构)
(java内存结构)
堆
存储对象,字符串常量池,静态变量
方法区
jdk7永久代
存储包括类定义,结构,字段,方法(数据及代码)以及常量在内的类相关数据(类的信息不是Class对象)
jdk8元空间
元空间代替了永久代,存储在本地内存(本地内存包含元空间和直接内存),存放已被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码
永久代被代替的原因
1.永久代的大小不好设置(类信息放在永久代的话,加载时不知道有多少个类),而且如果类加载过多很容易导致OOM
2.永久代的回收效率低
2.永久代的回收效率低
常量池
运行时常量池:一直在方法区,除了上述还包括字面量和符号引用
字符串常量池:1.7在方法区,1.8在堆
class文件常量池(非运行时常量池)
详解:https://www.jianshu.com/p/cf78e68e3a99
待补充https://www.processon.com/view/5dec50d7e4b0e2c298a6bd60?fromnew=1
待补充https://www.processon.com/view/5dec50d7e4b0e2c298a6bd60?fromnew=1
为什么jdk7之后字符串常量池要放入堆中呢?
因为永久代的回收效率很低,通常需要fullGC才进行回收,而fullGC是老年代触发majorCG后空间还不够或永久代快满才触发。放入堆中能够及时回收。
Java官方的HotSpot 虚拟机中,Java8版本以后,是用元空间来实现的方法区;在Java8之前的版本,则是用永久代实现的方法区;
也就是说,“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义;
也就是说,“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义;
虚拟机栈
栈帧
局部变量表
存放基本数据类型和对象引用
solt槽
Solt槽是局部变量表的最小单位
每个变量槽可以存放32位长度的内存空间
如果要放64位的数据就找两个槽
每个变量槽可以存放32位长度的内存空间
如果要放64位的数据就找两个槽
操作数栈
用来存放操作数
动态链接
每个栈帧都有一个指向运行时常量池该栈帧所属方法的引用。
目的是当调用其它方法时,从运行时常量池找到对应的符号引用,
并转成直接引用找到该方法。
目的是当调用其它方法时,从运行时常量池找到对应的符号引用,
并转成直接引用找到该方法。
方法返回地址
记录PC寄存器的值
当一个方法开始执行后,只有2种方式可以退出这个方法 :
方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。
而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。
而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
锁记录
本地方法栈
简单来讲就是Java调用非Java代码接口
为虚拟机使用的native方法服务,方法通常是使用C/C++编写,然后编译成.dll或者.so文件,再由JNI(Java Native Interface)调用执行。
为虚拟机使用的native方法服务,方法通常是使用C/C++编写,然后编译成.dll或者.so文件,再由JNI(Java Native Interface)调用执行。
程序计数器(唯一不会OOM的区域)
它的作用主要是通过程序计数器指针指向常量池的下一条偏移地址,
执行引擎根据偏移地址获取机器指令,再交给CUP执行
执行引擎根据偏移地址获取机器指令,再交给CUP执行
直接内存(堆外内存)
直接内存并不是jvm运行时数据区的一部分,也不是jvm规范中定义的内存区域。
在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式
它使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常
解决了NIO进行管道传输buffer时,发生full GC导致buffer位置发生改变。
在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式
它使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常
解决了NIO进行管道传输buffer时,发生full GC导致buffer位置发生改变。
和堆内存对比
直接内存创建和销毁更费性能,而IO读写的性能要优于普通的堆内存
直接内存是否会被GC?会
显式调用System.gc()强制执行FullGC进行回收
每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象
这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存
当directByteBuffer设为null后,指向它的虚引用cleaner就会进入pending队列,当发现队列里有cleaner对象,
就会调用方法把堆外的buffer给清掉
这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存
当directByteBuffer设为null后,指向它的虚引用cleaner就会进入pending队列,当发现队列里有cleaner对象,
就会调用方法把堆外的buffer给清掉
直接内存使用场景
有很大的数据需要存储,它的生命周期很长
适合频繁的IO操作,例如网络并发场景
优点
加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(堆外内存),然后在发送;而直接内存(堆外内存)相当于省略掉了这个工作
缺点
直接内存难以控制,如果内存泄漏,那么很难排查,且不适合储存很复杂的对象
对象
对象的访问定位
句柄 优势:reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改
对象实例数据(堆):对象中各个实例字段的数据
对象类型数据(方法区):对象的类型、父类、实现的接口、方法等
静态区(也在方法区中)用来存放静态变量,静态块
对象实例数据(堆):对象中各个实例字段的数据
对象类型数据(方法区):对象的类型、父类、实现的接口、方法等
静态区(也在方法区中)用来存放静态变量,静态块
直接指针 优势:速度快,它节省了一次指针定位的时间开销
对象的内存布局
对象头
markword
对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等
类型指针:指向自己是哪个类的实例
ps:并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身
数组长度(只有数组对象才有)
实例数据
指定的数据
对齐填充
jvm要求对象大小为8字节整数倍,对象头大小为8字节的1~2倍,只有实例数据可能没对齐,所以要填充补齐
new对象的过程
第一步:类加载检查
先检查要new的对象对应的类在运行时常量池(方法区里)里有没有符号引用,如果有就检查这个符号引用代表的类有没有被加载、解析或初始化过
ps:class文件常量池主要存放两大常量:字面量和符号引用,class常量池加载到内存中是运行时常量池
先检查要new的对象对应的类在运行时常量池(方法区里)里有没有符号引用,如果有就检查这个符号引用代表的类有没有被加载、解析或初始化过
ps:class文件常量池主要存放两大常量:字面量和符号引用,class常量池加载到内存中是运行时常量池
第二步:分配内存
为对象在堆内存分配一块空间,该空间大小在类加载完成后确定
分配空间根据堆是否规整有两种方法:1、指针碰撞 2、空闲列表
为对象在堆内存分配一块空间,该空间大小在类加载完成后确定
分配空间根据堆是否规整有两种方法:1、指针碰撞 2、空闲列表
①指针碰撞:前提要求是JAVA堆中的内存对象是绝对完整的,所有的内存都放在一边,空闲的放在另一边,中间放着一个指针作为分界点,分配时即把指针移动类大小即可
②空闲列表:这时的堆内存是相互交错的,虚拟机维护了一个列表记录了堆中哪里还有足够大的空闲内存可以使用,然后分配一块类需要的内存大小。
这两种方法和垃圾收集算法选取有关系,比如使用Serial\Parnew等带Compact过程的系统采用①,CMS基于Mark-Sweep(标记清除)的使用②
②空闲列表:这时的堆内存是相互交错的,虚拟机维护了一个列表记录了堆中哪里还有足够大的空闲内存可以使用,然后分配一块类需要的内存大小。
这两种方法和垃圾收集算法选取有关系,比如使用Serial\Parnew等带Compact过程的系统采用①,CMS基于Mark-Sweep(标记清除)的使用②
分配内存空间时可能存在线程安全问题
解决办法:
①对内存分配采取同步处理,实际上是虚拟机采用CAS算法失败重试的方式保证更新操作的原子性
②TLAB(本地线程缓冲),为每个线程在Java堆中预先分配一小块内存,每一个线程预先在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
解决办法:
①对内存分配采取同步处理,实际上是虚拟机采用CAS算法失败重试的方式保证更新操作的原子性
②TLAB(本地线程缓冲),为每个线程在Java堆中预先分配一小块内存,每一个线程预先在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
第三步:初始化零值(不包括对象头)
内存分配完后,虚拟机需要将分配到的内存空间中的数据都初始化为零值(不包括对象头)
(如果第二步使用TLAB,这一过程可能提前至TLAB分配时进行。(可以通过volatile关键字禁止指令重排,在单例模式双重校验锁中就是这个原理))
内存分配完后,虚拟机需要将分配到的内存空间中的数据都初始化为零值(不包括对象头)
(如果第二步使用TLAB,这一过程可能提前至TLAB分配时进行。(可以通过volatile关键字禁止指令重排,在单例模式双重校验锁中就是这个原理))
第四步,设置对象头
比如对象头的这个对象属于哪个类的实例,类指针,对象hashcode,锁信息以及GC分代年龄等。
比如对象头的这个对象属于哪个类的实例,类指针,对象hashcode,锁信息以及GC分代年龄等。
至此,从虚拟机视角来看,一个新的对象已经产生了。但是在Java程序视角来看,执行new操作后会接着执行如下步骤
第五步:调用对象的init<> ,根据传入的属性值给对象属性赋值,在线程的栈中新建对象引用 ,并指向堆中刚刚新建的对象实例
对象的init() : Java 在编译之后会在字节码文件中生成 init 方法,称之为实例构造器,该实例构造器会将语句块,变量初始化,调用父类的构造器等操作收敛到 init 方法中,收敛顺序为
问题
String s1 = new String("abc");这句话创建了几个字符串对象?
如果字符串常量池有abc则在堆上创建1次对象(ps:字符串常量池则存在于方法区)
如果没有,则字符串常量池创1次,堆创1次,还有1个引用对象
变式:
String str1 = new String("A"+"B")
String str2 = new String("ABC") + "ABC"
String str1 = new String("A"+"B")
String str2 = new String("ABC") + "ABC"
str1.intern() 过程
jdk6之前:查看str1的字面量在字符串常量池中有没有,有则直接返回在常量池的值,没有则新建再返回。
jdk7之后:查看str1的字面量在字符串常量池中有没有,有则直接返回在常量池的值,没有则将堆中引用添加到字符串常量池,返回堆中值。
对象的init方法和类的clinit方法区别
执行引擎
interpreter
java内存模型(JMM)定义了多线程和共享内存之间的并发模型
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证
(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,
尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)
(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,
尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)
happens-before原则
单线程的hb规则
在同一个线程中,书写在前面的操作happen-before后面的操作。
监视器锁的hb规则
同一个锁的unlock操作happen-before此锁的lock操作
volatile的hb规则
对一个volatile变量的写操作happen-before对此变量的任意操作(包括写操作)
hb的传递性
如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作
线程启动hb规则
同一个线程的start方法happen-before此线程的其它方法
线程中断hb规则
对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码
线程终结hb原则
线程中的所有操作都happen-before线程的终止检测
对象创建hb原则
一个对象的初始化完成先于他的finalize方法调用
重排序
可能导致线程安全问题,如DCL问题
《java并发编程实战》之java内存模型
8大happen-before原则超全面详解
volatile内存可见性和禁止指令重排序
将代码一行行解析,启动直接运行,速度慢,而且会有重复代码
解释器:输入的代码 -> [ 解释器 解释执行 ] -> 执行结果
JIT编译器:输入的代码 -> [ 编译器 编译 ] -> 编译后的代码 -> [ 执行 ] -> 执行结果
JIT执行编译后的代码比解释器快,但如果代码只执行一次(如只被调用一次
,例如类的构造器(class initializer或者没有循环的代码)则解释器比JIT编译器快
,例如类的构造器(class initializer或者没有循环的代码)则解释器比JIT编译器快
JIT(Just In Time) compiler
在解析器在解析代码时,当虚拟机发现某些代码运行比较频繁时,(也就是热点代码)。
JIT即时编译器就会把这些代码片段全部编译打包成可执行文件。开始执行时间会比较晚
JIT即时编译器就会把这些代码片段全部编译打包成可执行文件。开始执行时间会比较晚
检测热点代码
回边计数器--也就是计算循环体执行的次数
方法调用计数器
优化代码
逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除
逃逸分析
通俗来讲当对象的指针被多个方法或线程引用,称为逃逸分析
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
同步省略
JIT判断同步块里的锁对象如果只被一个线程调用,那么同步锁取消。
标量替换
JIT发现当一个对象不会被外界访问到,就会将对象分成若干个标量
(标量就是最小不可分割的单位)比如基本数据类型
(标量就是最小不可分割的单位)比如基本数据类型
栈上分配
由于JIT将对象进行标量替换后,对象变成基本类型,存储在栈帧里,
变量随着方法运行完而释放,减少了垃圾回收器的压力。
变量随着方法运行完而释放,减少了垃圾回收器的压力。
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:- DoEscapeAnalysis
从jdk1.7版本开始默认开启逃逸分析
关闭逃逸分析:-XX:- DoEscapeAnalysis
从jdk1.7版本开始默认开启逃逸分析
分类
HotSpot集成了两个JIT compiler — C1及C2(或称为Client及Server,C++实现)
目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。
目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。
C1(Client Complier)
运行在客户端,编译速度快
C2(Server Complier)
运行在服务端,编译质量好
Graal
GC
Eden:from:to = 8:1:1 -XX:survivorRatio
新生代:老年代 = 1:2 -XX:Ratio
新生代:老年代 = 1:2 -XX:Ratio
Minor GC触发条件:当Eden区满时,触发Minor GC
并从from(to)区的数据移到to(from)区,年龄+1,当年龄=15就会晋升到老年代
-XX:MaxTenuringThreshold=15
至于为什么是 15次,原因是 HotSpot会在对象头的中的标记字段里记录年龄,分配到的空间只有4位,所以最多只能记录到15
-XX:MaxTenuringThreshold=15
至于为什么是 15次,原因是 HotSpot会在对象头的中的标记字段里记录年龄,分配到的空间只有4位,所以最多只能记录到15
Full GC触发条件:
显式调用System.gc时,系统建议执行Full GC,但是不必然执行
老年代/方法区满
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
面试题:为什么s区要划分为from和to? 因为 避免碎片+使用标记复制算法
如果只有Eden和from,Eden满,发生minorGC
第一次minorGC之后,Eden为空,from有从Eden复制过来的存活对象
第二次minorGC之后,Eden又会向from复制转移,但此时from也有死亡的和存活需要复制的对象,那么from无法使用标记复制(无处可去)
如果有from和to区,那么Eden和from就可以复制到to,然后交换from和to,使to区永远为空。(to区满后转移到老年代)
如果只有Eden和from,Eden满,发生minorGC
第一次minorGC之后,Eden为空,from有从Eden复制过来的存活对象
第二次minorGC之后,Eden又会向from复制转移,但此时from也有死亡的和存活需要复制的对象,那么from无法使用标记复制(无处可去)
如果有from和to区,那么Eden和from就可以复制到to,然后交换from和to,使to区永远为空。(to区满后转移到老年代)
MGC、YGC、OGC、FGC等差别
对象动态年龄判断
如果将要放入s0区的对象大小超过s0区容量的50%,那么其中年龄最大的对象就会被转移到老年代(一般在minor gc后触发)
老年代空间分配担保机制
1.如果Eden区MinorGC后,s区也放不下,则通过 分配担保机制 把新生代的对象提前转移到老年代中去
2.大对象直接放入老年代,为了避免为大对象分配内存时由于 分配担保机制 带来的复制而降低效率。
2.大对象直接放入老年代,为了避免为大对象分配内存时由于 分配担保机制 带来的复制而降低效率。
分层编译模式
对象是否死亡
引用计数法 无法解决循环依赖问题
可达性分析
可作为gc roots的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
对象持有的四种引用
强引用 一定不收即使OOM
想中断或者回收强引用对象,可以显式地将引用赋值为null,变为软引用,这样的话JVM就会在合适的时间,进行垃圾回收
或者指向另一对象:obj= newObject();
或者指向另一对象:obj= newObject();
软引用 空间不够才收
使用较多,可以同时保证 垃圾回收速度 和 不OOM
可以和引用队列一起用
弱引用 扫描到就收
弱引用和软引用的区别在于:弱引用的对象拥有更短的生命周期,只要垃圾回收器扫描到它,不管内存空间充足与否,都会回收它的内存
虚引用 不影响对象生命周期,随时被收
如果一个对象没有强引用和软引用,对于垃圾回收器而言便是可以被清除的,在清除之前,会调用其finalize方法,如果一个对象已经被调用过finalize方法但是还没有被释放,它就变成了一个虚可达对象(如果一个对象与GC Roots之间仅存在虚引用,则称这个对象为虚可达对象)
虚引用通常与引用队列结合使用
一个对象将要被回收之前将虚引用放入队列,程序从队列中检测到这个对象将要回收之前可以做些事情
一个对象将要被回收之前将虚引用放入队列,程序从队列中检测到这个对象将要回收之前可以做些事情
作用
在于跟踪垃圾回收过程,在对象被收集器回收时收到一个系统通知。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。 所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收
引用队列
当引用的对象将要被JVM回收时,会将其加入到引用队列中。
应用:通过引用队列可以了解JVM垃圾回收情况
应用:通过引用队列可以了解JVM垃圾回收情况
常量池中的常量若没被引用便属于废弃常量
废弃类:需同时满足 实例被回收+ClassLoader被回收+类没被引用
垃圾回收算法
标记清除
原地清除垃圾,会产生碎片
标记复制
空间一分为二。先把有用的复制到另一边,再清空这边
标记整理
直接靠一端放,即使出现覆盖
标记整理耗时>=标记复制
虽然整理与复制都涉及移动对象,但取决于具体算法,整理可能要 计算对象目标地址,修正指针,移动对象;复制则可以把这几件事情合为一体来做,所以可以快一些
虽然整理与复制都涉及移动对象,但取决于具体算法,整理可能要 计算对象目标地址,修正指针,移动对象;复制则可以把这几件事情合为一体来做,所以可以快一些
分代收集
新生代存活率低用标记复制,只有少量会复制
老年代存活率高且都是大对象没有足够空间对它担保,用标记清除或标记整理
效率:复制算法>标记整理>标记清除
内存整齐度:复制=标记整理>标记清除
内存使用率:标记整理=标记清除>复制
内存整齐度:复制=标记整理>标记清除
内存使用率:标记整理=标记清除>复制
垃圾回收器
jvm垃圾收集图示链接
serial
年轻代 串行回收 标记复制 -XX:UseSerialGC (开启后会使用Serial + Serial Old的组合收集器)
serial old
老年代 串行回收 标记整理
parNew
年轻代 并行回收 标记复制 -XX:UseParNewGC(开启后会使用ParNew + Serial Old的组合收集器)
parllerl Scavenge
年轻代 并行回收 标记复制 (对应的JVM参数 -XX:UseParallelGC 或 -XX:UseParallelOldGC (可互相激活))
(开启后会使用Parallel + Parallel Old的组合收集器)
(开启后会使用Parallel + Parallel Old的组合收集器)
parllerl old
老年代 并行回收 标记整理
CMS
老年代 并发回收 标记清除
初始标记:标记GCRoot直接关联的对象
并发标记:从GCRoot关联的对象开始,往下遍历整棵树
重新标记:因为并发标记是并发的,会新增一些需标记的对象,所以重新标记
并发清理:清理未被标记的对象
并发标记:从GCRoot关联的对象开始,往下遍历整棵树
重新标记:因为并发标记是并发的,会新增一些需标记的对象,所以重新标记
并发清理:清理未被标记的对象
优点:最耗时的并发标记和并发清除是并发进行的,所以低停顿,低延时。
缺点
1.old gen使用标记清除算法,会造成内存碎片
2.并发标记和并发清理,会占用线程资源,降低吞吐量(吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间))
3.并发清除时候,其他线程产生的垃圾对象,无法被清理会造成浮动垃圾,只能下次GC清理
4.当触发FullGC时,CMS会尝试通过一次串行的完整垃圾收集来回收碎片化的堆内存,这个过程会持续很长时间,造成STW
G1(JDK9默认)
适用于多处理器和大容量内存服务器的垃圾收集器,满足 GC停顿时间要求 和 高吞吐量
并发+并行回收 分代收集 可预测停顿
局部看是标记复制,整体看是标记整理
为什么可以预测停顿时间
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)
这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
为何设置H区?
如果短期的大对象放入老年代,会影响效率
回收流程
youngGC
根扫描
扫描根和本地变量
更新Rset
处理dirty card队列,更新Rset
处理Rset
检测年轻代指向老年代的对象
复制对象
年轻代复制到survival/old区
处理引用
处理软引用、弱引用、虚引用
mixedGC
全局并发标记
初始化标记stw
标记GCRoot直接可达的对象,此过程会触发一个youngGC,
因为要跟新Rset引用
因为要跟新Rset引用
根区域扫描
扫描初始化标记的存活区域对老年区的引用,并标记被引用的对象
并发标记
扫描标记在整个堆中存活的对象
最终标记stw
标记并发标记时新产生的对象,修改SATB缓存
清理stw
G1如何解决并发标记时对象的引用发生改变?
如何解决存活的对象与快照不一样?
如何解决存活的对象与快照不一样?
使用写屏障,每次修改引用时都会将旧值写入long buffer。在最终标记时会修改快照
三色标记算法
Rset
card table
当一个线程修改过region里的引用,那么Rset里的引用也需要修改,
Rset里的cart table就是用byte记录修改过的值。
但有并发线程并发修改时,Rset也会并发修改,所以G1进一步将Rset设计成hashtable,
一个线程对应一个hashtable。
Rset里的cart table就是用byte记录修改过的值。
但有并发线程并发修改时,Rset也会并发修改,所以G1进一步将Rset设计成hashtable,
一个线程对应一个hashtable。
拷贝存活对象
优缺点
优点:
1.G1是一个有整理内存过程的收集器,不会产生很多碎片
2.stw可以控,G1在停顿时间上添加了预测机制,用户可以根据预期设置停顿时间
3.region可以不连续,所以对内存负担小
1.G1是一个有整理内存过程的收集器,不会产生很多碎片
2.stw可以控,G1在停顿时间上添加了预测机制,用户可以根据预期设置停顿时间
3.region可以不连续,所以对内存负担小
ZGC jdk11
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进
在 ZGC 中出现 Stop The World 的情况会更少
在 ZGC 中出现 Stop The World 的情况会更少
颜色指针
读屏障
多重映射
epsilon jdk11
JVM调优
1.监控GC的状态
2.生成堆的dump文件
3.分析dump文件
4.分析结果,判断是否需要优化
5.调整GC类型和内存分配
6.不断分析和调整
2.生成堆的dump文件
3.分析dump文件
4.分析结果,判断是否需要优化
5.调整GC类型和内存分配
6.不断分析和调整
每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
年老代的内存越来越大并且每次FullGC后年老代没有内存被释放
之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。
FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
年老代的内存越来越大并且每次FullGC后年老代没有内存被释放
之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。
为什么调优?除了为了解决已出现的问题,更重要的是为了减少full gc次数(即减少stw时间)
调优案例1
亿级流量,日活500万用户,付费转化率10%,日均50万单,大促的时候每秒1000多单,有3台订单系统集群,配置均为4核8G,每秒可处理300单
一个订单中有几十个字段int为4字节,long为8字节,假设每个订单对象大小为1kb,每秒有300kb对象生成
加上围绕订单还需要一些库存、优惠券、积分对象,再扩大20倍,加上订单查询、修改、退款,再扩大10倍
即每秒产生300kb*200=60MB对象,这些对象在生成后1秒就会变成垃圾,因为订单对象后面不再需要
按4核8G来算,一般是留4G给系统,4G给jvm,jvm其他部分1G,给堆3G,Eden:s0:s1=800M:100M:100M,老年代2G
因此在大概14s后,Eden区已满(全是垃圾),发生MinorGC后,最后这次没生成完的订单不会在这次gc中被清除,因为还有外部引用着,且不能停留在s0区,因为这里没生成完的对象大小超过了100M的50%(对象动态年龄判断)
于是被放入老年代,也就是说,每15s就有50+MB对象被送入老年代,且这些对象都会在1s后变为垃圾,老年代很快就满,FullGC次数猛增
一个订单中有几十个字段int为4字节,long为8字节,假设每个订单对象大小为1kb,每秒有300kb对象生成
加上围绕订单还需要一些库存、优惠券、积分对象,再扩大20倍,加上订单查询、修改、退款,再扩大10倍
即每秒产生300kb*200=60MB对象,这些对象在生成后1秒就会变成垃圾,因为订单对象后面不再需要
按4核8G来算,一般是留4G给系统,4G给jvm,jvm其他部分1G,给堆3G,Eden:s0:s1=800M:100M:100M,老年代2G
因此在大概14s后,Eden区已满(全是垃圾),发生MinorGC后,最后这次没生成完的订单不会在这次gc中被清除,因为还有外部引用着,且不能停留在s0区,因为这里没生成完的对象大小超过了100M的50%(对象动态年龄判断)
于是被放入老年代,也就是说,每15s就有50+MB对象被送入老年代,且这些对象都会在1s后变为垃圾,老年代很快就满,FullGC次数猛增
解决方案:调高新生代内存,使得大多数订单对象都可以在新生代自生自灭,不进入老年代
调优案例2
单机几十万请求。Eden:s0:s1=32G:4G:4G,老年代24G,为了应对初始对象过多,将新生代调大。
但是在这种情况下,MinorGC一次实际上比FullGC还慢,因为新生代太大需要很长时间标记
但是在这种情况下,MinorGC一次实际上比FullGC还慢,因为新生代太大需要很长时间标记
解决方案:使用G1收集器的-XX:MaxGCPauseMillis(目标最大暂停时间,默认200ms。每200ms就发生一次MinorGC,不会等到Eden区满),发生MinorGC时只回收Eden区较少的空间,比如一次只回收4G,解决一次stw时间过长的问题
调优案例3
某个高QPS的服务重启的时候load会非常高,CPU使用率过高,在一段时间后负载才降下来。启动后高负载的原因大致是由于启动,随着代码的执行,jvm的JIT编译器会将部分热点代码编译为目标机器代码,这时产生的编译线程会占用大量的cpu 导致系统负载高。JIT编译器需要代码执行一定的频率才会进行编译优化,系统刚启动的时候大部分的代码只是解释执行,解释执行的性能比编译执行的性能当然要差很多,所以系统的负载高,等到主要热点代码都是编译执行,系统负载就降下来。 那接下来如何对这个问题调优
解决方案:JIT分层编译模式
效果:线上环境一台机器加入分层编译参数-XX:+TieredCompilation之后,在大多数情况下启动之后负载都不会升高,有时候即使有会升高,也比默认的恢复快很多。
调优指令
jmap使用
jmap -F -dump:live,format=b,file=heap.bin
live 只dump存活的对象,如果不加则会dump所有对象
format=b 表示以二进制格式
file=filepath 输出到某个文件中
format=b 表示以二进制格式
file=filepath 输出到某个文件中
发生OOM的情况
除了程序计数器,都可能发生OOM
堆满
调大堆:-Xmx
永久代满
内存泄漏
GC 开销超过限制
由于某种原因,垃圾收集器占用了过多的时间(默认为进程所有CPU时间的98%),每次运行时恢复的内存非常少(默认为堆的2%)
这实际上意味着您的程序停止执行任何进度,并且一直忙于只运行垃圾收集
由于某种原因,垃圾收集器占用了过多的时间(默认为进程所有CPU时间的98%),每次运行时恢复的内存非常少(默认为堆的2%)
这实际上意味着您的程序停止执行任何进度,并且一直忙于只运行垃圾收集
使用 -XX:-UseGCOverheadLimit 禁用这个提示功能
数组过大
面试题:JVM的调优方式有哪些
1、将新对象预留在新生代
由于 Full GC 的成本要远远高于 Minor GC ,因此尽可能将对象分配在新生代,在JVM 调优中,可以为应用程序分配一个合理的新生代空间,以最大限度避免新对象直接进去老年代。
注意:由于新生代垃圾回收的速度高于老年代回收,因此,将年轻对象预留在新生代有利于提高整体的 GC 效率
注意:由于新生代垃圾回收的速度高于老年代回收,因此,将年轻对象预留在新生代有利于提高整体的 GC 效率
2、大对象进入老年代
大对象占用空间多,如果直接放入新生代中会导致新生代空间不足,这样将会把大量的较小的年轻代对象移入到老年代中
如果有短命大对象,原本存放于老年代的永久对象会被短命大对象塞满,扰乱了分代内存回收的基本思路
如果有短命大对象,原本存放于老年代的永久对象会被短命大对象塞满,扰乱了分代内存回收的基本思路
解决方法:-XX:PretenureSizeThreshold 设置大对象直接进入老年代的阀值,当对象超过这个阀值时,将直接在老年代中分配。
PS:-XX:PretenureSizeThreshold 只对串行收集器和新生代并行收集器有效,并行回收收集器不识别这个参数。
PS:-XX:PretenureSizeThreshold 只对串行收集器和新生代并行收集器有效,并行回收收集器不识别这个参数。
扩展:大对象
需要占用大量连续内存空间的java对象是大对象,比如很长的字符串和数组。
1.需要占用大量非连续空间的java对象不能称为大对象
2.一个对象有很多属性也不能成为大对象
1.需要占用大量非连续空间的java对象不能称为大对象
2.一个对象有很多属性也不能成为大对象
3、设置对象进入老年代的年龄
因为对象满15岁后将被移到老年代,可以通过XX:MaxTenuringThreshold:默认值是15,这个参数是指定进入老年代的最大年龄值,对象实际进入老年代的年龄是 JVM 在运行时根据内存使用情况动态计算的。如果希望对象尽可能长地留在新生代中,可以设置一个较大的阀值。
4、稳定与震荡的堆大小
稳定的堆大小对垃圾回收是有利的,获得一个稳定堆大小的方法就是设置 -Xmx 和 -Xms 一样的值。不稳定的堆也不是木有用处,让堆大小在一个区间内震荡,在系统不需要使用大内存时压缩堆空间,使 GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这种思想,JVM 提供了两个参数用于压缩和扩展堆空间
5、吞吐量优先设置
6、使用大页案例
使用大的内存分页可以增强 CPU 的内存寻址能力,从而提高系统的性能
7、降低停顿案例
为了降低应用软件在垃圾回收时的停顿,首先考虑的使用关注系统停顿的 CMS 回收器,为了减少 Full GC 的次数,应尽可能将对象预留在新生代,新生代 Minor GC 的成本远远小于老年代的 Full GC
8、分层编译模式
多线程和并发
并发的三大特性
原子性
在一操作或多个操作中要么全部执行成功,否则全部失败
可见性
一个线程修改了共享变量数据,其它线程能知道
有序性
按代码的循序执行
指令重排:jvm为了程序的效率会将代码循序重排,单线程不影响结果,多线程有影响。避免使用Executors 工具类创建
15种锁
乐观锁/悲观锁
乐观锁
认为别的线程不会去修改值,不加锁。如果发现值被修改了,可以再次重试。乐观锁可以使用版本号机制和CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。
悲观锁
认为别的线程会修改值,会加锁。在 Java 语言中 synchronized 和 ReentrantLock等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable 等也是悲观锁的应用。
比较
乐观锁适合读多写少的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量。
写多读少的场景,即冲突比较严重,线程间竞争激励,使用乐观锁就是导致线程不断进行重试,这样可能还降低了性能,这种场景下使用悲观锁就比较合适
独占锁/共享锁
独占锁
锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。JDK中的synchronized和java.util.concurrent(JUC)包中Lock的实现类就是独占锁。
共享锁
锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。
互斥锁/读写锁
互斥锁
互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。
读写锁
是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。读写和写写,写读都是互斥的
公平锁/非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。在 java 中可以通过构造函数初始化公平锁Lock lock = new ReentrantLock(true);
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。
可重入锁/不可重入锁
可重入锁
可重入锁又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。
分段锁
是一种锁的设计,并不是具体的一种锁。
无锁/偏向锁/轻量锁/重量锁
synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。
自旋锁
概念
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待
,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
实现
CAS
优缺点
优点
1、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;
不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时
候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态
,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态
,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
缺点
1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高
2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题
图解java中的18把锁
Synchronized
介绍
synchronized是一种同步锁,用于修饰类、实例方法、静态方法和代码块,保证原子性、可见性、有序性和可重入性
同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入
加锁原理
synchronized有两种形式上锁:对 方法 和 代码块 上锁
同步方法和同步代码块都是在进入同步代码之前先获取锁,拿到锁则计数器+1,执行完-1
如果获取失败就阻塞式等待锁的释放。
同步方法和同步代码块都是在进入同步代码之前先获取锁,拿到锁则计数器+1,执行完-1
如果获取失败就阻塞式等待锁的释放。
识别方式
对方法上锁:在方法的flags里面多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放
对代码块上锁:同步块是由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1
底层实现(内存空间操作)
synchronized对java对象加锁实际上是对对象头里的markword操作,
Mark Word里默认存储对象的HashCode,分代年龄和锁标记位,运
行期间Mark Word里存储的数据和锁的状态会随着锁标志位的变化而变化
Mark Word里默认存储对象的HashCode,分代年龄和锁标记位,运
行期间Mark Word里存储的数据和锁的状态会随着锁标志位的变化而变化
扩展:对象头
Mark Word 存储对象的HashCode,分代年龄和锁标志位信息
Klass Point 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
Monitor
EntryList
Owner 会指向持有 Monitor 对象的线程
WaitSet
JVM对Synchronized的优化
锁膨胀
锁状态
无锁 001
markword内 锁状态标记 001
偏向锁 101
减少同一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁
轻量级锁 00
一个对象持有偏向锁时,又有另外线程来获取锁的时候,就会将101改为00,并且把hashcode记录在栈帧里的lock record中。对象头就记录着lock record的地址和状态。
当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块
当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块
重量级锁 10
当该对象持有轻量级锁的时候,有另一线程thread-1来获取锁时,获取失败自旋几次后还失败的话就会进入锁膨胀,向系统申请一个monitor锁,对象头改为记录着monitor地址,状态从00改为10.(但此时还是thread-0线程获取着锁).所以升级后thread-1就会进入entryList阻塞。
当thread-0解锁时就要用monitor的解锁方式了,就要根据monitor锁的地址找到monitor,然后将owner设为null,然后唤醒EntryList中阻塞的线程。
当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景
当thread-0解锁时就要用monitor的解锁方式了,就要根据monitor锁的地址找到monitor,然后将owner设为null,然后唤醒EntryList中阻塞的线程。
当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景
锁的膨胀方向
无锁->偏向锁->轻量级锁->重量级锁(方向不可逆)
锁消除
这种优化得更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。
(例如while循环内执行100次append,没有锁粗化的就要进行100次加锁/解锁,如果加在
while循环体外,则只加锁一次即可)
(例如while循环内执行100次append,没有锁粗化的就要进行100次加锁/解锁,如果加在
while循环体外,则只加锁一次即可)
自旋锁和自适应自旋锁
获取轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
自旋锁
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
Lock
Reentranlock
公平锁类型与非公平锁类型
ReentrantLock可以有公平锁和非公平锁的不同实现,只要在构造它的时候传入不同的布尔值
NonfairSync
非公平模式加锁流程
从调用lock()方法开始-----通过compareAndSetState(0,1)首先尝试快速获取锁,以cas的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了。若获取锁失败,则执行acquire方法
acquire()三个最重要的方法
tryAcquire
尝试获取锁的通用方法:tryAcquire()
该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不同实现
该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不同实现
return nonfairTryAcquire
用cas抢占锁
如果锁计数器为0,则直接cas上锁
若锁是自己这个线程拿的,那么计数器+1,并执行setState方法
addWaiter
获取锁失败的线程如何安全的加入同步队列:addWaiter()
面试题:addWaiter()为什么要从尾部遍历?
新节点pre指向tail,tail指向新节点,这里后继指向前驱的指针是由CAS操作保证线程安全的。
而cas操作之后t.next=node之前,可能会有其他线程进来。
所以出现了问题,从尾部向前遍历是一定能遍历到所有的节点
新节点pre指向tail,tail指向新节点,这里后继指向前驱的指针是由CAS操作保证线程安全的。
而cas操作之后t.next=node之前,可能会有其他线程进来。
所以出现了问题,从尾部向前遍历是一定能遍历到所有的节点
acquireQueued
线程加入同步队列后会做什么:acquireQueued()
非公平模式解锁流程
调用unlock()方法,unlock方法调用了release()方法
release()方法调用tryrelease()方法释放锁,并调用unparkSuccessor(h)唤醒同步队列里阻塞的线程
h!=null是为了防止队列为空,即没有任何线程处于等待队列中,那么也就不需要进行唤醒的操作
h.waitStatus != 0是为了防止队列中虽有线程,但该线程还未阻塞,由前面的分析知,线程在阻塞自己前必须设置前驱结点的状态为SIGNAL,否则它不会阻塞自己。
h.waitStatus != 0是为了防止队列中虽有线程,但该线程还未阻塞,由前面的分析知,线程在阻塞自己前必须设置前驱结点的状态为SIGNAL,否则它不会阻塞自己。
tryrelease()方法
将线程持有锁的次数减1,即 将state值减1,若减少后线程将完全释放锁(state值为0),则该方法将返回true,否则返回false。
将线程持有锁的次数减1,即 将state值减1,若减少后线程将完全释放锁(state值为0),则该方法将返回true,否则返回false。
unparkSuccessor(h)
CAS
FairSync
hasQueuedPredecessors(公平比非公平只多了这一个方法)
如果是当前持有锁的线程 可重入
可重入性
ReentrantLock的state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。
释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的
释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的
AbstractQueuedSynchronizer(AQS)
state(volatile修饰的int类型)
getState()
setState()
compareAndSetState()
CLH双向队列(多线程争用资源被阻塞时会进入此队列)
结点状态waitStatus
负值表示结点处于有效等待状态,而正值表示结点已被取消。
所以源码中很多地方用>0、<0来判断结点的状态是否正常
所以源码中很多地方用>0、<0来判断结点的状态是否正常
CANCELLED(1) 表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化
SIGNAL(-1) 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL
CONDITION(-2) 表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE(-3) 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
0 新结点入队时的默认状态
入队 出队
头结点设计
共享和独享的实现
CAS
原理
unsafe.
实际应用
存在的问题
cpu开销
只能保证一个共享变量原子操作
AtomicReference
ABA
标志位 时间戳
解决:使用版本号
如AtomicStampedReference类
最后cas两个pair对象
如AtomicStampedReference类
最后cas两个pair对象
原子类底层使用了cas,但如果不成功会一直循环
ReentrantReadWriteLock
ThreadLocal
简介
ThreadLocal意为线程变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
使用场景
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
结构
继承结构
源码
ThreadLocalMap(关键静态内部类)
code
Entry
code
待补充
SuppliedThreadLocal(继承了ThreadLocal)
原理
ThreadLocal什么使用弱引用
内存泄漏问题
线程
线程状态
就绪
运行
阻塞
等待
超时等待
终止
如何创建一个线程?
继承Thread类来创建并启动多线程
1、先定义一个类继承Thread类并重写run()方法(run()方法是线程的执行体)
2、创建Thread子类的实例,也就是创建了线程对象
3、启动线程,即调用线程的start()方法
1、先定义一个类继承Thread类并重写run()方法(run()方法是线程的执行体)
2、创建Thread子类的实例,也就是创建了线程对象
3、启动线程,即调用线程的start()方法
实现Runnable接口创建并启动线程
1、定义Runnable接口的实现类并重写run()方法(run()方法是线程的执行体)
2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建thread对象,这个thread对象才是真正的线程对象
1、定义Runnable接口的实现类并重写run()方法(run()方法是线程的执行体)
2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建thread对象,这个thread对象才是真正的线程对象
使用Callable和Future创建线程
1、创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3、使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
1、创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3、使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
使用线程池例如用Executor框架
Thread&Runnable&Callable区别
线程如果只是实现Runnable或实现Callable接口,还可以继承其他类
如果继承Thread类则无法继承其他类(java单继承)
Run/Call 多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法
如果继承Thread类则无法继承其他类(java单继承)
Run/Call 多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法
线程池
基础
优点
降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
提高响应速度:任务到达时,无需等待线程创建即可立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
提高响应速度:任务到达时,无需等待线程创建即可立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
四种类型
newCachedThreadPool()
执行最快,核心线程数为0,最大线程数为Integer.MAX_VALUE,使用同步队列SynchronousQueue,队列中一旦有元素,就会调入非核心线程区执行
如果执行100个任务,每个任务执行时间短,就会产生线程复用,提升速度,如果执行时间过长会在非核心线程区不断创建新线程
如果执行100个任务,每个任务执行时间短,就会产生线程复用,提升速度,如果执行时间过长会在非核心线程区不断创建新线程
不推荐使用:cpu100%+OOM
newFixedThreadPool()
执行慢,核心线程数用户自定且固定,最大线程数也等于这个值,使用LinkedBlockQueue
当核心线程数太少,则任务会一组一组的执行
当核心线程数太少,则任务会一组一组的执行
不推荐使用:OOM
newSingleThreadPool()
执行最慢,FixedThreadPool的单一版本
不推荐使用:OOM
newScheduleThreadPool()
增加功能的线程池,可以用做定时任务
不推荐使用:这个在实际项目中基本不会被用到,因为有其他方案选择比如quartz
关键字对比
Runnable vs Callable
Runnable 接口不会返回结果或抛出检查异常,Callable会
execute() vs submit()
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功
并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
shutdown() vs shutdownNow()
shutdown() 关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
shutdownNow() 关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
isTerminated() vs isShutdown()
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
isShutDown 当调用 shutdown() 方法后返回为 true。
线程池运行状态(生命周期)
运行状态由内部维护,不由用户显性设置
runState(运行状态)
workCount(线程数量)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//RUNNING代表运行状态参数,0代表workCount(有效线程数)数量
//RUNNING代表运行状态参数,0代表workCount(有效线程数)数量
1、running
能接受新提交的任务,也能处理阻塞队列里的任务
2、shutdonw
不能接受新提交的任务,但能处理阻塞队列里的人物
3、stop
不能接受新提交的任务,也不能处理阻塞队列里的任务,还会中断正在执行任务的线程
4、tidying
如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态
5、terminated
在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做
状态转换
线程池参数
1、corePoolSize 线程池核心线程大小
线程池中会维护一个核心线程数量(线程池的基本大小),corePoolSize即为指定核心线程数量大小,核心线程会一直存活,即使没有任务需要执行,并且只有在工作队列满了的情况下才会创建超出这个数量的线程,除非设置了allowCoreThreadTimeOut,设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。
2、maximumPoolSize 线程池最大线程数量
先判断是否超Corepoolsize,超了再判断工作队列满没满,满了最后判断maximumPoolSize超了没,超了就走拒绝策略
3、keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
4、unit 空闲线程存活时间单位
keepAliveTime的计量单位
5、workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列
ArrayBlockingQueue
基于数组的有界阻塞队列,可以防止资源耗尽问题
LinkedBlockingQuene
基于链表的无界阻塞队列
SynchronousQuene
不缓存任务的阻塞队列
PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
6、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程(守护线程)等等
7、handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,则需要拒绝策略处理。
jdk中提供了4种拒绝策略(又称饱和策略)
jdk中提供了4种拒绝策略(又称饱和策略)
Abort Policy(默认)
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常(拒绝执行异常)
CallerRuns Policy
任务被拒绝添加后,会调用当前线程池的所在的线程直接去执行被拒绝的任务的run()方法
如果线程池已经shutdown,则直接抛弃任务
缺点:可能导致主线程阻塞
如果线程池已经shutdown,则直接抛弃任务
缺点:可能导致主线程阻塞
Discard Policy
该策略下,直接丢弃任务,什么都不做
DiscardOldest Policy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
实际应用
1、核心线程数该怎么设置合适
1、获取CPU核心线程数
CPU核数=Runtime.getRuntime().availableProcessors()
CPU核数=Runtime.getRuntime().availableProcessors()
ps:Run.get.avai..()返回的是可用的计算资源(线程数),而不是CPU物理核心数
超线程的CPU来说,单个物理处理器相当于拥有两个逻辑处理器,能够同时执行两个线程。
例如物理计算机有4个处理器核心,返回值则为4x2=8
超线程的CPU来说,单个物理处理器相当于拥有两个逻辑处理器,能够同时执行两个线程。
例如物理计算机有4个处理器核心,返回值则为4x2=8
2、分析线程池处理的程序类型是CPU密集型,还是IO密集型
ps:IO密集型(某大厂实践经验)
核心线程数 = CPU核数 / (1-阻塞系数) 例如阻塞系数 0.8,CPU核数为4
则核心线程数为20
核心线程数 = CPU核数 / (1-阻塞系数) 例如阻塞系数 0.8,CPU核数为4
则核心线程数为20
ps:io密集型 2* cpu核数只是经验值,实际要找出最优线程数就需要进行压测,不同环境不同机器表现也不同
ps:最大线程数一般是核心的2倍左右
2、核心线程数和最大线程数区别
corePoolSize:核心线程数;maximunPoolSize:最大线程数
每当有新的任务到线程池时,
第一步: 先判断线程池中当前线程数量是否达到了corePoolSize,若未达到,则新建线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理,若当前线程数量已达到corePoolSize,则进入下一步;
第二步: 判断工作队列(workQueue)是否已满,未满则将新的任务提交到工作队列中,满了则进入下一步;
第三步: 判断线程池中的线程数量是否达到了maxumunPoolSize,如果未达到,则新建一个工作线程来执行这个任务,如果达到了则使用饱和策略来处理这个任务。注意: 在线程池中的线程数量超过corePoolSize时,每当有线程的空闲时间超过了keepAliveTime,这个线程就会被终止。直到线程池中线程的数量不大于corePoolSize为止。
(由第三步可知,在一般情况下,Java线程池中会长期保持corePoolSize个线程。)
每当有新的任务到线程池时,
第一步: 先判断线程池中当前线程数量是否达到了corePoolSize,若未达到,则新建线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理,若当前线程数量已达到corePoolSize,则进入下一步;
第二步: 判断工作队列(workQueue)是否已满,未满则将新的任务提交到工作队列中,满了则进入下一步;
第三步: 判断线程池中的线程数量是否达到了maxumunPoolSize,如果未达到,则新建一个工作线程来执行这个任务,如果达到了则使用饱和策略来处理这个任务。注意: 在线程池中的线程数量超过corePoolSize时,每当有线程的空闲时间超过了keepAliveTime,这个线程就会被终止。直到线程池中线程的数量不大于corePoolSize为止。
(由第三步可知,在一般情况下,Java线程池中会长期保持corePoolSize个线程。)
任务调度
存放优先级:corePollSize -> queue -> maxPoolSize
执行优先级:corePollSize -> maxPoolSize -> queue
放不下时:拒绝策略
如果线程达到maximumPoolSize任有新任务,
就会执行拒绝策略。
如果线程达到maximumPoolSize任有新任务,
就会执行拒绝策略。
abortPolicy:抛出RejectedExecutionException
callerRunsPolicy:如果线程没关闭,让调用者执行此任务
DiscardPolicy:放弃本次任务
DiscardOldestPolicy:丢弃阻塞队列中最老的任务,并将新任务加入
业务场景
1.快速响应用户请求(响应速度>吞吐量)
比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户
从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了
这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不让队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务
PS:使用同步队列:SynchronousQueue而不是缓冲队列,如:有界阻塞队列ArrayBlockingQueue、无界阻塞队列:LinkedBlockingQueue
比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户
从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了
这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不让队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务
PS:使用同步队列:SynchronousQueue而不是缓冲队列,如:有界阻塞队列ArrayBlockingQueue、无界阻塞队列:LinkedBlockingQueue
2.快速处理批量任务(吞吐量>响应速度)
比如说统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析
这种场景需要执行大量的任务,我们也会希望任务执行的越快越好,但这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题
所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量
比如说统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析
这种场景需要执行大量的任务,我们也会希望任务执行的越快越好,但这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题
所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量
美团故障场景
CPU密集型任务还是IO密集型任务?
如何判断?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序
但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序
但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上
如何根据类型决定线程数大小?
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
事故1:
事故描述:2018年XX页面展示接口产生大量调用降级,数量级在几十到上百。
事故原因:该服务接口内部逻辑使用线程池做并行计算,由于没有预估好调用的流量,导致最大核心数设置偏小,大量抛出RejectedExecutionException
事故描述:2018年XX页面展示接口产生大量调用降级,数量级在几十到上百。
事故原因:该服务接口内部逻辑使用线程池做并行计算,由于没有预估好调用的流量,导致最大核心数设置偏小,大量抛出RejectedExecutionException
事故2:
事故描述:2018年XX业务提供的服务执行时间过长,作为上游服务整体超时,大量下游服务调用失败。
事故原因:该服务处理请求内部逻辑使用线程池做资源隔离,由于队列设置过长,最大线程数设置失效,导致请求数量增加时,大量任务堆积在队列中,任务执行时间过长,最终导致下游服务的大量调用超时失败
事故描述:2018年XX业务提供的服务执行时间过长,作为上游服务整体超时,大量下游服务调用失败。
事故原因:该服务处理请求内部逻辑使用线程池做资源隔离,由于队列设置过长,最大线程数设置失效,导致请求数量增加时,大量任务堆积在队列中,任务执行时间过长,最终导致下游服务的大量调用超时失败
故障后的解决方案
能否不用线程池?不行,因为目前其他技术在java领域不够成熟
有没有通用计算公式设置参数?没有,cpu密集型和io密集型执行结果差距较大
动态配置线程池参数?可以
简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue
参数可动态修改
增加线程池监控
线程池最佳实践
使用 ThreadPoolExecutor 的构造函数声明线程池
监测线程池运行状态。比如 SpringBoot 中的 Actuator 组件
除此之外,还可以用ThreadPoolExecutor提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等
除此之外,还可以用ThreadPoolExecutor提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等
建议不同类别的业务用不同的线程池
事故3:
假如我们线程池的核心线程数为 n,父任务数量为 n,父任务下面有两个子任务,其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 "死锁"。
假如我们线程池的核心线程数为 n,父任务数量为 n,父任务下面有两个子任务,其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 "死锁"。
给线程池命名。默认情况下创建的线程名字类似 pool-1-thread-n 这样的,没有业务含义,不利于我们定位问题
正确配置线程池参数(最难)
volatile
轻量级同步机制,具有可见性,有序性。
每次使用被volatile修饰的变量都从主存里获取
每次使用被volatile修饰的变量都从主存里获取
底层原理:内存屏障
读屏障
在加了volatile的变量之后的变量都从主存里读取,
并且之后的代码不会重排序到volatile前面
并且之后的代码不会重排序到volatile前面
写屏障
加了volatile的变量之前的赋值操作都会同步到主存中,
并且之前的代码不会重排序到volatile后面
并且之前的代码不会重排序到volatile后面
为什么没有原子性?
因为volatile是轻量级机制,而原子操作是一整个操作完成或失败,
若遇到阻塞就会等很久。
若遇到阻塞就会等很久。
MESI协议
使用场景
1.状态标记量
2.double check
CAS机制
原理
图示
CAS可能的问题
ABA问题
自旋CAS循环时间长开销大
只能保证一个共享变量的原子操作
JDK中对CAS的支持 — Unsafe类
atomic: JDK中的相关原子操作类
AtomicInteger
AtomicIntegerArray
AtomicReference
面试题
线程
1、线程的创建
如何创建一个线程?
继承Thread类来创建并启动多线程
1、先定义一个类继承Thread类并重写run()方法(run()方法是线程的执行体)
2、创建Thread子类的实例,也就是创建了线程对象
3、启动线程,即调用线程的start()方法
1、先定义一个类继承Thread类并重写run()方法(run()方法是线程的执行体)
2、创建Thread子类的实例,也就是创建了线程对象
3、启动线程,即调用线程的start()方法
实现Runnable接口创建并启动线程
1、定义Runnable接口的实现类并重写run()方法(run()方法是线程的执行体)
2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建thread对象,这个thread对象才是真正的线程对象
1、定义Runnable接口的实现类并重写run()方法(run()方法是线程的执行体)
2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建thread对象,这个thread对象才是真正的线程对象
使用Callable和Future创建线程
1、创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3、使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
1、创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3、使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
使用线程池例如用Executor框架
2、线程的启动
为什么用start()方法而不用run()方法
首先通过对象.run()方法可以执行方法,但是不是使用的多线程的方式,就是一个普通的方法,要想实现多线程的方式,一定需要通过对象.start()方法。
调用start()里面的start0()方法该线程并不一定会立马执行,只是将线程变成了可运行状态(NEW ---> RUNNABLE)。具体什么时候执行,取决于 CPU ,由 CPU 统一调度
调用start()里面的start0()方法该线程并不一定会立马执行,只是将线程变成了可运行状态(NEW ---> RUNNABLE)。具体什么时候执行,取决于 CPU ,由 CPU 统一调度
一个线程两次调用start()方法会出现什么情况?
Java的线程是不允许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常,多次调用start被认为是编程错误。
3、线程的方法
join:t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程
4、线程间通信
共享内存
消息传递
5、线程间同步
synchronized+wait/notify
cas
volatile
信号量
reentrantlock
ThreadLocal
6、线程返回值
volatile和synchronized的区别
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
ReentrantLock和Synchronized的区别和原理
ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
ReentrantLock 可以实现公平锁
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,
因此使用 Lock 时需要在 finally 块中释放锁。
而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,
因此使用 Lock 时需要在 finally 块中释放锁。
wait()和sleep()区别
1.所属类不同
Object.wait()和Thread.sleep()
2.对待锁的方式不同
wait:释放执行权,释放锁 (指定时间 使得其他线程可以使用同步控制块或者方法)
sleep:释放执行权,不释放锁(相当于等待一定时间然后继续向下执行)
sleep:释放执行权,不释放锁(相当于等待一定时间然后继续向下执行)
3.使用场景不同
wait() 通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执⾏。
4.阻塞和等待唤醒方式不同
wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或 者 notifyAll() ⽅法。 sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(long
timeout) 超时后线程会⾃动苏醒。
timeout) 超时后线程会⾃动苏醒。
yield() 和sleep() 区别
sleep(int time)方法,会是线程从Running状态进入阻塞状态,不释放锁,带sleep时间结束,线程从阻塞状态进入Runable状态,等待操作系统分配时间片
yield()方法,会主动让出执行,不释放锁,使线程从Running状态进入Runable状态,等待操作系统分配时间片
区别
sleep()让出cpu时间,让其他线程执行,不区分线程优先级;yield让出时间,给相同优先级的线程执行
sleep()方法申明抛出InterruptedException异常,而yield方法则没有申明抛出任何异常
并发&并行&同步&异步
并发是一种机制,指在多个请求同时发起
支持并发意思就是包容同时来到的多个请求,或说包容同时存在的多个线程
支持并发意思就是包容同时来到的多个请求,或说包容同时存在的多个线程
处理这多个请求的方式可以是并行的,这样就要求硬件需要有多个计算核心
也可以是只有一个计算资源,处理完一个请求再处理第二个 / 在不同请求间不停切换运行,
意味着同时可以有许多线程存在,但每一时刻正在跑的线程只有一个
意味着同时可以有许多线程存在,但每一时刻正在跑的线程只有一个
并行:并行是一种处理方式(或可以说是一种架构方式),
支持并行即指“有多个计算核心,可以在同一时间同时处理多个任务 / 把一个任务分割成小块,在同一时间同时处理多个部分
支持并行即指“有多个计算核心,可以在同一时间同时处理多个任务 / 把一个任务分割成小块,在同一时间同时处理多个部分
同步:同步是一种要求,即指“在支持并发的情况下,不同请求之间(线程之间)不能发生数据冲突或资源抢占冲突”
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为
异步:调用者无需等待第一个方法返回,就可以开始其他的任务
异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
而异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作
异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
而异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作
同步&异步:注重的是结果,同步就是一直等待。异步:时不时地去看有没有结果
线程的几种状态
MySQL
数据库三大范式
第一范式:每个字段都是原子级别的,不可再分,第一范式就是无重复的列
第二范式:建立在第一范式基础上,非主键属性依赖主键
也就是说属性完全依赖主键,主键能唯一表示这些属性
第三范式:建立在第二范式基础上,非主键只能依赖主键,不能依赖非主键
就是不能有传递关系
比如student(学号,名字,校区,校区地址,校区电话)
这时 校区地址和电话就可以依赖校区了 也是存在传递关系
比如student(学号,名字,校区,校区地址,校区电话)
这时 校区地址和电话就可以依赖校区了 也是存在传递关系
依赖
如果通过A属性(属性组)(主键),可以确定唯一B属性,那么B依赖A
完全依赖
B属性需要完全依赖A属性组(主键)才能确定唯一,
比如语文成绩需要完全依赖学号和科目,不可以只
通过学号或者科目获得
比如语文成绩需要完全依赖学号和科目,不可以只
通过学号或者科目获得
部分依赖
B属性需要部分依赖A属性组(主键)才能确定唯一,
比如姓名需要依赖属性组(学号,班级)其中的学号就能行
比如姓名需要依赖属性组(学号,班级)其中的学号就能行
传递依赖
A确定B,B确定C,则C传递依赖A
超键
能唯一表示一条数据的属性或属性集,
比如(学号),(学号,性别)
比如(学号),(学号,性别)
候选键
能唯一表示一条数据的属性或属性集,但没有多余的属性,
如(学号,课程)可以查出成绩
如(学号,课程)可以查出成绩
主键
能唯一表示一条数据的属性
如(学号),(身份证)
如(学号),(身份证)
主属性
候选码的并集
非主属性
非候选码
数据库如何设计的?
先需求分析,再画E-R模型,然后根据模型设计数据库,最后就是写代码、测试和部署
mysql整体架构
server层
连接器(建立管理连接,验证权限)
负责和客户端建立连接,获取并验证用户权限以及维持和管理连接
(在用户建立连接后,即使管理员改变连接用户的权限,也不会影响到已连接的用户)
(在用户建立连接后,即使管理员改变连接用户的权限,也不会影响到已连接的用户)
连接为长连接,连接时长由wait_timeout控制,默认值8小时
使用show processlist 可以查看所有连接的信息
基于TCP协议
查询缓存(查询到缓存直接返回结果)
因为实际缓存命中率低,所以被mysql8后被废弃
但在实际情况下,查询缓存一般没有必要设置。因为在查询涉及到的表被更新时,缓存就会被清空。所以适用于静态表
即因为实际缓存命中率低,所以被废除
即因为实际缓存命中率低,所以被废除
分析器(语法、词法分析)
词法分析
扫描字符流,根据构词规则识别单个单词,如 select,表名,列名,判断其是否存在等。
语法分析
判断语句是否符合 MySQL 语法,在词法分析的基础上将单词序列组成语法短语,最后生成语法树,提交给优化器,
优化器(选择最优的执行方案)
基于开销的优化器,以确定例如:索引的使用,join 表的连接顺序以及处理查询等
的最优解方式,也就是说执行查询之前,都会先选择一条自以为最优的方案,然后执行这个方案来获取结果
的最优解方式,也就是说执行查询之前,都会先选择一条自以为最优的方案,然后执行这个方案来获取结果
执行器(操作存储引擎,返回结果)
在具体执行语句前,会先进行权限的检查,通过后使用数据引擎提供的接口,进行查询。如果设置了慢查询,会在对应日志中看到 rows_examined 来表示扫描的行数。在一些场景下(索引),执行器调用一次,但在数据引擎中扫描了多行,所以引擎扫描的行数和 rows_examined 并不完全相同。
存储引擎层
Innodb
内存管理
待补充
buffer pool
特性
插入缓存
使用条件
非聚簇索引
非唯一索引
定义:只对于非聚集索引(非唯一)的插入、删除和更新有效,对于每一次的插入不是写到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,
如果在则直接插入,若不在,则先放到Insert Buffer 中,再按照一定的频率进行合并操作,再写回disk。这样通常能将多个插入合并到一个操作中,目的还是为了减少随机IO带来性能损耗。
如果在则直接插入,若不在,则先放到Insert Buffer 中,再按照一定的频率进行合并操作,再写回disk。这样通常能将多个插入合并到一个操作中,目的还是为了减少随机IO带来性能损耗。
Insert Buffer
只对insert有效
Change Buffer
对insert、delete、update(实际参数叫change)、purge有效
innodb_change_buffering参数
all
默认,缓存全部
none
不缓存
inserts
缓存insert
deletes
缓存delete
changes
缓存delete+insert操作(delete+insert实现update操作)
purges
缓存后台执行的物理删除操作
二次写
自适应哈希
自动监控并为某被频繁访问的二级缓存设置哈希索引
预读
线性预读
将下一个extent提前读取到buffer pool中
随机预读(5.5废弃,但可以启用)
将下一个extent剩余的page提前读取到buffer pool中
innodb索引的组织方式
InnoDB 存储引擎以页(默认为16KB)为基本单位存储
InnoDB和MyISAM对比
InnoDB支持行级锁,外键,事务,崩溃后的安全恢复,MVCC。
MyISAM强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快
日志系统
redo log(重做日志)
保证持久性
记录事务对数据库做了哪些修改
刷盘时机
应用场景
崩溃恢复
确定恢复的起点
确定恢复的终点
怎样恢复
undo log(回滚日志)
保证原子性
应用场景
事务回滚
事务id(唯一、递增)
生成时机
在事务中对表中的记录做改动时
生成策略
服务器在内存中维护一个全局变量,且事务id分配之后自动+1
每当变量值为256的倍数时,会将变量值刷新到系统表空间页号为5的页面中
日志格式
INSERT操作对应的undo日志
DELETE操作对应的undo日志
UPDATE操作对应的undo日志
bin log
待补充
binlog 则是 Server 层的日志,主要用于归档,在备份,主备同步,恢复数据时发挥作用,常见的日志格式有 row, mixed, statement 三种
分库分表
水平分库
水平分表
垂直分库
垂直分表
将一个表按照字段分成多表,每个表存储其中一部分字段。
它带来的提升是:
1.为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响
2.充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累
它带来的提升是:
1.为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响
2.充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累
主从复制
简介
主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库;主数据库一般是准实时的业务数据库
作用
1、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,物理服务器增加,能承载的负荷增加
2、读写分离,使数据库能支撑更大的并发。主从只负责各自的写和读,极大程度的缓解X锁和S锁争用。在报表中尤其重要。由于部分报表sql语句非常的慢,导致锁表,影响前台服务。如果前台使用master,报表使用slave,那么报表sql将不会造成前台锁,保证了前台速度
S锁(共享锁 shared lock)
读锁是共享的,或者说是相互不阻塞的
又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。
这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
读锁是共享的,或者说是相互不阻塞的
又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。
这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
X锁(排他锁 exclusive lock)
写锁是排他的,一个写锁会阻塞其他的写锁和读锁
又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
写锁是排他的,一个写锁会阻塞其他的写锁和读锁
又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
3、做数据的热备份,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。
原理(重要)
待补充
索引
b+树结构
索引为什么用b+树而不用b树或红黑树实现?
b树或红黑树都太高
b树查找不稳定,红黑树增删费时
B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找
B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当
没有利用局部性原理
b+树磁盘读写代价更低
b+树非叶子节点只储存索引,一次性读入内存中可以查找的关键字也就越多,降低IO读写次数
b+树遍历一遍叶子节点完成整棵树遍历,b树不行
B+ Tree索引和Hash索引区别?
无法范围查找,无法排序,不支持最左匹配原则,有大量重复键值效率低
哈希索引适合等值查询,但是无法进行范围查询
哈希索引没办法利用索引完成排序
哈希索引不支持多列联合索引的最左匹配规则
如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题
哈希索引没办法利用索引完成排序
哈希索引不支持多列联合索引的最左匹配规则
如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题
为什么 MongoDB 选用 B 树作为索引实现?
B+树的叶子节点上有指针进行相连,因此在做数据遍历的时候,只需要对叶子节点进行遍历即可,这个特性使得B+树非常适合做范围查询
而Mongodb是做单一查询比较多,数据遍历操作比较少,所以用B树作为索引结构
而Mongodb是做单一查询比较多,数据遍历操作比较少,所以用B树作为索引结构
红黑树和AVL树(平衡二叉树)比较
红黑树性能更高,因为其利用了“缓存”,算法一书中说过,红黑树相当于2-3树。
2节点等价于普通平衡二叉树的节点,而3节点本质上是“非平衡的缓存”,
当数据随机性强时,2节点向3节点转化可以吸收一些非平衡性性,减少旋转次数,快速完成平衡
2节点等价于普通平衡二叉树的节点,而3节点本质上是“非平衡的缓存”,
当数据随机性强时,2节点向3节点转化可以吸收一些非平衡性性,减少旋转次数,快速完成平衡
计算多少数据的树高/三层可以放多少数据
InnoDB存储引擎的最小存储单元是页,默认一个数据页大小为16kb,一个页中key假设为bigint占8byte,
指向下个页的地址指针占6byte,16kb/(8+6)b=1170个对象(关键字-页指针)
指向下个页的地址指针占6byte,16kb/(8+6)b=1170个对象(关键字-页指针)
假设数据记录大小1KB -> 叶子节点(页)可以存 16kb/1kb = 16条数据
高度为2的B+树:1170 * 16 = 18720,约存2万条数据记录。
高度为3的B+树:1170 * 1170 * 16 = 21902400,约存2千万条数据记录。
如果是b树,那么2000万数据的树高为log16 2000万 远远大于3层
索引失效场景
案例
or中至少有一个字段无索引
不符合最左前缀原则
模糊匹配%在最前
not in
mysql自动调优选择了不走索引
如果没有主键,还会不会有有聚簇索引?有
mysql会找到一个值都不相同的列,为它加上UNIQUE INDEX
如果没有具有唯一值的特征列,那么InnoDB会自动生成一个不可见的名为ROW_ID的列,名为GEN_CLUST_INDEX的聚簇索引
该列是一个6字节的自增数值,随着插入而自增
该列是一个6字节的自增数值,随着插入而自增
为什么主键推荐设为自增?
递增才能支持范围查找
递增下,新增节点直接往后双链表后放。不递增的话,会因插入新节点造成大量页分裂
聚簇索引和非聚簇索引
聚簇索引和非聚簇索引(innodb)(回表查询就是非聚簇查询)
(1)先通过非聚簇索引定位到主键值id=14;
(2)在通过聚集索引14定位到行记录Eillson
(1)先通过非聚簇索引定位到主键值id=14;
(2)在通过聚集索引14定位到行记录Eillson
简单说就是以表的主键构建的一棵B+树的索引数据结构,
如果没有主键的话innodb会选择唯一非空的主键。
好坏处就是B+树的好坏处。
如果没有主键的话innodb会选择唯一非空的主键。
好坏处就是B+树的好坏处。
聚集索引通常是表的主键,若无主键则为表中第一个非空的唯一索引,
还是没有就采用innodb存储引擎为每行数据内置的ROWID作为聚集索引。
还是没有就采用innodb存储引擎为每行数据内置的ROWID作为聚集索引。
myisam默认非聚簇索引
主键索引和辅助索引是没啥区别的,都指向叶子节点的数据。
因为索引树都是独立的,所以辅助所以无须通过主键索引来查询数据。
因为索引树都是独立的,所以辅助所以无须通过主键索引来查询数据。
索引覆盖
如何实现索引覆盖?
常见的方法是:将被查询的字段,建立到联合索引里去
哪些场景可以利用索引覆盖来优化SQL?
场景1:全表count查询优化
场景2:列查询回表优化
场景3:分页查询
待补充
联合索引
创建多个单列索引好还是一个联合索引好?
创建多个单列索引好还是一个联合索引好?
创建表时创建联合索引
语句:KEY `联合索引` (`a`,`b`,`c`);
实际上创建了索引a、ab、abc
语句:KEY `联合索引` (`a`,`b`,`c`);
实际上创建了索引a、ab、abc
用到了联合索引
a
a and b
b and a
a and c
虽然explain显示用了联合索引,但实际上只用到了a的索引,c并没有用到
a and b and c
没用到索引
b
c
a or b
只有ab都有索引,才会都用索引,其他情况(a和b至少有一个没索引)都用不到索引
因为假设ab中b没索引的话,那么a or b的执行情况很可能是a走了索引扫了一遍没找到,最后还是要全表扫描,还不如直接全表扫描
因为假设ab中b没索引的话,那么a or b的执行情况很可能是a走了索引扫了一遍没找到,最后还是要全表扫描,还不如直接全表扫描
b and c
给a、b、c分别创建单列索引
a and b and c
只用到a索引(abc都可以用,只不过mysql优化器只选其中最有效的一个或几个)
b and c
只用到b(和上同理)
a or b
a和b都用到了
结论:多个单列索引底层会建立多个B+索引树,比较占用空间,也会浪费一定搜索效率,故如果只有多条件联合查询时最好建联合索引
where查询是按照从左到右的顺序,所以筛选力度大的条件尽量放前面?错误。实际上mysql优化器会根据索引自己选择,与顺序无关
联合索引在设置的时候应该以什么标准把哪一列的数据放在前面
数据重复性最小的
建立索引要注意什么?
给具有唯一值的列建唯一性索引(避免重复)
给经常查询、排序、分组的列建索引
对于过长的字段尽量使用前缀索引
尽量使用最左前缀匹配原则
删除不用的索引
给经常查询、排序、分组的列建索引
对于过长的字段尽量使用前缀索引
尽量使用最左前缀匹配原则
删除不用的索引
最左匹配原则
mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整
索引面试题
Mysql调优
慢查询优化步骤
0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE
1.where条件单表查,锁定最小返回记录表
这句话的意思是把查询语句的where都应用到 表中返回的记录数最小的表 开始查起,单表每个字段分别查询,看哪个字段的区分度最高(区分度的公式是count(distinct col)/count(*),表示字段不重复的比例)
2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
查看慢查询开启命令:show variables like '%slow_query_log%'
默认关闭
默认关闭
开启:set global slow_query_log=1;
只对当前数据库生效,重启MySQL后失效;
想永久开启就修改my.cnf文件
只对当前数据库生效,重启MySQL后失效;
想永久开启就修改my.cnf文件
查看慢查询阈值时间:show variables like 'long_query_time';默认10秒
修改 set global long_query_time=x ; x秒
修改 set global long_query_time=x ; x秒
但设置后再查看会发现没改变?
这时重新开个会话窗口就好了。
或者show global variables like '%long_query_time%';
这时重新开个会话窗口就好了。
或者show global variables like '%long_query_time%';
查看日志
查询当前表有多少条慢SQL
show status like '%slow_queries%';
查询整个系统的话就加global
查询整个系统的话就加global
mysqldumpslow
showprofile
语句和花费的时间
一个语句的生命周期,
SQL语句在上张图片
SQL语句在上张图片
加limit优化
mysql大表优化查询效率
对索引进行优化。
1.不要加过多的索引,考虑给where和order by加
2.避免在where子句中用NULL,会造成全表扫描
3.字符字段最好不做主键,只加前缀索引
4.使用覆盖索引,(例如通过添加联合索引实现覆盖索引)
1.不要加过多的索引,考虑给where和order by加
2.避免在where子句中用NULL,会造成全表扫描
3.字符字段最好不做主键,只加前缀索引
4.使用覆盖索引,(例如通过添加联合索引实现覆盖索引)
join
in和exists
SELECT 字段 FROM table WHERE EXISTS(subquery);
SELECT * FROM A WHERE id IN (SELECT id FROM B);
order by
group by
事务
事务特性
ACID
原子性(atomicity)
事务中的操作要么全部完成,要么全部失败
一致性(consistency)
事务的执行结果必须是使数据库从一个一致性状态到另一个一致性状态
隔离性(isolation)
一个事务的执行不受其它事务的干扰
通过MVCC或锁保证隔离性
扩展:四种隔离级别和各自的底层实现
持久性(durability)
事务一旦提交,它对数据库的改变就是永久性的
隔离级别
(强度由上往下增强)
(强度由上往下增强)
读未提交
读取到未提交的事务
读已提交(实际用的最多)
读取已经提交的事务
可重复读(mysql默认)
在事务A中读取数据时,事务B更改数据并提交,
但事务A未提交事务,读的还是和之前一样的数据
但事务A未提交事务,读的还是和之前一样的数据
串行化
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。
简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
底层实现
并发场景
读-读 不存在线程安全问题
读-写:有线程安全问题,可能会造成事务隔离性问题
并发事务带来的问题
脏读
读取到另一个事务未提交的数据
幻读
两次读到的数据(行数)不一致。
第一次读,然后有事务插入或删除了数据,第二次读与之前不一致。
第一次读,然后有事务插入或删除了数据,第二次读与之前不一致。
SQL规范中,可重复读不能解决幻读,但mysql使用gap锁解决了幻读问题
不可重复读
两次读取到的数据不一致。
读一次,然后有另一个事务更新了数据,第二次读就不一致了
读一次,然后有另一个事务更新了数据,第二次读就不一致了
写-写:有线程安全问题,可能会存在丢失更新问题
丢失更新
后一个事务的更新覆盖了前一个事务更新的情况
第一类丢失更新:回滚覆盖
由于某个事务的回滚操作,参与回滚的旧数据将其他事务的数据更新覆盖
标准定义的所有隔离界别都不允许第一类丢失更新发生。基本上数据库的使用者不需要关心此类问题
标准定义的所有隔离界别都不允许第一类丢失更新发生。基本上数据库的使用者不需要关心此类问题
第二类丢失更新:更新覆盖
多个事务同时更新一行数据,导致一个事务更新被另一个事务覆盖
解决:乐观锁或悲观锁
MVCC
什么是MVCC?
多版本并发控制协议,只存在innodb引擎下,用于解决读写冲突
通过版本号的控制,实现对不同事务的隔离。
它的好处就是读不加锁,读写不冲突,非阻塞并发读。
应对高并发事务, MVCC比单纯的加锁更高效。
通过版本号的控制,实现对不同事务的隔离。
它的好处就是读不加锁,读写不冲突,非阻塞并发读。
应对高并发事务, MVCC比单纯的加锁更高效。
MVCC只在读已提交和可重复读两个隔离级别下工作。
MVCC可以使用 乐观锁 和 悲观锁来实现
MVCC可以使用 乐观锁 和 悲观锁来实现
MVCC在可重复读下实现原理:每行记录后面隐藏了两列:创建时间和删除时间(实际上是系统版本号)
每开始一个事务,版本号就+1。
查询时只查出同时符合1.行的系统版本号<=事务的系统版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
2.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除
每开始一个事务,版本号就+1。
查询时只查出同时符合1.行的系统版本号<=事务的系统版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
2.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除
insert:InnoDB为新插入的每一行保存当前系统版本号作为行版本号
delete:InnoDB为删除的每一行保存当前系统版本号作为行删除标识
版本链
三个隐藏列
row_id
trx_id
用来存储的每次对某条聚簇索引记录进行修改的时候的事务id
roll_pointer
旧版本串成单链表,版本链的头结点是当前记录最新的值
ReadView
核心问题
判断版本链中哪些版本对当前事务可见
版本链访问记录判断逻辑
实现
Read Commited
每一次进行普通select操作前都会生成一个ReadView
Repeated Read
只在第一次进行普通select操作前生成一个ReadView,之后的select重复使用
undo
insert undo
事务提交即释放
update undo
需要支持MVCC,不能立即删除
delete mark
记录打删除标记(逻辑删除)
explain
用于sql语句前面查看sql语句的执行计划
select,update,delete,insert都可以
使用,explain字段相同,不过增删改在内部都
被重写为 执行增删改+select
select,update,delete,insert都可以
使用,explain字段相同,不过增删改在内部都
被重写为 执行增删改+select
id:选择标识符,
值越高,优先级越高
值越高,优先级越高
select_type:表示查询的类型
table:输出结果集的表
partitions:匹配的分区
type:表示表的连接类型
possible_keys:表示查询时,可能使用的索引
key:表示实际使用的索引
key_len:索引字段的长度
ref:列与索引的比较
rows:扫描出的行数(估算的行数)
rows是核心指标,绝大部分rows小的语句执行一定很快(有例外)。所以优化语句基本上都是在优化rows。
rows是核心指标,绝大部分rows小的语句执行一定很快(有例外)。所以优化语句基本上都是在优化rows。
Extra:执行情况的描述和说明
using where:使用了where
using index:使用了覆盖索引
using temporay:
表示在查询过程中产生了临时表用于保存中间结果。
mysql在对有不是索引的字段进行group by,会出现临时表
group by的实质是先排序后分组.
mysql在对有不是索引的字段进行group by,会出现临时表
group by的实质是先排序后分组.
using filesort
Using filesort通常出现在order by,
当试图对一个不是索引的字段进行排序时,
mysql就会自动对该字段进行排序,
这个过程就称为“文件排序”
当试图对一个不是索引的字段进行排序时,
mysql就会自动对该字段进行排序,
这个过程就称为“文件排序”
InnoDB锁
按加锁机制
乐观锁
每次去拿数据的时候都认为别人不会修改,所以不会上锁,
但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据(版本号或者时间戳)
但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据(版本号或者时间戳)
悲观锁
每次(开启事务)在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
按粒度
行级锁
对当前操作的行加锁
锁粒度小,开销大,加锁慢,会发生死锁,并发度高,发生锁冲突概率低
表级锁
对当前操作的表上锁
锁粒度大,开销小,加锁块,不会发生死锁,并发度低,发生锁冲突概率高
页级锁
处于行级锁和表级锁之间
会发生死锁,并发度一般
按兼容性
共享锁(读锁)
读取数据时,任何事务不能修改数据
用法:lock in share mode
排他锁(写锁)
修改数据时,任何事务不能读取和写数据
用法:for update
按模式
Record Lock
单个行记录上的锁
Gap Lock(间隙锁)
会对键值条件内但并不存在的数据加锁。(行锁是前提)
它是innoDB在可重复读的级别下防止幻读引入的锁
它是innoDB在可重复读的级别下防止幻读引入的锁
Next-key Lock(临键锁)
它是行锁和间隙锁的本身。它规定是左开右闭区间。
比如(3,5]这个区间会加锁。
比如(3,5]这个区间会加锁。
主键生成策略
分布式ID生成系统需求
全局唯一,不能重复,(基本要求)
递增,下一个ID大于上一个ID;(可选,某些需求)
信息安全,非连续ID,避免恶意用户/竞争对手发现ID规则,从而猜出下一个ID或者根据ID总量猜出业务总量
高可用,不能故障,可用性4个9或者5个9;(99.99%、99.999%)
高QPS,性能不能太差,否则容易造成线程堵塞;
平均延迟尽可能低;
1、mysql主键自增
MySQL中,可通过数据列的auto_increment属性来自动生成自动递增的唯一编号
优点
简单,代码方便,性能可以接受
数字ID天然排序,对分页或者需要排序的结果很有帮助
缺点
不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理
ID生成依赖数据库单机的读写性能;(高并发条件下性能不是很好)
在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险
在性能达不到要求的情况下,比较难于扩展
如果遇见多个系统需要合并或者涉及到数据迁移会相当复杂
分表分库的时候无法依靠auto_increment属性来唯一标识一条记录
2、uuid
优点
简单,代码方便
生成ID性能非常好,基本不会有性能问题
全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对
缺点
没有排序,无法保证趋势递增
UUID往往是使用字符串存储,查询的效率比较低
传输数据量大
存储空间比较大,如果是海量数据库,就需要考虑存储量的问题
不可读
3、redis固定步长生成
通过Redis原子操作命令INCR和INCRBY(redis自增)实现递增,同时可使用Redis集群提高吞吐量,
举例:集群后每台Redis的初始值为1,2,3,4,5,固定步长为5,可以实现1、6、11 , 2、7、12 等等
举例:集群后每台Redis的初始值为1,2,3,4,5,固定步长为5,可以实现1、6、11 , 2、7、12 等等
ps:比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。
可以每天在Redis中生成一个Key,使用INCR进行累加
可以每天在Redis中生成一个Key,使用INCR进行累加
4、Twitter的雪花算法
Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0
https://github.com/twitter/snowflake
5、Zookeeper生成分布式唯一ID
方案一:通过持久顺序节点实现;
方案二:通过节点版本号;
Sql语句练习
查找最晚入职员工的所有信息
sql
1、ORDER BY x 对结果集根据x来进行排序(默认从小到大排序)
2、DESC 就是将默认排序改为从大到小降序排序
3、LiMIT
LIMIT x 取最前面x条记录显示
LIMIT x , y 取x+1行开始往下y行进行显示(例如LIMIT 2,2 从第3行(x+1)开始取第3,4行)
LIMIT x 取最前面x条记录显示
LIMIT x , y 取x+1行开始往下y行进行显示(例如LIMIT 2,2 从第3行(x+1)开始取第3,4行)
查找薪水记录超过15次的员工号emp_no
sql
count()
计算出现了多少次
sum()
求和
HAVING
子主题
GROUP BY
Spring
ioc
IoC 容器控制了外部资源获取(不只是对象包括文件等),由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象
为什么需要ioc
Bean之间的解耦
不关心bean的底层实现
换dao的时候只要更改配置类
单例
aop
aop利用静态代理(AspectJ aop)和动态代理(JDK动态代理、CGLIB动态代理)实现。
静态代理是在编译时增强代码,动态代理是在运行时增强代码。静态代理性能更高
静态代理是在编译时增强代码,动态代理是在运行时增强代码。静态代理性能更高
jdk动态代理和cglib代理的区别
子主题
aop具体的底层实现
子主题
aop在项目中怎么用,做什么
子主题
jdk动态代理是基于invocationHandler接口和Proxy类实现,invocationHandler接口通过invoke方法反射调用目标类中的方法
IOC和AOP面试
IOC是什么
IOC容器初始化过程
依赖注入的实现方法
依赖注入的相关注解
依赖注入的过程
Bean的生命周期
Bean的作用范围
Bean创建的两种方式
XML
使用Spring XML方式配置,该方式用于在纯Spring 应用中,适用于简单的小应用,当应用变得复杂,将会导致XMl配置文件膨胀 ,不利于对象管理。
<bean id="xxxx" class="xxxx.xxxx"/>
<bean id="xxxx" class="xxxx.xxxx"/>
注解
1、@Component,@Service,@Controler,@Repository注解
这几个注解都是同样的功能,被注解的类将会被Spring 容器创建单例对象。
@Component : 侧重于通用的Bean类
@Service:标识该类用于业务逻辑
@Controler:标识该类为Spring MVC的控制器类
@Repository: 标识该类是一个实体类,只有属性和Setter,Getter
ps:当用于Spring Boot应用时,被注解的类必须在启动类的根路径或者子路径下,否则不会生效。
如果不在,可以使用@ComponentScan标注扫描的路径
@Component : 侧重于通用的Bean类
@Service:标识该类用于业务逻辑
@Controler:标识该类为Spring MVC的控制器类
@Repository: 标识该类是一个实体类,只有属性和Setter,Getter
ps:当用于Spring Boot应用时,被注解的类必须在启动类的根路径或者子路径下,否则不会生效。
如果不在,可以使用@ComponentScan标注扫描的路径
2、@Bean注解
这种方式用在Spring Boot 应用中。
@Configuration 标识这是一个Spring Boot 配置类,其将会扫描该类中是否存在@Bean 注解的方法,
比如如下代码,将会创建User对象并放入容器中。
@ConditionalOnBean 用于判断存在某个Bean时才会创建User Bean.
这里创建的Bean名称默认为方法的名称user。也可以@Bean("xxxx")定义。
@Configuration 标识这是一个Spring Boot 配置类,其将会扫描该类中是否存在@Bean 注解的方法,
比如如下代码,将会创建User对象并放入容器中。
@ConditionalOnBean 用于判断存在某个Bean时才会创建User Bean.
这里创建的Bean名称默认为方法的名称user。也可以@Bean("xxxx")定义。
3、@Import注解
使用注解@Import,也会创建对象并注入容器中
4、使用ImportSelector或者ImportBeanDefinitionRegistrar接口,配合@Import实现。
手动注入Bean容器
有些场景下需要代码动态注入,以上方式都不适用。这时就需要创建 对象手动注入。
通过DefaultListableBeanFactory注入。
registerSingleton(String beanName,Object object);
这里手动使用new创建了一个Location对象。并注入容器中
通过DefaultListableBeanFactory注入。
registerSingleton(String beanName,Object object);
这里手动使用new创建了一个Location对象。并注入容器中
这种方式的应用场景是为接口创建动态代理对象,并向SPRING容器注册。
比如MyBatis中的Mapper接口,Mapper没有实现类,启动时创建动态代理对象,将该对象注册到容器中,使用时只要@Autowired注入即可使用,调用接口方法将会被代理拦截,进而调用相关的SqlSession执行相关的SQL业务逻辑
比如MyBatis中的Mapper接口,Mapper没有实现类,启动时创建动态代理对象,将该对象注册到容器中,使用时只要@Autowired注入即可使用,调用接口方法将会被代理拦截,进而调用相关的SqlSession执行相关的SQL业务逻辑
如何通过注解配置文件
BeanFactory & FactoryBean & ApplicationContext
AOP是什么
AOP相关注解
AOP相关术语
AOP的过程
springboot自动配置原理
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
spring mvc运行原理↑
用户发送请求至前端控制器DispatcherServlet;
DispatcherServlet收到请求调用HandlerMapping;
HandlerMapping根据请求的url找到具体的HandlerMapping处理器,生成并返回处理器执行链HandlerExecutionChain。(拦截器链里装有handler对象,拦截器)
根据handler拿到对应的适配器。
处理拦截器的prehandler方法。
适配器执行handler,返回ModelAndView;
处理拦截器的poshandler方法
DispatcherServlet将ModelAndView传给ViewReslover视图解析器
ViewReslover解析后返回具体View。(主要是提供视图缓存的支持,对redirect和forward前缀的处理)
DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。
DispatcherServlet响应用户
注解
@Autowired与@Resource的区别
提供方 @Autowired是Spring的注解,@Resource是javax.annotation注解
注入方式 @Autowired先按照Type 注入,如果有多个bean,则按照Name注入;@Resource默认按Name自动注入,也提供按照Type 注入
@Transactional失效场景
介绍spring事务
编程式事务:在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强
声明式事务
基于TX和AOP的xml配置文件方式
基于@Transactional注解
作用位置
作用在类:表示所有该类的public方法都配置相同的事务属性信息
作用在方法:方法上的事务 > 类的事务,方法上的优先
作用在接口(不推荐):因为注解是不能继承的,第一个实现接口的类会有事务属性,但这个类再去调用别的方法,就会失效
常用属性
propagation 事务传播行为
spring在TransactionDefinition接口中定义了七个事务传播行为:
- propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
- propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
- propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
- propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
- propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
- propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作
isolation 事务的隔离级别
readOnly 是否只读
rollbackFor :用于指定能够触发事务回滚的异常类型,可以指定多个异常类型
最常见:含事务的B方法的异常被A方法的catch捕获,导致B方法不能正常回滚,进而事务失效
@Transactional 应用在非 public 修饰的方法上(且不会报错)
因为事务拦截器需要在目标方法执行前后进行拦截
同一个类中的不含事务的A方法调用含事务的B方法(不论B是public还是private)
因为基于AOP代理,只有从类的外部调用类中含事务的方法才会生效
最少见:数据库引擎不支持事务,InnoDB支持,MyISAM不支持
@Component
@Service
@Controller
@Repository
@PostConstruct
bean
生命周期(singleton才有)
所有的Aware方法都是在初始化阶段之前调用的
加载过程
作用域
singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的
prototype : 每次请求都会创建一个新的 bean 实例
request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效
session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效
BeanFactory和FactoryBean区别?
BeanFactory源码
BeanFactory是ioc容器的底层实现接口,是ApplicationContext顶级接口,是spring中比较原始的Factory。
如XMLBeanFactory就是一种典型的BeanFactory。
原始的BeanFactory无法支持spring的许多插件,如AOP功能、Web应用等。ApplicationContext接口,它由BeanFactory接口派生而来,ApplicationContext包含BeanFactory的所有功能,通常建议比BeanFactory优先
如XMLBeanFactory就是一种典型的BeanFactory。
原始的BeanFactory无法支持spring的许多插件,如AOP功能、Web应用等。ApplicationContext接口,它由BeanFactory接口派生而来,ApplicationContext包含BeanFactory的所有功能,通常建议比BeanFactory优先
FactoryBean源码
区别
BeanFactory是接口,提供了IOC容器最基本的形式,给具体的IOC容器的实现提供了规范,
FactoryBean也是接口,为IOC容器中Bean的实现提供了更加灵活的方式,FactoryBean在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式(如果想了解装饰模式参考:修饰者模式(装饰者模式,Decoration) 我们可以在getObject()方法中灵活配置。其实在Spring源码中有很多FactoryBean的实现类.
FactoryBean也是接口,为IOC容器中Bean的实现提供了更加灵活的方式,FactoryBean在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式(如果想了解装饰模式参考:修饰者模式(装饰者模式,Decoration) 我们可以在getObject()方法中灵活配置。其实在Spring源码中有很多FactoryBean的实现类.
当我们尝试按name从BeanFactory.getBean(beanname)一个Bean时,返回的一定是A类对应的实例吗?
答案是否, 当A需要需要创建代理对象时,我们getBean 得到是 代理对象的引用
答案是否, 当A需要需要创建代理对象时,我们getBean 得到是 代理对象的引用
循环依赖
A绑定到ObjectFactory 注册到工厂缓存singletonFactory中,
B在填充A时,先查成品缓存有没有,再查半成品缓存有没有,最后看工厂缓存有没有单例工厂类,有A的ObjectFactory。调用getObject ,执行扩展逻辑,可能返回的代理引用,也可能返回原始引用。
成功获取到A的早期引用,将A放入到半成品缓存中,B填充A引用完毕。
代理问题, 循环依赖问题都解决了
B在填充A时,先查成品缓存有没有,再查半成品缓存有没有,最后看工厂缓存有没有单例工厂类,有A的ObjectFactory。调用getObject ,执行扩展逻辑,可能返回的代理引用,也可能返回原始引用。
成功获取到A的早期引用,将A放入到半成品缓存中,B填充A引用完毕。
代理问题, 循环依赖问题都解决了
三级缓存
singletonObjects:第一级缓存,里面存放的都是创建好的成品Bean
earlySingletonObjects : 第二级缓存,里面存放的都是半成品的Bean(实例化,未完成初始化的单例对象(未完成属性注入的对象))
singletonFactories :第三级缓存, 不同于前两个存的是 Bean对象引用,此缓存存的bean 工厂对象,也就存的是 专门创建Bean的一个工厂对象。此缓存用于解决循环依赖(存放ObjectFactory对象)
为什么要有二级缓存?
放入二级缓存的bean是包装了一成ObjectFactory,意味着这时它不会被代理。
为了防止对象在后面的初始化(init)时重复代理,在创建代理时,earlyProxyReferences缓存会记录已代理的对象。
从spring的bean生命周期看,bean是在完成注入后,基本完成构造后才进行代理的。
那为什么要使它不会被代理呢?或者说不要这个二级缓存会有什么样的结果?
所以它如果不要二级缓存就违背了spring设计原则。
弄三级缓存的好处是可以扩展
另一种说法是earlyProxyReferences缓存会记录已代理的对象,防止每次获取对象都返回一个新的代理对象
为了防止对象在后面的初始化(init)时重复代理,在创建代理时,earlyProxyReferences缓存会记录已代理的对象。
从spring的bean生命周期看,bean是在完成注入后,基本完成构造后才进行代理的。
那为什么要使它不会被代理呢?或者说不要这个二级缓存会有什么样的结果?
所以它如果不要二级缓存就违背了spring设计原则。
弄三级缓存的好处是可以扩展
另一种说法是earlyProxyReferences缓存会记录已代理的对象,防止每次获取对象都返回一个新的代理对象
所有循环依赖都可以用三级缓存解决吗?
AOP
概念
简介
面向切面编程,声明式的事务管理,就是将交叉代码逻辑封装成切面;利用AOP的功能织入到主业务逻辑中
而所谓交叉业务逻辑是指与主业务逻辑无关的代码,如安全检查,事务,日志等
而所谓交叉业务逻辑是指与主业务逻辑无关的代码,如安全检查,事务,日志等
相关术语
Target(目标)
Proxy(代理)
JoinPoint(连接点)
PointCut(切点)
Advice(增强)
Advisor(切面)
Weaving(织入)
Introdution(引入)
增强类型
Before Advice(前置增强)
After Advice(后置增强)
Around Advice(环绕增强)
Throws Advice(抛出增强)
Introdution Advice(引入增强)
作用:
增加功能
代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是通过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
控制访问
代理类不让客户类访问委托类(例如 商家不让顾客直接与厂家交易),其特征是代理类和委托类实现相同的接口
分类
使用
Spring+AspectJ
基于注解(@Aspect)
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>
基于xml配置(aop:config)
<aop:config expose-proxy="true" proxy-target-class="false">
</aop:config>
默认false,true使用cglib代理模式,false或者省略使用JDK代理
</aop:config>
默认false,true使用cglib代理模式,false或者省略使用JDK代理
原理
获取和匹配增强器
静态代理
静态代理类是手动实现的,创建一个java类,并和委托类实现同一个接口表示该类为代理类
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
动态代理
动态代理无需手动创建代理类,代理对象直接由代理生成工具动态生成(在程序运行期间由JVM根据反射等机制动态的生成源码,类似工厂模式
JDK动态代理
用户类通过调用java.lang.reflect.Proxy类下的newProxyInstance()方法来创建代理对象
优点:因为有接口,所以使系统更加松耦合
缺点:JDK动态代理必须要有接口
优点:因为有接口,所以使系统更加松耦合
缺点:JDK动态代理必须要有接口
CGLIB动态代理
优点:因为代理类与目标类是继承关系,所以不需要有接口的存在
缺点:因为没有使用接口,所以系统的耦合性比使用JDK的动态代理高
缺点:因为没有使用接口,所以系统的耦合性比使用JDK的动态代理高
待补充
Aware作用
它是一个接口,在spring用作一个标识作用
比如beanNameAware继承aware接口
自定义一个类实现这个接口,重写它的setBeanNameAware方法
当容器bean初始化时发现这个bean属于BeanNameAawre这个类
那么就会去调用它的setBeanName以及其它重写的方法
比如beanNameAware继承aware接口
自定义一个类实现这个接口,重写它的setBeanNameAware方法
当容器bean初始化时发现这个bean属于BeanNameAawre这个类
那么就会去调用它的setBeanName以及其它重写的方法
Spring编程常见错误50例
SpringCore篇
SpringBean定义常见错误
案例1:隐式扫描不到Bean的定义
案例2:Bean中自定义的构造方法缺少隐式依赖
案例3:原型Bean被固定为单例
SpringBean依赖注入常见错误
案例1:@Autowire 过多赠与,无所适从
案例2:显示引用的Bean的首字母忽略大小写
案例3:引用内部类的Bean遗忘类名
案例4:@Value没有注入期望值
案例5:错乱的注入集合
SpringBean生命周期常见错误
案例1:Bean的构造器内的变量抛NullPointerExeception
SpringWeb篇
补充篇
Spring事务常见错误
异常与事务回滚(rollbackFor)
@Transactional注解,默认情况下只会对RuntimeException和error 的异常进行事务回滚,要想也对Exception的异常回滚,需要加rollbackFor = Exception.class
试图给private方法添加事务
只用被事务注解修饰的public方法才能支持回滚
调用事务的方法,必须是被aop代理的方法,也就是不能通过内部this调用,需要注入对象
嵌套事务
属性propagation
默认propagation = Propagation.REQUIRED,父事务和子事务合并成一个事务,子事务如果想独立事务,设置REQUIRED_NEW
多数据源切换之谜
Spring Test
Redis
redis优缺点
Redis 的全称是:Remote Dictionary.Server,本质上是一个 Key-Value 类型的内存数据库,很像 memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬 盘上进行保存。 因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value DB。 Redis 的出色之处不仅仅是性能,Redis 最大的魅力是支持保存多种数据结构,此外单个 value 的最大 限制是 1GB,不像 memcached 只能保存 1MB 的数据,因此 Redis 可以用来实现很多有用的功能。 比方说用他的 List 来做 FIFO 双向链表,实现一个轻量级的高性 能消息队列服务,用他的 Set 可以做 高性能的 tag 系统等等。 另外 Redis 也可以对存入的 Key-Value 设置 expire 时间,因此也可以被当作一 个功能加强版的 memcached 来用。 Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能 读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上
应用场景
热点数据缓存
限时业务
分布式锁
Redis可以实现的项目功能
具体功能
其他
排行榜
共同关注
set操作
点赞/收藏(标签)
抽奖
微博/公众号消息流
购物车
数据类型对应应用
五种基本数据类型
String
计数器
微信小程序中的喜欢作者和踩一下,采用incrby xx即可
Hash
小型购物车
hlen获取商品总数
hgetall勾选所有商品
List
消息队列
微信订阅号中消息的推送
Set
微信小程序的开奖
srandmember xxx
spop xxx
Zset
热搜榜对应的key,value,根据热度进行排序
三种特殊数据类型
Bitmap
签到
Hyperloglog
基于loglog算法
用于基数统计
用于基数统计
GEO
定位
附近的人
redis五种数据类型
注意:在Redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象,
所以我们通常说的键为字符串键,表示的是这个键对应的值为字符串对象,我们说一
个键为集合键时,表示的只是这个键对应的值为集合对象,键本身为字符串。
所以我们通常说的键为字符串键,表示的是这个键对应的值为字符串对象,我们说一
个键为集合键时,表示的只是这个键对应的值为集合对象,键本身为字符串。
string
int编码
保存的是可以用 long 类型表示的整数值。
例如 1 、 233 、1000000
保存的是可以用 long 类型表示的整数值。
例如 1 、 233 、1000000
embstr 编码
保存长度小于44字节的字符串
(redis3.2版本之前是39字节,之后是44字节)
保存长度小于44字节的字符串
(redis3.2版本之前是39字节,之后是44字节)
cpucacheline每次读是64byte(64操作系统),
redisObject一般只占20byte,所以剩下空间缓存embstr的字符串
redisObject一般只占20byte,所以剩下空间缓存embstr的字符串
raw 编码
保存长度大于44字节的字符串
(redis3.2版本之前是39字节,之后是44字节)
保存长度大于44字节的字符串
(redis3.2版本之前是39字节,之后是44字节)
使用场景
1、计数器
2、分布式锁
3、储存对象(不常变化)
list
ziplist(压缩列表) quickList(3.2之后)
ziplist是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表
可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存
可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存
当同时满足下面两个条件时,使用ziplist(压缩列表)编码:
1、列表保存元素个数小于512个
2、每个元素长度小于64字节
不能满足上面两个条件的时候使用 linkedlist 编码。
上面两个条件可以在redis.conf 配置文件中的 list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置
1、列表保存元素个数小于512个
2、每个元素长度小于64字节
不能满足上面两个条件的时候使用 linkedlist 编码。
上面两个条件可以在redis.conf 配置文件中的 list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置
ziplist+linkedlist(双端链表)(3.2之前)
使用场景:
1、排行榜
hash
ziplist
储存方式和list的ziplist一样
hashtable
hashtable 编码的哈希表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对
使用场景:
1、购物车 以用户id为key,商品id为field,商品信息为value
2、储存对象(经常变化)
set
intset(整数集合)(会排序不重复)
intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中
hashtable
hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null
zset
ziplist
分值小的靠近表头,分值大的靠近表尾
skiplist
skiplist 编码的有序集合对象使用 zset 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表
字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值
说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合
字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值
说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合
优点:跳表的线程安全是通过cas锁实现的,跳表的构建相对简单,同时支持范围查找
缺点:相对于红黑树,空间消耗增加
过期删除策略
Expire 设置过期时间
定时删除:出生的key自带一个定时器,到期就删
及时删除对内存好,但是偶尔过多需要删除的话对cpu不好
惰性删除:要用这个key的时候发现过期了才删
对cpu好,但是对内存不好,还有可能内存泄漏
定期删除:定期随机抽取一部分key看是否过期,过期就删
兼容上面两种优点,但缺点是频率不好把握,频率过高就跟定时删除一样,过低就跟惰性删除一样
更致命的是还可能返回已经过期的key的值
更致命的是还可能返回已经过期的key的值
Redis的过期删除策略就是:惰性删除和定期删除两种策略配合使用
内存淘汰策略
先设置redis最大内存,默认无限制,一般设3/4
策略有很多种,通常用allkeys-lru 利用LRU算法移除任何key(包括过期和不过期
大key问题
场景:热门话题的评论,答案排序,大v的粉丝列表
整存整取:多redis实例拆分。只要部分:hash+filed片段访问
本地缓存
热key问题
本地缓存:Ehcache、hashmap
集群+随机数:给请求加随机数以分配到集群中不同的redis
最好提前发现:监控热key+通知系统去做本地缓存
分布式锁
setnx拿到锁后 删锁前 宕机,死锁
解决:设置过期时间(expire)
setnx拿到锁后 设置过期时间前 宕机,死锁
解决:拿锁和设置过期必须原子(setnx ex+Lua脚本)
setnx ex拿到锁后 锁因已过期 现在删了别的锁
解决:删锁必须原子(uuid+Lua脚本)
Redisson的看门狗
每过1/3的过期时间就判断一次锁状态,锁还在就续满
即使哨兵模式也无法解决主从复制的时延问题,redis主机一崩就会丢失锁信息
RedLock
记录个毫秒级的开始时间。加锁时轮询所有redis服务器,如果某台连接时间过长则放弃它
>=n/2+1台已加锁 && 当前时间 - 开始时间 < 锁的超时时间 则创建成功
缓存问题
缓存穿透
用户不断发起缓存和数据库都不存在的请求
业务层校验
为不存在的数据设置短过期时间
布隆过滤器
缓存击穿
热点key失效的瞬间大量请求进来
互斥锁
缓存雪崩
key大面积同时失效或redis宕机
过期时间均匀分布一下
数据预热
持久化策略
RDB
把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。
Redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。
Redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。
优点:文件适合备份和恢复,大数据集恢复速度比AOF快
生成RDB时会让主进程fork()一个子进程来做,主进程不需要磁盘IO
缺点:不能实时持久化会丢失最后一次修改产生的数据
生成RDB时会让主进程fork()一个子进程来做,主进程不需要磁盘IO
缺点:不能实时持久化会丢失最后一次修改产生的数据
AOF
通过保存Redis服务器所执行的写命令来记录数据库状态。
AOF重写: AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件
优点:每秒同步一次
文件格式可读性强可手动修改一些命令
缺点:同等数据量下AOF比RDB文件更大
每秒同步一次导致性能比RDB低一些
AOF保存的是指令容易出bug
文件格式可读性强可手动修改一些命令
缺点:同等数据量下AOF比RDB文件更大
每秒同步一次导致性能比RDB低一些
AOF保存的是指令容易出bug
RDB-AOF混合持久化(redis4.0以后)
redis支持事务吗
开启事务multi、执行事务exec、取消事务discard
谁说NoSql都不支持事务,虽然redis的事务提供的并不是严格的ACID的事务(比如一串用EXEC提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行),但是这个事务还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间会有其他客户端命令插进来执行)。redis还提供了一个watch功能,你可以对一个key进行watch,然后再执行事务,在这个过程中,如果这个watch的值进行了修改,那么这个事务会发现并拒绝执行
待补充
三种常用读写策略
旁路缓存模式
写:先更新 DB
然后直接删除 cache
然后直接删除 cache
读 :从 cache 中读取数据,读取到就直接返回
cache中读取不到的话,就从 DB 中读取数据返回
再把数据放到 cache 中
cache中读取不到的话,就从 DB 中读取数据返回
再把数据放到 cache 中
可能存在的问题
缺陷1:首次请求数据一定不在 cache 的问题
解决办法:可以将热点数据可以提前放入cache 中。
解决办法:可以将热点数据可以提前放入cache 中。
缺陷2:写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。
解决办法:
数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。
可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
解决办法:
数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。
可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
问题1:在写数据的过程中,可以先删除 cache ,后更新 DB 吗?
答案: 不行 因为这样可能会造成数据库(DB)和缓存(Cache)数据不一致的问题。
例如:请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新
请求2读取的数据和DB里的数据就不一致了
答案: 不行 因为这样可能会造成数据库(DB)和缓存(Cache)数据不一致的问题。
例如:请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新
请求2读取的数据和DB里的数据就不一致了
读写穿透模式
异步缓存写入
发布/订阅模式
主从复制一致性问题
待补充
redis和mysql区别
待补充
redis压测
redis-benchmark
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000
可以打印出详细的耗时时间,默认以3个字节进行请求
表示模拟 100个 并发,10万个请求
-c 表示客户端数量 即100并发
-n 表示请求的数量 即10万请求
可以打印出详细的耗时时间,默认以3个字节进行请求
表示模拟 100个 并发,10万个请求
-c 表示客户端数量 即100并发
-n 表示请求的数量 即10万请求
待补充
RocketMQ
作用
异步、解耦、削峰、数据分发
优缺点
作用就是优点
缺点:引入外部依赖,系统稳定性降低,复杂度提高
还要考虑消息一致性问题
还要考虑消息一致性问题
应用场景
微信qq就是典型的mq应用。超时订单自动关闭
RocketMQ和RabbitMQ对比
组成
nameserver(broker的路由中心)、producer、broker、consumer
nameserver 无状态的含义:每台nameserver之间没有连接无信息同步,都会收到broker上报的信息,和producer、consumer的询问,搭建无状态集群比较方便
broker主从部署:nameserver将两台broker分为一组,其中broker的nameserverId为0的是主,其它的都是从,比如1
消费模式
集群消费:每个group只有一个consumer能消费
广播消费:每个group每个consumer能消费
重复消费问题
consumer消费后发送的ack因网络原因导致broker没及时收到,让consumer再消费了一遍
每条消息都会有一条唯一的消息ID,消费者接收到消息会存储消息日志,如果日志中存在相同ID的消息,就证明这条消息已经被处理过了
顺序消费问题
单个队列:天然FIFO顺序没有问题
多个队列:限制1个topic里只有1个queue,且被1个consumer消费
缺点:降低并发和吞吐量
消息丢失问题
producer
发送失败后重试(一般设最多3次)
采取send()同步发消息,发送结果是同步感知的
broker
默认异步刷盘,修改成同步刷盘
集群部署
consumer:消费正常后再进行手动ack确认
消息堆积问题
添加consumer
加topic
决定是否丢弃
死信队列
场景:一般应用在当正常业务处理时出现异常时,将消息拒绝则会进入到死信队列中,有助于统计异常数据并做后续处理
延时队列
rocketmq实现的延时队列只支持特定的延时时间段,1s,5s,10s,...2h,不能支持任意时间段的延时
把每种延迟时间段的消息都存放到同一个队列中,然后通过一个定时器进行轮询这些队列,查看消息是否到期,如果到期就把这个消息发送到指定topic的队列中
应用
订单超时未支付,自动取消
支付后24小时未评论自动好评
心跳机制
RocketMQ如何实现分布式事务?
ZooKeeper
zookeeper
概述
优点
acl权限控制
scheme:id:permission
scheme:id:permission
1.权限控制使用在节点上
2.一个节点可以有多个权限
3.权限对子节点无影响
2.一个节点可以有多个权限
3.权限对子节点无影响
secheme策略
world
所有人可用 id固定是anyone
ip
对ip认证
auth
对已添加认证的用户认证
digest
使用用户名密码认证
permission权限:cdrwa
watch
可以监听节点的状态和是否发生了变化。
只能使用一次。
如果想要继续监听就要再回调时重新设置watch
只能使用一次。
如果想要继续监听就要再回调时重新设置watch
两种数据类型
DataWathch
监听node节点的数据变化
监听node节点的数据变化
getData()
exists()
setData()
create()
ChildWatch
监听孩子节点发生变化
监听孩子节点发生变化
getChildren()
create()
分布式唯一id
依赖数据版本号生成
分布式锁
1.每个客户端与zookeeper连接时,都会再lock/目录下创建临时节点,并存储该客户带信息
2.按cZxid大小排序lock/目录下的节点(leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid事务id,通过zxid的大小比较就可以实现因果有序这个特征)
3. 判断排在第一的目录 是不是 自己,是的话就获取锁,不是的话就监听排在自己前面的节点
2.按cZxid大小排序lock/目录下的节点(leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid事务id,通过zxid的大小比较就可以实现因果有序这个特征)
3. 判断排在第一的目录 是不是 自己,是的话就获取锁,不是的话就监听排在自己前面的节点
如果拿着锁的客户端宕机了,与zookeeper断开连接,
那么它的临时节点就会被删除,监听这个节点的节点就会被通知
那么它的临时节点就会被删除,监听这个节点的节点就会被通知
zab协议
zookeeper原子广播
zookeeper原子广播
ZAB协议包含两种基本模式
崩溃恢复之数据恢复
消息广播之原子广播
简化版2PC
1》leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid,通过zxid的大小比较就可以实现因果有序这个特征。
2》leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的 follower。
3》当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack。
4》当leader接收到合法数量(超过半数节点)的ack后,leader就会向这些follower发送commit命令,同时会在本地执行该消息。
5》当follower收到消息的commit命令以后,会提交该消息。
2》leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的 follower。
3》当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack。
4》当leader接收到合法数量(超过半数节点)的ack后,leader就会向这些follower发送commit命令,同时会在本地执行该消息。
5》当follower收到消息的commit命令以后,会提交该消息。
监听原理
同步数据
选举机制
大于半数机制
大于半数机制
服务器状态
looking
寻找leader状态
leading
领导者状态
following
跟随着状态
observing
观察者状态,不投票
当有两个服务器时,
每个服务器都给自己投票,平票
然后就会比较zxid,如果相等,再比较myid,选择大的
最后票数大于一半服务器就成功选出leader
每个服务器都给自己投票,平票
然后就会比较zxid,如果相等,再比较myid,选择大的
最后票数大于一半服务器就成功选出leader
znode
临时节点
断开连接后,创建的节点自己删除
永久节点
断开连接,创建的节点不删除
Zookeeper第二版
应用场景:
1、维护配置信息
2、分布式锁场景
3、集群管理
4、生成分布式唯一ID
设计目标
1、高性能 zk的全量数据放在内存里
2、高可用 只要集群内超过一半机器正常工作,整个集群就能正常对外提供服务
3、严格顺序访问 对客户端的更新请求分配全局唯一的递增编号,用于反映操作先后顺序
数据类型
树状结构(目录),被称为znode(zookeeper node),一个znode可以有多个子节点
使用路径来定位某个znode节点:/ns-1/icast/mysql/schema1/table1
使用路径来定位某个znode节点:/ns-1/icast/mysql/schema1/table1
znode
znode内容
1、数据:znode-data,类似map中的key-value关系
2、状态 stat 用来描述当前节点的创建,修改记录,包括cZxid、ctime等
ps:写(增删改)操作会在zk服务器内部自动维护一个事务,读不会
ps:写(增删改)操作会在zk服务器内部自动维护一个事务,读不会
具体属性(get命令查看)
属性说明(ACL权限列表)
3、子节点:children
znode类型
节点的类型在创建时被确定,且不能改变
临时节点
生命周期依赖会话,会话(Session)结束后,临时节点自动删除,也可以手动删除
ps:虽然临时znode创建后会被绑定到一个客户端会话,但它对所有客户端也可见
pss:临时节点不允许拥有子节点
ps:虽然临时znode创建后会被绑定到一个客户端会话,但它对所有客户端也可见
pss:临时节点不允许拥有子节点
持久节点
生命周期不依赖会话,只有在客户端执行删除操作的时候才会被删除
待补充:watch、client、zab协议、acl权限控制、监听原理
Elasticsearch
基本概念
简介图
Index(索引)
动词:相当于mysql的insert
在es里插入一条数据即称为索引一条数据到es
在es里插入一条数据即称为索引一条数据到es
名词:相当于mysql的database
mysql里的database内存储一张张table(表)
对应es里index里存储一个个Type
mysql里的database内存储一张张table(表)
对应es里index里存储一个个Type
Type(类型)
在Index里可以定义一个或多个Type(类型)
类似mysql里的table,每一种类型的数据放在一起
es的数据(Document)存在某个索引的某个类型下
类似mysql里的table,每一种类型的数据放在一起
es的数据(Document)存在某个索引的某个类型下
Document(文档)
相当于mysql里的数据,格式为Json,文档内每个记录称为属性
保存在某个Index(索引)下的某个Type(类型)里的一个数据(文档)
相当于mysql 某个database下的某个table里的一个记录
保存在某个Index(索引)下的某个Type(类型)里的一个数据(文档)
相当于mysql 某个database下的某个table里的一个记录
PS:最新版本es移除了type字段
在 5.X 版本中,一个 index 下可以创建多个 type;
在 6.X 版本中,一个 index 下只能存在一个 type;
在 7.X 版本中,直接去除了 type 的概念,就是说 index 不再会有 type
在 6.X 版本中,一个 index 下只能存在一个 type;
在 7.X 版本中,直接去除了 type 的概念,就是说 index 不再会有 type
为什么移除:es基于lucene的倒排索引,倒排索引的生成基于index而非type,多个type会影响倒排索引性能
倒排索引
查询过程
1、维护一个倒排索引表
2、分词
3、根据保存的数据分词情况在索引表里添加记录
4、相关性得分
5、检索
组成
ES存储的是一个JSON格式的文档,其中包含多个字段,每个字段会有自己的倒排索引
单词词典(Term Dictionary)
B+树实现
倒排列表(Posting List)
倒排列表记录了单词对应的文档集合,有倒排索引项(Posting)组成
倒排索引项主要包含如下信息:
1.文档id用于获取原始信息
2.单词频率(TF,Term Frequency),记录该单词在该文档中出现的次数,用于后续相关性算分
3.位置(Posting),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query),分词所在位置从0开始计算
4.偏移(Offset),记录单词在文档的开始和结束位置,也是从0开始,用于高亮显示
1.文档id用于获取原始信息
2.单词频率(TF,Term Frequency),记录该单词在该文档中出现的次数,用于后续相关性算分
3.位置(Posting),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query),分词所在位置从0开始计算
4.偏移(Offset),记录单词在文档的开始和结束位置,也是从0开始,用于高亮显示
倒排索引不可变的好处
- 不需要锁,提升并发能力,避免锁的问题
- 数据不变,一直保存在os cache中,只要cache内存足够
- filter cache一直驻留在内存,因为数据不变
- 可以压缩,节省cpu和io开销
实战
安装&启动
docker安装es
docker pull elasticsearch:7.4.2
docker pull kibana:7.4.2 可视化检索数据界面,可装可不装
ps:如果访问拒绝则在最前面加上sudo
docker pull kibana:7.4.2 可视化检索数据界面,可装可不装
ps:如果访问拒绝则在最前面加上sudo
创建实例
(sudo)mkdir -p /mydata/elasticsearch/config
(sudo)mkdir -p /mydata/elasticsearch/data
(sudo)echo "http.host:0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
http.host:0.0.0.0表示允许任何服务器端口访问 >>表示写入到某个文件
(sudo)mkdir -p /mydata/elasticsearch/data
(sudo)echo "http.host:0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
http.host:0.0.0.0表示允许任何服务器端口访问 >>表示写入到某个文件
启动并指定参数
1、暴露端口9200用于发送接收请求 9300分布式集群通信端口
2、指定单节点模式
3、重要:指定es初始和最大内存大小,不指定会占用全部内存导致卡死
2、指定单节点模式
3、重要:指定es初始和最大内存大小,不指定会占用全部内存导致卡死
略
增删改查
es主要通过PUT/GET/POST命令发送请求到对应端口,es就会返回相关json数据或者操作
查
查看命令 _cat
GET /_cat/nodes :查看所有节点
GET /_cat/health :查看es的健康状况
GET /_cat/master :查看主节点
GET /_cat/indices :查看所有索引 (类似mysql的show database)
增
索引一个文档(保存)
保存一个数据到某个索引的某个类型下,并可以指定一个唯一标识
保存一个数据到某个索引的某个类型下,并可以指定一个唯一标识
新增 PUT请求/POST请求
POST(主要用于新增)
新增:不带id,或带id之前没数据
修改:带id且之前有数据
不指定id会自动生成id,如果指定id就会修改这个数据并新增版本号
PUT(主要用于修改)
新增:带id之前没数据,因为必须带id导致每次新增要换新id
修改:带id且之前有数据
PUT必须指定id,不指定会保错,一般用于修改操作(更新同一个id)
PUT A/B/1 :在A索引的B类型下保存了一个自定的json同时指定id为1
POST A/B :同理,POST可以不指定id,会自动生成一个
POST A/B :同理,POST可以不指定id,会自动生成一个
返回值
ps:PUT:如果同样的请求发了2次或以上,version++,result从create变为updated,PUT发送多次即为更新操作
POST:如果指定id则和PUT没区别
而如果不指定id,则发送多次都会是新增操作,version不变,result为create不变,id会变另一个随机生成的id
ps:PUT:如果同样的请求发了2次或以上,version++,result从create变为updated,PUT发送多次即为更新操作
POST:如果指定id则和PUT没区别
而如果不指定id,则发送多次都会是新增操作,version不变,result为create不变,id会变另一个随机生成的id
Netty
八股
简介
Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端,同时也是基于NIO(封装了jdk的nio),让我们使用起来更加方法灵活
特点
高并发
Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。
传输快
Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。
封装好
Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口
优势
使用简单
封装了 NIO 的很多细节,使用更简单
功能强大
预置了多种编解码功能,支持多种主流协议,如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议
极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好
定制功能强
可以通过 ChannelHandler 对通信框架进行灵活地扩展。
社区活跃
Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快
稳定
Netty 修复了已经发现的NIO的 bug,让开发人员可以专注于业务本身
经历大型项目考验
很多开源项目都使用到了 Netty, 比如我们经常接触的 Dubbo、RocketMQ 等等
性能高
通过与其他业界主流的 NIO 框架对比,如mina,Grizzly之类,Netty 的综合性能最优
IO 线程模型:同步非阻塞,用最少的资源做更多的事。
内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
串形化处理读写:避免使用锁带来的性能开销
高性能序列化协议:支持 protobuf 等高性能序列化协议
应用场景
RPC 框架的网络通信工具
实现一个自定义的HTTP 服务器(类似tomcat)
即时通讯系统
消息推送系统
NIO基础
1、三大组件
Channel(双向通道,输入输出数据)
FileChannel 文件数据传输通道
DatagramChannel UDP网络数据传输通道
SocketChannel 客户端/服务端TCP网络数据传输通道
ServerSocketChannel 服务端TCP网络数据传输通道
Buffer(缓冲读写数据)
ByteBuffer
MappedByteBuffer
DirectByteBuffer
HeapByteBuffer
使用
1、channel向buffer写入数据(channel.read(buffer))
2、切换到读模式(buffer,flip())
3、从buffer读取数据(buffer.get())
4、切换到写模式(buffer.clear()&buffer.compact())
5、向buffer里写入数据(buffer.put(byte)) (参数可以是byte也可以是byte数组)
结构
1、position(当前修改的位置)
从写切换到读,position会指向0(即从0开始读),limit会指向最后一个写的位置
2、Limit(写入限制)
3、capacity(buffer容量)
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
CharBuffer
Selector(配合一个线程管理多个Channel)
核心组件
Channel
Netty 网络操作(读写等操作)抽象类,包括基本的 I/O 操作,如 bind()、connect()、read()、write()
EventLoop
本质上 是单线程执行器(同时维护了一个selector),里面有run方法处理channel上源源不断的io事件
ps:一个eventloop就是一个线程
功能为 负责监听网络事件并调用事件处理器注册到其上的Channel 进行相关 I/O 操作的处理
ps:一个eventloop就是一个线程
功能为 负责监听网络事件并调用事件处理器注册到其上的Channel 进行相关 I/O 操作的处理
NioEventLoop
EventLoopGroup
EventLoopGroup是一组EventLoop,Channel调用Eventloopgroup的register方法来绑定其中的一个EventLoop
后续这个channel上的io事件都由这个eventloop来处理,保证了io事件处理时的线程安全问题
后续这个channel上的io事件都由这个eventloop来处理,保证了io事件处理时的线程安全问题
NioEventLoopGroup
EventLoopGroup是一个接口,需要具体实现,例如NioEventLoopGroup
功能最全,支持io事件,普通任务,定时任务
功能最全,支持io事件,普通任务,定时任务
NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2
主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
DefaultEventLoopGroup
相比NioEventLoopGroup,不支持io事件,只能处理普通任务和定时任务
学习
helloworld
客户端
服务端
相关解析
channel:数据的通道
msg:流动的数据
handle:数据的处理工序 handle合在一起就是pipeline
pipeline:加工流水线 加工msg
eventloop:处理数据的工人
msg:流动的数据
handle:数据的处理工序 handle合在一起就是pipeline
pipeline:加工流水线 加工msg
eventloop:处理数据的工人
eventloop的基本使用
设计模式
七大原则
开闭原则
对扩展开放,对修改关闭
降低维护带来的新风险
依赖倒置原则
高层不应该依赖低层,要面向接口编程
更利于代码结构的升级扩展
单一职责原则
一个类只干一件事,实现类要单一
便于理解,提高代码的可读性
接口隔离原则
一个接口只干一件事,接口要精简单一
功能解耦,高聚合、低耦合
迪米特法则
不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度
只和朋友交流,不和陌生人说话,减少代码臃肿
里氏替换原则
不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义
防止继承泛滥
合成复用原则
尽量使用组合或者聚合关系实现代码复用,少使用继承
降低代码耦合
GoF的23种设计模式
创建型模式
单例模式:Bean默认为单例模式
应用:工具类、共享数据、单例线程池
应用:工具类、共享数据、单例线程池
饿汉式
懒汉式
双重校验锁
静态内部类
枚举类
最优
工厂模式:BeanFactory
简单工厂
只有一个具体的工厂类,非接口或抽象方法,getinstance获取实例时,通过if-else或switch来判断new出的是接口对应的哪个实例
工厂方法
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行
抽象工厂:声明了工厂方法的接口。
具体产品工厂:实现工厂方法的接口,负责创建产品对象。
产品抽象类或接口:定义工厂方法所创建的产品对象的接口。
具体产品实现:具有统一父类的具体类型的产品。
具体产品工厂:实现工厂方法的接口,负责创建产品对象。
产品抽象类或接口:定义工厂方法所创建的产品对象的接口。
具体产品实现:具有统一父类的具体类型的产品。
进程切换和线程切换的过程
抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
产品族难扩展,产品等级易扩展
建造者模式
一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。
产品的组成部分是不变的,但每一部分是可以灵活选择的
产品的组成部分是不变的,但每一部分是可以灵活选择的
指挥者
产品
抽象建造者
具体建造者
建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用
原型模式
当存在大量相同或相似对象的创建问题,用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
结构型模式
代理模式
静态代理
两种实现方法
基于继承: 被代理类不需要实现接口,代理类继承被代理类,扩展方法
缺点:会产生大量的代理类
优点:被代理类和代理类均不需要实现接口
基于接口:被代理类和代理类需要实现相同接口,代理类接口属性在实例化时候需要传入对应被代理的实例
缺点:被代理的类必须实现接口
优点:可以代理所有实现接口的类
动态代理
实现方法
JDK动态代理:通过java提供的Proxy类帮我们创建代理对象
缺点:JDK反射生成代理必须面向接口, 这是由Proxy的内部实现决定的。生成代理的方法中你必须指定实现类的接口,它根据这个接口来实现代理类生成的所实现的接口
优点:可以生成所有实现接口的代理对象
CGLIB动态代理:cglib生成代理是被代理对象的子类
需要引入外部依赖
Spring AOP:底层使用cglib
(对象)适配器模式
桥接模式
装饰模式
外观模式
享元模式
组合模式
行为型模式
策略模式
命令模式
责任链模式
介绍:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止
应用:
JDK的java.util.logging.Logger#log()和javax.servlet.Filter#doFilter()
Spring Security 使用责任链模式,可以动态地添加或删除责任(处理 request 请求)
Spring AOP 通过责任链模式来管理 Advisor、
Netty 中的 Pipeline 和 ChannelHandler 通过责任链设计模式来组织代码逻辑
Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候做一些操作
JDK的java.util.logging.Logger#log()和javax.servlet.Filter#doFilter()
Spring Security 使用责任链模式,可以动态地添加或删除责任(处理 request 请求)
Spring AOP 通过责任链模式来管理 Advisor、
Netty 中的 Pipeline 和 ChannelHandler 通过责任链设计模式来组织代码逻辑
Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候做一些操作
状态模式
观察者模式(Spring中listener的实现:ApplicationListener):定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,有时又称作发布-订阅模式、模型-视图模式
中介者模式(MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;QQ 聊天程序的“中介者”是 QQ 服务器)定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
迭代器模式
访问者模式
备忘录模式
JDK里用到的设计模式
工厂模式
java.lang.Proxy#newProxyInstance()
java.lang.Object#toString()
java.lang.Class#newInstance()
java.lang.Class#forName()
...
责任链模式:
java.util.logging.Logger#log()
javax.servlet.Filter#doFilter()
...
操作系统
进程和线程
介绍
什么是进程?
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
什么是线程?
一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
Java 中,线程作为小调度单位,进程作为资源分配的小单位。 在 windows 中进程是不活动的,只是作为线程的容器
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
Java 中,线程作为小调度单位,进程作为资源分配的小单位。 在 windows 中进程是不活动的,只是作为线程的容器
进程和线程的区别
1.进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
2.每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;
而线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
2.每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;
而线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
线程和协程的区别
线程每次创建和销毁都要调用操作系统,是很昂贵的资源
而协程就像是用户态的轻量级线程,协程的创建、切换发生在用户态。是一个在线程的基础上,针对某些应用场景发展出来的功能,由编程语言直接创建
协程按照组织好的代码流程,并发地执行一些操作,代替一个线程,虽然时间上慢几毫秒,但省了创建线程过程
而协程就像是用户态的轻量级线程,协程的创建、切换发生在用户态。是一个在线程的基础上,针对某些应用场景发展出来的功能,由编程语言直接创建
协程按照组织好的代码流程,并发地执行一些操作,代替一个线程,虽然时间上慢几毫秒,但省了创建线程过程
协程应用场景:线程池
切换
进程间切换
第1步、进程地址空间切换
切换页目录以使用新的地址空间
地址空间切换主要是针对用户进程而言
地址空间切换主要是针对用户进程而言
第2步、处理器状态切换
主要为切换内核栈和硬件上下文
处理器状态切换对应于所有的调度单位
处理器状态切换对应于所有的调度单位
为什么进程间切换很慢 因为切换虚拟地址空间后页面缓存失效,内存访问低效
每个进程对应一块虚拟地址空间,虚拟地址空间内有一个页表(页目录)记录虚拟地址空间对应的物理地址空间
把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用一个叫TLB的Cache来缓
存常用的地址映射,这样可以加速页表查找。
一旦切换进程后,页表也要切换,导致已缓存的TLB全部失效,缓存命中率降低,虚拟地址查找页表对应物理地址
变慢,导致内存的访问在一段时间内相当的低效,因此进程间切换很慢
每个进程对应一块虚拟地址空间,虚拟地址空间内有一个页表(页目录)记录虚拟地址空间对应的物理地址空间
把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用一个叫TLB的Cache来缓
存常用的地址映射,这样可以加速页表查找。
一旦切换进程后,页表也要切换,导致已缓存的TLB全部失效,缓存命中率降低,虚拟地址查找页表对应物理地址
变慢,导致内存的访问在一段时间内相当的低效,因此进程间切换很慢
就绪态、运行态、阻塞态(待补充)
线程间切换
时间片
系统调度
抢占式调度(java线程切换模式)
指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞
Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间
Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间
协同式调度
指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
线程主动让出cpu
1、当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
2、当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上
3、线程执行完成,例如执行完run()里面方法
上下文切换
CPU切换前把当前任务的状态保存下来,比如CPU的所有寄存器中的值、进程的状态以及堆栈上的内容,以便下次切换回这个任务时可以再次加载这个任务的状态,然后加载下一任务的状态并执行。任务的状态保存及再加载, 这段过程就叫做上下文切换。
1、挂起当前任务(线程/进程),将这个任务在 CPU 中的状态(上下文)存储于内存中的某处
2、恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复
3、跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中
2、恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复
3、跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中
为什么引起上下文切换
时间片用完
当前执行任务(线程)的时间片用完之后,系统CPU正常调度下一个任务
中断(硬件中断、软件中断)
中断处理,在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断和软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。
用户态切换
抢占锁资源
多个任务抢占锁资源,在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换
区别
1、进程间切换需要切换虚拟地址空间,而线程间切换不用
这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
2、进程的切换会扰乱处理器的缓存机制,让处理器已经缓存的内存地址全部失效,同时虚拟地址空间的改变导致TLB被刷新,缓存命中率降低
而线程切换没有这些问题
而线程切换没有这些问题
通信
进程间通信
匿名管道(pipe)
用于具有亲缘关系的父子进程间或者兄弟进程之间的通信
比如:ls | grep 1 就是将ls的输出结果作为grep 1的输入,实现进程间通信
有名管道
有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信
信号(signal)
比如按ctrl+c会发送(2)SIGINT、kill pid会发送(15)SIGTERM(中断信号)结果为Terminated、kill -9 pid会发送(9)SIGKILL结果为killed
信号量
计数器
共享内存
每个进程都有一个虚拟地址到物理地址的映射,一般情况虚拟地址可能相同,但物理地址不同
这时将物理地址改为相同就可以共同访问同一块内存,利用共享内存实现进程间通信
这时将物理地址改为相同就可以共同访问同一块内存,利用共享内存实现进程间通信
消息队列
内核创建了一个消息队列,进程可以对其发送或接收数据
套接字(socket)
支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点
操作系统线程间通信
操作系统线程间同步
临界区(CriticalSection)
事件(Event)
互斥量(Mutex)
信号量(Semphore)
共享内存
套接字
进程调度算法
操作系统管理了系统的有限资源,当有多个进程(或多个进程发出的请求)要使用这些资源时,因为资源的有限性,必须按照一定的原则选择进程(请求)来占用资源。这就是调度。目的是控制资源使用者的数量,选取资源使用者许可占用资源或占用资源
非抢占式
1、最短工作优先(SJF)
2、最短剩余时间优先(SRTF)
抢占式
3、最高响应比优先(HRRF)
4、优先级调度(Priority)
5、轮转调度(RR)
线程消耗哪些资源,要多少开销?
时间上,创建线程需要分配内存和列入调度,切换线程时将会内存换页和cpu缓存会被清空,
在切换回来时,又需要重新从内存中读取信息,破坏了数据的局部性
在切换回来时,又需要重新从内存中读取信息,破坏了数据的局部性
空间上,线程所占用空间一般不受Java程序控制,而受系统资源限制。一般分配给其1MB堆栈空间
死锁
产生死锁的条件:资源互斥、请求与保持、不可剥夺、循环等待
避免死锁
银行家算法
解除死锁
资源剥夺法。 挂起某些死锁进程,抢占它资源,分配给其他的死锁进程
进程回退法。 让一部分进程回退到足以避免死锁的地步,进程回退时资源被释放而不是被剥夺
进程撤销法。 强制撤销并剥夺一部分进程的资源
面试题
1、僵尸进程和孤儿进程
产生原因
一般进程
子进程由父进程创建,子进程再创建新的进程。父子进程是一个异步过程,父进程永远无法预测子进程的结束,所以,当子进程结束后,它的父进程会调用wait()或waitpid()取得子进程的终止状态,回收掉子进程的资源。
僵尸进程
子进程退出了,但是父进程没有用wait或waitpid去获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵尸进程
孤儿进程
父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程(father died)。子进程的资源由init进程(进程号PID = 1)回收
问题危害
前提:unix提供了一种机制保证父进程知道子进程结束时的状态信息
在每个进程退出的时候,内核会释放所有的资源,包括打开的文件,占用的内存等。
但是仍保留一部分信息(进程号PID,退出状态,运行时间等)。直到父进程通过wait或waitpid来取时才释放
但是仍保留一部分信息(进程号PID,退出状态,运行时间等)。直到父进程通过wait或waitpid来取时才释放
导致:如果父进程不调用wait或waitpid的话,那么保留的信息就不会被释放,其进程号就会被一直占用,但是系统所能使用的进程号是有限的,如果大量产生僵死进程,将因没有可用的进程号而导致系统无法产生新的进程,这就是僵尸进程的危害
孤儿线程没什么危害
孤儿线程没什么危害
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程的数据结构,等待父进程去处理。如果父进程在子进程exit()之后,没有及时处理,出现僵尸进程,并可以用ps命令去查看,它的状态是“Z”。
解决方案
1、通过信号机制,在处理函数中调用wait,回收资源
通过信号机制,子进程退出时向父进程发送SIGCHLD信号,父进程调用signal(SIGCHLD,sig_child)去处理SIGCHLD信号,在信号处理函数sig_child()中调用wait进行处理僵尸进程。什么时候得到子进程信号就什么时候进行信号处理,父进程可以继续执行别的任务,不用去阻塞等待。
2、kill杀死元凶父进程(一般不用)
严格的说,僵尸进程并不是问题的根源,罪魁祸首是产生大量僵死进程的父进程。因此,我们可以直接除掉元凶,通过kill发送SIGTERM或者SIGKILL信号。元凶死后,僵尸进程进程变成孤儿进程,由init充当父进程,并回收资源。
3、父进程用wait或waitpid去回收资源(方案不好)
父进程通过wait或waitpid等函数去等待子进程结束,但是不好,会导致父进程一直等待被挂起,相当于一个进程在干活,没有起到多进程的作用。
内存管理
内存管理机制
连续分配
块式管理
会浪费空间,会有碎片
非连续分配
段式管理
分段是从程序运行逻辑的角度划分,段都是有实际意义的信息
例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址
例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址
页式管理
分页是建立在分段的基础上,继续把段内的内存划分,页式管理通过页表对应逻辑地址和物理地址
段页式管理
把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。有段表+页表
段和页区别
页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
快表和多级页表
快表相当于页表的cache,原本需要两次访问内存,现在只要一次cache一次内存
多级页表的主要目的是避免把全部页表一直放在内存中占用过多空间
虚拟内存
32位操作系统会为每个进程分配多大的内存空间?
4G,因为32位cpu的寻址空间最多4G,因此32位的cpu或操作系统都最多支持4G内存
4G,因为32位cpu的寻址空间最多4G,因此32位的cpu或操作系统都最多支持4G内存
64位的话就是2^64bit的寻址空间,数值远远大于1亿GB
虚拟内存实现
请求分页管理
建立在分页管理之上,为了支持虚拟内存功能而增加了请求调页功能和页面置换功能
请求分段管理
请求段页式管理
局部性原理,保证了虚拟内存不至于效率太低
页面置换算法
OPT(不可能实现,只作为目标)
FIFO
LRU
LFU
CPU的工作状态
内核态(管态)
系统中既有操作系统的程序,也由普通用户的程序。为了安全和稳定性操作系统的程序不能随便访问,这就是内核态
内核态可以使用所有的硬件资源
内核态可以使用所有的硬件资源
用户态(目态)
不能直接使用系统资源,也不能改变CPU的工作状态,并且只能访问这个用户程序自己的存储空间
(Linux)特权级别
分为Ring0(内核态)、Ring1、Ring2、Ring3(用户态),每种特权等级可以使用不同的指令集合
Ring设计的初衷是将系统权限与程序分离出来,使之能够让OS更好的管理当前系统资源,也使得系统更加稳定。
内核态运行在R0特权级别上,可以使用特权指令,控制中断、修改页表、访问设备等等
特权级别决定现在是内核态还是用户态
Ring设计的初衷是将系统权限与程序分离出来,使之能够让OS更好的管理当前系统资源,也使得系统更加稳定。
内核态运行在R0特权级别上,可以使用特权指令,控制中断、修改页表、访问设备等等
特权级别决定现在是内核态还是用户态
工作状态的切换
用户态——>内核态
唯一途径是通过中断、异常、陷入机制(访管指令)
1、系统调用(用户主动切换为内核态)
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如linux中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如linux中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断
2、异常(被动)
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
3、外围设备的中断(被动)
外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到 内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到 内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
内核态——>用户态
设置程序状态字PSW
PS:程序状态字(Program Status Word, PSW)又称状态寄存器,主要用于反映处理器的状态及某些计算结果以及控制指令的执行。
内核态与用户态区别
1、内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;当程序运行在0级特权级上时,就可以称之为运行在内核态。
2、处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ;
处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
零拷贝
拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽
优点:减少CPU和内存的占用
应用场景:RocketMQ持久化时用的是mmap+write()
DMA直接内存访问硬件
在内存到硬件的直接工作
读:DMA会把硬盘数据拷贝缓冲区
写:DMA将缓冲区的数据拷贝到网卡
写:DMA将缓冲区的数据拷贝到网卡
传统I/O
4次用户空间与内核空间的上下文切换+2次CPU拷贝和2次DMA拷贝
通过mmap实现的零拷贝I/O
mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。
mmap就是把内核空间的读缓冲区与用户空间的缓冲区映射到同一物理地址,
用户空间与内核空间读缓冲区共享着物理内存。写的话要读了之后拷贝到socket
mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。
mmap就是把内核空间的读缓冲区与用户空间的缓冲区映射到同一物理地址,
用户空间与内核空间读缓冲区共享着物理内存。写的话要读了之后拷贝到socket
4次用户空间与内核空间的上下文切换+1次CPU拷贝和2次DMA拷贝
通过sendfile实现的零拷贝I/O
2次用户空间与内核空间的上下文切换+1次CPU拷贝和2次DMA拷贝
带有DMA收集拷贝功能的sendfile实现的I/O
2次用户空间与内核空间的上下文切换+1次DMA拷贝和1次DMA gather拷贝
"传统I/O” VS “sendfile零拷贝I/O”
零拷贝第二版
出现的背景
磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存 10 倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接 I/O、异步 I/O 等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效的减少磁盘的访问次数(减少上下文切换次数)。
未引入DMA技术的传统IO
整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且在这个过程中CPU 是不能做其他事情的。
简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来
简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来
DMA( 直接内存访问 Direct Memory Access)
在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。
引入DMA技术后的传统IO
整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,
但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。
但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。
从文件传输中看零拷贝
传统IO下的文件传输
传统IO的工作方式:数据读取和写入是从用户空间到内核空间来回复制,
而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入
而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入
文件传输:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统IO下的一次文件传输:
4 次用户态与内核态的上下文切换:两次系统调用:read()、write():
每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态
4 次数据拷贝,其中2次是DMA的拷贝,2次CPU 拷贝
4 次用户态与内核态的上下文切换:两次系统调用:read()、write():
每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态
4 次数据拷贝,其中2次是DMA的拷贝,2次CPU 拷贝
传统IO下的一次文件传输,原本只搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能
简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能
所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能
所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
系统调用会导致上下文切换,所以要尽可能减少系统调用的次数
因为文件传输的应用场景中,在用户空间并不会对数据「再加工」,
所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的
去掉用户缓冲区可以减少内存拷贝的次数
所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的
去掉用户缓冲区可以减少内存拷贝的次数
零拷贝下的文件传输
mmap+write
过程解析
1、把read()系统调用替换成mmap()系统调用,mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间
应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区
应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区
2、应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
3、最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。
这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作,相比传统IO少了一次CPU拷贝,因为直接由内核空间的缓冲区搬运到socket缓冲区
但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。
但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。
4次上下文切换+3次拷贝(2次DMA拷贝和1次CPU拷贝)
sendfile
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile()
它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销
它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销
2次上下文切换+3次拷贝(2次DMA拷贝和1次CPU拷贝)
直观对比
传统文件传输 4次上下文切换+4次拷贝(2次DMA+2次CPU)
mmap+write 4次上下文切换+3次拷贝(2次DMA+1次CPU)
sendfile 2次上下文切换+3次拷贝(2次DMA+1次CPU)
Linux常用命令
文件 touch rm [-rf] vi/vim cat最后一屏/more百分比显示/less翻页查看/tail
目录 mkdir rm [-rf] mv(剪切)/cp(拷贝) find cd(目录切换) ls(目录查看)
查看tcp连接状态: netstat -napt
进程 ps aux | grep redis
ps所有进程,aux运行中的,grep xxx搜索xxx的结果
杀进程 kill -9 进程pid
-9:强制
查看第几行/一些行
sed -n '3,9' filename
显示第3行到第9行的内容
压缩 tar -zcvf
grep 查找文件里符合条件的字符串
top Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器
chmod 改变权限,-R是目录下所有文件,777就是高权限(读、写、执行)
chmod -R 777 * 意思就是将当前目录下所有文件都给予777权限
可能会带来巨大的安全风险,建议如果你的Web服务器遇到权限问题,请将文件的所有权更改为运行应用程序的用户
并将文件的权限设置为644,将目录的权限设置为755,而不是递归地将权限设置为777
chmod -R 777 * 意思就是将当前目录下所有文件都给予777权限
可能会带来巨大的安全风险,建议如果你的Web服务器遇到权限问题,请将文件的所有权更改为运行应用程序的用户
并将文件的权限设置为644,将目录的权限设置为755,而不是递归地将权限设置为777
网络
输入url到显示界面,经历了什么?用了什么协议?
1.DNS解析并返回ip地址(DNS
2.tcp三次握手建立连接(TCP、IP、OSPF、ARP
3.浏览器发送http请求(HTTP
4.服务器处理请求并返回http报文
5.浏览器渲染数据并显示
6.tcp四次挥手断开连接
2.tcp三次握手建立连接(TCP、IP、OSPF、ARP
3.浏览器发送http请求(HTTP
4.服务器处理请求并返回http报文
5.浏览器渲染数据并显示
6.tcp四次挥手断开连接
OSPF(Open Shortest Path First,ospf)开放最短路径优先协议,是由Internet工程任务组开发的路由选择协议
ARP
1、游览器向DNS请求解析并返回ip地址
1、游览器检查自己缓存
游览器搜索自己的缓存有没有被解析过的这个域名对应的ip地址,如果有,解析结束。同时域名被缓存的时间也可通过TTL属性来设置
2、检查操作系统的缓存
Windows DNS缓存的默认值是 MaxCacheTTL,默认值是86400s,一天
3、检查操作系统里的host文件
如果在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址
存在问题:域名劫持 这种操作系统级别的域名解析规程也被很多黑客利用,通过修改hosts文件里的内容把特定的域名解
析到指定的ip地址上,造成域名劫持。所以在windows7中将hosts文件设置成了readonly,防止被恶意篡改
析到指定的ip地址上,造成域名劫持。所以在windows7中将hosts文件设置成了readonly,防止被恶意篡改
4、请求本地域名服务器(LDNS)来解析这个域名
这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了
5、跳转root server进行查询
1、如果LDNS仍然没有命中,LDNS就直接去Root Server 域名服务器请求解析
2、根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如.com .cn .org等)地址
(根域名服务器告诉LDNS一个gTLD地址,让LDNS去gTLD里查)
(根域名服务器告诉LDNS一个gTLD地址,让LDNS去gTLD里查)
3、此时LDNS再发送请求给上一步返回的gTLD
(LDNS向gTLD发起查询)
(LDNS向gTLD发起查询)
4、接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器
5、Name Server根据映射关系表找到目标ip,返回给LDNS
6、LDNS把gTLD解析的结果返回给用户,同时缓存这个域名和对应的ip,
用户根据TTL值缓存到操作系统缓存中
用户根据TTL值缓存到操作系统缓存中
7、操作系统缓存后,返回IP到游览器,域名解析过程结束
2、三次握手建立TCP连接
3、游览器发起HTTP请求
4、服务器接受并解析HTTP请求,
查找指定资源,并返回HTTP响应消息
查找指定资源,并返回HTTP响应消息
5、客户端解析html代码,并请求html代码中的资源
6、客户端渲染展示页面
7、四次挥手关闭TCP连接
OSI七层/五层协议
应用层:通过应用进程间的交互来完成特定网络应用
运输层:负责向两台主机进程之间的通信提供通用的数据传输服务
网络层:在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送
数据链路层:两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧
物理层:实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异
dns解析
查找过程:本地dns -> 根dns -> 主dns -> 下级dns(递归查找) -> 已找到就缓存到本地
dns协议同时利用了tcp和udp
HTTP
http和https区别
http明文传输,https密文传输
https要用CA证书验证
http响应更快,因为https除了tcp还需要ssl(或tls)连接
http在tcp端口80,https为443
https加密机制
非对称&对称加密
定义
非对称加密
速度慢,只适合加密少量数据。私钥自己保存,公钥可随意分发。
密钥包括:公钥和私钥。如公钥加密,则私钥解密;如私钥加密,则公钥解密。
常用加密算法:RSA
对称加密
加密速度快,适合大量数据的处理。但密钥的管理和分发安全性要求高。
加密和解密使用相同的密钥
常用加密算法:DES,3DES
报文摘要
摘要的计算是单向的,其不是加密技术。
通过哈希(散列)函数对不同长度的报文计算出相同长度的特征值。报文如有修改,则特征值会变化。
常用签名算法:MD5,SHA-1,SHA-256
使用
1、网站拥有用于非对称加密的公钥A、私钥A
2‘浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器。
3、浏览器随机生成一个用于对称加密的密钥X,用公钥A加密后传给服务器。
4、服务器拿到后用私钥A’解密得到密钥X。
5、这样双方就都拥有密钥X了,且别人无法知道它。之后双方所有数据都通过密钥X加密解密即可。
2‘浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器。
3、浏览器随机生成一个用于对称加密的密钥X,用公钥A加密后传给服务器。
4、服务器拿到后用私钥A’解密得到密钥X。
5、这样双方就都拥有密钥X了,且别人无法知道它。之后双方所有数据都通过密钥X加密解密即可。
漏洞
中间人攻击
1、某网站有用于非对称加密的公钥A、私钥A。
2、浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器。
3、中间人劫持到公钥A,保存下来,把数据包中的公钥A替换成自己伪造的公钥B(它当然也拥有公钥B对应的私钥B)。
4、浏览器生成一个用于对称加密的密钥X,用公钥B(浏览器无法得知公钥被替换了)加密后传给服务器。
5、中间人劫持后用私钥B解密得到密钥X,再用公钥A加密后传给服务器。服务器拿到后用私钥A’解密得到密钥X
2、浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器。
3、中间人劫持到公钥A,保存下来,把数据包中的公钥A替换成自己伪造的公钥B(它当然也拥有公钥B对应的私钥B)。
4、浏览器生成一个用于对称加密的密钥X,用公钥B(浏览器无法得知公钥被替换了)加密后传给服务器。
5、中间人劫持后用私钥B解密得到密钥X,再用公钥A加密后传给服务器。服务器拿到后用私钥A’解密得到密钥X
如何防止中间人攻击
CA证书,确保服务端传的公钥A和游览器收到的公钥是同一个
整个非对称加密过程都是为了最后的对称加密服务的,最终目的是证明证书中的公钥是安全且未篡改。
使用公钥来加密一个秘钥,并把秘钥传给后端,后端使用私钥解密秘钥,这样两端都拥有同一个秘钥,从而进行对称加密
使用公钥来加密一个秘钥,并把秘钥传给后端,后端使用私钥解密秘钥,这样两端都拥有同一个秘钥,从而进行对称加密
当建立好链接后,客户端发送加密套件列表,看服务器支持哪种算法。
服务器发送匹配算法,然后发送第二个报文(数字证书->包括CA认证,公钥,域信息,过期时间等)
客户端验证数字证书,如果通过就随机生成一个对称加密算法的密钥。然后使用公钥加密这密钥。发送给服务器
服务器用私钥解密得到密钥。它们就可以通过密钥来加密传输数据
服务器发送匹配算法,然后发送第二个报文(数字证书->包括CA认证,公钥,域信息,过期时间等)
客户端验证数字证书,如果通过就随机生成一个对称加密算法的密钥。然后使用公钥加密这密钥。发送给服务器
服务器用私钥解密得到密钥。它们就可以通过密钥来加密传输数据
CA证书
证书内容:颁发机构信息+公钥+公司信息+域名+有效期+指纹
如何验证合法性:验证域名和有效期等信息是否正确+判断证书来源是否合法+判断证书是否被篡改+判断证书是否已吊销
签名算法&签名哈希算法
签名哈希算法又称指纹算法,通过对比证书的hash值和传递的哈希值确认证书的安全性
签名算法用于加密签名哈希算法,防止证书内的hash值被修改
CA证书认证流程
SSL
SSL记录协议
SSL握手协议
SSL警报协议
http1.0&1.1
长连接
在一个TCP连接上可以传送多个HTTP请求和响应,为了减少建立和关闭的消耗和延迟
节约宽带
只传输头部信息,若有权限继续传输body
host域
可存在多个虚拟主机共享一个IP地址
缓存处理
引入了更多的缓存控制策略
http1.1&2.0
多路复用
一个连接处理多个请求
头部压缩
HPACK算法压缩header信息
服务器推送
当客户端请求资源时,服务器把多个资源都传输给你
二进制协议
http2.0&3.0
放弃使用TCP协议,而使用基于UDP的QUIC协议
HTTP的报文格式
请求报文
响应报文
Cookie和Session和Token
Token
服务端生成的一串字符串,以作客户端进行请求的一个令牌
减少服务器和数据库查询压力
用设备号/设备mac地址作为Token(推荐)
用session值作为Token
cookie和session区别
Session是在服务端保存的一个数据结构,用来标识用户和跟踪用户的状态,这个数据可以保存在集群、数据库、文件中
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式( Cookie 里面记录一个Session ID)
如果cookie被禁用,session无法标识,则会用url重写方式,在url链接里带上sid=xxxx
如果cookie被禁用,session无法标识,则会用url重写方式,在url链接里带上sid=xxxx
对头阻塞
在tcp链接中,http请求必须等待前一个请求响应之后,才能发送,后面的依次类推,由此可以看出,如果在一个tcp通道中如果某个http请求的响应因为某个原因没有及时返回,后面的响应会被阻塞
Http报文首部
请求报文
响应报文
DHCP
TCP和UDP
TCP(传输控制协议)
TCP报文首部
源端口和目的端口
告诉 TCP 协议应该把报文发给哪个进程,最大端口数目为65535(2的16次方)
(哪个进程在侦听这个端口,就发哪个进程)
(哪个进程在侦听这个端口,就发哪个进程)
序号
第一个报文的序号在第一次交互时由系统随机生成
变化过程:初始值+偏移量即为下一个报文的序号值
变化过程:初始值+偏移量即为下一个报文的序号值
确认号
数据被接收后,接收端给发送端回馈确认的机制。
若接收端接收到2000,则回复2001。
还能够处理重复的报文段,一旦接收到相同的序号就丢弃
若接收端接收到2000,则回复2001。
还能够处理重复的报文段,一旦接收到相同的序号就丢弃
数据偏移
头部长度
保留
占6位,保留为今后使用,目前应设置为0
控制位
紧急数据URG
当UGR置1时,发送应用进程就告诉发送方的TCP有紧急数据要传送。于是发送方的TCP就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据
确认报文ACK
确认报文段,仅当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。
尽快推送PSH
当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方响应。在这种情况下,TCP可以使用PSUH(推送操作)。这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去。接收方TCP收到PSH=1的报文段,就尽快(推送)交付给接收应用进程,而不在等整个缓存都填满了再向上交付
差错释放RST
当RST=1时,表明TCP连接出现了严重差错,必须释放连接,然后重新建立新运输连接。**RST=1还可以用来拒接一个非法报文段或拒绝打开一个连接
例如:0窗窗口探测3次都无ack返回,time_wait状态结束
例如:0窗窗口探测3次都无ack返回,time_wait状态结束
同步信号SYN
在建立连接时用来同步序号。当SYN=1,ACK=0时,表明这是一个连接请求报文段;对方若同意连接,则应在相应的报文段中使SYN=1,ACK=1。因此SYN置1就表示这是一个连接请求或连接接受报文段。
终止信号FIN
用来释放一个连接,当FIN=1时,表明此报文段的发送方数据已经发送完毕,并要求释放运输连接。
窗口
校验和
紧急指针
三次握手
为什么握手要三次
1、序列号
TCP的可靠连接是靠 seq( sequence numbers 序列号)来达成的,TCP 设计中一个基本设定就是:通过TCP 连接发送的每一个包,都有一个sequence number。而因为每个包都是有序列号的,所以都能被对方确认收到这些包
三次握手时ACK丢失怎么办
四次挥手
为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
TIME_WAIT
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可能最后一个ACK丢失。
如果ACK丢失,可以在TIME_WAIT状态的2MSL内,等对方再次发送FIN,然后重新给对方发ACK
PS:2MSL中,MSL是报文最大生存时间,可以自定义MSL为30秒,1分钟,2分钟,默认为2分钟,2MSL是指2倍MSL
如果ACK丢失,可以在TIME_WAIT状态的2MSL内,等对方再次发送FIN,然后重新给对方发ACK
PS:2MSL中,MSL是报文最大生存时间,可以自定义MSL为30秒,1分钟,2分钟,默认为2分钟,2MSL是指2倍MSL
出现大量time_wait会发生什么?怎么办?
time_wait状态结束后才会释放端口,当并发请求过多无法及时断开的话,会占用大量的端口资源和服务器资源
内核里有两个hashtable:一个既包含time_wait状态的连接,也包含其他状态的连接。不同内核的hashtable大小设置不同
另一个用来保存所有的bound ports,用于遍历找到一个可用端口或者随机端口,占用CPU
不过占用内存很少很少。 一个tcp socket占用不到4k。1万条time_wait的连接,也就多消耗1M左右的内存
另一个用来保存所有的bound ports,用于遍历找到一个可用端口或者随机端口,占用CPU
不过占用内存很少很少。 一个tcp socket占用不到4k。1万条time_wait的连接,也就多消耗1M左右的内存
解决:优化TCP/IP 的内核参数(有难度和风险),及时将time_wait状态的端口清理掉
1、客户端改用长连接
需要客户端的改动比较大,但能彻底解决问题,高并发的场景下,长连接的性能也明显好于短连接。
需要客户端的改动比较大,但能彻底解决问题,高并发的场景下,长连接的性能也明显好于短连接。
2、修改linux内核减小MSL时间
能够降低出问题的概率,需要修改linux内核,难度和风险都较大。
能够降低出问题的概率,需要修改linux内核,难度和风险都较大。
为什么一定要time_wait?
可靠的实现TCP全双工连接的终止
客户端发送最后一次ACK之后,自身进入time_wait状态,如果ack在网络中丢失
则服务端将再次发送FIN报文,如果没超过2MSL,则客户端重发ACK,如果超过
2MSL,接受FIN的时候客户端已经关闭,则客户端发送RST,服务端收到后认为该连接出现异常
则服务端将再次发送FIN报文,如果没超过2MSL,则客户端重发ACK,如果超过
2MSL,接受FIN的时候客户端已经关闭,则客户端发送RST,服务端收到后认为该连接出现异常
允许老的重复节点在网络中消逝
如果刚关闭连接就立刻建立新连接,可能会出现上一个连接发送较慢的
数据包被新连接接收,破坏了新连接
数据包被新连接接收,破坏了新连接
tcp如何保证可靠传输
校验和
TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段
流量控制
(被接收方要求发送方降低速率
(被接收方要求发送方降低速率
TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。(窗口字段包含在确认报文中,为0的时候表示不能发送数据)
滑动窗口协议详解
滑动窗口中,窗口关闭导致的死锁问题,用0窗口探测解决
发送方的窗口的上限值应当取为接收方窗口rwnd和拥塞窗口cwnd这两个变量中较小的一个。
即发送方窗口的上限值 = Min [rwnd ,cwnd],谁更小就是谁在限制发送方窗口的最大值
即发送方窗口的上限值 = Min [rwnd ,cwnd],谁更小就是谁在限制发送方窗口的最大值
拥塞控制
(被网络太拥堵要求发送方降低速率
(被网络太拥堵要求发送方降低速率
当网络拥塞时,减少数据的发送。
慢开始
一开始立即把大量数据字节注入到网络,可能会引起网络阻塞,因为现在还不知道网络的符合情况
经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口
经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口
cwnd初始值为1,每经过一个传播轮次,cwnd加倍
拥塞避免
每经过一个往返时间就把发送方的cwnd加1
cwnd>=门限就会触发拥塞发生算法
拥塞发生算法的两种触发方式和算法
超时重传
门限减半,起点为1,然后执行慢开始
快速重传
门限减半,起点减半
通过快重传引起的拥塞发生算法才会进入快恢复
快重传是啥
快重传算法首先要求接收方每收到一个失序的报文段就立即发出重复确认(为的是使发送方及早的知道有报文段没有到达对方)而不要等到自己发送数据时才捎带确认。
快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待为其设置的重传计时器到期。
快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待为其设置的重传计时器到期。
1、为什么要设置为3个重复确认
概率问题
TCP按序发送,但TCP包是封装在IP包内,IP包在传输时乱序,意味着TCP包到达接收端也是乱序
TCP按序发送,但TCP包是封装在IP包内,IP包在传输时乱序,意味着TCP包到达接收端也是乱序
2、重传的时候,是只重传丢失的报文,还是重传在重复确认时发送的所有报文
SACK
快恢复
快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈
在门限减半和起点减半之后,cwnd=门限+3( 3 的意思是确认有 3 个数据包被收到了),并进入拥塞避免
ARQ协议(自动重传请求协议
停止等待ARQ协议
它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送
如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送
优点: 简单
缺点: 信道利用率低,等待时间长
缺点: 信道利用率低,等待时间长
超时重传机制
当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段
确认丢失机制
A向B发送消息,B收到后给A发确认,但是确认丢失了,但A不知道,超时后A就继续发消息
这时B会做两件事 1.把A重发的消息丢弃 2.再次向A发确认消息
这时B会做两件事 1.把A重发的消息丢弃 2.再次向A发确认消息
确认迟到机制
A向B发送消息,B收到后给A发确认,但确认迟到,但A不知道,超时后A就继续发消息
B又会收到重复消息,又发了个确认。结果是A发了两次消息,B发了两次确认
处理如下:1.A丢弃第二次确认消息 2.B丢弃第二次收到的消息
B又会收到重复消息,又发了个确认。结果是A发了两次消息,B发了两次确认
处理如下:1.A丢弃第二次确认消息 2.B丢弃第二次收到的消息
连续ARQ协议
连续 ARQ 协议 可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了
优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。
缺点: 不能及时向发送方反映出接收方已经正确收到的所有分组的信息。
缺点: 不能及时向发送方反映出接收方已经正确收到的所有分组的信息。
粘包&拆包
发生原因
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包
解决方案
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开
UDP(用户数据报协议)报文首部
tcp和udp区别
TCP面向连接,UDP无连接
三挥四握
TCP可靠,UDP不可靠
三挥四握,确认、窗口、重传、流量阻塞控制等
TCP传输慢,UDP传输快
TCP占用资源多,UDP占用资源少
IP
IP报文首部
IP(网络层)和MAC(数据链路层)的区别
MAC 的作用则是实现「直连」的两个设备之间通信
IP 则负责在「没有直连」的两个网络之间进行通信传输。
IP 则负责在「没有直连」的两个网络之间进行通信传输。
I/O模型
阻塞IO
非阻塞IO
IO多路复用
select(windows/linux)
fd:file description 文件描述符。fds:存放fd的数组。
虚线以上内容:创建socket服务端+fd+fds
select(max+1, &rset, NULL, NULL, NULL)
max:最大fd编号,标记轮询的范围
rset(fd_set):实际上是一个默认1024的bitmap
单线程下多个网络请求被系统(DMA)识别成多个fd,形成fds数组,再将fds数组中的编号存放至fd_set(编号是几,从0开始bitmap从左往右数第几位就是1),并在内核态下检查哪些位置是1,也就是有数据的
bitmap会从用户态放入内核态,对有数据的位置进行置位(做个标记),当有数据时select有返回值,然后回到用户态下的for遍历哪些位置被置位过,并读取该数据。否则没有返回值,select会阻塞
成功调用返回结果大于 0,超时返回结果为 0,出错返回结果为 -1
四个缺点
默认最大1024的bitmap虽然可调大小,但仍旧有上限
fdset不可重用,在内核中被置位了
每次都需要重新将fdset从用户态拷贝进内核态,有一定开销
O(n)再次遍历问题。因为rset里的fd被置位后,select函数并不知道哪个被置位了,需要从头遍历到尾,逐个对比
个人思考
1. 最好不要频繁的进入内核区,不但危险而且实际上是很慢的。2. 对于每一个系统级的函数粒度越细越好
poll(Linux)
解决了select的1,2两点缺点
解决了select的1,2两点缺点
基于结构体存储fd
struct pollfd{
int fd; //fd编号
short events; //将做读还是写
short revents; //可重用
}
struct pollfd{
int fd; //fd编号
short events; //将做读还是写
short revents; //可重用
}
解决缺点1:
存储结构:以页为单位的链表结构,页中包含有pollfd这个数据。
由poll_list(链表结构)指向pollfds数组,然后pollfd结构体存放在pollfds数组里
存储结构:以页为单位的链表结构,页中包含有pollfd这个数据。
由poll_list(链表结构)指向pollfds数组,然后pollfd结构体存放在pollfds数组里
好处是不用一次申请足够大的空间,而可以分批次去申请空间。
解决缺点2
revents在每次为1时,又会恢复为0再执行读取等后续操作
revents在每次为1时,又会恢复为0再执行读取等后续操作
epoll(基于Linux)
解决select的1,2,3,4
解决select的1,2,3,4
epoll_create调用时:1.内核在epoll文件系统里建了个file结点,建立一个rdllist双向链表,用于存储准备就绪的事件
2.内核在cache里建了个红黑树用于存储以后epoll_ctl传来的socket
2.内核在cache里建了个红黑树用于存储以后epoll_ctl传来的socket
epoll_ctl 用于向内核注册新的或修改文件描述符,已注册的描述符在内核中的红黑树上
epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效
epoll两种模式
LT(水平触发)模式下,只要有数据就触发,缓冲区剩余未读尽的数据会导致 epoll_wait都会返回它的事件;
ET(边缘触发)模式下,只有新数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回。
ET(边缘触发)模式下,只有新数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回。
解决缺点1:
在epoll中对于每一个事件都会建立一个epitem结构体,无上限问题
在epoll中对于每一个事件都会建立一个epitem结构体,无上限问题
解决缺点2:
epitem结构体可重用
epitem结构体可重用
解决缺点3:
只在while(1)循环外通过epoll_ctl将fd添加进内核一次,无需重复拷贝
只在while(1)循环外通过epoll_ctl将fd添加进内核一次,无需重复拷贝
解决缺点4:
epoll_wait检查到rdllist有数据并执行完后,会返回告知用户有几个fd,那么for遍历时走几步即可,时间O1
epoll_wait检查到rdllist有数据并执行完后,会返回告知用户有几个fd,那么for遍历时走几步即可,时间O1
使用场景:redis、nginx、(linux下)javaNIO
总结
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
使用场景
当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用
如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用
多路复用就是在单线程里利用一个监听机制实现对多个客户端的监听,若客户端有反应,则代表有读写事件
IO多路复用第二版
信号驱动IO
异步IO
文件描述符(FD)
打开一个文件时,内核会返回一个文件描述符
应用程序进程拿到的文件描述符ID == 进程文件描述符表的索引,通过索引拿到文件指针,
指向系统级文件描述符表的文件偏移量,再通过文件偏移量找到inode指针,最终对应到真实的文件
应用程序进程拿到的文件描述符ID == 进程文件描述符表的索引,通过索引拿到文件指针,
指向系统级文件描述符表的文件偏移量,再通过文件偏移量找到inode指针,最终对应到真实的文件
同步&异步
注重的是结果,同步就是一直等待。
异步:时不时地去看有没有结果
异步:时不时地去看有没有结果
阻塞&非阻塞
注重的是过程
BIO&NIO&AIO
面试题
分布式
分布式原理
简述CAP理论
c是指consistency一致性,保证读时能返回最新写的数据;
a指available可用性,表示服务器可以在合理时间内返回合理结果(不是错误或者超时的响应);
p指partition tolerance分区容错性,表示出现网络分区后仍能够对外提供服务。
a指available可用性,表示服务器可以在合理时间内返回合理结果(不是错误或者超时的响应);
p指partition tolerance分区容错性,表示出现网络分区后仍能够对外提供服务。
很多人认为cap只能任意三选二,不对,实际上出现网络分区后,p是前提,也就是必须保证能对外提供服务,再从ca中2选1,为什么ca只能2选1呢?因为一致性相当于在写的时候要让读等待,就保证不了可用性,也就是不能及时返回结果。
应用方面,zk可以保证cp,无法保证ap,因为zk在选举leader或者超过一半节点故障时,整个服务器将不可用。
Eureka保证ap,因为节点都是平等的,但无法保证cp可能会返回旧数据。而Nacos可以保证cp和ap。总结:在没出现网络分区时尽量思考如何保证ca,出现网络分区时,考虑保证cp还是ap
Eureka保证ap,因为节点都是平等的,但无法保证cp可能会返回旧数据。而Nacos可以保证cp和ap。总结:在没出现网络分区时尽量思考如何保证ca,出现网络分区时,考虑保证cp还是ap
BASE理论
(本质上是对ap的延伸)
(本质上是对ap的延伸)
BA基本可用
ap状态会产生一些损耗,如时间上慢些,非必须功能不可用
S软状态
数据暂时不一致,但最终会一致,如银行转账延迟
E最终一致性
分布式一致性三个级别
强一致性(实时一致性)
弱一致性(不确定什么时候一致)
最终一致性(确定未来某个时间会一致)(业界推崇)
分布式事务
Seata
TCC
2PC
3PC
发展演变
orm
单一应用架构
单一应用架构
所有功能都在一个机器上部署,
扩展性低,不利于维护
扩展性低,不利于维护
mvc
垂直应用架构
垂直应用架构
多个模块系统分开部署,
但公用模块无法重复利用,开发性的浪费
但公用模块无法重复利用,开发性的浪费
RPC
分布式服务架构
分布式服务架构
在垂直架构基础上,把公共模块抽取出来
soa
流动计算架构
流动计算架构
场景题
微信红包实现
注意点:
高并发,考虑redis
大容量,考虑分库分表
redis/db2,考虑同步
有状态,考虑hash
消息顺序,考虑单线程
实时,考虑同步,延时,考虑异步
多线程交替打印奇偶数
项目
登录功能如何实现
单点登录SSO
为什么用单点登录:多系统中session不共享问题
SSO系统生成一个token,并将用户信息存到Redis中,并设置过期时间
其他系统请求SSO系统进行登录,得到SSO返回的token,写到Cookie中
每次请求时,Cookie都会带上,拦截器得到token,判断是否已经登录
其他系统请求SSO系统进行登录,得到SSO返回的token,写到Cookie中
每次请求时,Cookie都会带上,拦截器得到token,判断是否已经登录
单点登录(SSO)扩展
概念
在一个多系统共存的环境下,用户在一处登录后,就不用再其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。
应用场景
单点登录在大型网站使用非常频繁,例如阿里巴巴网站,在网站的背后是成白上千的子系统,
用户的一次操作可能涉及到几十个子系统的协作,如果每个子系统都需要用户验证会导致系统效率非常低
用户的一次操作可能涉及到几十个子系统的协作,如果每个子系统都需要用户验证会导致系统效率非常低
需要解决的问题
解决如何产生和存储信任,系统如何验证这个信任的有效性(1.存储信任 2.验证信任)
解决方案
1、cookie
创建一个cookies
通过注解的方式获得cookies
cookies存在跨域问题
路径
cookie 一般是由与用户访问页面而被创建的 , 可是并不是只有在创建 cookie 的页面才可以访问这个cookie。在默认情况下,出于安全方面的考虑,只有与创建 cookie 的页面处于同一个目录或在创建cookie页面的子目录下的网页才可以访问。那么此时如果希望其父级或者整个网页都能够使用cookie,就需要进行路径的设置
共享Cookie
当我们的子系统都在一个父级域名下时,我们可以将Cookie种在父域下,这样浏览器同域名下的Cookie则可以共享,这样可以通过Cookie加解密的算法获取用户SessionID,从而实现SSO。
但是,后面我们发现这种方式有几种弊端:
a. 所有同域名的系统都能获取SessionID,易被修改且不安全;
b. 跨域无法使用。
当我们的子系统都在一个父级域名下时,我们可以将Cookie种在父域下,这样浏览器同域名下的Cookie则可以共享,这样可以通过Cookie加解密的算法获取用户SessionID,从而实现SSO。
但是,后面我们发现这种方式有几种弊端:
a. 所有同域名的系统都能获取SessionID,易被修改且不安全;
b. 跨域无法使用。
解决
1
2
流程
用户注册
数据校验接口
Controller只是发布服务。接收三个参数,一个是要校验的数据,一个数据类型,一个是callback。
调用Service校验,返回json数据,需要支持jsonp,需要判断callback (扩展:@PathVariable在备注里)
调用Service校验,返回json数据,需要支持jsonp,需要判断callback (扩展:@PathVariable在备注里)
Service接收两个参数,一个是要校验的数据,一个是数据类型。根据不同的数据类型生成不同的查询条件,
到user表中进行查询如果查询到结果返回false,查询结果为空返回true
到user表中进行查询如果查询到结果返回false,查询结果为空返回true
用户注册接口
Controller接收一个表单,请求的方法为post。使用TbUser接收表单的内容。调用Service插入数据,返回
Service接收TbUser参数,对数据进行校验,校验成功,插入数据,返回结果
用户登录
准备jedisClient接口
jedisClient用于设置和更新session过期时间
用户登录接口
Controller接收两个参数,一个是用户名,一个是密码,请求的方法为post。调用Service方法返回登录处理结果,响应json数据
Service接收用户名、密码。校验密码是否正确,生成token,向redis中写入用户信息,把token写入cookie,并在返回结果中包含token。
共享Session
共享Session可谓是实现单点登录最直接、最简单的方式。将用户认证信息保存于Session中,即以Session内存储的值为用户凭证,这在单个站点内使用是很正常也很容易实现的,而在用户验证、用户信息管理与业务应用分离的场景下即会遇到单点登录的问题,在应用体系简单,子系统很少的情况下,可以考虑采用Session共享的方法来处理这个问题
共享Session可谓是实现单点登录最直接、最简单的方式。将用户认证信息保存于Session中,即以Session内存储的值为用户凭证,这在单个站点内使用是很正常也很容易实现的,而在用户验证、用户信息管理与业务应用分离的场景下即会遇到单点登录的问题,在应用体系简单,子系统很少的情况下,可以考虑采用Session共享的方法来处理这个问题
这个架构使用了基于Redis的Session共享方案。将Session存储于Redis上,然后将整个系统的全局Cookie Domain设置于顶级域名上,这样SessionID就能在各个子系统间共享。
这个方案存在着严重的扩展性问题,首先,ASP.NET的Session存储必须为SessionStateItemCollection对象,而存储的结构是经过序列化后经过加密存储的。并且当用户访问应用时,他首先做的就是将存储容器里的所有内容全部取出,并且反序列化为SessionStateItemCollection对象。这就决定了他具有以下约束:
1、 Session中所涉及的类型必须是子系统中共同拥有的(即程序集、类型都需要一致),这导致Session的使用受到诸多限制;
2、 跨顶级域名的情况完全无法处理;
这个方案存在着严重的扩展性问题,首先,ASP.NET的Session存储必须为SessionStateItemCollection对象,而存储的结构是经过序列化后经过加密存储的。并且当用户访问应用时,他首先做的就是将存储容器里的所有内容全部取出,并且反序列化为SessionStateItemCollection对象。这就决定了他具有以下约束:
1、 Session中所涉及的类型必须是子系统中共同拥有的(即程序集、类型都需要一致),这导致Session的使用受到诸多限制;
2、 跨顶级域名的情况完全无法处理;
通过token查询用户信息
Controller从url中取token的内容,调用Service取用户信息,响应json数据。
Service接收token,根据token查询redis,查询到结果返回用户对象,更新过期时间。如果查询不到结果,返回Session已经过期,状态码400
展示注册和登录页面
其他系统整合SSO
门户登录
当用户在首页点击登录或者注册的时候需要跳转到sso系统。进行相应的操作。登录成功跳转到首页。首页应该显示当前登录的用户
登录拦截器
有些页面是需要登录之后才能访问的,比如订单页面,当用户查看订单页面时此时必须要求用户登录,可以使用拦截器来实现。拦截器的处理流程为
- 拦截请求url
- 从cookie中取token
- 如果没有token跳转到登录页面。
- 取到token,需要调用sso系统的服务查询用户信息。
- 如果用户session已经过期,跳转到登录页面
- 如果没有过期,放行
拦截器配置
在springmvc中实现HandlerInterceptor接口
开始拦截->取token->验证token->调用相关服务
开始拦截->取token->验证token->调用相关服务
扩展:HandlerInterceptor拦截器
其对应的Service作用为:
根据token取用户信息,如果取到返回TbUser对象,如果取不到,返回null
根据token取用户信息,如果取到返回TbUser对象,如果取不到,返回null
购物车功能如何实现
个人商城系统,后台对购物车数据进行“半持久化”。
因为购物车增删改的操作很频繁,如果使用mysql效率会很低,
所以使用redis进行存储。如果担心redis会挂,可使用redis集群,还是很靠谱的
因为购物车增删改的操作很频繁,如果使用mysql效率会很低,
所以使用redis进行存储。如果担心redis会挂,可使用redis集群,还是很靠谱的
核心思路
用户可以在登录状态下将商品加入在线购物车,在未登录状态下加入离线购物车
登录后会将未登录时缓存的购物车数据合并到账号中,并清空原先缓存
退出登录后原来的离线购物车数据也不存在
redis存入购物车商品的数据结构
hset key filed value(hset cart:userid id count)(多加个cart: 是因为要区分别的功能)
添加功能:假设用户id为1001,放3个商品,产品id为10021,10025,10079,各放1个
删除商品:hdel
加减商品数量:hincrby(减时用负数)
商品数量:hlen
全选功能:hgetall
1
解决跨域问题
由于浏览器同源策略产生跨域问题
使用nginx代理配置为同一域
利用jsonp在<script>标签里添加外源地址
CORS
@CrossOrigin
可以向@RequestMapping注解处理程序方法添加一个@CrossOrigin注解,以便启用CORS
(默认情况下,@CrossOrigin允许在@RequestMapping注解中指定的所有源和HTTP方法)
(默认情况下,@CrossOrigin允许在@RequestMapping注解中指定的所有源和HTTP方法)
@CrossOrigin中的2个参数:
origins: 允许可访问的域列表
maxAge:准备响应前的缓存持续的最大时间(以秒为单位)
origins: 允许可访问的域列表
maxAge:准备响应前的缓存持续的最大时间(以秒为单位)
原理:CORS 请求分为两类:简单请求和复杂请求
简单请求
请求方式为 HEAD、GET、POST 这三种方式之一
HTTP头信息中开发者添加的信息不超过以下几种
HTTP头信息中开发者添加的信息不超过以下几种
对于简单请求,浏览器直接发出 CORS 请求,具体来说,就是在头信息之中,添加一个 Origin 字段.
Origin 字段用来说明,本次请求来自来个源,服务器根据这个值决定是否同意这个请求。如果该值在许可范围(即允许跨域访问),服务器就会返回一个正常的 HTTP 回应,会多出几个头信息字段:
Access-Control-Allow-Origin : 该字段必须的,表示接受该值对应的域名的请求。
Access-Control-Allow-Credentials : 该值是一个布尔值,表示是否允许发送 Cookie
Access-Control-Allow-Origin : 该字段必须的,表示接受该值对应的域名的请求。
Access-Control-Allow-Credentials : 该值是一个布尔值,表示是否允许发送 Cookie
文件上传问题
分片上传、断点续传、分片导出、HTTP 请求头、响应头的字段(待补充)
手写RPC框架
工具
Git
基本操作
Clone:克隆,就是将远程仓库复制到本地
Push:推送,就是将本地仓库代码上传到远程仓库
Pull:拉取,就是将远程仓库代码下载到本地仓库
工作流程
工作流程如下:
1.从远程仓库中克隆代码到本地仓库
2.从本地仓库中checkout代码然后进行代码修改
3.在提交前先将代码提交到暂存区
4.提交到本地仓库。本地仓库中保存修改的各个历史版本
5.修改完成后,需要和团队成员共享代码时,将代码push到远程仓库
1.从远程仓库中克隆代码到本地仓库
2.从本地仓库中checkout代码然后进行代码修改
3.在提交前先将代码提交到暂存区
4.提交到本地仓库。本地仓库中保存修改的各个历史版本
5.修改完成后,需要和团队成员共享代码时,将代码push到远程仓库
常用命令
环境配置
当安装Git后首先要做的事情是设置用户名称和email地址。这是非常重要的,因为每次Git提交都会使用该用户信息
设置用户信息
git config --global user.name “itcast”
git config --global user.email “kinggm520@163.com”
查看配置信息
git config --list
git config user.name
通过上面的命令设置的信息会保存在~/.gitconfig文件中
设置用户信息
git config --global user.name “itcast”
git config --global user.email “kinggm520@163.com”
查看配置信息
git config --list
git config user.name
通过上面的命令设置的信息会保存在~/.gitconfig文件中
获取Git仓库
Git中的revert(撤消操作)和reset(版本撤回)
- git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit。
- 在回滚这一操作上看,效果差不多。但是在日后继续merge以前的老版本时有区别。因为git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,导致这部分改变不会再次出现,但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入。
- git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容。
Maven
测试
数据结构与算法
数据结构
树
基本概念
阶(度)
分类
二叉树
二叉排序树(二叉查找树)BST
平衡二叉树(AVL)
红黑树
多路查找树
B树(balance tree)
图示
图
2-3树
2-3-4树
B+树
图示
图
B*树
图示
图
算法
排序算法(8大排序算法)
面试题
手写递归快排,非递归快排,堆排,归并,Dijkstra,Kruskal
快速排序((快速排序的最差时间复杂度是 O(n^2)),堆排序,归并排序 时间复杂度O(n*logn)
手写5种单例,枚举类
时空复杂度及其优化
手写生产者/消费者模型
手写LRU
手写前中后序遍历的迭代写法
B+树,B*树和B树有什么区别
B树:1.叶子节点和非叶子节点都存数据。2.数据无链指针。
B+树:1.只有叶子节点存数据。2.数据有链指针。
B树优势:1.靠近根节点的数据,访问速度快。
B+树优势:1.一页内存可以容纳更多的键,访问数据需要更少的缓存未命中。2.全面扫描只需要扫描叶子节点
B+树:1.只有叶子节点存数据。2.数据有链指针。
B树优势:1.靠近根节点的数据,访问速度快。
B+树优势:1.一页内存可以容纳更多的键,访问数据需要更少的缓存未命中。2.全面扫描只需要扫描叶子节点
红黑树是什么
LSM是什么
计算机结构
CPU
总线
南北桥
内存
收藏
0 条评论
下一页