Java基础复习
Java和c++的区别:
- 首先是都是面向对象的编程语言
- 其次Java是没有指针,对内存管理更加安全
- Java是单继承的,c++是多继承的,但是Java的接口是多继承的
- Java有自动的垃圾回收机制,无需手动释放内存
- Java只支持方法的重载,但是c++支持方法重载和操作符的重载
import java 和javax有什么区别:
没有本质的区别,因为当时javax是java api的扩展包,随着时间的推移,javax的api包,逐渐称为java扩展api的一部分,但是合并javax包到Java包中太麻烦,所以将javax作为Java标准api的一部分
字符型常量和字符串常量的区别:
- 一个是单引号引用,一个是双引号引用,长度可以为0
- 字符型常量相当于一个ascll值,可以参与运算,但是字符串常量代表的是一个地址值(存放在内存中的地址值)
- 字符型常量的长度是2个字节,另一个占若干个字符
java中的注释有几种:
- 单行注释
- 多行注释
- 文本注释
continue、break、和 return 的区别是什么?
- 跳出当前循环,下面的循环继续进行
- 跳出整个循环体
- 返回 :
- 返回一个特定的值
- 直接返回,相当于方法的结束
== 和equals的区别:
==对于基本数据类型是值的比较,对于引用类型,比较的是引用比较,equals是引用比较,但是有的类重写了equals方法,例如String,Interger将引用比较变成了值比较
为什么重写equals就必须重写hashcode?
因为如果两个对象的hashcode的值相等,但是两个对象不是相等的,那么他们存储就会存在问题,所以重写equals也必须重写hashcode的算法来实现
例子:现在我们new两个对象,这两个对象都是自定义的相同的属性,如果这里我们不重写equals方法和hashcode方法,那么我们会发现计算这两个hashcode的值是不同的,但是实际上属性是相同的,那么存储到hashmap中,就会发现存储了两个一样的对象,因此在重写了equals方法之后,必须重写hashcode的计算方法,否则会出现存储的元素重复情况。
介绍一下hashcode
hashcode()这个方法是返回一个int类型的值,这个值对应的就是对象存储在散列表中的位置
为什么要有hashcode
因为在散列表中存储对象的时候,如果没有hashcode,那么就以hashset为例子,我们需要对每个地址中的对象都进行equals比较,这样耗费太多的时间与性能,所以用hashcode就很好的解决了这个问题,我们首先在存储对象的时候,可以先根据这个对象所算出的散列值在散列表中查找,如果已经存在那么就不用再进行重复插入了,这个值已经存在,如果没有那么就可以进行插入。
为什么两个对象有相同的hashcode值,但是他们却不相等
因为这个涉及到hash算法的问题,越简单的算法就代表着重复的可能性会越高,所以不可避免的就是hashcode可能一样,但是对象不一样,以hashset为例子,在存储对象的时候,如果两个对象的hashcode值是相同的,这时候会用equals进行比较,如果相同那么就不会进行插入,如果不相同就会散列到其他位置
Java中有哪几种数据类型:
有八种数据类型:
数字类型:byte、short、int、long、float、double
字符类型:char
布尔类型:Boolean
自动装箱与拆箱
装箱:将基本数据类型包装成对应的引用类型
实质上:装箱就是用了valueof()方法
拆箱:将引用类型转换为基本数据类型
实质上:拆箱就是用了***value()方法
什么是方法的返回值
就是一个方法执行后,得到的结果
方法有哪几种类型
- 无参无返回值
- 无参有返回值
- 有参无返回值
- 有参有返回值
在静态方法内调用一个非静态成员为什么是违法的
因为静态方法是属于类的,在类加载的时候,就会分配内存给静态方法,因此静态方法可以用过类来调用,但是非静态成员是属于对象的,只有当对象创建的时候,非静态成员才会被创建,因此在类加载的时候调用内存中不存在的非静态成员是违法的。
静态方法和实例方法有什么不同
这里有两处不同:
首先是方法的调用方式不同:
静态方法可以通过类名.方法名或者对象.方法名进行调用,但是实例方法只能是对象.方法名,但是为了不混淆,静态方法还是使用类型.方法名进行调用
成员变量访问限制:
静态方法只能访问静态变量,但是实例方法都可以访问
Java是按值传递的,不是按引用传递的
- 一个方法不能改变基本类型参数的值
- 一个方法可以改变对象的状态,但是不能让对象参数引用另一个对象
重写和重载:
- 重载:
- 方法的重载一般是发生在一个类中,重载的方法必须有着相同的方法名,但是参数类型的数量、顺序、方法返回值和访问修饰符都可以不同
- 编译器在进行重载方法的匹配时,叫做重载解析
- 重写:
- 发生在父类和子类中,方法重写返回值类型、方法名以及参数都必须一致
- 这里返回值类型可以小于等于父类的返回值类型
- 抛出的异常可以小于等于返回值类型
- 访问修饰符要大于等于父类的访问修饰符
深拷贝和浅拷贝:
- 浅拷贝:就是增加了一个新的指针,指向了原来的内存地址,当原地址发生变化,新的指针也跟着变化
- 深拷贝:也是增加了一个新指针,但是指向了一个新的内存地址,当原地址发生改变,新指针不会发生变化
面向对象和面向过程的区别
- 面向对象:面向对象没有面向过程的性能高,但是由于面向对象有着封装、继承、多态这些特性,所以有着易维护、易扩展、易复用的特点
- 面向过程:面向过程性能比面向对象高,但是类调用需要实例化,开销比较大。
成员变量和局部变量的区别:
- 成员变量可以被访问修饰符所修饰,但是局部变量不能,不过这里特殊情况就是他们都能被final所修饰
- 成员变量随着对象创建而产生,局部变量随着方法调用而消失
- 成员变量在对象实例化的时候会被赋给类型的默认值,但是局部变量不行
- 如果有static修饰符,那么成员变量时属于类的,但是如果成员变量没有static修饰符,就是数据实例化对象的。对象存储在堆中,而局部变量存储在栈中
创建一个对象用new运算符
对象实例存在与堆内存中,对象引用存在与栈内存中
对象的相等和指向他们引用的相等有什么区别
对象的相等指的是对象的值相等,而对象引用的相等时指向他们内存的地址相等
一个类的构造方法有什么作用?如果没有构造方法程序是否能正确执行,为什么
- 完成类的初始化工作
- 没有构造方法程序时可以执行的,因为即使类中没有声明构造方法,也会有默认的无参构造方法,如果有我们创建的构造方法,那么就必须使用我们自己创建的构造方法来初始化一个对象、
构造方法有哪些特点,可以被override吗?
- 构造方法的名字必须和类名相同
- 构造方法没有返回值,不能用void声明构造函数
- 构造方法自动执行,无需调用
- 构造方法不能被override,但是可以被重载,一个类中可以有多个构造方法
面向对象的三大特征:
- 封装:就是将对象中的属性,全部隐藏在对象内部,外部对象不能直接操作对象内部的属性,只能通过对象内部提供的方法来操作属性
- 继承:通过已有的类,创建出新类,也就是子类继承父类,子类拥有父类的所有属性和方法,并且子类可以重写父类的方法,以及可以定义自己的属性及方法,这里子类拥有父类的所有属性,但是不能直接访问父类的属性,只是拥有
- 多态:就是继承了父类或者接口的类,以多种不同的形态进行展示
String为什么是不可变的?
因为String类中,是以final来定义字符串存储的数组,所以是不可变的
Java9之后,就改变成byte数组来进行存储字符串
StringBuffer和StringBuilder以及String的区别:
- StringBuffer和StringBuilder都继承了abstractStringBuilder类,在abstractStringBuilder中,保存字符串的修饰符并没有final字段进行限制
- String中的对象是不可变的,所以可以理解为常量,线程安全,StringBuffer中的方法都加了同步锁,线程也是安全的,但是StringBuilder没有同步锁,所以线程是不安全的,但是StringBuilder的性能比StringBuffer的性能高处10%-15%
- 综上所述:
- 操作少量数据用String
- 单线程下,操作大量数据使用StringBuilder
- 多线程下,操作大量数据使用StringBuffer
何为反射?
- 反射就是在运行期间,我们拥有执行类的方法和分析类的能力
- 反射可以获取类的所有属性和方法,并且可以调用执行方法
反射机制的优缺点:
- 优点:可以让代码更加灵活,为各种框架提供开箱即用的便利性
- 缺点:让我们在运行期间拥有分析类和执行类的能力,这也造成了安全问题,性能也会有一定的影响
什么事序列化,什么是反序列化?
- 序列化:就是将要持久化的对象转换成机器能识别的二进制字节流的过程
- 反序列化:就是将二进制字节流转换成我们的对象
final关键字总结:
- final关键字意为最终的,不可修改的,可以用来修饰类,方法以及变量
- 用来修饰类:那么类不能被继承,而且这个类的所有成员方法都被隐式的用final修饰了
- 用来修饰方法:那么这个方法不能被重写
- 用来修饰变量:如果是基本类型,那么初始化后就不能被修改,如果是引用类型,那么在初始化之后,就不能指向别的对象
代理模式
静态代理
类中的所有接口都需要实现,一旦接口发生了改变,那么代理对象的方法也必须重写,因此会很麻烦
动态代理
动态代理,不需要实现所有的接口,可以针对我们需要代理的对象,然后实现其方法
对比:动态代理比静态代理更加灵活,静态代理在编译的时候就将接口,实现类以及代理类变成了.class文件,而动态代理是在运行期将代理类,接口实现变成二进制字节码加载进jvm中
集合
集合存放单一元素:Collection接口
collection接口
List
ArrayList
arrayList:object[]数组
怎么进行初始化的?
- 如果是默认无参构造,那么就会置为一个空数组
- 如果是有参,参数是大小,那么就会根据这个大小进行初始化数组的大小
- 如果参数是集合,首先判断传过来的集合大小是否为空,空则置为空数组,不为空就进行数组拷贝,数组大小就是集合传过来的数据大小
怎么进行增删改查的?(扩容根据数据添加来讲解)
- 增:首先判断数组是否为空,如果为空就将数组设为默认的大小10,然后进入到扩容判断,扩容判断是根据当前数组的大小和元素添加进数组之后的大小进行比较,如果大于那么就需要进行扩容,创建一个新数组,数组长度是原来数组的1.5倍,然后进行数组拷贝。
- 删:根据下标进行删除
- 改:set方法,首先判断下标是否越界,然后将旧值记录,将新值写入,返回旧值
- 查:通过下标进行查找
如何解决线程安全?线程不安全表现?
- 线程不安全表现:
- 输出值为null;
- 数组越界异常;
- 某些线程没有输出值;
- public static void main(String[] args) throws InterruptedException {
}List<String> list = new ArrayList<>(); for (int i =1; i<=30 ; i++) { new Thread(() -> { list.add("a"); list.add("b"); list.add("c"); list.add("d"); System.out.println(list.toString()); }).start(); }
- 解决办法:Vector、Collections.synchronizedList()、CopyOnWriteArrayList
- Vector在方法上添加了synchronize的锁,对代码进行加锁,力度大,所以代码执行效率低下
- CopyOnWrite容器即写时复制的容器。往一个容器添加元索的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy,复制出一个新的容器object[] newElements,然后往新的容器object[] newElements 里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements);。 这样做的好处是可以CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器
- 线程不安全表现:
频繁扩容带来的影响?如何解决?
- 添加效率低,可以通过初始化的时候设置容量大小,不过这样浪费空间。以空间换时间
LinkedList:jdk1.6之前是循环链表,之后变成了双向链表
- 如何进行增删改查的?
- 增:可以通过头插法和尾插法进行(中间插入:通过获取当前要插入节点的前面一个节点,然后通过指针进行添加),主要设置前驱节点后后驱节点的指向
- 删:删除头结点或者尾节点
- 改:首先查找要修改的节点,进行遍历找到,通过旧值返回,新值覆盖的操作进行修改。
- 查:通过下标索引进行查找,主要是遍历整个链表进行数据的返回
- 线程安全?如何解决?什么情况下出现不安全?
- 线程是不安全的
- 解决:Collections.synchronizedList()、ConcurrentLinkedQueue
- 如何进行增删改查的?
Vector:object[]数组 线程安全!
set
HashSet
- 底层用的hashMap存储数据
- LinkHashSet:其实是hash set的子类,不过底层用的是LinkHashMap进行存储元素的
- TreeSet:红黑树(自平衡的二叉树)
LinkedHashSet
TreeSet
Queue
- queue
- PriorityQueue:object[]数组
- ArraryQueue:object[]数组+双指针
- dequeue:双端队列
- 怎么实现双端队列的?
Map接口
三大接口:
- hashTable
- hashMap
- SortedMap
HashMap详解
jdk1.7的hashmap:
hashmap的构造方法,基本上都是初始化一些基本参数,比如默认的容量为16,默认的负载因子是0.75,还有阈值是通过计算得出的
为什么初始化的数组都是2的幂次
方便与运算
因为我们计算出来了hash值之后,还需要根据hash值计算出对应的数组下标,而这里的计算都是通过与运算进行的,所以都需要转换成二进制进行计算,如果此时数组的长度不是2的幂次,那么得到的与运算只是数组下标的部分值。
均匀分布
为2的幂次,length-1 为奇数,奇数的二进制最后一位是 1,这样便保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性。
而如果 length 为奇数的话,很明显 length-1 为偶数,它的最后一位是 0,这样 hash & (length-1) 的最后一位肯定为 0,即只能为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间
为什么计算hashcode需要右移以及进行异或运算?
用与运算计算完之后,会发现我们都是用低四位进行运算的,而高四位并没有进行运算,因此再进行了与运算之后,又将获取到的结果进行异或运算,这样高四位也得到了使用,这样就使散列更加均匀,减少了hash冲突的问题。
当我们存入的对象键是相同的,会进行怎么样的操作
会将原来的值进行覆盖
扩容机制:
在1.7的扩容机制是先判断是否扩容,再进行添加的,而在1.8之后,就是先插入后进行扩容的判断。
怎么进行扩容的?
1.7的扩容机制,首先判断size的大小是否超出阈值,如果超出了就会进行扩容,首先将数组的大小扩容到原来的两倍,也就是新创建一个数组,这个数组是原来的两倍,然后遍历这个数组中的链表,通过hashcode进行计算,是否需要rehash,如果得到的数组下标不变,那么就直接将这个链表转移到新的数组上,如果下标发生变化,就需要重新计算下标的值,然后将链表转移过去。
插入:jdk1.7的HashMap的插入,实际上就是根据key然后算出一个hashcode,然后根据hashcode的值来找到相应的地址,然后将对象插入进去,当然在插入的过程中是很有可能发生hash冲突的,因此解决hash冲突我们需要将此节点下加上链表,使用链表进行冲突元素的存储,而且存储冲突元素是根据头插法进行插入的。将冲突元素插入到头部之后,还需要将头节点移动到数组下表位置,否则遍历的时候找不到头结点的元素。
为什么使用头插法呢?
因为头插法效率比尾插法高,因为使用头插法可以直接将当前要插入的元素的引用指向头结点就可以的,但是如果使用尾插法,那么我们还需要遍历整个链表,直到找到链表的尾部才能进行插入
get方法:首先是通过key值计算hash,然后通过hash计算出数组的下标,然后找到这个数组下表对应的key值所对应的元素
jdk1.8的hashMap·
为什么1.8中新加了红黑树代替了链表?
因为对链表的添加很方便,但是遍历就会很麻烦,而红黑树对于添加和遍历都是差不多的,因此将红黑树代替了链表
为什么链表默认值大于等于8的时候变成红黑树,而链表默认值小于等于6的时候是链表,这两个临界值为什么不一样
因为当我们在这个临界值左右频繁的做增删操作的时候,如果这个临界值是一样的,那么就会导致频繁的在链表和红黑树之间进行转换,这样会严重的影响map的效率。
put方法:
首先根据key来计算hashcode,然后根据hashcode计算数组下标的位置,这里的计算也是同1.7一样,用与和异或来计算出来的,计算出来之后,就将这个元素插入进去,不过这里是尾插法,不再是1.7的头插法了;因为我们需要判断链表的长度,所以无论如何我们都需要对链表进行遍历,因此这里使用尾插法来插入元素,在插入过程中,依然会对这个键进行判断,如果重复则会进行覆盖。
线程安全问题
Hashmap是线程不安全的
- 为什么不安全?
- 插入的时候不安全,如果此时有两个线程进来进行插入操作,插入的对象计算出来的下标是一样的,那么如果此时线程一进行插入操作之后,紧接着线程二也进行插入操作,那么就会出现线程二插入的数据覆盖了线程一所插入的数据
- 针对1.7扩容会出现死循环问题,因为1.7中使用的是头插法,比如现在两个线程都检测到hashmap应该进行扩容操作,那么线程会同时进行扩容,因为是头插法,会将之前的数据倒序,这样如果第一个线程记录了头结点以及头结点的next节点,但是此时线程一被挂起,线程二进行执行,并且完成了扩容操作,那么线程一此时指向的节点就是扩容后链表的尾节点,那么进行遍历会将尾节点的next指针指向前一个节点,这样就形成了一个环
解决办法?
- Hashtable:对put和get方法直接加了synchronized互斥锁,效率很低,不长用
- ConcurrentHashMap
- Collections.synchronizedMap:也是加了synchronized
LinkedHashMap详解:
LinkedHashMap的底层实现因为它是继承HashMap,所以也是通过数组、链表和红黑树完成的,当然区别在于LinkedHashMap增加了两个指针,用于双向链表的维护。
添加元素操作
LinkedHashMap的插入操作其实是重写了HashMap的插入操作,只不过因为LinkedHashMap中有双向链表的操作,而HashMap中没有,因此在重写的方法中,添加了对双向链表的操作,首先将插入的元素new了一个新的Entry对象,然后将这个新的对象插入到链表中,然后通过后置的LinkNodeLast方法将两个指针指向链表的前置和后置
删除元素的操作
删除元素的操作,在LinkedHashMap中,其实也是重写了HashMap的remove方法,只不过这里还重写了一个AfterNodeRemove方法,这个方法就是在删除了节点之后的操作
主要的步骤:
- 首先定位要删除的节点位置
- 然后删除这个节点
- 之后删除双向链表的指向
访问顺序的维护
LinkedHashMap是按插入顺序维护链表的,不过我们可以在初始化的时候指定accessOrder为ture。这样就是使他按访问顺序维护链表,当我们访问一个节点的时候,就会将这个节点移到末尾。
ConcurrentHashmap详解:(https://blog.csdn.net/zycxnanwang/article/details/105424734)(https://blog.csdn.net/wangnanwlw/article/details/111587507)
1.7ConcurrentHashmap
数据结构:Segment数组加链表
怎么保证线程安全的?
主要是通过获取Segment分段锁来保证线程安全的。
get操作
1.7的时候,get操作并没有加锁,因为它所有的共享变量都定义成volatile类型,保证了变量在线程间的可见性
put操作
1.7的时候,首先获取segment锁,然后判断是否扩容,再进行添加操作
1.8ConcurrentHashmap
数据结构:数组+链表+红黑树
怎么保证线程安全的?
初始化数组的时候怎么保证线程安全的?
在JDK1.8中,初始化ConcurrentHashMap的时候这个Node[]数组是还未初始化的,会等到第一次put方法调用时才初始化,线程如果要进行初始化,首先会通过CAS操作将标志位置为-1,别的线程同时进来进行初始化的时候,如果标志位不为0,那么就会等待,进来的线程就会完成初始化操作,这样就保证了只有一个线程完成初始化工作。
put操作怎么保证线程安全的?
putValue函数,首先调用spread函数,计算hash值,之后进入一个自旋循环过程,直到插入或替换成功,才会返回。如果table未被初始化,则调用initTable进行初始化。之后判断hash映射的位置是否为null,如果为null,直接通过CAS自旋操作,插入元素成功,则直接返回,如果映射的位置值为MOVED(-1),则直接去协助扩容,排除以上条件后,尝试对链头Node节点f加锁,加锁成功后,链表通过尾插遍历,进行插入或替换。红黑树通过查询遍历,进行插入或替换。之后如果当前链表节点数量大于阈值,则调用treeifyBin函数,转换为红黑树最后通过调用addCount,执行CAS操作,更新数组大小,并且判断是否需要进行扩容扩容怎么保证线程安全?
- 构建一个nextTable,大小为table的两倍。
- 把table的数据复制到nextTable中。
get怎么保证线程安全?
判断table是否为空,如果为空,直接返回null;
首先计算hash值,定位到该table索引位置,如果是首节点符合就返回;
如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable(扩容新数组)
- eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
- eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。 通过遍历链表或则树结构找到对应的节点,返回value值。
HashTable详解
- 底层数据结构:数组+链表
- 通过对方法加synchronize保证了线程安全,性能很差
- 初始化默认的容量为11,扩容机制就是2n+1
TreeMap底层实现:
底层数据结构:红黑树
弄清楚了红黑树,基本上TreeMap就没有什么秘密了
红黑树详解:
定义:红黑树是一个自平衡的二叉树,一种高效的查找树,可以在O(logN)时间内完成增删查等操作。
性质:
- 节点是红色或者黑色
- 根是黑色
- 所有叶子都是黑色
- 每个红色节点必须有两个黑色子节点,并且不能出现连续的两个红色节点
- 从任意节点到叶子节点,包含的黑色节点都是相同的
插入:
删除:
List、set、queue、Map的区别?
List、set、queue、map的区别:
List:存储的数据是有序的,可重复的
set:存储的数据是无序的,不可重复的
queue:按照特定的顺序进行存储,数据可以重复
map:通过键值对进行存储,key只能是唯一的,value可以是重复的,他们都是无序的
并发基础
什么是进程?
比如在电脑上运行一个程序,进程就是这个程序运行的基本单位,程序的一次运行就代表着进程的创建,运行到消亡的过程,因此进程是动态的。‘
什么是线程
线程是比进程更小的执行单位,一个进程中可以包含多个线程,与进程不同的是,同类的多个线程共享进程区的堆和方法区资源,而每个线程又拥有自己的程序计数器、虚拟机栈、本地方法栈。线程又被称为轻量级进程
对象的创建过程:
- 类加载检查:虚拟机遇到了new指令的时候,首先会检查这个指令的参数是否能在常量池中定位到这个类的引用,然后检查这个符号引用代表的类是否存在,如果不存在才会进行类加载过程
- 分配内存:在类加载完成之后,虚拟机就会分配一块堆的内存给这个对象。分配的方式有两种,这取决于Java堆是否规整,Java堆是否规整又取决于垃圾收集器是否带有压缩整理功能
- 指针碰撞:适用于堆内存规整的情况,用过的内存全部分配到一边,没用过的内存全部分配到另一边,并且中间有一个分界指针
- 空闲列表:适用于对内存不规整的情况,虚拟机通过维护一个列表来分配内存,它会找一个足够大的堆内存划分给对象实例
- 初始化零值
- 设置对象头
- 执行init方法
对象的访问定位
- 句柄
通过句柄访问的话,首先在Java堆中就会分配一块内存用于存储句柄池,句柄池中存储着到对象实例和对象类型的指针;实例池存在于Java堆中,而对象类型则存储在方法区中 - 直接指针
如果使用直接指针,那么reference指向的直接就是对象实例的数据,这时候我们就需要考虑如何存放数据类型的相关信息
程序计数器为什么是私有的?
主要是因为线程切换后,能正确的恢复到执行位置
程序计数器的作用:
1. 字节码解释器通过改变程序计数器来执行指令,从而实现代码的流程控制
2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而能在线程切换回来的时候知道上次运行到哪了
虚拟机栈和本地内存栈为什么是私有的?
虚拟机栈:因为每个Java方法在执行的同时都会创建一个栈针用于存储局部变量表,操作数栈,常量池引用等信息,从方法的执行到完成对应着,栈针在虚拟机栈中的入栈和出栈操作
本地内存栈:和虚拟机栈的作用是相同的,区别在于本地内存栈是为Native方法服务的,而虚拟机栈是为Java方法服务的。
简单的介绍一下堆和方法区
首先这两个都是线程的共享资源,堆是进程中最大的内存,主要用于存储新创建的对象,而方法区则用于存储已被加载的类信息、常量、静态变量等信息
并发和并行的区别
并发:同一时间内多个任务同时进行
并行:单位时间内,多个任务同时进行
为什么要使用多线程呢?
从计算机底层原理解释:线程也称之为轻量级进程,是程序的最小执行单位,线程的切换和调用的成本远远小于进程。然后现代的多核CPU的出现意味着多个线程可以同时运行,减少上下文切换的开销。
从互联网发展来看:现在的系统几乎都是要支持百万级甚至千万级的并发量,而多线程并发编程正是支持这些高并发的系统的基础,充分利用多线程就机制可以有效的提升高并发系统的性能
使用多线程可能带来什么问题
内存泄露、死锁、线程不安全
线程的生命周期和状态
初始化->运行->阻塞->等待->超时等待->终止
什么是上下文切换?
上下文切换意思就是,保存当前线程运行的条件和状态,当下次切换到这个线程的时候,可以恢复上次的状态进行运行。
什么时候会发生上下文切换呢?
- 主动让出了cpu。例如调用了sleep()、wait()方法
- 当前线程的时间片用完
- 调用阻塞类型的系统中断
什么是线程死锁?如何避免死锁
死锁:多个线程同时被阻塞,他们中的一个或者多个都在等待某个资源的释放,因为这些线程被无限期的阻塞,导致程序无法正常终止。
产生死锁的必要条件:
- 互斥条件:该资源任意时刻只能被一个线程占用
- 请求与保持
- 不剥夺
- 循环等待
如何预防死锁呢?(破坏产生死锁的必要条件)
- 破坏请求与保持条件
- 破坏不剥夺条件
- 破坏循环等待条件
如何避免死锁?
就是在分配资源的时候借助算法(银行家算法)来对资源分配进行计算,使其进入安全状态
安全状态:系统能够按照某种推进顺序,为每个进程分配所需的资源,直到进程所需的资源全部完成分配。
Java中是如何解决死锁问题的?
出现死锁一般都是
获取锁的顺序不一致
,容易导致死锁状况解决:可以让线程获取锁的顺序一致,可以减少死锁的情况
加锁时限
在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行
sleep和wait方法的异同
- sleep没有释放锁,wait释放锁
- 它们俩都可以暂停线程,但是wait通常用于线程的交互/通信,sleep通常用于线程的暂停
- sleep方法暂停之后可以让线程自动苏醒,但是wait方法不会,除非在wait方法上设置一个超时时间,那么时间超时后就会自动苏醒,但是如果不设置超时时间,那么就必须调用notify或者notifyall方法让线程苏醒
为什么不能直接调用run方法,而是需要调用start来执行run方法
因为如果直接使用run方法,那么就会将这个线程当做为main方法下的线程来执行,并不是以线程的方式来执行。而使用start方法运行run方法的时候,首先线程会先进入就绪状态,当有时间片分配到当前线程的时候,才会运行run方法,这是线程的完整的执行流程。
对synchronize的理解
synchronize主要是解决多个线程之间访问资源的同步性
synchronize可以保证被它修饰的方法或者代码块只能被一个线程执行
synchronize在jdk6之前被称之为重量级锁,因为在6之前,synchronize是依靠操作系统的mutex lock进行实现的,这就相当于我们对线程的操作,都需要依靠操作系统来配合完成,会花费很长的时间成本。在6之后对synchronize进行了很大的优化,这就让原始的synchronize不再是重量级锁了,因为6之后加入了很多的新锁,比如首先是无锁状态,然后还有偏向锁,轻量级锁最后会变成重量级锁
自己是怎么使用synchronize?
- 修饰实例方法:对当前对象的实例进行加锁
- 修饰静态方法:对当前类进行加锁
- 修饰代码块:给指定的对象加锁,给对象/类加锁
说说jdk6之后,对synchronize做了那些优化
优化之后的synchronize,现在锁的状态主要有四种:无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态,随着竞争的激烈,锁会逐渐升级,且锁只能升级成越来越重的锁,而不会降级,这种策略提高获取锁和释放锁的效率。
锁升级的过程?
锁的状态:无锁->偏向锁->轻量级锁->重量级锁
偏向锁
当一个线程访问同步块的时候,会在对象头和栈针中记录锁偏向的线程ID,然后下一次进来的时候就不用通过CAS操作来加锁和解锁,只需要简单的验证一下MarkWord里面指向的是否为当前线程的偏向锁,如果成功就获取锁,如果失败则需要再测试一下MarkWord中偏向锁的标志位是否为1,如果是则尝试使用Cas将对象头的偏向锁指向当前线程,如果不是则尝试CAS竞争锁。
synchronize和reentrantlock的区别
- 两者都是可重入锁:可重入锁的意思就是当线程获取了一个对象的锁之后,锁没有被释放,但是还是可以再次获取这个锁,如果是不可重入锁那么就会造成死锁的状态,因为每个线程获取锁的时候,计数器会加1,而线程释放锁的时候计数器就-1也就是0才能释放锁。
- synchronize是依赖JVM实现的,而reentrantlock是依赖于API实现的(jdk层面的实现,需要lock、unlock方法配合try/finally块来完成)
- reentrantlock比synchronize新增了三个功能
- 等待可中断:中断等待锁的机制,线程可以选择放弃等待改为做其他的事情
- 可实现公平锁:reentrantlock默认是非公平的,但是可以通过构造方法将它改成公平锁(先等待的线程先获得锁)
- 可实现选择性通知:一个Lock对象中可以创建多个condition对象,然后线程对象可以指定的注册在某个condition对象中,从而可以有选择性的进行线程的通知。(synchronize中Lock对象只有一个Condition对象,这样所有的线程对象都注册在这一个condition对象中,这样使得如果调用notifyall就会使所有的线程都进行了通知,会造成很大的效率问题)
volatile关键字
CPU缓存模型
为什么要使用CPU高速缓存模型呢?
用CPU高速缓存模型是为了解决CPU处理速度和内存处理速度不对等的问题。
说说JMM(Java内存模型)
volatile关键字除了防止JVM的指令重排,还保证了变量的可见性
并发编程的三个重要特征
- 原子性:一次或多次操作,要么全部执行成功,要么全部执行失败
- 可见性:当一个线程对共享变量进行了修改的时候,其他的线程都是可以立即看到修改后的值,volatile保证了共享变量了可见性
- 有序性:代码在执行的过程中有先后顺序,但是Java编译器在运行期间会进行优化,所以可能导致执行的顺序并不是编写代码时的顺序,volatile关键字可以防止指令重排保证代码执行的有序性
说说synchronize和volatile的区别
它们是互补的存在,而不是对立的存在
- volatile是线程同步的轻量级实现,性能比synchronize好,但是volatile只能用于变量,而synchronize可以用于方法和代码块
- synchronize可以保证数据的原子性和可见性,但是volatile只能保证数据的可见性
- volatile主要解决的是变量在多个线程间的可见性,而synchronize主要解决的是多个线程之间访问资源的同步性
ThreadLocal
简介:可以把ThreadLocal简单的比喻成数据盒子,这个数据盒子中存储着每个线程的私有数据,当线程访问这个ThreadLocal的时候,就会得到这个数据的副本。
ThreadLocal内存泄露问题怎么解决
在ThreadLocalMap中,是以ThreadLocal为key的弱引用和强引用的value组成,如果ThreadLocal没有被强引用,在垃圾回收的时候会将key清除掉,而value还继续存在,这样就会形成一个键为null的entry对象, 如果不采取任何措施,那么value值将永远不会被Gc,这时候就会产生内存泄露的问题。ThreadLocal已经考虑了这种情况,在调用set、get方法的时候,ExpungeStaleEntry方法会自动清除掉键为null的entry对象。建议:使用完ThreadLocal对象最好是手动调用remove方法
线程池
为什么使用线程池
- 降低资源的消耗:重复利用已经创建的线程来降低线程创建和销毁带来的消耗
- 提高响应速度:当任务到达的时候,不用等待线程的创建,直接就可以运行
- 提高线程的可管理性
实现runnable接口和callable的区别
主要的区别在于:runnable接口不会有返回值或者抛出异常,而callable就有;首先runnable接口是从1.0就有的,而callable接口是从1.5开始的,callable的出现主要解决runnable无法解决的情况。
执行execute方法和submit方法的区别
execute方法主要是用于提交没有返回值的任务,而submit主要是提交那些有返回值的任务,有返回值的任务可以通过Future类的get方法来获取返回值,如果调用get方法则会阻塞当前的任务(必须等任务完成)ThreadPoolExecutor构造函数的七大重要参数
- corePoolSize:核心线程数,定义了最小可以同时运行的线程数
- maximumPoolSize:当队列中存放的任务达到队列的容量的时候,就会将当前线程数变成最大线程数
- workQueue:当任务进来的时候,如果线程数达到核心线程数,就会将新任务存储到队列中
- keepAliveTime:当线程池中的线程数量大于核心线程数,这时候又没有新的任务提交,多出的线程数不会立即销毁,而是会等待一段时间之后再进行销毁
- unit:keepAliveTime的时间单位
- threadFactory:executor创建线程会用到
- handler:饱和策略
- AbortPolicy(拒绝策略):当有新任务时,直接拒绝执行,并且报拒绝异常
- CallerRunsPolicy:这个是调用自己的线程来执行任务,但是这个会延迟任务的提交速度,如果程序能等待这么久,或者你的每一个线程必须执行,那么就可以使用这种策略
- DiscordPolicy:不执行,直接抛弃的策略
- Discord01destPolicy:丢弃最早未处理的线程策略
创建线程池有哪几种方式?
- 可以直接使用构造方法进行创建
- 可以使用executor的框架工具类executors进行创建
- fixedThreadPool:该方法创建了一个固定线程数量的线程池。当任务线程进来之后,如果线程池有空闲,那么就执行该任务,如果线程池没有空闲,那么就会将任务存放到任务队列中
- SingleThreadExecutor:该方法创建的是只有一个线程的线程池。若多余任务进来,那么就会将任务暂存到任务队列中,等线程池空闲之后,就将任务队列中的任务取出进行运行,这些任务的执行顺序遵循先进先出
- CachedThreadPool:该任务创建了一个可以根据实际情况来改变线程数量的线程池。当多余任务进来时,如果线程池有空闲的线程就会复用空闲的线程,如果没有,那么就会新创建一个线程对当前任务进行执行
线程池原理分析
线程池到底是依据什么原理进行运行的呢?
首先当有任务进来的时候,先判断核心线程数是否满了,如果没满那么就创建线程对任务进行执行,如果满了那么就加入任务队列,然后当任务不断进来的时候,核心线程数满了,这时候要考虑任务队列是否满了,如果任务队列没满,就将新任务先添加到任务队列中,如果满了就将核心线程数变成最大线程数,这时候,如果线程数足够使用,那么就创建线程对任务执行,如果不够用加进任务队列,如果队列也满了,那么就会根据定制的饱和策略进行处理。
JUC中的原子类有哪几种?
一共有四类
- 基本类型
- AtomicInteger
- AtomicLong
- AtomicBoolean
- 数组类型
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray:引用类型数组原子类
- 引用类型
- AtomicReference:引用类型
- AtomicStampedReference:带有版本号的引用类型
- AtomicMarkableReference:带有标记的引用类型
- 对象属性修改类型
AtomicIntegerFieldUpdater
:原子更新整形字段的更新器AtomicLongFieldUpdater
:原子更新长整形字段的更新器AtomicReferenceFieldUpdater
:原子更新引用类型字段的更新器
JUC中Atomic原子类的总结
什么是AQS?
AQS是用来构建锁和同步器的框架
AQS原理分析
AQS的核心思想就是,如果共享资源是空闲状态,那么就将当前请求资源的线程设置为有效工作线程。如果当前共享资源是被占有的状态,那么就会将这些获取不到锁的线程都加入到CLH队列中,这个队列有一套阻塞等待以及唤醒锁的机制。CLH队列是一个虚拟双向队列,这个队列遵从先进先出的原则。AQS使用原子操作,实现对值的修改。
AQS对资源共享的方式
- Exclusive(独占):只有一个线程能执行。这里分为两种情况:公平和非公平竞争
- Share:可以多个线程同时执行
JVM
有哪几种类加载器
简单介绍一下程序计数器
程序计数器是线程私有的,每个线程都有一个程序计数器,它是随着线程的创建而创建,随着线程的销毁而销毁
它有两个作用:
- 字节码解释器通过改变程序计数器的值依次读取指令,从而实现代码的流程控制
- 在多线程情况下,记录上一次程序执行的地方,保证在上下文切换的时候可以回到上一次线程执行的位置。
简单说一下虚拟机栈和本地方法栈
它们俩都属于线程私有的,生命周期和线程也是相同的。
虚拟机栈主要是Java方法的运行,而本地方法栈主要是Native方法服务。虚拟机栈在运行方法的时候,都会有一个栈针对应这个方法,被压入方法栈中,带待所有方法都被执行完成的时候,就会一一弹出。每个栈针中都会存在局部变量表、操作数栈、方法出口、动态连接等信息。栈都会出现OOM和StackOverFlowError错误
简单的说一下堆
首先堆在1.8之前分为新生代、老年代和永久代,在1.8的时候就变成了新生代、老年代、元空间。而新生代又分为Eden区,Survivor区,survivor有两个区,一个是from 一个是to区。然后新生代是发生GC的频繁区域,而且它的大小大约占用整个新生区的三分之一。而且GC算法有四种:在新生区用的都是复制算法而在老年区用的都是标记清除和标记压缩算法
经常发生的错误:OOM
- 内存不足而出现的OOM
- GC时间太长出现的OOM
简单说一下方法区
主要用于存储已经被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的数据
方法区和永久代有什么区别?
永久代是方法区的一种实现
为什么元空间替换永久代
因为之前的永久代都是需要自定义大小的,内存很受限制,很容易出现内存不足的异常,为了解决这个问题,直接将元空间代替永久带,并且移入本地内存区,这样不管是类的加载还是数据的存放,都只受本地内存的影响,并且很少出现内存异常
空间分配担保
什么是空间分配担保呢??
首先在发生minorGc之前,虚拟机会检查老年代的剩余空间大小是否大于新生代的空间大小,如果成立那么就会进行一次minorGC,如果不成立那么就会判断虚拟机中的担保参数,如果设置了这个担保参数为ture,那么就会检查历代新生区中晋升对象的平均大小空间,如果小于老年代的剩余空间,那么就允许这次GC进行,如果小于那么就不允许这次的GC进行,而是进行FUllGC
有哪几种垃圾收集器
- serial(串行)收集器:这个串行收集器是一个单线程收集器,它的单线程不仅仅在于收集垃圾的时候使用的是一个线程进行的,而是在垃圾回收期间暂停其他所有的线程进行垃圾回收的
- 优点:因为是单线程的垃圾收集器,它没有线程交互的额外开销,所以它是很高效的。
- ParNew收集器:这个收集器就是serial的多线程版本,其他都和serial是一致的
- parallelScanvege收集器:这个收集器和ParNew没什么区别,主要的区别是这个收集器更注重于吞吐量的提升,可以自定义参数,最大效率的利用了cpu
类加载过程(https://blog.csdn.net/m0_38075425/article/details/81627349)
- 加载:加载第一步主要完成下面三件事
- 通过类的全限定名来获取该类的二进制字节流
- 将这个字节流所代表的静态存储结构转换成方法区的运行时数据结构
- 在内存区生成一个代表此类的class对象,作为方法区这个类的各种数据访问入口
- 连接
- 验证
- 文件格式验证:验证字节流是否符合class文件格式的规范(例如:魔数、版本、常量池。。。)
- 元数据验证:主要是对类的元数据进行语意校验,保证不存在与Java语言规范相悖的元数据信息
- 字节码验证:是最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语意是否合法、符合逻辑
- 符号引用验证:确保解析动作能够正确执行
- 准备:这里的准备阶段就是将类的静态变量进行内存分配。这里进行内存分配的时候类变量都是初始值,而不是我们给定的值,只有在后面的初始化之后,才会加载给定的值
- 解析:这个阶段主要就是将常量池中的符号引用全部转换成直接饮用的过程,也就是得到类、字段、方法在内存中的地址或者偏移量
- 验证
- 初始化:虚拟机执行字节码操作
- 卸载
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
垃圾回收机制
https://blog.csdn.net/seriousboy/article/details/81913799
说说CMS垃圾回收机制
说说G1垃圾回收机制
区别?
JVM性能调优
Mysql
何为索引?有什么作用
索引是一种快速查询和检索的数据结构。常见的索引结构:B树,B+树和hash
索引通俗易懂的解释:其实索引就相当于一本书的目录一样,有了索引我们就可以根据目录快速查询我们想看的数据,不然全面扫描会很慢
索引的优缺点
优点
- 使用检索可以大大的提升查询速度,这也是创建索引的最主要原因
- 通过创建唯一索引,可以保证数据库中的每一行数据的唯一性
缺点
- 创建索引和维护索引需要耗费很多时间。
- 增删改的时候,如果数据有索引,那么就会在操作的时候索引也需要动态修改,这样会降低sql的效率
- 索引需要物理空间的存储,这样也会耗费一些资源
问题:索引一定可以提高查询性能吗?
大多数情况下索引比全盘扫描的效率是更高的,但是如果数据库的数据量不是很大的话,那么查询效率也不会提高太多,反而会增加创建索引维护索引带来的资源消耗。
索引的底层数据结构
hash表&b+树
因为hash表中存储的数据都是以键值对的格式,所以通过hash算法,我们可以很容易通过键来获取相应的value。但是同样的也存在hash冲突的问题,解决hash冲突的问题就是通过链地址的方法。
问题:既然hash索引的这么快,为什么MySQL不使用hash作为索引的数据结构呢?
- 首先是因为hash冲突的原因,但是这不是根本原因
- 根本原因是:Hash索引不支持顺序和范围查询(例如:我们要对数据库的数据进行排序或者范围查询,那么将是很大的问题)
B树&B+树
问题:它们俩有什么异同点?
- b树的所有节点都存放键也存放data,但是b+树只在叶子节点存放key和data,其他的节点都只存放key。
- b树的叶子节点都是独立的,但是b+树的叶子节点都有一条引用链指向相邻的叶子节点
- b树的检索对范围内所有的节点的关键字都做二分查找,而b+树则是从根节点到叶子节点依次进行检索
索引类型
主键索引
数据表中主键使用的就是主键索引
二级索引
定义:二级索引称为辅助索引,因为其叶子节点存储的数据是主键,我们可以通过二级索引来定义主键的位置
- 唯一索引:唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许值为null,一张表可以创建多个唯一索引。建立唯一索引一般不是为了查询效率,而是保证数据的唯一性
- 普通索引:普通索引唯一作用就是提高查询速度,一张表中允许创建多个普通索引,并且允许数据重复和null
- 前缀索引:前缀索引只适用于字符类型的数据,前缀索引对文本的前几个字符创建索引,相比普通索引它的数据更小
- 全文索引:全文检索主要是检索大文件数据中的关键字
聚集索引和非聚集索引
聚集索引
索引结构和数据存放在一起的叫聚集索引,主键索引叫聚集索引
优点
聚集索引的查询速度非常快
缺点
- 依赖有顺序的数据:因为B+树是多路平衡树,如果索引的数据不是有序的,那么在插入的时候就会进行排序,如果这个排序的是整型还好,如果不是整形像是字符串或者UUID之类的,排序比较难,这样在插入和查询到时候效率会大打折扣
- 更新代价大:因为如果数据修改的时候会影响到索引的修改,因为聚集索引的数据和索引都存在一起,修改的代价很大,所以一般主键索引都不允许修改主键
非聚集索引
非聚集索引就是索引的数据和索引是分开存放的
二级索引属于非聚集索引
优点
更新代价小于聚集索引,因为它的节点没有存放数据
缺点
- 依赖有序的数据(和聚集索引一样)
- 可能会二次查询(最大的缺点):当找到索引所对应的指针或者主键后,可能会根据指针或者主键再进行一次查询
创建索引需要注意的事项
- 选择合适的字段创建索引
- 被频繁更新的字段应该谨慎建索引
- 尽可能建立联合索引而不是单列索引
- 避免冗余索引
- 考虑在字符串类型上使用前缀索引代替普通索引
Innodb和MyiSAM对比
- Innodb支持行锁和表锁 MYISAM只支持表锁
- Innodb支持事务 MYISAM不支持事务
- Innodb支持外键 MYISAM不支持外键
- Innodb支持数据恢复 MYISAM不支持系统崩溃后的数据恢复
Mysql Innodb是怎样实现ACID的
- Innodb使用redo log 来保证事务的持久性
- Innodb使用undo log 来保证事务的原子性
- Innodb使用锁机制、MVCC 来保证事务的隔离性
- 保证了以上的事务性质,才能保证一致性
事务
并发事务会存在哪些问题
- 脏读:当一个事务对数据进行修改的时候,这个数据还没有进行提交,但是另外一个事务又过来了,它查询得到的数据是没有提交的脏数据,这时候就发生了脏读的情况
- 丢失修改:意思就是当两个事务同时查询了一个数据,然后同时要对这个数据进行修改,但是后面的修改就会覆盖前面的修改,这就叫丢失修改
- 不可重复读:也比喻是两个事务,第一个事务读取到了数据之后,第二个事务也读取到了数据,这时候第二个事务对数据进行了修改,然后第一个事务发现前后得到的数据不一致,这种情况就是不可重读读
- 幻读:比喻两个事务,第一个事务读取了几条数据之后,第二个事务对数据进行了增加或者删除,这时候第一个事务发现前后得到的数据不一致,这个就叫幻读
不可重复读和幻读的区别:
不可重复读针对的是修改前后的数据,而幻读针对的是增加或者删除前后的数据
事务隔离级别有哪些?
- 读取未提交
- 读取已提交
- 可重复读
- 可串行化
Redis
简单介绍一下redis
redis是c语言开发的数据库,它与别的数据库不同的是它的数据是存在内存中的,所以读写是非常快的,因此它经常用于缓存、分布式锁甚至是消息队列。
Spring框架
- spring的模块有七种:
IOC
什么是IOC?
IOC(控制反转),是一种设计思想,DI(依赖注入)是IOC的一种实现方法,之前是手动创建对象,由程序自己控制,现在是将对象的创建交给IOC容器来控制
使用IOC有什么好处呢?
解耦,使对象之间的耦合关系变低
使用单例模式,减少内存开销,提高性能
只用去写Bean的实现,而不用具体去创建Bean的实现
什么是依赖注入?
依赖注入是IOC的一种实现方式依赖注入的方式有哪几种?
三种方式:- 接口注入
- setter方法注入
- 构造器注入
IOC装配Bean的方式有哪几种?
- XML配置文件
- Java类
- @Configuration
- 注解
- @Autowire
- @Primary(首选注入bean)
- @Qualifier(按指定的bean名称进行注入)
IOC的作用域
- Singleton(单例)
- prototype(多例):对象如果有多个状态,那么就是用多例
- request
- session
- global session
IOC的初始化过程
IOC怎么实现对象的创建和依赖管理
首先从加载Bean的配置信息到容器中,然后创建一个Bean定义的注册表,通过这个注册表实例化Bean,然后将实例化的Bean添加到Map缓存区中,供应用程序调用
Spring容器可以简单分成两种
- BeanFactory:面向Spring
- ApplicationContext:面向使用者(主要包含一下两种常用的实现类)
- ClasspathXmlApplicationContext
- FileSystemXmlApplicationContext
- WebApplicationContext(主要用于Web应用)
Bean的生命周期
大致可以分为:
- 实例化Bean:Ioc容器通过获取BeanDefinition对象中的信息进行实例化,实例化对象被包装在BeanWrapper对象中
- 设置对象属性(DI):通过BeanWrapper提供的设置属性的接口完成属性依赖注入;
- 注入Aware接口(BeanFactoryAware, 可以用这个方式来获取其它 Bean,ApplicationContextAware):Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给bean
- BeanPostProcessor:自定义的处理(分前置处理和后置处理)
- InitializingBean和init-method:执行我们自己定义的初始化方法
- 使用
- destroy:bean的销毁
AOP
什么是AOP?
AOP就是面向切面编程,主要使用动态代理的方式实现,将相同逻辑的重复代码横向抽取出来,使用动态代理技术将这些重复代码织入到目标对象方法中,实现和原来一样的功能。为什么使用AOP?
减少相同代码的冗余度,提高代码的可维护性
怎么使用AOP?
- 配置切面
- 配置切入点
- 配置切入表达式
代理能干嘛?
增强对象的行为,在调用对象方法的时候,拦截方法,对方法进行改造增强
什么是静态代理?什么是动态代理?它们有什么区别?
- 静态代理:由程序员创建或者工具生成代理类,也就是在程序运行之前,就已经确定了代理类和委托类之间的关系
- 优点:业务类只需要关注业务逻辑的本身。
- 缺点
- 代理对象一个接口只服务于一种类型对象,如果接口方法过多,还需要对每个方法进行代理,多了很多繁琐的业务
- 如果接口增加了方法,那么下面实现接口的所有代理类都会将这个方法进行重写,增加代码维护难度
- 动态代理:在程序运行期间,由JVM通过反射等机制动态生成,代理类和委托类是在运行期间确定关系的
- 优点:可以灵活的实现接口中的方法,而不是像静态代理一样全部都实现
- 静态代理:由程序员创建或者工具生成代理类,也就是在程序运行之前,就已经确定了代理类和委托类之间的关系
Aop的动态代理有那些方式?
JDK动态代理
Cglib动态代理
AOP默认使用的是JDK动态代理,如果代理对象没有实现的接口那么就使用Cglib进行代理
JDK和Cglib这两种方式有什么区别?
JDK是基于接口进行代理的,而Cglib是基于父类进行代理的,如果被代理的对象没有实现的接口,那么就需要使用Cglib进行代理
怎么选择JDK和Cglib这两种代理,原因是什么?
- 如果是单例模式就使用Cglib,如果是多例模式就使用JDK
- 原因:JDK创建对象的性能比Cglib高,而Cglib生成代理对象的性能比JDK高
Spring中AOP有哪几种实现方式?
- 基于注解的方式@AspctJ
- 基于代理(自定义代码进行实现)
- 使用XML进行实现(POJO)
Spring MVC
SpringMVC原理是什么?
简单原理:
详细原理:
流程讲解:
- 首先用户发送请求,然后请求进入到DispatcherServlet
- DispatcherServlet根据请求信息从HandlerMapping中找到对应的handler
- 找到之后,然后再请求handler适配器进行处理
- handler适配器根据传过来的handler进行请求的处理
- 处理完成之后就会返回一个ModelAndView对象
- View resolver会根据返回的ModelAndView中的View信息进行解析,并返回对应的View
- 然后将返回的Model数据添加进View中,进行视图渲染
- 最后返回给浏览器
循环依赖
什么是循环依赖?怎么解决循环依赖呢?
循环依赖就是比如有两个对象A和B,它们之间互相引用形成一个环,这样的情况叫做循环依赖
怎么解决呢?
如果是通过构造器方式进行对象注入,那么是无法解决的,会直接报错,如果是setter方法进行注入的,那么就可以解决为什么通过构造器创建对象就不能解决循环依赖呢?
因为Spring解决循环依赖是依靠Bean的中间态进行解决的,中间态指的就是,对象已经实例化,但是还没有初始化。而构造器是直接初始化了,所以无法解决。
Spring采用三级缓存的方式进行处理循环依赖问题。
spring中使用了哪些设计模式
- 工厂模式:spring中的BeanFactory就是简单工厂模式的体现,根据传入唯一的标识来获得bean对象;
- 单例模式:提供了全局的访问点BeanFactory;
- 代理模式:AOP功能的原理就使用代理模式(1、JDK动态代理。2、CGLib字节码生成技术代理。)
- 装饰器模式:依赖注入就需要使用BeanWrapper;
- 观察者模式:spring中Observer模式常用的地方是listener的实现。如ApplicationListener。
- 策略模式:Bean的实例化的时候决定采用何种方式初始化bean实例(反射或者CGLIB动态字节码生成)
事务
Spring的事务的特性是什么?
ACID- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Isolation:持久性
事务的传播行为有哪些?
七种行为:
支持当前事务的情况:
- propagation_requierd:如果当前没有事务,就创建一个事务,如果当前已经存在一个事务,那么就加入到这个事务中
- propagation_supports:支持当前事务,如果当前没有事务,就以非事务方法执行
- propagation_mandatory:使用当前事务,如果没有就抛出异常
不支持当前事务的情况:
- propagation_required_new:新建事务,如果当前存在事务,就将当前事务挂起
- propagation_not_supported:以非事务方式执行,如果当前存在事务,就将当前事务挂起
- propagation_never:以非事务方式执行,如果当前存在事务那么抛出异常
特殊情况:
- propagation_nested:如果当前存在事务就嵌入到该事务中,如果没有就执行require一样的操作
事务的隔离级别
- 读未提交
- 读已提交
- 可重复读
- 串行化
如果不设置事务,会出现哪些问题
- 脏读
- 不可重复读
- 幻读
事务的回滚机制
创建事务回滚就需要使用@transactional这个注解,在这个注解中有一个属性是rollbakfor,如果配置了这个属性,那么在运行期间如果报错,就会通过注解找到这儿自定义的回滚机制进行事务回滚
配置事务方式
编程式事务
编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。
声明式事务
声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
编程式事务每次实现都要单独实现,但业务量大功能复杂时,使用编程式事务无疑是痛苦的,而声明式事务不同,声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置。
事务超时
为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。
假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
回滚机制
在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。事务常用的参数
事物配置中有哪些属性可以配置?以下只是简单的使用参考
- 事务的传播性:
@Transactional(propagation=Propagation.REQUIRED) - 事务的隔离级别:
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
读取未提交数据(会出现脏读, 不可重复读) 基本不使用
- 只读:
@Transactional(readOnly=true)
该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。 - 事务的超时性:
@Transactional(timeout=30) - 回滚:
指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)
指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。
- 事务的传播性:
Mybatis框架
架构图
什么是JDBC
Java DataBase Connection,意思就是使用Java语言操作数据库什么是ORM
ORM就是持久层,意思就是将数据存储到硬盘什么是Mybatis
就是优秀的ORM框架Mybatis和JDBC什么关系
如何防止SQL注入?
#{}或者${},前者就是相当于一个占位符,后者就是将数据直接拼接到SQL后面,这样就可以防止SQL注入,需要注意的是使用$必须要添加@param这个注解
Netty
什么是BIO、NIO、AIO?
- BIO:同步阻塞I/O模式,保证读写都是又一个线程来完成的,在不高的并发量下是没有问题的,但是如果是百万级并发量,那么BIO会显得很无力
- NIO:同步非阻塞I/O模式,这个相较于传统的BIO就有了很大的提升,主要是它支持面向缓冲,基于通道的I/O实现方式,可以适用于高并发的情景
- AIO:异步非阻塞I/O模式,这种模式主要是有回调机制的实现,在应用操作之后就会直接返回给用户信息,然后后台再通知相应的线程完成剩余的事情。
什么是Netty?
- Netty是基于NIO的一种客户端服务器的框架
- 它极大的优化了TCP和UDP套接字服务器的性能,并且安全性也有很高的提升
- 支持多种协议(FTP、HTTP、SMTP)
面试题
什么是面向对象?
面向对象就是我们在设计任何东西的时候,都是以角色进行划分的,而不是关心其过程,如果关心其过程那么就是面向过程编程。面向对象易于维护、复用和扩展。
面向对象的三大特性:封装、继承、多态
如何选用集合?
如果我们要通过键值来获取数据的时候那么就选用map
- 如果需要排序就选择TreeMap
- 如果不需要排序就选择HashMap
- 如果需要保证线程安全就选择ConcurrentHashMap
如果我们只存储元素值的时候就选择Collection集合
- 如果保证元素唯一就选择Set
- 不需要就选择Arraylist或者LinkedList
为什么要使用集合?
因为如果在实际开发中使用数组会有很多的局限性,比如数组一旦定义,那么长度和类型就不能改变,而且存储的类型是可重复、单一的,因此选择集合就能充分解决上述所有的问题,集合是非常灵活多变的。
ArrayList和Vector有什么区别?
他们底层都是用数组实现的,但是ArrayList线程不安全,而Vector线程安全
ArrayList和LinkedList的区别
它们都是不同步的,也就是它们线程都是不安全的
底层数据结构不同,ArrayList的底层数据结构是数组,而LinkedList底层数据结构是双向链表,在1.6之前是循环链表
插入和删除:
ArrayList因为底层数据结构是数组,所以在查找元素的时候很快,但是在插入和删除元素的时候,复杂度为O(n-i);LinkedList底层数据结构是双向链表,双向链表对于头尾插入和删除都是非常方便的,但是如果对一个指定的位置进行插入和删除,那么时间复杂度就为O(n)
是否支持快速访问
ArrayList支持快速访问,LinedList不支持
内存空间占用
ArrayList的空间浪费,就是List的初始化都会预留一定的空间;而LinkedList的空间浪费是在存储每一个节点的时候,会有多出的空间来存储这个节点的指针。
无序性和不可重复性的含义:
无序性:意思就是存储在底层的数据结构不是按数组索引存储的,而是按照哈希值决定的
不可重复性:不可重复的意思就是指添加元素是按equals进行判断的,并且判断结果为false才进行存储,需要同时重写equals和hashcode方法
比较HashSet、LinkedHashSet、TreeSet的异同
- HashSet是Set的实现类,HashSet底层是HashMap,线程不安全,可以存储null值
- LinkedHashSet是HashSet的子类,可以按照添加顺序进行遍历
- TreeSet底层是红黑树,元素是有序的,排序的方法有自然排序和定制排序
Queue和Deque的区别
- Queue:
- 是单端队列,只能从一端插入元素,另一端删除元素,遵循先进先出原则
- Queue扩展了Collection接口因容量问题而导致操作失败后处理问题的方式不同:
- 操作失败后抛出异常
- 返回特殊值
- Deque
- Deque是双端队列,可以在两端进行插入和删除
- Deque扩展了Queue接口,增加了在队头队尾的增加和删除功能,失败后处理的方式不同,分为两类:抛出异常、返回特殊值
- ArrayDeque和LinkdeList的区别:它们都实现了Deque,都具有队列功能,但是有什么区别呢?
- ArrayDeque是通过可变长的数组和双指针实现的,而LinkedList是通过链表实现的
- ArrayQueue不支持存储null值,而LinkedList支持
- ArrayQueue是在1.6引入的,而LinkedList是1.2就已经引入了
- ArrayDeque插入时可能就存在扩容,但是均摊后,插入的时间依然为O(1),LinkedList虽然不存在扩容,但是每插入一个都需要申请新的存储空间,均摊性比较差
- Queue:
HashMap和HashTable的区别
- HashMap的线程是不安全的,而HashTable是线程安全的,因为HashTable方法基本都经过了synchronized修饰,如果要保证线程安全,那么可以使用ConcurrentHashMap
- HashMap的效率比HashTable的效率,而且HashTable现在几乎不用了
- HashMap可以存储null值和null键,但是HashTable不行,如果插入则会报空指针异常
- 如果没有指定容量的默认值,那么HashMap的默认容量是16,而HashTable的容量是11,而且每次扩容HashMap都是原有的两倍,而HashTable是原来的2n+1。
- 底层数据结构:HashMap在1.8以后有很大的改动,首先是扩容机制,之前的扩容就是数组的扩容,但是1。8之后,首先是判断数组的长度是否<64,如果<那么就会先将数组扩容,而不是将链表变成红黑树,如果数组长度大于64,而且链表的长度大于8,那么就会将链表转换成红黑树
HashMap和HashSet的区别
- HashMap实现了Map接口,HashSet实现了Set接口
- HashMap存储的是键值对,HashSet存储的是对象
- HashMap调用put进行添加,HashSet调用add进行添加
- HashMap的hashcode是通过键来计算的,HashSet是通过对象进行计算的,如果两个对象的hashCode值一样,那么就会进行equals判断
HashMap和TreeMap的区别
TreeMap主要是多了对集合中元素的排序功能和对元素的搜索功能
HashMap常见的遍历方式有哪几种?
使用iterator EntrySet进行遍历
使用iterator KeySet进行遍历
使用for each EntrySet进行遍历
使用for each KeySet进行遍历
使用Lambda表达式进行遍历
使用Streams API 单线程方式遍历
使用Streams API多线程方式遍历
性能比较:EntrySet最快,KeySet最慢
删除数据的安全性:用map.remove()是不安全的,用iterator.remove()是安全的,
总结:不管是遍历还是删除数据,建议都使用Iterator的EntrySet来操作
ConcurrentHashMap和HashTable的区别
主要的区别:实现线程的安全的方式不同
- ConcurrentHashMap的数据结构1.7是分段数组+链表,1.8是数组+链表+红黑树;HashTable的底层数据结构数组+链表
- ConcurrentHashMap在1.7的时候,它使用的锁是分段式锁,也就是将数据分开进行加锁,这样做在高并发下就不会因为抢占一把锁而造成阻塞,从而提高并发的效率;在1.8的时候使用的是synchronized+CAS来实现锁;HashTable使用的加锁机制是synchronized,同一把锁,这样加锁确实可以保证线程的安全,但是效率非常低,当同时访问一个同步方法的时候,可能会造成阻塞或者轮询,效率大打折扣。
ConcurrentHashMap线程安全底层的具体实现
- 在1.7的时候,因为它的底层数据结构是分段数组+链表进行实现的,这个分段数组相当于HashMap里面的数组,里面包含链表,也就是HashEntry,每个分段数组守护着一个链表,当我们对HashEntry里面的数据进行修改的时候,就必须先获取分段锁。
- 在1.8的时候,因为它的底层数据结构变成了数组+链表+红黑树,取消了分段锁,使用的是synchronized和CAS对链表的头结点进行加锁,因为只要hash不冲突,那么就不会产生并发。