├── .gitattributes ├── LICENSE ├── README.md ├── image ├── B+Tree.png ├── BTree.png ├── CGLIB反编译.png ├── DNS查询.png ├── DispatcherServlet工作流程.jpg ├── HTTPS加密.png ├── HTTP响应报文.jpg ├── HTTP请求报文.jpg ├── HashMap的Hash冲突.png ├── HashMap的PUT方法.jpg ├── JVM内存结构.png ├── MySQL主从复制.png ├── SSH加密.png ├── String变量存储位置.jpg ├── TCP报文段.png ├── bean准备工作.png ├── bean初始化工作.png ├── catch多个异常情况.png ├── 七层参考网络模型.png ├── 三次握手.png ├── 依赖关系.png ├── 关联关系.png ├── 四次挥手.png ├── 实现关系.png ├── 线程状态.png ├── 组合关系.png ├── 继承关系.png ├── 聚合关系.png ├── 虚拟内存与物理内存关系.png ├── 进程状态.png └── 集合框架.jpg ├── interviews ├── 2020届秋招面试题总结-JAVA基础篇.md ├── 2020届秋招面试题总结-JVM篇.md ├── 2020届秋招面试题总结-MySQL篇.md ├── 2020届秋招面试题总结-Redis篇.md ├── 2020届秋招面试题总结-Spring篇.md ├── 2020届秋招面试题总结-多线程篇.md ├── 2020届秋招面试题总结-操作系统篇.md ├── 2020届秋招面试题总结-数据结构篇.md └── 2020届秋招面试题总结-网络篇.md └── notes ├── Java常量池理解及总结.md ├── 从字节码角度分析try、catch、finally运行细节.md ├── 浅谈JVM中的符号引用和直接引用.md ├── 记BitMap-BitSet.md ├── 记System.identityHashCode(obj)与obj.hashCode()的关系.md ├── 记动态代理.md ├── 记单例模式.md └── 记循环依赖.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ZingBug 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java-Notes 2 | 3 | ## 面试题目 4 | 5 | - [2020届秋招面试题总结——JAVA基础篇](/interviews/2020届秋招面试题总结-JAVA基础篇.md) 6 | - [2020届秋招面试题总结——网络篇](/interviews/2020届秋招面试题总结-网络篇.md) 7 | - [2020届秋招面试题总结——操作系统篇](/interviews/2020届秋招面试题总结-操作系统篇.md) 8 | - [2020届秋招面试题总结——JVM篇](/interviews/2020届秋招面试题总结-JVM篇.md) 9 | - [2020届秋招面试题总结——MySQL篇](/interviews/2020届秋招面试题总结-MySQL篇.md) 10 | - [2020届秋招面试题总结——Redis篇](/interviews/2020届秋招面试题总结-Redis篇.md) 11 | - [2020届秋招面试题总结——数据结构篇](/interviews/2020届秋招面试题总结-数据结构篇.md) 12 | - [2020届秋招面试题总结——多线程篇](/interviews/2020届秋招面试题总结-多线程篇.md) 13 | - [2020届秋招面试题总结——Spring篇](/interviews/2020届秋招面试题总结-Spring篇.md) 14 | 15 | ## 笔记整理 16 | 17 | - [记动态代理](/notes/记动态代理.md) 18 | - [记单例模式](/notes/记单例模式.md) 19 | - [记BitMap/BitSet](/notes/记BitMap-BitSet.md) 20 | - [记循环依赖](/notes/记循环依赖.md) 21 | - [记System.identityHashCode(obj)与obj.hashCode()的关系](/notes/记System.identityHashCode(obj)与obj.hashCode()的关系.md) 22 | - [从字节码角度分析try、catch、finally运行细节](/notes/从字节码角度分析try、catch、finally运行细节.md) 23 | - [Java常量池理解及总结](/notes/Java常量池理解及总结.md) 24 | - [浅谈JVM中的符号引用和直接引用](/notes/浅谈JVM中的符号引用和直接引用.md) 25 | -------------------------------------------------------------------------------- /image/B+Tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/B+Tree.png -------------------------------------------------------------------------------- /image/BTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/BTree.png -------------------------------------------------------------------------------- /image/CGLIB反编译.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/CGLIB反编译.png -------------------------------------------------------------------------------- /image/DNS查询.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/DNS查询.png -------------------------------------------------------------------------------- /image/DispatcherServlet工作流程.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/DispatcherServlet工作流程.jpg -------------------------------------------------------------------------------- /image/HTTPS加密.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/HTTPS加密.png -------------------------------------------------------------------------------- /image/HTTP响应报文.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/HTTP响应报文.jpg -------------------------------------------------------------------------------- /image/HTTP请求报文.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/HTTP请求报文.jpg -------------------------------------------------------------------------------- /image/HashMap的Hash冲突.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/HashMap的Hash冲突.png -------------------------------------------------------------------------------- /image/HashMap的PUT方法.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/HashMap的PUT方法.jpg -------------------------------------------------------------------------------- /image/JVM内存结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/JVM内存结构.png -------------------------------------------------------------------------------- /image/MySQL主从复制.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/MySQL主从复制.png -------------------------------------------------------------------------------- /image/SSH加密.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/SSH加密.png -------------------------------------------------------------------------------- /image/String变量存储位置.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/String变量存储位置.jpg -------------------------------------------------------------------------------- /image/TCP报文段.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/TCP报文段.png -------------------------------------------------------------------------------- /image/bean准备工作.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/bean准备工作.png -------------------------------------------------------------------------------- /image/bean初始化工作.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/bean初始化工作.png -------------------------------------------------------------------------------- /image/catch多个异常情况.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/catch多个异常情况.png -------------------------------------------------------------------------------- /image/七层参考网络模型.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/七层参考网络模型.png -------------------------------------------------------------------------------- /image/三次握手.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/三次握手.png -------------------------------------------------------------------------------- /image/依赖关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/依赖关系.png -------------------------------------------------------------------------------- /image/关联关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/关联关系.png -------------------------------------------------------------------------------- /image/四次挥手.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/四次挥手.png -------------------------------------------------------------------------------- /image/实现关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/实现关系.png -------------------------------------------------------------------------------- /image/线程状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/线程状态.png -------------------------------------------------------------------------------- /image/组合关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/组合关系.png -------------------------------------------------------------------------------- /image/继承关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/继承关系.png -------------------------------------------------------------------------------- /image/聚合关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/聚合关系.png -------------------------------------------------------------------------------- /image/虚拟内存与物理内存关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/虚拟内存与物理内存关系.png -------------------------------------------------------------------------------- /image/进程状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/进程状态.png -------------------------------------------------------------------------------- /image/集合框架.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZingBug/Java-Notes/525bbdc9b0180925023678ec3edf7b6972eec5fd/image/集合框架.jpg -------------------------------------------------------------------------------- /interviews/2020届秋招面试题总结-JAVA基础篇.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——JAVA基础篇 2 | 3 | **1、JAVA中的几种数据类型是什么,各自占用多少字节。** 4 | 5 | Java语言提供了八种基本数据类型。六种数据类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。 6 | 7 | - byte:1字节,表示范围是-128~127之间。 8 | - short:2字节,表示范围是-32768~32767之间。 9 | - int:4字节,表示范围是负的2的31次方到正的2的31次方减1。 10 | - long:8字节,表示范围为负的2的63次方到正的2的63次方减1。 11 | - float:4字节,表示范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。 12 | - double:8字节,表示范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。 13 | - boolean:只有true和false两个取值。 14 | - char:2字节,存储Unicode码,用单引号赋值。 15 | 16 | **2、String类能被继承吗,为什么。** 17 | 18 | 不能被继承,因为String类有final修饰符,而final修饰的类是不能被继承的。 19 | 20 | ```java 21 | public final class String implements java.io.Serializable, Comparable, CharSequence { 22 | // 省略... 23 | } 24 | ``` 25 | 26 | **3、String,StringBuffer,StringBuilder的区别。** 27 | 28 | String为字符串常量(因为内部数组value[]有final修饰),而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。 29 | 30 | 在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的,因为StringBuffer在append()方法上添加了synchronized修饰。 31 | 32 | String,StringBuffer,StringBuilder都被final修饰,不能继承。 33 | String变量创建后是放入方法区的常量池(或者常量池)中,而StringBuilder和StringBuffer则是存入堆中。 34 | 35 | 构造String对象的方式有很多,String的内存分配比较特殊: 36 | 37 | - 方式一:String str1="123"; 38 | 39 | 通过引号直接创建字符串对象,先会从常量池中判断是否存在"123"对象,如果不存在,则会在常量池中创建该对象,并且返回常量池中"123"对象的引用给str;如果之前常量池存在"123"的话,则直接返回常量池中"123"的对象引用。 40 | 41 | - 方式二:String str2=new String("123"); 42 | 43 | 首先"123"是一个常量字符串,因此会先在常量池创建"123"字符串对象,然后在堆中再创建一个字符串对象,将常量池中的"123"字符串复制到堆中新创建的对象字符数组中,因此该方式不仅会在堆中,还会在常量池中创建"123"字符串对象。 44 | 45 | - 方式三:String str3="123".intern(); 46 | 47 | 该种方式通过intern方法返回一个字符串引用,intern方法是一个native方法,当常量池中存在"123"字符串常量时,则直接返回该常量池中的字符串引用;若不存在,则会先在常量池中创建"123"字符串对象,然后返回新创建对象的引用,与方式一类似。该方法常用于将某些经常访问的字符串对象保存在常量池中,避免经常创建对象。 48 | 方式四:String str4=str2.intern(); 49 | 50 | 该种方式是在方式二基础上进行的,intern 方法会先判断常量池中是否存在与str2 相同字符串的对象,若有,则返回该引用;若无,则在常量池创建一个引用(CONSTAT_String_info)指向 str2,然后返回该引用,实际上返回的是 str2 的引用。 51 | 52 | 具体可以参考这篇文献:[String 对象内存分配(常量池和堆的分配)](https://blog.csdn.net/Mypromise_TFS/article/details/81504137) 53 | 54 | **4、ArrayList和LinkedList有什么区别。** 55 | 56 | - ArraryList是基于动态数组的数据结构,LinkeList是基于链表的数据结果(LinkedList是双向链表,有next也有previous)。 57 | - 对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。 58 | - 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。 59 | 60 | **5、讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段。当new的时候,他们的执行顺序。** 61 | 62 | Java程序的初始化一般遵循3个原则(优先级以此递减): 63 | 64 | - 静态对象(变量)优先于非静态对象(变量)初始化,其中,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次。 65 | - 父类优先于子类进行初始化。 66 | - 按照成员变量的顺序进行初始化。 67 | 68 | 当new时,他们的执行顺序为:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。 69 | 70 | **6、用过哪些Map类,都有什么区别,HashMap时线程安全的吗,并发下使用的Map是什么,他们的内部原理分别是什么,比如存储方法,hashcode,扩容,默认容量等。** 71 | 72 | 主要用过HashMap,HashMap不是线程安全的,并发下使用的Map是ConcurrentHashMap,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。 73 | 74 | HashMap中Node[] table的默认长度length是16,所能容纳的最大容量数据的Node(键值对)个数为threshold=length*Loadfactor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。 75 | 76 | 结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率的一个平衡选择。 77 | 78 | Hash算法本质上就是三步:取key的hashCode值,高位运算,取模运算。 79 | 80 | 注意,一般hashtable桶数都会选择素数,因为素数因子最少,能减少冲突。但是,hashmap却采用非常规方法,没有选用素数,而是选用合数,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。 81 | 82 | 强力推荐这篇文章: [Java 8系列之重新认识HashMap]( https://zhuanlan.zhihu.com/p/21673805) 83 | 84 | **7、Java8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。** 85 | 86 | jdk8放弃了分段锁而采用了Node锁,降低了锁的粒度,提高了性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁。 87 | 88 | 但是,ConcurrentHashMap的一些操作使用了synchronized锁,而不是ReentrantLock,虽然说jdk8中对synchronized进行了性能优化,但是我觉得使用ReentrantLock锁能更多的提高性能。 89 | 90 | ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。 91 | 92 | Synchronized是悲观锁,在jdk1.8之后,加入了偏向锁,轻量级锁(自旋锁),性能得到了极大优化。 93 | 94 | 上述两种锁都是可重入锁,在锁的细粒度和灵活度方面,很明显ReenTrantLock优于Synchronized。 95 | 96 | **8、有没有有顺序的Map实现类,如果有,他们怎么保证有序。** 97 | 98 | 顺序的Map实现类:LinkedHashMap,TreeMap 99 | 100 | - LinkedHashMap是基于元素进入集合的顺序或者被访问的先后顺序排序。 101 | - TreeMap则是基于元素的固有顺序(由Comparator或者Comarable确定)。 102 | 103 | **9、抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口吗,类可以实现多个接口吗。** 104 | 105 | 抽象类和接口的区别有: 106 | 107 | - 抽象类可以有自己的实现方法,接口在jdk1.8之后才可以有自己的实现方法(用default修饰)。 108 | - 抽象类的抽象方法必须有继承的子类实现,如果子类不实现,则子类也需要定义为抽象的;接口的抽象方法必须由实现类来实现,如果实现类不能实现接口中所有方法,则将实现类定位为抽象类。 109 | - 抽象方法必须是pulic/protected,接口中的变量隐式指定为public static final变量,抽象方法被隐式指定为public abstract。 110 | - 抽象类中可以存在普通属性、方法、静态属性和方法。如果一个类中有一个抽象方法,那么当前类肯定是抽象类。 111 | - 子类只能继承一个父类,接口可以继承多个接口,类似于:Interface1 Extends Interface2, Interface3, Interface4…… 112 | - 类也可以实现多个接口。 113 | 114 | 主要注意的是,抽象方法不能用synchronized修饰。 115 | 116 | 从设计角度来看抽象类和接口: 117 | 118 | - 抽象类是is a,是实例必须要有的,比如Door必须有开和关。而接口就是has a,可以有也可以没有,比如Door可以有报警器,但不是必须的,是可拓展的行为。 119 | - 抽象类强调的是同类事务的抽象,接口强调的是同类方法的抽象。 120 | - 抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类;接口是根本不知道子类的存在,方法如何实现还不确认,预先定义。 121 | - 若行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类。 122 | 123 | **10、继承和聚合的区别在哪。** 124 | 125 | 分别介绍这几种关系: 126 | 127 | - 继承:指的是一个类继承另一个类的功能,并可以增加自己的的新功能的能力,Is-A继承关系是类与类或者接口与接口之间最常见的关系。在Java类中通过关键词extends明确标识。 128 | 129 | ![YWvf1J.png](https://s1.ax1x.com/2020/05/18/YWvf1J.png) 130 | 131 | - 实现:指的是一个class类实现interface接口(可以多个)的功能,实现是类与接口之间最常见的关系。在Java类中通过关键词implements明确标识。 132 | 133 | ![YWvo0x.png](https://s1.ax1x.com/2020/05/18/YWvo0x.png) 134 | 135 | - 依赖:可以简单的理解,就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、、临时性的、非常弱的,但是B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖;表现在代码层面,为类B作为参数被类A在某个method方法中使用。 136 | 137 | ![YWvT76.png](https://s1.ax1x.com/2020/05/18/YWvT76.png) 138 | 139 | - 关联:关联关系在java中一般使用成员变量来实现,有时也用方法形参的形式实现。依然使用Driver和Car的例子,使用方法参数形式可以表示依赖关系,也可以表示关联关系,毕竟我们无法在程序中太准确的表达语义。 140 | 141 | ![YWvrmq.png](https://s1.ax1x.com/2020/05/18/YWvrmq.png) 142 | 143 | - 聚合:是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等。 144 | 145 | ![YWv4XR.png](https://s1.ax1x.com/2020/05/18/YWv4XR.png) 146 | 147 | - 组合(a拥有b,a没了b也就没了,实心):也是关联关系的一种特例,他体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束;比如你和你的大脑;表现在代码层面,和关联关系是一致的,只能从语义级别来区分; 148 | 149 | ![YWvHAK.png](https://s1.ax1x.com/2020/05/18/YWvHAK.png) 150 | 151 | 具体代码层次理解可以看这篇文章: [java--依赖、关联、聚合和组合之间区别的理解]( https://www.cnblogs.com/wanghuaijun/p/5421419.html) 152 | 153 | **11、讲讲你理解的nio,他和bio的区别是什么,谈谈reactor模型。** 154 | 155 | - BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个线程不做任何事情会造成不必要的线程开销,当然可以通过线程池来改善。 156 | 157 | - NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。是基于事件驱动思想完成的。 158 | 159 | - AIO:异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。reactor模型:反应器模式(事件驱动模式):当一个主体发生改变时,所有的属性都得到通知,类似于观察者模式。 160 | 161 | 解释一下同步与异步: 162 | 163 | - 同步IO,是一种用户空间与内核空间的调用发起方式。同步IO是指用户空间线程是主动发起IO请求的一方,内核空间是被动接受方。 164 | 165 | - 异步IO则反过来,是指内核kernel是主动发起IO请求的一方,用户线程是被动接受方。 166 | 167 | 再解释一下阻塞和非阻塞: 168 | 169 | - 阻塞是指用户空间(调用线程)一直在等待,而且别的事情什么都不做; 170 | 171 | - 非阻塞是指用户空间(调用线程)拿到状态就返回,IO操作可以干就干,不可以干,就去干的事情。 172 | 173 | I/O多路复用是指内核一旦发现进程中指定的一个或者多个IO条件准备读取,它就通知该进程。也可以理解为,使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。 174 | 175 | 具体reactor可以看这篇文章: [Reactor模式详解]( https://www.cnblogs.com/winner-0715/p/8733787.html) 176 | 177 | 在Java中,Selector这个类是select/epoll/poll的外包类。 178 | 179 | **12、反射的原理,反射创建类实例的三种方式是什么。** 180 | 181 | 反射机制:Java反射机制是在运行状态中,对于任意一个类,如果知道一个类的名称,都能够知道这个类的所有属性和方法;对于任意一个对象,如果知道一个实例对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。 182 | 183 | 反射获取Class对象有三种方式:使用Class.forName("类路径名称")静态方法。 184 | 185 | - 使用类的.class方法。 186 | - 使用实例对象的getClass()方法。 187 | 188 | 根据Class获取实例对象有两种方式: 189 | 190 | - 直接使用字节码文件获取对应实例,Object o=clazz.newInstance(); 191 | - 对带参数的构造函数的类,先获取到其构造对象,再通过该构造方法类获取实例,如下。 192 | 193 | ```java 194 | / /获取构造函数类的对象 195 | Constroctor constroctor = clazz.getConstructor(String.class,Integer.class); 196 | // 使用构造器对象的newInstance方法初始化对象 197 | Object obj = constroctor.newInstance("龙哥", 29); 198 | ``` 199 | 200 | **13、反射中,Class.forName 和ClassLoader区别。** 201 | 202 | ClassLoader是类加载器,通过一个类的全限定名来获取描述此类的二进制字节流,遵循双亲委派模型,将.class文件加载到jvm中,不会执行static中的内容(会先赋值为零值),只有在newInstance才会执行static块。 203 | 204 | class.forName()方法内部实际上也是调用的ClassLoader来实现,但会对类进行初始化,执行类中的静态代码块,以及对静态变量的赋值等操作。 205 | 206 | **14、描述动态代理的几种实现方式,分别说出相应的优缺点。** 207 | 208 | 动态代理有两种实现方式,分别是:jdk动态代理和cglib动态代理。 209 | 210 | - jdk动态代理的前提是目标类必须实现一个接口,代理对象跟目标类实现一个接口。 211 | 212 | - cglib动态类是继承并重写了目标类(enhancer.create()方法返回的就是一个继承目标类的子类),所以目标类和方法不能被声明为final。 213 | 214 | 具体可以看我的另一篇文章,详细讲解了,[记动态代理](../notes/记动态代理.md) 215 | 216 | CGLib创建的动态代理对象性能比JDK创建的动态代理对象的性能高不少,但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。 217 | 218 | **15、动态代理与cglib实现的区别。** 219 | 220 | - JDK动态代理是通过接口中的方法名,在动态生成的代理类中调用业务实现类的同名方法; 221 | 222 | - CGlib动态代理是通过继承业务类,生成的动态代理类是业务类的子类,通过重写业务方法进行代理; 223 | 224 | **16、为什么CGlib方式可以对接口实现代理。** 225 | 226 | cglib动态代理是继承并重写目标类,所以目标类和方法不能被声明为final。而接口时可以被继承的。 227 | 228 | **17、final的用途。** 229 | 230 | - final修饰的对象不能被修改。 231 | - final修饰的类不能被继承。 232 | - final修饰的方法不能被重写(但可以被重载)。 233 | 234 | **18、写出三种单例模式实现。** 235 | 236 | 单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类成为单例类。 237 | 238 | 单例模式有三种:懒汉式单例,饿汉式单例,登记式单例(最好的)。 239 | 240 | 具体可以看我的其他文章。 241 | 242 | **19、如何在父类中为子类自动完成所有的hashcode和equals实现,这样做有何优劣。** 243 | 244 | 父类的equals不一定满足子类的equals需求。比如所有的对象都继承于Object,默认使用的是Object的equals方法,在比较两个对象的时候,是看他们是否指向同一个地址。 245 | 246 | 但我们的需求时对象的某个属性想通了,就相等了,而默认的equals方法满足不了当前的需求,所以要重写equals方法,加入自定义逻辑。 247 | 248 | 重写equals方法就必须重写hashcode方法,否则就会降低map等集合的索引速度。 249 | 250 | **20、请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用。** 251 | 252 | OO面向对象编程的设计理念是: 253 | 254 | - 抽象,先不考虑细节 255 | 256 | - 封装,隐藏内部实现 257 | 258 | - 继承,复用现有代码 259 | 260 | - 多态,改写对象行为 261 | 262 | 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的变量和方法只让可信的类或者对象操作,对不可信的进行隐藏,所以我们可以通过修饰符public、private、protected、default来进行访问控制。 263 | 264 | | 修饰符 | 类内部 | 本包 | 子类 | 外部包 | 265 | | --------- | ------ | ---- | ---- | ------ | 266 | | public | √ | √ | √ | × | 267 | | protected | √ | √ | √ | × | 268 | | default | √ | √ | × | × | 269 | | private | √ | × | × | × | 270 | 271 | **21、深拷贝和浅拷贝的区别。** 272 | 273 | - 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝。 274 | 275 | - 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容。 276 | 277 | **22、数组和链表数据结构描述,各自的时间复杂度。** 278 | 279 | - 数组是将元素在内存中连续存放,由于每个元素占用内存空间相同,可以通过下标迅速访问数组中任何元素。 280 | - 链表恰好相反,链表中元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。 281 | - 数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n); 282 | - 数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。 283 | - 数组和栈都存放在堆中,在虚拟机栈中只需要定义引用变量即可。 284 | 285 | **23、error和exception的区别,CheckedException,RuntimeException的区别。** 286 | 287 | - Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。 288 | 289 | - error(错误)表示系统级的错误和程序不必处理的异常,是java运行环境中的内部错误或者硬件问题。比如:内存资源不足等。对于这种错误,程序基本无能为力,除了推出运行外别无选择,它是由java虚拟机抛出的,是不可预料的异常情况。 290 | 291 | - Exception(违例)表示需要捕捉或者需要程序进行处理的异常,他处理的是因为程序设计的瑕疵而引起的问题或者在外的输入等引起的一般性问题,是程序必须处理的,是可预料的异常情况。 292 | 293 | Exception又分为运行时异常,受检查异常。 294 | 295 | - CheckedException(检查性异常)必须在编写代码时,使用try catch捕获(比如:IOException异常)。 296 | 297 | - RuntimeException(运行时异常)在代码编写时可以忽略捕获操作(比如:ArrayIndexOutOfBoundsException),这种异常是在代码编写或者使用过程中通过规范可以避免发生的。 298 | 299 | **24、请列出5个运行时异常。** 300 | 301 | - NullPointerException(空指针) 302 | - IndexOutOfBoundsException(数组越界) 303 | - ClassCastException(类转换异常) 304 | - ArrayStoreException(数据存储异常,操作数组时类型不一致) 305 | - IllegalArgumentException(非法参数异常) 306 | 307 | **25、在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么。** 308 | 309 | 不可以,双亲委派模式会保证父类加载器先加载类,就是BootStrap(启动类)加载器加载jdk里面的java.lang.String类,而自定义的java.lang.String类永远不会被加载到。 310 | 311 | 如果打破双亲委派模式,自己写一个classLoader来加载自己写的java.lang.String类,但是也会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。 312 | 313 | **26、说一说你对java.lang.Object对象中的hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。** 314 | 315 | Object类中的equals方法和“==”是一样的,没有区别,即俩个对象的比较是比较他们的栈内存中存储的内存地址。而String类,Integer类等等一些类,是重写了equals方法,才使得equals和“==不同”,他们比较的是值是不是相等。所以,当自己创建类时,自动继承了Object的equals方法,要想实现不同的等于比较,必须重写equals方法。比如我们的需求是对象的某个属性相同,就相等了,而默认的equals方法满足不了当前的需求,所以我们要重写equals方法。 316 | 317 | 如果重写了equals方法就必须重写hashcode方法,否则就会降低map等集合的索引速度。 318 | 319 | **27、在jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题。** 320 | 321 | 泛型的好处是在编译的时候检查类型安全,减少运行时由于对象类型不匹配引发的异常,并且所有的强制转换都是自动和隐士的,提高代码的复用率。 322 | 323 | **28、这样的a.hashcode()有什么用,与a.equels(b)有什么关系。** 324 | 325 | hashCode()方法是得到相应对象的hash值,它常用于基于hash的集合类,比如Hashtable,HashMap,LinkedHashMap等等。它与equals()方法关系特别密切,根据java规范,两个通过equals()方法来判断为相等的对象,必须拥有相同的hashcode。 326 | 327 | **29、有没有可能2个不相等的对象有相同的hashcode。** 328 | 329 | 有可能,最简单的方法是在自己实现类中重写hashcode()方法。 330 | 331 | **30、Java中的HashSet内部是如何工作的?** 332 | 333 | HashSet内部默认是通过一个HashMap来实现的。如下 334 | 335 | ```Java 336 | public HashSet() { 337 | map = new HashMap<>(); 338 | } 339 | private static final Object PRESENT = new Object(); 340 | public boolean add(E e) { 341 | return map.put(e, PRESENT)==null;//其中PRESENT是一个虚拟的对象。 342 | } 343 | ``` 344 | 345 | **31、什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。** 346 | 347 | 序列化是一种用户处理对象流的机制,所谓对象流就是将对象的内容进行流化。 348 | 349 | - 序列化:把对象转换为字节序列的过程称为对象的序列化。 350 | - 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。 351 | 352 | 对象实现序列化只需要实现Serializable接口即可。 353 | 354 | **32、java8的新特性。** 355 | 356 | - Lambda表达式和函数式接口 357 | - 接口的默认方法和静态方法,允许在接口内添加新的方法。 358 | - 重复注解,允许在同一个地方多次使用同一个注解。 359 | 360 | **33、Java序列化和反序列化相关。** 361 | 362 | Java序列化的的是对象的非静态字段及其值。 363 | 364 | Transient关键字修饰的成员变量在序列化过程中会被自动忽略。 365 | 366 | 而static静态成员属于类变量,也无法被序列化。 367 | 368 | **34、二叉树、平衡二叉树、红黑树的性质。** 369 | 370 | 普通的二叉查找树的性质是: 371 | 372 | - 若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值; 373 | - 若它的右子树上所有结点的值均大于它的根节点的值; 374 | - 它的左、右子树也分别为二叉排序树。 375 | 376 | 平衡二叉树,又被称为AVL树,它的左右两个子树的高度差不能超过1。平衡二叉树的目的是为了减少二叉查找树层次,提高查找速度。 377 | 378 | - 红黑树,也是一种近似的平衡二叉树。红黑树的每个节点上都有存储位表示节点的颜色,可能是红或黑。红黑树的特性是: 379 | - 每个节点或者是黑色,或者是红色。 380 | - 根节点是黑色。 381 | - 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] 382 | 如果一个节点是红色的,则它的子节点必须是黑色的。 383 | - 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。 384 | 385 | 红黑树的时间复杂度为O(logN),效率很高。 386 | 387 | 有一个比较有意思的小公式,树中节点数=总分叉数+1。(这里的分叉数就是所有节点的度之和) 388 | 389 | 后续的B-树或者B+树都是多路搜索树,不一定是二叉的了。在MySQL那篇我会仔细说明。 390 | 391 | **35、Java的IO流用了什么设计模式。** 392 | 393 | ava IO流的设计是基于装饰者模式&适配模式,面对IO流庞大的包装类体系,核心是要抓住其功能所对应的装饰类。 394 | 装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。装饰模式通过创建一个包装对象,也就是装饰,来包裹真实的对象。装饰模式以对客户端透明的方式动态地给一个对象附加上更多的责任。换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不创造更多子类的情况下,将对象的功能加以扩展。装饰模式把客户端的调用委派到被装饰类。装饰模式的关键在于这种扩展是完全透明的。 395 | 396 | 例如,InputStream就是装饰者模式中的超类,ByteArrayInputStream,FileInputStream相当于被装饰者,FilterInputStream就是装饰者。 397 | 398 | **36、Java中对象的引用。** 399 | 400 | Java语言中,除了原始的八个基本数据类型外,其他都是引用类型,指向各种不同的对象。不同的引用类型,不同之处在于对象不同的可达性及对垃圾回收的影响。主要分为: 401 | 402 | - 强引用: 403 | 404 | 只要引用存在,垃圾回收器永远不会回收。例如: 405 | 406 | ```java 407 | Object obj = new Object(); //其中obj就是强引用。通过关键字new创建的对象所关联的引用是强引用。 408 | ``` 409 | 410 | 特点:JVM内存空间不足时,JVM宁愿抛出OutOfMemoryError(OOM)运行时错误,使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通对象,如果没有其他的引用关系,只要超过了引用的作用域(如超出局部变量作用范围)或者显示将相应(强)引用赋值为null,就可以根据具体的垃圾回收机制被回收。 411 | 412 | - 软引用: 413 | 414 | 软引用通过SoftReference实现,非必须引用,生命周期比强引用短一些,内存溢出之前进行回收。 415 | 416 | 应用场景: 软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。 417 | 418 | - 弱引用: 419 | 420 | 弱引用通过WeakReference类实现,弱引用的生命周期比软引用短,被弱引用关联的对象只能生存到下一次垃圾回收之前,。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现具有弱引用的对象,不管当前的内存空间足够与否,都会回收他的内存。(由于垃圾回收器县城是一个优先级很低的线程,因此不一定会很快回收弱引用的对象)。 421 | 422 | 应用场景:弱引用同样是很多缓存实现的选择。 423 | 424 | - 幻象引用(虚引用): 425 | 426 | 通过PhantomReference类来实现。无法通过幻象引用访问对象的任何属性或函数。幻象引用仅仅是提供一种确保对象被finalize以后,做某些事情的机制。如果一个对象仅持有幻象引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 427 | 428 | **37、select、poll和epoll之间的区别。** 429 | 430 | 目前支持I/O多路复用的系统调用有select、poll和epoll,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select、poll和epoll本质上都是同步I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。 431 | 432 | 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。 433 | 434 | - select 435 | 436 | select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。 437 | 438 | 它的优点是跨平台性能好,几乎在所有的平台上都支持。 439 | 440 | select的不足在三个方面,第一个是单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024。第二个是对socket进行扫描时是线性扫描,即采用轮询的方式,效率较低。第三个是需要维护一个用来存放大量fd的数据结构,这样会使用户空间和内核空间在传递该结构时复制开销大。 441 | 442 | - poll 443 | 444 | poll和select没有区别,它的优点是没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点,则是大量的fd数组被整体复制于用户态和内核地址之间。 445 | 446 | - epoll 447 | 448 | epoll是select和poll的增强版本,相对于前两者来说,它更加灵活,没有描述符的数量限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。所有的fd都存入红黑树中。 449 | 450 | epoll支持水平触发和边缘触发,最大的特点是边缘触发,它只告诉线程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll通过“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来把该fd放入到双向链表(保存已就绪的事件),epoll_wait便可以收到通知。 451 | 452 | epoll的优点主要是三个方面:第一是没有最大并发连接的限制,能打开的FD的上限远大于1024;第二是效率提升,不是轮询的方式,不会随着FD数目的增加效率下降,这时候用到了回调;第三是内存拷贝,使用mmap减少了复制开销。 453 | 454 | 综上: 455 | 456 | - 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 457 | - select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。 458 | 459 | **38、final修饰的变量是引用不可变,还是引用的对象不能改变。** 460 | 461 | final修饰的变量是引用不可变,但是引用的对象还是可以发生改变。 462 | 463 | 如果final修饰的是一个基本数据类型的变量,那么这个变量就确定了,不能变了。 464 | 465 | 而如果final修饰的是一个引用变量,那么该变量存的是一个内存地址,该地址就不能变了,但是该地址所指向的那个对象还是可以变的。 466 | 467 | 举个例子 468 | 469 | ```Java 470 | final StringBuilder sb=new StringBuilder(); 471 | //sb=new StringBuilder();//程序报错。 472 | sb.append("a");//成功执行,sb对象内部可以改变。 473 | ``` 474 | 475 | **39、Java中Comparable和Comparator接口的区别。** 476 | 477 | Comparable是排序接口,若一个类实现了Comparable接口,就意味着这个类支持排序,不需要再去指定比较器。接口中通过x.compareTo(y)来比较x和y的大小。若返回负数,意味着x比y小;返回零,意味着x等于y;返回正数,意味着x大于y。 478 | 479 | Comparator是比较器接口。我们若需对某个类集合进行排序,而该类本身不支持排序(即没有实现Comparable接口),那么,我们就可以建立一个“该类的比较器”来进行排序。这个比较器只需要实现Comparator接口即可。 480 | 481 | 两者的联系则为:Comparable相当于内部比较器,而Comparator相当于外部比较器。 482 | 483 | **40、如何通过反射获取和设置对象私有字段的值。** 484 | 485 | 可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。代码如下所示。 486 | 487 | ```java 488 | public class Test { 489 | static class User { 490 | private String name; 491 | private int age; 492 | } 493 | public static void main(String[] args) { 494 | Class clz = User.class; 495 | try { 496 | Field age = clz.getDeclaredField("age"); 497 | age.setAccessible(true); 498 | Object obj = clz.newInstance(); 499 | age.setInt(obj, 18); 500 | } catch (Exception e) { 501 | e.printStackTrace(); 502 | } 503 | } 504 | } 505 | ``` 506 | 507 | **41、sleep()方法和yield()方法的区别。** 508 | 509 | - sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会。 510 | - 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态。 511 | - sleep()方法声明抛出InterruptedExecption,而yield()方法没有声明任何异常。 512 | - sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。 513 | 514 | 需要注意的是: 515 | 516 | 如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。 517 | 518 | 弥有,2019年9月 519 | [EOF] 520 | -------------------------------------------------------------------------------- /interviews/2020届秋招面试题总结-JVM篇.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——JVM篇 2 | 3 | **1、什么情况下会发生栈内存溢出。** 4 | 5 | 在HotSpot虚拟机中是不区分虚拟机栈和本地方法栈,栈是线程私有的,它的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈桢在虚拟机栈中入栈到出栈的过程。本地方法栈与虚拟机栈相似,区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 6 | 7 | 栈内存溢出是指线程请求的栈深度大于虚拟机所允许的最大深度,则将抛出StackOverflowError异常(StackOverflowError 不属于 OOM 异常)。最有可能的原因就是方法递归产生的这种结果。 8 | 9 | 另一个可能是引用了大的变量,在拓展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常(这个属于内存溢出)。 10 | 11 | **2、JVM的内存结构,Eden和Survivor比例。** 12 | 13 | ![Y4UgeO.png](https://s1.ax1x.com/2020/05/19/Y4UgeO.png) 14 | 15 | Java虚拟机在执行Java程序的过程中把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。 16 | 17 | - 程序计数器。当前线程执行的字节码的行号指示器,是线程私有的。也是唯一一个不会发生内存溢出的区域。 18 | - Java虚拟机栈。也是线程私有的,描述的是Java方法执行的内存模型,线程请求的栈深度大于虚拟机所允许的最大深度,则将抛出StackOverflowError异常。 19 | - 本地方法栈。与虚拟机栈相似,区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 20 | - Java堆。是Java虚拟机中管理的内存中最大的一块,所有线程共享区域,唯一目的就是存放对象实例。所有的对象实例以及**数组**都要在堆上分配内存。Java堆也是垃圾回收器管理的主要区域,也被称为gc堆,收集器基本都采用分代收集算法,Java堆中还可以细分为:新生代和老年代。 21 | - 方法区。所有线程共享区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。很多人也愿意称之为“永久代”。 22 | - 运行时常量池。是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。 23 | - 直接内存。并不是虚拟机运行时数据区的一部分。例如NIO,它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样避免了在Java堆和Native堆中来回复制数据,提高了性能。 24 | 25 | JVM中要对堆进行分代,分代的理由是优化GC性能,很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。 26 | 27 | HotSpot JVM把新生代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。 28 | 29 | 因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。 30 | 31 | **3、JVM内存为什么要分成新生代、老年代和持久代。新生代中为什么要分Eden和Survivor。** 32 | 33 | 堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率,这简直太可怕了。 34 | 35 | 有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。 36 | 37 | HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。 38 | 39 | **4、JVM中一次完整的GC流程是什么样子的,对象如何晋升到老年代,说说你知道的几种主要的JVM参数。** 40 | 41 | GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。 42 | 43 | 对象晋升老年代有三种可能: 44 | 45 | - 当对象达到成年,经历过15次GC(默认是15,可配置),对象就晋升到老年代了。 46 | - 大的对象会直接在老年代创建。 47 | - 新生代的Survivor空间内存不足时,对象可能直接晋升到老年代。 48 | 49 | jvm参数: 50 | 51 | - -Xms:初始堆大小 52 | - -Xmx:堆最大内存 53 | - -Xss:栈内存 54 | - -XX:PermSize 初始永久代内存 55 | - -XX:MaxPermSize 最大永久带内存 56 | 57 | **5、你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。** 58 | 59 | 常见的垃圾收集器主要有以下四种: 60 | 61 | - 串行收集器(Serial、ParNew收集器):简单高效,但它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,中间停顿时间长。 62 | - 并行收集器(Parallel Scavenge收集器):吞吐量优先,主要关注点在于精确控制吞吐量,即减少GC停顿时间,但收集次数变多。 63 | - CMS:以获取最短回收停顿时间为目标的收集器,并发**标记-清除**,主要步骤有,初始标记,并发标记,重新标记和并发清除。其中,整个过程耗时最长的并发标记和并发清除过程收集器线程都可以和用户线程一起工作,CMS收集器的内存回收过程始于用户线程一起并发执行的。重新标记是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。但缺点有,CMS收集器对CPU资源非常敏感,并且无法处理浮动垃圾。 64 | - G1:可预测停顿的收集器,并发**标记-整理**,主要步骤分为,初始标记,并发标记,最终标记和筛选回收。G1把内存“化整为零”,并且可以分代收集。注意:CMS是清除,所以会存在很多的内存碎片。G1是整理,所以碎片空间较小。 65 | 66 | **6、垃圾回收算法的实现原理。** 67 | 68 | 垃圾收集算法主要分为以下三种: 69 | 70 | - 标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。 71 | - 复制算法:将可用内存按容量分为两块(Eden和Survivor空间),每次只使用一块,当这一块内存用完后,就将还活着的对象复制到另外一块上面,然后再把已使用过内存空间一次清理掉。 72 | - 标记-整理算法:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界外的内存。 73 | 74 | 需要注意的是,“标记-清除”算法存在**两个不足**: 75 | 76 | - 一个是**效率问题**,标记和清除两个过程的效率都不高; 77 | - 另一个是**空间问题**,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一个垃圾收集动作。作为对比,复制算法每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。 78 | 79 | **7、当出现了内存溢出,怎么排错。** 80 | 81 | - 首先控制台查看错误日志。 82 | - 然后使用jdk自带的VisualVM来查看系统的堆栈日志(也可以用jmap查看堆转储快照)。 83 | - 定位出内存溢出的空间:堆,栈还是永久代(jdk8后没有永久代的溢出了)。 84 | - 如果是堆内存溢出,看是否创建了超大的对象。 85 | - 如果是栈内存溢出,看是否创建了超大的对象,或者产生了死循环,或者递归调用。 86 | 87 | **8、JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等。** 88 | 89 | 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 90 | 91 | 内存屏障是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。 92 | 93 | happen-before用来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。具体原则如下: 94 | 95 | - 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。 96 | - 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。 97 | - volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。 98 | - 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。 99 | - 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。 100 | - 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。 101 | - 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。 102 | - 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C。 103 | 104 | 主内存是指所有线程共享的内存空间。 105 | 106 | 工作内存是指每个线程特有的内存空间。工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写在主内存中的变量。 107 | 108 | 参考链接:[JVM内存模型、指令重排、内存屏障概念解析](https://www.cnblogs.com/chenyangyao/p/5269622.html) 109 | 110 | **9、讲讲JAVA的反射机制。** 111 | 112 | Java反射说的是在运行状态中,对于任何一个类,我们都能够知道这个类有哪些方法和属性。对于任何一个对象,我们都能够对它的方法和属性进行调用。我们把这种动态获取对象信息和调用对象方法的功能称之为反射机制。 113 | 114 | **10、你们线上应用的JVM参数有哪些。** 115 | 116 | - -Xms512m //初始堆大小 117 | - -Xmx1024m //最大堆大小 118 | - -XX:PermSize=640m //设置持久代初始值 119 | - -XX:MaxPermSize=1280m //设置持久代最大值 120 | - -XX:NewSize=64m //设置年轻代初始值 121 | - -XX:MaxNewSize=256m //设置年轻代最大值 122 | - -verbose:gc //表示输出虚拟机中GC的详细情况 123 | - -XX:+PrintGCDetails //日志输出形式 124 | - -XX:+PrintGCTimeStamps //日志输出形式 125 | 126 | 在默认情况下,JVM初始分配的堆内存大小是物理内存的1/64,最大分配的堆内存大小是物理内存的1/4。 127 | 128 | 默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。 129 | 130 | 因此服务器一般设置-Xms、-Xmx相等,来避免每次GC后调整堆的大小。 131 | 132 | **11、g1和cms区别,吞吐量优先和响应优先的垃圾收集器选择。** 133 | 134 | CMS是基于“标记-清除”实现的,主要步骤是初始标记,并发标记,重新标记和并发清除。 135 | 136 | G1是基于“标记-整理”实现的,主要步骤是初始标记,并发标记,最终标记和筛选回收。 137 | 138 | CMS的缺点是对CPU的要求比较高。 139 | 140 | G1的缺点是将内存划分了多块,所以对内存段的大小有很大的要求。 141 | 142 | CMS是清除,所有会有很多的内存碎片。 143 | 144 | G1是整理,所有碎片空间较小。 145 | 146 | G1和CMS都是响应优先,他们的目的都是尽量控制stop the world的时间。 147 | 148 | G1和CMS的Full GC都是单线程 mark sweep compact算法,直到JDK10才优化成并行的。 149 | 150 | CMS目前只用于老年代,而G1是将整个Java堆划分为多个大小不等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔的了,他们都是一部分Region(不需要连续)的集合。 151 | 152 | 吞吐量优先的话可以选择并行垃圾收集器,Parallel Scavenge收集器。吞吐量是指CPU用于运行用户代码的时间与CPU总消耗时间的比值。 153 | 154 | **12、怎么打印线程栈信息。** 155 | 156 | ```java 157 | StackTraceElement[] elements = (new Throwable()).getStackTrace(); 158 | StringBuffer buf = new StringBuffer(); 159 | for(int i=0; i constructor =User.class.getConstructor(); 206 | User user= constructor.newInstance(); 207 | ``` 208 | 209 | - 用反序列化,调用ObjectInputStream类的readObject()方法。 210 | 211 | **15、Java中的对象一定在堆上分配内存吗?** 212 | 213 | 前面我们说过,Java堆中主要保存了对象实例,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。 214 | 215 | 其实,在编译期间,JIT会对代码做很多优化。其中有一部分优化的目的就是减少内存堆分配压力,其中一种重要的技术叫做逃逸分析。 216 | 217 | 如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。 218 | 219 | 参考文章:[深入理解Java中的逃逸分析](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650121615&idx=1&sn=00d412f68fe58dceab6d13fdfefac113&chksm=f36bb8aec41c31b8d62069e2663345c0452ebdded331616496637e19b2cad72725f6ce90daec&scene=21#wechat_redirect) 和 [对象并不一定都是在堆上分配内存的](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650121307&idx=1&sn=5526473d0248cca8385d2a18ba6b25af&chksm=f36bb97ac41c306c354ebf0335cd2fd77cac03f3434894e4e5b44a01754a5494b04350d26d14&scene=21#wechat_redirect) 220 | 221 | **16、运行时数据中哪些区域是线程共享的,哪些是独享的。** 222 | 223 | 在JVM运行时内存区域中,程序计数器、虚拟机栈和本地方法栈是线程独享的。而Java堆、方法区是线程共享的。但是值得注意的是,Java堆其实还为每一个线程单独分配了一块TLAB空间(本地线程分配缓冲),这部分空间在分配时是线程独享的,在使用时是线程共享的。([TLAB介绍](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650124457&idx=1&sn=1c33947700dfb28048df4a913b434077&chksm=f36bad88c41c249ea854b371a1c8597959e2e35c2890bdd6a5945df0b568bdfc980d1dd2cf2b&scene=21#wechat_redirect)) 224 | 225 | 创建对象时,内存分配过程如何保证线程安全性?有两种解决方案: 226 | 227 | - 对分配内存空间的动作做同步处理,采用CAS机制,配合失败重试的方式保证更新操作的线程安全性。 228 | - 每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配,当这部分区域用完之后,再分配新的"私有"内存。**这个本地线程分配缓冲就叫做TLAB。** 229 | 230 | **17、Java中数组是存储在堆上还是栈上。** 231 | 232 | 在Java中,数组同样是一个对象,所以对象在内存中如何存放同样适用于数组; 233 | 234 | 所以,数组的实例是保存在堆中,而数组的引用是保存在栈上的。 235 | 236 | **18、Java对象创建的过程是怎么样的。** 237 | 238 | 对于一个普通的Java对象的创建,大致过程如下: 239 | 240 | 1. 虚拟机遇到new指令,到常量池定位到这个类的符号引用。 241 | 2. 检查符号引用代表的类是否被加载、解析、初始化过 ,如果没有的话,则执行相应的类加载过程。 242 | 3. 虚拟机为对象分配内存。 根据Java内存是否规整,分别通过“指针碰撞”或“空闲列表”来分配。 243 | 4. 虚拟机将分配到的内存空间都初始化为零值。 244 | 5. 虚拟机对对象进行必要的设置。 245 | 6. 执行方法,成员变量进行初始化。 246 | 247 | **19、怎么获取堆和栈的dump文件。** 248 | 249 | Java Dump,Java虚拟机的运行时快照。将Java虚拟机运行时的状态和信息保存到文件。 250 | 251 | 可以使用在服务器上使用jmap命令来获取堆dump,使用jstack命令来获取线程的调用栈dump。 252 | 253 | 参考文章:[Java命令学习系列(二)——Jstack](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=402296484&idx=1&sn=8e7fc8197a216afb590b17e15f9b721e&chksm=796493854e131a932b3dd53839820eaba022cb87a601062b6bf6a574d742cd8e92a707432173&scene=21#wechat_redirect) 和 [Java命令学习系列(三)——Jmap](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=402312019&idx=1&sn=97736feb967ecbffb454fa037015ad6d&chksm=7964d6724e135f64a5c0d65e41afbeac45700dd91149375f99071731954e855e13b11cd6c30b&scene=21#wechat_redirect) 254 | 255 | **20、Minor GC和Full GC的触发条件。** 256 | 257 | Minor GC触发条件:当Eden区满时,触发Minor GC。 258 | 259 | Full GC触发条件: 260 | 261 | - 调用System.gc时,系统建议执行Full GC,但是不必然执行。 262 | - 老年代空间不足。 263 | - 方法区空间不足。 264 | - concurrent mode failure,当执行CMS GC过程时(“标记-清除”,存在内存碎片),同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。 265 | 266 | **21、在Java语言中,可以作为GC Roots的对象有什么。** 267 | 268 | 可作为GC Roots的对象包括以下几种: 269 | 270 | - 虚拟机栈(栈桢中的本地变量表)中引用的对象。 271 | - 方法区中类静态属性引用的对象。 272 | - 方法区中常量引用的对象。 273 | - 本地方法栈中JNI(即一般说的Native方法)引用的对象。 274 | 275 | 获取GC Roots最主要的部分在解决如果快速找到JVM栈的栈桢的局部变量表中的局部变量所引用的对象。大致思路是JVM采用了**OopMap**这个数据结构记录了GC Roots,GC的标记开始的时候,直接用OopMap就可以获得GC Roots。OopMap记录了特定时刻栈上(内存)和寄存器(CPU)的哪些位置是引用,通过这些引用就可以找到堆中的对象,这些对象就是GC Roots,而不需要一个一个的去判断某个内存位置的值是不是引用。 276 | 277 | **22、类加载过程。** 278 | 279 | Java虚拟机中类加载的全过程包括:加载、验证、准备、解析和初始化这5个阶段。 280 | 281 | **加载:** 282 | 283 | 在加载阶段,虚拟机主要完成以下3个事情。 284 | 285 | - 通过一个类的全限定名来获取定义此类的二进制字节流。(这一步骤就是通过类加载器来实现的) 286 | - 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 287 | - 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各类数据的访问入口。 288 | 289 | **验证:** 290 | 291 | 验证时连接阶段的第一步,这一阶段的目的是为了**确保Class文件的字节流中包含的信息符合当前虚拟机的要求**,并且不会危害虚拟机自身的安全。 292 | 293 | - 文件格式验证:验证字节流是否符合Class文件格式的规范,包括文件头部的魔数因子、class文件主次版本号、class文件的MD5指纹等。 294 | - 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范。简单来说就是验证Java语法的正确性。 295 | - 字节码验证:主要验证程序的控制流程,如循环、分支等。 296 | 297 | **准备:** 298 | 299 | 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在**方法区**中分配。需要注意的是,这时候进行内存分配的仅包括**类变量(被static修饰的变量)**,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。 300 | 301 | **解析:** 302 | 303 | 解析阶段是虚拟机在常量池内寻找类、接口、字段和方法的符号引用,并且将这些**符号引用替换为直接引用**的过程。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。 304 | 305 | **初始化:** 306 | 307 | 初始化阶段是类的加载过程的最后一个阶段,该阶段主要做一件事情就是执行<clinit>(),该方法**会为所有的静态变量赋予正确的值**。 308 | 309 | 参考文章:[万万没想到,JVM内存结构的面试题可以问的这么难?](https://mp.weixin.qq.com/s?__biz=MzI5NTYwNDQxNA==&mid=2247485214&idx=1&sn=32aa3d83464435188be9ac52c8e9c588&chksm=ec505ecfdb27d7d97897f74b36d28bc67536b55b0f252cb1f5f006b5a3439a2e70965f86a1ff&scene=0&xtrack=1&key=546b3b791faf1f5f02217e88c54f3b221d19b0d2e192b57f4847f7cf607d2c54b426e5e62c83ab9bd93c35a1b850ecf229accf8266fc1ac362a34ae7d687b9cc4a342ed4f54c7809a3e40847fff4160a&ascene=1&uin=MjQ3MzkwMTc2Mw==&devicetype=Windows+10&version=62060844&lang=zh_CN&pass_ticket=W35VcrckR39Y5Fn9My7l/KozGVDszT28Gg6T3fIMrYKZDiAMan7yl4BY759W+Uo+) 310 | 311 | **23、Java内存泄漏的场景。** 312 | 313 | 内存泄漏是指,一个不再被程序使用的对象或变量还在内存中占有存储空间。虽然Java拥有GC,但还是会出现内存泄漏。举个例子。 314 | 315 | ```Java 316 | //首先,要明白,GC它回收的是不可到达的对象,但是,在static 的集合类中,引用可以到达,但是却有可能对象已经不用了 317 | //首先定义一个静态变量 318 | public static ArrayList list = new ArrayList(); 319 | public void stackOverExam(Object object){ 320 | //当非静态变量被static变量持有引用的时候,容易发生内存泄露,因为object是一直被list引用着的 321 | list.add(object); 322 | object = null;//这里设置为null并没有达到释放object引用对应对象的效果,毕竟list还是持有引用 323 | } 324 | ``` 325 | 326 | 通过上面的代码可以看到,由于static指向的对象是不能被垃圾回收器回收的,所以,间接的object也是无法被回收的,当业务对象很大而且很多的时候,便有了内存泄漏的风险。所以,可以总结如下规则: 327 | 328 | 当全局的静态变量持有局部变量(或者说,大范围的变量持有小范围变量而且小范围变量消耗内存表达、数目变多时),程序便有内存泄漏的风险。一般来说,类似的例子还有,单例模式中的对象,模块之间的调用(后面这个例子提到)等。 329 | 330 | 先举一个单例对象的例子。由于单例的静态特征使得其生命周期和应用的生命周期一样长,如果一个对象已经不再被使用,而单例对象还会持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。 331 | 332 | ```java 333 | public class Singleton { 334 | private static Singleton singleton; 335 | private List list; 336 | private Singleton(List list) { 337 | this.list = list; 338 | } 339 | public Singleton getInstance(List list) { 340 | if (singleton == null) { 341 | singleton = new Singleton(list); 342 | } 343 | return singleton; 344 | } 345 | } 346 | ``` 347 | 348 | 另外再举一个模块调用的例子,现在有两个类A和B,其中B的默认构造函数上是需要一个A的实例作为参数的,这就让A和B产生了依赖。 349 | 350 | ```java 351 | A a=new A(); 352 | B b=new B(a); 353 | a=null; 354 | ``` 355 | 356 | a是对象A的引用,b是对象B的引用,对象B同时还依赖对象A,那么这个时候就可以认为对象B是可以到达对象A的。当A对象的引用a置为null后,a不再指向对象A的引用了,按理说对象A可以GC了。但是因为B依赖着A,所以这个时候,A对象是不可能被回收了,造成了内存泄漏。这个时候可以用弱引用WeakReference来代替对象B,就可以解决了这个问题。如下所示。 357 | 358 | ```java 359 | A a=new A(); 360 | WeakReference wr=new WeakReference(a); 361 | a=null; 362 | ``` 363 | 364 | **24、jdk1.8的虚拟机中内存模型变化。** 365 | 366 | 在jdk1.8中变化最大的是取消了永久区Perm,而是用元数据空间Metaspace来进行替换。需要注意的是,元空间占用的内存不是虚拟机内部的,而是本地内存空间,当然也不是堆内存。这个变化的理由如下: 367 | 368 | - 在jdk1.8之前的HotSpot实现中,类的元数据如方法数据、方法信息(字节码、栈和变量的大小)、运行时常量池等保存在永久代。32位默认永久代为64M,64位默认85M,可以通过参数-XX:MaxPermSize进行设置,一旦类的元数据超过了永久代的大小,就会抛出OOM异常了。 369 | - 对永久代的调优过程很困难,因为永久代的大小很难确定,其中涉及到很多因素,如类的总数、常量池大小和方法数量等,而且永久代的护具可能会随着每一次Full GC而发生移动。 370 | - 在jdk1.8中,类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间。 371 | 372 | **25、频繁GC的原因。** 373 | 374 | - 人为因素,在代码中调用了System.gc()方法。 375 | - 内存原因,设置的堆大小比较小,可以提高堆的空间,比如说提高最小堆空间-Xms和最大堆空间-Xmx的大小,当然,最好是针对内存的DUMP文件进行分析。 376 | - 框架问题,有些框架内部会调用gc方法。 377 | - 其他原因,构建的对象实例化十分频繁并且释放对象较为频繁时,也会引起频繁gc。 378 | 379 | 如果线上系统突然产生的运行缓慢问题,如果该问题导致线上系统不可用,那么首先需要做的就是,导出jstack和内存信息,然后重启系统,尽快保证系统的可用性。这种情况可能的原因主要有两种: 380 | 381 | - 代码中某个位置读取数据量较大,导致系统内存耗尽,从而导致Full GC次数过多,系统缓慢; 382 | - 代码中有比较耗CPU的操作,导致CPU过高,系统运行缓慢。 383 | 384 | **26、高并发时,JVM调优。** 385 | 386 | 优化虚拟机堆的空间大小,根据实际物理内存的大小进行比例分配,并根据程序调整好新生代和老年代的比例。并且,堆不进行自动扩展。然后使用ParNew(并发)+CMS进行垃圾回收,在多线程高并发的情况下,表现很好。 387 | 388 | 调优的目标是: 389 | 390 | - 将转移到老年代的对象数量降低到最小 。 391 | - 减少Full GC的执行时间。 392 | 393 | **27、System.gc()和Runtime.gc()的区别。** 394 | 395 | - java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,两者的行为没有任何不同。 396 | 397 | - System.gc()和runtime.gc()用于建议jvm进行垃圾回收,但是否立即回收还是延迟回收由Java虚拟机决定。 398 | 399 | 另外,当我们调用System.gc()的时候,其实并不会马上进行垃圾回收,甚至不一定会执行垃圾回收。 400 | 401 | 以上主要参考来源为:《深入理解Java虚拟机:JVM高级特征与最佳实践》 402 | 403 | 弥有,2019年9月 404 | [EOF] 405 | -------------------------------------------------------------------------------- /interviews/2020届秋招面试题总结-MySQL篇.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——MySQL篇 2 | 3 | **1、数据库隔离级别有哪些,各自的含义是什么,MySQL默认的隔离级别是多少。** 4 | 5 | 隔离级别有四种。 6 | 7 | - 未提交读:是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。会出现脏读。 8 | - 读写提交:是指一个事务只能读取另一个事务已经提交的数据,不能读取未提交的数据。克服了脏读,但会出现不可重复读现象。 9 | - 可重复读:克服读写提交中出现的不可重复读现象。但会出现幻读现象。 10 | - 串行化:数据库中最高的隔离级别,她会要求所有的SQL都会按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以它能完全保证数据的一致性。 11 | 12 | MySQL默认的隔离级别是可重复读。 13 | 14 | **2、什么是幻读。** 15 | 16 | 幻读是指在同一个事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。 17 | 18 | 事务A读取与搜索条件相匹配的若干行,事务B以插入或删除行等方式来修改事务A的结果集,然后再提交,就会发生幻读。例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样。 19 | 20 | 在默认的事务隔离级别下,即REPEATABLE READ(可重复读)下,InnoDB存储引擎采用**Next-Key Locking**机制来避免幻读。 21 | 22 | 具体看《MySQL技术内幕-InnoDB存储引擎》的6.4.2小节。 23 | 24 | **3、MySQL有哪些存储引擎,各自优缺点。** 25 | 26 | MySQL支持InnoDB、MyISAM、MEMORY等存储引擎。 27 | 28 | InnoDB引擎(MySQL5.5以后默认使用): 29 | 30 | - 灾难恢复性好 31 | - 支持事务 32 | - 使用行级锁和表级锁,能支持更多的并发量 33 | - 查询不加锁 34 | - 支持外键关联 35 | - 支持热备份 36 | - 实现缓冲管理 37 | 38 | MyISAM引擎: 39 | 40 | - 不支持事务 41 | - 使用表级锁,并发性差 42 | - 主机宕机后,MyISAM表易损坏,灾难恢复性不佳 43 | - 可以配合锁,实现操作系统下的复制备份、迁移 44 | - 只缓存索引 45 | - 数据紧凑存储,因此可获得更小的索引和更快的全表扫描性能 46 | 47 | 两者主要区别: 48 | 49 | - InnoDB支持事务,MyISAM不支持事务处理等高级处理。 50 | - InnoDB支持行级锁,而MyISAM仅支持表级锁。 51 | - MyISAM类型的表强调的是性能,其执行速度比InnoDB类型更快。 52 | - MyISAM适合查询以及插入为主的应用,InnoDB适合频繁修改以及涉及到安全性较高的应用。 53 | - InnoDB支持外键,MyISAM不支持。 54 | - MyISAM支持全文搜索,而InnoDB 1.2.x版本后才支持。 55 | - 对于自增长的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中可以和其他字段一起建立联合索引。 56 | 57 | **4、高并发下,如何做到安全的修改同一行数据。** 58 | 59 | - 使用悲观锁。本质是当前只有一个线程执行操作,排斥外部请求的修改。遇到加锁的状态,就必须等待。结束了唤醒其他线程进行处理。但是,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。 60 | - FIFO(先进先出)缓存队列思路,直接将请求放入队列中,这样就不会导致某些请求永远获取不到锁。有点强行把多线程变成单线程的感觉。 61 | - 使用乐观锁。相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。 62 | 63 | **5、乐观锁和悲观锁是什么,InnoDB的标准行级锁有哪两种,解释其含义。** 64 | 65 | 悲观锁和乐观锁是两种常见的资源并发锁设计思路。 66 | 67 | **悲观锁**:它指的是对数据被外界(包括当前系统的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁是使用数据库内部的锁(排他锁)对记录进行加锁,从而使得其他事务等待以保证数据的一致性。**通常通过常用的select … for update操作来实现悲观锁**,在SQL的最后加入for update语句,就可以在数据库事务执行过程中,锁定查询出来的数据,其他事务将不能再对其进行读写操作,这样避免了数据的不一致,单个请求直至数据库事务完成,才会释放这个锁。 68 | 69 | **乐观锁**:是指一种不使用数据库锁和不阻塞线程并发的思路。它的特点是先进行业务操作,不到万不得已不去拿“锁”。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。乐观锁在数据库的实现完全是逻辑的,不需要数据库提供特殊的支持,一般的做法是在需要锁的数据上增加一个版本号,或者时间戳。 70 | 71 | InnoDB存储引擎实现了两种标准的行级锁: 72 | 73 | - 共享锁(S Lock),允许事务读一行数据,可以多个事务同时获取,也成为锁兼容。但阻止其他事务获得相同数据集的排他锁。 74 | - 排他锁(X Lock),允许事务删除或更新一行数据,组织其他事务取得相同数据集的共享读锁和排他写锁。 75 | 76 | 另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁,这两种意向锁都是表级别的锁。 77 | 78 | - 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁,事务再给一个数据行加共享锁之前必须先取得该表的IS锁。 79 | - 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁,事务在给一个数据行加排他锁之前必须先取得该表的IX锁。 80 | 81 | 简洁来说,悲观锁就是用共享锁和排他锁;而乐观锁,实际通过版本号,从而实现CAS原子性更新。 82 | 83 | **5、SQL优化的一般步骤是什么,怎么看执行计划,如何理解其中各个字段的含义。** 84 | 85 | SQL优化步骤一般是: 86 | 87 | - 通过show status命令了解各种SQL的执行频率 88 | - 定位执行效率较低的SQL语句 89 | - 通过EXPLAIN分析较低SQL的执行计划 90 | - 通过show profile分析SQL 91 | - 通过trace分析优化器如何选择执行计划 92 | - 确定问题并采取相应的优化措施 93 | 94 | 执行计划是SQL在数据库执行时的表现情况,通常用于SQL性能分析、优化等场景。在MySQL中使用explain关键字来查看。 95 | 96 | 参考链接: [SQL优化的一般步骤是什么,怎么看执行计划,如何理解其中各个字段的含义。](https://blog.csdn.net/riemann_/article/details/91349161) 97 | 98 | **6、数据库会死锁吗,举一个死锁的例子,mysql是怎么解决死锁的。** 99 | 100 | 数据库会出现死锁。死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种相互等待的现象。 101 | 102 | 举个例子,一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。 103 | 104 | 解决死锁最简单的方式是不要有等待,将任何的等待都转换为回滚,并且事务重新开启。但这可能导致并发性能的下降,甚至任何一个事务都不能进行。这个方法不适用。 105 | 106 | 另一个简单方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。 107 | 108 | 除了超时机制外,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测,这是一种更为主动的死锁检测方式,InnoDB存储引擎中也采用这种方式。 109 | 110 | **7、MySQL的索引原理,索引的类型有哪些,如何创建合理的索引,索引如何优化。** 111 | 112 | MySQL中索引采用的数据结构主要是B+Tree,Hash,平衡二叉树等。 113 | 114 | 索引的类型可以分为: 115 | 116 | - 普通索引(INDEX),最基本的索引,没有任何的约束。INDEX index_name (name) 117 | - 唯一索引(UNIQUE),与普通索引类似,但索引列的值必须唯一,但允许有控制(注意和主键不同)。如果是组合索引,则列值的组合必须唯一,创建方法和普通索引类似。UNIQUE index_name (name) 118 | - 全文索引( FULLTEXT ),MyISAM 表全系支持,InnoDB 1.2.x后支持。FULLTEXT (content) 119 | - 主键索引(PRIMARY KEY),特殊的唯一索引,一个表只能有一个,不允许有空值。 120 | - 复合索引,将多个列组合在一起创建索引,可以覆盖多个列。 121 | 122 | 索引如何优化: 123 | 124 | - 非空字段 NOT NULL,Mysql 很难对空值作查询优化 125 | - 区分度高,离散度大,作为索引的字段值尽量不要有大量相同值 126 | - 索引的长度不要太长(比较耗费时间) 127 | 128 | 参考链接:[mysql索引总结(1)-mysql 索引类型以及创建](https://www.cnblogs.com/crazylqy/p/7615388.html) 和 [MySQL的索引原理,索引的类型有哪些,如何创建合理的索引,索引如何优化。](https://blog.csdn.net/riemann_/article/details/91358943) 129 | 130 | **8、聚集索引和非聚集索引的区别。** 131 | 132 | 非聚集索引也称之为辅助索引。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的数据。聚集索引叶子节点存放的即为整张表的行记录数据;而辅助索引叶子节点除了包含键值以外,还包含了一个书签(bookmark),该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此**InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。** 所以,聚集索引一般比辅助索引体积大。 133 | 134 | 由于实际的数据页只能按照一颗B+树进行排序,因此**每张表只能拥有一个聚集索引**。在多数情况下,查询优化器倾向于采用聚集索引。因为聚集索引能够在B+树索引的叶子节点上只能找到数据。聚集索引的好处在于,它对于主键的排序查找和范围查找速度都非常快,叶子节点的数据就是用户所要查询的数据。需要注意的是: 135 | 136 | - 如果一个主键被定义了,那么这个主键就是作为聚集索引。 137 | - 如果没有主键被定义,那么该表的第一个唯一非空索引被作为聚集索引。 138 | - 如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,改列的值会随着数据的插入自增。 139 | 140 | 另外,切记的是,**聚集索引的存储并不是物理上连续的,而是逻辑上连续的。** 这其中有两点: 141 | 142 | - 每个表的数据页通过双向链表链接,页按照主键的顺序排序; 143 | - 每个页的记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储。 144 | 145 | 辅助索引的存在并不影响数据在聚集索引的组织,所以每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引(也就是聚集索引)的主键,然后在通过主键索引来找到一个完整的行记录。 146 | 147 | 以上说的都是InnoDB存储引擎场景下,而对于MyISAM引擎,索引文件和数据文件是分离的,索引文件仅保存数据记录的地址,具体不细究。 148 | 149 | 具体看《MySQL技术内幕-InnoDB存储引擎》的5.4小节。 150 | 151 | **9、select for update是什么含义,会锁表还是锁行还是其他。** 152 | 153 | select for update会锁定查询出来的数据,其他事务将不能再对其进行读写操作,这样避免了数据的不一致,单个请求直至数据库事务完成,才会释放这个锁。**记住,for update是排他锁。** 154 | 155 | 当使用select ... for update ...where ...时,mysql进行row lock还是table lock只取决于是否有明确的指定主键,能则为行锁,否则为表锁;未查到数据则无锁。 156 | 157 | **10、为什么要用Btree实现,它是怎么分裂的,什么时候分裂,为什么是平衡的。** 158 | 159 | 具体看《MySQL技术内幕-InnoDB存储引擎》 160 | 161 | **11、数据库的ACID是什么。** 162 | 163 | 数据库系统引入事务的主要目的在于,事务会把数据库从一种一致性状态转换为另一种一致状态。 164 | 165 | 数据库事务具有以下4个基本特征,也就是著名的ACID。 166 | 167 | - Atomic(原子性):事务中包含的操作被看作是一个整体的业务单元,这个业务单元中的操作要么全部成功,要么全部失败,不会出现部分失败、部分成功的场景。 168 | - Consistency(一致性):事务在完成时,必须使所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证了数据的完整性。 169 | - Isolation(隔离性):数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上压制丢失更新的发生。 170 | - Durability(持久性):事务结束后,所有的数据会固化到一个地方,如保存到磁盘中,即使断电重启后也可以提供给应用程序访问。 171 | 172 | **12、某一个表有近千万的数据,CRUD比较慢,如何优化。** 173 | 174 | - 可以做表拆分,减少单表字段数量,优化表结构。 175 | - 在保证主键有效的情况下,检查主键索引的字段顺序,使得查询语句中条件的字段顺序和主键索引的字段顺序保持一致。 176 | - 建立合理的索引。 177 | - 可以结合Redis、Memcache等缓存服务,把复杂的SQL进行拆分,充分利用二级缓存,减少数据库IO操作。 178 | 179 | 参考链接: [某个表有近千万数据,CRUD比较慢,如何优化?](https://blog.csdn.net/riemann_/article/details/93676341) 180 | 181 | **13、MySQL是怎么优化table scan的。** 182 | 183 | 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。 184 | 185 | 避免全表扫描的优化方案: 186 | 187 | - 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null 188 | - 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。 189 | - 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num=10 or num=20。也可以这样查询: select id from t where num=10 union all select id from t where num=20 190 | - in 和 not in 也要慎用,否则会导致全表扫描,如:select id from t where num in(1,2,3) 。可以用between来代替in,如select id from t where num between 1 and 3 191 | - 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:select id from t where num/2=100。应改为:select id from t where num=100*2 192 | 193 | **14、如何写SQL能够有效地使用到复合索引。** 194 | 195 | 复合索引也叫组合索引,用户可以在多个列上建立索引,这种索引叫做复合索引(组合索引)。复合索引在数据库操作期间所需的开销更小,可以代替多个单一索引。 196 | 197 | 创建复合索引:CREATE INDEX columnId ON table1(col1,col2,col3) ; 198 | 199 | 使用复合索引:select * from table1 where col1= A and col2= B and col3 = C 200 | 201 | 对于复合索引,在查询使用时,最好将条件顺序按找索引的顺序,这样效率最高。 202 | 203 | 复合索引可以用到多个where条件查询下,比如查询年龄是12和性别是男的所有学生。这样避免了多一次的排序操作。 204 | 205 | 参考链接: [如何写sql能够有效的使用到复合索引](https://blog.csdn.net/riemann_/article/details/94840416) 206 | 207 | 另外,**联合索引具有最左匹配原则,即最左优先。** 比如,我们建立了一个2列的联合索引(col1,col2),实际上已经建立了两个联合索引(col1)、(col1,col2),解释如下。 208 | 209 | B+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道第一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了(这种情况下无法使用联合索引)。 210 | 211 | 联合索引的意义在于: 212 | 213 | - 一个顶三个。建了一个(a,b,c)的复合索引,那么实际等于建了(a),(a,b),(a,b,c)三个索引,因为每多一个索引,都会增加写操作的开销和磁盘空间的开销。 214 | - 作为覆盖索引。同样的有复合索引(a,b,c),如果有如下的sql: select a,b,c from table where a=1 and b = 1。那么MySQL可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机io操作。 215 | - 索引列越多,通过索引筛选出的数据越少。 216 | 217 | 参考链接:[Mysql中联合索引的最左匹配原则](https://www.cnblogs.com/wangkaihua/p/10220462.html) 218 | 219 | **15、MySQL中in和exists的区别。** 220 | 221 | in和exists主要用在子查询: 222 | 223 | ```sql 224 | select * from A where id in (select id from B); 225 | select * from A where exists (select 1 from B where A.id=B.id); 226 | ``` 227 | 228 | 两者区别在于: 229 | 230 | - exists是对外表做loop循环,每次loop循环再对内表(子查询)进行查询,那么因为对内表的查询使用的索引(内表效率高,故可用大表),而外表有多大都需要遍历,不可避免(尽量用小表),故内表大的使用exists,可加快效率。 231 | - in是把外表和内表做hash连接,先查询内表,再把内表结果与外表匹配,对外表使用索引(外表效率高,可用大表),而内表多大都需要查询,不可避免,故外表大的使用in,可加快效率。 232 | 233 | 外层查询表小于子查询表,则用exists,外层查询表大于子查询表,则用in。 234 | 235 | **16、数据库自增主键可能的问题。** 236 | 237 | 自增长是一个很常见的数据属性,在MySQL中我很喜欢让一个自增长属性的字段(比如ID)当作一个主键。特别是InnoDB,因为InnoDB的聚集索引特性,使用自增长属性的字段当主键性能更好。但是也存在一些问题。 238 | 239 | 首先是MyISAM引擎下,由于该引擎是表锁设计,所以自增长不用考虑并发插入的问题。 240 | 241 | 然后来到较为复杂的InnoDB引擎情况下。在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器。当对含有自增长的计数器的表进行插入操作时,这个计数器会被初始化,执行语句来得到计数器的值。 242 | 243 | ```sql 244 | SELECT MAX(auto_inc_col) FROM t FOR UPDATE 245 | ``` 246 | 247 | 插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称作AUTO-INC Locking,这种锁其实是一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成自增长值插入的SQL语句后立即释放。 248 | 249 | 虽然AUTO-INC Locking从一定程度上提高了并发插入的效率。但还是存在一些性能上的问题。首先,插入性能较差,事务必须等待前一个插入的完成。其次,对于INSERT...SELECT的大数据量的插入会影响插入的性能,因为另一个事务的插入会被阻塞。 250 | 251 | 从MySQL 5.1.22版本开始,InnoDB引擎提供了一种轻量级互斥量的自增长实现机制,提高了自增长插入的性能。 252 | 253 | 另外,在InnoDB引擎中,自增长值的列必须是索引,同时必须是索引的第一列,如果不是会抛出异常,而MyISAM引擎没有这个问题。 254 | 255 | 参考《MySQL技术内幕-InnoDB存储引擎》的6.3.4小节-自增长与锁 256 | 257 | 另外,在多机数据库设计中,自增长主键ID会有重复现象,这也导致了系统设计时单点数据库不能拆库,因为ID会重复。 258 | 259 | **17、MVCC的含义,如何实现的。** 260 | 261 | MVCC(Multi Version Concurrency Control的简称),代表多版本并发控制。与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。 262 | 263 | MVCC,是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读。 264 | 265 | MVCC最大的优势:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。 266 | 267 | MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。 268 | 269 | 参考链接:[MVCC的含义,如何实现的?](https://blog.csdn.net/riemann_/article/details/94838870) 270 | 271 | **18、你做过的项目里遇到分库分表了吗?怎么做的,有用到中间件么,比如sharding、jdbc等,他们的原理知道吗。** 272 | 273 | 没有遇到过。 274 | 275 | **19、MySQL的主从延迟怎么解决。** 276 | 277 | (没用过主从数据库,这个问题可以不回答了^-^) 278 | 279 | 主从数据库复制存在的问题: 280 | 281 | - 主库宕机后,数据可能丢失; 282 | - 主从同步延迟。 283 | 284 | 主从延迟解决方法: 285 | 286 | - 优化网络 287 | - 升级Slave硬件配置 288 | - Slave调整参数,关闭binlog,修改innodb_flush_log_at_trx_commit参数值 289 | - 升级MySQL版本到5.7,使用并行复制 290 | 291 | **20、什么是回表,覆盖索引有什么作用。** 292 | 293 | 回表是指,数据库根据索引找到了指定的记录所在行后,还需要根据rowid再次到数据块里取数据的操作。在执行计划中,先索引扫描,再通过rowid去取索引中未能提供的数据,即为回表。 294 | 295 | 避免回表的方法就是将需要的字段放在索引中去,查询的时候避免回表。也就是覆盖索引。 296 | 297 | 覆盖索引:一个索引内包含(或覆盖)所有需要查询的字段的值,即只需扫描索引而无须回表,减少了IO操作,提高了效率。 298 | 299 | **21、B+Tree索引和Hash索引区别?** 300 | 301 | 在InnoDB存储引擎使用哈希算法对字典进行查找,其冲突机制采用链表方式(与JDK1.7的HashMap一样)。 302 | 303 | - 哈希索引是自适应索引,InnoDB存储引擎会根据表的使用情况自动为表生成哈希索引,是无法人为干预的。 304 | - 哈希索引适合等值查询,但是无法进行范围查询。 305 | - 哈希索引没办法利用索引完成排序。 306 | - 哈希索引不支持多列联合索引的最左匹配规则。 307 | - 如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题 308 | 309 | **22、在Mybatis中,占位符$和#的区别(防止SQL注入)。** 310 | 311 | 先举例来说明,看一个很简单的MySQL的Mapper。 312 | 313 | ```sql 314 | 319 | ``` 320 | 321 | 这里,parameterType表示了输入的参数类型,resultType表示了输出的参数类型。回应上文,如果我们想防止SQL注入,理所当然地要在输入参数上下功夫。上面代码中WHERE id=#{id}即输入参数在SQL中拼接的部分,传入参数后,打印出执行的SQL语句,会看到SQL是这样的: 322 | 323 | ```sql 324 | SELECT id,title,author,content FROM blog WHERE id = ? 325 | ``` 326 | 327 | 不管输入什么参数,打印出的SQL都是这样的。这是因为MyBatis启用了预编译功能,在SQL执行前,会先将上面的SQL发送给数据库进行编译;执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题。 328 | 329 | 【底层实现原理】MyBatis是如何做到SQL预编译的呢?其实在框架底层,是JDBC中的PreparedStatement类在起作用,PreparedStatement是我们很熟悉的Statement的子类,它的对象包含了编译好的SQL语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译。 330 | 331 | 话说回来,是否我们使用MyBatis就一定可以防止SQL注入呢?当然不是,请看下面的代码: 332 | 333 | ```sql 334 | 339 | ``` 340 | 341 | 仔细观察,内联参数的格式由“#{xxx}”变为了“${xxx}”。如果我们给参数“id”赋值为“3”,将SQL打印出来是这样的: 342 | 343 | ```sql 344 | SELECT id,title,author,content FROM blog WHERE id = 3 345 | ``` 346 | 347 | 显然,这样是无法阻止SQL注入的。在MyBatis中,“${xxx}”这样格式的参数会直接参与SQL编译,从而不能避免注入攻击。但涉及到动态表名和列名时,只能使用“${xxx}”这样的参数格式。所以,这样的参数需要我们在代码中手工进行处理来防止注入。比如 348 | 349 | ```sql 350 | 355 | ``` 356 | 357 | 如果我们给参数“orderParam”赋值为“id”,将SQL打印出来是这样的: 358 | 359 | ```sql 360 | SELECT id,title,author,content FROM blog ORDER BY id 361 | ``` 362 | 363 | 在编写MyBatis的映射语句时,尽量采用“#{xxx}”这样的格式。若不得不使用“${xxx}”这样的参数,要手工地做好过滤工作,来防止SQL注入攻击。 364 | 365 | - #{}:相当于JDBC中的PreparedStatement。 366 | 367 | - ${}:是输出变量的值。 368 | 369 | 简单说,#{}是经过预编译的,是安全的;${}是未经过预编译的,仅仅是取变量的值,是非安全的,存在SQL注入。 370 | 371 | 如果我们order by语句后用了${},那么不做任何处理的时候是存在SQL注入危险的,必须要事先手动处理过滤一下输入的内容。 372 | 373 | 参考文章:[Mybatis如何防止SQL注入](https://www.cnblogs.com/200911/p/5869097.html) 374 | 375 | **23、SQL的注入攻击是什么,如何防范。** 376 | 377 | SQL注入攻击,就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。 378 | 379 | 解决SQL注入问题的关键是对所有可能来自用户输入的数据进行严格的检查,对数据库配置使用最小权限原则。有几种防范方法: 380 | 381 | - 在JDBC中,使用PreparedStatement来拼接动态字符串,可以保证输入的数据被视作SQL中纯粹的字符串,而不会当作SQL语法来解释。比如。 382 | 383 | ```java 384 | String url="jdbcUrl"; 385 | String userName="userName"; 386 | String passWord="passWord"; 387 | Connection conn= DriverManager.getConnection(url); 388 | PreparedStatement stmt=conn.prepareStatement("SELECT * FROM user_table WHERE username=? AND password=?"); 389 | stmt.setString(1,userName); 390 | stmt.setString(2,passWord); 391 | ``` 392 | 393 | - 对进入数据库的特殊字符(’”\尖括号&*;等)进行转义处理,或编码转换。或者直接禁止用户向参数中写入特殊字符。 394 | - 网站每个数据库的编码统一,建议全部使用utf-8编码,上下层编码不一致有可能导致一些过滤模型被绕过。 395 | - 严格限制网站用户对数据库的操作权限,给此用户提供仅仅能够满足其工作的权限,从而最大限度地减少注入攻击对数据库的损坏。 396 | 397 | **24、什么时候添加B+树索引。** 398 | 399 | 如果每个字段的取值范围很广,几乎没有重复,即属于高选择性,则此时使用B+树索引最合适。 400 | 401 | 怎么查看索引是否是高选择性的呢?可以通过SHOW INDEX结果中的列Cardinality(索引基数)来观察。Cardinality值非常关键,表示索引中不重复记录数量的预估值。同时需要注意的是,Cardinality是一个预估值,而不是一个准确值,基本上用户也不可能得到一个准确的值。在实际应用中,Cardinality/n_rows_in_table应尽可能地接近1。如果非常小,那么用户需要考虑是否还有必要创建这个索引。故在访问高选择性属性地字段并从表中去除很少一部分数据时,对这个字段添加B+树索引是非常有必要的。 402 | 403 | 另外,在InnoDB存储引擎中,Cardinality统计信息的更新发生在两个操作中:INSERT和UPDATE。当然,并不是每次发生INSERT和UPDATE时就去更新Cardinality信息。 404 | 405 | **25、MySQL的嵌套事务。** 406 | 407 | (这道题主要为了纪念当时华为的面试题。。。) 408 | 409 | 在InnoDB引擎下,MySQL是支持嵌套事务的。嵌套事务是一个层次结构框架,由一个顶层事务控制各个层次的事务。顶级事务之下嵌套的事务被称为子事务,其控制着每一个局部的变换。需要注意的是: 410 | 411 | - 子事务既可以提交也可以回滚,但它的提交操作并不马上生效,除非其父事务已经提交。因此可以推断出,任务子事务都在顶层事务提交后才真正的提交。 412 | - 任何一个父事务的回滚会引起它所有的子事务一同回滚,故子事务仅保留A、C、I特征,不具有D特征(一致性)。 413 | 414 | 具体看《MySQL技术内幕-InnoDB存储引擎》7.1.2小节中的嵌套事务部分。 415 | 416 | **26、给出一个学生成绩studuent表,写一个SQL语句,统计每个学生所有成绩平均分大于80分的结果。** 417 | 418 | 这个问题主要考察GROUP BY和HAVING语句的联合使用,答案如下。 419 | 420 | ```sql 421 | SELECT id, COUNT(course) as numcourse, AVG(score) as avgscore 422 | FROM student 423 | GROUP BY id 424 | HAVING AVG(score)>=80; 425 | ``` 426 | 427 | 在select语句中可以使用groupby子句将行划分成较小的组,然后,使用聚组函数返回每一个组的汇总信息,另外,可以使用having子句限制返回的结果集。HAVING语句的存在弥补了WHERE关键字不能与聚合函数联合使用的不足。 428 | 429 | HAVING 子句对 GROUP BY 子句设置条件的方式与 WHERE 和 SELECT 的交互方式类似。WHERE 搜索条件在进行分组操作之前应用;而 HAVING 搜索条件在进行分组操作之后应用。 430 | 431 | HAVING 语法与 WHERE 语法类似,但 HAVING 可以包含聚合函数。HAVING 子句可以引用选择列表中显示的任意项。 432 | 433 | **27、MySQL中一条SQL语句的执行过程。** 434 | 435 | SQL是一套标准,是用来完成和数据库之间的通信的编程语言,SQL语言是脚本语言,直接运行在数据库上。同时,SQL语句与数据在数据库上的存储方式无关,只是不同的数据库对于同一条SQL语句的底层实现不同罢了,但结果相同。SQL语句如下,序号则为实际执行顺序: 436 | 437 | ```sql 438 | (7) SELECT 439 | (8) DISTINCT 440 | (1) FROM 441 | (3) JOIN 442 | (2) ON 443 | (4) WHERE 444 | (5) GROUP BY 445 | (6) HAVING 446 | (9) ORDER BY 447 | (10) LIMIT 448 | ``` 449 | 450 | 建议直接去看这篇文章:[SQL查询之执行顺序解析](http://zouzls.github.io/2017/03/23/SQL%E6%9F%A5%E8%AF%A2%E4%B9%8B%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F%E8%A7%A3%E6%9E%90/) 451 | 452 | **28、MySQL中int(11)中的11代表什么含义。** 453 | 454 | nt(11) 中的 11 ,不影响字段存储的范围,只影响展示效果。当数字不足11位时,前面会用0补齐。 455 | 456 | mysql中int长度并不影响数据的存储精度,长度只与显示有关。无论是int(3)还是int(9),存储的都是4字节无符号整数,也就是0~2^32。 457 | 458 | **29、InnoDB 中为什么采用B+树结构,而不是平衡树。** 459 | 460 | 数据库文件很大,不可能全部存储在内存中,故要存储到磁盘上。索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。索引采用B/+Tree而不是二叉查找树,关键因素就是磁盘I/O次数。这是一种**多路搜索树**,而不是简单的二叉树。 461 | 462 | **BTree** 是为磁盘等外存储设备设计的一种平衡查找树。因此在讲 B-Tree 之前先了解下磁盘的相关知识。 463 | 464 | - 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。 465 | - InnoDB 存储引擎中默认每个页的大小为 16 KB,可通过参数 innodb_page_size 将页的大小设置为 4K、8K、16K ,在 MySQL 中可通过如下命令查看页的大小: 466 | 467 | ```SQL 468 | mysql> show variables like 'innodb_page_size'; 469 | ``` 470 | 471 | - 而系统一个磁盘块的存储空间往往没有这么大,因此 InnoDB 每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小 16KB 。InnoDB 在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率。 472 | 473 | BTree 结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述BTree,首先定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key值互不相同。BTree结构如下图所示。 474 | 475 | ![Y4yrpF.png](https://s1.ax1x.com/2020/05/19/Y4yrpF.png) 476 | 477 | **B+Tree** 是在 B-Tree 基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用 B+Tree 实现其索引结构。从刚刚的B-Tree 结构图中可以看到,每个节点中不仅包含数据的 key 值,还有 data 值。而每一个页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率。在 B+Tree 中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储 key 值信息,这样可以大大加大每个节点存储的 key 值数量,降低 B+Tree 的高度。 478 | 479 | B+Tree 相对于 BTree 有几点不同: 480 | 481 | - 非叶子节点只存储键值信息。 482 | - 所有叶子节点之间都有一个链指针。 483 | - 数据记录都存放在叶子节点中。 484 | 485 | 将 B-Tree 优化,由于 B+Tree 的非叶子节点只存储键值信息,假设每个磁盘块能存储 4 个键值及指针信息,则变成 B+Tree 后其结构如下图所示: 486 | 487 | ![Y4yhtK.png](https://s1.ax1x.com/2020/05/19/Y4yhtK.png) 488 | 489 | **总结一波:** 因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低。 490 | 491 | 参考文章:[为什么数据库选B-tree或B+tree而不是二叉树作为索引结构](https://blog.csdn.net/sinat_27602945/article/details/80118362) 492 | 493 | **30、MySQL索引的“创建”原则。** 494 | 495 | 可以结合24题一起来看。主要在以下几种条件下,推荐创建索引。 496 | 497 | - 最适合索引的列是出现在 WHERE 子句中的列,或连接子句中的列,而不是出现在 SELECT 关键字后的列。 498 | - 索引列的基数Cardinality越大,索引效果越好。 499 | - 根据情况创建复合索引,复合索引可以提高查询效率。因为复合索引的基数会更大。 500 | - 避免创建过多的索引,索引会额外占用磁盘空间,降低写操作效率。 501 | - 主键尽可能选择较短的数据类型,可以有效减少索引的磁盘占用提高查询效率。 502 | - 对字符串进行索引,应该定制一个前缀长度,可以节省大量的索引空间。 503 | 504 | **31、为什么官方建议使用自增长主键作为索引。** 505 | 506 | 结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。 507 | 508 | **32、MySQL主从复制的作用和原理。** 509 | 510 | 主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库。主数据库一般是准实时的业务数据库。 511 | 512 | 主从复制的好处是从数据库可以作为数据的热备份,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。还可以支持读写分离,在主数据库上进行写入工作,在从数据库执行查询工作,支持更大的并发性能。 513 | 514 | 先介绍两个**重要概念**。 515 | 516 | - 主库bin-log:二进制日志,记录主库发生的修改事件。 517 | - 从库relay-log:中继日志,存储所有主库TP过来的bin-log事件。 518 | 519 | 主从复制库的原理: 520 | 521 | - 数据库有一个bin-log的二进制文件,记录了所有的SQL语句。 522 | - 目标就是把主数据库的bin-log文件的SQL语句复制过来。 523 | - 让其在从数据库的relay-log重做日志文件中再执行一次这些SQL语句即可。 524 | 525 | 具体操作过程需要三个线程。 526 | 527 | - bin-log输出线程:每当从库连接到主库的时候,主库都会创建一个线程,然后发送bin-log内容到从库。在从库里,当复制开始的时候,从库就会创建两个线程进行处理。 528 | - 从库I/O线程:当START SLAVE语句在从库开始执行之后,从库创建一个I/O线程,该线程连接到主库并请求主库发送到bin-log里面的更新记录到从库中,从库I/O线程读取主库的bin-log输出线程发送的更新并拷贝这些更新到本地文件,其中包括relay-log文件(中继日志)。 529 | - 从库的SQL线程:从库创建一个SQL线程,这个线程读取从库I/O线程写到relay log的更新事件并执行。 530 | 531 | 从上可知,对于每一个主从复制的连接,都有三个线程。拥有多个从库的主库为每一个连接到主库的从库创建一个bin-log输出线程,每一个从库都有它自己的I/O线程和SQL线程。 532 | 533 | ![Y4yz9S.png](https://s1.ax1x.com/2020/05/19/Y4yz9S.png) 534 | 535 | **33、MySQL事务日志。** 536 | 537 | innodb事务日志包括redo log和undo log。redo log是重做日志,提供前滚操作;undo log是回滚日志,提供回滚操作。 538 | 539 | - redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。又叫做重做日志文件,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。如数据库掉电后,InnoDB存储引擎会使用redo log来恢复到掉电前的时刻,以此来保证数据的完整性。 540 | - undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。 541 | 542 | **redo log :** 543 | 544 | 事务中所有操作会先写到redo log中,然后再同步到数据库文件中。所以数据库文件进行事务操作修改时,redo log肯定已经记录了所有事务操作,此时即使数据库挂掉,事务操作也都已经持久化到redo log中了,数据库恢复后可以继续执行剩下操作。它保证了事务的**一致性**。 545 | 546 | **undo log :** 547 | 548 | undo log有两个作用: 549 | 550 | - 提供回滚 551 | - 多个行版本控制(MVCC) 552 | 553 | undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。 554 | 555 | undo log是采用**段(segment)的方式**来记录的,每个undo操作在记录的时候占用一个undo log segment。 556 | 557 | 它保证了事务的**原子性**。 558 | 559 | **34、JOIN的用途。** 560 | 561 | JOIN 按照功能大致分为如下三类: 562 | 563 | - INNER JOIN(内连接,或等值连接):获取两个表中字段匹配关系的记录。 564 | - LEFT JOIN(左连接):获取左表所有记录,即使右表没有对应匹配的记录。 565 | - RIGHT JOIN(右连接): 与 LEFT JOIN 相反,用于获取右表所有记录,即使左表没有对应匹配的记录。 566 | 567 | **35、PreparedStatement和Statement的区别。** 568 | 569 | - PreparedStatement接口继承于Statement,PreparedStatement是**预编译**的,实例中包含的是已编译好的SQL语句,执行速度要快于Statement对象。以后每当执行同一个PreparedStatement对象时,预编译的命令是可以重复使用的。 570 | - PreparedStatement可以**防止SQL注入式攻击**,在使用参数化查询的情况下,数据库系统不会将参数的内容视为SQL指令的一部分来处理,而是在数据库完成SQL指令的编译后,才套用参数运行,因此就算参数中含有破坏性的指令,也不会被数据库所运行。 571 | 572 | **36、数据库第一、第二、第三范式的理解。** 573 | 574 | **第一范式,是指没有重复的列,** 表示数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式就是无重复的列。 575 | 576 | **第二范式,是指属性完全依赖主键,** 要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。例如员工信息表中加上了员工编号(emp_id)列,因为每个员工的员工编号是惟一的,因此每个员工可以被惟一区分。这个惟一属性列被称为主关键字或主键、主码。 577 | 578 | **第三范式,是要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。** 例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在的员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。 也就是说, 如果存在非主属性对于码的传递函数依赖,则不符合3NF的要求。 579 | 580 | **37、MySQL半同步复制原理。** 581 | 582 | MySQL主从复制分为**异步、同步和半同步复制**,区别主要如下: 583 | 584 | - 异步复制(Asynchronous replication),MySQL默认的复制是异步的,主库在执行完客户端提交的事务后会立即将结果返给给客户端,并不关心从库是否已经接收并处理。原理最简单,性能最好,但是主从之间数据不一致的概率很大。 585 | - 全同步复制(Fully synchronous replication),指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。 586 | - 半同步复制(Semisynchronous replication),介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到relay log中才返回给客户端。相对于异步复制,半同步复制牺牲了一定的性能,提高了数据的安全性。 587 | 588 | **半同步复制原理:** 589 | 590 | 默认情况下,MySQL的主从复制是异步的,异步复制可以提供最佳的性能, 主库把binlog日志发送给从库,然后将结果返回给客户端,并不会验证从库是否接收完毕。这也就意味着有可能出现当主库或从库发生故障的时候,从库没有接收到主库发送过来的binlog日志,导致主库和从库的数据不一致,甚至在恢复时造成数据的丢失。为了解决上述出现的问题,MySQL 5.5 引入了一种半同步复制模式。该模式可以确保从库接收完主库发送的binlog日志文件并写入到自己的中继日志relay log里,然后会给主库一个反馈,告诉主库已经接收完毕,这时主库才返回结果给客户端告知操作完成。当出现从库响应超时情况时,主库会暂时切换到异步复制模式,直到下一次同步没有超时转为半同步复制为止。(master的dump线程除了发送binlog数据到slave,还承担了接收slave的ack工作。如果出现异常,没有收到ack,那么将自动降为普通的异步复制,直到异常修复) 591 | 592 | **38、MySQL中Distinct与Group by的区别。** 593 | 594 | distinct简单来说就是用来去重的,而group by的设计目的则是用来聚合统计的,两者在能够实现的功能上有些相同之处。 595 | 596 | 单纯的去重操作使用distinct,速度是快于group by的。 597 | 598 | 弥有,2019年9月 599 | [EOF] 600 | -------------------------------------------------------------------------------- /interviews/2020届秋招面试题总结-Redis篇.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——Redis篇 2 | 3 | **1、Redis的优势。** 4 | 5 | - 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)。 6 | - 支持丰富的数据类型,支持string,list,set,zset和hash。 7 | - 支持事务性。操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行。(这块需要注意与之前MySQL不同) 8 | - 丰富的特性,可用于缓存,消息队列,按key设置过期时间,过期后将自动删除。 9 | 10 | **2、Redis的数据结构都有哪些。** 11 | 12 | Redis支持五种Value Type,其底层实现的编码数据结构有8种: 13 | 14 | - SDS - simple synamic string - 支持自动动态扩容的字节数组 15 | - list - 平平无奇的链表 16 | - dict - 使用双哈希表实现的, 支持平滑扩容的字典 17 | - zskiplist - 附加了后向指针的跳跃表 18 | - intset - 用于存储整数数值集合的自有结构 19 | - ziplist - 一种实现上类似于TLV, 但比TLV复杂的, 用于存储任意数据的有序序列的数据结构 20 | - quicklist - 一种以ziplist作为结点的双链表结构 21 | - zipmap - 一种用于在小规模场合使用的轻量级字典结构 22 | 23 | 衔接"底层数据结构"与"Value Type"的桥梁的, 则是Redis实现的另外一种数据结构: redisObject.。 24 | 25 | Redis中的Key与Value在表层都是一个redisObject实例, 故该结构有所谓的"类型", 即是ValueType. 对于每一种Value Type类型的redisObject, 其底层至少支持**两种**不同的底层数据结构来实现. 以应对在不同的应用场景中, Redis的运行效率, 或内存占用. 26 | 27 | 对于具体数据结构介绍,建议看《Redis设计与实现》第一部分,或者看这篇文章,[面试官:你看过Redis数据结构底层实现吗?](https://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247488733&idx=1&sn=c74645ca78024fdc8ddfa3265d527386&chksm=ebd62bf1dca1a2e7f76b8cc37a6518c744adc6f226727838ef2f8068bbecef0ae4577f7d4217&scene=21) 28 | 29 | **3、Redis的使用要注意什么。** 30 | 31 | 现阶段主要从数据存储和数据获取两个方面来说明开发时的注意事项: 32 | 33 | 数据存储:因为内存空间的局限性,注定了能存储的数据量有限,如何在有限的空间内存储跟多的数据信息是我们应该关注的。Redis内存存储的都是键值对,那么如何减少键值对所占据的内存空间就是空间优化的本质。比如可以在清晰表达业务含义的基础上尽可能缩减key的字符长度,也可以当value是图片、大文本等大数据信息时,借助压缩工具压缩之后再存入Redis中。 34 | 35 | 数据查询:Redis是一种数据库,和其他数据库一样,操作时也需要有连接对象,连接对象的创建和销毁也需要耗费资源,复用连接对象很有必要,所以推荐使用连接池来进行管理(比如在自己项目中用JedisPool来获取Redis连接)。此外,对于连续多次的查询,可以使用mget(针对string类型查询)/hmget(针对Hash类型查询),将多次请求合并为一次,提高响应速度。 36 | 37 | **4、Redis的事务性。** 38 | 39 | Redis通过MULTI、EXEC、WATCH等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕后,然后才去处理其他客户端的命令请求。 40 | 41 | 事务的生命周期为: 42 | 43 | - 事务开始:使用MULTI开启一个事务(自己项目中,采用jedis.multi()来返回一个事务Transaction,后续可以在此事务上进行操作) 44 | - 命令入队:在开启事务的时候,每次操作的命令将会被插入到一个队列中,同时这个命令并不会被真的执行。 45 | - 事务执行:EXEC命令进行提交事务 46 | 47 | Redis事务具有的性质: 48 | 49 | - 单独的隔离操作:事务中所有命令都会被序列化、按顺序执行,在执行过程中不会被其他客户端发送来的命令打断。 50 | - 没有隔离级别的概念:队列中的命令在事务没有被提交之前不会被实际执行。 51 | 52 | 在Redis中,事务总是具有原子性、一致性和隔离性。当Redis运行在某种特定的持久化模式(开启AOF和RDB服务)下时,事务也具有持久性。 53 | 54 | 着重讲一下原子性。 55 | 56 | 对于Redis的事务功能来说,事务队列中的命令要么就全部执行,要不就一个都不执行。从这点来说,事务具有原子性。但这个执行失败的条件是指**命令入队出错**(比如命令不存在,格式不正确等情况)而被服务器拒绝执行,而不是命令实际执行时出错。 57 | 58 | Redis的事务与传统的关系式数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。 59 | 一定注意,Redis的事务性与常见的关系式数据库有些不同(尤其原子性),建议直接去看《Redis设计与实现》的19.3小节-事务的ACID性质,网上各种博客说的参差不齐。 60 | 61 | **5、当前Redis** **cluster** **集群有哪些方式,各自优缺点,场景。** 62 | 63 | Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。 64 | 65 | Redis集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现:一个Redis集群包含16384个哈希槽(hash slot),数据库中的每个键都属于这个16384个哈希槽的其中一个,集群使用公式CRC16(key) % 16384来计算键key属于哪个槽,其中CRC16(key)语句用于计算键key的CRC16校验和。 66 | 67 | - 数据共享:Redis提供多个节点实例间的数据共享,也就是Redis A、B、C、D彼此之间的数据是同步的,同样彼此之间也可以通信,而对于客户端操作的keys是由Redis系统自行分配到各个节点中。 68 | - 主从复制:Redis的多个实例间通信时,一旦其中一个节点故障,那么Redis集群就不能继续正常工作了,所以需要一种复制机制(Master-Slave)机制,做到一旦节点A故障了,那么其从节点A1和A2就可以接管并继续提供与A同样的工作服务。 69 | - 哈希槽值:Redis集群中使用哈希槽来存储客户端的keys,而在Redis中,目前存有16384(2的14次方)个哈希槽,它们被全部分配给所有的节点。 70 | 71 | 参考链接:[Redis集群使用总结(一)](https://www.cnblogs.com/RENQIWEI1995/p/8931678.html) 72 | 73 | **6、Memcache的原理,哪些数据适合放在缓存中。** 74 | 75 | Memcache采用键值对存储方式。它本质是一个大的 hash表,key的最大长度为255个字符,最长过期时间为30天。它的内存模型如下:Memcache预先将可支配的内存空间进行分区(Slab),每个分区里再分为多个块(Chunk)最大1M,但同一个分区中块的大 小是固定的。然后,插入数据时,会根据数据大小寻找最合适的块,然后插入,当然这样也就会有部分内存浪费,但可一定程度上减少内存碎片,总体上,利大于弊。 76 | 77 | 应用场景主要是分布式应用,数据库前段缓存和服务期间数据共享等。 78 | 79 | **7、Redis相比memcached有哪些优势?两者的主要区别?** 80 | 81 | - memcached所有的值均是简单的字符串,Redis作为其替代者,支持更为丰富的数据类型 82 | - Redis的速度比memcached快很多 83 | - Redis可以持久化其数据 84 | - Redis支持数据的备份,即master-slave模式的数据备份。 85 | 86 | **8、Redis的并发竞争问题如何解决,了解Redis事务的CAS操作吗?** 87 | 88 | Redis的并发竞争问题主要是在并发写竞争上。体现在多客户端同时并发写一个key,修改值之后再写回去,只要顺序错了,数据就错了。 89 | 90 | 为了避免这个问题,我们可以对客户端读写Redis操作采用内部锁synchronized。但解决这个问题最好的方案是Redis自己提供的CAS类的乐观锁方案。 91 | 92 | redis具有高级事务CAS(乐观锁),可以被用作分布式锁。毕竟JVM提供的synchronized或者ReentrantLock不能应用于分布式环境下。 93 | 94 | watch指令在redis事物中提供了CAS的行为。为了检测被watch的keys在是否有多个clients同时改变引起冲突,这些keys将会被监控。如果至少有一个被监控的key在执行exec命令前被其他客户端修改,整个事务将会回滚,不执行任何动作,从而保证原子性操作,并且执行exec会得到null的回复。 95 | 96 | 具体工作机制:watch 命令会监视给定的每一个key,当exec时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。 97 | 98 | 参考连接:[redis的高级事务CAS(乐观锁)](https://www.cnblogs.com/martinzhang/p/3415204.html) 和 [Redis实现CAS的乐观锁](https://www.jianshu.com/p/d777eb9f27df) 99 | 100 | **9、Redis适合于哪些场景。** 101 | 102 | - Session共享(单点登陆) 103 | - 页面缓存 104 | - 队列(比如项目中用到的异步队列) 105 | - 排行榜/计数器 106 | - 发布/订阅(实现消息流) 107 | 108 | **10、Redis持久化的机制,AOF和RDB的区别。** 109 | 110 | Redis提供两种方式进行持久化,一种是RDB持久化(会按照配置的指定时间将内存中的数据快照到磁盘中,创建一个dump.rdb文件,Redis启动时再恢复到内存中),另一种是AOF持久化(以日志的形式记录每个写操作(读操作不记录),只需追加文件但不可以改写文件,Redis启动时会根据日志从头到尾全部执行一遍以完成数据的恢复工作)。 111 | 112 | 两者的区别在于: 113 | 114 | - RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。 115 | - AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。 116 | 117 | 需要注意的是,默认情况下,Redis选用的是快照RDB的持久化方式,将内存中的数据以快照的方式写入二进制文件中,默认的文件名为dump.rdb。 118 | 119 | RDB方式不能完全保证数据持久化,因为是定时保存,所以当redis服务down掉,就会丢失一部分数据,而且数据量大,写操作多的情况下,会引起大量的磁盘IO操作,会影响性能。所以,当RDB和AOF方式都开启的情况下,服务器会优先使用AOF文件来还原数据库状态,当然,AOF恢复数据速度要慢一些。 120 | 121 | 还一点需要注意,服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。 122 | 123 | **11、Redis对象的内存回收。** 124 | 125 | Redis在自己的对象系统中构建了一个引用计数技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。 126 | 127 | 每个对象的引用计数信息是由redisObject结构的refcount属性记录。与jvm中的应用计数法很相似,不在赘述。 128 | 129 | Redis对象在整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。 130 | 131 | **12、知道哪些Redis的优化操作。** 132 | 133 | - 使用简短的key。 134 | - 大的数据压缩后再存入value。 135 | - 设置key有效期。 136 | - 选择回收策略。当 Redis 的实例空间被填满了之后,将会尝试回收一部分key。在Redis中,允许用户设置最大使用内存大小server.maxmemory,当Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略,有很多不同的回收策略。 137 | - 在服务器端使用Lua脚本。(没看过Lua就算了,比如我自己) 138 | 139 | **13、Redis的主从复制机制原理。** 140 | 141 | 主从的意义: 142 | 143 | - redis要达到高可用、高并发,只有单个redis是不够的,单个redis也就只能支持几万的QPS,所以必须以集群的形式提供服务,而集群中又以多个主从组成。 144 | - 主从是以多个redis集合在一起,以一个master多个slave为模式对外提供服务,master主要以写为主,slave提供读,即是读写分离的情况,以读多写少为准,如果写比较多的情况一般就以异步的形式提供服务。 145 | 146 | 主从复制功能分为两个阶段: 147 | 148 | - 同步操作:将从服务器的数据库状态更新至主服务器当前所处的数据库状态。 149 | - 命令传播:用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。 150 | 151 | Redis2.8版本之前同步操作采用SYNC命令,只有全量同步,效率比较低。2.8版本之后使用PSYNC命令代替SYNC命令来执行复制时的同步操作,自行判断是全量同步还是增量同步(通过复制偏移量、复制积压缓冲区(其实就是一个FIFO的队列)和服务器运行ID三个部分来实现),效率较高。 152 | 153 | 在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:REPLCONF ACK ; 其中 replication_offset是从服务器当前的复制偏移量。 154 | 155 | 心跳检测对主从服务器有三个作用: 156 | 157 | - 检测主从服务器的网络连接状态。 158 | - 辅助实现min-slaves选项。 159 | - 检测命令丢失,通过对比主从服务器的复制偏移量。 160 | 161 | 参考来源:《Redis设计与实现》的15章-复制 162 | 163 | **14、Redis的线程模型是什么。** 164 | 165 | Redis基于Rector模型开发了自己的文件事件处理器(file event handler): 166 | 167 | - 文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 168 | - 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。 169 | 170 | 虽然文件事件处理器以单线程方式运行,但通过与I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。 171 | 172 | **15、Redis中set和zset的区别。** 173 | 174 | set是Redis下的无序集合对象,是通过intset或者hashtable编码实现。 175 | 176 | zset是Redis下的有序集合对象,是通过ziplist或者skiplist编码实现。 177 | 178 | ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点则保存元素的分值。对于skiplist编码实现,它同时内部包含一个字典和跳跃表,程序都可以用O(log(N))的复杂度往集合中添加成员,并可以用O(1)的复杂度查找给定成员的分值。 179 | 180 | **16、分布式使用场景(存储session)。** 181 | 182 | 还没遇到过 183 | 184 | **17、怎么保证缓存和数据库的一致性。** 185 | 186 | 只要用缓存,就可能会涉及到缓存与数据库双存储双写,只要是双写,就一定会有数据一致性的问题。 187 | 188 | 一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。 189 | 190 | 串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上请求。 191 | 192 | **最经典的缓存+数据库读写的模式,就是Cache Aside Pattern。** 193 | 194 | - 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后返回缓存,同时返回响应。 195 | - 更新的时候,先更新数据库,然后再删除缓存。 196 | 197 | 为什么是删除缓存,而不是更新缓存呢。原因有二,一是在复杂缓存场景中,缓存不单单是数据库中直接取出来的值;二是更新缓存的代价是很高的。举个例子,一个缓存涉及的表的字段,在 1 分钟内 就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低,用到缓存才去算缓存。 198 | 199 | 其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。 200 | 201 | 在这个读写模式下,也会出现数据不一致问题。 202 | 203 | **最初级的数据不一致问题分析:** 204 | 205 | 问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。 206 | 207 | 解决方案:先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。 208 | 209 | **比较复杂的数据不一致问题分析:** 210 | 211 | 问题:数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。在这个场景下,数据库和缓存中的数据不一样了。 212 | 213 | 针对这个方法有一种解决方案,叫做**延时双删策略**。伪代码如下: 214 | 215 | ```java 216 | public void write(String key,Object data){ 217 | redis.delKey(key); 218 | db.updateData(data); 219 | Thread.sleep(1000); 220 | redis.delKey(key); 221 | } 222 | ``` 223 | 224 | 转换为中文描述为: 225 | 226 | - 先淘汰缓存。 227 | - 再写数据库(这两步与原来一样)。 228 | - 休眠一秒,再次淘汰缓存。这样做可以将1秒内所造成的缓存脏数据再次删除。 229 | 230 | 其他主要的队列解决方案: 231 | 232 | - 更新数据的时候,根据数据的唯一标识,将数据操作请求,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。 233 | - 一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。 234 | - 这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。 235 | 236 | 在这个方法下,需要注意读缓存请求超时问题,每个读请求必须在超时时间内返回。如果超时还未读到更新后缓存的话,则直接从数据库读旧的值。 237 | 238 | 另外,保持读写一致性还可以用其他方法来做。比如**MySQL读写分离,或者Redis分布式锁**。 239 | 240 | 参考文章:[一个高频面试题:怎么保证缓存与数据库的双写一致性?](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247487406&idx=1&sn=29001c51362db626b31153d020ea7c09&chksm=fa49701fcd3ef90956edbbda056d1464681c17e57ec8ba1501356e1cc3f8efb25e1b302e7eb9&scene=0&xtrack=1&key=32c60e053085a25ac01d3ce21a0647c93bd6d6f8b103810d2362e2b0199027c468fe8b3573ed8459bf3b563144d7160f5024f17e5fe8ce409f1bbd6b0d47b4302d2ad9f79a26fe6ed22adf2bc8576ad2&ascene=1&uin=MjQ3MzkwMTc2Mw==&devicetype=Windows+10&version=62060833&lang=zh_CN&pass_ticket=8XpPIyMMLiCzCMbxmqfGQpreQccYQb60+6UwOsqsnbYwpIBCzsx3Ne16bvnrqbXg) 241 | 242 | **18、Redis为什么用skiplist而不用平衡树。** 243 | 244 | Redis里面使用跳跃表skiplist实现sorted set有序集合(实际上还有散列表),而没有采用红黑树、平衡树这种结构。原因主要有以下几点: 245 | 246 | - 跳表使用空间换时间的设计思路,通过构建多级索引来提高查询的效率,实现了基于链表的“二分查找”。 247 | - 跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都是 O(logn)。并且范围查找比红黑树高效。跳表的空间复杂度是 O(n)。不过, 248 | - 跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。虽然跳表的代码实现并不简单,但是作为一种动态数据结构,比起红黑树来说,实现要简单多了。所以很多时候,我们为了代码的简单、易读,比起红黑树,我们更倾向用跳表。 249 | 250 | 参考文章:[跳表:为什么Redis一定要用跳表来实现有序集合?](https://blog.csdn.net/z69183787/article/details/89396748) 251 | 252 | **19、Redis分布式锁的实现方式。** 253 | 254 | Redis分布式锁实现方式有以下几种。 255 | 256 | **第一种,使用redis的watch命令进行实现。** 257 | 258 | watch指令在redis事物中提供了CAS的行为。为了检测被watch的keys在是否有多个clients同时改变引起冲突,这些keys将会被监控。如果至少有一个被监控的key在执行exec命令前被其他客户端修改,整个事务将会回滚,不执行任何动作,从而保证原子性操作,并且执行exec会得到null的回复。 259 | 260 | 具体工作机制:watch 命令会监视给定的每一个key,当exec时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。 261 | 262 | **第二种,使用redis的setnx命令进行实现。** 263 | 264 | 先看一下这个相关的命令。 265 | 266 | ```sql 267 | SETNX key value 268 | ``` 269 | 270 | 如果key不存在,就设置key对应字符串value。在这种情况下,该命令和SET一样。当key已经存在时,就不做任何操作。SETNX是”SET if Not eXists”。 271 | 272 | ```SQL 273 | expire KEY seconds 274 | ``` 275 | 276 | 设置key的过期时间。如果key已过期,将会被自动删除。 277 | 278 | ```SQL 279 | del KEY 280 | ``` 281 | 282 | 删除key 283 | 284 | 由于当某个key不存在的时候,SETNX才会设置该key。且由于Redis采用单进行单线程模型,所以,不需要担心并发问题。那么,就可以利用SETNX的特性维护一个key,存在的时候,即锁被某个线程持有;不存在的时候,没有线程持有锁。 285 | 286 | 并且还可以设置key的过期时间当作锁的超时时间,释放锁就直接可以将key删除即可。 287 | 288 | **20、Redis遇到的问题和缺点。** 289 | 290 | - Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者切换数据库才能恢复。 291 | - Redis主从复制过程中,第一个步骤是同步,需要采用全量复制,复制过程中主机会fork出一个子进程对内存做一份快照,并将子进程的内存快照保存为文件发送给从机,这一过程需要确保主机有足够多的空余内存。若快照文件较大,对集群的服务能力会产生较大的影响。 292 | - Redis作为缓存的话,还会出现缓存和数据库双写一致性的问题。 293 | 294 | **21、Redis各个数据类型的使用场景。** 295 | 296 | Redis支持物种数据类型:string(字符串)、hash(哈希)、list(列表)、set(集合)及zset(有序集合)。 297 | 298 | 使用场景分别为: 299 | 300 | - **String:** 是简单的key-value类型,value其实不仅可以是String,也可以是数字。常规key-value缓存应用;常规计数:微博数,粉丝数等。 301 | - **hash:** 是一个string类型的field和value的映射表,hash特别适合用于存储对象。存储部分变更的信息,比如说用户信息等。 302 | - **list:** 是一个链表。可以轻松实现最新消息排行榜等功能。另外一个应用就是可以实现一个消息队列。可以利用List的PUSH操作,将任务存在List中,然后工作线程再用POP操作将任务取出进行执行。也可以通过zset构建有优先级的队列系统。此外,还可以将redis用作日志收集器,实际上还是一个队列,多个端点将日志信息写入redis,然后一个worker统一将所有日志写到磁盘。 303 | - **set:** 是一个没有重复值得集合。可以存储一些集合性的数据。在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合中。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好等功能。 304 | - **zset:** 相比set,zset增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。比如一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。也可以利用zset设计带有优先级的队列。另外,还可以做排行榜应用,取TOP N操作。 305 | 306 | **22、Redis数据淘汰策略。** 307 | 308 | Redis提供了五种数据淘汰策略: 309 | 310 | - volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的 311 | - keyallkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰 312 | - volatile-random:随机淘汰数据,只淘汰设定了有效期的key 313 | - allkeys-random:随机淘汰数据,所有的key都可以被淘汰 314 | - volatile-ttl:淘汰剩余有效期最短的key 315 | 316 | 此外,如果不设置如上策略的话,还有一种noeviction策略,当内存限制达到,谁也不删除,返回错误。 317 | 318 | **23、Redis哈希槽的概念。** 319 | 320 | Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。 321 | 322 | **24、Redis的缓存雪崩。** 323 | 324 | 缓存雪崩,简单的理解就是:由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间(缓存正常从Redis中获取),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机,造成系统的崩溃。对此,基本的解决思路有: 325 | 326 | - 考虑采用加锁或者队列的方式保证不会同时有大量的线程对数据库一次性进行读写,避免缓存失效时对数据库造成太大的压力,虽然能够一定的程度上缓解了数据库的压力,但是也降低了系统的吞吐量。 327 | - 分析用户的行为,不同的key设置不同的过期时间,尽量让缓存失效的时间均匀分布。 328 | - 做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。 329 | 330 | 具体方法如下: 331 | 332 | - 事发前:实现Redis的高可用(主从架构+Sentinel 或者Redis Cluster),尽量避免Redis挂掉这种情况发生。 333 | - 事发中:万一Redis真的挂了,我们可以设置本地缓存(ehcache)+限流(hystrix),尽量避免我们的数据库被干掉(起码能保证我们的服务还是能正常工作的) 334 | - 事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。 335 | 336 | **25、Redis的缓存穿透。** 337 | 338 | 缓存穿透是指用户查询数据时,在数据库中没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库中查询。查询一个必然不存在的数据。比如文章表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。对此,基本的解决思路有: 339 | 340 | - 如果查询数据库为空,直接设置一个默认值存放到缓存中,这样第二次缓存中获取就有值了,而不会继续访问数据库,这种方法最简单粗暴。 341 | - 根据缓存数据key的规则进行过滤,比如说缓存Key为mac地址。这就要求key必须有以顶的规则,这种方法可以缓解一部分的压力,但是无法根治。 342 | - 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力。对于布隆过滤器,可以用BitSet来构建。 343 | 344 | 具体方法如下: 345 | 346 | 由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就不让这个请求到数据库层! 347 | 348 | 当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。这种情况我们一般会将空对象设置一个较短的过期时间。 349 | 350 | **26、Redis的SDS相比char[]的优点。** 351 | 352 | SDS结构中拥有len(字符串长度)、free(未使用字节的数量)和buf数组(保存字符串)。具有以下几点优势。 353 | 354 | - 常数复杂度获取字符串长度。 355 | - 杜绝缓冲区溢出。 356 | - 减少修改字符串时带来的内存重分配次数,因为有空间预分配和惰性空间释放。 357 | 358 | 弥有,2019年9月 359 | [EOF] 360 | -------------------------------------------------------------------------------- /interviews/2020届秋招面试题总结-Spring篇.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——Spring篇 2 | 3 | **1、讲讲Spring的加载流程。** 4 | 5 | 这个有点长,,,建议大体读一遍spring源码。可以参考这篇文章:[Spring初始化加载流程分析](https://blog.csdn.net/u011043551/article/details/79675363) 6 | 7 | **2、Spring AOP的实现原理。** 8 | 9 | Spring AOP的面向切面编程,是面向对象编程的一种补充,用于处理系统中分布的各个模块的横切关注点,比如说事务管理、日志、缓存等。它是使用动态代理实现的,在内存中临时为方法生成一个AOP对象,这个对象包含目标对象的所有方法,在特定的切点做了增强处理,并回调原来的方法。 10 | 11 | Spring AOP的动态代理主要有两种方式实现,JDK动态代理和cglib动态代理。JDK动态代理通过反射来接收被代理的类,但是被代理的类必须实现接口,核心是InvocationHandler和Proxy类。cglib动态代理的类一般是没有实现接口的类,cglib是一个代码生成的类库,可以在运行时动态生成某个类的子类,所以,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。 12 | 13 | **3、讲讲Spring事务的传播属性。** 14 | 15 | 事务就是对一系列的数据库操作(比如插入多条数据)进行统一的提交或回滚操作。如果插入成功,那么一起成功,如果中间有一条出现异常,那么回滚之前的所有操作。这样可以防止出现脏数据,防止数据库数据出现问题。 16 | 17 | 事务的传播行为,指的是当前带有事务配置的方法,需要怎么处理事务。 18 | 19 | 例如,方法可以继续在当前事务中运行,也可能开启一个新的事务,并在自己的事务中运行。 20 | 21 | 有一点需要注意,事务的传播级别,并不是数据库事务规范中的名词,而是Spring自身所定义的。通过事务的传播级别,Spring才知道如何处理事务,是创建一个新的事务,还是继续使用当前的事务。 22 | 23 | 在TransactionDefinition接口中,定义了三类七种传播级别。如下: 24 | 25 | ```java 26 | // TransactionDefinition.java 27 | // ========== 支持当前事务的情况 ========== 28 | /** 29 | * 如果当前存在事务,则使用该事务。 30 | * 如果当前没有事务,则创建一个新的事务。 31 | */ 32 | int PROPAGATION_REQUIRED = 0; 33 | /** 34 | * 如果当前存在事务,则使用该事务。 35 | * 如果当前没有事务,则以非事务的方式继续运行。 36 | */ 37 | int PROPAGATION_SUPPORTS = 1; 38 | /** 39 | * 如果当前存在事务,则使用该事务。 40 | * 如果当前没有事务,则抛出异常。 41 | */ 42 | int PROPAGATION_MANDATORY = 2; 43 | // ========== 不支持当前事务的情况 ========== 44 | /** 45 | * 创建一个新的事务。 46 | * 如果当前存在事务,则把当前事务挂起。 47 | */ 48 | int PROPAGATION_REQUIRES_NEW = 3; 49 | /** 50 | * 以非事务方式运行。 51 | * 如果当前存在事务,则把当前事务挂起。 52 | */ 53 | int PROPAGATION_NOT_SUPPORTED = 4; 54 | /** 55 | * 以非事务方式运行。 56 | * 如果当前存在事务,则抛出异常。 57 | */ 58 | int PROPAGATION_NEVER = 5; 59 | // ========== 其他情况 ========== 60 | /** 61 | * 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行。 62 | * 如果当前没有事务,则等价于 {@link TransactionDefinition#PROPAGATION_REQUIRED} 63 | */ 64 | int PROPAGATION_NESTED = 6; 65 | ``` 66 | 67 | 当然,绝大数场景下,我们是只用到PROPAGATION_REQUIRED传播级别的。 68 | 69 | 需要注意的是,以PROPAGATION_NESTED启动的事务内嵌于外部事物(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。这点类似于JDBC中的保存点(SavePoint)概念,嵌套的子事务就相当于保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚。 70 | 71 | 参考链接:[可能是最漂亮的 Spring 事务管理详解](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484702&idx=1&sn=c04261d63929db09ff6df7cadc7cca21&chksm=fa497aafcd3ef3b94082da7bca841b5b7b528eb2a52dbc4eb647b97be63a9a1cf38a9e71bf90&token=165108535&lang=zh_CN#rd) 72 | 73 | **4、Spring如何管理事务的,怎么配置事务。** 74 | 75 | 所谓事务管理,其实就是“**按照给定的事务规则来执行提交或者回滚操作**”。 76 | 77 | Spring提供两种类型的事务管理: 78 | 79 | - 声明式事务,通过使用注解或基于XML的配置事务,从而事务管理与业务代码分离。 80 | - 编程式事务,通过编码的方式实现事务管理,需要在代码中显示的调用事务的获得、提交、回滚。它提供了极大的灵活性,但维护起来非常困难。 81 | 82 | 实际场景下,我们一般使用SpringBoot+注解(@Transactional)的声明式事务。Spring的声明式事务管理建立在AOP基础上,其本质是在目标方法执行前进行拦截,在方法开始前创建一个事务,在执行完方法后根据执行情况提交或回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不用侵入业务代码,只需要在配置文件中做相关的事物声明就可将业务规则应用到业务逻辑中。和编程式事务相比,声明式事务唯一的不足是只能作用到方法级别,无法做到像编程式事务那样到代码块级别。 83 | 84 | 具体可以去看[Spring事务管理-编程式事务、声明式事务](https://blog.csdn.net/xktxoo/article/details/77919508)。 85 | 86 | **5、说说你对Spring的理解,非单例注入的原理?它的生命周期?循环注入的原理,aop的实现原理,说说aop中的几个术语,它们是怎么相互工作的。** 87 | 88 | 在bean的scope属性中,当值是singleton表示单例,当值是prototype表示多例。 89 | 90 | 单例:多次用factory.getBean("user",user.class)获取实体类,获得是同一类。 91 | 92 | 多例:多次用factory.getBean("user",user.class)获取实体类,获得是多个类。 93 | 94 | **6、Spring MVC中DispatcherServlet工作流程。** 95 | 96 | DispatcherServlet工作流程可以用一幅图来说明。 97 | 98 | ![Y5PCYn.jpg](https://s1.ax1x.com/2020/05/19/Y5PCYn.jpg) 99 | 100 | **①发送请求 :** 101 | 102 | 用户向服务器发送HTTP请求,请求被Spring MVC的调度控制器DispatcherServlet捕获。 103 | 104 | **②映射处理器 :** 105 | 106 | DispatcherServlet根据请求URL,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExectuionChain对象的形式返回。 107 | 108 | **③处理器适配 :** 109 | 110 | DispatcherServlet根据获得Handler,选择一个合适的HandlerAdapter。(如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler()方法) 111 | 112 | 提取请求Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作: 113 | 114 | - HttpMessageConverter:会将请求信息(如JSON、XML等数据)转换为一个对象。 115 | - 数据转换:对请求消息进行数据转换,如String转换为Integer、Double等。 116 | - 数据格式化:对请求消息进行数据格式化,如将字符串转换为格式化数字或格式化日期等。 117 | - 数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResult或error中。(自定义验证机制需要使用注解@InitBinder) 118 | 119 | Handler(Controller)执行完成后,向DispatcherServlet返回一个ModelAndView对象。当然,如果判断方法中有@ResponseBody注解,则直接将结果写回用户浏览器。 120 | 121 | 图中没有④。 122 | 123 | **⑤解析试图 :** 124 | 125 | 根据返回的ModelAndView,选择一个合适的ViewResolver(必须是已经注册到Spring容器中的ViewResolver),解析出View对象,然后返回给DispatcherServlet。 126 | 127 | **⑥⑦渲染视图+相应请求 :** 128 | 129 | ViewResolver结合Model和View,来渲染视图,并写回给用户浏览器。 130 | 131 | **7、Spring MVC用到的注解,作用是什么,原理。** 132 | 133 | 在Spring MVC中主要用到以下注解: 134 | 135 | - @Controller注解,它将一个类标记为Spring Web MVC 控制器 Controller。 136 | - @RestController注解,在@Controller注解的基础上,增加了@ResponseBody注解,更加适合目前前后端分离的架构下,提供Restful API,返回例如JSON数据格式。当然,返回什么样的数据格式,根据客户端的“ACCEPT”请求头来决定。 137 | - @RequestMapping注解,用户将特定的HTTP请求方法映射到将处理相应请求的控制器的特定类/方法。此注释可应用于两个级别,一是类级别,映射请求的URL;另一个是方法级别,映射URL以及HTTP请求方法。 138 | - @GetMapping注解,相当于是@RequestMapping的GET请求方法的特例,目的是为了提高清晰度。并且仅可注册在方法上。 139 | 140 | **8、Spring boot启动机制。** 141 | 142 | Spring Boot启动时的关键步骤,主要在两个方面: 143 | 144 | - SpringApplication实例的构建过程,其中主要涉及到了初始化器(Initializer)以及监听器(Listener)这两大概念,它们都通过META-INF/spring.factories完成定义。 145 | - SpringApplication实例run方法的执行过程,其中主要有一个SpringApplicationRunListeners的概念,它作为Spring Boot容器初始化时各阶段事件的中转器,将事件派发给感兴趣的Listeners(在SpringApplication实例的构建过程中得到的)。 146 | 147 | 强烈建议直接去看这篇文章:[[Spring Boot] 1. Spring Boot启动过程源码分析](https://blog.csdn.net/dm_vincent/article/details/76735888) 148 | 149 | 注意,这篇文章没有讲关于IOC的东西。这块需要自己补充。 150 | 151 | **9、Spring中用到的设计模式。** 152 | 153 | Spring在设计中用了几种常用的设计模式。 154 | 155 | **a,工厂模式 :** 156 | 157 | 在Spring中我们一般是将Bean的实例化直接交给容器去管理的,实现了使用和创建的分离,这时容器直接管理对象,还有种情况是,bean的创建过程我们交给一个工厂去实现,而Spring容器管理这个工厂。Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。两者对比如下: 158 | 159 | - BeanFactory:延迟注入(使用到某个bean的时候才会注入),相比于ApplicationContext来说会占用更少的内存,程序启动速度更快。 160 | - ApplicationContext:容器启动的时候,不管bean是否用到,一次性创建所有的bean。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。 161 | 162 | **b,单例设计模式 :** 163 | 164 | 在系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象等。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能导致一些问题的产生,比如:程序的行为异常、资源使用量、或者不一致性的结果。 165 | 166 | 使用单例模式的好处: 167 | 168 | - 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。 169 | - 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。 170 | 171 | Spring中bean默认作用域是singleton(单例),除了singleton作用域外,Spring中bean还有下面几种作用域。 172 | 173 | - prototype:每次请求都会创建一个新的实例。 174 | - request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 175 | - session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 176 | 177 | Spring boot 实现单例注册表的核心代码(DefaultSingletonBeanRegistry类)如下: 178 | 179 | ```java 180 | //通过ConcurrentMap(线程安全)实现单例注册表 注意这行!!! 181 | private final Map singletonObjects = new ConcurrentHashMap<>(256); 182 | 183 | public Object getSingleton(String beanName, ObjectFactory singletonFactory) { 184 | Assert.notNull(beanName, "Bean name must not be null"); 185 | synchronized (this.singletonObjects) { 186 | 187 | //检查缓存中是否存在实例 188 | Object singletonObject = this.singletonObjects.get(beanName); 189 | if (singletonObject == null) { 190 | //省略部分代码 191 | try { 192 | singletonObject = singletonFactory.getObject(); 193 | newSingleton = true; 194 | } 195 | // 省略部分代码 196 | // 如果实例对象在不存在,我们注册到单例注册表中 197 | if (newSingleton) { 198 | addSingleton(beanName, singletonObject); 199 | } 200 | } 201 | return singletonObject; 202 | } 203 | } 204 | //将对象添加到单例注册表中 205 | protected void addSingleton(String beanName, Object singletonObject) { 206 | synchronized (this.singletonObjects) { 207 | this.singletonObjects.put(beanName, singletonObject); 208 | this.singletonFactories.remove(beanName); 209 | this.earlySingletonObjects.remove(beanName); 210 | this.registeredSingletons.add(beanName); 211 | } 212 | } 213 | ``` 214 | 215 | **c,代理模式 :** 216 | 217 | AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 218 | 219 | Spring AOP就是基于动态代理的,其中有两种不同的代理方法:JDK代理和Cglib代理。具体可以看之前文章。 220 | 221 | 当然,也可以使用AspectJ,Spring AOP 已经集成了AspectJ。使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。 222 | 223 | 另外,Spring AOP属于运行时增强,而Aspect J是编译时增强。Spring AOP基于代理来实现,而Aspect J是基于字节码操作。 224 | 225 | **d,模板方法 :** 226 | 227 | 模板方法是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。 228 | 229 | **e,观察者模式 :** 230 | 231 | 观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。 232 | 233 | Spring中Observer模式常用的地方是listener的实现。如ApplicationListener。 234 | 235 | **f,适配器模式 :** 236 | 237 | 适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。 238 | 239 | 我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter 。Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor、AfterReturningAdviceAdapter、AfterReturningAdviceInterceptor。Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor负责适配 MethodBeforeAdvice)。 240 | 241 | 在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。 242 | 243 | 为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样: 244 | 245 | ```java 246 | if(mappedHandler.getHandler() instanceof MultiActionController){ 247 | ((MultiActionController)mappedHandler.getHandler()).xxx 248 | }else if(mappedHandler.getHandler() instanceof XXX){ 249 | ... 250 | }else if(...){ 251 | ... 252 | } 253 | ``` 254 | 255 | 假如我们再增加一个 Controller类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。具体可以参考[SpringMVC中的适配器(适配者模式)](https://www.cnblogs.com/tongkey/p/7919401.html) 256 | 257 | **g,策略模式 :** 258 | 259 | 策略模式对应于解决某一个问题的一个算法族,允许用户从该算法族中任选一个算法解决某一问题,同时可以方便的更换算法或者增加新的算法。并且由客户端决定调用哪个算法,spring中在实例化对象的时候用到Strategy模式。 260 | 261 | 总结如下 : 262 | 263 | Spring 框架中用到了哪些设计模式? 264 | 265 | - 工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。 266 | - 代理设计模式 : Spring AOP 功能的实现。 267 | - 单例设计模式 : Spring 中的 Bean 默认都是单例的。 268 | - 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 269 | - 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 270 | - 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。 271 | - 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。 272 | - ...... 273 | 274 | 参考文章:[Spring用到的设计模式,你都知道吗?](https://mp.weixin.qq.com/s?__biz=MzA5NTUzNTA2Mw==&mid=2454932296&idx=1&sn=3c25c8227792ee790877f5254a251bfa&chksm=871a01f0b06d88e6da4da708ffd1dfa9a999efe995ff218635bd716b02702387277e7f6b3f1e&mpshare=1&scene=23&srcid=0826inDqoMocUsxGpicsKNXx#rd) 和 [面试官:“谈谈Spring中都用到了那些设计模式?”](https://www.cnblogs.com/snailclimb/p/spring-design-patterns.html) 275 | 276 | **10、一个HTTP请求就是一个线程吗。** 277 | 278 | 一个HTTP请求就是一个线程。tomcat会维护一个线程池,每一个http请求,会从线程池中取出一个空闲线程。 279 | 280 | 在Tomcat中server.xml中连接器设置如下: 281 | 282 | ```xml 283 | 288 | ``` 289 | 290 | 相关参数表示为: 291 | 292 | - minProcessors:最小空闲连接线程数,用于提高系统处理性能,默认值为10,服务器启动时创建的处理请求的线程数。 293 | - maxProcessors:最大连接线程数,即:并发处理的最大请求数,默认值为75。 294 | - acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。 295 | - connectionTimeout:一个请求最多等待时间,超过则报错。举个例子,现在这个值设置为20000,则代表请求建立一个socket连接后,如果一直没有收到客户端的FIN,也没有数据过来,那么此连接也必须等到20s后,才能被超时释放。 296 | 297 | 这三个值的具体使用场景如下: 298 | 299 | - 情况1:Tomcat启动时,会按照minProcessors数目创建线程。 300 | - 情况2:接受一个请求,此时tomcat启动的线程数目没有到达maxThreads,tomcat会启动一个线程来处理此请求。 301 | - 情况3:接受一个请求,此时tomcat启动的线程数已经到达maxThreads,tomcat会把此请求放入等待队列中,等待空闲线程。 302 | - 情况4:接受一个请求,此时tomcat启动的线程数已经到达maxThreads,等待队列中的请求个数也达到了acceptCount,此时tomcat会直接拒绝此次请求,返回connect refused。 303 | 304 | 参考链接:[一个http请求就是一个线程吗,java的服务是每收到一个请求就新开一个线程来处理吗](https://blog.csdn.net/weixin_39833509/article/details/88603957) 305 | 306 | **11、SpringMVC和Structs2的区别。** 307 | 308 | - Structs2是类级别的拦截,一个类对应一个request上下文;而Spring MVC是方法级别的拦截,一个方法对应一个request上下文,而方法同时又跟一个url对应。 309 | - 在拦截机制上,Structs2有自己定义的Interceptor机制,而Spring MVC是独立的AOP方式。 310 | - SpringMCV几乎实现了零配置,在配置了基本的东西之后,再编写controller类和方式时,只需要加上注解即可,无需频繁地配置文件,而Structs2的机制是无法使用注解开发,那就需要重新编写一个action类配置一遍,非常繁琐。 311 | - 在于Ajax的集成方面,Spring MVC只需要一个注解@ResponseBody就可在返回之中返回数据,SpringMVC会自动将返回值数据转换为json数据,非常方便;而Structs2则需要自己手动将返回值数据转换成json格式,再手动写回浏览器,比较麻烦。 312 | 313 | **12、Mybaits框架的优缺点。** 314 | 315 | 优点如下: 316 | 317 | - 与JDBC相比,减少了50%以上的代码量。 318 | - MyBatis是最简单的持久化框架,小巧并且简单易学。 319 | - MyBatis灵活,不会对应用程序或者数据库的现有设计强加任何影响,SQL写在XML里,从程序代码中彻底分离,降低了耦合度,便于统一管理和优化,可以重用。 320 | - 提供XML标签,支持编写动态SQL语句(XML中使用if,else)。 321 | - 提供映射标签,支持对象与数据库的ORM字段关系映射(在XML中配置映射关系,也可以使用注解)。 322 | 323 | 缺点如下: 324 | 325 | - SQL语句的编写工作量大,尤其是字段多、关联表多时,更是如此,对开发人员编写SQL语句的功底有一定要求。 326 | - SQL语句依赖数据库,导致数据库移植性差,不能随意更换数据库。 327 | 328 | **13、Spring中Bean的生命周期。** 329 | 330 | 首先来看bean初始化之前的准备工作,如下图。 331 | 332 | ![Y5iwE4.png](https://s1.ax1x.com/2020/05/19/Y5iwE4.png) 333 | 334 | 主要看前三步: 335 | 336 | spring通过我们的配置,比如@ComponentScan定义的扫描路径去找到带有@Component的类,这个过程就是一个资源定位的过程。 337 | 338 | 一旦找到资源,那么它就开始解析,并将定义的信息保存到beanDefinition中。注意,此时还没有初始化bean,也没有bean实例,它有的仅仅是bean的定义。 339 | 340 | 然后把bean定义发布到Spring IOC容器中给,此时,IOC容器也只有bean的定义,还是没有bean的实例生成。 341 | 342 | 对于IOC容器BeanFactory来说,它遵循延迟注入原则,只有当用到某个bean的时候才会初始化(就是只有当getBean()调用的时候才会触发bean实例化)。而对于ApplicationContext容器来说,它是在系统启动的时候就完成了所有bean的初始化。 343 | 344 | 下面来看IOC容器初始化bean的过程,如下图。 345 | 346 | ![Y5i0UJ.png](https://s1.ax1x.com/2020/05/19/Y5i0UJ.png) 347 | 348 | 简单来说就以下几步: 349 | 350 | - 如果Bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID。 351 | - 如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身。 352 | - 如果Bean实现了ApplicationContextAware接口,则可以调用setApplicationContext获取ApplicationContext。 353 | - 将Bean实例传递给Bean的前置处理器的postProcessBeforeInitialization(Object bean, String beanname)方法。调用Bean的初始化方法。 354 | - 将Bean实例传递给Bean的后置处理器的postProcessAfterInitialization(Object bean, String beanname)方法。Bean使用过程。 355 | - 如果Bean实现了DisposableBean接口,则可以调用其destory()方法,也或者可以通过重写@PreDestroy方法在关闭时释放一些资源。 356 | 357 | **14、Spring中事务失效的几种原因。** 358 | 359 | Spring中通过注解@Transactional来实现事务,但在以下几种情况时,事务会失效。 360 | 361 | - Spring中事务自调用会失效,如果A方法调用B方法,B方法上有事务注解,AB方法在同一个类中,那么B方法的事务就会失效,这是动态代理的原因。 362 | - Spring的事务注解@Transactional只能作用在public修饰的方法上才起作用,如果放在非public(如private、protected)方法上,虽然不报错,但是事务不起作用。 363 | - 如果MySQL用的引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB引擎。 364 | - Spring建议在具体的类(或类的方法)上使用@Transactional注解,而不要使用在类所实现的任何接口上。在接口上使用@Transactional注解,只能当你设置了基于接口的代理时他才会生效,因为注解是不能继承的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。 365 | - 如果在业务代码中抛出RuntimeException异常,事务回滚;但是抛出Exception,事务不回滚。 366 | 367 | 需要注意的是,@Transactional也可以作用于类上,放在类级别上等同于该类的每个公有方法都放上了@Transactional。 368 | 369 | **15、注解继承问题。** 370 | 371 | 对于使用元注解@Inherited修饰的自定义注解,作用在父类上的自定义注解可以被继承下来。作用在接口上自定义注解不能被实现它的类继承下来。 372 | 373 | 需要注意的是:使用Inherited声明出来的注解,只有在类上使用时才会有效,对方法,属性等其他无效,并且也对于接口无效。简单一点来说吧,就是写在父类的类上的注解可以被继承,卸载父类方法上的注解无法被继承! 374 | 375 | 所以一般来说,我们应当在实现方法上打注解。 376 | 377 | 回到上一题说的事务注解@Transactional,它也有@Inherited修饰,所以对它来说,如果父类级别使用@Transactional,那子类会继承;如果父类方法级别使用@Transactional,那么子类方法不会继承;如果接口实现@Transactional,子类不会继承。 378 | 379 | **16、MyBatis的分页。** 380 | 381 | MyBatis实现分页操作时,有逻辑分页和物理分页这两个区别。 382 | 383 | - 逻辑分页:将数据一次性从数据库查出到内存中,在内存中进行逻辑上的分页。在实际应用方面,Mybaits自带分页RowBounds可以实现逻辑分页。 384 | - 物理分页:直接特定的SQL语句,只从数据库中查询出需要的数据。在实际应用方面,Mybatis可以通过自写SQL或者通过分页插件PageHelper来实现物理分页。 385 | 386 | 逻辑分页内存开销比较大,物理分页内存开销比较小。在数据量比较小的情况下,逻辑分页效率比物理分页高;在数据量很大的情况下,建议使用物理分页,因为物理内存开销太大,容易造成内存溢出。 387 | 388 | 我习惯在项目中使用PageHelper来实现分页。 389 | 390 | 弥有,2019年9月 391 | [EOF] 392 | -------------------------------------------------------------------------------- /interviews/2020届秋招面试题总结-多线程篇.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——多线程篇 2 | 3 | **1、多线程的几种实现方式,什么是线程安全。** 4 | 5 | Java多线程实现方式主要有四种: 6 | 7 | - 继承Thread类,启动线程的唯一方法就是通过Thread类的start()方法,实例后调用start()方法启动。 8 | - 实现Runable接口。 9 | - 实现Callable接口。 10 | - 通过FutureTask包装器。 11 | 12 | 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 13 | 14 | **2、volatile的原理,作用,能代替锁吗?** 15 | 16 | volatile是轻量级的synchronized,它让多处理器开发中保证了共享变量的“**可见性**”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。具有几条特性。 17 | 18 | - volatile**无法保证复合操作的原子性**。Java只保证了基本数据类型变量的赋值操作才是原子性的,当然,可以用过锁、synchronized来确保原子性。其实严格的说,**对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作是不具有原子性。** 19 | - volatile可以保证**可见性**,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会自己被更新到主内存中,当其他线程读取共享变量时,他会直接从主内存中读取。当然,synchronized和锁能都保证可见性。 20 | - volatile可以保证**有序性**,禁止指令重排序。 21 | 22 | 综上,volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存 屏障”来实现的。 23 | 24 | 在使用场景中,轻量级锁volatile 是不能取代 synchronized,但也可以在有限的一些情形下可以用volatile变量代替锁。要使volatile变量提供理想的线程安全,必须同时满足下面两个条件: 25 | 26 | - 对变量的写操作不依赖于当前值。比如使volatile变量不能用作线程安全计数器,不能x++。 27 | - 该变量没有包含在具有其他变量的不变式中。这块理解像是volatile变量不能用于约束条件。 28 | 29 | 参考来源:[深入分析volatile的实现原理](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247483784&idx=1&sn=672cd788380b2096a7e60aae8739d264&chksm=fa497e39cd3ef72fcafe7e9bcc21add3dce0d47019ab6e31a775ba7a7e4adcb580d4b51021a9&scene=21#wechat_redirect) 30 | 31 | **3、sleep和wait的区别。** 32 | 33 | 主要有四点区别: 34 | 35 | - sleep()方法是Thread类的静态方法,wait()方法是Object超类的成员方法。 36 | - sleep()方法导致程序暂停指定的时间,让出cpu给其他线程,但是它的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程是不会释放锁的。而调用wait()方法会释放对象锁,只有当此对象调用notify()方法后才会唤醒线程。 37 | - sleep()方法可以在任何地方使用,wait()方法只能在同步方法和同步代码块中配合synchronized使用。 38 | - sleep()方法需要抛出异常,wait()方法不需要。 39 | 40 | **4、sleep(0)的意义。** 41 | 42 | Thread.Sleep(0) 并非是真的要线程挂起0毫秒,意义在于这次调用Thread.Sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread.Sleep(0) 是你的线程暂时放弃cpu,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作。 43 | 44 | 在线程没退出之前,线程有三个状态,就绪态,运行态,等待态。sleep(n)之所以在n秒内不会参与CPU竞争,是因为,当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,等待定时器n秒后的中断事件,当到达n秒计时后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的,只有就绪队列中的线程才会参与cpu竞争,所谓的cpu调度,就是根据一定的算法(优先级,FIFO等),从就绪队列中选择一个线程来分配cpu时间。 45 | 46 | 而sleep(0)之所以马上回去参与cpu竞争,是因为调用sleep(0)后,因为0的原因,线程直接回到就绪队列,而非进入等待队列,只要进入就绪队列,那么它就参与cpu竞争。 47 | 48 | **5、Lock和Synchronized的区别。** 49 | 50 | 下面主要以可重入锁ReentrantLock为例。 51 | 52 | 两者相同点是: 53 | 54 | - 都实现了多线程同步和内存可见性语义。 55 | - 都是可重入锁。 56 | 57 | 两者不同点是: 58 | 59 | - 同步实现机制不同,synchronized是通过Java对象头锁标记和Monitor对象实现同步;而ReentrantLock则是通过CAS、AQS和LockSupport(用于阻塞和解除阻塞)实现同步。 60 | - 可见性实现机制不同,synchronized依赖JVM内存模型保证包含共享变量的多线程内存可见性;而ReentrantLock是通过AQS中的volatile state状态来保证包含共享变量的多线程内存可见性。 61 | - 使用方式不同,synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象);ReentrantLock 显示调用 tryLock 和 lock 方法,需要在 finally 块中释放锁。 62 | - 功能丰富程度不同,synchronized 不可设置等待时间、不可被中断(interrupted);ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能。 63 | - 锁类型不同,synchronized只支持公平锁;而ReentrantLock提供公平锁和非公平锁实现,当然,在大部分情况下,非公平锁是高效的选择。 64 | 65 | 在 synchronized 优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从 synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用 synchronized 。 66 | 67 | **6、synchronized的原理是什么,一般用在什么地方(比如加载静态方法和非静态方法的区别)?** 68 | 69 | synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。 70 | 71 | Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: 72 | 73 | - 普通同步方法,锁的是当前实例对象。 74 | - 静态同步方法,锁的是当前类的class对象。 75 | - 同步方法块,锁的是括号里面的对象。 76 | 77 | **同步方法块**时使用monitorenter(插入到同步代码块开始的位置)和monitorexit(插入到方法块结束处和异常处)指令实现的,而对于**同步方法**则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质都是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。任何一个对象都有自己的监视器, 78 | 79 | 当这个对象由方法块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入方法块和同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口出,进入BLOCKED状态。 80 | 81 | Java对象头和monitor是实现synchronized的基础。Java对象头中主要包括两部分数据,Mark Word(标记字段,用处存储对象自身的运行时数据,比如哈希码、GC分代年龄、锁状态标志、偏向线程ID等)和Klass Pointer(类型指针)。其中Klass Point是对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。 82 | 83 | 之前synchronized是一个重量级锁,相对于Lock,会显得笨重。 84 | 85 | jdk1.6对synchronized进行了各种优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 86 | 87 | 锁的主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 88 | 89 | - 偏向锁:当一个线程访问同步块并获取锁时,会在对象头(具体是Mark Word)和栈桢中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果其他线程竞争该偏向锁,则尝试使用CAS将对象头的偏向锁指向当前咸亨。 90 | - 轻量级锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方成为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 91 | 92 | 参考来源(强烈建议去看): 93 | 94 | 《Java并发编程的艺术》-2.2节 synchronized的实现原理与应用 95 | 96 | 4.3.1节-volatile和synchronized关键字 97 | 98 | [深入分析synchronized的实现原理](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247483775&idx=1&sn=e3c249e55dc25f323d3922d215e17999&chksm=fa497ececd3ef7d82a9ce86d6ca47353acd45d7d1cb296823267108a06fbdaf71773f576a644&scene=21#wechat_redirect) 99 | 100 | [【死磕 Java 并发】----- synchronized 的锁膨胀过程](https://mp.weixin.qq.com/s?__biz=MzI5NTYwNDQxNA==&mid=2247485139&idx=1&sn=3e2a0f56907b8bf90597ad6846cfa846&chksm=ec505f02db27d614f5fa11b7b68c44e277cd22ff3d36c194257e4128b32fa72ab8996d604e73&scene=0&xtrack=1&key=546b3b791faf1f5f0783adf2814016288ac471a37e3789b41b7e73e13409f6f2a303a253f2815214b04be6601d0140df22f46cba359ca857cc2c12ff03fb0452253e064e47fc9fdade8a740899fea421&ascene=1&uin=MjQ3MzkwMTc2Mw%3D%3D&devicetype=Windows+10&version=62060833&lang=zh_CN&pass_ticket=3tcg20nlda63%2Foo26D5nUe0ABNJ1xXnR%2Bv41IjPVVK8dK033jiWTfEVuPYmcwrnY) 101 | 102 | **7、用过哪些原子类,他们的原理是什么。** 103 | 104 | Java中有13个原子操作类,都属于Atomic包,基本都是使用Unsafe实现的包装类,再底层就都是CAS操作实现的。 105 | 106 | **8、用过线程池吗?如果用过,请说明原理,并说说newCache和newFixed有什么区别,构造函数的各个参数的含义是什么,比如coreSize、maxSize等。** 107 | 108 | Java的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。 109 | 110 | - 线程复用:线程池中的线程是可以复用的,省去了创建、销毁线程的开销,提高了资源利用率(创建、销毁等操作都是要消耗系统资源的)和响应速度(任务提交过来线程已存在就不用等待线程创建了); 111 | - 合理利用利用资源:通过调整线程池大小,可以让所有处理器尽量保持忙碌,并且又能放置过多线程产生过多竞争浪费资源。 112 | - 提高线程的可管理型。使用线程池可以进行统一分配、调优和监控。 113 | 114 | 当提交一个新任务到之后,线程池的处理流程如下: 115 | 116 | - 线程池先判断其核心线程池里的线程(corePoolSize,基本线程数量)是否都在执行任务,如果不是,则创建一个新的工作线程。如果核心线程池的线程都在执行任务,则进入下个流程。 117 | - 线程池判断工作队列是否已满,如果工作序列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。 118 | - 线程池判断线程池的线程(maximumPoolSize,最大线程数量)是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。其中默认的饱和策略是AbortPolicy,表示无法处理新任务时抛出异常。 119 | 120 | Java中通过ThreadPoolExecutor来创建线程池。 121 | 122 | ```java 123 | new ThreadPoolExecutor(int corePoolSize,//线程池的基本大小 124 | int maximumPoolSize,//线程池最大数量 125 | long keepAliveTime,//线程活动保持时间,线程池的工作线程空间后,保持存活的时间 126 | TimeUnit unit,//线程活动保持时间的单位 127 | BlockingQueue workQueue,//任务队列,用于保存等待执行的任务的阻塞队列 128 | ThreadFactory threadFactory,//用于设置创建线程的工厂 129 | RejectedExecutionHandler handler)//饱和策略 130 | ``` 131 | 132 | JDK预定义了四种线程池: 133 | 134 | - newFixedThreadPool:固定大小线程池,创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列(无界)中等待。 135 | - newCachedThreadPool:创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 136 | - newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。 137 | - newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 138 | 139 | 对于高并发,可以将SynchronousQueue作为参数,使maximumPoolSize发挥作用,以防止线程被无限制的分配,同时可以通过提高maximumPoolSize来提高系统吞吐量,另外,也自定义一个RejectedExecutionHandler,当线程数超过maximumPoolSize时进行处理,处理方式为隔一段时间检查线程池是否可以执行新Task,如果可以把拒绝的Task重新放入到线程池,检查的时间依赖keepAliveTime的大小。 140 | 141 | **9、线程池的关闭方式有几种,各自的区别是什么。** 142 | 143 | 可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法中止。 144 | 145 | **10、spring的controller是单例还是多例,怎么保证并发的安全。** 146 | 147 | spring bean作用域有五种: 148 | 149 | - singleton:单例模式,当spring创建applicationContext容器的时候,spring会欲初始化所有的该作用域实例,加上lazy-init就可以避免预处理; 150 | - prototype:原型模式,每次通过getBean获取该bean就会新产生一个实例,创建后spring将不再对其管理; 151 | 152 | ====下面是在web项目下才用到的=== 153 | 154 | - request:搞web的大家都应该明白request的域了吧,就是每次请求都新产生一个实例,和prototype不同就是创建后,接下来的管理,spring依然在监听。 155 | - session:每次会话,同上。 156 | - global session:全局的web域,类似于servlet中的application。 157 | 158 | spring中的controller默认是单例,也就是singleton模式了。 159 | 160 | 所以如果controller中有一个私有变量a,所有请求到同一个controller时,使用的a变量都是共用的,即若是某个请求修改了这个变量a,则,在别的请求中能够读到这个修改的内容。 161 | 162 | 为了保证并发的安全,常见有两种解决方法。 163 | 164 | - 在controller中使用ThreadLocal变量。 165 | - 在spring配置文件Controller中声明为scope="prototype",每次都创建新的controller,不再使用单例模式。 166 | 167 | 另外,Servlet也不是线程安全的,Servlet是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。 168 | 169 | **11、ThreadLocal用过吗,用途是什么,原理是什么,需要注意什么。** 170 | 171 | ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程的一个值。 172 | 173 | - Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个map,它的key是ThreadLocal实例对象。 174 | - 当为ThreadLocal类的对象set值时,首先获取当前线程的ThreadLocalMap变量,然后以ThreadLocal类的对象为key,设定value。get值时则类似。 175 | - ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。 176 | - 由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的。 177 | 178 | 需要注意的是,每次set/get值,不直接用线程id来作为ThreadLocalMap的key,因为若直接用线程id当作key,无法区分放入ThreadLocalMap中的多个value。所以是使用ThreadLocal作为key,因为每一个ThreadLocal对象都可以由threadLocalHashCode属性(final修饰,每次实例创建后就不会更改了)唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分,所以可以用不同的ThreadLocal作为key,区分不同value。 179 | 180 | 如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger(提供原子操作的Integer类)类变量(nextHashCode)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次创建ThreadLocal对象都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。 181 | 182 | TreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。 183 | 184 | - 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。 185 | - 而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。 186 | - 概括起来说,对于多线程资源共享的问题,同步机制采用了 **“以时间换空间”** 的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。 187 | 188 | 在默认单例的Spring bean中,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。 189 | 190 | 需要注意的是,TheadLocalMap作为hash表的一种实现方式,是通过**开放寻址法** 来解决哈希冲突,这点不同于HashMap。开放寻址法的核心是如何出现了散列冲突,就重新探测一个空闲位置,将其插入。当我们往散列表插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,以此往后查找,看是否有空闲位置,直到找到为止。 191 | 192 | 另外,ThreadLocalMap的初始长度为16。当集合中size数量大于规定长度的1/2()时,则执行resize()操作,扩容到原来两倍。具体代码如下: 193 | 194 | ```java 195 | private void rehash() { 196 | expungeStaleEntries(); 197 | // Use lower threshold for doubling to avoid hysteresis 198 | if (size >= threshold - threshold / 4) //相当于size>= 3/4 * threshold,其中,threshold=2/3 * len,所以size>=1/2 * len时,即发生扩容 199 | resize(); 200 | } 201 | ``` 202 | 203 | **12、如何实现一个并发安全的链表。** 204 | 205 | 有以下几种方式: 206 | 207 | - 采用粗粒度锁,完全锁住链表。 208 | - 采用细粒度锁,只锁住需要修改的节点。 209 | - 利用CAS来修改节点。 210 | 211 | **13、有哪些无锁的数据结构,怎么做。** 212 | 213 | 要实现一个线程安全的队列有两种方式:阻塞和非阻塞。阻塞队列无非就是锁的应用,而非阻塞则是CAS算法的应用(无锁)。比较常见的是ConcurrentLinkedQueue,这是一个基于链表节点的无边界的线程安全队列,它采用FIFO原则对元素进行排序,采用“wait-free”算法(即CAS算法)来实现,这是单向链表。 214 | 215 | **14、讲讲java同步机制的wait和notify。** 216 | 217 | wait与notify是Java同步机制中的重要组成部分。结合与synchronized关键字使用,可以建立很多优秀的同步模型,例如生产者-消费者模型。需要注意几点: 218 | 219 | - wait()、notify()、notifyAll()方法不属于Thread类,而是属于Object基础类,因为Java为每个Object对象都分配了一个monitor。 220 | - 当需要调用以上的方法的时候,一定要对竞争资源进行加锁,如果不加锁的话,则会报 IllegalMonitorStateException 异常。 221 | - 当想要调用wait( )进行线程等待时,必须要取得这个锁对象的控制权(对象监视器),一般是放到synchronized(obj)代码中。 222 | - notify( )方法只会通知等待队列中的第一个相关线程(不会通知优先级比较高的线程) 223 | - notifyAll( )通知所有等待该竞争资源的线程(也不会按照线程的优先级来执行) 224 | 225 | **15、countdowlatch和cyclicbarrier的内部原理和用法,以及相互之间的差别(比如countdownlatch的await方法和是怎么实现的)。** 226 | 227 | CyclicBarrier:允许一组线程互相等待,直到到达某个公共屏障点,才会进行后续任务,内部是使用重入锁ReentrantLock和Condition。 228 | 229 | CountDownLatch:在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,内部依赖Sync实现,而Sync继承AQS。 230 | 231 | 两者的区别: 232 | 233 | - CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。 234 | - CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。 235 | 236 | await方法对比: 237 | 238 | CyclicBarrier中await()方法内部调用dowait方法,每当进来一个线程,则对设定的总量count--,直到为0,才会继续执行后续的任务Runnable。CountDownLatch中await()方法让当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。其中计数器是用AQS的状态值来表示的。如果计数器值不为零,则会调用AQS的自选方法尝试一直去获取同步状态。 239 | 240 | 参考链接: 241 | 242 | [【死磕Java并发】-----J.U.C之并发工具类:CountDownLatch](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484300&idx=1&sn=fcdadc7aeebfd397731820a50bbf1374&chksm=fa497c3dcd3ef52b9645f2912e2674c03944d36a1e5638e42da7a30b928d85a51746682b1df7&scene=21#wechat_redirect) 243 | 244 | [【死磕Java并发】—- J.U.C之并发工具类:CyclicBarrier](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484184&idx=1&sn=d221688af03cbab0bf7e719fa253a266&chksm=fa497ca9cd3ef5bf394189cc2432499b93eaaf92314ee5c4dd451b6ccf3aa20ab527d56bea8e&scene=21#wechat_redirect) 245 | 246 | **16、对AbstractQueuedSynchronizer了解多少,讲讲加锁和解锁的流程,独占锁和公平锁加锁有什么不同。** 247 | 248 | AQS,即队列同步器,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。 249 | 250 | 同步器的主要使用方法是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。 251 | 252 | AQS通过内置的FIFO同步队列来完成线程获取资源的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。 253 | 254 | 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。 255 | 256 | 共享式获取和独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以读写锁ReentrantReadWriteLock为例,它的读取锁ReadLock是共享式的,可以允许多个程序同时对文件进行读操作;但它的写入锁WriteLock是独占式的,同一时刻只能允许一个线程对文件进行写操作。 257 | 258 | 独占锁主要代码如下: 259 | 260 | ```java 261 | public final void acquire(int arg) { 262 | if (!tryAcquire(arg) && 263 | acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 264 | selfInterrupt(); 265 | } 266 | ``` 267 | 268 | - tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。**该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。** 269 | - addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。 270 | - acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。 271 | - selfInterrupt:产生一个中断。 272 | 273 | 共享锁主要代码如下: 274 | 275 | ```java 276 | public final void acquireShared(int arg) { 277 | if (tryAcquireShared(arg) < 0) 278 | doAcquireShared(arg); 279 | } 280 | ``` 281 | 282 | 方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功。同样,**tryAcquireShared()方法也需要自定义同步组件自己实现。在ReentrantReadWriteLock.ReadLock中重写的tryAcquireShared()方法中,通过获取锁的共享计数是否超过限制(MAX_COUNT,65535)来进行判断。** 283 | 284 | 锁的公平性,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获得锁,也就是说锁获取是顺序的。当然,公平锁机制往往没有非公平的效率高,但也能够减少“饥饿”发生的概率。 285 | 286 | **公平锁与非公平锁的区别在于获取锁的时候是否按照FIFO的顺序来。** 释放锁不存在公平性和非公平性。 287 | 288 | 比较公平锁和非公平锁获取同步状态的tryAcquire()方法(以独占式排他锁ReentrantLock为例,所以是tryAcquire(),如果是共享锁,则是tryAcquireShared()),两者区别在于公平锁在获取同步状态时多了一个限制条件hasQueuedPredecessors(),定义如下。 289 | 290 | ```java 291 | public final boolean hasQueuedPredecessors() { 292 | Node t = tail; //尾节点 293 | Node h = head; //头节点 294 | Node s; 295 | //头节点 != 尾节点 296 | //同步队列第一个节点不为null 297 | //当前线程是同步队列第一个节点 298 | return h != t && 299 | ((s = h.next) == null || s.thread != Thread.currentThread()); 300 | } 301 | ``` 302 | 303 | 该方法主要判断当前线程是否位于CLH同步队列中的第一个,如果是则返回true,否则返回false。这点保证了公平性。 304 | **具体代码强烈建议**这块去看一下ReentrantReadWriteLock、ReentrantLock类的源码。 305 | 306 | **17、简述ConcurrentLinkedQueue和LinkedBlockingQueue的用处和不同之处。** 307 | 308 | ConcurrentLinkedQueue类型基于lock-free,采用CAS操作Node节点(Node节点里的元素也用volatile修饰,类似于CLH),来保证元素的一致性。(需要看一波这个实现代码了^_~)LinkedBlockingQueue是用一个独占锁来保持线程安全,然后用Condition来做阻塞操作。实现了先进先出等特性,是作为生产者消费者的首选。 309 | 310 | **18、导致线程死锁的原因,怎么解除线程死锁。** 311 | 312 | 死锁是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 313 | 314 | 产生死锁的必要条件: 315 | 316 | - 互斥条件,所谓互斥就是线程在某一个时间内独占资源。 317 | - 请求与保持条件,一个进程因请求资源而阻塞时,对已获得的资源保持不变。 318 | - 不剥夺条件,线程已获得资源,在未使用之前,不能强行剥夺。 319 | - 循环等待条件,若干线程之间形成一种头尾相接的循环等待资源关系。 320 | 321 | 解除线程死锁的方法: 322 | 323 | - 加锁顺序,当多个线程需要相同的一些锁,必须确保所有线程都是按照相同的顺序获得锁。 324 | - 加锁时限,在尝试获取锁的时候加一个超时时间,若超过这个时限该线程则放弃对该锁的请求。 325 | - 死锁检测:每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。 326 | - 当出现死锁时,释放所有锁,回退,并且等待一段随机的时间后重试。或者更好的做法是给这些线程设置优先级,让一个(或多个)线程回退,其他线程继续保持它们需要的锁。 327 | 328 | **19、用过读写锁吗,原理是什么,一般在什么场景下用。** 329 | 330 | 与排他锁ReentrantLock不同,读写锁ReentrantReadWriteLock在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一个对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大的提升。 331 | 332 | 除了保证写操作对读操作的可见性以及并发性的提升外,读写锁能够简化读写交互场景的编程方法。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占用的时间很少,但写操作完成之后的更新需要对后续的读服务可见。 333 | 334 | 写锁就是一个支持可重入的排他锁。写锁的状态获取最终会调用tryAcquire(int arg)方法,注意,在判断重入时加入了一项条件:读锁是否存在。因为要确保写锁的操作对读锁是可见的,因此只有等读锁完全释放后,写锁才能够被当前线程所获取。一旦写锁获取了,所有其他读、写线程均会被阻塞。 335 | 336 | 读锁为一个可重入的共享锁,它能够被多个线程同时持有,在没有其他写线程访问时,读锁总是或获取成功。读锁是通过调用tryAcqurireShared(int arg)方法尝试获取读同步状态,该方法主要用于获取共享式同步状态,获取成功返回 >= 0的返回结果,否则返回 < 0 的返回结果。读锁获取方式比写锁麻烦一些,需要注意的是: 337 | 338 | - 因为存在锁降级情况,如果存在写锁且锁的持有者不是当前线程则直接返回失败,否则继续。 339 | - 依据公平性原则,判断读锁是否需要阻塞,读锁持有线程数小于最大值(65535),且设置锁状态成功。 340 | 341 | 参考链接:[【死磕Java并发】-----J.U.C之读写锁:ReentrantReadWriteLock](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484040&idx=1&sn=60633c2dc4814b26dc4b39bb2bb5d4dd&chksm=fa497d39cd3ef42f539cd0576c1a3575ee27307048248571e954f0ff21a5a9b1ddfab522c834&scene=21#wechat_redirect) 342 | 343 | **20、开启多个线程,如果保证顺序执行,有哪几种实现方式,或者如何保证多个线程都执行完毕后再拿到结果。** 344 | 345 | 保证多个线程顺序执行的方法有几种: 346 | 347 | - 采用信号量Sephmore,下一个线程执行的条件是上一个线程执行完毕后释放信号量。 348 | - 使用ReentrantLock,每次只有一个线程可以获取锁来执行。 349 | - 采用Countdownlatch,可以让下一个线程执行的条件是前一个线程为0。 350 | - 使用单线程池,这样就能根据传入的顺序执行线程。等等,还比如阻塞队列等。 351 | 352 | 保证多个线程都执行完毕后再拿到结果可以采用Countdowlatch和Cyclicbarrier来实现。 353 | 354 | **21、当一个线程进入某一个对象的一个synchronized的实例方法后,其他线程是否可以进入此对象的其他方法。** 355 | 356 | 如果该实例的其他方法没有synchronized修饰的话,其他线程是可以进入的。 357 | 358 | 另外,需要注意的是,synchronized是实例锁(锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念),static synchronized是类锁(该锁针对的是类,无论实例多少个对象,那么线程都是共享该锁),并且对象锁与类锁互不干扰,与对象无关。 359 | 360 | - synchronized是对类的**当前实例**(当前对象)进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块(注:是所有),注意这里是“类的当前实例”, 类的两个不同实例就没有这种约束了。 361 | - static synchronized恰好就是要控制类的所有实例的并发访问,static synchronized是限制**多线程中该类的所有实例**同时访问jvm中该类所对应的代码块。 362 | 363 | 参考链接:[Synchronized(对象锁)和Static Synchronized(类锁)的区别](https://www.cnblogs.com/lixuwu/p/5676143.html) 364 | 365 | **22、可以创建volatile数组吗?** 366 | 367 | Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。 368 | 369 | 同理,对于 Java POJO 类,使用 volatile 修饰,只能保证这个引用的可见性,不能保证其内部的属性。 370 | 371 | **23、用三个线程按顺序循环打印abc三个字母,比如abcabcabc?** 372 | 373 | 采用等待/通知机制来实现,主要分为两种。 374 | 375 | - 使用Lock+Condition来实现。参考[用三个线程按顺序循环打印abc 三个字母,比如abcabcabc](https://blog.csdn.net/Big_Blogger/article/details/65629204)。 376 | - 使用synchronized+await/notifyAll来实现。参考[Java用三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc](https://blog.csdn.net/weixin_41704428/article/details/80482928)。 377 | 378 | **24、一个线程池设计的最大线程数应该考量哪些因素。** 379 | 380 | 要想合理地配置线程池的大小,首先要分析任务的特性,可以从以下几个角度分析: 381 | 382 | - 任务的性质:计算密集型任务、IO密集型任务。 383 | - 任务的优先级:高、中、低。 384 | - 任务的执行时间:长、中、短。 385 | - 任务的依赖性:是否依赖其他系统资源,如数据库操作。 386 | 387 | 性质不同的任务可以交给不同规模的线程池执行。 388 | 389 | 在有N个CPU的系统上,计算密集型任务应配置尽可能少的线程,可以将线程池大小设置为N+1; 390 | 391 | IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如线程池大小设置为2N+1; 392 | 393 | 而对于混合型的任务,如果可以拆分,拆分成IO密集型和计算密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。 394 | 395 | 若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。 396 | 397 | 当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。 398 | 399 | 在这篇如何合理地估算线程池大小?有一个公认的估算合理值的公式,如下 400 | $$ 401 | 最佳线程数目 = (线程等待时间/线程CPU时间 + 1)* CPU数目 402 | $$ 403 | 可以得出一个结论:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 404 | 405 | 以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。 406 | 407 | 至于为什么要+1,我理解为要留一个给主线程使用的,避免后台任务将CPU资源完全耗尽。 408 | 409 | 另外,是否使用线程池就一定比单线程高效,答案是否定的,比如Redis就是单线程,但它却非常高效。从线程的角度,部分原因在于: 410 | 411 | - 多线程会带来线程上下文切换开销,单线程就没有这种开销。 412 | - 单线程避免了锁的设计。 413 | 414 | 当然,“Redis”更快的本质原因还在于:Redis基于内存操作,这种情况下单线程可以很高效地利用CPU。而多线程使用场景一般是:存在相当比例的IO和网络操作。 415 | 416 | **25、在Java中守护线程和本地线程区别。** 417 | 418 | Java中的线程分为两种:守护线程(Daemon)和用户线程(User)。 419 | 420 | 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(boolean);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则会抛出异常。 421 | 422 | 两者唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon没有可服务的线程,JVM撤离。也可以理解为守护线程是JVM自动创建的线程(但不一定),用户线程是程序创建的线程。比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。 423 | 424 | 另外,Thread Dump打印出来的线程信息,含有daemon字样的线程即为守护线程,可能会有:服务守护线程、编译守护线程、windows下的监听Ctrl+break的守护线程、GC守护线程等。 425 | 426 | **26、Java中用到的线程调度算法是什么。** 427 | 428 | 计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权。 429 | 430 | 有两种调度模型:分时调度模型和抢占式调度模型。 431 | 432 | - 分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。 433 | 434 | - java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。 435 | 436 | **27、线程的状态。** 437 | 438 | 一般来说,都把线程分为多个状态:NEW(新建状态)、RUNNABLE(运行状态)、BLOCKED(锁池)、TIMED_WAITING(定时等待)、WAITING(等待)、TERMINATED(终止、结束)。 439 | 440 | 具体的关系可以总结为一张图: 441 | 442 | ![Y51ziq.png](https://s1.ax1x.com/2020/05/19/Y51ziq.png) 443 | 444 | 注意,线程状态waiting和blocked的区别在于: 445 | 446 | - 线程可以通过notify,join,LockSupport.park方式进入wating状态,进入wating状态的线程等待唤醒(notify或notifyAll)才有机会获取cpu的时间片段来继续执行。 447 | - 线程的 blocked状态往往是无法进入同步方法/代码块来完成的。这是因为无法获取到与同步方法/代码块相关联的锁。 448 | 449 | 与wating状态相关联的是**等待队列**,与blocked状态相关的是**同步队列**,一个线程由等待队列迁移到同步队列时,线程状态将会由wating转化为blocked。可以这样说,blocked状态是处于wating状态的线程重新焕发生命力的必由之路。例如,notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中去,被移动的线程状态是从WAITING变成BLOCKED。 450 | 451 | 这块具体可以看一下《Java并发编程的艺术》中4.3.2小节 等待/通知机制,P101。 452 | 453 | **28、AQS组件总结。** 454 | 455 | **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 456 | 457 | **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 458 | 459 | **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 460 | 461 | **29、进程和线程的区别。** 462 | 463 | 进程是操作系统进行资源分配的基本单位。进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。一个进程中可以有多个线程,线程是独立调度的基本单位。同一个进程中的多个线程之间可以并发执行,它们共享进程资源。 464 | 465 | 进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有进程本身运行的情况下是不能访问其中的内容的。 466 | 467 | 线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定。线程的运行中需要使用计算机的内存资源和CPU。 468 | 469 | 线程和进程的区别归纳: 470 | 471 | - **地址空间与其他资源**:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其他进程不可见。 472 | - **通信**:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要线程同步和互斥手段的辅助,以保证数据的一致性。 473 | - **调度和切换**:线程上下文切换比进程上下文切换要快得多。并且线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。 474 | - **系统开销**:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置。而线程切换时只需保存和设置少量寄存器内容,开销很小。 475 | 476 | **30、进程间通信的方式。** 477 | 478 | 进程间通信基本上有5种通讯方式。 479 | 480 | - 无名管道通信:速度慢,容量有限,只有父子进程能通信。 481 | - FIFO:任何进程间都能通信,但是速度慢。 482 | - 消息队列:容量受到系统限制,可以实现消息的随即查询,消息不一定以先进先出的次序读取,也可以按照消息的类型读取。且要注意第一次读的时候,要考虑上一次没有读完数据的问题。 483 | - 信号量:不能传递复杂消息,只能用来同步。 484 | - 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全。 485 | 486 | **31、LockSupport的优势。** 487 | 488 | LockSuppor也是用于线程挂起和唤醒的,相比Object的wait/notify有两大优势: 489 | 490 | - LockSupport不需要在同步代码块中,所以线程间不需要维护一个共享的同步对象了,实现了线程间的解耦。 491 | - unpark函数可以先与park调用,所以不需要担心线程间的执行的先后顺序。 492 | 493 | **32、为什么wait(),notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?** 494 | 495 | 调用wait()方法就是释放锁,释放锁的前提是必须要先获取锁,先获取锁才能释放锁。 496 | 497 | notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,所以自身必须先有锁才行。 498 | 499 | **33、ThreadLocal是否可以用static修饰。** 500 | 501 | 在开发过程中,ThreadLocal一般会采用static修饰,这样做既有好处也有坏处。好处是它一定程度上可以避免错误,至少可以避免 重复创建TSO(Thead Specific Object,即ThreadLocal所关联的对象)所导致的浪费。坏处是这样做可能正好形成内存泄漏所需的条件。 502 | 503 | 我们知道,一个ThreadLocal实例对应当前线程中的一个TSO实例。因此,如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个实例都会导致一个新的TSO实例被创建。显然,这些被创建的TSO实例是同一个类的实例。于是,同一个线程可能会访问到同一个TSO(指类)的不同实例,这既便不会导致错误,也会导致浪费(重复创建等同的对象)!因此,我们一般将ThreadLocal使用static修饰即可。 504 | 505 | 现在来讨论缺点。 506 | 507 | 由于ThreadLocal是某个类的一个静态变量,因此,只要相应的类没有被垃圾回收掉,那么这个类就会持有相应的ThreadLocal实例的应用。另外,ThreadLocal的内部实现包括一个类似HashMap的对象,这里称之为ThreadLocalMap。ThreadLocalMap的key会持有对ThreadLocal实例的弱引用(Weak Reference),value会引用TSO实例。当服务器的工作者线程不会被垃圾回收时,ThreadLocal实例此时也不会被垃圾回收,这就产生了内存泄漏。 508 | 509 | 参考链接:[将ThreadLocal变量设置为private static的好处是啥? - Viscent大千的回答 - 知乎](https://www.zhihu.com/question/35250439/answer/101676937) 510 | 511 | 弥有,2019年9月 512 | [EOF] 513 | -------------------------------------------------------------------------------- /interviews/2020届秋招面试题总结-操作系统篇.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——操作系统篇 2 | 3 | **1、进程和线程的区别。** 4 | 5 | 进程是具有一定功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源调度和分配的一个独立单位。进程是通过进程控制块PCB来控制的,主要包括:进程描述(PID、用户标识、进程组关系)、进程控制(状态、优先级、入口地址、队列指针)、资源和使用状况(存储空间、文件)、CPU现场(进程不执行时保存寄存器值、指向页表的指针) 6 | 7 | 线程是进程的实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程可以有多个线程,多个线程也可以并发执行。 8 | 9 | **2、进程同步的几种方式。** 10 | 11 | 主要分为:管道、系统IPC(包括消息队列、信号量、共享存储)、SOCKET 12 | 13 | 管道主要分为:普通管道PIPE 、流管道(s_pipe)、命名管道(name_pipe) 14 | 15 | - 管道是一种半双工的通信方式,数据只能单项流动,并且只能在具有亲缘关系的进程间流动,进程的亲缘关系通常是父子进程。 16 | - 命名管道也是半双工的通信方式,它允许无亲缘关系的进程间进行通信。 17 | - 信号量是一个计数器,用来控制多个进程对资源的访问,它通常作为一种锁机制。 18 | - 消息队列是消息的链表,存放在内核中并由消息队列标识符标识。 19 | - 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 20 | - 共享内存就是映射一段能被其它进程访问的内存,这段共享内存由一个进程创建,但是多个进程可以访问。 21 | 22 | **3、线程间同步的方式。** 23 | 24 | - **互斥量Synchronized/Lock:**采用互斥对象机制,只要拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。 25 | - **信号量Semaphare:**它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。 26 | - **事件(信号),Wait/Notify:**通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。 27 | 28 | **4、什么是缓冲区溢出。有什么危害,其原因是什么。** 29 | 30 | 缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。 31 | 32 | 危害有以下两点: 33 | 34 | - 程序崩溃,导致拒绝的服务。 35 | - 跳转并且执行一段恶意代码。 36 | 37 | 造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。 38 | 39 | **5、进程中有哪几种状态。** 40 | 41 | 就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源。 42 | 43 | 运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数目。 44 | 45 | 阻塞状态:进程等待某种条件,在条件满足之前无法执行。 46 | 47 | 状态图如下图所示。 48 | 49 | ![Y4YJsO.png](https://s1.ax1x.com/2020/05/19/Y4YJsO.png) 50 | 51 | **6、分页和分段有什么区别。** 52 | 53 | 段式存储管理是一种符合用户视角地内存分配管理方案。在段式存储管理中,将程序的地址空间划分为若干段(segment),比如代码段、数据段、堆栈段。这样每个进程都有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如5k的段换4k的段,会产生1k的外碎片)。 54 | 55 | 页式存储管理方案是一种用户视角内存与物理内存相分离的内存分离管理方案。在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而屋里内存划分为同样大小的帧,程序加载时,可以将任意一页放入内存中的任意一个帧,这些帧不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)。 56 | 57 | 两者的不同如下: 58 | 59 | - **目的不同:** 段是信息的逻辑单位,它是根据用户的需求划分的,因此段是对用户可见的;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。 60 | - **大小不同:** 段的大小不固定,有它所完成的功能决定;页的大小是固定的,由系统决定。 61 | - **地址空间不同:** 段向用户提供二维地址空间;页向用户提供的是一维地址空间。 62 | - **信息共享:** 段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享收到限制。 63 | - **内存碎片:** 页式存储管理的优点是没有外碎片,但是会产生内碎片。而段式管理的优点是没有内碎片,但会产生外碎片。 64 | 65 | 在分页系统中,允许将进程的每一页离散地存储在内存的任一物理块中,为了能在内存中找到每个页面对应的物理块,系统为每个进程建立了一张页面映射表,简称页表。页表的作用就是实现从页号到物理块号的地址映射。 66 | 67 | **7、操作系统中进程调度策略有哪几种。** 68 | 69 | 操作系统中进程调度策略主要包括FCFS(先来先服务)、优先级、时间片轮流、多级反馈等。具体分为以下几种,不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。 70 | 71 | 首先讨论**批处理系统**。批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。 72 | 73 | **先来先服务 first-come first-serverd(FCFS):** 非抢占式,FCFS是一种最简单的调度算法,该算法即可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建线程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。 74 | 75 | **短作业优先 shortest job first(SJF):** 非抢占式,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。它们可以分别用于作业调度和进程调度。短作业优先的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一致执行到完成,或发生事件而被阻塞放弃处理机时再重新调度。 76 | 77 | **最短剩余时间优先 shortest remaining time next(SRTN):** 最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 78 | 79 | 然后讨论**交互式系统**,交互式系统有大量的用户交互操作,在该系统中调度算法的目标就是快速地进行响应。 80 | 81 | **时间片轮转算法:** 将所有就绪进程按照FCFS地原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序边停止该进程的执行,并将它送往就绪队列的末尾,同时继续把CPU时间分配给队首的进程。时间片轮转算法的效率和时间片的大小有很大的关系。时间片过小,会导致进程切换太频繁。如果时间片过长,那么实时性就不能得到保证。 82 | 83 | **优先级调度:** 为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远得不到调度,可以随着时间的推移增加等待线程的优先级。 84 | 85 | **多级反馈队列:** 一个进程需要执行100个时间片,如果采用时间片轮转调度算法,那么需要交换100次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如1,2,4,8....。进程在第一个队列没执行完,就会转移到下一个队列。这种方式下,之前的进行就只需要交换7次。每个丢列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排休,才能调度当前队列上的进程。 86 | 87 | **8、死锁的必要条件和处理方法。** 88 | 89 | 死锁的概念,在两个或者多个并发进程中,如果每个进程持有某个资源而又都等待别的进程释放它或他们现在保持的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗地讲,死锁就是两个或多个进程被无限期地阻塞、相互等待的一种状态。 90 | 91 | 死锁的必要条件有四个。 92 | 93 | - **互斥:** 进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。简单来说,就是一个资源只能被一个进程所获取。 94 | - **占有和等待:** 进程所获得的资源在未使用完毕之前(包括阻塞时),不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。 95 | - **不可抢占:** 已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显示地释放。 96 | - **环路等待:** 有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 97 | 98 | 主要有以下四种处理方法: 99 | 100 | - **鸵鸟策略:** 不才与任何措施,假装没有发生。 101 | - **死锁检测与死锁恢复:** 不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。每种类型一个资源的死锁检测算法是通过有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到了死锁的发生。还有一种是每种资源多个资源的死锁检测。死锁也可以通过抢占、回滚、杀死进程等方式恢复。 102 | - **死锁预防:** 在程序运行之前预防发生死锁。有多种方式,比如破坏互斥条件、破坏占有和等待条件、破坏不可抢占条件和破坏环路等待等。 103 | - **死锁避免:** 基本思想是动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说,如果存在一个安全序列,那么系统就处于安全状态。资源分配图算法和银行家算法是两种经典地死锁避免的算法,其可以确保系统始终处于安全状态。 104 | 105 | **9、Linux中的文件描述符与打开文件之间的关系。** 106 | 107 | 在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符是内核为了高效管理已被打开的的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都是通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。 108 | 109 | 文件描述符是有限制的。主要因为文件描述符是系统的一个重要资源,虽然说系统内存有多少就可以打开多少的文件描述符,但是在实际过程中内核是会做相应的处理的,一般是最大文件数会是系统内存的10%(以KB来计算),这个称之为系统级限制。与此同时,内核为了不让某一个进程消耗掉所有的文件资源,其也会对单个进程最大打开文件数做默认值处理,一般默认值为1024,这个称之为用户级限制。 110 | 111 | 此外,内核也会对所有打开的文件维护有一个系统级的描述符表格,成为打开文件表,并将表格中各条目称为打开文件句柄。 112 | 113 | 由于进程级文件描述符表的存在,不同的进程中会出现相同的我呢见描述符,它们可以指向同一个文件,也可能指向不同的文件。两个不同的文件描述符,若指向同一个打开文件句柄,将共享同一个文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(调用read()、write()或lseek()所致),那么从另一个描述符中也会观察到变化,无论这两个文件描述符是否属于不同进程,还是同一个进程,情况都是如此。 114 | 115 | **10、进程间同步与互斥的区别。** 116 | 117 | 互斥:指某一个资源同时只允许一个访问者对其进行访问,具有唯一性和排他性。但是互斥无法限制访问者对资源的访问顺序,即访问是无序的。 118 | 119 | 同步:是指在互斥的基础上(大多数情况下),通过其它机制实现访问者对资源的有序访问。大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。 120 | 121 | 同步体现的是一种协作性,互斥体现的是排他性。 122 | 123 | **11、为什么要引入虚拟内存。** 124 | 125 | 为了更加有效地管理内存并且少出错,操作系统推出了虚拟内存(VM)。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。 126 | 127 | 虚拟内存提供了三个重要的能力: 128 | 129 | - 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。 130 | - 它为每个进程提供了一致的地址空间,从而简化了内存管理。 131 | - 它保护了每个进程的地址空间不被其他进程破坏。 132 | 133 | 虚拟内存的大小有两点限制条件。 134 | 135 | - 虚拟内存<=内存+外存容量之和。 136 | - 虚拟内存<=计算机地址位数所能容纳的(2^计算机位数)。 137 | 138 | 虚拟内存允许执行进程不必完全在内存中。虚拟内存的基本思想是:每个进程拥有独立的地址空间,这个空间被分为大小相等的多个块,称为页(page),每个页都是一段连续的地址。这些页被映射到物理内存(页表),但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻进行必要的映射;当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令。这样,**对于进程而言,逻辑上似乎有很大的内存空间,实际上其中一部分对应物理内存上的一块(称为帧,通常页和帧大小相等),还有一些没有加载在内存中的对应在硬盘上。**如下图所示。 139 | 140 | ![Y4tFTH.png](https://s1.ax1x.com/2020/05/19/Y4tFTH.png) 141 | 142 | 从上图可知,虚拟内存实际比物理内存要大,当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如上图的0,1,2)。如果虚拟内存的页并不存在于物理内存中(如上图的3,4),会产生缺页中断,从硬盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将硬盘中的页换出来。 143 | 144 | **12、页面置换算法。** 145 | 146 | **FIFO先进先出算法:** 在操作系统中经常被用到,比如作业调度。 147 | 148 | **LRU最近最少使用算法:** 根据使用时间到现在的长短来判断。 149 | 150 | **LFU最少使用次数算法:** 根据使用次数来判断。 151 | 152 | **OPT最优置换算法:** 理论的最优,当然,这是理论情况。就是要保证置换出去的是不再被使用的页,或者是在实际内存中最晚使用的算法。 153 | 154 | **13、颠簸。** 155 | 156 | 颠簸本质上是指**频繁的页调度行为**,具体来讲,进程发生缺页中断,这时,必须置换某一页。然而,其他所有的页都在使用,它置换一个页,但又立即再次需要这个页。因此,会不断产生缺页中断,导致整个系统的效率急剧下降,这种现象称之为颠簸(抖动)。 157 | 158 | 内存颠簸的解决策略包括: 159 | 160 | - 如果是因为页面替换策略失误,可以修改替换算法来解决这个问题。 161 | - 因为是运行的程序太多,造成程序无法同时将所有频繁访问的页面调入内存,则要降低程序的数量。 162 | - 否则,还剩下两个办法:终止该进程或增加物理内存容量。 163 | 164 | **14、fork()函数。** 165 | 166 | 在Linux系统中,创建子线程的方法是使用系统调用fork()函数。fork()函数用于从一个已存在的进程内创建一个新的进程,新的进程称为“子进程”,相应地称创建子进程的进程为“父进程”。使用fork()函数得到的子进程是父进程的复制品,子进程完全复制了父进程的资源,包括进程上下文,代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录等信息。而子进程与父进程的区别有进程号、资源使用情况和计时器等。 167 | 168 | 由于复制父进程的资源需要大量的操作,十分浪费时间与系统资源,因此Linux内核采取了写时拷贝技术(只有进程空间的各段内容要发生变化时,才会将父进程的内容复制给子进程)来提高效率。 169 | 170 | 由于子进程几乎对父进程完全复制,因此父子进程会同时运行同一个程序。因此我们需要某种方式来区分父子进程。区分父子进程常见的方法是查看fork()函数的返回值或者区分父子进程的PID。 171 | 172 | 父子进程的运行先后顺序是完全随机的(取决于系统的调度)。 173 | 174 | 多线程与fork()的协作性很差。fork()一般不会在多线程程序中调用,因为**Linux的fork()只克隆当前线程的thread of control,不克隆其他线程。fork()之后,除了当前线程之外,其他线程都消失了。**也就是不能一下子fork()出一个和父进程一样的多线程子进程。 175 | 176 | 弥有,2019年9月 177 | [EOF] 178 | -------------------------------------------------------------------------------- /interviews/2020届秋招面试题总结-网络篇.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——网络篇 2 | 3 | **1、http1.0和http1.1的区别。** 4 | 5 | 主要是如下的8点: 6 | 7 | - 可拓展性。 8 | - 缓存。 9 | - **带宽优化,带来了分块传输。** 10 | - **长连接,HTTP1.1支持长连接(默认开启Connect: keep-alive)和请求的流水线处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。** 11 | - 消息传递。 12 | - Host头域。 13 | - 错误提示。 14 | - 内容协商。 15 | 16 | **2、TCP三次握手和四次挥手的流程,为什么断开连接要四次,如果握手只有两次,会出现什么。** 17 | 18 | **三次握手:** 19 | 20 | ![三次握手](https://s1.ax1x.com/2020/05/18/YhwdED.png) 21 | 22 | 主要流程为: 23 | 24 | - 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SEND 状态。 25 | 26 | - 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端进入 SYN_RCVD 状态。 27 | 28 | - 第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手,即可以开始数据传输。 29 | 30 | 为什么 TCP 连接需要三次握手,两次不可以么,为什么? 31 | 32 | 为了防止**已失效的连接请求**报文突然又传送到了服务端,因而产生错误。 33 | 34 | - 客户端发出的连接请求报文并未丢失,而是在某个网络节点长时间滞留了,以致延误到链接释放以后的某个时间才到达 Server 。 35 | - 若不采用“三次握手”,那么只要 Server 发出确认数据包,新的连接就建立了。由于 Client 此时并未发出建立连接的请求,所以其不会理睬 Server 的确认,也不与 Server 通信;而这时 Server 一直在等待 Client 的请求,这样 Server 就白白浪费了一定的资源。 36 | - 若采用“三次握手”,在这种情况下,由于 Server 端没有收到来自客户端的确认,则就会知道 Client 并没有要求建立请求,就不会建立连接。 37 | 38 | **四次挥手:** 39 | 40 | ![四次挥手](https://s1.ax1x.com/2020/05/19/Y41xAI.png) 41 | 42 | 主要流程为: 43 | 44 | - 第一次挥手(FIN=1,seq=a),发送完毕后,客户端进入 FIN_WAIT_1 状态。 45 | - 第二次挥手(ACK=1,ACKnum=a+1),发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态。 46 | - 第三次挥手(FIN=1,seq=b),发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。 47 | - 第四次挥手(ACK=1,ACKnum=b+1),客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。 48 | 49 | 为什么需要四次挥手:因为TCP连接是全双工的网络协议,允许同时通信的双方同时进行数据的收发,同样也允许收发两个方向的连接被独立关闭,以避免client数据发送完毕,向server发送FIN关闭连接,而server还有发送到client的数据没有发送完毕的情况。所以关闭TCP连接需要进行四次握手,每次关闭一个方向上的连接需要FIN和ACK两次握手。 50 | 51 | 握手过程如果只有两次,可能会出现已失效的连接请求报文突然又传送到了服务端,因而产生错误。 52 | 53 | 在三次握手过程中,为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。 54 | 55 | **3、TIME_WAIT和CLOSE_WAIT的区别。** 56 | 57 | TIME_WAIT表示主动关闭,CLOSE_WAIT表示被动关闭。 58 | 59 | TCP协议规定,对于已经建立的连接,网络双方要进行四次挥手才能断开连接,如果缺少了其中某个步骤,将会使连接处于假死状态,连接本身占用的资源不会被释放。网络服务器程序要同时管理大量连接,所以很有必要保证无用连接完全断开,否则大量僵死的连接会浪费许多服务器资源。在众多TCP状态中,最值得注意的状态有两个:CLOSE_WAIT和TIME_WAIT。 60 | 61 | - TIME_WAIT 是主动关闭链接时形成的,等待2MSL时间,约4分钟。一方面是为了把原来的连接里面的重复数据包都已经在网络中消逝。避免老的连接的数据影响新建立的连接(新老连接的IP和端口号相同,新的被称为老的连接到化身)。 另一方面, 假如客户端回复的ACK丢失,服务端会重发FIN,客户端此时还能接收到FIN,还能再回复一个ACK(此时time_wait会重新计时)(MSL是指一个包的最大存活时间,一般是两分钟。) 62 | - 另一种对于TIME_WAIT的解释:如果没有TIME_WEIT这个等待,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的 TCP 报文可能与新 TCP 连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的 TCP 连接的活跃报文全部死翘翘,2MSL 时间可以满足这个需求(尽管非常保守)! 63 | - CLOSE_WAIT是被动关闭连接是形成的。根据TCP状态机,服务器端收到客户端发送的FIN,则按照TCP实现发送ACK,因此进入CLOSE_WAIT状态。但如果服务器端不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。此时,可能是系统忙于处理读、写操作,而未将已收到FIN的连接,进行close。此时,recv/read已收到FIN的连接socket,会返回0。 64 | 65 | 参考链接:[TIME_WAIT和CLOSE_WAIT状态区别](https://blog.csdn.net/kobejayandy/article/details/17655739) 66 | 67 | **4、说说你知道的几种HTTP响应码,比如200,302和404。** 68 | 69 | HTTP响应码主要分为五种: 70 | 71 | - 1XX:请求处理中,请求已被接收,正在处理。 72 | - 2XX:请求成功,请求被成功处理。比如200,OK,表示客户端请求成功。 73 | - 3XX:重定向,要完成请求必须进行进一步处理。比如301,Moved Permanently,永久重定向,使用域名跳转;302,Found,临时重定向,未登录的用户访问用户中心重定向到登陆界面。 74 | - 4XX:客户端错误,请求不合符。比如400,Bad Request,客户端请求有语法错误,不能被服务器所理解;401,Unauthrized,请求未经授权,这个状态代码必须和WWW-Authenticate 报头域一起使用;403,Forbidden,服务器收到请求,但是拒绝提供服务;404,Not Found,请求资源不存在,输入了错误的URL。 75 | - 5XX:服务器端错误,服务器不能处理合法请求。比如500,Internal Servel Error,服务器发生不可预期的错误;503,Server Unavailable,服务器当前不能处理客户端的请求,一段时间后可能恢复正常。 76 | 77 | **5、当你用浏览器打开一个链接(如: )的时候,计算机做了哪些工作步骤。** 78 | 79 | 计算机的工作主要是将域名解析成ip地址。 80 | 81 | 主机解析域名的顺序依次是: 82 | 83 | - 浏览器缓存。 84 | - 找本机的hosts文件。 85 | - 路由缓存。 86 | - 找DNS服务器(本地域名、顶级域名、根域名),主要分为递归查询和迭代查询。 87 | 88 | 需要注意的是: 89 | 90 | - 主机向本地域名服务器的查询一般都是采用递归查询。所谓递归查询就是:如果主机所询问的本地域名服务器不知道被查询的域名的IP地址,那么本地域名服务器就以DNS客户的身份,向其它根域名服务器继续发出查询请求报文(即替主机继续查询),而不是让主机自己进行下一步查询。因此,递归查询返回的查询结果或者是所要查询的IP地址,或者是报错,表示无法查询到所需的IP地址。 91 | - 本地域名服务器向根域名服务器的查询的迭代查询。迭代查询的特点:当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的IP地址,要么告诉本地服务器:“你下一步应当向哪一个域名服务器进行查询”。然后让本地服务器进行后续的查询。根域名服务器通常是把自己知道的顶级域名服务器的IP地址告诉本地域名服务器,让本地域名服务器再向顶级域名服务器查询。顶级域名服务器在收到本地域名服务器的查询请求后,要么给出所要查询的IP地址,要么告诉本地服务器下一步应当向哪一个权限域名服务器进行查询。 92 | 93 | 递归和迭代查询示意图如下: 94 | 95 | ![YhsJ00.png](https://s1.ax1x.com/2020/05/18/YhsJ00.png) 96 | 97 | 参考文章:[DNS原理总结及其解析过程详解(递归查询+迭代查询)](https://blog.csdn.net/wyq_tc25/article/details/51679520) 98 | 99 | **6、TCP/IP如何保证可靠性,说说TCP头的结构。** 100 | 101 | TCP提供一种面向连接的、可靠的字节流服务。其中,面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个TCP连接。 102 | 103 | 对于可靠性,TCP通过以下方式进行保证: 104 | 105 | - **数据包校验:** 目的是检验数据在传输过程中的变化,若检验包有错,则丢弃报文段并且不给出响应,这时TCP发送数据超时后会重发数据。 106 | - **序号机制(序号、确认号):** 确保了数据是按序、完整到达。 107 | - **对失序数据包重排序:** 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层。TCP传输时将每个字节的数据都进行了编号,这就是序列号。TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。 108 | - **丢弃重复数据:** 对于重复数据,能够丢弃重复数据。这是在超时重传情况下可能发生,判断依据就是序列号。 109 | - **应答机制:** 当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。 110 | - **超时重发:** 当TCP发出一个段后,他启动一个定时器,等待目的端确认收到这个报文段,如果不能及时收到一个确认,将重发这个报文段。 111 | - **流量控制:** TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。在TCP协议的报头信息当中,有一个16位字段的窗口大小。在介绍这个窗口大小时我们知道,窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。TCP使用的流量控制协议是可变大小的**滑动窗口协议**。 112 | - **拥塞控制:** TCP传输过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题,网络可能在开始的时候就很拥堵,如果给网络再扔出大量数据,那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包,以及大量的超时重传,严重影响传输。所以TCP引入了**慢启动**的机制,在刚开始发送数据时,先发送少量的数据探路,探清当前的网络状态如何,再决定多大的速度进行传输。这个时候就引入了一个叫做拥塞窗口的概念。**拥塞窗口是发送端根据网络拥塞情况确定的窗口值。** 在刚刚开始发送报文的时候,**先把拥塞窗口设置1,每经过一个传输轮次(把拥塞窗口所允许发送的报文段都连续发送出去,并收到接受方的确认应答),拥塞窗口就加倍,** 这个增长速度是指数级别的,为了控制拥塞窗口的增长,不能使拥塞窗口单纯的加倍,设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。慢开始的“慢”并不是指拥塞窗口的增长速率慢,而是指在TCP开始发送报文段时先设置拥塞窗口=1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大拥塞窗口。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取最小的值作为实际发送的窗口。一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为 1。 113 | 114 | 参考文章:[网络基础:TCP协议-如何保证传输可靠性](https://blog.csdn.net/liuchenxia8/article/details/80428157) 115 | 116 | **7、如何避免浏览器缓存。** 117 | 118 | 主要有以下几种方法: 119 | 120 | - Cache-Control/Pragma这个HTTP Head字段用于指定所有缓存机制在整个请求/响应链中必须服从的指令,如果知道该页面是否为缓存,不仅可以控制浏览器,还可以控制和HTTP协议相关的缓存或代理服务器。 121 | - Expires通常的使用格式是Expires:Sat,25Feb201212:22:17GMT,后面跟着一个日期和时间,超过这个时间值后,缓存的内容将失效,也就是浏览器在发出请求之前检查这个页面的这个字段,看该页面是否已经过期了,过期了就重新向服务器发起请求。 122 | - Last-Modified/EtagLast-Modified字段一般用于表示一个服务器上的资源的最后修改时间,资源可以是静态(静态内容自动加上Last-Modified字段)或者动态的内容(如Servlet提供了一个getLastModified方法用于检查某个动态内容是否已经更新),通过这个最后修改时间可以判断当前请求的资源是否是最新的。 123 | - 在每次请求后面加上一个随机数,这样保证每次发送的都是不一样的请求。比如url = "http://localhost:8080/test/login/index"?r=Math.random(); 124 | 125 | 参考连接:[面试官:你了解过浏览器缓存机制吗?](https://mp.weixin.qq.com/s?__biz=MzIzMzgxOTQ5NA==&mid=2247487949&idx=1&sn=9d89ed1d4b332f6bd09dd12d8cc0752e&chksm=e8fe8dc4df8904d2109b7880872323feb75a7e219939255bfc9c4181179fa87c621fd3784520&scene=0&xtrack=1&key=313f257bb4a0297bb5bad9fa9f806ecca1d3a07c7e8c255741fdca2de1cb284d62f25767a9f71846cefc38952d389894acd061d7404a5fb55c0a5128e29114692001c448ed9dd122aba594e6f2b47d1a&ascene=1&uin=MjQ3MzkwMTc2Mw%3D%3D&devicetype=Windows+10&version=62060833&lang=zh_CN&pass_ticket=x8Wl3BnnSoBme0p%2BQC6c6ydRcCey73sPZ6v6hAXhRRpQY85TYPB4NLf4B0eFrhZj) 126 | 127 | **8、如何理解HTTP协议的无状态性。** 128 | 129 | 无状态,是指协议对于事务处理没有记忆功能。HTTP 是一个无状态协议,这意味着每个请求都是独立的,Keep-Alive 没能改变这个结果。无状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就很快。 130 | 131 | 无状态,更容易做服务的扩容,支撑更大的访问量。 132 | 133 | **9、简述Http请求中get和post的区别及数据包格式。** 134 | 135 | GET:对服务器资源的简单请求,把参数包含在URL中。 136 | 137 | POST:用于发送包含用户提交数据的请求,通过request body传递阐述。 138 | 139 | 另外,对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 140 | 141 | **10、HTTP中有哪些method。** 142 | 143 | GET:对服务器资源的简单请求。 144 | 145 | HEAD:类似于GET,但服务器在响应中只返回首部,不返回实体的主体部分。 146 | 147 | PUT:向指定资源位置上传其最新内容,是幂等的。 148 | 149 | POST:用于发送包含用户提交数据的请求,是不幂等的,当我们多次发出同样的POST请求后,其结果是创建出了若干个资源。 150 | 151 | TRACE:发送一个请求副本,以跟踪其处理进程。 152 | 153 | OPTIONS:返回所有可用方法,检查服务器支持哪些方法。DELETE:请求服务器删除请求URL指定的资源。 154 | 155 | 另外,对幂等理解,幂等是数学的一个用于,对于单个输入或者无输入的运算方法,如果每次都是同样的结果,则称其是幂等的。方法GET,HEAD,PUT,DELETE都有这种性质。POST方法不是幂等的。 156 | 157 | **11、简述HTTP请求的报文格式。** 158 | 159 | HTTP的请求报文如图,由四部分构成: 160 | 161 | ![YhsDXR.jpg](https://s1.ax1x.com/2020/05/18/YhsDXR.jpg) 162 | 163 | - 请求行,用来说明请求类型、要访问的资源以及所使用的HTTP版本。例如 GET /books/java.html HTTP/1.1 164 | - 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息,第二行起为请求头部。 165 | - 空行,请求头部后面的空行是必须的。 166 | - 请求数据,也叫主体,可以添加任意的其他数据。 167 | 168 | HTTP的响应报文如图,也是由四部分构成: 169 | 170 | ![Yhsy0x.jpg](https://s1.ax1x.com/2020/05/18/Yhsy0x.jpg) 171 | 172 | - 状态行,由HTTP协议版本号、状态码、状态消息三部分构成。 173 | - 消息报文,用来说明客户端要使用的一些附加消息。 174 | - 空行,消息报文后面的空行是必须的。 175 | - 响应报文,服务器返回给客户端的文本信息。 176 | 177 | **12、HTTP的长连接是什么意思。** 178 | 179 | HTTP1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求,此外,由于大多数网页的流量都比较小,一次TCP连接很少能通过slow-start区,不利于提高带宽利用率。 180 | 181 | HTTP 1.1支持**长连接(PersistentConnection)和请求的流水线(Pipelining)** 处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。例如:一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。 182 | 183 | HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间(请求的流水线)。 184 | 185 | 在HTTP/1.0中,要建立长连接,可以在请求消息中包含Connection: Keep-Alive头域,如果服务器愿意维持这条连接,在响应消息中也会包含一个Connection: Keep-Alive的头域。同时,可以加入一些指令描述该长连接的属性,如max,timeout等。 186 | 187 | **13、HTTPS的加密方式是什么,讲讲整个加密解密流程。** 188 | 189 | HTTP直接通过明文在浏览器和服务器之间传递消息,容易被监听抓取到通信内容。 190 | 191 | HTTPS采用对称加密和非对称加密结合的方式来进行通信,HTTPS不是应用层的新协议,而是HTTP通信接口用SSL/TLS来加强加密和认证机制。 192 | 193 | 整个加密流程如下: 194 | 195 | ![YhsonI.png](https://s1.ax1x.com/2020/05/18/YhsonI.png) 196 | 197 | 需要注意的是,第一次服务器向客户端传输证书的具体过程为: 198 | 199 | - 把公钥以及服务器的个人信息通过Hash算法生成信息摘要; 200 | - 为了防止信息摘要被人调换,服务器还会用CA提供的私钥对信息摘要进行加密来形成数字签名; 201 | - 最后还会把原来没Hash算法之前的个人信息以及公钥 和 数字签名合并在一起,形成数字证书。 202 | 203 | 当客户端拿到这份数字证书之后,就会用CA提供的公钥来对数字证书里面的数字签名进行解密来得到信息摘要,然后对数字证书里服务器的公钥以及个人信息进行Hash得到另外一份信息摘要。最后把两份信息摘要进行对比,如果一样,则证明这个人是服务器,否则就不是。 204 | 205 | 整个的流程如下: 206 | 207 | 1. 客户端向服务器发起HTTPS请求,连接到服务器的443端口。 208 | 2. 服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。 209 | 3. 服务器将自己的公钥发送给客户端。 210 | 4. 客户端收到服务器端的公钥之后,会对公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。严格的说,这里应该是验证服务器发送的数字证书的合法性,关于客户端如何验证数字证书的合法性,下文会进行说明。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,我们将该密钥称之为client key,即客户端密钥,这样在概念上和服务器端的密钥容易进行区分。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,至此,HTTPS中的第一次HTTP请求结束。 211 | 5. 客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。 212 | 6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。 213 | 7. 然后服务器将加密后的密文发送给客户端。 214 | 8. 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束,整个HTTPS传输完成。 215 | 216 | 一共是两次非对称加密+一次对称加密。 217 | 218 | **14、HTTPS握手。** 219 | 220 | HTTPS有四次握手,具体如上图所示。 221 | 222 | **15、什么是分块传输。** 223 | 224 | 通常情况下,HTTP的响应消息体 message body 是作为整包发送到客户端的,用头『Content-Length』 来表示消息体的长度, 这个长度对客户端非常重要,因为对于持久连接TCP并不会在请求完立马结束,而是可以发送多次请求/响应,客户端需要知道哪个位置才是响应消息的结束,以及后续响应的开始,因此Content-Length显得尤为重要,服务端必须精确地告诉客户端 message body 的长度是多少, 如果Content-Length 比实际返回的长度短,那么就会造成内容截断,如果比实体内容长,客户端就一直处于pendding状态,直到所有的 message body 都返回了请求才结束。 225 | 226 | 分块传输编码:它把数据分解为一系列的数据块,并以多个块发送给客户端,服务器发送数据时不再需要预先告诉客户端发送内容的总大小,只需在响应头里面添加Transfer-Encoding: chunked,以此来告诉浏览器我使用的是分块传输编码,这样就不需要 Content-Length 了。 227 | 228 | 分块编码传输优点: 229 | 230 | - HTTP分块传输编码允许服务器为动态生成的内容维持HTTP持久链接。通常,持久链接需要服务器在开始发送消息体前发送Content-Length消息头字段,但是对于动态生成的内容来说,在内容创建完之前是不可知的。 231 | - 分块传输编码允许服务器在最后发送消息头字段。对于那些头字段值在内容被生成之前无法知道的情形非常重要,例如消息的内容要使用散列进行签名,散列的结果通过HTTP消息头字段进行传输。没有分块传输编码时,服务器必须缓冲内容直到完成后计算头字段的值并在发送内容前发送这些头字段的值。 232 | - HTTP服务器有时使用压缩(gzip)以缩短传输花费的时间。分块传输编码可以用来分隔压缩对象的多个部分。在这种情况下,块不是分别压缩的,而是整个负载进行压缩,压缩的输出使用本文描述的方案进行分块传输。在压缩的情形中,分块编码有利于一边进行压缩一边发送数据,而不是先完成压缩过程以得知压缩后数据的大小。 233 | 234 | 参考连接:[HTTP中的分块传输编码是怎么回事?](https://foofish.net/http-transfer-encoding.html) 235 | 236 | **16、Session和cookie的区别。** 237 | 238 | Session和cookie都是实现对话管理的方案。主要区别在于: 239 | 240 | - Session在服务端,Cookie存储在客户端。 241 | - Session的运行依赖Session ID,而Session ID是存在Cookie中的,也就是说,如果浏览器禁用了Cookie,同时Session也会失效,但是,可以通过其他方式实现,比如在url参数中传递Session ID。 242 | - Tomcat中的Session是存在服务器内存中,不过也可以通过特殊的方式做持久化处理(memcache,redis),方便Session共享。另外,PHP中的Session是存在文件中的。 243 | - cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。 244 | 245 | 参考连接:[Session是怎么实现的?存储在哪里?](https://blog.csdn.net/qq_15096707/article/details/74012116#java中的session存储) 246 | 247 | **17、用户在浏览器输入一个URL并回车,这个过程涉及到哪些网络协议,请具体描述。** 248 | 249 | 浏览器输入一个URL并回车: 250 | 251 | 1. 首先进行域名解析,浏览器搜索自己的DNS缓存,缓存中维护一张域名与IP地址的对应表。若没有,则搜索操作系统的DNS缓存;若没有,则将域名发送至本地域名服务器(递归查询方式),本地域名服务器查询自己的DNS缓存,查找成功则返回结果,否则,本地的DNS服务器向根域名服务器发出查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。 252 | 2. 得到IP地址以后,浏览器就要与服务器建立一个HTTP连接,因此要用到HTTP协议。HTTP生成一个GET请求报文。 253 | 3. 接下来到了传输层,选择传输协议,TCP或者UDP,TCP是可靠的传输控制协议,对HTTP请求进行封装,加入了端口号等信息。 254 | 4. 然后到了网络层,通过IP协议将IP地址封装为IP数据报;然后此时会用到ARP协议,主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址,找到目的MAC地址。 255 | 5. 接下来到了数据链路层,把网络层交下来的IP数据报添加首部和尾部,封装为MAC帧,现在根据目的mac开始建立TCP连接,三次握手,接收端在收到物理层上交的比特流后,根据首尾的标记,识别帧的开始和结束,将中间的数据部分上交给网络层,然后层层向上传递到应用层。 256 | 6. 服务器响应请求并请求客户端要的资源,传回给客户端。 257 | 7. 断开TCP连接,浏览器对页面进行渲染呈现给客户端。 258 | 259 | **18、一致性哈希问题。** 260 | 261 | 在解决分布式系统中负载均衡的问题时候可以使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡的作用。但是普通的余数hash(hash(比如用户id)%服务器机器数)算法伸缩性很差,当新增或者下线服务器机器时候,用户id与服务器的映射关系会大量失效。一致性hash则利用hash环对其进行了改进。 262 | 263 | 一致性hash主要是建立起一个在[0,2^32-1]分布的哈希环。根据资源的Key的Hash值(也是分布为[0,,2^32-1])H1,在环上顺时针的找到离 H1最近(第一个大于或等于 H1)的一个节点,就建立了资源和节点的映射关系。 264 | 265 | 另外,当一致性哈希算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜问题。为了解决这个问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。 266 | 267 | 具体信息建议直接去看文章:[深入浅出一致性Hash原理](https://www.jianshu.com/p/e968c081f563)和[一致性哈希(hash)算法](https://www.cnblogs.com/study-everyday/p/8629100.html) 268 | 269 | **19、ETag的含义和作用。** 270 | 271 | ETag是Entity Tag的缩写,中文译过来就是实体标签的意思。在HTTP1.1协议中的其实就是请求Head中的一个属性。比如 272 | 273 | ```javascript 274 | HTTP/1.1 200 OK 275 | Date: Mon, 23 May 2005 22:38:34 GMT 276 | Content-Type: text/html; charset=UTF-8 277 | Content-Encoding: UTF-8 278 | Content-Length: 138 279 | Last-Modified: Wed, 08 Jan 2019 23:11:55 GMT 280 | Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux) 281 | ETag: "3f80f-1b6-3e1cb03b" 282 | Accept-Ranges: bytes 283 | Connection: close 284 | ``` 285 | 286 | ETag是HTTP1.1中才加入的一个属性,类似于资源指纹,用来帮助服务器控制Web端的缓存验证。它的原理是这样的,当浏览器请求服务器的某项资源(A)时,服务器根据A算出一个哈希值(3f80f-1b6-3e1cb03b)并通过ETag返回给浏览器,浏览器把“3f80f-1b6-3e1cb03b”和A同时缓存在本地,当下次再次向服务器请求A时,会通过类似If-None-Match: "3f80f-1b6-3e1cb03b"的请求头把Etag信息发送给服务器,服务器再次计算A的哈希值并和浏览器请求的值作比较,如果发现A发生了变化就把A返回给浏览器(200),如果发现A没有变化,就给浏览器返回一个304未修改。这样通过控制浏览器端的缓存,可以节省服务器的带宽,因为服务器不需要每次都把全量数据返回给客户端。 287 | 288 | 注意的是,HTTP并没有指定如何生成ETag,哈希是比较理想的选择。但在一些服务器情况下,并不会用哈希来计算ETag,因为这会严重浪费服务器资源,所以很多时候通过资源的版本或者修改时间来生成ETag。如果通过资源修改时间来生成ETag,那么效果和HTTP协议里面的另外一个控制属性(Last-Modified)就雷同了,使用 Last-Modified 的问题在于它的精度在秒(s)的级别,比较适合不太敏感的静态资源。 289 | 290 | **20、转发和重定向的区别。** 291 | 292 | 转发是指RequestDispatcher.forward,而重定向是指HttpServletResponse.sendRedirect。主要区别如下: 293 | 294 | - forword方法只能将请求转发给同一个WEB应用中的组件,而sendRedirect方法不仅可以重定向到当前应用程序的其他资源,还可以重定向到同一个站点上的其他应用程序中的资源。 295 | - forword方法的请求转发过程结束后,浏览器地址栏保持初始的URL地址不变;而sendRedirect方法重定向的访问过程结束后,浏览器地址栏中显示的URL会发现改变,由初始的URL地址变成重定向的目标URL。 296 | - forword方法服务器端内部将请求转发给另外一个资源,浏览器只知道发出了请求并得到了响应结果,并不知道在服务器程序内部发生了转发行为;而sendRedirect方法对浏览器的请求直接作出响应,响应的结果就是告诉浏览器去重新发出对另外一个URL的访问请求。 297 | - forword方法的调用者与被调用者之间共享相同的request对象和response对象,它们属于同一个访问请求和响应过程;而sendRedirect方法调用者与被调用者使用各自的request对象和response对象,它们属于两个独立的访问请求和响应过程。 298 | 299 | 注意: 300 | 301 | - 转发比重定向快,因为重定向需要经过客户端,而转发没有。 302 | - 使用重定向不太方便的地方是,使用它无法将值轻松地传递给目标页面。而采用转发,则可以简单地将属性添加到Model,使得目标视图可以轻松访问。 303 | 304 | 重定向的使用场景:当提交产品表单的时候,执行保存的方法将会被调用,并执行相应的动作;这在一个真实的应用程序中,很有可能将表单中的所有产品信息加入到数据库中。但是如果在提交表单后,重新加载页面,执行保存的方法就很有可能再次被调用。同样的产品信息就将可能再次被添加,为了避免这种情况,提交表单后,你可以将用户重定向到一个不同的页面,这样的话,这个网页任意重新加载都没有副作用。 305 | 306 | **21、HTTP2.0带来的变化。** 307 | 308 | HTTP2.0和HTTP1.x相比的新特性为: 309 | 310 | - **新的二进制格式**,HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。 311 | - **多路复用**,即连接共享,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。 312 | - **多个请求可以同时在一个连接上并行执行**,某个请求任务耗时严重,不会影响到其他连接的正常执行。这块和HTTP1.1的长连接有区别,长连接是串行化执行多个请求。 313 | - **首部压缩**,HTTP1.1不支持header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。 314 | - **服务器推送**,当我们对支持HTTP2.0的web server请求数据的时候,服务器会顺便把一些客户端需要的资源(比如css、js文件)一起推送到客户端,免得客户端再次创建连接发送请求到服务器端获取。**这种方式非常合适加载静态资源**。还没有收到浏览器的请求,服务器就把各种资源推送给浏览器了。 315 | 316 | **22、SSH相关。** 317 | 318 | SSH是一种协议标准,其目的是实现远程登录以及其他安全网络服务。SSH和telnet、ftp等协议主要的区别在于安全性。 319 | 320 | SSH中用了非对称加密方式。但与HTTPS可以通过CA认证不同,SSH的公钥和密钥都是自己生成的,没法公证,只能通过client端自己对公钥进行确认。一般是在第一次登陆的过程时实现。具体过程如下。 321 | 322 | ![YhsL4S.png](https://s1.ax1x.com/2020/05/18/YhsL4S.png) 323 | 324 | **23、网络模型。** 325 | 326 | ![七层网络模型](https://s1.ax1x.com/2020/05/18/YhySun.png) 327 | 328 | 自上而下分别是: 329 | 330 | - 应用层(数据):确定进程之间通信的性质以满足用户需要以及提供网络与用户应用。 331 | - 表示层(数据):主要解决用户信息的语法表示问题,如加密解密。在表示层进行代码/编码转换。 332 | - 会话层(数据):提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制,如服务器验证用户登录便是由会话层完成的。在会话层封装会话控制参数。 333 | - 传输层(段):实现网络不同主机上用户进程之间的数据通信,可靠与不可靠的传输,传输层的错误检测,流量控制等。在传输层封装传输控制。 334 | - 网络层(包):提供逻辑地址(IP)、选路,数据从源端到目的端的传输。在网络层加上逻辑寻址地址。 335 | - 数据链路层(帧):将上层数据封装成帧,用MAC地址访问媒介,错误检测与修正。在数据链路层封装基于MAC的信息。 336 | - 物理层(比特流):设备之间比特流的传输,物理接口,电气特性等。在物理层连接到线缆系统进行实际传递。 337 | 338 | **24、网络层的ARP协议工作原理。** 339 | 340 | 网络层的 ARP 协议完成了 IP 地址与物理地址的映射。 341 | 342 | 首先,每台主机都会在自己的 ARP 缓冲区中建立一个 ARP 列表,以表示 IP 地址和 MAC 地址的对应关系。 343 | 344 | 当源主机需要将一个数据包要发送到目的主机时,会首先检查自己 ARP 列表中是否存在该 IP 地址对应的 MAC 地址: 345 | 346 | - 如果有,就直接将数据包发送到这个 MAC 地址。 347 | - 如果没有,就向本地网段发起一个ARP请求的广播包,查询此目的主机对应的 MAC 地址。 348 | 349 | **25、对于TCP连接,客户端不断进行请求链接会怎么样?** 350 | 351 | 服务器端准备为每个请求创建一个链接,并向其发送确认报文,然后等待客户端进行确认后创建。如果此时客户端一直不确认,会造成 SYN 攻击,即:SYN 攻击,英文为 SYN Flood ,是一种典型的 DoS/DDoS 攻击。 352 | 353 | - 客户端向服务端发送请求连接数据包。 354 | - 服务端向客户端发送确认数据包。 355 | - 客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认。 356 | 357 | 这时候服务器上有大量的半连接状态,特别是源IP地址是随机的,基本可以断定是一次SYN攻击。 358 | 359 | 对于SYN攻击,只能预防,没有彻底根治的办法,除非不使用 TCP 。方法主要如下: 360 | 361 | - 限制同时打开SYN半链接的数目。 362 | - 缩短SYN半链接的超时时间。 363 | - 关闭不必要的服务。 364 | - 增加半链接数据。 365 | - 过滤网关防护。 366 | 367 | **26、TCP和UDP的区别。** 368 | 369 | TCP和UDP都属于传输层协议,它们之间的区别在于: 370 | 371 | - TCP 是面向连接的;UDP 是无连接的。 372 | - TCP 是可靠的;UDP 是不可靠的。 373 | - TCP 只支持点对点通信;UDP 支持一对一、一对多、多对一、多对多的通信模式。 374 | - TCP 是面向字节流的;UDP 是面向报文的。 375 | - TCP 有拥塞控制机制;UDP 没有拥塞控制,适合媒体通信。 376 | - TCP 首部开销(20 个字节),比 UDP 的首部开销(8 个字节)要大。 377 | 378 | **27、TCP中RST标志位的作用。** 379 | 380 | TCP报文中一共有6个标志位,分别为:URG/ACK/PSH/RST/SYN/FIN。 381 | 382 | - SYN:TCP三次握手时,如果A是发起端,则A就对服务器发送一个SYN报文,表示想要建立连接。 383 | - ACK:收到数据或者请求后发送响应时发送ACK报文。 384 | - RST:关闭异常连接。 385 | - FIN:TCP四次挥手时,表示关闭连接。 386 | - PSH:发送端需要发送一段数据,这个数据需要接收端一收到就进行向上交付。而接收端在收到PSH标志位有效的数据时,迅速将数据交付给应用层,所以PSH又叫做急迫比特。 387 | - URG:紧急指针,意思为URG位有效的数据包,是一个紧急需要处理的数据包,需要接收端在接收到之后迅速处理。 388 | 389 | RST标志位应用场景如下。 390 | 391 | TCP正常关闭连接的时候使用FIN,但是如果是关闭异常连接,则使用RST,发送RST包。与FIN包存在两点不同: 392 | 393 | - RST不必等缓冲区的包都发出去,直接就丢弃缓冲区的包去发送RST包,而FIN需要先处理完缓冲区的包才行。 394 | - 接收端收到RST包之后,不需要发送ACK包进行确认,而接收端接收到FIN包的时候需要ACK包应答。 395 | 396 | **28、TCP和UDP报文段的首部格式。** 397 | 398 | TCP报文分为首部和数据两部分,TCP的全部功能体现在首部的各字段作用上。 399 | 400 | 首部固定部分各字段的意义。 401 | 402 | - **源端口和目的端口:** 各占两个字节。 403 | - **序号:** 占四个字节。 404 | - **确认号:** 占四个字节。 405 | - **数据偏移:** 占四个字节,指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。 406 | - **保留:** 占六个字节,保留为今后使用,但目前应置为0。 407 | 408 | 还有六个控制位,就在27题里讲述了。TCP示意图如下所示。 409 | 410 | ![YhyF4U.png](https://s1.ax1x.com/2020/05/18/YhyF4U.png) 411 | 412 | UDP报文首部相对简单。主要分为四部分: 413 | 414 | - **源端口和目的端口。** 415 | - **数据包长度。** 416 | - **校验值。** 417 | 418 | 弥有,2019年9月 419 | [EOF] 420 | -------------------------------------------------------------------------------- /notes/Java常量池理解及总结.md: -------------------------------------------------------------------------------- 1 | # Java常量池理解及总结 2 | 3 | ## 1、相关概念 4 | 5 | Java中的常量池,实际上分为两种形态:**静态常量池** 和 **运行时常量池** 。 6 | 7 | ### 1.1 静态常量池 8 | 9 | 在Class文件结构中,最头的4个字节用于存储魔数Magic Number(OxCAFEBABE),用于确定一个文件是否能被JVM接受;再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号;再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。这部分常量池称为静态常量池。与Java中语言习惯不同,常量池容量计数是从1而不是0开始的。 10 | 11 | Class常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References),字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等,而符号引用则属于编译原理方面的概念。Class常量将在类加载后进入方法区的运行时常量池中存放(稍后讲解)。其中,符号引用主要包括下面几类常量: 12 | 13 | - 被模块导出或者开放的包(Package) 14 | - 类和接口的全限定名(Fully Qualified Name) 15 | - 字段的名称和描述符(Descriptor) 16 | - 方法的名称和描述符 17 | - 方法的句柄和方法类型(Method Handle、Method Type、Invoke Dynamic) 18 | - 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant) 19 | 20 | 常量池中每一项常量都是一个表,共有17种不同类型的表。它们共有一个特定,表结构起始的第一位是个u1类型的标志位(tag,用于区分常量类型),代表着当前常量属于哪种常量类型。17种常量类型所代表的具体含义如下表所示。 21 | | 类 型 | 标 志 | 描 述 | 22 | | :-----| :----: | :----: | 23 | | CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 | 24 | | CONSTANT_Integer_info | 3 | 整型字面量 | 25 | | CONSTANT_Float_info | 4 | 浮点型字面量 | 26 | | CONSTANT_Long_info | 5 | 长整型字面量 | 27 | | CONSTANT_Double_info | 6 | 双精度浮点型字面量 | 28 | | CONSTANT_Class_info | 7 | 类或接口的符号引用 | 29 | | CONSTANT_String_info | 8 | 字符串类型字面量 | 30 | | CONSTANT_Fieldref_info | 9 | 字段的符号引用 | 31 | | CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | 32 | | CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | 33 | | CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 | 34 | | CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | 35 | | CONSTANT_MethodType_info | 16 | 表示方法类型 | 36 | | CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 | 37 | | CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | 38 | | CONSTANT_Module_info | 19 | 表示一个模块 | 39 | | CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 | 40 | 41 | 以简单的Java代码为例: 42 | 43 | ```java 44 | public static void main(String[] args) { 45 | String str="zingbug";// 46 | } 47 | ``` 48 | 49 | 利用javap命令,生成字节码后,静态常量池如下,具体含义见上表。 50 | 51 | ```java 52 | Constant pool: 53 | #1 = Methodref #2.#3 // java/lang/Object."":()V 54 | #2 = Class #4 // java/lang/Object 55 | #3 = NameAndType #5:#6 // "":()V 56 | #4 = Utf8 java/lang/Object 57 | #5 = Utf8 58 | #6 = Utf8 ()V 59 | #7 = String #8 // zingbug 60 | #8 = Utf8 zingbug 61 | #9 = Class #10 // Main 62 | #10 = Utf8 Main 63 | ... 64 | ``` 65 | 66 | 顺便提一下,最常见的CONSTANT_Utf8_info型常量结构体如下表所示。 67 | | 类 型 | 名 称 | 数 量 | 68 | | :----: :----: | :----- | 69 | | u1 | tag | 1 | 70 | | u2 | length | 1 | 71 | | u1 | bytes | length | 72 | 73 | 其中,length值说明了这个UTF-8编码的字符串长度是多少字节,它后面就紧跟着长度为length字节的连续数据,用于表示UTF-8编码的字符串。 74 | 由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,即u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。 75 | 76 | ### 1.2 运行时常量池 77 | 78 | 一个类加载到JVM中后对应一个运行时常量池。运行时常量池,是指jvm虚拟机在类加载的解析阶段,将class常量池内的符号引用替换为直接引用(具体请参考另一篇笔记),,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。这其中涉及到类加载的解析阶段。 79 | 80 | 运行时常量池相对于CLass文件常量池的另外一个重要特征是**具备动态性**,Class文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。 81 | 82 | Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。 83 | 84 | > String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。 85 | 86 | 我自己的理解是,**运行时常量池是Class文件常量池在运行时的表示。** 87 | 88 | 我们都知道运行时常量池是放在方法区的,但需要注意的是,方法区本身就是一个逻辑性的区域。在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;但在JDK8及以后,取消了永久代,新增了元空间(Metaspace),方法区就“四分五裂了”,不再是在单一的一个去区域内进行存储,其中,元空间只存储类和类加载器的元数据信息,符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,这时候方法区只是对逻辑概念的表述罢了。 89 | 90 | ### 1.3 常量池的优势 91 | 92 | 常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。 93 | 94 | - **节省内存空间**:常量池中所有相同的字符串常量被合并,只占用一个空间。 95 | - **节省运行时间**:比较字符串时,==比equals()快。对于两个引用变量,只用==判断地址引用是否相等,也就可以判断实际值是否相等。 96 | 97 | > 双等号==的含义: 98 | > 基本数据类型之间应用双等号,比较的是他们的数值。 99 | > 复合数据类型(字符串、类)之间应用双等号,比较的是他们在内存中的存放地址。当然,内存地址相同,值也必定相同。 100 | 101 | ## 2、Java基本数据类型的包装类和缓存池 102 | 103 | Java基本数量类型有8种,分别是byte、short、int、long、float、double、char和boolean。 104 | 105 | ### 2.1 6种基本数据类型的包装类实现了缓存池技术,即Byte、Short、Integer、Long、Character和Boolean 106 | 107 | 这五种基本数据类型默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。以Integer为例: 108 | 109 | ```java 110 | Integer i1=100; 111 | Integer i2=100; 112 | System.out.println(i1==i2);//输出true 113 | Integer i3=300; 114 | Integer i4=300; 115 | System.out.println(i3==i4);//输出false 116 | ``` 117 | 118 | 对于"Integer i1=100;”,Java在编译的时候会直接将代码封装成“Integer i1=Integer.valueOf(100);”,从而使用常量池的缓存。进一步去看Integer.valueOf方法的实现源码。 119 | 120 | ```java 121 | public static Integer valueOf(int i) { 122 | if (i >= IntegerCache.low && i <= IntegerCache.high) 123 | return IntegerCache.cache[i + (-IntegerCache.low)]; 124 | return new Integer(i); 125 | } 126 | ``` 127 | 128 | 其中,IntegerCache.low为-128,IntegerCache.high默认为127,但也可以通过JVM的启动参数 -XX:AutoBoxCacheMax=size 修改。 129 | 130 | 更多比较场景: 131 | 132 | ```java 133 | Integer i1 = 100; 134 | Integer i2 = 0; 135 | Integer i3 = new Integer(100); 136 | Integer i4 = new Integer(100); 137 | Integer i5 = new Integer(0); 138 | 139 | System.out.println("i1=i3 " + (i1 == i3)); 140 | System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); 141 | System.out.println("i3=i4 " + (i3 == i4)); 142 | System.out.println("i3=i4+i5 " + (i3 == i4 + i5)); 143 | System.out.println("100=i4+i5 " + (100 == i4 + i5)); 144 | 145 | //输出 146 | i1=i3 false 147 | i1=i2+i3 true 148 | i3=i4 false 149 | i3=i4+i5 true 150 | 100=i4+i5 true 151 | ``` 152 | 153 | 对此,有如下解释: 154 | 155 | - Integer i3 = new Integer(100);这种情况下会创建新的对象,所以i1不等于i3; 156 | 157 | - 语句i1 == i2 + i3,因为+这个操作符不适用于Integer对象,首先i2和i3会自动拆箱操作,进行数值相加,即i1 == 100,然后Integer对象无法与数值进行直接比较,所以i1自动拆箱转为int值100,最终这条语句转为100 == 100进行数值比较,故输出true; 158 | 159 | - i3和i4分别是不同的新对象,地址不同,故而不相等。 160 | 161 | - 语句i3 == i4 + i5,和100 == i4 + i5,同样是自动拆箱操作,最后为数值比较而已。 162 | 163 | ### 2.2 2种浮点数类型的包装类Float,Double并没有实现缓存池技术 164 | 165 | ```java 166 | Double d1=1.8; 167 | Double d2=1.8; 168 | System.out.println(d1==d2);//输出false 169 | ``` 170 | 171 | ## 3、String类和常量池 172 | 173 | ### 3.1 不同场景下的String比较 174 | 175 | String与常量池的关系比较复杂多样,我们直接以实际代码为例。 176 | 177 | ```java 178 | String s1 = "Hello"; 179 | String s2 = "Hello"; 180 | String s3 = "Hel" + "lo"; 181 | String s4 = "Hel" + new String("lo"); 182 | String s5 = new String("Hello"); 183 | String s6 = s5.intern(); 184 | String s7 = "H"; 185 | String s8 = "ello"; 186 | String s9 = s7 + s8; 187 | 188 | System.out.println(s1 == s2); // true 189 | System.out.println(s1 == s3); // true 190 | System.out.println(s1 == s4); // false 191 | System.out.println(s1 == s9); // false 192 | System.out.println(s4 == s5); // false 193 | System.out.println(s1 == s6); // true 194 | ``` 195 | 196 | 首先说明一点,在String中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。对上述场景一一分析: 197 | 198 | - s1 == s2 这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。 199 | - s1 == s3 这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello",所以s1 == s3成立。**只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。** 200 | - s1 == s4 当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,不清楚s4被分配到哪去了,所以地址肯定不同。**对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中。** 201 | - s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,不能在编译期被确定,所以不做优化,只能等到运行时,在堆中创建s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。jvm常量池,堆,栈内存分布如下: 202 | ![N2xxLd.jpg](https://s1.ax1x.com/2020/06/28/N2xxLd.jpg) 203 | - s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。 204 | - s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。 205 | 206 | ### 3.2 静态变量赋值 207 | 208 | ```java 209 | public class Main { 210 | public static final String A = "ab"; // 常量A 211 | public static final String B = "cd"; // 常量B 212 | 213 | public static void main(String[] args) { 214 | String s = A + B; // 将两个常量用+连接对s进行初始化 215 | String t = "abcd"; 216 | System.out.println(s == t);//true 217 | } 218 | } 219 | ``` 220 | 221 | 此时A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s="ab"+"cd"; 222 | 223 | ### 3.3 静态方法赋值 224 | 225 | ```java 226 | public class Main { 227 | public static final String A; // 常量A 228 | public static final String B; // 常量B 229 | 230 | static { 231 | A = "ab"; 232 | B = "cd"; 233 | } 234 | 235 | public static void main(String[] args) { 236 | // 将两个常量用+连接对s进行初始化 237 | String s = A + B; 238 | String t = "abcd"; 239 | System.out.println(s == t);//false 240 | } 241 | } 242 | ``` 243 | 244 | 此时A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。 245 | 246 | ### 3.4 intern方法 247 | 248 | 运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。 249 | 250 | ```java 251 | public class Main { 252 | public static void main(String[] args) { 253 | String s1 = new String("Hello"); 254 | String s2 = s1.intern(); 255 | String s3 = "Hello"; 256 | System.out.println(s1 == s2);//false 257 | System.out.println(s3 == s2);//true 258 | } 259 | } 260 | ``` 261 | 262 | String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回指向该字符串的引用(CONSTAT_String_info),如果没有则添加自己的字符串进入常量池,也同时返回引用。 263 | 264 | ### 3.5 延伸 265 | 266 | ```java 267 | String s1 = new String("Hello"); 268 | ``` 269 | 270 | 上面这行代码中,一共创建了几个对象? 271 | 272 | "Hello"会首先在常量池中里创建一个字符常量,然后在new创建对象时,会将常量池中"Hello"的字符串复制到堆中新创建的对象字符数组中,并建立引用s1指向它。所以,这条语句共创建了2个对象,分别位于常量池和堆。 273 | 274 | ## 4、总结 275 | 276 | - 运行时常量池中的常量,基本来源于各个Class文件中的常量池。也就是说,运行时常量池是Class文件常量池在运行时的表示。 277 | - 程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则JVM不会自动添加常量到常量池。 278 | 279 | ## 5、参考资料 280 | 281 | [《Java核心技术系列:Java虚拟机规范(Java SE 8版)》](https://book.douban.com/subject/26418340/) 282 | 283 | [《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) 284 | 285 | [浅谈JVM中的符号引用和直接引用](/notes/浅谈JVM中的符号引用和直接引用.md) 286 | 287 | [深入浅出java常量池](https://www.cnblogs.com/syp172654682/p/8082625.html) 288 | 289 | [Java常量池理解与总结](https://mp.weixin.qq.com/s?__biz=MzU3NDg0MTY0NQ==&mid=2247485452&idx=1&sn=64178cb2b4e2768b2feedbe0d0971ee1&chksm=fd2d7e4eca5af7587be00fccbec403117cdb7eb681c0542ff542335b1239049bd0216fc1bb1d&mpshare=1&scene=1&srcid=0623b0MIhBaU2sZEhvxEjoME&sharer_sharetime=1592875293438&sharer_shareid=e9e6d524172161e8393308ae6db3aa63&key=242af3e89b1070825a226d604188d71bc5c856baa07c40c96c01389ddc232167ec2a8196658f02f540cce13a26664b46549909f0156708f4d7d26fc7c470faeb92a6c0d4f3ad7328a7cbe59a5c3a0cd6&ascene=1&uin=MjQ3MzkwMTc2Mw%3D%3D&devicetype=Windows+10+x64&version=62090523&lang=zh_CN&exportkey=AVRh0T9RxdKUe0T%2BS0Vu7Jk%3D&pass_ticket=mrrMmVKWvl4QF8i0mBVDO7Xre7kYXlm7qLoXUV%2FJeUsPvRILMjcMWMW1A%2BzBALMH) 290 | 291 | ZingBug,2020/6/28 292 | [EOF] 293 | -------------------------------------------------------------------------------- /notes/从字节码角度分析try、catch、finally运行细节.md: -------------------------------------------------------------------------------- 1 | # 从字节码角度分析try、catch、finally运行细节 2 | 3 | ## 0、字节码指令 4 | 5 | 回顾一下字节码的指令含义(参考《Java虚拟机规范》-2.11.2节 加载和存储指令): 6 | 加载和存储指令用于将数据从栈帧的本地变量表和操作数栈之间来回传递。 7 | 8 | > - 将一个本地变量加载到操作数栈的指令包括:*iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>*。 9 | > - 将一个数值从操作数栈存储到局部变量表的指令包括:*istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>*。 10 | > - 将一个常量加载到操作数栈的指令包括:*bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_null、iconst_<i>、lconst_<i>、fconst_<i>、dconst_<i>*。 11 | > - 用户扩充局部变量表的访问索引或立即数的指令:*wide*。 12 | 13 | 上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如*iload_<n>*),这些指令助记符实际上代表了一组指令(例如*iload_<n>*代表了*iload_0、iload_1、iload_2和iload_3*这几个指令,需要注意的是,n是从0开始计数的)。这几组指令都是某个带有一个操作数的通用指令(例如*iload*)的特殊形式。在尖括号之间的字母指定了指令隐含操作数的数据类型,<n>代表非负的整数,<i>代表是int类型数据,<l>代表了long类型,<f>表示float类型,<d>代表了double类型。操作byte、char和short类型数据时,经常用int类型的指令来表示。 14 | 另外,根据int值范围,JVM整型入栈指令分为四类:当int取值-1\~5采用iconst指令,取值-128\~127采用bipush指令,取值-32768\~32767采用sipush指令,取值-2147483648\~2147483647采用 ldc 指令。(参考[JVM字节码之整型入栈指令(iconst、bipush、sipush、ldc)](https://blog.csdn.net/zhaow823/article/details/81199093)) 15 | 16 | ## 1、简单的try、catch、finally例子 17 | 18 | 写一个最简单的try、catch、finally例子,分析最后返回的结果,并用javap -verbose 命令来显示目标文件(.class文件)字节码信息,首先确定一下运行环境和JDK版本,不同JDK版本的字节码信息有所不同。 19 | 20 | > 系统运行环境:Microsoft Windows [版本 10.0.19041.329] 系统 64bit 21 | > JDK信息:Java(TM) SE Runtime Environment (build 14.0.1+7) 22 | Java HotSpot(TM) 64-Bit Server VM (build 14.0.1+7, mixed mode, sharing) 23 | 24 | 字节码部分略过了常量池等部分,只对Code和Exception table部分进行分析。 25 | 26 | ```java 27 | //Java代码 28 | public int inc() { 29 | int x; 30 | try { 31 | x = 1; 32 | return x; 33 | } catch (Exception e) { 34 | x = 2; 35 | return x; 36 | } finally { 37 | x = 3; 38 | } 39 | } 40 | //编译后的ByteCode字节码和异常表 41 | public int inc(); 42 | descriptor: ()I 43 | flags: (0x0001) ACC_PUBLIC 44 | Code: 45 | stack=1, locals=5, args_size=1 46 | 0: iconst_1 //try块,将int类型值(1)压入栈顶 47 | 1: istore_1 //将栈顶数据(1)存入到第二个本地变量(从0开始计数) 48 | 2: iload_1 //将第二个本地int变量(1)压入栈顶 49 | 3: istore_2 //将栈顶数据(1)存入第三个本地变量 50 | 4: iconst_3 //将int类型值(3)压入栈顶,第一个finally 51 | 5: istore_1 //将栈顶数据(3)存入到第二个本地变量 52 | 6: iload_2 //将第三个本地int变量(1)压入栈顶 53 | 7: ireturn //返回栈顶数据值(1) 54 | 8: astore_2 //给catch中定义的Exception e赋值,并存储到第三个变量槽中 55 | 9: iconst_2 //将int类型值(2)压入栈顶 56 | 10: istore_1 //将栈顶数据(2)存入到第二个本地变量 57 | 11: iload_1 //将第二个本地int变量(2)压入栈顶 58 | 12: istore_3 //将栈顶数据(2)存入到第四个本地变量 59 | 13: iconst_3 //将int类型值(3)压入栈顶,第二个finally 60 | 14: istore_1 //将栈顶数据(3)存入到第二个本地变量 61 | 15: iload_3 //将第四个本地int变量(2)压入栈顶 62 | 16: ireturn //返回栈顶数据值(2) 63 | 17: astore 4 //如果出现了不属于java.lang.Exception及其子类的异常才会走到这 64 | 19: iconst_3 //将int类型值(3)压入栈顶,第三个finally 65 | 20: istore_1 //将栈顶数据(3)存入到第二个本地变量 66 | 21: aload 4 //将异常放入栈顶 67 | 23: athrow //抛出栈顶的异常 68 | Exception table: //异常表 69 | from to target type 70 | 0 4 8 Class java/lang/Exception 71 | 0 4 17 any 72 | 8 13 17 any 73 | 17 19 17 any 74 | ``` 75 | 76 | 先解释一下异常表组成: 77 | 78 | - From : 从第几行开始检测; 79 | - To :到第几行结束; 80 | - Target : 假如From和To中的代码执行发生异常跳到哪一行处理; 81 | - Type : Java代码中定义的全部异常类型。 82 | Type 为 any 的情况,就是抛出了捕获不到的类型。所以全部跳到17行去处理。17行就是Java代码中定义的finally语句块,相当于最后一道防线。 83 | 84 | 编译器为这段Java代码生成了四条异常表记录,对应四条代码可能出现的执行路径,从Java代码的语义上讲,这四条执行路径分别为: 85 | 86 | - 如果try语句块中出现属于Exception及其子类的异常时,会转到catch语句块处理; 87 | - 如果try语句块中出现不属于Exception及其子类的异常时,会转到finally语句块处理; 88 | - 如果catch语句块中出现不属于Exception及其子类的异常时,会转到finally语句块处理; 89 | - 如果finally语句块中出现不属于Exception及其子类的异常时,会转到finally语句块处理; 90 | 91 | 返回到一开始的问题,这段代码的返回值是多少?答案是: 92 | 93 | - 如果没有出现异常值,返回值是1; 94 | - 如果出现了Exception异常,返回值是2; 95 | - 如果出现了Exception以外的异常,没有返回值。 96 | 97 | 具体的运行逻辑根据上文字节码的注释可以知道,字节码中第0-3行所做的操作是将整数1赋值给变量x,并且将此时x的值复制一份副本到第三个本地变量槽中,作为后面方法返回值使用。如果这时候没有出现异常,则会继续走到第4-5行,将变量x赋值为3,然后将之前保存在第三个本地变量槽中的整数1读入到操作栈顶,最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。如果出现了异常,PC寄存器指针转到第8行,第9-12行所做的事情就是将2赋值给变量x,然后将变量x此时的值赋给第四个本地变量槽,最后再将变量x的值改为3。方法返回前同样将第四个本地变量槽中保留的整数2读到了操作栈顶。从第17行开始的代码,作用是将变量x的值赋为3,并将栈顶的异常抛出,方法结束。 98 | 99 | ## 2、finally中加入return的例子 100 | 101 | 改一下代码,在finally中加入return,这样这段代码中有两个return语句,但是程序到底返回的是try 还是 finally。接下来我们还是看字节码信息。 102 | 103 | ```java 104 | //Java代码 105 | public int inc() { 106 | int x; 107 | try { 108 | x = 1; 109 | return x; 110 | } catch (Exception e) { 111 | x = 2; 112 | return x; 113 | } finally { 114 | x = 3; 115 | return x;//加入了return 116 | } 117 | } 118 | //编译后的ByteCode字节码和异常表 119 | public int inc(); 120 | descriptor: ()I 121 | flags: (0x0001) ACC_PUBLIC 122 | Code: 123 | stack=1, locals=5, args_size=1 124 | 0: iconst_1 //try块,将int类型值(1)压入栈顶 125 | 1: istore_1 //将栈顶数据(1)存入到第二个本地变量(从0开始计数) 126 | 2: iload_1 //将第二个本地int变量(1)压入栈顶 127 | 3: istore_2 //将栈顶数据(1)存入第三个本地变量 128 | 4: iconst_3 //将int类型值(3)压入栈顶,第一个finally 129 | 5: istore_1 //将栈顶数据(3)存入到第二个本地变量 130 | 6: iload_1 //将第二个本地int变量(3)压入栈顶 131 | 7: ireturn //返回栈顶数据值(3) 132 | 8: astore_2 //给catch中定义的Exception e赋值,并存储到第三个变量槽中 133 | 9: iconst_2 //将int类型值(2)压入栈顶 134 | 10: istore_1 //将栈顶数据(2)存入到第二个本地变量 135 | 11: iload_1 //将第二个本地int变量(2)压入栈顶 136 | 12: istore_3 //将栈顶数据(2)存入到第四个本地变量 137 | 13: iconst_3 //将int类型值(3)压入栈顶,第二个finally 138 | 14: istore_1 //将栈顶数据(3)存入到第二个本地变量 139 | 15: iload_1 //将第二个本地int变量(3)压入栈顶 140 | 16: ireturn //返回栈顶数据值(3) 141 | 17: astore 4 //如果出现了不属于java.lang.Exception及其子类的异常才会走到这 142 | 19: iconst_3 //将int类型值(3)压入栈顶,第三个finally 143 | 20: istore_1 //将栈顶数据(3)存入到第二个本地变量 144 | 21: iload_1 //将第2个本地int变量(3)压入栈顶 145 | 22: ireturn //异常都不用抛出了,直接返回栈顶值(3) 146 | Exception table: 147 | from to target type 148 | 0 4 8 Class java/lang/Exception 149 | 0 4 17 any 150 | 8 13 17 any 151 | 17 19 17 any 152 | ``` 153 | 154 | 与第一个例子相比,大部分代码相似,但区别在于,try语句块中的return被忽略覆盖掉了,它没有进行真正的返回,只是进行一个赋值操作而已,直到finally重新修改了x=3,再return。需要注意的是,异常athrow操作指令消失了。 155 | 156 | 正常来说我们catch到的Exception e,在这一步处理的时候,如果finally有return,那么会发生吞噬异常的情况,也就是抛出的异常不见了。 157 | 158 | ## 3、catch多个异常类型的例子 159 | 160 | 直接去看代码和字节码中的异常表: 161 | 162 | ```java 163 | //Java代码 164 | public void inc() { 165 | int x; 166 | try { 167 | x = 1; 168 | } catch (NullPointerException e) { 169 | x = 2; 170 | } catch (RuntimeException e) { 171 | x = 3; 172 | } catch (Exception e) { 173 | x = 4; 174 | } finally { 175 | x = 5; 176 | } 177 | } 178 | //字节码中的异常表部分 179 | Exception table: 180 | from to target type 181 | 0 2 7 Class java/lang/NullPointerException 182 | 0 2 15 Class java/lang/RuntimeException 183 | 0 2 23 Class java/lang/Exception 184 | 0 2 31 any 185 | 7 10 31 any 186 | 15 18 31 any 187 | 23 26 31 any 188 | ``` 189 | 190 | 当catch多个异常的时候,字节码中异常表会有对应的体现。需要注意的是,小的异常要放在前面,大的异常类放在后面。不然编译不会通过。这样是为了防止大炮打蚊子的浪费,如下所示,我们将大范围的Exception异常放在RuntimeException异常前面的时候,会出现报错现象。 191 | 192 | ![Nwd5tS.png](https://s1.ax1x.com/2020/06/24/Nwd5tS.png) 193 | 194 | ## 4、总结 195 | 196 | 对以上所有的例子进行总结 197 | 198 | - try、catch、finally语句中,在如果try语句有return语句,则返回的之后当前try中变量此时对应的值,此后对变量做任何的修改,都不影响try中return的返回值。 199 | 200 | - 如果finally块中有return 语句,则返回try或catch中的返回语句忽略。 201 | 202 | - 如果finally块中抛出异常,则整个try、catch、finally块中抛出异常 203 | 204 | 所以使用try、catch、finally语句块中需要注意的是: 205 | 206 | - 尽量在try或者catch中使用return语句。通过finally块中达到对try或者catch返回值修改是不可行的。 207 | 208 | - finally块中避免使用return语句,因为finally块中如果使用return语句,会显示的消化掉try、catch块中的异常信息,屏蔽了错误的发生。 209 | 210 | - finally块中避免再次抛出异常,否则整个包含try语句块的方法回抛出异常,并且会消化掉try、catch块中的异常。 211 | 212 | ## 5、参考资料 213 | 214 | [《Java核心技术系列:Java虚拟机规范(Java SE 8版)》](https://book.douban.com/subject/26418340/) 215 | 216 | [《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) 217 | 218 | [Java字节码指令收集大全](https://www.cnblogs.com/longjee/p/8675771.html) 219 | 220 | [try catch finally异常机制Java源码+JVM字节码(内含小彩蛋)](https://blog.csdn.net/whiteBearClimb/article/details/104131005) 221 | 222 | [Java中关于try、catch、finally中的细节分析](https://mp.weixin.qq.com/s?__biz=MzU3NDg0MTY0NQ==&mid=2247485213&idx=1&sn=982c5e92d2c15a48b59194258a40c2d2&chksm=fd2d715fca5af84976b8cd034fc52aa94e78c2750ba6bcb586f979438bf38baee283fee7de9b&mpshare=1&scene=1&srcid=0623mdjtJSGrvIRPkaj6HhNx&sharer_sharetime=1592875254832&sharer_shareid=e9e6d524172161e8393308ae6db3aa63&key=c32b5c198f0599707981fa0619311426b3104054b7dd310f3346ed0d2159420a6119aaa288eb9df1d369808227d2bb1eec8f9cbe2510d831061991b4215d7bd786a293f6f9c1033009510d6df3fc2f7f&ascene=1&uin=MjQ3MzkwMTc2Mw%3D%3D&devicetype=Windows+10+x64&version=6209007b&lang=zh_CN&exportkey=AYaxCNjrIahDUSf8A0fYYT4%3D&pass_ticket=4yLw2AbquUCok67oAc4LWcLTUZY6fTDbcUcWHB4Mj69rD%2BHCBbKgabd4I%2BGIDkfk) 223 | 224 | ZingBug,2020/6/24 225 | [EOF] 226 | -------------------------------------------------------------------------------- /notes/浅谈JVM中的符号引用和直接引用.md: -------------------------------------------------------------------------------- 1 | # 浅谈JVM中的符号引用和直接引用 2 | 3 | ## 1、前言 4 | 5 | 在 JVM 类加载过程的解析阶段,Java虚拟机将常量池中的符号引用替换为直接引用。符号引用存在于 Class 常量池,在Class文件格式中多以 CONSTANT_Class_info 、 CONSTANT_Fieldref_info 、 CONSTANT_Methodref_info 等类型的常量出现,符号引用和直接引用的区别在于: 6 | 7 | - 符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只用使用时能无歧义地定位到目标即可,与当前虚拟机实现的内存布局无关。 8 | - 直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在虚拟机的内存中存在。 9 | 10 | 此外,在JVM 中方法调用(分派、执行)过程中,有五条相关指令用于方法调用: 11 | 12 | - invokevirtual 指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这 也是 Java 语言中最常见的方法分配方式。 13 | - invokeinterface 指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。 14 | - invokespecial 指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。 15 | - invokestatic 指令:用于调用类静态方法( static 方法)。 16 | - invokedynamic 指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面四条调用指令的分派逻辑都固化在 Java 虚拟机内部,用户无法改变,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。 17 | 18 | 方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括 ireturn (当返回值是 boolean、 byte、 char、 short和 int类型时使用)、 ireturn、 ireturn、ireturn 和 ireturn,另外还有一个 return 指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。 19 | 20 | 以上的概念介绍,结合一个实例来分析举证。看下面的Java代码: 21 | 22 | ```java 23 | public class Main { 24 | public static void main(String[] args) { 25 | Sub sub = new Sub(); 26 | sub.a(); 27 | } 28 | } 29 | 30 | class Sub { 31 | public void a() { 32 | } 33 | } 34 | ``` 35 | 36 | 编译后使用javap工具,得到Class字节码,如下: 37 | 38 | ```java 39 | Constant pool: 40 | #1 = Methodref #2.#3 // java/lang/Object."":()V 41 | #2 = Class #4 // java/lang/Object 42 | #3 = NameAndType #5:#6 // "":()V 43 | #4 = Utf8 java/lang/Object 44 | #5 = Utf8 45 | #6 = Utf8 ()V 46 | #7 = Class #8 // Sub 47 | #8 = Utf8 Sub 48 | #9 = Methodref #7.#3 // Sub."":()V 49 | #10 = Methodref #7.#11 // Sub.a:()V 50 | #11 = NameAndType #12:#6 // a:()V 51 | #12 = Utf8 a 52 | #13 = Class #14 // Main 53 | #14 = Utf8 Main 54 | #15 = Utf8 Code 55 | #16 = Utf8 LineNumberTable 56 | #17 = Utf8 LocalVariableTable 57 | #18 = Utf8 this 58 | #19 = Utf8 LMain; 59 | #20 = Utf8 main 60 | #21 = Utf8 ([Ljava/lang/String;)V 61 | #22 = Utf8 args 62 | #23 = Utf8 [Ljava/lang/String; 63 | #24 = Utf8 sub 64 | #25 = Utf8 LSub; 65 | #26 = Utf8 SourceFile 66 | #27 = Utf8 Main.java 67 | { 68 | public Main(); 69 | descriptor: ()V 70 | flags: (0x0001) ACC_PUBLIC 71 | Code: 72 | stack=1, locals=1, args_size=1 73 | 0: aload_0 74 | 1: invokespecial #1 // Method java/lang/Object."":()V 75 | 4: return 76 | LineNumberTable: 77 | line 8: 0 78 | LocalVariableTable: 79 | Start Length Slot Name Signature 80 | 0 5 0 this LMain; 81 | 82 | public static void main(java.lang.String[]); 83 | descriptor: ([Ljava/lang/String;)V 84 | flags: (0x0009) ACC_PUBLIC, ACC_STATIC 85 | Code: 86 | stack=2, locals=2, args_size=1 87 | 0: new #7 // class Sub 88 | 3: dup 89 | 4: invokespecial #9 // Method Sub."":()V 90 | 7: astore_1 91 | 8: aload_1 92 | 9: invokevirtual #10 // Method Sub.a:()V 93 | 12: return 94 | LineNumberTable: 95 | line 10: 0 96 | line 11: 8 97 | line 12: 12 98 | LocalVariableTable: 99 | Start Length Slot Name Signature 100 | 0 13 0 args [Ljava/lang/String; 101 | 8 5 1 sub LSub; 102 | } 103 | ``` 104 | 105 | 因为篇幅有限,上面的内容只保留了静态常量池和 Code 部分。 106 | 107 | - Sub 实例初始化的指令如下,印证invokespecial指令。 108 | 109 | ```java 110 | 4: invokespecial #9 // Method Sub."":()V 111 | ``` 112 | 113 | - void 方法的返回指令如下,印证return指令。 114 | 115 | ```java 116 | 12: return 117 | ``` 118 | 119 | 下面我们主要对 Sub.a() 方法的调用来进行说明。 120 | 121 | ## 2、符号引用 122 | 123 | 在 main 方法的字节码中,调用 Sub.a() 方法的指令如下: 124 | 125 | ```java 126 | 9: invokevirtual #10 // Method Sub.a:()V 127 | ``` 128 | 129 | invokevirtual 指令就是调用实例方法的指令,后面的操作数 10 是 Class 文件中常量池的下标,表示用来指定要调用的目标方法。我们再来看常量池在这个位置上的内容: 130 | 131 | ```java 132 | #10 = Methodref #7.#11 // Sub.a:()V 133 | ``` 134 | 135 | 这是一个 Methodref 类型的数据,我们再来看看虚拟机规范中对该类型的说明: 136 | 137 | ```java 138 | CONSTANT_Methodref_info { 139 | u1 tag; 140 | u2 class_index; 141 | u2 name_and_type_index; 142 | } 143 | ``` 144 | 145 | 这实际上就是一种引用类型,tag 表示了常量池数据类型,这里固定是 10 (参照 Class 常量池项目类型表)。class_index 表示了类的索引,name_and_type_index 表示了名称与类型的索引,这两个也都是常量池的下标。在 javap 的输出中,已经将对应的关系打印了出来,我们可以直接的观察到它都引用了哪些类型: 146 | 147 | ```java 148 | #10 = Methodref #7.#11 // Sub.a:()V 类中方法的符号引用 149 | |--#7 = Class #8 // Sub 类或接口的符号引用 150 | | |--#8 = Utf8 Sub 151 | |--#11 = NameAndType #12:#6 // a:()V 字段或者方法的部分符号引用 152 | | |--#12 = Utf8 a 153 | | |--#6 = Utf8 ()V 154 | ``` 155 | 156 | 这里我们将其表现为树的形式。可以看到,我们可以得到该方法所在的类,以及方法的名称和描述符。于是我们根据 invokevirtual 的操作数,找到了常量池中方法对应的 Methodref,进而找到了方法所在的类以及方法的名称和描述符,当然这些内容最终都是 UTF8 字符串形式。 157 | 158 | 实际上这就是一个符号引用的例子,符号引用可以理解为利用文字形式来描述引用关系,简单来说是一个包含足够信息的字符串,以供实际使用时可以找到相应的位置。比如说“java/io/PrintStream.println:(Ljava/lang/String;)V”,这里面有类、方法名和方法参数等信息。 159 | 160 | ## 3、直接引用 161 | 162 | 在第一次运行时,发现指令还没有被解析,根据指令去把常量池中有关系的所有项找出来,得到以“UTF-8”编码描述的此方法所属的“类,方法名,描述符”的常量池项,这就是上文中提到的**符号引用**的字符串信息,类的加载解析过程会根据字符串的内容,到该类的方法表中搜索这个方法,得到放发的偏移量(指针),这个偏移量(指针)就是**直接引用** 。再将偏移量赋给常量池的#10 (根据指令,在常量池中找到的第一个项),最后再将 Code 中方法调用的 invokevirtual 指令修改为invokevirtual_quick,并把操作数修改成指向方法表的偏移量(指针), 并加上参数个数,类似于以下格式: 163 | 164 | ```java 165 | #10 = Methodref 偏移量(指针) // Sub.a:()V 166 | 9: invokevirtual_quick 偏移量(指针) // Method Sub.a:()V 167 | ``` 168 | 169 | 运行一次之后,符号引用会被替换为直接引用,下次就不用重新搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。 170 | 171 | 上面提到的“invokevirtual_quick”是R大借用Sun JDK 1.0.2 虚拟机为例,提出解释直接引用过程的,详细说明请去看 RednaxelaFX 的回答,参考链接在后文。 172 | 173 | ## 4、总结 174 | 175 | 谈一下我自己的理解,Java代码中所有方法调用的目标方法在 Class 文件里面都是一个常量池的符号引用,在类加载的解析阶段,会将其中符号引用转换为直接引用,这个根据字符串搜素具体方法的过程有些类似于类加载器的运行机制。 176 | 177 | 此外,解析能够顺利进行的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译的那一刻就已经确定下来了,这类方法的调用就叫解析。 178 | 179 | 解析调用一定是一个静态的过程,在编译期间就已经完成,将涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。而另一种主要的方法调用形式,分派调用则复杂很多,它可能是静态的也可能是动态的,与继承、多态相关,有机会去学习理解一波,这里就先挖个坑,不再做多介绍了。 180 | 181 | ## 5、参考资料 182 | 183 | [《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) 184 | 185 | [《自己动手写Java虚拟机》](https://book.douban.com/subject/26802084/) 186 | 187 | [浅析 JVM 中的符号引用与直接引用](https://www.dazhuanlan.com/2019/10/18/5da94ef24e667/) 188 | 189 | [JVM里的符号引用如何存储? - RednaxelaFX的回答 - 知乎](https://www.zhihu.com/question/30300585/answer/51335493) 190 | 191 | [JVM方法调用(invokevirtual)](https://www.cnblogs.com/Jc-zhu/p/4482294.html) 192 | 193 | ZingBug,2020/6/28 194 | [EOF] 195 | -------------------------------------------------------------------------------- /notes/记BitMap-BitSet.md: -------------------------------------------------------------------------------- 1 | # 2020届秋招面试题总结——网络篇 2 | 3 | ## 前言 4 | 5 | 提到一个bitmap,肯定想到它是非常经典的海量数据处理工具,其本质是用bit数组的某一位来表示某一数据,从而一个bit数组可以表示海量数据。 6 | 7 | 在Java中,BitSet类实现了bitmap算法。BitSet是位操作的对象,值只有0或1即false和true,**内部维护了一个long数组,初始只有一个long,所以BitSet最小的size是64,当随着存储的元素越来越多,BitSet内部会动态扩充,最终内部是由N个long来存储**,这些针对操作都是透明的。 8 | 9 | **用1位来表示一个数据是否出现过,0为没有出现过,1表示出现过。使用用的时候既可根据某一个是否为0表示,此数是否出现过。** 10 | 11 | 一个1G的空间,有 `8*1024*1024*1024=8.58*10^9bit`,也就是可以表示85亿个不同的数。 12 | 13 | ## 源码实现 14 | 15 | 我们来看一下其内部实现,首先看一下**构造函数**。 16 | 17 | ```java 18 | private final static int ADDRESS_BITS_PER_WORD = 6; 19 | private long[] words;//用一个long数组来存储位 20 | public BitSet(int nbits) { 21 | // nbits can't be negative; size 0 is OK 22 | if (nbits < 0) 23 | throw new NegativeArraySizeException("nbits < 0: " + nbits); 24 | initWords(nbits); 25 | sizeIsSticky = true; 26 | } 27 | private void initWords(int nbits) { 28 | words = new long[wordIndex(nbits-1) + 1];//数组初始化 29 | } 30 | private static int wordIndex(int bitIndex) { 31 | return bitIndex >> ADDRESS_BITS_PER_WORD;//获取数组长度 32 | } 33 | ``` 34 | 35 | 关键在于wordIndex函数,注意这里ADDRESS_BITS_PER_WORD的值是6。为什么是6呢,答案很简单。 36 | 37 | 在最开始提到的:BitSet里使用一个Long数组里的每一位来存放当前Index是否有数存在。 38 | 39 | 因为在Java里Long类型是64位,所以一个Long可以存储64个数,而要计算给定的参数bitIndex应该放在数组(在BitSet里存在word的实例变量里)的哪个long里,只需要计算:bitIndex / 64即可,这里正是使用>>来代替除法(因为位运算要比除法效率高)。而64正好是2的6次幂,所以ADDRESS_BITS_PER_WORD的值是6。 40 | 41 | 通过wordIndex函数就能计算出参数bitIndex应该存放在words数组里的哪一个long里。 42 | 43 | 接下来看一下**set函数**。 44 | 45 | ```java 46 | public void set(int bitIndex) { 47 | if (bitIndex < 0) 48 | throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex); 49 | int wordIndex = wordIndex(bitIndex); 50 | expandTo(wordIndex); 51 | words[wordIndex] |= (1L << bitIndex); // Restores invariants 52 | checkInvariants(); 53 | } 54 | ``` 55 | 56 | 这里面比较重要的就是标红的那句。 57 | 58 | ```java 59 | words[wordIndex] |= (1L << bitIndex); 60 | ``` 61 | 62 | 类似于 63 | 64 | 1<<0 = 1(1) 65 | 66 | 1<<1 = 2(10) 67 | 68 | 1<<2 = 4(100) 69 | 70 | 1<<3 = 8(1000) 71 | 72 | **(1L << bitIndex)就意味着将bitIndex所对应的位设置为1,表示这个数字已经存在了。**剩下的就是用|来将当前算出来的和以前的值进行合并了。 73 | 74 | 最后看一下**get函数**。 75 | 76 | ```java 77 | public boolean get(int bitIndex) { 78 | if (bitIndex < 0) 79 | throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex); 80 | checkInvariants(); 81 | int wordIndex = wordIndex(bitIndex); 82 | return (wordIndex < wordsInUse) 83 | && ((words[wordIndex] & (1L << bitIndex)) != 0); 84 | } 85 | ``` 86 | 87 | 整体意思就是判断bitIndex所对应的位数是否为1。 88 | 89 | ## 应用场景 90 | 91 | 怎么去**运用**这个bitMap思想呢。 92 | 93 | 首先,在原理上(1L << bitIndex),这个联想到LeetCode上一个题,第268题,**Missing Number**,从0到n之间取出n个不同的数,找出漏掉的那个。 94 | 95 | 这个题的思路有很多,先举一个,先对0-n的数运用加法进行求和得到SUM1,然后对数组遍历求和得到SUM2,那漏掉的就是SUM1-SUM2了。 96 | 97 | 这次我们采用bitset的思想来解决。遍历数组,用一个整数的位来记录。 98 | 99 | ```java 100 | public int missingNumberInByBitSet(int[] array) { 101 | int bitSet = 0; 102 | for (int element : array) { 103 | bitSet |= 1 << element; 104 | } 105 | for (int i = 0; i < array.length; i++) { 106 | if ((bitSet & 1 << i) == 0) { 107 | return i; 108 | } 109 | } 110 | return 0; 111 | } 112 | ``` 113 | 114 | 这里不用数组,只用一个int就可以实现。 115 | 116 | 回到这个文章开头,bitMap思想最主要的应用是在大数据量上。比如说在20亿个整数中找到一个数,或者找到唯一重复的数字,如下。 117 | 118 | ```java 119 | public class BitSetTest { 120 | public static void main(String[] args) 121 | { 122 | BitSet bitSet=new BitSet(2000000000); 123 | int[] nums={1,2,3,4,5,6,7,5};//等等,一共有20亿个数字 124 | for(int num:nums) 125 | { 126 | if(bitSet.get(num)) 127 | { 128 | System.out.println(num); 129 | break; 130 | } 131 | bitSet.set(num); 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | 为什么刚才那个问题是20亿个数字呢,是因为BitSet在构造函数中,**传入的长度变量类型为int,int最大值为2147483647**,也就是20亿左右,所以限制了每个BitSet的长度。 138 | 139 | 如果遇到比20亿更多的数字怎么办,比如说50亿。那么可以采用**分段思想**,将50亿中的最大值进行分段,得到多个BitSet,每段的BitSet都在int下标范围内。代码如下所示。 140 | 141 | ```java 142 | public class HugeBitset { 143 | //分段式bitset存储在list中 144 | private List bitsetList = new ArrayList(){}; 145 | //下标最大值 146 | private long max; 147 | //每段值大小 148 | private int seg; 149 | //根据下标最大值,段大小初始化bitset 150 | HugeBitset(long max,int seg){ 151 | this.max = max; 152 | this.seg = seg; 153 | int segs = (int)(max/seg) +1; 154 | for(int i = 0;i[] interfaces,InvocationHandler h) 72 | ``` 73 | 74 | 在这个方法内,主要流程为: 75 | 76 | **1、根据类加载器classLoader和接口数组来生成代理类的class对象。** 77 | 78 | ```java 79 | final Class[] intfs = interfaces.clone(); 80 | Class cl = getProxyClass0(loader, intfs);//二进制的class文件 81 | ``` 82 | 83 | **2、获取代理类中参数为InvocationHandler的构造方法。** 84 | 85 | ```java 86 | //private static final Class[] constructorParams = { InvocationHandler.class }; 87 | final Constructor cons = cl.getConstructor(constructorParams); 88 | ``` 89 | 90 | **3、判断代理类cl代理类Class的修饰符是不是pulic类型,如果是,则传入自定义的InvocationHandler,并返回指定构造器cons的实例,即代理对象的实例。如果不是,则将cons构造方法设置为可访问。** 91 | 92 | ```java 93 | if (!Modifier.isPublic(cl.getModifiers())) { 94 | AccessController.doPrivileged(new PrivilegedAction() { 95 | public Void run() { 96 | cons.setAccessible(true); 97 | return null; 98 | } 99 | }); 100 | } 101 | return cons.newInstance(new Object[]{h});//传入自定义的InvocationHandler 102 | ``` 103 | 104 | 另外,还需要看一下通过类加载器和接口数组来生成代理类字节码的方法getProxyClass0(**使用了缓存技术,只需要生成一次**)。 105 | 106 | ```java 107 | private static final WeakCache[], Class> 108 | proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()); 109 | 110 | private static Class getProxyClass0(ClassLoader loader, 111 | Class... interfaces) { 112 | if (interfaces.length > 65535) { 113 | throw new IllegalArgumentException("interface limit exceeded"); 114 | } 115 | // If the proxy class defined by the given loader implementing 116 | // the given interfaces exists, this will simply return the cached copy; 117 | // otherwise, it will create the proxy class via the ProxyClassFactory 118 | return proxyClassCache.get(loader, interfaces); 119 | } 120 | ``` 121 | 122 | 可知,proxyClassCache是WeakCache类型,如果缓存中包含接口数组和classloader的class类已经存在,就返回缓存副本,否则通过ProxyClassFactory创建一个对应的class对象。 123 | 124 | 到此为止,讲完了代理类ProxyBean的生成流程,那既然已经实现了InvocationHandler接口,就需要重写**invoke方法**。 125 | 126 | 在invoke(Object proxy, Method method, Object[] args)方法中利用**反射调用**实际的方法。注意,是委托类的方法,而不是代理类的方法。这也是前面提到的JDK动态代理中,内部被调用的方法不会被代理,因为调用的printUser方法是由委托类target执行,里面调用的vaildUser方法不会被代理。 127 | 128 | ```java 129 | method.invoke(target,args);//反射调用 130 | ``` 131 | 132 | target是委托类,也就是UserServiceImpl。而传入参数中的proxy是代理的字节码类。 133 | 134 | 代理类完事后,下面进行测试。 135 | 136 | ```java 137 | public static void testJdk() { 138 | UserService userService = (UserService) ProxyBean.getProxyBean(new UserServiceImpl()); 139 | userService.printUser(); 140 | 141 | //将代理类字节码保存下来 142 | byte[] classFile= ProxyGenerator.generateProxyClass("$Proxy0",UserServiceImpl.class.getInterfaces()); 143 | String path="自定义保存路径"; 144 | try { 145 | FileOutputStream fos=new FileOutputStream(path); 146 | fos.write(classFile); 147 | fos.flush(); 148 | System.out.println("写入成功"); 149 | } 150 | catch (Exception e){ 151 | System.out.println("写入错误"); 152 | } 153 | } 154 | 155 | 运行结果: 156 | before JDK ... 157 | I'm real 158 | I'm Zingbug 159 | after JDK ... 160 | ``` 161 | 162 | 我们可以看出内部被调用的方法vaildUser()没有被代理,符合之前的结论和原因分析。另外,我们将生成的字节码类文件保留下来,编译出来分析一波。 163 | 164 | ```java 165 | public final class $Proxy0 extends Proxy implements UserService {//继承自Proxy,实现了UserService接口 注意这行!!! 166 | private static Method m1; 167 | private static Method m4; 168 | private static Method m2; 169 | private static Method m0; 170 | private static Method m3; 171 | public $Proxy0(InvocationHandler var1) throws { 172 | super(var1); 173 | } 174 | public final boolean equals(Object var1) throws { 175 | try { 176 | return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); 177 | } catch (RuntimeException | Error var3) { 178 | throw var3; 179 | } catch (Throwable var4) { 180 | throw new UndeclaredThrowableException(var4); 181 | } 182 | } 183 | public final void printUser() throws {//注意这行!!! 184 | try { 185 | super.h.invoke(this, m4, (Object[])null);//注意这行!!! 186 | } catch (RuntimeException | Error var2) { 187 | throw var2; 188 | } catch (Throwable var3) { 189 | throw new UndeclaredThrowableException(var3); 190 | } 191 | } 192 | public final void vaildUser() throws { 193 | try { 194 | super.h.invoke(this, m3, (Object[])null); 195 | } catch (RuntimeException | Error var2) { 196 | throw var2; 197 | } catch (Throwable var3) { 198 | throw new UndeclaredThrowableException(var3); 199 | } 200 | } 201 | //省略部分代理的Object方法源码 202 | static { 203 | try { 204 | m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); 205 | m4 = Class.forName("com.example.demo.service.UserService").getMethod("printUser");//注意这行!!! 206 | m2 = Class.forName("java.lang.Object").getMethod("toString"); 207 | m0 = Class.forName("java.lang.Object").getMethod("hashCode"); 208 | m3 = Class.forName("com.example.demo.service.UserService").getMethod("vaildUser");//注意这行!!! 209 | } catch (NoSuchMethodException var2) { 210 | throw new NoSuchMethodError(var2.getMessage()); 211 | } catch (ClassNotFoundException var3) { 212 | throw new NoClassDefFoundError(var3.getMessage()); 213 | } 214 | } 215 | } 216 | ``` 217 | 218 | 从这个字节码中,可以发现代理类继承自Proxy,实现了UserService方法,其构造函数就是传递了InvocationHandler参数,同时实现了Object类的equals ,hashcode,toString方法,以及UserService接口自己定义的printUser和vaildUser方法。并且,所有接口方法的实现都委托给InvocationHandler的invoke方法了,这也就是实现代理模式的地方了。 219 | 220 | **下面来看CGLIB**,其中被代理的类还是UserServiceImpl类。 221 | 222 | **1、定义一个拦截器。在调用目标方法时,CGLib会回调MethodInterceptor接口方法拦截,来实现自己的代理逻辑,类似于JDK中的InvocationHandler接口。** 223 | 224 | ```java 225 | public class UserCglib implements MethodInterceptor { 226 | @Override 227 | public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { 228 | System.out.println("before CGLIB ..."); 229 | Object value=methodProxy.invokeSuper(o,objects);//不要用methodProxy.invoke() 注意这行!!! 230 | System.out.println("after CGLIB ..."); 231 | return value; 232 | } 233 | } 234 | ``` 235 | 236 | 看一下intercept方法入参: 237 | 238 | - o:cglib生成的代理对象, 239 | - method:被代理对象方法, 240 | - objects:方法入参, 241 | - methodProxy:代理方法。 242 | 243 | **2、生成动态代理类,并在代理类上进行方法测试。** 244 | 245 | ```java 246 | public static void testCglib() 247 | { 248 | System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"自定义路径");//将代理类的字节码保存下来 249 | Enhancer enhancer=new Enhancer(); 250 | enhancer.setSuperclass(UserServiceImpl.class); 251 | enhancer.setCallback(new UserCglib()); 252 | UserService userService=(UserService)enhancer.create(); 253 | userService.printUser(); 254 | } 255 | 256 | 运行结果: 257 | before CGLIB ... 258 | before CGLIB ... 259 | I'm real 260 | after CGLIB ... 261 | I'm Zingbug 262 | after CGLIB ... 263 | ``` 264 | 265 | 这里Enhancer是CGLib的一个字节码增强器,它可以方便的对你想要处理的类进行扩展。首先将被代理类UserServiceImpl设置成父类,然后设置拦截器UserCglib,最后执行enhancer.create()动态生成一个代理类,并从Object强制转型成父类型UserServiceImpl。 266 | 267 | 可以看出,与JDK原生方法不同的是,这次内部被调用的方法vaildUser()也被代理了。 268 | 269 | 执行testCglib()测试可以得到CGLib生成的class文件,一共有三个class文件,反编译以供学习。 270 | 271 | ![Y43vMF.png](https://s1.ax1x.com/2020/05/19/Y43vMF.png) 272 | 273 | 其中UserServiceImpl$$EnhancerByCGLIB$$dd4bde41.class就是CGLIB生成的代理类,它继承了UserServiceImpl类。无用代码不再展示。 274 | 275 | ```java 276 | public class UserServiceImpl$$EnhancerByCGLIB$$dd4bde41 extends UserServiceImpl implements Factory { 277 | private boolean CGLIB$BOUND; 278 | public static Object CGLIB$FACTORY_DATA; 279 | private static final ThreadLocal CGLIB$THREAD_CALLBACKS; 280 | private static final Callback[] CGLIB$STATIC_CALLBACKS; 281 | private MethodInterceptor CGLIB$CALLBACK_0;//拦截器 注意这行!!! 282 | private static Object CGLIB$CALLBACK_FILTER; 283 | private static final Method CGLIB$printUser$0$Method;//被代理方法printUser() 注意这行!!! 284 | private static final MethodProxy CGLIB$printUser$0$Proxy; 285 | private static final Object[] CGLIB$emptyArgs; 286 | private static final Method CGLIB$vaildUser$1$Method;//被代理方法vaildUser() 注意这行!!! 287 | private static final MethodProxy CGLIB$vaildUser$1$Proxy; 288 | private static final Method CGLIB$equals$2$Method; 289 | private static final MethodProxy CGLIB$equals$2$Proxy; 290 | private static final Method CGLIB$toString$3$Method; 291 | private static final MethodProxy CGLIB$toString$3$Proxy; 292 | private static final Method CGLIB$hashCode$4$Method; 293 | private static final MethodProxy CGLIB$hashCode$4$Proxy; 294 | private static final Method CGLIB$clone$5$Method; 295 | private static final MethodProxy CGLIB$clone$5$Proxy; 296 | static void CGLIB$STATICHOOK1() { 297 | CGLIB$THREAD_CALLBACKS = new ThreadLocal(); 298 | CGLIB$emptyArgs = new Object[0]; 299 | Class var0 = Class.forName("com.example.demo.service.UserServiceImpl$$EnhancerByCGLIB$$dd4bde41");//代理类 注意这行!!! 300 | Class var1;//委托类UserServiceImpl 注意这行!!! 301 | Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods()); 302 | CGLIB$equals$2$Method = var10000[0]; 303 | CGLIB$equals$2$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$2"); 304 | CGLIB$toString$3$Method = var10000[1]; 305 | CGLIB$toString$3$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$3"); 306 | CGLIB$hashCode$4$Method = var10000[2]; 307 | CGLIB$hashCode$4$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$4"); 308 | CGLIB$clone$5$Method = var10000[3]; 309 | CGLIB$clone$5$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$5"); 310 | var10000 = ReflectUtils.findMethods(new String[]{"printUser", "()V", "vaildUser", "()V"}, (var1 = Class.forName("com.example.demo.service.UserServiceImpl")).getDeclaredMethods()); //注意这行!!! 311 | CGLIB$printUser$0$Method = var10000[0]; //注意这行!! 312 | CGLIB$printUser$0$Proxy = MethodProxy.create(var1, var0, "()V", "printUser", "CGLIB$printUser$0"); //注意这行!! 313 | CGLIB$vaildUser$1$Method = var10000[1]; //注意这行!! 314 | CGLIB$vaildUser$1$Proxy = MethodProxy.create(var1, var0, "()V", "vaildUser", "CGLIB$vaildUser$1"); } //注意这行!! 315 | //代理方法(methodProxy.invokeSuper会调用) 316 | final void CGLIB$printUser$0() { super.printUser(); } 317 | //代理方法(methodProxy.invoke会调用,这就是为什么在拦截器中调用methodProxy.invoke会死循环,一直在调用拦截器) 318 | public final void printUser() { 319 | MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; 320 | if (var10000 == null) { 321 | CGLIB$BIND_CALLBACKS(this); 322 | var10000 = this.CGLIB$CALLBACK_0; 323 | } 324 | if (var10000 != null) { 325 | var10000.intercept(this, CGLIB$printUser$0$Method, CGLIB$emptyArgs, CGLIB$printUser$0$Proxy); 326 | } else { 327 | super.printUser(); 328 | } 329 | } 330 | final void CGLIB$vaildUser$1() { 331 | super.vaildUser(); 332 | } 333 | public final void vaildUser() { 334 | MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; 335 | if (var10000 == null) { 336 | CGLIB$BIND_CALLBACKS(this); 337 | var10000 = this.CGLIB$CALLBACK_0; 338 | } 339 | if (var10000 != null) { 340 | var10000.intercept(this, CGLIB$vaildUser$1$Method, CGLIB$emptyArgs, CGLIB$vaildUser$1$Proxy); 341 | } else { 342 | super.vaildUser(); 343 | } 344 | } 345 | ....... 346 | } 347 | ``` 348 | 349 | 通过代理类的源码可以看到,代理类会获得所有在父类继承来的方法(包括所有的Object方法),并且会有MethodProxy与之对应,比如 CGLIB$printUser$0$Method、CGLIB$printUser$0$Proxy等。 350 | 351 | 对于方法调用过程:代理对象调用this.printUser()方法 -> 调用拦截器 -> methodProxy.invokeSuper函数 -> CGLIB$printUser$0()方法 -> 委托类对象的printUser()方法。 352 | 353 | 拦截器MethodInterceptor中就是由MethodProxy的invokeSuper方法调用代理方法的,MethodProxy非常关键。具体看一下这个代码。 354 | 355 | 1、**首先,创建MethodProxy。** 356 | 357 | ```java 358 | public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) { 359 | MethodProxy proxy = new MethodProxy(); 360 | proxy.sig1 = new Signature(name1, desc); 361 | proxy.sig2 = new Signature(name2, desc); 362 | proxy.createInfo = new MethodProxy.CreateInfo(c1, c2); 363 | return proxy; 364 | } 365 | ``` 366 | 367 | 其中,c1是委托对象Class,c2是代理对象Class,desc是入参类型,name1是委托方法名,name2是代理方法名。 368 | 369 | 2、**对于invokeSuper调用。** 370 | 371 | ```java 372 | public Object invokeSuper(Object obj, Object[] args) throws Throwable { 373 | try { 374 | this.init();//初始化,生成FastClass,并放入缓存。 注意这行!!! 375 | MethodProxy.FastClassInfo fci = this.fastClassInfo; 376 | return fci.f2.invoke(fci.i2, obj, args);//直接调用方法 注意这行!!! 377 | } catch (InvocationTargetException var4) { 378 | throw var4.getTargetException(); 379 | } 380 | } 381 | 382 | private static class FastClassInfo { 383 | FastClass f1; 384 | FastClass f2; 385 | int i1; 386 | int i2; 387 | private FastClassInfo() { 388 | } 389 | } 390 | ``` 391 | 392 | 代码调用过程就是获取到代理类对应的FastClass,并执行了代理方法。还记得之前生成三个class文件吗? 393 | 394 | UserServiceImpl$$EnhancerByCGLIB$$dd4bde41$$FastClassByCGLIB$$6ad9babb.class就是代理类的 395 | 396 | FastClass,UserServiceImpl$$FastClassByCGLIB$$ff73797.class就是委托类的FastClass。 397 | 398 | 3、**下面这就引入了FastClass机制。** 399 | 400 | Cglib动态代理执行代理方法效率之所以比JDK的高是因为Cglib采用了FastClass机制,它的原理简单来说就是:为代理类和被代理类各生成一个Class,这个Class会为代理类或被代理类的方法分配一个index(int类型)。 401 | 402 | 这个index当做一个入参,FastClass就可以直接定位要调用的方法直接进行调用,这样**省去了反射调用**,所以调用效率比JDK动态代理通过反射调用高。下面我们反编译一个FastClass看看: 403 | 404 | ```java 405 | //根据方法签名获取index 406 | public int getIndex(Signature var1) { 407 | String var10000 = var1.toString(); 408 | switch(var10000.hashCode()) { 409 | case -1544526380: 410 | if (var10000.equals("vaildUser()V")) { 411 | return 1; 412 | } 413 | break; 414 | case -7204771: 415 | if (var10000.equals("printUser()V")) { 416 | return 0; 417 | } 418 | break; 419 | //省略其他Object类方法代码.... 420 | } 421 | return -1; 422 | } 423 | 424 | //根据index直接定位执行方法 425 | public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException { 426 | UserServiceImpl var10000 = (UserServiceImpl)var2; 427 | int var10001 = var1; 428 | try { 429 | switch(var10001) { 430 | case 0: 431 | var10000.printUser();//注意这行!!! 432 | return null; 433 | case 1: 434 | var10000.vaildUser();//注意这行!!! 435 | return null; 436 | case 2: 437 | return new Boolean(var10000.equals(var3[0])); 438 | case 3: 439 | return var10000.toString(); 440 | case 4: 441 | return new Integer(var10000.hashCode()); 442 | } 443 | } catch (Throwable var4) { 444 | throw new InvocationTargetException(var4); 445 | } 446 | throw new IllegalArgumentException("Cannot find matching method/constructor"); 447 | } 448 | ``` 449 | 450 | 其中,FastClass并不是跟代理类一块生成的,而是在第一次执行MethodProxy invoke/invokeSuper时生成的并放在了缓存中。 451 | 452 | ```java 453 | //MethodProxy invoke/invokeSuper都调用了init() 454 | private void init() { 455 | if (this.fastClassInfo == null) { 456 | synchronized(this.initLock) { 457 | if (this.fastClassInfo == null) { 458 | MethodProxy.CreateInfo ci = this.createInfo; 459 | MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo(); 460 | fci.f1 = helper(ci, ci.c1);//如果缓存中就取出,没有就生成新的FastClass 注意这行!!! 461 | fci.f2 = helper(ci, ci.c2); 462 | fci.i1 = fci.f1.getIndex(this.sig1);//获取方法的index 注意这行!!! 463 | fci.i2 = fci.f2.getIndex(this.sig2); 464 | this.fastClassInfo = fci; 465 | this.createInfo = null; 466 | } 467 | } 468 | } 469 | } 470 | ``` 471 | 472 | 至此,CGLIB动态代理原理就差不多搞清楚了,还是深究源码才能弄懂。 473 | 474 | ## 参考链接 475 | 476 | [jdk和cglib原理区别](https://blog.csdn.net/weixin_39158271/article/details/78074442) 477 | 478 | [JDK动态代理实现原理——JDK1.8](https://blog.csdn.net/huangwei18351/article/details/82460589) 479 | 480 | [CGLIB动态代理实现方式](https://www.cnblogs.com/monkey0307/p/8328821.html) 481 | 482 | 弥有,2019/7/25 483 | [EOF] 484 | -------------------------------------------------------------------------------- /notes/记单例模式.md: -------------------------------------------------------------------------------- 1 | # 单例模式 2 | 3 | 本文将一步步介绍单例模式。 4 | 5 | ## 1、懒汉模式 6 | 7 | ```java 8 | public class Singleton { 9 | private static final Singleton instance = new Singleton(); 10 | //私有的默认构造函数 11 | private Singleton() { 12 | } 13 | //静态工厂方法 14 | public static Singleton getInstance() { 15 | return instance; 16 | } 17 | } 18 | ``` 19 | 20 | 这是最简单的,缺点很明显,第一次加载类的时候会连带着创建Singleton实例,这样的结果与我们所期望的不同,因为创建实例的时候可能并不是我们需要这个实例的时候。同时如果这个Singleton实例的创建非常消耗系统资源,而应用始终都没有使用Singleton实例,那么创建Singleton消耗的系统资源就被白白浪费了。但是也有优点,这个是线程安全的。 21 | 22 | 为了避免这种情况,我们通常使用**惰性加载** 的机制,也就是在使用的时候才去创建。但简单的惰性加载会有线程安全问题,一般会加入锁来保证每次只有唯一线程获取实例。 23 | 24 | ## 2、饿汉模式 25 | 26 | ```java 27 | public class Singleton { 28 | private static Singleton instance = null; 29 | //私有的默认构造函数 30 | private Singleton() { 31 | } 32 | //静态工厂方法 33 | public synchronized static Singleton getInstance() { 34 | if (instance == null) { 35 | instance = new Singleton(); 36 | } 37 | return instance; 38 | } 39 | } 40 | ``` 41 | 42 | 这种方法解决了多线程并发安全问题,但是它却很影响性能,每次调用getInstance()方法的时候都必须获得Singleton类的锁(静态方法,synchronized锁针对的是类)。而实际上,当单例实例被创建后,其后的请求没有必要再使用互斥机制了。为了解决这个问题,提出了double-checked locking的解决方案。 43 | 44 | ## 3、双重检查锁 45 | 46 | ```java 47 | public static Singleton getInstance() { 48 | if (instance == null) { 49 | synchronized (instance) { 50 | if (instance == null) { 51 | instance = new Singleton(); 52 | } 53 | } 54 | } 55 | return instance; 56 | } 57 | ``` 58 | 59 | 当一个线程发出请求后,会先检查instance是否为null,如果不是则直接返回其内容,这样避免了进入synchronized块所需要花费的资源。这个方案似乎解决了面临的问题,但从JVM角度来说,这些代码依旧可能发生错误。 60 | 61 | 对于JVM而言,它执行的是一个个Java指令。在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。(即先赋值指向了内存地址,再初始化)这样就使出错成为了可能,我们以A、B两个线程为例: 62 | 63 | 1. A、B线程同时进入了第一个if判断。 64 | 2. A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton(); 65 | 3. 由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。 66 | 4. B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。 67 | 5. 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。 68 | 69 | ## 4、内部类 70 | 71 | ```java 72 | public class Singleton { 73 | //私有的默认构造函数 74 | private Singleton() { 75 | } 76 | //内部类 77 | private static class SingletonContainer { 78 | private static Singleton instance = new Singleton(); 79 | } 80 | //静态工厂方法 81 | public static Singleton getInstance() { 82 | return SingletonContainer.instance; 83 | } 84 | } 85 | ``` 86 | 87 | JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,避免了双重检查方案的问题。此外该方法也只会在第一次调用的时候使用互斥机制,这样就解决了懒汉模式的低效问题。最后instance是在第一次加载SingletonContainer类时被创建的,而SingletonContainer类则在调用getInstance方法的时候才会被加载,因此也实现了惰性加载。 88 | 89 | 弥有,2019年7月 90 | [EOF] 91 | -------------------------------------------------------------------------------- /notes/记循环依赖.md: -------------------------------------------------------------------------------- 1 | # 循环依赖 2 | 3 | ## 问题概述 4 | 5 | ### 什么是循环依赖 6 | 7 | Bean A 依赖 B,Bean B 依赖 A这种情况下出现循环依赖。 8 | 9 | Bean A → Bean B → Bean A 10 | 11 | 更复杂的间接依赖造成的循环依赖如下。 12 | 13 | Bean A → Bean B → Bean C → Bean D → Bean E → Bean A 14 | 15 | ### 循环依赖会产生什么结果 16 | 17 | 当Spring正在加载所有Bean时,Spring尝试以能正常创建Bean的顺序去创建Bean。 18 | 19 | 例如,有如下依赖: 20 | 21 | Bean A → Bean B → Bean C 22 | 23 | Spring先创建beanC,接着创建bean B(将C注入B中),最后创建bean A(将B注入A中)。 24 | 25 | 但当存在循环依赖时,Spring将无法决定先创建哪个bean。这种情况下,Spring将产生异常BeanCurrentlyInCreationException。 26 | 27 | 当使用**构造器注入**时经常会发生循环依赖问题。如果使用其它类型的注入方式能够避免这种问题。 28 | 29 | 下面举个例子。首先定义两个相互通过构造器注入依赖的bean。 30 | 31 | ```java 32 | @Component 33 | public class CircularDependencyA { 34 | private CircularDependencyB circB; 35 | @Autowired 36 | public CircularDependencyA(CircularDependencyB circB) {//构造器注入依赖 37 | this.circB = circB; 38 | } 39 | } 40 | ``` 41 | 42 | ```java 43 | @Component 44 | public class CircularDependencyB { 45 | private CircularDependencyA circA; 46 | @Autowired 47 | public CircularDependencyB(CircularDependencyA circA) { 48 | this.circA = circA; 49 | } 50 | } 51 | ``` 52 | 53 | 写一个配置类,保证能够扫描到上面创建的bean。 54 | 55 | ```java 56 | @Configuration 57 | @ComponentScan(basePackages = { "com.baeldung.circulardependency" }) 58 | public class TestConfig { 59 | } 60 | ``` 61 | 62 | 最后写一个测试案例 63 | 64 | ```java 65 | @RunWith(SpringJUnit4ClassRunner.class) 66 | @ContextConfiguration(classes = { TestConfig.class }) 67 | public class CircularDependencyTest { 68 | @Test 69 | public void givenCircularDependency_whenConstructorInjection_thenItFails() { 70 | // Empty test; we just want the context to load 71 | } 72 | } 73 | ``` 74 | 75 | 运行方法CircularDependencyTest将会产生异常: 76 | 77 | ```java 78 | The dependencies of some of the beans in the application context form a cycle: 79 | ┌────┐ 80 | | circa defined in file [CircularDependencyA.class] 81 | ↑ ↓ 82 | | circb defined in file [CircularDependencyB.class] 83 | └────┘ 84 | ``` 85 | 86 | ## 解决方法 87 | 88 | 处理这种问题有几种常见的方式。 89 | 90 | ### 1、重新设计 91 | 92 | 重新设计结构,消除循环依赖。 93 | 94 | ### 2、使用懒注解@Lazy 95 | 96 | 最简单的消除循环依赖的方式是通过延迟加载。具体代码如下。 97 | 98 | ```java 99 | @Component 100 | public class CircularDependencyA { 101 | private CircularDependencyB circB; 102 | @Autowired 103 | public CircularDependencyA(@Lazy CircularDependencyB circB) {//先被初始化 104 | this.circB = circB; 105 | } 106 | } 107 | ``` 108 | 109 | CircularDependencyB是被延迟加载了,它在注入CircularDependencyA中时,并没有完全初始化,而仅是通过一个代理(本文案例中是CGLIB动态代理)将它注入到CircularDependencyA。而CircularDependencyB只有在第一次需要时才会完全创建。 110 | 111 | 总结为:对于单实例bean,默认是在容器启动的时候创建对象。但当使用懒加载时,将对象的创建推迟到第一次获取的时候。 112 | 113 | ### 3、直接使用Autowired单独注入 114 | 115 | 直接使用@Autowired注入依赖,不要使用构造器的方式注入,如下。 116 | 117 | ```Java 118 | @Component 119 | public class CircularDependencyA { 120 | @Autowired 121 | private CircularDependencyB circB;//直接注入 122 | } 123 | ``` 124 | 125 | ```java 126 | @Component 127 | public class CircularDependencyB { 128 | @Autowired 129 | private CircularDependencyA circA;//直接注入 130 | } 131 | ``` 132 | 133 | ### 4、使用Setter注入 134 | 135 | 如下: 136 | 137 | ```java 138 | @Component 139 | public class CircularDependencyA { 140 | private CircularDependencyB circB; 141 | @Autowired 142 | public void setCircB(CircularDependencyB circB) {//Setter注入 143 | this.circB = circB; 144 | } 145 | } 146 | ``` 147 | 148 | ```java 149 | @Component 150 | public class CircularDependencyB { 151 | private CircularDependencyA circA; 152 | @Autowired 153 | public void setCircA(CircularDependencyA circA) { 154 | this.circA = circA; 155 | } 156 | } 157 | ``` 158 | 159 | 另外,还有一些其他方法,比如使用@PostConstruct等等。 160 | 161 | ## 总结 162 | 163 | 最后,当遇到循环依赖时。首先考虑是否能够通过重新设计依赖来避免循环依赖。如果确实需要循环依赖,那么可以通过前文提到的方式来处理。优先建议使用setter注入来解决。 164 | 165 | ## 参考链接 166 | 167 | [Circular Dependencies in Spring](https://www.baeldung.com/circular-dependencies-in-spring) 168 | 169 | 弥有,2019/7/26 170 | [EOF] 171 | --------------------------------------------------------------------------------