下你所需,载你所想!
IT技术源码资料下载网站

Java面试知识点,精华背诵版

:其他软件 2020-07-07 17:35:02

Java面试知识点,精华背诵版

Java开发知识点【背诵版】Java基础40
语言特性12
Q1:Java语言的优点?
①平台无关性,摆脱硬件束缚,“一次编写,到处运行”。
②相对安全的内存管理和访问机制,避免大部分内存泄漏和指针越界。
③热点代码检测和运行时编译及优化,使程序随运行时间增长获得更高性能。
④完善的应用程序接口,支持第三方类库。Q2:Java如何实现平台无关?
JVM:Java编译器可生成与计算机体系结构无关的字节码指令,字节码文件不仅可以轻易地在任何机器上解释执行,还可以动态地转换成本地机器代码,转换是由JVM实现的,JVM是平台相关的,屏蔽了不同操作系统的差异。
语言规范:基本数据类型大小有明确规定,例如int永远为32位,而CC++中可能是16位、32位,也可能是编译器开发商指定的其他大小。Java中数值类型有固定字节数,二进制数据以固定格式存储和传输,字符串采用标准的Uni格式存储。Q3:JDK和JRE的区别?
JDK:JavaDevelomentKit,开发工具包。提供了编译运行Java程序的各种工具,包括编译器、JRE及常用类库,是JAVA核心。
JRE:JavaRuntimeEnvironment,运行时环境,运行Java程序的必要环境,包括JVM、核心类库、核心配置工具。Q4:Java按值调用还是引用调用?
按值调用指方法接收调用者提供的值,按引用调用指方法接收调用者提供的变量地址。
Java总是按值调用,方法得到的是所有参数值的副本,传递对象时实际上方法接收的是对象引用的副本。方法不能修改基本数据类型的参数,如果传递了一个int值,改变值不会影响实参,因为改变的是值的一个副本。
可以改变对象参数的状态,但不能让对象参数引用一个新的对象。如果传递了一个int数组,改变数组的内容会影响实参,而改变这个参数的引用并不会让实参引用新的数组对象。Q5:浅拷贝和深拷贝的区别?
浅拷贝:只复制当前对象的基本数据类型及引用变量,没有复制引用变量指向的实际对象。修改克隆对象可能影响原对象,不安全。
深拷贝:完全拷贝基本数据类型和引用数据类型,安全。Q6:什么是反射?
在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。缺点是破坏了封装性以及泛型约束。反射是框架的核心,Sring大量使用反射。Q7:Class类的作用?如何获取一个Class对象?
在程序运行期间,Java运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是Class,这是一个泛型类。
获取Class对象:①类名.class。②对象的getClass方法。③Class.forName(类的全限定名)。Q8:什么是注解?什么是元注解?
注解是一种标记,使类或接口附加额外信息,帮助编译器和JVM完成一些特定功能,例如@Override标识一个方法是重写方法。
元注解是自定义注解的注解,例如:
@Target:约束作用位置,值是ElementTye枚举常量,包括METHOD方法、VARIABLE变量、TYPE类接口、PARAMETER方法参数、CONSTRUCTORS构造方法和LOACL_VARIABLE局部变量等。
@Rentention:约束生命周期,值是RetentionPocy枚举常量,包括SOURCE源码、CLASS字节码和RUNTIME运行时。
@Documented:表明这个注解应该被javadoc记录。Q9:什么是泛型,有什么作用?
泛型本质是参数化类型,解决不确定对象具体类型的问题。泛型在定义处只具备执行Object方法的能力。
泛型的好处:①类型安全,放置什么出来就是什么,不存在ClassCastExcetion。②提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。③代码重用,合并了同类型的处理代码。
Q10:泛型擦除是什么?
泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。例如定义List<Object>或List<Sing>,在编译后都会变成List。
定义一个泛型类型,会自动提供一个对应原始类型,类型变量会被擦除。如果没有限定类型就会替换为Object,如果有限定类型就会替换为第一个限定类型,例如<TextendsA&am;B>会使用A类型替换T。Q11:JDK8新特性有哪些?
**lambda表达式:**允许把函数作为参数传递到方法,简化匿名内部类代码。
**函数式接口:**使用@FunctionalInterface标识,有且仅有一个抽象方法,可被隐式转换为lambda表达式。
**方法引用:**可以引用已有类或对象的方法和构造方法,进一步简化lambda表达式。
**接口:**接口可以定义defat修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。
**注解:**引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。
**类型推测:**加强了类型推测机制,使代码更加简洁。
**Otional类:**处理空指针异常,提高代码可读性。
**Seam类:**引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括forEach遍历、count统计个数、filter按条件过滤、mit取前n个元素、ski跳过前n个元素、ma映射加工、concat合并seam流等。
**日期:**增强了日期和时间API,新的java.time包主要包含了处理日期、时间、日期时间、时区、时刻和时钟等操作。
**JavaScrit:**提供了一个新的JavaScrit引擎,允许在JVM上运行特定JavaScrit应用。Q12:异常有哪些分类?
所有异常都是Towable的子类,分为Error和Excetion。Error是Java运行时系统的内部错误和资源耗尽错误,例如StackOverFlowError和OutOfMemoryError,这种异常程序无法处理。
Excetion分为受检异常和非受检异常,受检异常需要在代码中显式处理,否则会编译出错,非受检异常是运行时异常,继承自RuntimeExcetion。
受检异常:①无能为力型,如字段超长导致的SQLExcetion。②力所能及型,如未授权异常UnAuorizedExcetion,程序可跳转权限申请页面。常见受检异常还有FileNotFoundExcetion、ClassNotFoundExcetion、IOExcetion等。
非受检异常:①可预测异常,例如IndexOutOfBoundsExcetion、NlPointerExcetion、ClassCastExcetion等,这类异常应该提前处理。②需捕捉异常,例如进行RPC调用时的远程服务超时,这类异常客户端必须显式处理。③可透出异常,指框架或系统产生的且会自行处理的异常,例如Sring的NoSuchRequestHandingMeodExcetion,Sring会自动完成异常处理,将异常自动映射到合适的状态码。数据类型5
Q1:Java有哪些基本数据类型?
数据类型
内存大小
默认值
取值范围
byte
1B
(byte)0
-128~127
short
2B
(short)0
-215~215-1
int
4B
0
-231~231-1
long
8B
0L
-263~263-1
float
4B
0.0F
±3.4E+38(有效位数6~7位)
double
8B
0.0D
±1.7E+308(有效位数15位)
char
英文1B,中文UTF-8占3B,GBK占2B。
‘u0000’
‘u0000’~‘uFFFF’
boolean
单个变量4B数组1B
false
ue、false
JVM没有boolean赋值的专用字节码指令,booleanf=false就是使用ICONST_0即常数0赋值。单个boolean变量用int代替,boolean数组会编码成byte数组。Q2:自动装箱拆箱是什么?
每个基本数据类型都对应一个包装类,除了int和char对应Integer和Character外,其余基本数据类型的包装类都是首字母大写即可。
自动装箱:将基本数据类型包装为一个包装类对象,例如向一个泛型为Integer的集合添加int元素。
自动拆箱:将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
比较两个包装类数值要用equals,而不能用==。Q3:Sing是不可变类为什么值可以修改?
Sing类和其存储数据的成员变量value字节数组都是final修饰的。对一个Sing对象的任何修改实际上都是创建一个新Sing对象,再引用该对象。只是修改Sing变量引用的对象,没有修改原Sing对象的内容。Q4:字符串拼接的方式有哪些?
①直接用+,底层用SingBuilder实现。只适用小数量,如果在循环中使用+拼接,相当于不断创建新的SingBuilder对象再转换成Sing对象,效率极差。
②使用Sing的concat方法,该方法中使用Arrays.coyOf创建一个新的字符数组buf并将当前字符串value数组的值拷贝到buf中,buf长度=当前字符串长度+拼接字符串长度。之后调用getChars方法使用System.arraycoy将拼接字符串的值也拷贝到buf数组,最后用buf作为构造参数new一个新的Sing对象返回。效率稍高于直接使用+。
③使用SingBuilder或SingBuffer,两者的aend方法都继承自AbsactSingBuilder,该方法首先使用Arrays.coyOf确定新的字符数组容量,再调用getChars方法使用System.arraycoy将新的值追加到数组中。SingBuilder是JDK5引入的,效率高但线程不安全。SingBuffer使用synconized保证线程安全。Q5:Singa=“a”+newSing(“b”)创建了几个对象?
常量和常量拼接仍是常量,结果在常量池,只要有变量参与拼接结果就是变量,存在堆。
使用字面量时只创建一个常量池中的常量,使用new时如果常量池中没有该值就会在常量池中新创建,再在堆中创建一个对象引用常量池中常量。因此Singa="a"+newSing("b")会创建四个对象,常量池中的a和b,堆中的b和堆中的ab。面向对象10
Q1:谈一谈你对面向对象的理解
面向过程让计算机有步骤地顺序做一件事,是过程化思维,使用面向过程语言开发大型项目,软件复用和维护存在很大问题,模块之间耦合严重。面向对象相对面向过程更适合解决规模较大的问题,可以拆解问题复杂度,对现实事物进行抽象并映射为开发对象,更接近人的思维。
例如开门这个动作,面向过程是oen(Doordoor),动宾结构,door作为操作对象的参数传入方法,方法内定义开门的具体步骤。面向对象的方式首先会定义一个类Door,抽象出门的属性(如尺寸、颜色)和行为(如oen和close),主谓结构。
面向过程代码松散,强调流程化解决问题。面向对象代码强调高内聚、低耦合,先抽象模型定义共性行为,再解决实际问题。Q2:面向对象的三大特性?
封装是对象功能内聚的表现形式,在抽象基础上决定信息是否公开及公开等级,核心问题是以什么方式暴漏哪些信息。主要任务是对属性、数据、敏感行为实现隐藏,对属性的访问和修改必须通过公共接口实现。封装使对象关系变得简单,降低了代码耦合度,方便维护。
迪米特原则就是对封装的要求,即A模块使用B模块的某接口行为,对B模块中除此行为外的其他信息知道得应尽可能少。不直接对ubc属性进行读取和修改而使用gettersetter方法是因为假设想在修改属性时进行权限控制、日志记录等操作,在直接访问属性的情况下无法实现。如果将ubc的属性和行为修改为rivate一般依赖模块都会报错,因此不知道使用哪种权限时应优先使用rivate。
继承用来扩展一个类,子类可继承父类的部分属性和行为使模块具有复用性。继承是"is-a"关系,可使用里氏替换原则判断是否满足"is-a"关系,即任何父类出现的地方子类都可以出现。如果父类引用直接使用子类引用来代替且可以正确编译并执行,输出结果符合子类场景预期,那么说明两个类符合里氏替换原则。
多态以封装和继承为基础,根据运行时对象实际类型使同一行为具有不同表现形式。多态指在编译层面无法确定最终调用的方法体,在运行期由JVM动态绑定,调用合适的重写方法。由于重载属于静态绑定,本质上重载结果是完全不同的方法,因此多态一般专指重写。Q3:重载和重写的区别?
重载指方法名称相同,但参数类型个数不同,是行为水平方向不同实现。对编译器来说,方法名称和参数列表组成了一个唯一键,称为方法签名,JVM通过方法签名决定调用哪种重载方法。不管继承关系如何复杂,重载在编译时可以根据规则知道调用哪种目标方法,因此属于静态绑定。
JVM在重载方法中选择合适方法的顺序:①精确匹配。②基本数据类型自动转换成更大表示范围。③自动拆箱与装箱。④子类向上转型。⑤可变参数。
重写指子类实现接口或继承父类时,保持方法签名完全相同,实现不同方法体,是行为垂直方向不同实现。
元空间有一个方法表保存方法信息,如果子类重写了父类的方法,则方法表中的方法引用会指向子类实现。父类引用执行子类方法时无法调用子类存在而父类不存在的方法。
重写方法访问权限不能变小,返回类型和抛出的异常类型不能变大,必须加@Override。Q4:类之间有哪些关系?
类关系
描述
权力强侧
举例
继承
父子类之间的关系:is-a
父类
小狗继承于动物
实现
接口和实现类之间的关系:can-do
接口
小狗实现了狗叫接口
组合
比聚合更强的关系:contains-a
整体
头是身体的一部分
聚合
暂时组装的关系:has-a
组装方
小狗和绳子是暂时的聚合关系
依赖
一个类用到另一个:deends-a
被依赖方
人养小狗,人依赖于小狗
关联
平等的使用关系:nks-a
平等
人使用卡消费,卡可以提取人的信息Q5:Object类有哪些方法?
**equals:**检测对象是否相等,默认使用==比较对象引用,可以重写equals方法自定义比较规则。equals方法规范:自反性、对称性、传递性、一致性、对于任何非空引用x,x.equals(nl)返回false。
**hashCode:**散列码是由对象导出的一个整型值,没有规律,每个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。为了在集合中正确使用,一般需要同时重写equals和hashCode,要求equals相同hashCode必须相同,hashCode相同equals未必相同,因此hashCode是对象相等的必要不充分条件。
toSing:打印对象时默认的方法,如果没有重写打印的是表示对象值的一个字符串。
**clone:**clone方法声明为rotected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为ubc。如果一个对象的类没有实现Cloneable接口,该对象调用clone方法会抛出一个CloneNotSuort异常。默认的clone方法是浅拷贝,一般重写clone方法需要实现Cloneable接口并指定访问修饰符为ubc。
**finaze:**确定一个对象死亡至少要经过两次标记,如果对象在可达性分析后发现没有与GCRoots连接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行finaze方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。如果有必要执行,对象会被放置在F-Queue队列,由一条低调度优先级的Finazer线程去执行。虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的finaze方法执行缓慢或发生死循环。只要对象在finaze方法中重新与引用链上的对象建立关联就会在第二次标记时被移出回收集合。由于运行代价高昂且无法保证调用顺序,在JDK9被标记为过时方法,并不适合释放资源。
**getClass:**返回包含对象信息的类对象。
**waitnotifynotifyAll:**阻塞或唤醒持有该对象锁的线程。Q6:内部类的作用是什么,有哪些分类?
内部类可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括rivate数据。
内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用$分隔外部类名与内部类名,其中匿名内部类使用数字编号,虚拟机对此一无所知。
静态内部类:属于外部类,只加载一次。作用域仅在包内,可通过外部类名.内部类名直接访问,类内只能访问外部类所有静态属性和方法。HashMa的Node节点,ReenantLock中的Sync类,ArrayList的SubList都是静态内部类。内部类中还可以定义内部类,如TeadLoacl静态内部类TeadLoaclMa中定义了内部类Eny。
成员内部类:属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容。
局部内部类:定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明类的代码块中。
匿名内部类:只用一次的没有名字的类,可以简化代码,创建的对象类型相当于new的类的子类类型。用于实现事件监听和其他回调。Q7:访问权限控制符有哪些?
访问权限控制符
本类
包内
包外子类
任何地方
ubc




rotected



×



×
×
rivate

×
×
×Q8:接口和抽象类的异同?
接口和抽象类对实体类进行更高层次的抽象,仅定义公共行为和特征。
语法维度
抽象类
接口
成员变量
无特殊要求
默认ubcstaticfinal常量
构造方法
有构造方法,不能实例化
没有构造方法,不能实例化
方法
抽象类可以没有抽象方法,但有抽象方法一定是抽象类。
默认ubcabsact,JDK8支持默认静态方法,JDK9支持私有方法。
继承
单继承
多继承Q9:接口和抽象类应该怎么选择?
抽象类体现is-a关系,接口体现can-do关系。与接口相比,抽象类通常是对同类事物相对具体的抽象。
抽象类是模板式设计,包含一组具体特征,例如某汽车,底盘、控制电路等是抽象出来的共同特征,但内饰、显示屏、座椅材质可以根据不同级别配置存在不同实现。
接口是契约式设计,是开放的,定义了方法名、参数、返回值、抛出的异常类型,谁都可以实现它,但必须遵守接口的约定。例如所有车辆都必须实现刹车这种强制规范。
接口是顶级类,抽象类在接口下面的第二层,对接口进行了组合,然后实现部分接口。当纠结定义接口和抽象类时,推荐定义为接口,遵循接口隔离原则,按维度划分成多个接口,再利用抽象类去实现这些,方便后续的扩展和重构。
例如Plane和Bird都有fly方法,应把fly定义为接口,而不是抽象类的抽象方法再继承,因为除了fly行为外Plane和Bird间很难再找到其他共同特征。Q10:子类初始化的顺序
①父类静态代码块和静态变量。②子类静态代码块和静态变量。③父类普通代码块和普通变量。④父类构造方法。⑤子类普通代码块和普通变量。⑥子类构造方法。集合7
Q1:说一说ArrayList
ArrayList是容量可变的非线程安全列表,使用数组实现,集合扩容时会创建更大的数组,把原有数组复制到新数组。支持对元素的快速随机访问,但插入与删除速度很慢。ArrayList实现了RandomAcess标记接口,如果一个类实现了该接口,那么表示使用索引遍历比迭代器更快。
elementData是ArrayList的数据域,被ansient修饰,序列化时会调用writeObject写入流,反序列化时调用adObject重新赋值到新对象的elementData。原因是elementData容量通常大于实际存储元素的数量,所以只需发送真正有实际值的数组元素。
size是当前实际大小,elementData大小大于等于size。
**modCount**记录了ArrayList结构性变化的次数,继承自AbsactList。所有涉及结构变化的方法都会增加该值。exectedModCount是迭代器初始化时记录的modCount值,每次访问新元素时都会检查modCount和exectedModCount是否相等,不相等就会抛出异常。这种机制叫做fail-fast,所有集合类都有这种机制。Q2:说一说LinkedList
LinkedList本质是双向链表,与ArrayList相比插入和删除速度更快,但随机访问元素很慢。除继承AbsactList外还实现了Deque接口,这个接口具有队列和栈的性质。成员变量被ansient修饰,原理和ArrayList类似。
LinkedList包含三个重要的成员:size、first和last。size是双向链表中节点的个数,first和last分别指向首尾节点的引用。
LinkedList的优点在于可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率较高。Q3:Set有什么特点,有哪些实现?
Set不允许元素重复且无序,常用实现有HashSet、LinkedHashSet和TeSet。
HashSet通过HashMa实现,HashMa的Key即HashSet存储的元素,所有Key都使用相同的Value,一个名为PRESENT的Object类型常量。使用Key保证元素唯一性,但不保证有序性。由于HashSet是HashMa实现的,因此线程不安全。
HashSet判断元素是否相同时,对于包装类型直接按值比较。对于引用类型先比较hashCode是否相同,不同则代表不是同一个对象,相同则继续比较equals,都相同才是同一个对象。
LinkedHashSet继承自HashSet,通过LinkedHashMa实现,使用双向链表维护元素插入顺序。
TeSet通过TeMa实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。Q4:TeMa有什么特点?
TeMa基于红黑树实现,增删改查的平均和最差时间复杂度均为O(logn),最大特点是Key有序。Key必须实现Comarable接口或提供的Comarator比较器,所以Key不允许为nl。
HashMa依靠hashCode和equals去重,而TeMa依靠Comarable或Comarator。TeMa排序时,如果比较器不为空就会优先使用比较器的coma方法,否则使用Key实现的Comarable的comaTo方法,两者都不满足会抛出异常。
TeMa通过ut和deleteEny实现增加和删除树节点。插入新节点的规则有三个:①需要调整的新节点总是红色的。②如果插入新节点的父节点是黑色的,不需要调整。③如果插入新节点的父节点是红色的,由于红黑树不能出现相邻红色,进入循环判断,通过重新着色或左右旋转来调整。TeMa的插入操作就是按照Key的对比往下遍历,大于节点值向右查找,小于向左查找,先按照二叉查找树的特性操作,后续会重新着色和旋转,保持红黑树的特性。Q5:HashMa有什么特点?
JDK8之前底层实现是数组+链表,JDK8改为数组+链表红黑树,节点类型从Eny变更为Node。主要成员变量包括存储数据的数组、元素数量size、加载因子loadFactor。
数组记录HashMa的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,NodeEny节点包含四个成员变量:key、value、next指针和hash值。
HashMa中数据以键值对的形式存在,键对应的hash值用来计算数组下标,如果两个元素key的hash值一样,就会发生哈希冲突,被放到同一个链表上,为使查询效率尽可能高,键的hash值要尽可能分散。
HashMa默认初始化容量为16,扩容容量必须是2的幂次方、最大容量为1<<30、默认加载因子为0.75。Q6:HashMa相关方法的源码?
JDK8之前
hash:计算元素key的散列值
①处理Sing类型时,调用singHas2方法获取hash值。
②处理其他类型数据时,提供一个相对于HashMa实例唯一不变的随机值hashSeed作为计算初始量。
③执行异或和无符号右移使hash值更加离散,减小哈希冲突概率。
indexFor:计算元素下标
将hash值和数组长度-1进行与操作,保证结果不会超过数组范围。
get:获取元素的value值
①如果key为nl,调用getForNlKey方法,如果size为0表示链表为空,返回nl。如果size不为0说明存在链表,遍历[0]链表,如果找到了key为nl的节点则返回其value,否则返回nl。
②如果key为不为nl,调用getEny方法,如果size为0表示链表为空,返回nl值。如果size不为0,首先计算key的hash值,然后遍历该链表的所有节点,如果节点的key和hash值都和要查找的元素相同则返回其Eny节点。
③如果找到了对应的Eny节点,调用getValue方法获取其value并返回,否则返回nl。
ut:添加元素
①如果key为nl,直接存入[0]。
②如果key不为nl,计算key的hash值。
③调用indexFor计算元素存放的下标i。
④遍历[i]对应的链表,如果key已存在,就更新value然后返回旧value。
⑤如果key不存在,将modCount值加1,使用addEny方法增加一个节点并返回nl。
size:扩容数组
①如果当前容量达到了最大容量,将阈值设置为Integer最大值,之后扩容不再触发。
②否则计算新的容量,将阈值设为newCaacityxloadFactor和最大容量+1的较小值。
③创建一个容量为newCaacity的Eny数组,调用ansfer方法将旧数组的元素转移到新数组。
ansfer:转移元素
①遍历旧数组的所有元素,调用hash方法判断是否需要哈希重构,如果需要就重新计算元素key的hash值。
②调用indexFor方法计算元素存放的下标i,利用头插法将旧数组的元素转移到新数组。
JDK8
hash:计算元素key的散列值
如果key为nl返回0,否则就将key的hashCode方法返回值高低16位异或,让尽可能多的位参与运算,让结果的0和1分布更加均匀,降低哈希冲突概率。
ut:添加元素
①调用utVal方法添加元素。
②如果为空或长度为0就进行扩容,否则计算元素下标位置,不存在就调用newNode创建一个节点。
③如果存在且是链表,如果首节点和待插入元素的hash和key都一样,更新节点的value。
④如果首节点是TeNode类型,调用utTeVal方法增加一个树节点,每一次都比较插入节点和当前节点的大小,待插入节点小就往左子树查找,否则往右子树查找,找到空位后执行两个方法:balanceInsert方法,插入节点并调整平衡、moveRootToFront方法,由于调整平衡后根节点可能变化,需要重置根节点。
⑤如果都不满足,遍历链表,根据hash和key判断是否重复,决定更新value还是新增节点。如果遍历到了链表末尾则添加节点,如果达到建树阈值7,还需要调用eeifyBin把链表重构为红黑树。
⑥存放元素后将modCount加1,如果++size>teshold,调用size扩容。
get:获取元素的value值
①调用getNode方法获取Node节点,如果不是nl就返回其value值,否则返回nl。
②getNode方法中如果数组不为空且存在元素,先比较第一个节点和要查找元素的hash和key,如果都相同则直接返回。
③如果第二个节点是TeNode类型则调用getTeNode方法进行查找,否则遍历链表根据hash和key查找,如果没有找到就返回nl。
size:扩容数组
重新规划长度和阈值,如果长度发生了变化,部分数据节点也要重新排列。
重新规划长度
①如果当前容量oldCa>0且达到最大容量,将阈值设为Integer最大值,turn终止扩容。
②如果未达到最大容量,当oldCa<<1不超过最大容量就扩大为2倍。
③如果都不满足且当前扩容阈值oldT>0,使用当前扩容阈值作为新容量。
④否则将新容量置为默认初始容量16,新扩容阈值置为12。
重新排列数据节点
①如果节点为nl不进行处理。
②如果节点不为nl且没有next节点,那么通过节点的hash值和新容量-1进行与运算计算下标存入新的数组。
③如果节点为TeNode类型,调用st方法处理,如果节点数hc达到6会调用uneeify方法转回链表。
④如果是链表节点,需要将链表拆分为hash值超出旧容量的链表和未超出容量的链表。对于hash&am;oldCa==0的部分不需要做处理,否则需要放到新的下标位置上,新下标=旧下标+旧容量。Q7:HashMa为什么线程不安全?
JDK7存在死循环和数据丢失问题。
数据丢失:
并发赋值被覆盖:在cateEny方法中,新添加的元素直接放在头部,使元素之后可以被更快访问,但如果两个线程同时执行到此处,会导致其中一个线程的赋值被覆盖。
已遍历区间新增元素丢失:当某个线程在ansfer方法迁移时,其他线程新增的元素可能落在已遍历过的哈希槽上。遍历完成后,数组引用指向了newTable,新增元素丢失。
新表被覆盖:如果size完成,执行了=newTable,则后续元素就可以在新表上进行插入。但如果多线程同时size,每个线程都会new一个数组,这是线程内的局部对象,线程之间不可见。迁移完成后size的线程会赋值给线程共享变量,可能会覆盖其他线程的操作,在新表中插入的对象都会被丢弃。
死循环:扩容时size调用ansfer使用头插法迁移元素,虽然newTable是局部变量,但原先中的Eny链表是共享的,问题根源是Eny的next指针并发修改,某线程还没有将设为newTable时用完了CPU时间片,导致数据丢失或死循环。
JDK8在size方法中完成扩容,并改用尾插法,不会产生死循环,但并发下仍可能丢失数据。可用ConcurntHashMa或Collections.synconizedMa包装成同步集合。IO流6
Q1:同步异步阻塞非阻塞IO的区别?
同步和异步是通信机制,阻塞和非阻塞是调用状态。
同步IO是用户线程发起IO请求后需要等待或轮询内核IO操作完成后才能继续执行。异步IO是用户线程发起IO请求后可以继续执行,当内核IO操作完成后会通知用户线程,或调用用户线程注册的回调函数。
阻塞IO是IO操作需要彻底完成后才能返回用户空间。非阻塞IO是IO操作调用后立即返回一个状态值,无需等IO操作彻底完成。Q2:什么是BIO?
BIO是同步阻塞式IO,JDK1.4之前的IO模型。服务器实现模式为一个连接请求对应一个线程,服务器需要为每一个客户端请求创建一个线程,如果这个连接不做任何事会造成不必要的线程开销。可以通过线程池改善,这种IO称为伪异步IO。适用连接数目少且服务器资源多的场景。Q3:什么是NIO?
NIO是JDK1.4引入的同步非阻塞IO。服务器实现模式为多个连接请求对应一个线程,客户端连接请求会注册到一个多路复用器Selector,Selector轮询到连接有IO请求时才启动一个线程处理。适用连接数目多且连接时间短的场景。
同步是指线程还是要不断接收客户端连接并处理数据,非阻塞是指如果一个管道没有数据,不需要等待,可以轮询下一个管道。
核心组件:
Selector:多路复用器,轮询检查多个Channel的状态,判断注册事件是否发生,即判断Channel是否处于可读或可写状态。使用前需要将Channel注册到Selector,注册后会得到一个SelectionKey,通过SelectionKey获取Channel和Selector相关信息。
Channel:双向通道,替换了BIO中的Seam流,不能直接访问数据,要通过Buffer来读写数据,也可以和其他Channel交互。
Buffer:缓冲区,本质是一块可读写数据的内存,用来简化数据读写。Buffer三个重要属性:osition下次读写数据的位置,mit本次读写的极限位置,caacity最大容量。f将写转为读,底层实现原理把osition置0,并把mit设为当前的osition值。
clear将读转为写模式(用于读完全部数据的情况,把osition置0,mit设为caacity)。
comact将读转为写模式(用于存在未读数据的情况,让osition指向未读数据的下一个)。
通道方向和Buffer方向相反,读数据相当于向Buffer写,写数据相当于从Buffer读。使用步骤:向Buffer写数据,调用f方法转为读模式,从Buffer中读数据,调用clear或comact方法清空缓冲区。Q4:什么是AIO?
AIO是JDK7引入的异步非阻塞IO。服务器实现模式为一个有效请求对应一个线程,客户端的IO请求都是由操作系统先完成IO操作后再通知服务器应用来直接使用准备好的数据。适用连接数目多且连接时间长的场景。
异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。
实现方式包括通过Futu的get方法进行阻塞式调用以及实现ComletionHandler接口,重写请求成功的回调方法comleted和请求失败回调方法failed。Q5:java.io包下有哪些流?
主要分为字符流和字节流,字符流一般用于文本文件,字节流一般用于图像或其他文件。
字符流包括了字符输入流Reader和字符输出流Writer,字节流包括了字节输入流InutSeam和字节输出流OututSeam。字符流和字节流都有对应的缓冲流,字节流也可以包装为字符流,缓冲流带有一个8KB的缓冲数组,可以提高流的读写效率。除了缓冲流外还有过滤流FilterReader、字符数组流CharArrayReader、字节数组流ByteArrayInutSeam、文件流FileInutSeam等。Q6:序列化和反序列化是什么?
Java对象JVM退出时会全部销毁,如果需要将对象及状态持久化,就要通过序列化实现,将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化。
常见的序列化有三种:
Java原生序列化
实现Seriazabale标记接口,Java序列化保留了对象类的元数据(如类、成员变量、继承类信息)以及对象数据,兼容性最好,但不支持跨语言,性能一般。序列化和反序列化必须保持序列化ID的一致,一般使用rivatestaticfinallongserialVersionUID定义序列化ID,如果不设置编译器会根据类的内部实现自动生成该值。如果是兼容升级不应该修改序列化ID,防止出错,如果是不兼容升级则需要修改。
Hessian序列化
Hessian序列化是一种支持动态类型、跨语言、基于对象传输的网络协议。Java对象序列化的二进制流可以被其它语言反序列化。Hessian协议的特性:①自描述序列化类型,不依赖外部描述文件,用一个字节表示常用基础类型,极大缩短二进制流。②语言无关,支持脚本语言。③协议简单,比Java原生序列化高效。Hessian会把复杂对象所有属性存储在一个Ma中序列化,当父类和子类存在同名成员变量时会先序列化子类再序列化父类,因此子类值会被父类覆盖。
JSON序列化
JSON序列化就是将数据对象转换为JSON字符串,在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好,方便调试。
序列化通常会使用网络传输对象,而对象中往往有敏感数据,容易遭受攻击,Jackson和fastjson等都出现过反序列化漏洞,因此不需要进行序列化的敏感属性传输时应加上ansient关键字。ansient的作用就是把变量生命周期仅限于内存而不会写到磁盘里持久化,变量会被设为对应数据类型的零值。JVM32
内存区域划分8
Q1:运行时数据区是什么?
虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区,这些区域有各自的用途、创建和销毁时间。
线程私有:程序计数器、Java虚拟机栈、本地方法栈。
线程共享:Java堆、方法区。Q2:程序计数器是什么?
程序计数器是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。字节码解释器工作时通过改变计数器的值选取下一条执行指令。分支、循环、跳转、线程恢复等功能都需要依赖计数器完成。是唯一在虚拟机规范中没有规定内存溢出情况的区域。
如果线程正在执行Java方法,计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法,计数器值为Undefined。Q3:Java虚拟机栈的作用?
Java虚拟机栈来描述Java方法的内存模型。每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和方法出口等信息。每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程。
有两类异常:①线程请求的栈深度大于虚拟机允许的深度抛出StackOverflowError。②如果JVM栈容量可以动态扩展,栈扩展无法申请足够内存抛出OutOfMemoryError(HotSot不可动态扩展,不存在此问题)。Q4:本地方法栈的作用?
本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚本地方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法。
虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如HotSot将虚拟机栈和本地方法栈合二为一。
本地方法栈在栈深度异常和栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError。Q5:堆的作用是什么?
堆是虚拟机所管理的内存中最大的一块,被所有线程共享的,在虚拟机启动时创建。堆用来存放对象实例,Java里几乎所有对象实例都在堆分配内存。堆可以处于物理上不连续的内存空间,逻辑上应该连续,但对于例如数组这样的大对象,多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间。
堆既可以被实现成固定大小,也可以是可扩展的,可通过-Xms和-Xmx设置堆的最小和最大容量,当前主流JVM都按照可扩展实现。如果堆没有内存完成实例分配也无法扩展,抛出OutOfMemoryError。Q6:方法区的作用是什么?
方法区用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK8之前使用永久代实现方法区,容易内存溢出,因为永久代有-XX:MaxPermSize上限,即使不设置也有默认大小。JDK7把放在永久代的字符串常量池、静态变量等移出,JDK8中永久代完全废弃,改用在本地内存中实现的元空间代替,把JDK7中永久代剩余内容(主要是类型信息)全部移到元空间。
虚拟机规范对方法区的约束宽松,除和堆一样不需要连续内存和可选择固定大小可扩展外,还可以不实现垃圾回收。垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载。如果方法区无法满足新的内存分配需求,将抛出OutOfMemoryError。Q7:运行时常量池的作用是什么?
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池。一般除了保存Class文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。
运行时常量池相对于Class文件常量池的一个重要特征是动态性,Java不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是Sing的intern方法。
运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError。Q8:直接内存是什么?
直接内存不属于运行时数据区,也不是虚拟机规范定义的内存区域,但这部分内存被频繁使用,而且可能导致内存溢出。
JDK1.4中新加入了NIO这种基于通道与缓冲区的IO,它可以使用Native函数库直接分配堆外内存,通过一个堆里的DictByteBuffer对象作为内存的引用进行操作,避免了在Java堆和Native堆来回复制数据。
直接内存的分配不受Java堆大小的限制,但还是会受到本机总内存及处理器寻址空间限制,一般配置虚拟机参数时会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使内存区域总和大于物理内存限制,导致动态扩展时出现OOM。
由直接内存导致的内存溢出,一个明显的特征是在HeaDum文件中不会看见明显的异常,如果发现内存溢出后产生的Dum文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是NIO),那么就可以考虑检查直接内存方面的原因。内存溢出5
Q1:内存溢出和内存泄漏的区别?
内存溢出OutOfMemory,指程序在申请内存时,没有足够的内存空间供其使用。
内存泄露MemoryLeak,指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。Q2:堆溢出的原因?
堆用于存储对象实例,只要不断创建对象并保证GCRoots到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会OOM,例如在while死循环中一直new创建实例。
堆OOM是实际应用中最常见的OOM,处理方法是通过内存映像分析工具对Dum出的堆转储快照分析,确认内存中导致OOM的对象是否必要,分清到底是内存泄漏还是内存溢出。
如果是内存泄漏,通过工具查看泄漏对象到GCRoots的引用链,找到泄露对象是通过怎样的引用路径、与哪些GCRoots关联才导致无法回收,一般可以准确定位到产生内存泄漏代码的具体位置。
如果不是内存泄漏,即内存中对象都必须存活,应当检查JVM堆参数,与机器内存相比是否还有向上调整的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。Q3:栈溢出的原因?
由于HotSot不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由-Xss参数来设定,存在两种异常:
StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。
OutOfMemoryError:如果JVM栈可以动态扩展,当扩展无法申请到足够内存时会抛出OutOfMemoryError。HotSot不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OOM,否则在线程运行时是不会因为扩展而导致溢出的。Q4:运行时常量池溢出的原因?
Sing的intern方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此Sing对象的字符串,则返回池中这个字符串的Sing对象的引用,否则将此Sing对象包含的字符串添加到常量池并返回此Sing对象的引用。
在JDK6及之前常量池分配在永久代,因此可以通过-XX:PermSize和-XX:MaxPermSize限制永久代大小,间接限制常量池。在while死循环中调用intern方法导致运行时常量池溢出。在JDK7后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中。Q5:方法区溢出的原因?
方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用JDK反射或CGLib直接操作字节码在运行时生成大量的类。很多框架如Sring、Hibernate等对类增强时都会使用CGLib这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。
JDK8使用元空间取代永久代,HotSot提供了一些参数作为元空间防御措施,例如-XX:MetasaceSize指定元空间初始大小,达到该值会触发GC进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。创建对象5
Q1:创建对象的过程是什么?
字节码角度NEW:如果找不到Class对象则进行类加载。加载成功后在堆中分配内存,从Object到本类路径上的所有属性都要分配。分配完毕后进行零值设置。最后将指向实例对象的引用变量压入虚拟机栈顶。
**DUP:**在栈顶复制引用变量,这时栈顶有两个指向堆内实例的引用变量。两个引用变量的目的不同,栈底的引用用于赋值或保存局部变量表,栈顶的引用作为句柄调用相关方法。
INVOKESPECIAL:通过栈顶的引用变量调用init方法。执行角度
①当JVM遇到字节码new指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
②在类加载检查通过后虚拟机将为新生对象分配内存。
③内存分配完成后虚拟机将成员变量设为零值,保证对象的实例字段可以不赋初值就使用。
④设置对象头,包括哈希码、GC信息、锁信息、对象所属类的类元信息等。
⑤执行init方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。Q2:对象分配内存的方式有哪些?
对象所需内存大小在类加载完成后便可完全确定,分配空间的任务实际上等于把一块确定大小的内存块从Java堆中划分出来。
指针碰撞:假设Java堆内存规整,被使用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。
空闲列表:如果Java堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
选择哪种分配方式由堆是否规整决定,堆是否规整由垃圾收集器是否有空间压缩能力决定。使用Serial、ParNew等收集器时,系统采用指针碰撞;使用CMS这种基于清除算法的垃圾收集器时,采用空间列表。Q3:对象分配内存是否线程安全?
对象创建十分频繁,即使修改一个指针的位置在并发下也不是线程安全的,可能正给对象A分配内存,指针还没来得及修改,对象B又使用了指针来分配内存。
解决方法:①CAS加失败重试保证更新原子性。②把内存分配按线程划分在不同空间,即每个线程在Java堆中预先分配一小块内存,叫做本地线程分配缓冲TLAB,哪个线程要分配内存就在对应的TLAB分配,TLAB用完了再进行同步。Q4:对象的内存布局了解吗?
对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。
对象头占12B,包括对象标记和类型指针。对象标记存储对象自身的运行时数据,如哈希码、GC分代年龄、锁标志、偏向线程ID等,这部分占8B,称为MarkWord。MarkWord被设计为动态数据结构,以便在极小的空间存储更多数据,根据对象状态复用存储空间。
类型指针是对象指向它的类型元数据的指针,占4B。JVM通过该指针来确定对象是哪个类的实例。
实例数据是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。
对齐填充不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是8B的倍数,对象头已被设为8B的1或2倍,如果对象实例数据部分没有对齐,需要对齐填充补全。Q5:对象的访问方式有哪些?
Java程序会通过栈上的fence引用操作堆对象,访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针。
句柄:堆会划分出一块内存作为句柄池,fence中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。优点是fence中存储的是稳定句柄地址,在GC过程中对象被移动时只会改变句柄的实例数据指针,而fence本身不需要修改。
直接指针:堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,fence存储对象地址,如果只是访问对象本身就不需要多一次间接访问的开销。优点是速度更快,节省了一次指针定位的时间开销,HotSot主要使用直接指针进行对象访问。垃圾回收7
Q1:如何判断对象是否是垃圾?
**引用计数:**在对象中添加一个引用计数器,如果被引用计数器加1,引用失效时计数器减1,如果计数器为0则被标记为垃圾。原理简单,效率高,但是在Java中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。
**可达性分析:**主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过一系列称为GCRoots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到GCRoots没有任何引用链相连,则会被标记为垃圾。可作为GCRoots的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。Q2:Java的引用有哪些类型?
JDK1.2后对引用进行了扩充,按强度分为四种:
强引用:最常见的引用,例如Objectobj=newObject()就属于强引用。只要对象有强引用指向且GCRoots可达,在内存回收时即使濒临内存耗尽也不会被回收。
软引用:弱于强引用,描述非必需对象。在系统将发生内存溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。用来缓存服务器中间计算结果及不需要实时保存的用户行为等。
弱引用:弱于软引用,描述非必需对象。弱引用关联的对象只能生存到下次YGC前,当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。由于YGC具有不确定性,因此弱引用何时被回收也不确定。
虚引用:最弱的引用,定义完成后无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,垃圾回收时如果出现虚引用,就会在回收对象前把这个虚引用加入引用队列。Q3:有哪些GC算法?
标记-清除算法
分为标记和清除阶段,首先从每个GCRoots出发依次标记有引用关系的对象,最后清除没有标记的对象。
执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低。
存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易触发FlGC。
标记-复制算法
为了解决内存碎片问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于进行新生代。
实现简单、运行高效,解决了内存碎片问题。代价是可用内存缩小为原来的一半,浪费空间。
HotSot把新生代划分为一块较大的Eden和两块较小的Survivor,每次分配内存只使用Eden和其中一块Survivor。垃圾收集时将Eden和Survivor中仍然存活的对象一次性复制到另一块Survivor上,然后直接清理掉Eden和已用过的那块Survivor。HotSot默认Eden和Survivor的大小比例是8:1,即每次新生代中可用空间为整个新生代的90%。
标记-整理算法
标记-复制算法在对象存活率高时要进行较多复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保,应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法。
老年代使用标记-整理算法,标记过程与标记-清除算法一样,但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。
标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式的。如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作,而且移动必须全程暂停用户线程。如果不移动对象就会导致空间碎片问题,只能依赖更复杂的内存分配器和访问器解决。Q4:你知道哪些垃圾收集器?
Serial
最基础的收集器,使用复制算法、单线程工作,只用一个处理器或一条线程完成垃圾收集,进行垃圾收集时必须暂停其他所有工作线程。
Serial是虚拟机在客户端模式的默认新生代收集器,简单高效,对于内存受限的环境它是所有收集器中额外内存消耗最小的,对于处理器核心较少的环境,Serial由于没有线程交互开销,可获得最高的单线程收集效率。
ParNew
Serial的多线程版本,除了使用多线程进行垃圾收集外其余行为完全一致。
ParNew是虚拟机在服务端模式的默认新生代收集器,一个重要原因是除了Serial外只有它能与CMS配合。自从JDK9开始,ParNew加CMS不再是官方推荐的解决方案,官方希望它被G1取代。
ParallelScavenge
新生代收集器,基于复制算法,是可并行的多线程收集器,与ParNew类似。
特点是它的关注点与其他收集器不同,ParallelScavenge的目标是达到一个可控制的吞吐量,吞吐量就是处理器用于运行用户代码的时间与处理器消耗总时间的比值。
SerialOld
Serial的老年代版本,单线程工作,使用标记-整理算法。
SerialOld是虚拟机在客户端模式的默认老年代收集器,用于服务端有两种用途:①JDK5及之前与ParallelScavenge搭配。②作为CMS失败预案。
PallelOld
ParallelScavenge的老年代版本,支持多线程,基于标记-整理算法。JDK6提供,注重吞吐量可考虑ParallelScavenge加ParallelOld。
CMS
以获取最短回收停顿时间为目标,基于标记-清除算法,过程相对复杂,分为四个步骤:初始标记、并发标记、重新标记、并发清除。
初始标记和重新标记需要STW(StoTheWorld,系统停顿),初始标记仅是标记GCRoots能直接关联的对象,速度很快。并发标记从GCRoots的直接关联对象开始遍历整个对象图,耗时较长但不需要停顿用户线程。重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那部分记录。并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象,该阶段也可与用户线程并发。
缺点:①对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量。②无法处理浮动垃圾,有可能出现并发失败而导致FlGC。③基于标记-清除算法,产生空间碎片。
G1
开创了收集器面向局部收集的设计思路和基于Region的内存布局,主要面向服务端,最初设计目标是替换CMS。
G1之前的收集器,垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆。而G1可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代,而是哪块内存中存放的垃圾数量最多,回收受益最大。
跟踪各Region里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的Region。这种方式保证了G1在有限时间内获取尽可能高的收集效率。
G1运作过程:**初始标记:**标记GCRoots能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用Region中分配新对象。需要STW但耗时很短,在MinorGC时同步完成。
**并发标记:**从GCRoots开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理SATB记录的在并发时有变动的对象。
**最终标记:**对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量SATB记录。
**筛选回收:**对各Region的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成。可由用户指定期望停顿时间是G1的一个强大功能,但该值不能设得太低,一般设置为100~300ms。Q5:ZGC了解吗?
JDK11中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在10ms以内的低延迟。
基于Region内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。
ZGC的Region具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。Q6:你知道哪些内存分配与回收策略?
对象优先在Eden区分配
大多数情况下对象在新生代Eden区分配,当Eden没有足够空间时将发起一次MinorGC。
大对象直接进入老年代
大对象指需要大量连续内存空间的对象,典型是很长的字符串或数量庞大的数组。大对象容易导致内存还有不少空间就提前触发垃圾收集以获得足够的连续空间。
HotSot提供了-XX:PtenuSizeTeshold参数,大于该值的对象直接在老年代分配,避免在Eden和Survivor间来回复制。
长期存活对象进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头。如果经历过第一次MinorGC仍然存活且能被Survivor容纳,该对象就会被移动到Survivor中并将年龄设置为1。对象在Survivor中每熬过一次MinorGC年龄就加1,当增加到一定程度(默认15)就会被晋升到老年代。对象晋升老年代的阈值可通过-XX:MaxTenuringTeshold设置。
动态对象年龄判定
为了适应不同内存状况,虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在Survivor中相同年龄所有对象大小的总和大于Survivor的一半,年龄不小于该年龄的对象就可以直接进入老年代。
空间分配担保
MinorGC前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次MinorGC确定安全。
如果不满足,虚拟机会查看-XX:HandlePromotionFailu参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次MinorGC,否则改成一次FlGC。
冒险是因为新生代使用复制算法,为了内存利用率只使用一个Survivor,大量对象在MinorGC后仍然存活时,需要老年代进行分配担保,接收Survivor无法容纳的对象。Q7:你知道哪些故障处理工具?
js:虚拟机进程状况工具
功能和s命令类似:可以列出正在运行的虚拟机进程,显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID(LVMID)。LVMID与操作系统的进程ID(PID)一致,使用Windows的任务管理器或UNIX的s命令也可以查询到虚拟机进程的LVMID,但如果同时启动了多个虚拟机进程,必须依赖js命令。
jstat:虚拟机统计信息监视工具
用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有GUI界面的服务器上是运行期定位虚拟机性能问题的常用工具。
参数含义:S0和S1表示两个Survivor,E表示新生代,O表示老年代,YGC表示YoungGC次数,YGCT表示YoungGC耗时,FGC表示FlGC次数,FGCT表示FlGC耗时,GCT表示GC总耗时。
jinfo:Java配置信息工具
实时查看和调整虚拟机各项参数,使用js的-v参数可以查看虚拟机启动时显式指定的参数,但如果想知道未显式指定的参数值只能使用jinfo的-flag查询。
jma:Java内存映像工具
用于生成堆转储快照,还可以查询finaze执行队列、Java堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。和jinfo一样,部分功能在Windows受限,除了生成堆转储快照的-dum和查看每个类实例的-histo外,其余选项只能在Linux使用。
jhat:虚拟机堆转储快照分析工具
JDK提供jhat与jma搭配使用分析jma生成的堆转储快照。jhat内置了一个微型的HTTPWeb服务器,生成堆转储快照的分析结果后可以在浏览器查看。
jstack:Java堆栈跟踪工具
用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。线程出现停顿时通过jstack查看各个线程的调用堆栈,可以获知没有响应的线程在后台做什么或等什么资源。类加载机制7
Q1:Java程序是怎样运行的?
首先通过Javac编译器将.java转为JVM可加载的.class字节码文件。
Javac是由Java编写的程序,编译过程可以分为:①词法解析,通过空格分割出单词、操作符、控制符等信息,形成token信息流,传递给语法解析器。②语法解析,把token信息流按照Java语法规则组装成语法树。③语义分析,检查关键字使用是否合理、类型是否匹配、作用域是否正确等。④字节码生成,将前面各个步骤的信息转换为字节码。
字节码必须通过类加载过程加载到JVM后才可以执行,执行有三种模式,解释执行、JIT编译执行、JIT编译与解释器混合执行(主流JVM默认执行的方式)。混合模式的优势在于解释器在启动时先解释执行,省去编译时间。
之后通过即时编译器JIT把字节码文件编译成本地机器码。
Java程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会认定其为"热点代码",热点代码的检测主要有基于采样和基于计数器两种方式,为了提高热点代码的执行效率,虚拟机会把它们编译成本地机器码,尽可能对代码优化,在运行时完成这个任务的后端编译器被称为即时编译器。
还可以通过静态的提前编译器AOT直接把程序编译成与目标机器指令集相关的二进制代码。Q2:类加载是什么?
Class文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程称为虚拟机的类加载机制。
与编译时需要连接的语言不同,Java中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java动态扩展的语言特性就是依赖运行期动态加载和连接实现的。
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持Java的动态绑定。Q3:类初始化的情况有哪些?
①遇到new、getstatic、utstatic或invokestatic字节码指令时,还未初始化。典型场景包括new实例化对象、读取或设置静态字段、调用静态方法。
②对类反射调用时,还未初始化。
③初始化类时,父类还未初始化。
④虚拟机启动时,会先初始化包含main方法的主类。
⑤使用JDK7的动态语言支持时,如果MeodHandle实例的解析结果为指定类型的方法句柄且句柄对应的类还未初始化。
⑥接口定义了默认方法,如果接口的实现类初始化,接口要在其之前初始化。
其余所有引用类型的方式都不会触发初始化,称为被动引用。被动引用实例:①子类使用父类的静态字段时,只有父类被初始化。②通过数组定义使用类。③常量在编译期会存入调用类的常量池,不会初始化定义常量的类。
接口和类加载过程的区别:初始化类时如果父类没有初始化需要初始化父类,但接口初始化时不要求父接口初始化,只有在真正使用父接口时(如引用接口中定义的常量)才会初始化。Q4:类加载的过程是什么?
加载
该阶段虚拟机需要完成三件事:①通过一个类的全限定类名获取定义类的二进制字节流。②将字节流所代表的静态存储结构转化为方法区的运行时数据区。③在内存中生成对应该类的Class实例,作为方法区这个类的数据访问入口。
验证
确保Class文件的字节流符合约束。如果虚拟机不检查输入的字节流,可能因为载入有错误或恶意企图的字节流而导致系统受攻击。验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证重要但非必需,因为只有通过与否的区别,通过后对程序运行期没有任何影响。如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验证缩短类加载时间。
准备
为类静态变量分配内存并设置零值,该阶段进行的内存分配仅包括类变量,不包括实例变量。如果变量被final修饰,编译时Javac会为变量生成ConstantValue属性,准备阶段虚拟机会将变量值设为代码值。
解析
将常量池内的符号引用替换为直接引用。
符号引用以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。
直接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局相关,引用目标必须已在虚拟机的内存中存在。
初始化
直到该阶段JVM才开始执行类中编写的代码。准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是执行类构造方法中的<cent>方法,该方法是Javac自动生成的。Q5:有哪些类加载器?
自JDK1.2起Java一直保持三层类加载器:
启动类加载器
在JVM启动时创建,负责加载最核心的类,例如Object、System等。无法被程序直接引用,如果需要把加载委派给启动类加载器,直接使用nl代替即可,因为启动类加载器通常由操作系统实现,并不存在于JVM体系。
平台类加载器
从JDK9开始从扩展类加载器更换为平台类加载器,负载加载一些扩展的系统类,比如XML、加密、压缩相关的功能类等。
应用类加载器
也称系统类加载器,负责加载用户类路径上的类库,可以直接在代码中使用。如果没有自定义类加载器,一般情况下应用类加载器就是默认的类加载器。自定义类加载器通过继承ClassLoader并重写findClass方法实现。Q6:双亲委派模型是什么?
类加载器具有等级制度但非继承关系,以组合的方式复用父加载器的功能。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父加载器。
一个类加载器收到了类加载请求,它不会自己去尝试加载,而将该请求委派给父加载器,每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器,只有当父加载器反馈无法完成请求时,子加载器才会尝试。
类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性。Q7:如何判断两个类是否相等?
任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性。
两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个Class文件,被同一个JVM加载,只要类加载器不同,这两个类就必定不相等。并发39
JMM8
Q1:JMM的作用是什么?
Java线程的通信由JMM控制,JMM的主要目的是定义程序中各种变量的访问规则。变量包括实例字段、静态字段,但不包括局部变量与方法参数,因为它们是线程私有的,不存在多线程竞争。JMM遵循一个基本原则:只要不改变程序执行结果,编译器和处理器怎么优化都行。例如编译器分析某个锁只会单线程访问就消除锁,某个volatile变量只会单线程访问就把它当作普通变量。
JMM规定所有变量都存储在主内存,每条线程有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。不同线程间无法直接访问对方工作内存中的变量,线程通信必须经过主内存。
关于主内存与工作内存的交互,即变量如何从主内存拷贝到工作内存、从工作内存同步回主内存,JMM定义了8种原子操作:
操作
作用变量范围
作用
lock
主内存
把变量标识为线程独占状态
unlock
主内存
释放处于锁定状态的变量
ad
主内存
把变量值从主内存传到工作内存
load
工作内存
把ad得到的值放入工作内存的变量副本
user
工作内存
把工作内存中的变量值传给执行引擎
assign
工作内存
把从执行引擎接收的值赋给工作内存变量
sto
工作内存
把工作内存的变量值传到主内存
write
主内存
把sto取到的变量值放入主内存变量中Q2:as-if-serial是什么?
不管怎么重排序,单线程程序的执行结果不能改变,编译器和处理器必须遵循as-if-serial语义。
为了遵循as-if-serial,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执行结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
as-if-serial把单线程程序保护起来,给程序员一种幻觉:单线程程序是按程序的顺序执行的。Q3:haens-befo是什么?
先行发生原则,JMM定义的两项操作间的偏序关系,是判断数据是否存在竞争的重要手段。
JMM将haens-befo要求禁止的重排序按是否会改变程序执行结果分为两类。对于会改变结果的重排序JMM要求编译器和处理器必须禁止,对于不会改变结果的重排序,JMM不做要求。
JMM存在一些天然的haens-befo关系,无需任何同步器协助就已经存在。如果两个操作的关系不在此列,并且无法从这些规则推导出来,它们就没有顺序性保障,虚拟机可以对它们随意进行重排序。**程序次序规则:**一个线程内写在前面的操作先行发生于后面的。
管程锁定规则:unlock操作先行发生于后面对同一个锁的lock操作。
**volatile规则:**对volatile变量的写操作先行发生于后面的读操作。
**线程启动规则:**线程的start方法先行发生于线程的每个动作。
**线程终止规则:**线程中所有操作先行发生于对线程的终止检测。
**对象终结规则:**对象的初始化先行发生于finaze方法。
**传递性:**如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
Q4:as-if-serial和haens-befo有什么区别?
as-if-serial保证单线程程序的执行结果不变,haens-befo保证正确同步的多线程程序的执行结果不变。
这两种语义的目的都是为了在不改变程序执行结果的前提下尽可能提高程序执行并行度。Q5:什么是指令重排序?
为了提高性能,编译器和处理器通常会对指令进行重排序,重排序指从源代码到指令序列的重排序,分为三种:①编译器优化的重排序,编译器在不改变单线程程序语义的前提下可以重排语句的执行顺序。②指令级并行的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。③内存系统的重排序。Q6:原子性、可见性、有序性分别是什么?
原子性
基本数据类型的访问都具备原子性,例外就是long和double,虚拟机将没有被volatile修饰的64位数据操作划分为两次32位操作。
如果应用场景需要更大范围的原子性保证,JMM还提供了lock和unlock操作满足需求,尽管JVM没有把这两种操作直接开放给用户使用,但是提供了更高层次的字节码指令monitonter和monitoxit,这两个字节码指令反映到Java代码中就是synconized。
可见性
可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。JMM通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新的方式实现可见性,无论普通变量还是volatile变量都是如此,区别是volatile保证新值能立即同步到主内存以及每次使用前立即从主内存刷新。
除了volatile外,synconized和final也可以保证可见性。同步块可见性由"对一个变量执行unlock前必须先把此变量同步回主内存,即先执行sto和write"这条规则获得。final的可见性指:被final修饰的字段在构造方法中一旦初始化完成,并且构造方法没有把is引用传递出去,那么其他线程就能看到final字段的值。
有序性
有序性可以总结为:在本线程内观察所有操作是有序的,在一个线程内观察另一个线程,所有操作都是无序的。前半句指as-if-serial语义,后半句指指令重排序和工作内存与主内存延迟现象。
Java提供volatile和synconized保证有序性,volatile本身就包含禁止指令重排序的语义,而synconized保证一个变量在同一时刻只允许一条线程对其进行lock操作,确保持有同一个锁的两个同步块只能串行进入。Q7:谈一谈volatile
JMM为volatile定义了一些特殊访问规则,当变量被定义为volatile后具备两种特性:
保证变量对所有线程可见
当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。volatile变量在各个线程的工作内存中不存在一致性问题,但Java的运算操作符并非原子操作,导致volatile变量运算在并发下仍不安全。
禁止指令重排序优化
使用volatile变量进行写操作,汇编指令带有lock前缀,相当于一个内存屏障,后面的指令不能重排到内存屏障之前。
使用lock前缀引发两件事:①将当前处理器缓存行的数据写回系统内存。②使其他处理器的缓存无效。相当于对缓存变量做了一次sto和write操作,让volatile变量的修改对其他处理器立即可见。
静态变量i执行多线程i++的不安全问题
自增语句由4条字节码指令构成的,依次为getstatic、iconst_1、iadd、utstatic,当getstatic把i的值取到操作栈顶时,volatile保证了i值在此刻正确,但在执行iconst_1、iadd时,其他线程可能已经改变了i值,操作栈顶的值就变成了过期数据,所以utstatic执行后就可能把较小的i值同步回了主内存。
适用场景
①运算结果并不依赖变量的当前值。②一写多读,只有单一的线程修改变量值。
内存语义
写一个volatile变量时,把该线程工作内存中的值刷新到主内存。
读一个volatile变量时,把该线程工作内存值置为无效,从主内存读取。
指令重排序特点
第二个操作是volatile写,不管第一个操作是什么都不能重排序,确保写之前的操作不会被重排序到写之后。
第一个操作是volatile读,不管第二个操作是什么都不能重排序,确保读之后的操作不会被重排序到读之前。
第一个操作是volatile写,第二个操作是volatile读不能重排序。
JSR-133增强volatile语义的原因
在旧的内存模型中,虽然不允许volatile变量间重排序,但允许volatile变量与普通变量重排序,可能导致内存不可见问题。JSR-133严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。Q8:final可以保证可见性吗?
final可以保证可见性,被final修饰的字段在构造方法中一旦被初始化完成,并且构造方法没有把is引用传递出去,在其他线程中就能看见final字段值。
在旧的JMM中,一个严重缺陷是线程可能看到final值改变。比如一个线程看到一个int类型final值为0,此时该值是未初始化前的零值,一段时间后该值被某线程初始化,再去读这个final值会发现值变为1。
为修复该漏洞,JSR-133为final域增加重排序规则:只要对象是正确构造的(被构造对象的引用在构造方法中没有逸出),那么不需要使用同步就可以保证任意线程都能看到这个final域初始化后的值。
写final域重排序规则
禁止把final域的写重排序到构造方法之外,编译器会在final域的写后,构造方法的turn前,插入一个StoSto屏障。确保在对象引用为任意线程可见之前,对象的final域已经初始化过。
读final域重排序规则
在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器在读final域操作的前面插入一个LoadLoad屏障,确保在读一个对象的final域前一定会先读包含这个final域的对象引用。锁17
Q1:谈一谈synconized
每个Java对象都有一个关联的monitor,使用synconized时JVM会根据使用环境找到对象的monitor,根据monitor的状态进行加解锁的判断。如果成功加锁就成为该monitor的唯一持有者,monitor在被释放前不能再被其他线程获取。
同步代码块使用monitonter和monitoxit这两个字节码指令获取和释放monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的Class对象;对于同步方法块,锁是synconized括号里的对象。
执行monitonter指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加1,执行monitoxit指令时会将锁计数器减1。一旦计数器为0锁随即就被释放。
例如有两个线程A、B竞争monitor,当A竞争到锁时会将monitor中的owner设置为A,把B阻塞并放到等待资源的ContentionList队列。ContentionList中的部分线程会进入EnyList,EnyList中的线程会被指定为OnDeck竞争候选者,如果获得了锁资源将进入Owner状态,释放锁后进入!Owner状态。被阻塞的线程会进入WaitSet。
被synconized修饰的同步块对一条线程来说是可重入的,并且同步块在持有锁的线程释放锁前会阻塞其他线程进入。从执行成本的角度看,持有锁是一个重量级的操作。Java线程是映射到操作系统的内核线程上的,如果要阻塞或唤醒一条线程,需要操作系统帮忙完成,不可避免用户态到核心态的转换。
不公平的原因
所有收到锁请求的线程首先自旋,如果通过自旋也没有获取锁将被放入ContentionList,该做法对于已经进入队列的线程不公平。
为了防止ContentionList尾部的元素被大量线程进行CAS访问影响性能,Owner线程会在释放锁时将ContentionList的部分线程移动到EnyList并指定某个线程为OnDeck线程,该行为叫做竞争切换,牺牲了公平性但提高了性能。Q2:锁优化有哪些策略?
JDK6对synconized做了很多优化,引入了自适应自旋、锁消除、锁粗化、偏向锁和轻量级锁等提高锁的效率,锁一共有4个状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,状态会随竞争情况升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高锁获得和释放的效率。Q3:自旋锁是什么?
同步对性能最大的影响是阻塞,挂起和恢复线程的操作都需要转入内核态完成。许多应用上共享数据的锁定只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得。如果机器有多个处理器核心,我们可以让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。为了让线程等待只需让线程执行一个忙循环,这项技术就是自旋锁。
自旋锁在JDK1.4就已引入,默认关闭,在JDK6中改为默认开启。自旋不能代替阻塞,虽然避免了线程切换开销,但要占用处理器时间,如果锁被占用的时间很短,自旋的效果就会非常好,反之只会白白消耗处理器资源。如果自旋超过了限定的次数仍然没有成功获得锁,就应挂起线程,自旋默认限定次数是10。Q4:什么是自适应自旋?
JDK6对自旋锁进行了优化,自旋时间不再固定,而是由前一次的自旋时间及锁拥有者的状态决定。
如果在同一个锁上,自旋刚刚成功获得过锁且持有锁的线程正在运行,虚拟机会认为这次自旋也很可能成功,进而允许自旋持续更久。如果自旋很少成功,以后获取锁时将可能直接省略掉自旋,避免浪费处理器资源。
有了自适应自旋,随着程序运行时间的增长,虚拟机对程序锁的状况预测就会越来越精准。Q5:锁消除是什么?
锁消除指即时编译器对检测到不可能存在共享数据竞争的锁进行消除。
主要判定依据来源于逃逸分析,如果判断一段代码中堆上的所有数据都只被一个线程访问,就可以当作栈上的数据对待,认为它们是线程私有的而无须同步。Q6:锁粗化是什么?
原则需要将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中进行同步,这是为了使等待锁的线程尽快拿到锁。
但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之外的,即使没有线程竞争也会导致不必要的性能消耗。因此如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把同步的范围扩展到整个操作序列的外部。Q7:偏向锁是什么?
偏向锁是为了在没有竞争的情况下减少锁开销,锁会偏向于第一个获得它的线程,如果在执行过程中锁一直没有被其他线程获取,则持有偏向锁的线程将不需要进行同步。
当锁对象第一次被线程获取时,虚拟机会将对象头中的偏向模式设为1,同时使用CAS把获取到锁的线程ID记录在对象的MarkWord中。如果CAS成功,持有偏向锁的线程以后每次进入锁相关的同步块都不再进行任何同步操作。
一旦有其他线程尝试获取锁,偏向模式立即结束,根据锁对象是否处于锁定状态决定是否撤销偏向,后续同步按照轻量级锁那样执行。Q8:轻量级锁是什么?
轻量级锁是为了在没有竞争的前提下减少重量级锁使用操作系统互斥量产生的性能消耗。
在代码即将进入同步块时,如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前MarkWord的拷贝。然后虚拟机使用CAS尝试把对象的MarkWord更新为指向锁记录的指针,如果更新成功即代表该线程拥有了锁,锁标志位将转变为00,表示处于轻量级锁定状态。
如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的MarkWord是否指向当前线程的栈帧,如果是则说明当前线程已经拥有了锁,直接进入同步块继续执行,否则说明锁对象已经被其他线程抢占。如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为10,此时MarkWord存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。
解锁同样通过CAS进行,如果对象MarkWord仍然指向线程的锁记录,就用CAS把对象当前的MarkWord和线程复制的MarkWord替换回来。假如替换成功同步过程就顺利完成了,如果失败则说明有其他线程尝试过获取该锁,就要在释放锁的同时唤醒被挂起的线程。Q9:偏向锁、轻量级锁和重量级锁的区别?
偏向锁的优点是加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块的场景。
轻量级锁的优点是竞争线程不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会自旋消耗CPU,适用追求响应时间、同步代码块执行快的场景。
重量级锁的优点是线程竞争不使用自旋不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐量、同步代码块执行慢的场景。Q10:Lock和synconized有什么区别?
Lock接是juc包的顶层接口,基于Lock接口,用户能够以非块结构来实现互斥同步,摆脱了语言特性束缚,在类库层面实现同步。Lock并未用到synconized,而是利用了volatile的可见性。
重入锁ReenantLock是Lock最常见的实现,与synconized一样可重入,不过它增加了一些高级功能:**等待可中断:**持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待而处理其他事情。
公平锁:公平锁指多个线程在等待同一个锁时,必须按照申请锁的顺序来依次获得锁,而非公平锁不保证这一点,在锁被释放时,任何线程都有机会获得锁。synconized是非公平的,ReenantLock在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会急剧下降,影响吞吐量。
锁绑定多个条件:一个ReenantLock可以同时绑定多个Condition。synconized中锁对象的wait跟notify可以实现一个隐含条件,如果要和多个条件关联就不得不额外添加锁,而ReenantLock可以多次调用newCondition创建多个条件。一般优先考虑使用synconized:①synconized是语法层面的同步,足够简单。②Lock必须确保在finally中释放锁,否则一旦抛出异常有可能永远不会释放锁。使用synconized可以由JVM来确保即使出现异常锁也能正常释放。③尽管JDK5时ReenantLock的性能优于synconized,但在JDK6进行锁优化后二者的性能基本持平。从长远来看JVM更容易针对synconized优化,因为JVM可以在线程和对象的元数据中记录synconized中锁的相关信息,而使用Lock的话JVM很难得知具体哪些锁对象是由特定线程持有的。Q11:ReenantLock的可重入是怎么实现的?
以非公平锁为例,通过nonfairTryAcqui方法获取锁,该方法增加了再次获取同步状态的处理逻辑:判断当前线程是否为获取锁的线程来决定获取是否成功,如果是获取锁的线程再次请求则将同步状态值增加并返回ue,表示获取同步状态成功。
成功获取锁的线程再次获取锁将增加同步状态值,释放同步状态时将减少同步状态值。如果锁被获取了n次,那么前n-1次yRelease方法必须都返回fasle,只有同步状态完全释放才能返回ue,该方法将同步状态是否为0作为最终释放条件,释放时将占有线程设置为nl并返回ue。
对于非公平锁只要CAS设置同步状态成功则表示当前线程获取了锁,而公平锁则不同。公平锁使用yAcqui方法,该方法与nonfairTryAcqui的唯一区别就是判断条件中多了对同步队列中当前节点是否有前驱节点的判断,如果该方法返回ue表示有线程比当前线程更早请求锁,因此需要等待前驱线程获取并释放锁后才能获取锁。Q12:什么是读写锁?
ReenantLock是排他锁,同一时刻只允许一个线程访问,读写锁在同一时刻允许多个读线程访问,在写线程访问时,所有的读写线程均阻塞。读写锁维护了一个读锁和一个写锁,通过分离读写锁使并发性相比排他锁有了很大提升。
读写锁依赖AQS来实现同步功能,读写状态就是其同步器的同步状态。读写锁的自定义同步器需要在同步状态,即一个int变量上维护多个读线程和一个写线程的状态。读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。
写锁是可重入排他锁,如果当前线程已经获得了写锁则增加写状态,如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获得写锁的线程则进入等待。写锁的释放与ReenantLock的释放类似,每次释放减少写状态,当写状态为0时表示写锁已被释放。
读锁是可重入共享锁,能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功获取。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取则进入等待。读锁每次释放会减少读状态,减少的值是(1<<16),读锁的释放是线程安全的。
锁降级指把持住当前拥有的写锁,再获取读锁,随后释放先前拥有的写锁。
锁降级中读锁的获取是必要的,这是为了保证数据可见性,如果当前线程不获取读锁而直接释放写锁,假设此刻另一个线程A获取写锁修改了数据,当前线程无法感知线程A的数据更新。如果当前线程获取读锁,遵循锁降级的步骤,A将被阻塞,直到当前线程使用数据并释放读锁之后,线程A才能获取写锁进行数据更新。Q13:AQS了解吗?
AQS队列同步器是用来构建锁或其他同步组件的基础框架,它使用一个volatileintstate变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态进行更改需要使用同步器提供的3个方法getState、setState和comaAndSetState,它们保证状态改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅定义若干同步状态获取和释放的方法,同步器既支持独占式也支持共享式。
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁面向使用者,定义了使用者与锁交互的接口,隐藏实现细节;同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。
每当有新线程请求资源时都会进入一个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资源。等待队列通过双向链表实现,线程被封装在链表的Node节点中,Node的等待状态包括:CANCELLED(线程已取消)、SIGNAL(线程需要唤醒)、CONDITION(线程正在等待)、PROPAGATE(后继节点会传播唤醒操作,只在共享模式下起作用)。Q14:AQS有哪两种模式?
独占模式表示锁只会被一个线程占用,其他线程必须等到持有锁的线程释放锁后才能获取锁,同一时间只能有一个线程获取到锁。
共享模式表示多个线程获取同一个锁有可能成功,ReadLock就采用共享模式。
独占模式通过acqui和lease方法获取和释放锁,共享模式通过acquiShad和leaseShad方法获取和释放锁。Q15:AQS独占式获取释放锁的原理?
获取同步状态时,调用acqui方法,维护一个同步队列,使用yAcqui方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过addWaiter方法加入到同步队列的尾部,在队列中自旋。之后调用acquiQueued方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停止自旋的条件是前驱节点是头结点且成功获取了同步状态。
释放同步状态时,同步器调用yRelease方法释放同步状态,然后调用unarkSuccessor方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。Q16:为什么只有前驱节点是头节点时才能尝试获取同步状态?
头节点是成功获取到同步状态的节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
目的是维护同步队列的FIFO原则,节点和节点在循环检查的过程中基本不通信,而是简单判断自己的前驱是否为头节点,这样就使节点的释放规则符合FIFO,并且也便于对过早通知的处理,过早通知指前驱节点不是头节点的线程由于中断被唤醒。Q17:AQS共享式式获取释放锁的原理?
获取同步状态时,调用acquiShad方法,该方法调用yAcquiShad方法尝试获取同步状态,返回值为int类型,返回值不小于于0表示能获取同步状态。因此在共享式获取锁的自旋过程中,成功获取同步状态并退出自旋的条件就是该方法的返回值不小于0。
释放同步状态时,调用leaseShad方法,释放后会唤醒后续处于等待状态的节点。它和独占式的区别在于yReleaseShad方法必须确保同步状态安全释放,通过循环CAS保证,因为释放同步状态的操作会同时来自多个线程。线程13
Q1:线程的生命周期有哪些状态?
NEW:新建状态,线程被创建且未启动,此时还未调用start方法。
RUNNABLE:Java将操作系统中的就绪和运行两种状态统称为RUNNABLE,此时线程有可能在等待时间片,也有可能在执行。
BLOCKED:阻塞状态,可能由于锁被其他线程占用、调用了slee或join方法、执行了wait方法等。
WAITING:等待状态,该状态线程不会被分配CPU时间片,需要其他线程通知或中断。可能由于调用了无参的wait和join方法。
TIME_WAITING:限期等待状态,可以在指定时间内自行返回。导可能由于调用了带参的wait和join方法。
TERMINATED:终止状态,表示当前线程已执行完毕或异常退出。Q2:线程的创建方式有哪些?
①继承Tead类并重写run方法。实现简单,但不符合里氏替换原则,不可以继承其他类。
②实现Runnable接口并重写run方法。避免了单继承局限性,编程更加灵活,实现解耦。
③实现Callable接口并重写call方法。可以获取线程执行结果的返回值,并且可以抛出异常。Q3:线程有哪些方法?
①slee方法会导致当前线程进入休眠状态,与wait不同的是该方法不会释放锁资源,进入的是TIMED-WAITING状态。
②yiled方法使当前线程让出CPU时间片给优先级相同或更高的线程,回到RUNNABLE状态,与其他线程一起重新竞争CPU时间片。
③join方法用于等待其他线程运行终止,如果当前线程调用了另一个线程的join方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间片。底层使用的是wait,也会释放锁。Q4:什么是守护线程?
守护线程是一种支持型线程,可以通过setDaemon(ue)将线程设置为守护线程,但必须在线程启动前设置。
守护线程被用于完成支持性工作,但在JVM退出时守护线程中的finally块不一定执行,因为JVM中没有非守护线程时需要立即退出,所有守护线程都将立即终止,不能靠在守护线程使用finally确保关闭资源。Q5:线程通信的方式有哪些?
命令式编程中线程的通信机制有两种,共享内存和消息传递。在共享内存的并发模型里线程间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里线程间没有公共状态,必须通过发送消息来显式通信。Java并发采用共享内存模型,线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
volatile告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可见性。
synconized确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子性、可见性和有序性。
等待通知机制指一个线程A调用了对象的wait方法进入等待状态,另一线程B调用了对象的notifynotifyAll方法,线程A收到通知后结束阻塞并执行后序操作。对象上的wait和notifynotifyAll如同开关信号,完成等待方和通知方的交互。
如果一个线程执行了某个线程的join方法,这个线程就会阻塞等待执行了join方法的线程终止,这里涉及等待通知机制。join底层通过wait实现,线程终止时会调用自身的notifyAll方法,通知所有等待在该线程对象上的线程。
管道IO流用于线程间数据传输,媒介为内存。PiedOututSeam和PiedWriter是输出流,相当于生产者,PiedInutSeam和PiedReader是输入流,相当于消费者。管道流使用一个默认大小为1KB的循环缓冲数组。输入流从缓冲数组读数据,输出流往缓冲数组中写数据。当数组已满时,输出流所在线程阻塞;当数组首次为空时,输入流所在线程阻塞。
TeadLocal是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响。Q6:线程池有什么好处?
降低资源消耗,复用已创建的线程,降低开销、控制最大并发数。
隔离线程环境,可以配置独立线程池,将较慢的线程与较快的隔离开,避免相互影响。
实现任务线程队列缓冲策略和拒绝机制。
实现某些与时间相关的功能,如定时执行、周期执行等。Q7:线程池处理任务的流程?
①核心线程池未满,创建一个新的线程执行任务,此时workCount<coPoolSize。
②如果核心线程池已满,工作队列未满,将线程存储在工作队列,此时workCount>=coPoolSize。
③如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务,此时workCount<maximumPoolSize,这一步也需要获取全局锁。
④如果超过大小线程数,按照拒绝策略来处理任务,此时workCount>maximumPoolSize。
线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后还会循环获取工作队列中的任务来执行。Q8:有哪些创建线程池的方法?
可以通过Executors的静态工厂方法创建线程池:
①newFixedTeadPool,固定大小的线程池,核心线程数也是最大线程数,不存在空闲线程,keeAveTime=0。该线程池使用的工作队列是无界阻塞队列LinkedBlockingQueue,适用于负载较重的服务器。
②newSingleTeadExecutor,使用单线程,相当于单线程串行执行所有任务,适用于需要保证顺序执行任务的场景。
③newCachedTeadPool,maximumPoolSize设置为Integer最大值,是高度可伸缩的线程池。该线程池使用的工作队列是没有容量的SynconousQueue,如果主线程提交任务的速度高于线程处理的速度,线程池会不断创建新线程,极端情况下会创建过多线程而耗尽CPU和内存资源。适用于执行很多短期异步任务的小程序或负载较轻的服务器。
④newSchededTeadPool:线程数最大为Integer最大值,存在OOM风险。支持定期及周期性任务执行,适用需要多个后台线程执行周期任务,同时需要限制线程数量的场景。相比Timer更安全,功能更强,与newCachedTeadPool的区别是不回收工作线程。
⑤newWorkSteangPool:JDK8引入,创建持有足够线程的线程池支持给定的并行度,通过多个队列减少竞争。Q9:创建线程池有哪些参数?
①coPoolSize:常驻核心线程数,如果为0,当执行完任务没有任何请求时会消耗线程池;如果大于0,即使本地任务执行完,核心线程也不会被销毁。该值设置过大会浪费资源,过小会导致线程的频繁创建与销毁。
②maximumPoolSize:线程池能够容纳同时执行的线程最大数,必须大于等于1,如果与核心线程数设置相同代表固定大小线程池。
③keeAveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下coPoolSize个线程为止,避免浪费内存资源。
④unit:keeAveTime的时间单位。
⑤workQueue:工作队列,当线程请求数大于等于coPoolSize时线程会进入阻塞队列。
⑥teadFactory:线程工厂,用来生产一组相同任务的线程。可以给线程命名,有利于分析错误。
⑦handler:拒绝策略,默认使用AbortPocy丢弃任务并抛出异常,CallerRunsPocy表示重新尝试提交该任务,DiscardOldestPocy表示抛弃队列里等待最久的任务并把当前任务加入队列,DiscardPocy表示直接抛弃当前任务但不抛出异常。Q10:如何关闭线程池?
可以调用shuown或shuownNow方法关闭线程池,原理是遍历线程池中的工作线程,然后逐个调用线程的interrut方法中断线程,无法响应中断的任务可能永远无法终止。
区别是shuownNow首先将线程池的状态设为STOP,然后尝试停止正在执行或暂停任务的线程,并返回等待执行任务的列表。而shuown只是将线程池的状态设为SHUTDOWN,然后中断没有正在执行任务的线程。
通常调用shuown来关闭线程池,如果任务不一定要执行完可调用shuownNow。Q11:线程池的选择策略有什么?
可以从以下角度分析:①任务性质:CPU密集型、IO密集型和混合型。②任务优先级。③任务执行时间。④任务依赖性:是否依赖其他资源,如数据库连接。
性质不同的任务可用不同规模的线程池处理,CPU密集型任务应配置尽可能小的线程,如配置Ncu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,应配置尽可能多的线程,如2*Ncu。混合型的任务,如果可以拆分,将其拆分为一个CPU密集型任务和一个IO密集型任务,只要两个任务执行的时间相差不大那么分解后的吞吐量将高于串行执行的吞吐量,如果相差太大则没必要分解。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue处理。
执行时间不同的任务可以交给不同规模的线程池处理,或者使用优先级队列让执行时间短的任务先执行。
依赖数据库连接池的任务,由于线程提交SQL后需要等待数据库返回的结果,等待的时间越长CPU空闲的时间就越长,因此线程数应该尽可能地设置大一些,提高CPU的利用率。
建议使用有界队列,能增加系统的稳定性和预警能力,可以根据需要设置的稍微大一些。Q12:阻塞队列有哪些选择?
阻塞队列支持阻塞插入和移除,当队列满时,阻塞插入元素的线程直到队列不满。当队列为空时,获取元素的线程会被阻塞直到队列非空。阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器。
Java中的阻塞队列
ArrayBlockingQueue,由数组组成的有界阻塞队列,默认情况下不保证线程公平,有可能先阻塞的线程最后才访问队列。
LinkedBlockingQueue,由链表结构组成的有界阻塞队列,队列的默认和最大长度为Integer最大值。
PriorityBlockingQueue,支持优先级的无界阻塞队列,默认情况下元素按照升序排序。可自定义comaTo方法指定排序规则,或者初始化时指定Comarator排序,不能保证同优先级元素的顺序。
DelayQueue,支持延时获取元素的无界阻塞队列,使用优先级队列实现。创建元素时可以指定多久才能从队列中获取当前元素,只有延迟期满时才能从队列中获取元素,适用于缓存和定时调度。
SynconousQueue,不存储元素的阻塞队列,每一个ut必须等待一个take。默认使用非公平策略,也支持公平策略,适用于传递性场景,吞吐量高。
LinkedTransferQueue,链表组成的无界阻塞队列,相对于其他阻塞队列多了yTransfer和ansfer方法。ansfer方法:如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者,否则会将元素放在队列的尾节点并等到该元素被消费者消费才返回。yTransfer方法用来试探生产者传入的元素能否直接传给消费者,如果没有消费者等待接收元素则返回false,和ansfer的区别是无论消费者是否消费都会立即返回。
LinkedBlockingDeque,链表组成的双向阻塞队列,可从队列的两端插入和移出元素,多线程同时入队时减少了竞争。
实现原理
使用通知模式实现,生产者往满的队列里添加元素时会阻塞,当消费者消费后,会通知生产者当前队列可用。当往队列里插入一个元素,如果队列不可用,阻塞生产者主要通过LockSuort的ark方法实现,不同操作系统中实现方式不同,在Linux下使用的是系统方法tead_cond_wait实现。Q13:谈一谈TeadLocal
TeadLoacl是线程共享变量,主要用于一个线程内跨类、方法传递数据。TeadLoacl有一个静态内部类TeadLocalMa,其Key是TeadLocal对象,值是Eny对象,Eny中只有一个Object类的vae值。TeadLocal是线程共享的,但TeadLocalMa是每个线程私有的。TeadLocal主要有set、get和move三个方法。
set方法
首先获取当前线程,然后再获取当前线程对应的TeadLocalMa类型的对象ma。如果ma存在就直接设置值,key是当前的TeadLocal对象,value是传入的参数。
如果ma不存在就通过cateMa方法为当前线程创建一个TeadLocalMa对象再设置值。
get方法
首先获取当前线程,然后再获取当前线程对应的TeadLocalMa类型的对象ma。如果ma存在就以当前TeadLocal对象作为key获取Eny类型的对象e,如果e存在就返回它的value属性。
如果e不存在或者ma不存在,就调用setInitialValue方法先为当前线程创建一个TeadLocalMa对象然后返回默认的初始值nl。
move方法
首先通过当前线程获取其对应的TeadLocalMa类型的对象m,如果m不为空,就解除TeadLocal这个key及其对应的value值的联系。
存在的问题
线程复用会产生脏数据,由于线程池会重用Tead对象,因此与Tead绑定的TeadLocal也会被重用。如果没有调用move清理与线程相关的TeadLocal信息,那么假如下一个线程没有调用set设置初始值就可能get到重用的线程信息。
TeadLocal还存在内存泄漏的问题,由于TeadLocal是弱引用,但Eny的value是强引用,因此当TeadLocal被垃圾回收后,value依旧不会被释放。因此需要及时调用move方法进行清理操作。JUC11
Q1:什么是CAS?
CAS表示ComaAndSwa,比较并交换,CAS需要三个操作数,分别是内存位置V、旧的预期值A和准备设置的新值B。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。但不管是否更新都会返回V的旧值,这些处理过程是原子操作,执行期间不会被其他线程打断。
在JDK5后,Java类库中才开始使用CAS操作,该操作由Unsafe类里的comaAndSwaInt等几个方法包装提供。HotSot在内部对这些方法做了特殊处理,即时编译的结果是一条平台相关的处理器CAS指令。Unsafe类不是给用户程序调用的类,因此JDK9前只有Java类库可以使用CAS,譬如juc包里的AtomicInteger类中comaAndSet等方法都使用了Unsafe类的CAS操作实现。Q2:CAS有什么问题?
CAS从语义上来说存在一个逻辑漏洞:如果V初次读取时是A,并且在准备赋值时仍为A,这依旧不能说明它没有被其他线程更改过,因为这段时间内假设它的值先改为B又改回A,那么CAS操作就会误认为它从来没有被改变过。
这个漏洞称为ABA问题,juc包提供了一个AtomicStamedRefence,原子更新带有版本号的引用类型,通过控制变量值的版本来解决ABA问题。大部分情况下ABA不会影响程序并发的正确性,如果需要解决,传统的互斥同步可能会比原子类更高效。Q3:有哪些原子类?
JDK5提供了java.util.concurnt.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。到JDK8该包共有17个类,依据作用分为四种:原子更新基本类型类、原子更新数组类、原子更新引用类以及原子更新字段类,atomic包里的类基本都是使用Unsafe实现的包装类。
AtomicInteger原子更新整形、AtomicLong原子更新长整型、AtomicBoolean原子更新布尔类型。
AtomicIntegerArray,原子更新整形数组里的元素、AtomicLongArray原子更新长整型数组里的元素、AtomicRefenceArray原子更新引用类型数组里的元素。
AtomicRefence原子更新引用类型、AtomicMarkableRefence原子更新带有标记位的引用类型,可以绑定一个boolean标记、AtomicStamedRefence原子更新带有版本号的引用类型,关联一个整数值作为版本号,解决ABA问题。
AtomicIntegerFieldUdater原子更新整形字段的更新器、AtomicLongFieldUdater原子更新长整形字段的更新器AtomicRefenceFieldUdater原子更新引用类型字段的更新器。Q4:AtomicIntger实现原子更新的原理是什么?
AtomicInteger原子更新整形、AtomicLong原子更新长整型、AtomicBoolean原子更新布尔类型。
getAndIncment以原子方式将当前的值加1,首先在for死循环中取得AtomicInteger里存储的数值,第二步对AtomicInteger当前的值加1,第三步调用comaAndSet方法进行原子更新,先检查当前数值是否等于exect,如果等于则说明当前值没有被其他线程修改,则将值更新为next,否则会更新失败返回false,程序会进入for循环重新进行comaAndSet操作。
atomic包中只提供了三种基本类型的原子更新,atomic包里的类基本都是使用Unsafe实现的,Unsafe只提供三种CAS方法:comaAndSwaInt、comaAndSwaLong和comaAndSwaObject,例如原子更新Boolean是先转成整形再使用comaAndSwaInt。Q5:CountDownLatch是什么?
CountDownLatch是基于执行时间的同步类,允许一个或多个线程等待其他线程完成操作,构造方法接收一个int参数作为计数器,如果要等待n个点就传入n。每次调用countDown方法时计数器减1,await方法会阻塞当前线程直到计数器变为0,由于countDown方法可用在任何地方,所以n个点既可以是n个线程也可以是一个线程里的n个执行步骤。Q6:CyccBarrier是什么?
循环屏障是基于同步到达某个点的信号量触发机制,作用是让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障才会解除。构造方法中的参数表示拦截线程数量,每个线程调用await方法告诉CyccBarrier自己已到达屏障,然后被阻塞。还支持在构造方法中传入一个Runnable任务,当线程到达屏障时会优先执行该任务。适用于多线程计算数据,最后合并计算结果的应用场景。
CountDownLac的计数器只能用一次,而CyccBarrier的计数器可使用set方法重置,所以CyccBarrier能处理更为复杂的业务场景,例如计算错误时可用重置计数器重新计算。Q7:Semaho是什么?
信号量用来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理使用公共资源。信号量可以用于流量控制,特别是公共资源有限的应用场景,比如数据库连接。
Semaho的构造方法参数接收一个int值,表示可用的许可数量即最大并发数。使用acqui方法获得一个许可证,使用lease方法归还许可,还可以用yAcqui尝试获得许可。Q8:Exchanger是什么?
交换者是用于线程间协作的工具类,用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。
两个线程通过exchange方法交换数据,第一个线程执行exchange方法后会阻塞等待第二个线程执行该方法,当两个线程都到达同步点时这两个线程就可以交换数据,将本线程生产出的数据传递给对方。应用场景包括遗传算法、校对工作等。P9:JDK7的ConcurntHashMa原理?
ConcurntHashMa用于解决HashMa的线程不安全和HashTable的并发效率低,HashTable之所以效率低是因为所有线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器的部分数据,那么多线程访问容器不同数据段的数据时,线程间就不会存在锁竞争,从而有效提高并发效率,这就是ConcurntHashMa的锁分段技术。首先将数据分成Segment数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。
get实现简单高效,先经过一次再散列,再用这个散列值通过散列运算定位到Segment,最后通过散列算法定位到元素。get的高效在于不需要加锁,除非读到空值才会加锁重读。get方法中将共享变量定义为volatile,在get操作里只需要读所以不用加锁。
ut必须加锁,首先定位到Segment,然后进行插入操作,第一步判断是否需要对Segment里的HashEny数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。
size操作用于统计元素的数量,必须统计每个Segment的大小然后求和,在统计结果累加的过程中,之前累加过的count变化几率很小,因此先尝试两次通过不加锁的方式统计结果,如果统计过程中容器大小发生了变化,再加锁统计所有Segment大小。判断容器是否发生变化根据modCount确定。P10:JDK8的ConcurntHashMa原理?
主要对JDK7做了三点改造:①取消分段锁机制,进一步降低冲突概率。②引入红黑树结构,同一个哈希槽上的元素个数超过一定阈值后,单向链表改为红黑树结构。③使用了更加优化的方式统计集合内的元素数量。具体优化表现在:在ut、size和size方法中设计元素总数的更新和计算都避免了锁,使用CAS代替。
get同样不需要同步,ut操作时如果没有出现哈希冲突,就使用CAS添加元素,否则使用synconized加锁添加元素。
当某个槽内的元素个数达到7且容量不小于64时,链表转为红黑树。当某个槽内的元素减少到6时,由红黑树重新转为链表。在转化过程中,使用同步块锁住当前槽的首元素,防止其他线程对当前槽进行增删改操作,转化完成后利用CAS替换原有链表。由于TeNode节点也存储了next引用,因此红黑树转为链表很简单,只需从first元素开始遍历所有节点,并把节点从TeNode转为Node类型即可,当构造好新链表后同样用CAS替换红黑树。P11:ArrayList的线程安全集合是什么?
可以使用CoyOnWriteArrayList代替ArrayList,它实现了读写分离。写操作复制一个新的集合,在新集合内添加或删除元素,修改完成后再将原集合的引用指向新集合。这样做的好处是可以高并发地进行读写操作而不需要加锁,因为当前集合不会添加任何元素。使用时注意尽量设置容量初始值,并且可以使用批量添加或删除,避免多次扩容,比如只增加一个元素却复制整个集合。
适合读多写少,单个添加时效率极低。CoyOnWriteArrayList是fail-safe的,并发包的集合都是这种机制,fail-safe在安全的副本上遍历,集合修改与副本遍历没有任何关系,缺点是无法读取最新数据。这也是CAP理论中C和A的矛盾,即一致性与可用性的矛盾。框架27
SringIoC11
Q1:IoC是什么?
IoC即控制反转,简单来说就是把原来代码里需要实现的对象创建、依赖反转给容器来帮忙实现,需要创建一个容器并且需要一种描述让容器知道要创建的对象间的关系,在Sring中管理对象及其依赖关系是通过Sring的IoC容器实现的。
IoC的实现方式有依赖注入和依赖查找,由于依赖查找使用的很少,因此IoC也叫做依赖注入。依赖注入指对象被动地接受依赖类而不用自己主动去找,对象不是从容器中查找它依赖的类,而是在容器实例化对象时主动将它依赖的类注入给它。假设一个Car类需要一个Engine的对象,那么一般需要需要手动new一个Engine,利用IoC就只需要定义一个私有的Engine类型的成员变量,容器会在运行时自动创建一个Engine的实例对象并将引用自动注入给成员变量。Q2:IoC容器初始化过程?
基于XML的容器初始化
当创建一个ClassPaXmlAcationContext时,构造方法做了两件事:①调用父容器的构造方法为容器设置好Bean资源加载器。②调用父类的setConfigLocations方法设置Bean配置信息的定位路径。
ClassPaXmlAcationContext通过调用父类AbsactAcationContext的fsh方法启动整个IoC容器对Bean定义的载入过程,fsh是一个模板方法,规定了IoC容器的启动流程。在创建IoC容器前如果已有容器存在,需要把已有的容器销毁,保证在fsh方法后使用的是新创建的IoC容器。
容器创建后通过loadBeanDefinitions方法加载Bean配置资源,该方法做两件事:①调用资源加载器的方法获取要加载的资源。②真正执行加载功能,由子类XmlBeanDefinitionReader实现。加载资源时首先解析配置文件路径,读取配置文件的内容,然后通过XML解析器将Bean配置信息转换成文档对象,之后按照SringBean的定义规则对文档对象进行解析。
SringIoC容器中注册解析的Bean信息存放在一个HashMa集合中,key是字符串,值是BeanDefinition,注册过程中需要使用synconized保证线程安全。当配置信息中配置的Bean被解析且被注册到IoC容器中后,初始化就算真正完成了,Bean定义信息已经可以使用且可被检索。SringIoC容器的作用就是对这些注册的Bean定义信息进行处理和维护,注册的Bean定义信息是控制反转和依赖注入的基础。
基于注解的容器初始化
分为两种:①直接将注解Bean注册到容器中,可以在初始化容器时注册,也可以在容器创建之后手动注册,然后刷新容器使其对注册的注解Bean进行处理。②通过扫描指定的包及其子包的所有类处理,在初始化注解容器时指定要自动扫描的路径。Q3:依赖注入的实现方法有哪些?
构造方法注入:IoCServiceProvider会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。这种方法的优点是在对象构造完成后就处于就绪状态,可以马上使用。缺点是当依赖对象较多时,构造方法的参数列表会比较长,构造方法无法被继承,无法设置默认值。对于非必需的依赖处理可能需要引入多个构造方法,参数数量的变动可能会造成维护的困难。
setter方法注入:当前对象只需要为其依赖对象对应的属性添加setter方法,就可以通过setter方法将依赖对象注入到被依赖对象中。setter方法注入在描述性上要比构造方法注入强,并且可以被继承,允许设置默认值。缺点是无法在对象构造完成后马上进入就绪状态。
接口注入:必须实现某个接口,接口提供方法来为其注入依赖对象。使用少,因为它强制要求被注入对象实现不必要接口,侵入性强。Q4:依赖注入的相关注解?
@Autowid:自动按类型注入,如果有多个匹配则按照指定Bean的id查找,查找不到会报错。
@Quafier:在自动按照类型注入的基础上再按照Bean的id注入,给变量注入时必须搭配@Autowid,给方法注入时可单独使用。
@Resource:直接按照Bean的id注入,只能注入Bean类型。
@Value:用于注入基本数据类型和Sing类型。Q5:依赖注入的过程?
getBean方法获取Bean实例,该方法会调用doGetBean,doGetBean真正实现从IoC容器获取Bean的功能,也是触发依赖注入的地方。
具体创建Bean对象的过程由ObjectFactory的cateBean完成,该方法主要通过cateBeanInstance方法生成Bean包含的Java对象实例和oateBean方法对Bean属性的依赖注入进行处理。
在oateBean方法中,注入过程主要分为两种情况:①属性值类型不需要强制转换时,不需要解析属性值,直接进行依赖注入。②属性值类型需要强制转换时,首先解析属性值,然后对解析后的属性值进行依赖注入。依赖注入的过程就是将Bean对象实例设置到它所依赖的Bean对象属性上,真正的依赖注入是通过setProertyValues方法实现的,该方法使用了委派模式。
BeanWraerIml类负责对完成初始化的Bean对象进行依赖注入,对于非集合类型属性,使用JDK反射,通过属性的setter方法为属性设置注入后的值。对于集合类型的属性,将属性值解析为目标类型的集合后直接赋值给属性。
当容器对Bean的定位、载入、解析和依赖注入全部完成后就不再需要手动创建对象,IoC容器会自动为我们创建对象并且注入依赖。Q6:Bean的生命周期?
在IoC容器的初始化过程中会对Bean定义完成资源定位,加载读取配置并解析,最后将解析的Bean信息放在一个HashMa集合中。当IoC容器初始化完成后,会进行对Bean实例的创建和依赖注入过程,注入对象依赖的各种属性值,在初始化时可以指定自定义的初始化方法。经过这一系列初始化操作后Bean达到可用状态,接下来就可以使用Bean了,当使用完成后会调用desoy方法进行销毁,此时也可以指定自定义的销毁方法,最终Bean被销毁且从容器中移除。
XML方式通过配置bean标签中的init-Meod和destory-Meod指定自定义初始化和销毁方法。
注解方式通过@PConct和@PostConct注解指定自定义初始化和销毁方法。Q7:Bean的作用范围?
通过scoe属性指定bean的作用范围,包括:
①singleton:单例模式,是默认作用域,不管收到多少Bean请求每个容器中只有一个唯一的Bean实例。
②rototye:原型模式,和singleton相反,每次Bean请求都会创建一个新的实例。
③quest:每次HTTP请求都会创建一个新的Bean并把它放到quest域中,在请求完成后Bean会失效并被垃圾收集器回收。
④session:和quest类似,确保每个session中有一个Bean实例,session过期后bean会随之失效。
⑤globalsession:当应用部署在Portlet容器时,如果想让所有Portlet共用全局存储变量,那么该变量需要存储在globalsession中。Q8:如何通过XML方式创建Bean?
默认无参构造方法,只需要指明bean标签中的id和class属性,如果没有无参构造方法会报错。
静态工厂方法,通过bean标签中的class属性指明静态工厂,factory-meod属性指明静态工厂方法。
实例工厂方法,通过bean标签中的factory-bean属性指明实例工厂,factory-meod属性指明实例工厂方法。Q9:如何通过注解创建Bean?
@Comonent把当前类对象存入Sring容器中,相当于在xml中配置一个bean标签。value属性指定bean的id,默认使用当前类的首字母小写的类名。
@Conoller,@Service,@Reository三个注解都是@Comonent的衍生注解,作用及属性都是一模一样的。只是提供了更加明确语义,@Conoller用于表现层,@Service用于业务层,@Reository用于持久层。如果注解中有且只有一个value属性要赋值时可以省略value。
如果想将第三方的类变成组件又没有源代码,也就没办法使用@Comonent进行自动配置,这种时候就要使用@Bean注解。被@Bean注解的方法返回值是一个对象,将会实例化,配置和初始化一个新对象并返回,这个对象由Sring的IoC容器管理。name属性用于给当前@Bean注解方法创建的对象指定一个名称,即bean的id。当使用注解配置方法时,如果方法有参数,Sring会去容器查找是否有可用bean对象,查找方式和@Autowid一样。Q10:如何通过注解配置文件?
@Configuration用于指定当前类是一个sring配置类,当创建容器时会从该类上加载注解,value属性用于指定配置类的字节码。
@ComonentScan用于指定Sring在初始化容器时要扫描的包。basePackages属性用于指定要扫描的包。
@ProertySource用于加载.roerties文件中的配置。value属性用于指定文件位置,如果是在类路径下需要加上classa。
@Imort用于导入其他配置类,在引入其他配置类时可以不用再写@Configuration注解。有@Imort的是父配置类,引入的是子配置类。value属性用于指定其他配置类的字节码。Q11:BeanFactory、FactoryBean和AcationContext的区别?
BeanFactory是一个Bean工厂,使用简单工厂模式,是SringIoC容器顶级接口,可以理解为含有Bean集合的工厂类,作用是管理Bean,包括实例化、定位、配置对象及建立这些对象间的依赖。BeanFactory实例化后并不会自动实例化Bean,只有当Bean被使用时才实例化与装配依赖关系,属于延迟加载,适合多例模式。
FactoryBean是一个工厂Bean,使用了工厂方法模式,作用是生产其他Bean实例,可以通过实现该接口,提供一个工厂方法来自定义实例化Bean的逻辑。FactoryBean接口由BeanFactory中配置的对象实现,这些对象本身就是用于创建对象的工厂,如果一个Bean实现了这个接口,那么它就是创建对象的工厂Bean,而不是Bean实例本身。
AcationConext是BeanFactory的子接口,扩展了BeanFactory的功能,提供了支持际化的文本消息,统一的资源文件读取方式,事件传播以及应用层的特别配置等。容器会在初始化时对配置的Bean进行预实例化,Bean的依赖注入在容器初始化时就已经完成,属于立即加载,适合单例模式,一般推荐使用。SringAOP4
Q1:AOP是什么?
AOP即面向切面编程,简单地说就是将代码中重复的部分抽取出来,在需要执行的时候使用动态技术,在不修改源码的基础上对方法进行增强。
Sring根据类是否实现接口来判断动态方式,如果实现接口会使用JDK的动态,核心是InvocationHandler接口和Proxy类,如果没有实现接口会使用CGLib动态,CGLib是在运行时动态生成某个类的子类,如果某个类被标记为final,不能使用CGLib。
JDK动态主要通过重组字节码实现,首先获得被对象的引用和所有接口,生成新的类必须实现被类的所有接口,动态生成Java代码后编译新生成的.class文件并重新加载到JVM运行。JDK直接写Class字节码,CGLib是采用ASM框架写字节码,生成类的效率低。但是CGLib调用方法的效率高,因为JDK使用反射调用方法,CGLib使用FastClass机制为类和被类各生成一个类,这个类会为类或被类的方法生成一个index,这个index可以作为参数直接定位要调用的方法。
常用场景包括权限认证、自动缓存、错误处理、日志、调试和事务等。Q2:AOP的相关注解有哪些?
@Asect:声明被注解的类是一个切面Bean。
@Befo:前置通知,指在某个连接点之前执行的通知。
@After:后置通知,指某个连接点退出时执行的通知(不论正常返回还是异常退出)。
@AfterReturning:返回后通知,指某连接点正常完成之后执行的通知,返回值使用turning属性接收。
@AfterTowing:异常通知,指方法抛出异常导致退出时执行的通知,和@AfterReturning只会有一个执行,异常使用towing属性接收。Q3:AOP的相关术语有什么?
Asect:切面,一个关注点的模块化,这个关注点可能会横切多个对象。
Joinoint:连接点,程序执行过程中的某一行为,即业务层中的所有方法。。
Advice:通知,指切面对于某个连接点所产生的动作,包括前置通知、后置通知、返回后通知、异常通知和环绕通知。
Pointcut:切入点,指被拦截的连接点,切入点一定是连接点,但连接点不一定是切入点。
Proxy:,SringAOP中有JDK动态和CGLib,目标对象实现了接口时采用JDK动态,反之采用CGLib。
Target:的目标对象,指一个或多个切面所通知的对象。
Weaving:织入,指把增强应用到目标对象来创建对象的过程。Q4:AOP的过程?
SringAOP由BeanPostProcessor后置处理器开始,这个后置处理器是一个监听器,可以监听容器触发的Bean生命周期事件,向容器注册后置处理器以后,容器中管理的Bean就具备了接收IoC容器回调事件的能力。BeanPostProcessor的调用发生在SringIoC容器完成Bean实例对象的创建和属性的依赖注入后,为Bean对象添加后置处理器的入口是initiazeBean方法。
Sring中JDK动态通过JdkDynamicAoProxy调用Proxy的newInstance方法来生成类,JdkDynamicAoProxy也实现了InvocationHandler接口,invoke方法的具体逻辑是先获取应用到此方法上的拦截器链,如果有拦截器则创建MeodInvocation并调用其roceed方法,否则直接反射调用目标方法。因此SringAOP对目标对象的增强是通过拦截器实现的。SringMVC3
Q1:SringMVC的处理流程?
Web容器启动时会通知Sring初始化容器,加载Bean的定义信息并初始化所有单例Bean,然后遍历容器中的Bean,获取每一个Conoller中的所有方法访问的URL,将URL和对应的Conoller保存到一个Ma集合中。
所有的请求会转发给DisatcherServlet前端处理器处理,DisatcherServlet会请求HandlerMaing找出容器中被@Conoler注解修饰的Bean以及被@RequestMaing修饰的方法和类,生成Handler和HandlerIntercetor并以一个HandlerExcutionChain处理器执行链的形式返回。
之后DisatcherServlet使用Handler找到对应的HandlerAater,通过HandlerAater调用Handler的方法,将请求参数绑定到方法的形参上,执行方法处理请求并得到ModelAndView。
最后DisatcherServlet根据使用ViewResolver试图解析器对得到的ModelAndView逻辑视图进行解析得到View物理视图,然后对视图渲染,将数据填充到视图中并返回给客户端。Q2:SringMVC有哪些组件?
DisatcherServlet:SringMVC中的前端控制器,是整个流程控制的核心,负责接收请求并转发给对应的处理组件。
Handler:处理器,完成具体业务逻辑,相当于Servlet或Action。
HandlerMaing:完成URL到Conoller映射,DisatcherServlet通过HandlerMaing将不同请求映射到不同Handler。
HandlerIntercetor:处理器拦截器,是一个接口,如果需要完成一些拦截处理,可以实现该接口。
HandlerExecutionChain:处理器执行链,包括两部分内容:Handler和HandlerIntercetor。
HandlerAdater:处理器适配器,Handler执行业务方法前需要进行一系列操作,包括表单数据验证、数据类型转换、将表单数据封装到JavaBean等,这些操作都由HandlerAdater完成。DisatcherServlet通过HandlerAdater来执行不同的Handler。
ModelAndView:装载模型数据和视图信息,作为Handler处理结果返回给DisatcherServlet。
ViewResolver:视图解析器,DisatcherServlet通过它将逻辑视图解析为物理视图,最终将渲染的结果响应给客户端。Q3:SringMVC的相关注解?
@Conoller:在类定义处添加,将类交给IoC容器管理。
@RequtestMaing:将URL请求和业务方法映射起来,在类和方法定义上都可以添加该注解。value属性指定URL请求的实际地址,是默认值。meod属性限制请求的方法类型,包括GET、POST、PUT、DELETE等。如果没有使用指定的请求方法请求URL,会报405MeodNotAllowed错误。arams属性限制必须提供的参数,如果没有会报错。
@RequestParam:如果Conoller方法的形参和URL参数名一致可以不添加注解,如果不一致可以使用该注解绑定。value属性表示HTTP请求中的参数名。quid属性设置参数是否必要,默认false。defatValue属性指定没有给参数赋值时的默认值。
@PaVariable:SringMVC支持RESTf风格URL,通过@PaVariable完成请求参数与形参的绑定。SringDataJPA4
Q1:ORM是什么?
ORM即Object-RelationalMaing,表示对象关系映射,映射的不只是对象的值还有对象之间的关系,通过ORM就可以把对象映射到关系型数据库中。操作实体类就相当于操作数据库表,可以不再重点关注SQL语句。Q2:JPA如何使用?
只需要持久层接口继承JaReository即可,泛型参数列表中第一个参数是实体类类型,第二个参数是主键类型。
运行时通过JdkDynamicAoProxy的invoke方法创建了一个动态对象SimleJaReository,SimleJaReository中封装了JPA的操作,通过hibernate(封装了JDBC)完成数据库操作。Q3:JPA实体类相关注解有哪些?
@Entity:表明当前类是一个实体类。
@Table:关联实体类和数据库表。
@Column:关联实体类属性和数据库表中字段。
@Id:声明当前属性为数据库表主键对应的属性。
@GeneratedValue:配置主键生成策略。
@OneToMany:配置一对多关系,maedBy属性值为主表实体类在从表实体类中对应的属性名。
@ManyToOne:配置多对一关系,targetEntity属性值为主表对应实体类的字节码。
@JoinColumn:配置外键关系,name属性值为外键名称,fencedColumnName属性值为主表主键名称。Q4:对象导航查询是什么?
通过get方法查询一个对象的同时,通过此对象可以查询它的关联对象。
对象导航查询一到多默认使用延迟加载的形式,关联对象是集合,因此使用立即加载可能浪费资源。
对象导航查询多到一默认使用立即加载的形式,关联对象是一个对象,因此使用立即加载。
如果要改变加载方式,在实体类注解配置加上fetch属性即可,LAZY表示延迟加载,EAGER表示立即加载。Mybatis5
Q1:Mybatis的优缺点?
优点
相比JDBC减少了大量代码量,减少冗余代码。
使用灵活,SQL语句写在XML里,从程序代码中彻底分离,降低了耦合度,便于管理。
提供XML标签,支持编写动态SQL语句。
提供映射标签,支持对象与数据库的ORM字段映射关系。
缺点
SQL语句编写工作量较大,尤其是字段和关联表多时。
SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。Q2:Mybatis的XML文件有哪些标签属性?
select、insert、udate、delete标签分别对应查询、添加、更新、删除操作。
arameterTye属性表示参数的数据类型,包括基本数据类型和对应的包装类型、Sing和JavaBean类型,当有多个参数时可以使用#{argn}的形式表示第n个参数。除了基本数据类型都要以全限定类名的形式指定参数类型。
stTye表示返回的结果类型,包括基本数据类型和对应的包装类型、Sing和JavaBean类型。还可以使用把返回结果封装为复杂类型的stMa。Q3:Mybatis的一级缓存是什么?
一级缓存是SqlSession级别,默认开启且不能关闭。
操作数据库时需要创建SqlSession对象,对象中有一个HashMa存储缓存数据,不同SqlSession之间缓存数据区域互不影响。
一级缓存的作用域是SqlSession范围的,在同一个SqlSession中执行两次相同的SQL语句时,第一次执行完毕会将结果保存在缓存中,第二次查询直接从缓存中获取。
如果SqlSession执行了DML操作(insert、udate、delete),Mybatis必须将缓存清空保证数据有效性。Q4:Mybatis的二级缓存是什么?
二级缓存是Maer级别,默认关闭。
使用二级缓存时多个SqlSession使用同一个Maer的SQL语句操作数据库,得到的数据会存在二级缓存区,同样使用HashMa进行数据存储,相比于一级缓存,二级缓存范围更大,多个SqlSession可以共用二级缓存,作用域是Maer的同一个namesace,不同SqlSession两次执行相同的namesace下的SQL语句,参数也相等,则第一次执行成功后会将数据保存在二级缓存中,第二次可直接从二级缓存中取出数据。
要使用二级缓存,需要在全局配置文件中配置<settingname="cacheEnabled"value="ue">,再在对应的映射文件中配置一个<cache>标签。Q5:Mybatis#{}和${}的区别?
使用${}相当于使用字符串拼接,存在SQL注入的风险。
使用#{}相当于使用占位符,可以防止SQL注入,不支持使用占位符的地方就只能使用${},典型情况就是动态参数。数据结构和算法13
数据结构4
Q1:什么是AVL树?
AVL树是平衡二叉查找树,增加和删除节点后通过树形旋转重新达到平衡。右旋是以某个节点为中心,将它沉入当前右子节点的位置,而让当前的左子节点作为新树的根节点,也称为顺时针旋转。同理左旋是以某个节点为中心,将它沉入当前左子节点的位置,而让当前的右子节点作为新树的根节点,也称为逆时针旋转。Q2:什么是红黑树?
红黑树是1972年发明的,称为对称二叉B树,1978年正式命名红黑树。主要特征是在每个节点上增加一个属性表示节点颜色,可以红色或黑色。红黑树和AVL树类似,都是在进行插入和删除时通过旋转保持自身平衡,从而获得较高的查找性能。与AVL树相比,红黑树不追求所有递归子树的高度差不超过1,保证从根节点到叶尾的最长路径不超过最短路径的2倍,所以最差时间复杂度是O(logn)。红黑树通过重新着色和左右旋转,更加高效地完成了插入和删除之后的自平衡调整。
红黑树在本质上还是二叉查找树,它额外引入了5个约束条件:①节点只能是红色或黑色。②根节点必须是黑色。③所有NIL节点都是黑色的。④一条路径上不能出现相邻的两个红色节点。⑤在任何递归子树中,根节点到叶子节点的所有路径上包含相同数目的黑色节点。这五个约束条件保证了红黑树的新增、删除、查找的最坏时间复杂度均为O(logn)。如果一个树的左子节点或右子节点不存在,则均认定为黑色。红黑树的任何旋转在3次之内均可完成。Q3:AVL树和红黑树的区别?
红黑树的平衡性不如AVL树,它维持的只是一种大致的平衡,不严格保证左右子树的高度差不超过1。这导致节点数相同的情况下,红黑树的高度可能更高,也就是说平均查找次数会高于相同情况的AVL树。
在插入时,红黑树和AVL树都能在至多两次旋转内恢复平衡,在删除时由于红黑树只追求大致平衡,因此红黑树至多三次旋转可以恢复平衡,而AVL树最多需要O(logn)次。AVL树在插入和删除时,将向上回溯确定是否需要旋转,这个回溯的时间成本最差为O(logn),而红黑树每次向上回溯的步长为2,回溯成本低。因此面对频繁地插入与删除红黑树更加合适。Q4:B树和B+树的区别?
B树中每个节点同时存储key和data,而B+树中只有叶子节点才存储data,非叶子节点只存储key。InnoDB对B+树进行了优化,在每个叶子节点上增加了一个指向相邻叶子节点的链表指针,形成了带有顺序指针的B+树,提高区间访问的性能。
B+树的优点在于:①由于B+树在非叶子节点上不含数据信息,因此在内存页中能够存放更多的key,数据存放得更加紧密,具有更好的空间利用率,访问叶子节点上关联的数据也具有更好的缓存命中率。②B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子节点即可。而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。但是B树也有优点,由于每个节点都包含key和value,因此经常访问的元素可能离根节点更近,访问也更迅速。排序9
Q1:排序有哪些分类?
排序可以分为内部排序和外部排序,在内存中进行的称为内部排序,当数据量很大时无法全部拷贝到内存需要使用外存,称为外部排序。
内部排序包括比较排序和非比较排序,比较排序包括插入选择交换归并排序,非比较排序包括计数基数桶排序。
插入排序包括直接插入希尔排序,选择排序包括直接选择堆排序,交换排序包括冒泡快速排序。Q2:直接插入排序的原理?
稳定,平均最差时间复杂度O(n2),元素基本有序时最好时间复杂度O(n),空间复杂度O(1)。
每一趟将一个待排序记录按其关键字的大小插入到已排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
ubcvoidinsertionSort(int[]nums){
for(inti=1;i<nums.leng;i++){
intinsertNum=nums[i];
intinsertIndex;
for(insertIndex=i-1;insertIndex>=0&am;&am;nums[insertIndex]>insertNum;insertIndex--){
nums[insertIndex+1]=nums[insertIndex];
}
nums[insertIndex+1]=insertNum;
}
}直接插入没有利用到要插入的序列已有序的特点,插入第i个元素时可以通过二分查找找到插入位置insertIndex,再把i~insertIndex之间的所有元素后移一位,把第i个元素放在插入位置上。
ubcvoidbinaryInsertionSort(int[]nums){
for(inti=1;i<nums.leng;i++){
intinsertNum=nums[i];
intinsertIndex=-1;
intstart=0;
intend=i-1;
while(start<=end){
intmid=start+(end-start)2;
if(insertNum>nums[mid])
start=mid+1;
elseif(insertNum<nums[mid])
end=mid-1;
else{
insertIndex=mid+1;
bak;
}
}
if(insertIndex==-1)
insertIndex=start;
if(i-insertIndex>=0)
System.arraycoy(nums,insertIndex,nums,insertIndex+1,i-insertIndex);
nums[insertIndex]=insertNum;
}
}
Q3:希尔排序的原理?
又称缩小增量排序,是对直接插入排序的改进,不稳定,平均时间复杂度O(n1.3),最差时间复杂度O(n2),最好时间复杂度O(n),空间复杂度O(1)。
把记录按下标的一定增量分组,对每组进行直接插入排序,每次排序后减小增量,当增量减至1时排序完毕。
ubcvoidshellSort(int[]nums){
for(intd=nums.leng2;d>0;d=2){
for(inti=d;i<nums.leng;i++){
intinsertNum=nums[i];
intinsertIndex;
for(insertIndex=i-d;insertIndex>=0&am;&am;nums[insertIndex]>insertNum;insertIndex-=d){
nums[insertIndex+d]=nums[insertIndex];
}
nums[insertIndex+d]=insertNum;
}
}
}
Q4:直接选择排序的原理?
不稳定,时间复杂度O(n2),空间复杂度O(1)。
每次在未排序序列中找到最小元素,和未排序序列的第一个元素交换位置,再在剩余未排序序列中重复该操作直到所有元素排序完毕。
ubcvoidselectSort(int[]nums){
intminIndex;
for(intindex=0;index<nums.leng-1;index++){
minIndex=index;
for(inti=index+1;i<nums.leng;i++){
if(nums[i]<nums[minIndex])
minIndex=i;
}
if(index!=minIndex){
swa(nums,index,minIndex);
}
}
}
Q5:堆排序的原理?
是对直接选择排序的改进,不稳定,时间复杂度O(nlogn),空间复杂度O(1)。
将待排序记录看作完全二叉树,可以建立大根堆或小根堆,大根堆中每个节点的值都不小于它的子节点值,小根堆中每个节点的值都不大于它的子节点值。
以大根堆为例,在建堆时首先将最后一个节点作为当前节点,如果当前节点存在父节点且值大于父节点,就将当前节点和父节点交换。在移除时首先暂存根节点的值,然后用最后一个节点代替根节点并作为当前节点,如果当前节点存在子节点且值小于子节点,就将其与值较大的子节点进行交换,调整完堆后返回暂存的值。
ubcvoidadd(int[]nums,inti,intnum){
nums[i]=num;
intcurIndex=i;
while(curIndex>0){
intantIndex=(curIndex-1)2;
if(nums[antIndex]<nums[curIndex])
swa(nums,antIndex,curIndex);
elsebak;
curIndex=antIndex;
}
}ubcintmove(int[]nums,intsize){
intst=nums[0];
nums[0]=nums[size-1];
intcurIndex=0;
while(ue){
intleftIndex=curIndex*2+1;
intrightIndex=curIndex*2+2;
if(leftIndex>=size)bak;
intmaxIndex=leftIndex;
if(rightIndex<size&am;&am;nums[maxIndex]<nums[rightIndex])
maxIndex=rightIndex;
if(nums[curIndex]<nums[maxIndex])
swa(nums,curIndex,maxIndex);
elsebak;
curIndex=maxIndex;
}
turnst;
}
Q6:冒泡排序的原理?
稳定,平均最坏时间复杂度O(n2),元素基本有序时最好时间复杂度O(n),空间复杂度O(1)。
比较相邻的元素,如果第一个比第二个大就进行交换,对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,每一轮排序后末尾元素都是有序的,针对n个元素重复以上步骤n-1次排序完毕。
ubcvoidbubbleSort(int[]nums){
for(inti=0;i<nums.leng-1;i++){
for(intindex=0;index<nums.leng-1-i;index++){
if(nums[index]>nums[index+1])
swa(nums,index,index+1)
}
}
}当序列已经有序时仍会进行不必要的比较,可以设置一个标志记录是否有元素交换,如果没有直接结束比较。
ubcvoidbetterBubbleSort(int[]nums){
booleanswa;
for(inti=0;i<nums.leng-1;i++){
swa=ue;
for(intindex=0;index<nums.leng-1-i;index++){
if(nums[index]>nums[index+1]){
swa(nums,index,index+1);
swa=false;
}
}
if(swa)bak;
}
}
Q7:快速排序的原理?
是对冒泡排序的一种改进,不稳定,平均最好时间复杂度O(nlogn),元素基本有序时最坏时间复杂度O(n2),空间复杂度O(logn)。
首先选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全部小于等于基准元素,一部分全部大于等于基准元素,再按此方法递归对这两部分数据进行快速排序。
快速排序的一次划分从两头交替搜索,直到low和high指针重合,一趟时间复杂度O(n),整个算法的时间复杂度与划分趟数有关。
最好情况是每次划分选择的中间数恰好将当前序列等分,经过log(n)趟划分便可得到长度为1的子表,这样时间复杂度O(nlogn)。
最坏情况是每次所选中间数是当前序列中的最大或最小元素,这使每次划分所得子表其中一个为空表,这样长度为n的数据表需要n趟划分,整个排序时间复杂度O(n2)。
ubcvoidquickSort(int[]nums,intstart,intend){
if(start<end){
intivotIndex=getPivotIndex(nums,start,end);
quickSort(nums,start,ivotIndex-1);
quickSort(nums,ivotIndex+1,end);
}
}ubcintgetPivotIndex(int[]nums,intstart,intend){
intivot=nums[start];
intlow=start;
inthigh=end;
while(low<high){
while(low<=high&am;&am;nums[low]<=ivot)
low++;
while(low<=high&am;&am;nums[high]>ivot)
high--;
if(low<high)
swa(nums,low,high);
}
swa(nums,start,high);
turnhigh;
}**优化:**当规模足够小时,例如end-start<10时,采用直接插入排序。Q8:归并排序的原理?
归并排序基于归并操作,是一种稳定的排序算法,任何情况时间复杂度都为O(nlogn),空间复杂度为O(n)。
**基本原理:**应用分治法将待排序序列分成两部分,然后对两部分分别递归排序,最后进行合并,使用一个辅助空间并设定两个指针分别指向两个有序序列的起始元素,将指针对应的较小元素添加到辅助空间,重复该步骤到某一序列到达末尾,然后将另一序列剩余元素合并到辅助空间末尾。
**适用场景:**数据量大且对稳定性有要求的情况。
int[]hel;ubcvoidmergeSort(int[]arr){
int[]hel=newint[arr.leng];
sort(arr,0,arr.leng-1);
}ubcvoidsort(int[]arr,intstart,intend){
if(start==end)turn;
intmid=start+(end-start)2;
sort(arr,start,mid);
sort(arr,mid+1,end);
merge(arr,start,mid,end);
}ubcvoidmerge(int[]arr,intstart,intmid,intend){
if(end+1-start>=0)System.arraycoy(arr,start,hel,start,end+1-start);
int=start;
intq=mid+1;
intindex=start;
while(<=mid&am;&am;q<=end){
if(hel[]<hel[q])
arr[index++]=hel[++];
else
arr[index++]=hel[q++];
}
while(<=mid)arr[index++]=hel[++];
while(q<=end)arr[index++]=hel[q++];
}
Q9:排序算法怎么选择?
数据量规模较小,考虑直接插入或直接选择。当元素分布有序时直接插入将大大减少比较和移动记录的次数,如果不要求稳定性,可以使用直接选择,效率略高于直接插入。
数据量规模中等,选择希尔排序。
数据量规模较大,考虑堆排序(元素分布接近正序或逆序)、快速排序(元素分布随机)和归并排序(稳定性)。
一般不使用冒泡。设计模式15
Q1:设计模式有哪些原则?
**开闭原则:**OOP中最基础的原则,指一个软件实体(类、模块、方法等)应该对扩展开放,对修改关闭。强调用抽象构建框架,用实现扩展细节,提高代码的可复用性和可维护性。
**单一职责原则:**一个类、接口或方法只负责一个职责,降低代码复杂度以及变更引起的风险。
**依赖倒置原则:**程序应该依赖于抽象类或接口,而不是具体的实现类。
**接口隔离原则:**将不同功能定义在不同接口中实现接口隔离,避免了类依赖它不需要的接口,减少了接口之间依赖的冗余性和复杂性。
**里氏替换原则:**开闭原则的补充,规定了任何父类可以出现的地方子类都一定可以出现,可以约束继承泛滥,加强程序健壮性。
**迪米特原则:**也叫最少知道原则,每个模块对其他模块都要尽可能少地了解和依赖,降低代码耦合度。
**合成聚合原则:**尽量使用组合(has-a)聚合(contains-a)而不是继承(is-a)达到软件复用的目的,避免滥用继承带来的方法污染和方法爆炸,方法污染指父类的行为通过继承传递给子类,但子类并不具备执行此行为的能力;方法爆炸指继承树不断扩大,底层类拥有的方法过于繁杂,导致很容易选择错误。Q2:设计模式的分类,你知道哪些设计模式?
创建型:在创建对象的同时隐藏创建逻辑,不使用new直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括工厂抽象工厂单例建造者原型模式。
**结构型:**通过类和接口间的继承和引用实现创建复杂结构的对象。包括适配器桥接模式过滤器组合装饰器外观享元模式。
**行为型:**通过类之间不同通信方式实现不同行为。包括责任链命名解释器迭代器中介者备忘录观察者状态策略模板访问者模式。Q3:说一说简单工厂模式
简单工厂模式指由一个工厂对象来创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。
适用于工厂类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则,且产品多的话会使工厂类比较复杂。
Calendar抽象类的getInstance方法,调用cateCalendar方法根据不同的地区参数创建不同的日历对象。
Sring中的BeanFactory使用简单工厂模式,根据传入一个唯一的标识来获得Bean对象。Q4:说一说工厂方法模式
工厂方法模式指定义一个创建对象的接口,让接口的实现类决定创建哪种对象,让类的实例化推迟到子类中进行。
客户端只需关心对应工厂而无需关心创建细节,主要解决了产品扩展的问题,在简单工厂模式中如果产品种类变多,工厂的职责会越来越多,不便于维护。
Collection接口这个抽象工厂中定义了一个抽象的iterator工厂方法,返回一个Iterator类的抽象产品。该方法通过ArrayList、HashMa等具体工厂实现,返回I、KeyIterator等具体产品。
Sring的FactoryBean接口的getObject方法也是工厂方法。Q5:抽象工厂模式了解吗?
抽象工厂模式指提供一个创建一系列相关或相互依赖对象的接口,无需指定它们的具体类。
客户端不依赖于产品类实例如何被创建和实现的细节,主要用于系统的产品有多于一个的产品族,而系统只消费其中某一个产品族产品的情况。抽象工厂模式的缺点是不方便扩展产品族,并且增加了系统的抽象性和理解难度。
java.sql.Connection接口就是一个抽象工厂,其中包括很多抽象产品如Statement、Blob、Saveoint等。Q6:单例模式的特点是什么?
单例模式属于创建型模式,一个单例类在任何情况下都只存在一个实例,构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。
优点是内存中只有一个实例,减少了开销,尤其是频繁创建和销毁实例的情况下并且可以避免对资源的多重占用。缺点是没有抽象层,难以扩展,与单一职责原则冲突。
Sring的AcationContext创建的Bean实例都是单例对象,还有ServletContext、数据库连接池等也都是单例模式。Q7:单例模式有哪些实现?
**饿汉式:**在类加载时就初始化创建单例对象,线程安全,但不管是否使用都创建对象可能会浪费内存。
ubcclassHungrySingleton{
rivateHungrySingleton(){}rivatestaticHungrySingletoninstance=newHungrySingleton();ubcstaticHungrySingletongetInstance(){
turninstance;
}
}**懒汉式:**在外部调用时才会加载,线程不安全,可以加锁保证线程安全但效率低。
ubcclassLazySingleton{
rivateLazySingleton(){}rivatestaticLazySingletoninstance;ubcstaticLazySingletongetInstance(){
if(instance==nl){
instance=newLazySingleton();
}
turninstance;
}
}**双重检查锁:**使用volatile以及多重检查来减小锁范围,提升效率。
ubcclassDoubleCheckSingleton{
rivateDoubleCheckSingleton(){}rivatevolatilestaticDoubleCheckSingletoninstance;ubcstaticDoubleCheckSingletongetInstance(){
if(instance==nl){
synconized(DoubleCheckSingleton.class){
if(instance==nl){
instance=newDoubleCheckSingleton();
}
}
}
turninstance;
}
}**静态内部类:**同时解决饿汉式的内存浪费问题和懒汉式的线程安全问题。
ubcclassStaticSingleton{
rivateStaticSingleton(){}ubcstaticStaticSingletongetInstance(){
turnStaticClass.instance;
}rivatestaticclassStaticClass{
rivatestaticfinalStaticSingletoninstance=newStaticSingleton();
}
}枚举:《EffectiveJava》提倡的方式,不仅能避免线程安全问题,还能防止反序列化重新创建新的对象,绝对防止多次实例化,也能防止反射破解单例的问题。
ubcenumEnumSingleton{
INSTANCE;
}
Q8:讲一讲模式
模式属于结构型模式,为其他对象提供一种以控制对这个对象的访问。优点是可以增强目标对象的功能,降低代码耦合度,扩展性好。缺点是在客户端和目标对象之间增加对象会导致请求处理速度变慢,增加系统复杂度。
Sring利用动态实现AOP,如果Bean实现了接口就使用JDK,否则使用CGLib。
**静态:**对象持有被对象的引用,调用对象方法时也会调用被对象的方法,但是会在被对象方法的前后增加其他逻辑。需要手动完成,在程序运行前就已经存在类的字节码文件,类和被类的关系在运行前就已经确定了。缺点是一个类只能为一个目标服务,如果要服务多种类型会增加工作量。
**动态:**动态在程序运行时通过反射创建具体的类,类和被类的关系在运行前是不确定的。动态的适用性更强,主要分为JDK动态和CGLib动态。**JDK动态:**通过Proxy类的newInstance方法获取一个动态对象,需要传入三个参数,被对象的类加载器、被对象实现的接口,以及一个InvocationHandler调用处理器来指明具体的逻辑,相比静态的优势是接口中声明的所有方法都被转移到InvocationHandler的invoke方法集中处理。
**CGLib动态:**JDK动态要求实现被对象的接口,而CGLib要求继承被对象,如果一个类是final类则不能使用CGLib。两种都在运行期生成字节码,JDK动态直接写字节码,而CGLib动态使用ASM框架写字节码,ASM的目的是生成、转换和分析以字节数组表示的已编译Java类。JDK动态调用方法通过反射机制实现,而GCLib动态通过FastClass机制直接调用方法,它为类和被类各生成一个类,该类为类和被类的方法分配一个int参数,调用方法时可以直接定位,因此调用效率更高。
Q9:讲一讲装饰器模式
装饰器模式属于结构型模式,在不改变原有对象的基础上将功能附加到对象,相比继承可以更加灵活地扩展原有对象的功能。
装饰器模式适合的场景:在不想增加很多子类的前提下扩展一个类的功能。
java.io包中,InutSeam字节输入流通过装饰器BuffedInutSeam增强为缓冲字节输入流。Q10:装饰器模式和动态的区别?
装饰器模式的关注点在于给对象动态添加方法,而动态更注重对象的访问控制。动态通常会在类中创建被对象的实例,而装饰器模式会将装饰者作为构造方法的参数。Q11:讲一讲适配器模式
适配器模式属于结构型模式,它作为两个不兼容接口之间的桥梁,结合了两个独立接口的功能,将一个类的接口转换成另外一个接口使得原本由于接口不兼容而不能一起工作的类可以一起工作。
缺点是过多使用适配器会让系统非常混乱,不易整体把握。
java.io包中,InutSeam字节输入流通过适配器InutSeamReader转换为Reader字符输入流。
SringMVC中的HandlerAdater,由于handler有很多种形式,包括Conoller、HttRequestHandler、Servlet等,但调用方式又是确定的,因此需要适配器来进行处理,根据适配规则调用handle方法。
Arrays.asList方法,将数组转换为对应的集合(注意不能使用修改集合的方法,因为返回的ArrayList是Arrays的一个内部类)。Q12:适配器模式和和装饰器模式以及模式的区别?
适配器模式没有层级关系,适配器和被适配者没有必然连续,满足has-a的关系,解决不兼容的问题,是一种后置考虑。
装饰器模式具有层级关系,装饰器与被装饰者实现同一个接口,满足is-a的关系,注重覆盖和扩展,是一种前置考虑。
适配器模式主要改变所考虑对象的接口,而模式不能改变所类的接口。Q13:讲一讲策略模式
策略模式属于行为型模式,定义了一系列算法并封装起来,之间可以互相替换。策略模式主要解决在有多种算法相似的情况下,使用ifelse所带来的难以维护。
优点是算法可以自由切换,可以避免使用多重条件判断并且扩展性良好,缺点是策略类会增多并且所有策略类都需要对外暴露。
在集合框架中,经常需要通过构造方法传入一个比较器Comarator进行比较排序。Comarator就是一个抽象策略,一个类通过实现该接口并重写coma方法成为具体策略类。
创建线程池时,需要传入拒绝策略,当创建新线程使当前运行的线程数超过maximumPoolSize时会使用相应的拒绝策略处理。Q14:讲一讲模板模式
模板模式属于行为型模式,使子类可以在不改变算法结构的情况下重新定义算法的某些步骤,适用于抽取子类重复代码到公共父类。
优点是可以封装固定不变的部分,扩展可变的部分。缺点是每一个不同实现都需要一个子类维护,会增加类的数量。
为防止恶意操作,一般模板方法都以final修饰。
HttServlet定义了一套处理HTTP请求的模板,service方法为模板方法,定义了处理HTTP请求的基本流程,doXXX等方法为基本方法,根据请求方法的类型做相应的处理,子类可重写这些方法。Q15:讲一讲观察者模式
观察者模式属于行为型模式,也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。主要解决一个对象状态改变给其他对象通知的问题,缺点是如果被观察者对象有很多的直接和间接观察者的话通知很耗时,如果存在循环依赖的话可能导致系统崩溃,另外观察者无法知道目标对象具体是怎么发生变化的。
ServletContextListener能够监听ServletContext对象的生命周期,实际上就是监听Web应用。当Servlet容器启动Web应用时调用contextInitiazed方法,终止时调用contextDesoyed方法。MySQL33
逻辑架构13
Q1:MySQL的逻辑架构了解吗?
第一层是服务器层,主要提供连接处理、授权认证、安全等功能。
第二层实现了MySQL核心服务功能,包括查询解析、分析、优化、缓存以及日期和时间等所有内置函数,所有跨存储引擎的功能都在这一层实现,例如存储过程、触发器、视图等。
第三层是存储引擎层,存储引擎负责MySQL中数据的存储和提取。服务器通过API与存储引擎通信,这些接口屏蔽了不同存储引擎的差异,使得差异对上层查询过程透明。除了会解析外键定义的InnoDB外,存储引擎不会解析SQL,不同存储引擎之间也不会相互通信,只是简单响应上层服务器请求。Q2:谈一谈MySQL的读写锁
在处理并发读或写时,可以通过实现一个由两种类型组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁和排它锁,也叫读锁和写锁。读锁是共享的,相互不阻塞,多个客户在同一时刻可以同时读取同一个资源而不相互干扰。写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,确保在给定时间内只有一个用户能执行写入并防止其他用户读取正在写入的同一资源。
在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySQL会通过锁定防止其他用户读取同一数据。写锁比读锁有更高的优先级,一个写锁请求可能会被插入到读锁队列的前面,但是读锁不能插入到写锁前面。Q3:MySQL的锁策略有什么?
表锁是MySQL中最基本的锁策略,并且是开销最小的策略。表锁会锁定整张表,一个用户在对表进行写操作前需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获取读锁,读锁之间不相互阻塞。
行锁可以最大程度地支持并发,同时也带来了最大开销。InnoDB和XaDB以及一些其他存储引擎实现了行锁。行锁只在存储引擎层实现,而服务器层没有实现。Q4:数据库死锁如何解决?
死锁是指多个事务在同一资源上相互占用并请求锁定对方占用的资源而导致恶性循环的现象。当多个事务试图以不同顺序锁定资源时就可能会产生死锁,多个事务同时锁定同一个资源时也会产生死锁。
为了解决死锁问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,例如InnoDB存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误。这种解决方式很有效,否则死锁会导致出现非常慢的查询。还有一种解决方法,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。InnoDB目前处理死锁的方法是将持有最少行级排它锁的事务进行回滚。
死锁发生之后,只有部分或者完全回滚其中一个事务,才能打破死锁。对于事务型系统这是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。Q5:事务是什么?
事务是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说事务内的语句要么全部执行成功,要么全部执行失败。Q6:事务有什么特性?
原子性atomicity
一个事务在逻辑上是必须不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说不可能只执行其中的一部分。
一致性consistency
数据库总是从一个一致性的状态转换到另一个一致性的状态。
隔离性isolation
针对并发事务而言,隔离性就是要隔离并发运行的多个事务之间的相互影响,一般来说一个事务所做的修改在最终提交以前,对其他事务是不可见的。
持久性durabity
一旦事务提交成功,其修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失。Q7:MySQL的隔离级别有哪些?
未提交读READUNCOMMITTED
在该级别事务中的修改即使没有被提交,对其他事务也是可见的。事务可以读取其他事务修改完但未提交的数据,这种问题称为脏读。这个级别还会导致不可重复读和幻读,性能没有比其他级别好很多,很少使用。
提交读READCOMMITTED
多数数据库系统默认的隔离级别。提交读满足了隔离性的简单定义:一个事务开始时只能"看见"已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前的任何修改对其他事务都是不可见的。也叫不可重复读,因为两次执行同样的查询可能会得到不同结果。
可重复读REPEATABLEREAD(MySQL默认的隔离级别)
可重复读解决了不可重复读的问题,保证了在同一个事务中多次读取同样的记录结果一致。但还是无法解决幻读,所谓幻读指的是当某个事务在读取某个范围内的记录时,会产生幻行。InnoDB存储引擎通过多版本并发控制MVCC解决幻读的问题。
可串行化SERIALIZABLE
最高的隔离级别,通过强制事务串行执行,避免幻读。可串行化会在读取的每一行数据上都加锁,可能导致大量的超时和锁争用的问题。实际应用中很少用到这个隔离级别,只有非常需要确保数据一致性且可以接受没有并发的情况下才考虑该级别。Q8:MVCC是什么?
MVCC是多版本并发控制,在很多情况下避免加锁,大都实现了非阻塞的读操作,写操作也只锁定必要的行。
InnoDB的MVCC通过在每行记录后面保存两个隐藏的列来实现,这两个列一个保存了行的创建时间,一个保存行的过期时间间。不过存储的不是实际的时间值而是系统版本号,每开始一个新的事务系统版本号都会自动递增,事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
MVCC只能在READCOMMITTED和REPEATABLEREAD两个隔离级别下工作,因为READUNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE则会对所有读取的行都加锁。Q9:谈一谈InnoDB
InnoDB是MySQL的默认事务型引擎,用来处理大量短期事务。InnoDB的性能和自动崩溃恢复特性使得它在非事务型存储需求中也很流行,除非有特别原因否则应该优先考虑InnoDB。
InnoDB的数据存储在表空间中,表空间由一系列数据文件组成。MySQL4.1后InnoDB可以将每个表的数据和索引放在单独的文件中。
InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。其默认级别是REPEATABLEREAD,并通过间隙锁策略防止幻读,间隙锁使InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定防止幻行的插入。
InnoDB表是基于聚簇索引建立的,InnoDB的索引结构和其他存储引擎有很大不同,聚簇索引对主键查询有很高的性能,不过它的二级索引中必须包含主键列,所以如果主键很大的话其他所有索引都会很大,因此如果表上索引较多的话主键应当尽可能小。
InnoDB的存储格式是平台独立的,可以将数据和索引文件从一个平台复制到另一个平台。
InnoDB内部做了很多优化,包括从磁盘读取数据时采用的可预测性预读,能够自动在内存中创建加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。Q10:谈一谈MyISAM
MySQL5.1及之前,MyISAM是默认存储引擎,MyISAM提供了大量的特性,包括全文索引、压缩、空间函数等,但不支持事务和行锁,最大的缺陷就是崩溃后无法安全恢复。对于只读的数据或者表比较小、可以忍受修复操作的情况仍然可以使用MyISAM。
MyISAM将表存储在数据文件和索引文件中,分别以.MYD和.MYI作为扩展名。MyISAM表可以包含动态或者静态行,MySQL会根据表的定义决定行格式。MyISAM表可以存储的行记录数一般受限于可用磁盘空间或者操作系统中单个文件的最大尺寸。
MyISAM对整张表进行加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但是在表有读取查询的同时,也支持并发往表中插入新的记录。
对于MyISAM表,MySQL可以手动或自动执行检查和修复操作,这里的修复和事务恢复以及崩溃恢复的概念不同。执行表的修复可能导致一些数据丢失,而且修复操作很慢。
对于MyISAM表,即使是BLOB和TEXT等长字段,也可以基于其前500个字符创建索引。MyISAM也支持全文索引,这是一种基于分词创建的索引,可以支持复杂的查询。
MyISAM设计简单,数据以紧密格式存储,所以在某些场景下性能很好。MyISAM最典型的性能问题还是表锁问题,如果所有的查询长期处于Locked状态,那么原因毫无疑问就是表锁。Q12:谈一谈Memory
如果需要快速访问数据且这些数据不会被修改,重启以后丢失也没有关系,那么使用Memory表是非常有用的。Memory表至少要比MyISAM表快一个数量级,因为所有数据都保存在内存,不需要磁盘IO,Memory表的结构在重启后会保留,但数据会丢失。
Memory表适合的场景:查找或者映射表、缓存周期性聚合数据的结果、保存数据分析中产生的中间数据。
Memory表支持哈希索引,因此查找速度极快。虽然速度很快但还是无法取代传统的基于磁盘的表,Memory表使用表级锁,因此并发写入的性能较低。它不支持BLOB和TEXT类型的列,并且每行的长度是固定的,所以即使指定了VARCHAR列,实际存储时也会转换成CHAR,这可能导致部分内存的浪费。
如果MySQL在执行查询的过程中需要使用临时表来保持中间结果,内部使用的临时表就是Memory表。如果中间结果太大超出了Memory表的限制,或者含有BLOB或TEXT字段,临时表会转换成MyISAM表。Q13:查询执行流程是什么?
简单来说分为五步:①客户端发送一条查询给服务器。②服务器先检查查询缓存,如果命中了缓存则立刻返回存储在缓存中的结果,否则进入下一阶段。③服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划。④MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。⑤将结果返回给客户端。数据类型3
Q1:VARCHAR和CHAR的区别?
VARCHAR用于存储可变字符串,是最常见的字符串数据类型。它比CHAR更节省空间,因为它仅使用必要的空间。VARCHAR需要1或2个额外字节记录字符串长度,如果列的最大长度不大于255字节则只需要1字节。VARCHAR不会删除末尾空格。
VARCHAR适用场景:字符串列的最大长度比平均长度大很多、列的更新很少、使用了UTF8这种复杂字符集,每个字符都使用不同的字节数存储。
CHAR是定长的,根据定义的字符串长度分配足够的空间。CHAR会删除末尾空格。
CHAR适合存储很短的字符串,或所有值都接近同一个长度,例如存储密码的MD5值。对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR不容易产生碎片。对于非常短的列,CHAR在存储空间上也更有效率,例如用CHAR来存储只有Y和N的值只需要一个字节,但是VARCHAR需要两个字节,因为还有一个记录长度的额外字节。Q2:DATETIME和TIMESTAMP的区别?
DATETIME能保存大范围的值,从1001~9999年,精度为秒。把日期和时间封装到了一个整数中,与时区无关,使用8字节存储空间。
TIMESTAMP和UNIX时间戳相同,只使用4字节的存储空间,范围比DATETIME小得多,只能表示1970~2038年,并且依赖于时区。Q3:数据类型有哪些优化策略?
更小的通常更好
一般情况下尽量使用可以正确存储数据的最小数据类型,更小的数据类型通常也更快,因为它们占用更少的磁盘、内存和CPU缓存。
尽可能简单
简单数据类型的操作通常需要更少的CPU周期,例如整数比字符操作代价更低,因为字符集和校对规则使字符相比整形更复杂。应该使用MySQL的内建类型date、time和datetime而不是字符串来存储日期和时间,另一点是应该使用整形存储IP地址。
尽量避免NULL
通常情况下最好指定列为NOTNULL,除非需要存储NULL值。因为如果查询中包含可为NULL的列对MySQL来说更难优化,可为NULL的列使索引、索引统计和值比较都更复杂,并且会使用更多存储空间。当可为NULL的列被索引时,每个索引记录需要一个额外字节,在MyISAM中还可能导致固定大小的索引变成可变大小的索引。
如果计划在列上建索引,就应该尽量避免设计成可为NULL的列。索引10
Q1:索引有什么作用?
索引也叫键,是存储引擎用于快速找到记录的一种数据结构。索引对于良好的性能很关键,尤其是当表中数据量越来越大时,索引对性能的影响愈发重要。在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但数据量逐渐增大时,性能会急剧下降。
索引大大减少了服务器需要扫描的数据量、可以帮助服务器避免排序和临时表、可以将随机IO变成顺序IO。但索引并不总是最好的工具,对于非常小的表,大部分情况下会采用全表扫描。对于中到大型的表,索引就非常有效。但对于特大型的表,建立和使用索引的代价也随之增长,这种情况下应该使用分区技术。
在MySQL中,首先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行。索引可以包括一个或多个列的值,如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能使用索引的最左前缀。Q2:谈一谈MySQL的B-Te索引
大多数MySQL引擎都支持这种索引,但底层的存储引擎可能使用不同的存储结构,例如NDB使用T-Te,而InnoDB使用B+Te。
B-Te通常意味着所有的值都是按顺序存储的,并且每个叶子页到根的距离相同。B-Te索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么找到对应的值,要么该记录不存在。叶子节点的指针指向的是被索引的数据,而不是其他的节点页。
B-Te索引的限制:如果不是按照索引的最左列开始查找,则无法使用索引。
不能跳过索引中的列,例如索引为(id,name,sex),不能只使用id和sex而跳过name。
如果查询中有某个列的范围查询,则其右边的所有列都无法使用索引。
Q3:了解Hash索引吗?
哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希码是一个较小的值,并且不同键值的行计算出的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。
只有Memory引擎显式支持哈希索引,这也是Memory引擎的默认索引类型。
因为索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这让哈希索引的速度非常快,但它也有一些限制:哈希索引数据不是按照索引值顺序存储的,无法用于排序。
哈希索引不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如在数据列(a,b)上建立哈希索引,如果查询的列只有a就无法使用该索引。
哈希索引只支持等值比较查询,不支持任何范围查询。
Q4:什么是自适应哈希索引?
自适应哈希索引是InnoDB引擎的一个特殊功能,当它注意到某些索引值被使用的非常频繁时,会在内存中基于B-Te索引之上再创键一个哈希索引,这样就让B-Te索引也具有哈希索引的一些优点,比如快速哈希查找。这是一个完全自动的内部行为,用户无法控制或配置,但如果有必要可以关闭该功能。Q5:什么是空间索引?
MyISAM表支持空间索引,可以用作地理数据存储。和B-Te索引不同,这类索引无需前缀查询。空间索引会从所有维度来索引数据,查询时可以有效地使用任意维度来组合查询。必须使用MySQL的GIS即地理信息系统的相关函数来维护数据,但MySQL对GIS的支持并不完善,因此大部分人都不会使用这个特性。Q6:什么是全文索引?
通过数值比较、范围过滤等就可以完成绝大多数需要的查询,但如果希望通过关键字匹配进行查询,就需要基于相似度的查询,而不是精确的数值比较,全文索引就是为这种场景设计的。
MyISAM的全文索引是一种特殊的B-Te索引,一共有两层。第一层是所有关键字,然后对于每一个关键字的第二层,包含的是一组相关的"文档指针"。全文索引不会索引文档对象中的所有词语,它会根据规则过滤掉一些词语,例如停用词列表中的词都不会被索引。Q7:什么是聚簇索引?
聚簇索引不是一种索引类型,而是一种数据存储方式。InnoDB的聚簇索引实际上在同一个结构中保存了B-Te索引和数据行。当表有聚餐索引时,它的行数据实际上存放在索引的叶子页中,因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
优点:①可以把相关数据保存在一起。②数据访问更快,聚簇索引将索引和数据保存在同一个B-Te中,因此获取数据比非聚簇索引要更快。③使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
缺点:①聚簇索引最大限度提高了IO密集型应用的性能,如果数据全部在内存中将会失去优势。②更新聚簇索引列的代价很高,因为会强制每个被更新的行移动到新位置。③基于聚簇索引的表插入新行或主键被更新导致行移动时,可能导致页分裂,表会占用更多磁盘空间。④当行稀疏或由于页分裂导致数据存储不连续时,全表扫描可能很慢。Q8:什么是覆盖索引?
覆盖索引指一个索引包含或覆盖了所有需要查询的字段的值,不再需要根据索引回表查询数据。覆盖索引必须要存储索引列的值,因此MySQL只能使用B-Te索引做覆盖索引。
优点:①索引条目通常远小于数据行大小,可以极大减少数据访问量。②因为索引按照列值顺序存储,所以对于IO密集型防伪查询回避随机从磁盘读取每一行数据的IO少得多。③由于InnoDB使用聚簇索引,覆盖索引对InnoDB很有帮助。InnoDB的二级索引在叶子节点保存了行的主键值,如果二级主键能覆盖查询那么可以避免对主键索引的二次查询。Q9:你知道哪些索引使用原则?
建立索引
对查询频次较高且数据量比较大的表建立索引。索引字段的选择,最佳候选列应当从WHERE子句的条件中提取,如果WHERE子句中的组合比较多,应当挑选最常用、过滤效果最好的列的组合。业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。
使用前缀索引
索引列开始的部分字符,索引创建后也是使用硬盘来存储的,因此短索引可以提升索引访问的IO效率。对于BLOB、TEXT或很长的VARCHAR列必须使用前缀索引,MySQL不允许索引这些列的完整长度。前缀索引是一种能使索引更小更快的有效方法,但缺点是MySQL无法使用前缀索引做ORDERBY和GROUPBY,也无法使用前缀索引做覆盖扫描。
选择合适的索引顺序
当不需要考虑排序和分组时,将选择性最高的列放在前面。索引的选择性是指不重复的索引值和数据表的记录总数之比,索引的选择性越高则查询效率越高,唯一索引的选择性是1,因此也可以使用唯一索引提升查询效率。
删除无用索引
MySQL允许在相同列上创建多个索引,重复的索引需要单独维护,并且优化器在优化查询时也需要逐个考虑,这会影响性能。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应该避免创建重复索引。如果创建了索引(A,B)再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引,对于B-Te索引来说是冗余的。解决重复索引和冗余索引的方法就是删除这些索引。除了重复索引和冗余索引,可能还会有一些服务器永远不用的索引,也应该考虑删除。Q10:索引失效的情况有哪些?
如果索引列出现了隐式类型转换,则MySQL不会使用索引。常见的情况是在SQL的WHERE条件中字段类型为字符串,其值为数值,如果没有加引号那么MySQL不会使用索引。
如果WHERE条件中含有OR,除非OR前使用了索引列而OR之后是非索引列,索引会失效。
MySQL不能在索引中执行LIKE操作,这是底层存储引擎API的限制,最左匹配的LIKE比较会被转换为简单的比较操作,但如果是以通配符开头的LIKE查询,存储引擎就无法做比较。这种情况下MySQL只能提取数据行的值而不是索引值来做比较。
如果查询中的列不是独立的,则MySQL不会使用索引。独立的列是指索引列不能是表达式的一部分,也不能是函数的参数。
对于多个范围条件查询,MySQL无法使用第一个范围列后面的其他索引列,对于多个等值查询则没有这种限制。
如果MySQL判断全表扫描比使用索引查询更快,则不会使用索引。
索引文件具有B-Te的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。优化5
Q1:如何定位低效SQL?
可以通过两种方式来定位执行效率较低的SQL语句。一种是通过慢查询日志定位,可以通过慢查询日志定位那些已经执行完毕的SQL语句。另一种是使用SHOWPROCESSLIST查询,慢查询日志在查询结束以后才记录,所以在应用反应执行效率出现问题的时候查询慢查询日志不能定位问题,此时可以使用SHOWPROCESSLIST命令查看当前MySQL正在进行的线程,包括线程的状态、是否锁表等,可以实时查看SQL的执行情况,同时对一些锁表操作进行优化。找到执行效率低的SQL语句后,就可以通过SHOWPROFILE、EXPLAIN或ace等丰富来继续优化语句。Q2:SHOWPROFILE的作用?
通过SHOWPROFILE可以分析SQL语句性能消耗,例如查询到SQL会执行多少时间,并显示CPU、内存使用量,执行过程中系统锁及表锁的花费时间等信息。例如SHOWPROFILECPUMEMORYBLOCKIOFORQUERYN分别查询id为N的SQL语句的CPU、内存以及IO的消耗情况。Q3:ace是干什么的?
从MySQL5.6开始,可以通过ace文件进一步获取优化器是是如何选择执行计划的,在使用时需要先打开设置,然后执行一次SQL,最后查看information_schema.otimizer_ace表而都内容,该表为联合i表,只能在当前会话进行查询,每次查询后返回的都是最近一次执行的SQL语句。Q4:EXPLAIN的字段有哪些,具有什么含义?
执行计划是SQL调优的一个重要依据,可以通过EXPLAIN命令查看SQL语句的执行计划,如果作用在表上,那么该命令相当于DESC。EXPLAIN的指标及含义如下:
指标名
含义
id
表示SELECT子句或操作表的顺序,执行顺序从大到小执行,当id一样时,执行顺序从上往下。
select_tye
表示查询中每个SELECT子句的类型,例如SIMPLE表示不包含子查询、表连接或其他复杂语法的简单查询,PRIMARY表示复杂查询的最外层查询,SUBQUERY表示在SELECT或WHERE列表中包含了子查询。
tye
表示访问类型,性能由差到好为:ALL全表扫描、index索引全扫描、range索引范围扫描、f返回匹配某个单独值得所有行,常见于使用非唯一索引或唯一索引的非唯一前缀进行的查找,也经常出现在join操作中、eq_f唯一性索引扫描,对于每个索引键只有一条记录与之匹配、const当MySQL对查询某部分进行优化,并转为一个常量时,使用这些访问类型,例如将主键或唯一索引置于WHERE列表就能将该查询转为一个const、system表中只有一行数据或空表,只能用于MyISAM和Memory表、NULL执行时不用访问表或索引就能得到结果。SQL性能优化的目标:至少要达到range级别,要求是f级别,如果可以是consts最好。
ossible_keys
表示查询时可能用到的索引,但不一定使用。列出大量可能索引时意味着备选索引数量太多了。
key
显示MySQL在查询时实际使用的索引,如果没有使用则显示为NULL。
key_len
表示使用到索引字段的长度,可通过该列计算查询中使用的索引的长度,对于确认索引有效性以及多列索引中用到的列数目很重要。
f
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。
rows
表示MySQL根据表统计信息及索引选用情况,估算找到所需记录所需要读取的行数。
Exa
表示额外信息,例如Usingtemorary表示需要使用临时表存储结果集,常见于排序和分组查询。Usingfilesort表示无法利用索引完成的文件排序,这是ORDERBY的结果,可以通过合适的索引改进性能。Usingindex表示只需要使用索引就可以满足查询表得要求,说明表正在使用覆盖索引。Q5:有哪些优化SQL的策略?
优化COUNT查询
COUNT是一个特殊的函数,它可以统计某个列值的数量,在统计列值时要求列值是非空的,不会统计NULL值。如果在COUNT中指定了列或列的表达式,则统计的就是这个表达式有值的结果数,而不是NULL。
COUNT的另一个作用是统计结果集的行数,当MySQL确定括号内的表达式不可能为NULL时,实际上就是在统计行数。当使用COUNT(*)时,*不会扩展成所有列,它会忽略所有的列而直接统计所有的行数。
某些业务场景并不要求完全精确的COUNT值,此时可以使用近似值来代替,EXPLAIN出来的优化器估算的行数就是一个不错的近似值,因为执行EXPLAIN并不需要真正地执行查询。
通常来说COUNT都需要扫描大量的行才能获取精确的结果,因此很难优化。在MySQL层还能做的就只有覆盖扫描了,如果还不够就需要修改应用的架构,可以增加汇总表或者外部缓存系统。
优化关联查询
确保ON或USING子句中的列上有索引,在创建索引时就要考虑到关联的顺序。
确保任何GROUPBY和ORDERBY的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程。
在MySQL5.5及以下版本尽量避免子查询,可以用关联查询代替,因为执行器会先执行外部的SQL再执行内部的SQL。
优化GROUPBY
如果没有通过ORDERBY子句显式指定要排序的列,当查询使用GROUPBY时,结果集会自动按照分组的字段进行排序,如果不关心结果集的顺序,可以使用ORDERBYNULL禁止排序。
优化LIMIT分页
在偏移量非常大的时候,需要查询很多条数据再舍弃,这样的代价非常高。要优化这种查询,要么是在页面中限制分页的数量,要么是优化大偏移量的性能。最简单的办法是尽可能地使用覆盖索引扫描,而不是查询所有的列,然后根据需要做一次关联操作再返回所需的列。
还有一种方法是从上一次取数据的位置开始扫描,这样就可以避免使用OFFSET。其他优化方法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表只包含主键列和需要做排序的数据列。
优化UNION查询
MySQL通过创建并填充临时表的方式来执行UNION查询,除非确实需要服务器消除重复的行,否则一定要使用UNIONALL,如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致对整个临时表的数据做唯一性检查,这样做的代价非常高。
使用用户自定义变量
在查询中混合使用过程化和关系化逻辑的时候,自定义变量可能会非常有用。用户自定义变量是一个用来存储内容的临时容器,在连接MySQL的整个过程中都存在,可以在任何可以使用表达式的地方使用自定义变量。例如可以使用变量来避免重复查询刚刚更新过的数据、统计更新和插入的数量等。
优化INSERT
需要对一张表插入很多行数据时,应该尽量使用一次性插入多个值的INSERT语句,这种方式将缩减客户端与数据库之间的连接、关闭等消耗,效率比多条插入单个值的INSERT语句高。也可以关闭事务的自动提交,在插入完数据后提交。当插入的数据是按主键的顺序插入时,效率更高。复制2
Q1:MySQL主从复制的作用?
复制解决的基本问题是让一台服务器的数据与其他服务器保持同步,一台主库的数据可以同步到多台备库上,备库本身也可以被配置成另外一台服务器的主库。主库和备库之间可以有多种不同的组合方式。
MySQL支持两种复制方式:基于行的复制和基于语句的复制,基于语句的复制也称为逻辑复制,从MySQL3.23版本就已存在,基于行的复制方式在5.1版本才被加进来。这两种方式都是通过在主库上记录二进制日志、在备库重放日志的方式来实现异步的数据复制。因此同一时刻备库的数据可能与主库存在不一致,并且无法包装主备之间的延迟。
MySQL复制大部分是向后兼容的,新版本的服务器可以作为老版本服务器的备库,但是老版本不能作为新版本服务器的备库,因为它可能无法解析新版本所用的新特性或语法,另外所使用的二进制文件格式也可能不同。
复制解决的问题:数据分布、负载均衡、备份、高可用性和故障切换、MySQL升级测试。Q2:MySQL主从复制的步骤?
①在主库上把数据更改记录到二进制日志中。②备库将主库的日志复制到自己的中继日志中。③备库读取中继日志中的事件,将其重放到备库数据之上。
第一步是在主库上记录二进制日志,每次准备提交事务完成数据更新前,主库将数据更新的事件记录到二进制日志中。MySQL会按事务提交的顺序而非每条语句的执行顺序来记录二进制日志,在记录二进制日志后,主库会告诉存储引擎可以提交事务了。
下一步,备库将主库的二进制日志复制到其本地的中继日志中。备库首先会启动一个工作的IO线程,IO线程跟主库建立一个普通的客户端连接,然后在主库上启动一个特殊的二进制转储线程,这个线程会读取主库上二进制日志中的事件。它不会对事件进行轮询。如果该线程追赶上了主库将进入睡眠状态,直到主库发送信号量通知其有新的事件产生时才会被唤醒,备库IO线程会将接收到的事件记录到中继日志中。
备库的SQL线程执行最后一步,该线程从中继日志中读取事件并在备库执行,从而实现备库数据的更新。当SQL线程追赶上IO线程时,中继日志通常已经在系统缓存中,所以中继日志的开销很低。SQL线程执行的时间也可以通过配置选项来决定是否写入其自己的二进制日志中。Redis37
架构3
Q1:Redis有什么特点?
基于键值对的数据结构服务器
Redis中的值不仅可以是字符串,还可以是具体的数据结构,这样不仅能应用于多种场景开发,也可以提高开发效率。它主要提供五种数据结构:字符串、哈希、列表、集合、有序集合,同时在字符串的基础上演变出了Bitmas和HyerLogLog两种数据结构,Redis3.2还加入了有关GEO地理信息定位的功能。
丰富的功能
①提供了键过期功能,可以实现缓存。②提供了发布订阅功能,可以实现消息系统。③支持Lua脚本,可以创造新的Redis命令。④提供了简单的事务功能,能在一定程度上保证事务特性。⑤提供了流水线功能,客户端能将一批命令一次性传到Redis,减少网络开销。
简单稳定
Redis的简单主要体现在三个方面:①源码很少,早期只有2万行左右,在3.0版本由于添加了集群特性,增加到了5万行左右,相对于很多NoSQL数据库来说代码量要少很多。②采用单线程模型,使得服务端处理模型更简单,也使客户端开发更简单。③不依赖底层操作系统的类库,自己实现了事件处理的相关功能。虽然Redis比较简单,但也很稳定。
客户端语言多
Redis提供了简单的TCP通信协议,很多编程语言可以方便地接入Redis,例如Java、PHP、Pyon、C、C++等。
持久化
通常来说数据放在内存中是不安全的,一旦发生断电或故障数据就可能丢失,因此Redis提供了两种持久化方式RDB和AOF将内存的数据保存到硬盘中。
高性能
Redis使用了单线程架构和IO多路复用模型来实现高性能的内存数据库服务。
每次客户端调用都经历了发送命令、执行命令、返回结果三个过程,因为Redis是单线程处理命令的,所以一条命令从客户端到达服务器不会立即执行,所有命令都会进入一个队列中,然后逐个被执行。客户端的执行顺序可能不确定,但是可以确定不会有两条命令被同时执行,不存在并发问题。
通常来说单线程处理能力要比多线程差,Redis快的原因:①纯内存访问,Redis将所有数据放在内存中。②非阻塞IO,Redis使用eoll作为IO多路复用技术的实现,再加上Redis本身的事件处理模型将eoll中的连接、读写、关闭都转换为时间,不在网络IO上浪费过多的时间。③单线程避免了线程切换和竞争产生的消耗。单线程的一个问题是对于每个命令的执行时间是有要求的,如果某个命令执行时间过长会造成其他命令的阻塞,对于Redis这种高性能服务来说是致命的,因此Redis是面向快速执行场景的数据库。Q2:Redis的数据结构有哪些?
可以使用tye命令查看当前键的数据类型结构,它们分别是:sing、hash、st、set、zset,但这些只是Redis对外的数据结构。实际上每种数据结构都有自己底层的内部编码实现,这样Redis会在合适的场景选择合适的内部编码,sing包括了raw、int和embs,hash包括了hash和zist,st包括了nkedst和zist,set包括了hash和intset,zset包括了skist和zist。可以使用objectencoding查看内部编码。Q3:Redis为什么要使用内部编码?
①可以改进内部编码,而对外的数据结构和命令没有影响。
②多种内部编码实现可以在不同场景下发挥各自的优势,例如zist比较节省内存,但在列表元素较多的情况下性能有所下降,这时Redis会根据配置选项将列表类型的内部实现转换为nkedst。sing4
Q1:简单说一说sing类型
字符串类型是Redis最基础的数据结构,键都是字符串类型,而且其他几种数据结构都是在字符串类型的基础上构建的。字符串类型的值可以实际可以是字符串(简单的字符串、复杂的字符串如JSON、XML)、数字(整形、浮点数)、甚至二进制(图片、音频、视频),但是值最大不能超过512MB。Q2:你知道哪些sing的命令?
设置值
setkeyvalue[exseconds][xmillseconds][nx|xx]exseconds:为键设置秒级过期时间,跟setex效果一样
xmillseconds:为键设置毫秒级过期时间
nx:键必须不存在才可以设置成功,用于添加,跟setnx效果一样。由于Redis的单线程命令处理机制,如果多个客户端同时执行,则只有一个客户端能设置成功,可以用作分布式锁的一种实现。
xx:键必须存在才可以设置成功,用于更新获取值
getkey,如果不存在返回nil
批量设置值
msetkeyvalue[keyvalue...]
批量获取值
mgetkey[key...]
批量操作命令可以有效提高开发效率,假如没有mget,执行n次get命令需要n次网络时间+n次命令时间,使用mget只需要1次网络时间+n次命令时间。Redis可以支持每秒数万的读写操作,但这指的是Redis服务端的处理能力,对于客户端来说一次命令处理命令时间还有网络时间。因为Redis的处理能力已足够高,对于开发者来说,网络可能会成为性能瓶颈。
计数
incrkey
incr命令用于对值做自增操作,返回结果分为三种:①值不是整数返回错误。②值是整数,返回自增后的结果。③值不存在,按照值为0自增,返回结果1。除了incr命令,还有自减decr、自增指定数字incrby、自减指定数组decrby、自增浮点数incrbyfloat。Q3:sing的内部编码是什么?int:8个字节的长整形
embs:小于等于39个字节的字符串
raw:大于39个字节的字符串
Q4:sing的应用场景有什么?
缓存功能
Redis作为缓存层,MySQL作为存储层,首先从Redis获取数据,如果失败就从MySQL获取并将结果写回Redis并添加过期时间。
计数
Redis可以实现快速计数功能,例如视频每播放一次就用incy把播放数加1。
共享Session
一个分布式Web服务将用户的Session信息保存在各自服务器,但会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问负载到不同服务器上,用户刷新一次可能会发现需要重新登陆。为解决该问题,可以使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或查询登录信息都直接从Redis集中获取。
限速
例如为了短信接口不被频繁访问会限制用户每分钟获取验证码的次数或者网站限制一个IP地址不能在一秒内访问超过n次。可以使用键过期策略和自增计数实现。hash4
Q1:简单说一说hash类型
哈希类型指键值本身又是一个键值对结构,哈希类型中的映射关系叫field-value,这里的value是指field对于的值而不是键对于的值。Q2:你知道哪些hash的命令?
设置值
hsetkeyfieldvalue,如果设置成功会返回1,反之会返回0,此外还提供了hsetnx命令,作用和setnx类似,只是作用于由键变为field。
获取值
hgetkeyfield,如果不存在会返回nil。
删除field
hdelkeyfield[field...],会删除一个或多个field,返回结果为删除成功field的个数。
计算field个数
hlenkey
批量设置或获取field-value
hmgetkeyfield[field...]
hmsetkeyfieldvalue[fieldvalue...]判断field是否存在
hexistskeyfield,存在返回1,否则返回0。
获取所有的field
hkeyskey,返回指定哈希键的所有field。
获取所有value
hvalskey,获取指定键的所有value。
获取所有的field-value
hgetallkey,获取指定键的所有field-value。Q3:hash的内部编码是什么?
zist压缩列表:当哈希类型元素个数和值小于配置值(默认512个和64字节)时会使用zist作为内部实现,使用更紧凑的结构实现多个元素的连续存储,在节省内存方面比hash更优秀。
hash哈希表:当哈希类型无法满足zist的条件时会使用hash作为哈希的内部实现,因为此时zist的读写效率会下降,而hash的读写时间复杂度都为O(1)。Q4:hash的应用场景有什么?
缓存用户信息,每个用户属性使用一对field-value,但只用一个键保存。
优点:简单直观,如果合理使用可以减少内存空间使用。
缺点:要控制哈希在zist和hash两种内部编码的转换,hash会消耗更多内存。st4
Q1:简单说一说st类型
st是用来存储多个有序的字符串,列表中的每个字符串称为元素,一个列表最多可以存储232-1个元素。可以对列表两端插入(ush)和弹出(o),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发中有很多应用场景。
st有两个特点:①列表中的元素是有序的,可以通过索引下标获取某个元素或者某个范围内的元素列表。②列表中的元素可以重复。Q2:你知道哪些st的命令?
添加
从右边插入元素:rushkeyvalue[value...]
从左到右获取列表的所有元素:lrange0-1
从左边插入元素:lushkeyvalue[value...]
向某个元素前或者后插入元素:nsertkeybefo|afterivotvalue,会在列表中找到等于ivot的元素,在其前或后插入一个新的元素value。
查找
获取指定范围内的元素列表:lrangekeystartend,索引从左到右的范围是0~N-1,从右到左是-1~-N,lrange中的end包含了自身。
获取列表指定索引下标的元素:ndexkeyindex,获取最后一个元素可以使用ndexkey-1。
获取列表长度:llenkey
删除
从列表左侧弹出元素:lokey
从列表右侧弹出元素:rokey
删除指定元素:lmkeycountvalue,如果count大于0,从左到右删除最多count个元素,如果count小于0,从右到左删除最多个count绝对值个元素,如果count等于0,删除所有。
按照索引范围修剪列表:mkeystartend,只会保留start~end范围的元素。
修改
修改指定索引下标的元素:lsetkeyindexnewValue。
阻塞操作
阻塞式弹出:blobrokey[key...]timeout,timeout表示阻塞时间。
当列表为空时,如果timeout=0,客户端会一直阻塞,如果在此期间添加了元素,客户端会立即返回。
如果是多个键,那么bro会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。
如果多个客户端对同一个键执行bro,那么最先执行该命令的客户端可以获取弹出的值。Q3:st的内部编码是什么?
zist压缩列表:跟哈希的ziist相同,元素个数和大小小于配置值(默认512个和64字节)时使用。
nkedst链表:当列表类型无法满足zist的条件时会使用nkedst。
Redis3.2提供了quickst内部编码,它是以一个zist为节点的nkedst,它结合了两者的优势,为列表类提供了一种更为优秀的内部编码实现。Q4:st的应用场景有什么?
消息队列
Redis的lush+bro即可实现阻塞队列,生产者客户端使用lush从列表左侧插入元素,多个消费者客户端使用bro命令阻塞式地抢列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
文章列表
每个用户有属于自己的文章列表,现在需要分页展示文章列表,就可以考虑使用列表。因为列表不但有序,同时支持按照索引范围获取元素。每篇文章使用哈希结构存储。
lush+lo=栈、lush+ro=队列、lush+m=优先集合、lush+bro=消息队列。set4
Q1:简单说一说set类型
集合类型也是用来保存多个字符串元素,和列表不同的是集合不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。一个集合最多可以存储232-1个元素。Redis除了支持集合内的增删改查,还支持多个集合取交集、并集、差集。Q2:你知道哪些set的命令?
添加元素
saddkeyelement[element...],返回结果为添加成功的元素个数。
删除元素
smkeyelement[element...],返回结果为成功删除的元素个数。
计算元素个数
scardkey,时间复杂度为O(1),会直接使用Redis内部的遍历。
判断元素是否在集合中
sismemberkeyelement,如果存在返回1,否则返回0。
随机从集合返回指定个数个元素
srandmemberkey[count],如果不指定count默认为1。
从集合随机弹出元素
sokey,可以从集合中随机弹出一个元素。
获取所有元素
smemberskey
求多个集合的交集并集差集
sinterkey[key...]
nionkey[key...]
sdiffkey[key...]
保存交集、并集、差集的结果
sinterstonionstosdiffstodestinationkey[key...]
集合间运算在元素较多情况下比较耗时,Redis提供这三个指令将集合间交集、并集、差集的结果保存在destinationkey中。Q3:set的内部编码是什么?
intset整数集合:当集合中的元素个数小于配置值(默认512个时),使用intset。
hash哈希表:当集合类型无法满足intset条件时使用hash。当某个元素不为整数时,也会使用hash。Q4:set的应用场景有什么?
set比较典型的使用场景是标签,例如一个用户可能与娱乐、体育比较感兴趣,另一个用户可能对例时、新闻比较感兴趣,这些兴趣点就是标签。这些数据对于用户体验以及增强用户黏度比较重要。
sadd=标签、sosrandmember=生成随机数,比如抽奖、sadd+sinter=社交需求。zset4
Q1:简单说一说zset类型
有序集合保留了集合不能有重复成员的特性,不同的是可以排序。但是它和列表使用索引下标作为排序依据不同的是,他给每个元素设置一个分数(sco)作为排序的依据。有序集合提供了获取指定分数和元素查询范围、计算成员排名等功能。Q2:你知道哪些zset的命令?
添加成员
zaddkeyscomember[scomember...],返回结果是成功添加成员的个数
Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项:nx:member必须不存在才可以设置成功,用于添加。
xx:member必须存在才能设置成功,用于更新。
ch:返回此次操作后,有序集合元素和分数变化的个数。
incr:对sco做增加,相当于zincrby。zadd的时间复杂度为O(logn),sadd的时间复杂度为O(1)。
计算成员个数
zcardkey,时间复杂度为O(1)。
计算某个成员的分数
zscokeymember,如果不存在则返回nil。
计算成员排名
zrankkeymember,从低到高返回排名。
zvrankkeymember,从高到低返回排名。
删除成员
zmkeymember[member...],返回结果是成功删除的个数。
增加成员的分数
zincrbykeyincmentmember
返回指定排名范围的成员
zrangekeystartend[wiscos],从低到高返回
zvrangekeystartend[wiscos],从高到底返回
返回指定分数范围的成员
zrangebyscokeyminmax[wiscos][mitoffsetcount],从低到高返回
zvrangebyscokeyminmax[wiscos][mitoffsetcount],从高到底返回
返回指定分数范围成员个数
zcountkeyminmax
删除指定分数范围内的成员
zmrangebyscokeyminmax
交集和并集
zinterstozunionstodestinationnumkeyskey[key...][weightsweight[weight...]][agggatem|min|max]
destination:交集结果保存到这个键
numkeys:要做交集计算键的个数
key:需要做交集计算的键
weight:每个键的权重,默认1
agggatem|min|max:计算交集后,分值可以按和、最小值、最大值汇总,默认m。Q3:zset的内部编码是什么?
zist压缩列表:当有序集合元素个数和值小于配置值(默认128个和64字节)时会使用zist作为内部实现。
skist跳跃表:当zist不满足条件时使用,因为此时zist的读写效率会下降。Q4:zset的应用场景有什么?
有序集合的典型使用场景就是排行榜系统,例如用户上传了一个视频并获得了赞,可以使用zadd和zincrby。如果需要将用户从榜单删除,可以使用zm。如果要展示获取赞数最多的十个用户,可以使用zrange。键和数据库管理5
Q1:如何对键重命名?
namekeynewkey
如果name前键已经存在,那么它的值也会被覆盖。为了防止强行覆盖,Redis提供了namenx命令,确保只有newkey不存在时才被覆盖。由于重命名键期间会执行del命令删除旧的键,如果键对应值比较大会存在阻塞的可能。Q2:如何设置键过期?
exikeyseconds:键在seconds秒后过期。
如果过期时间为负值,键会被立即删除,和del命令一样。ersist命令可以将键的过期时间清除。
对于字符串类型键,执行set命令会去掉过期时间,set命令对应的函数setKey最后执行了moveExi函数去掉了过期时间。setex命令作为set+exi的组合,不单是原子执行并且减少了一次网络通信的时间。Q3:如何进行键迁移?
move
move命令用于在Redis内部进行数据迁移,movekeydb把指定的键从源数据库移动到目标数据库中。
dum+sto
可以实现在不同的Redis实例之间进行数据迁移,分为两步:
①dumkey,在源Redis上,dum命令会将键值序列化,格式采用RDB格式。
②stokeyttlvalue,在目标Redis上,sto命令将序列化的值进行复原,ttl代表过期时间,ttl=0则没有过期时间。
整个迁移并非原子性的,而是通过客户端分步完成,并且需要两个客户端。
migrate
实际上migrate命令就是将dum、sto、del三个命令进行组合,从而简化操作流程。migrate具有原子性,支持多个键的迁移,有效提高了迁移效率。实现过程和dum+sto类似,有三点不同:
①整个过程是原子执行,不需要在多个Redis实例开启客户端。
②数据传输直接在源Redis和目标Redis完成。
③目标Redis完成sto后会发送OK给源Redis,源Redis接收后根据migrate对应选项来决定是否在源Redis上删除对应键。Q4:如何切换数据库?
selectdbIndex,Redis中默认配置有16个数据库,例如select0将切换到第一个数据库,数据库之间的数据是隔离的。Q5:如何清除数据库?
用于清除数据库,flushdb只清除当前数据库,flushall会清除所有数据库。如果当前数据库键值数量比较多,flushdbflushall存在阻塞Redis的可能性。持久化9
Q1:RDB持久化的原理?
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
手动触发分别对应save和bgsave命令:save:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
bgasve:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。bgsave是针对save阻塞问题做的优化,因此Redis内部所有涉及RDB的操作都采用bgsave的方式,而save方式已经废弃。除了手动触发外,Redis内部还存在自动触发RDB的持久化机制,例如:使用save相关配置,如savemn,表示m秒内数据集存在n次修改时,自动触发bgsave。
如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
执行debugload命令重新加载Redis时也会自动触发save操作。
默认情况下执行shuown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
Q2:bgsave的原理?
①执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDBAOF子进程,如果存在bgsave命令直接返回。
②父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞。
③父进程fork完成后,bgsave命令返回并不再阻塞父进程,可以继续响应其他命令。
④子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。
⑤进程发送信号给父进程表示完成,父进程更新统计信息。Q3:RDB持久化的优点?
RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适合于备份,全量复制等场景。例如每6个消时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中,用于灾难恢复。
Redis加载RDB恢复数据远远快于AOF的方式。Q4:RDB持久化的缺点?
RDB方式数据无法做到实时持久化秒级持久化,因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式。
RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。Q5:AOF持久化的原理?
AOF持久化以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前是Redis持久化的主流方式。
开启AOF功能需要设置:aendonlyyes,默认不开启。保存路径同RDB方式一致,通过dir配置指定。
AOF的工作流程操作:命令写入aend、文件同步sync、文件重写write、重启加载load:所有的写入命令会追加到aof_buf缓冲区中。
AOF缓冲区根据对应的策略向硬盘做同步操作。
随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
当服务器重启时,可以加载AOF文件进行数据恢复。
Q6:AOF命令写入的原理?
AOF命令写入的内容直接是文本协议格式,采用文本协议格式的原因:文本协议具有很好的兼容性。
开启AOF后所有写入命令都包含追加操作,直接采用协议格式避免了二次处理开销。
文本协议具有可读性,方便直接修改和处理。AOF把命令追加到缓冲区的原因:
Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区中还有另一个好处,Redis可以提供多种缓冲区同步硬盘策略,在性能和安全性方面做出平衡。Q7:AOF文件同步的原理?
Redis提供了多种AOF缓冲区文件同步策略,由参数aendfsync控制,不同值的含义如下:
always:命令写入缓冲区后调用系统fsync操作同步到AOF文件,fsync完成后线程返回。每次写入都要同步AOF,性能较低,不建议配置。
everysec:命令写入缓冲区后调用系统write操作,write完成后线程返回。fsync同步文件操作由专门线程每秒调用一次。是建议的策略,也是默认配置,兼顾性能和数据安全。
no:命令写入缓冲区后调用系统write操作,不对AOF文件做fsync同步,同步硬盘操作由操作系统负责,周期通常最长30秒。由于操作系统每次同步AOF文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但安全性无法保证。Q8:AOF文件重写的原理?
文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程,可以降低文件占用空间,更小的文件可以更快地被加载。
重写后AOF文件变小的原因:进程内已经超时的数据不再写入文件。
旧的AOF文件含有无效命令,重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据写入命令。
多条写命令可以合并为一个,为了防止单条命令过大造成客户端缓冲区溢出,对于st、set、hash、zset等类型操作,以64个元素为界拆分为多条。AOF重写分为手动触发和自动触发,手动触发直接调用bgwriteaof命令,自动触发根据auto-aof-write-min-size和auto-aof-write-ercentage参数确定自动触发时机。
重写流程:
①执行AOF重写请求,如果当前进程正在执行AOF重写,请求不执行并返回,如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再执行。
②父进程执行fork创建子进程,开销等同于bgsave过程。
③父进程fork操作完成后继续响应其他命令,所有修改命令依然写入AOF缓冲区并同步到硬盘,保证原有AOF机制正确性。
④子进程根据内存快照,按命令合并规则写入到新的AOF文件。每次批量写入数据量默认为32MB,防止单次刷盘数据过多造成阻塞。
⑤新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息。
⑥父进程把AOF重写缓冲区的数据写入到新的AOF文件并替换旧文件,完成重写。Q9:AOF重启加载的原理?
AOF和RDB文件都可以用于服务器重启时的数据恢复。Redis持久化文件的加载流程:
①AOF持久化开启且存在AOF文件时,优先加载AOF文件。
②AOF关闭时且存在RDB文件时,记载RDB文件。
③加载AOFRDB文件成功后,Redis启动成功。
④AOFRDB文件存在错误导致加载失败时,Redis启动失败并打印错误信息。

TAG:

相关阅读

最新推荐