Java
基础
JVM&JDK&JRE
- JDK(Java Development Kit):Java开发工具包,提供了Java的开发环境和运行环境
- JRE(Java Runtime Environment):Java运行时环境,为Java运行提供所需的环境
- JVM:Java虚拟机
具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
什么是字节码
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件)
Java和C++的区别
- Java不提供指针来直接访问内存,程序内存更加安全;
- Java的类是单继承,C++支持多重继承;
- Java有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
continue、break和return的区别是什么
- continue:指跳出当前的这一次循环,继续下一次循环
- break:指跳出整个循环体,继续执行循环体下面的语句
- return:用于跳出所在方法,结束该方法的运行
变量
成员变量和局部变量的区别?
面向对象和面向过程的区别
- 面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
- 面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要低。
重写(Override)和重载(Overload)的区别
重写
- 发生在父子类之间
- 方法名、参数列表、返回类型必须相同
- 访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
- 重写方法一定不能排除新的检查异常或者比被重写方法声明更加宽泛的检查型异常
重载
- 重载是一个类中多态性的一种表现
- 重载要求同名方法的参数列表不同(参数类型、参数个数、参数顺序)
- 重载的时候,返回值类型可以相同也可以不同
equals与==的区别
- ==比较的是两个对象的内存地址,即它们是否指向同一个对象。而equals()比较的是两个对象的内容是否相同,即它们是否具有相同的值
- 对于基本数据类型(byte、short、int、long、float、double、char、boolean),==比较的是它们的值是否相等,而不是地址是否相等
- 对于引用类型(对象),==比较的是它们在内存中的地址是否相同。而equals()默认使用Object类中的equals()方法,该方法是比较两个对象的地址是否相同,如果要实现对象内容比较,需要重写equals()方法
- == 是一个操作符,可以应用于任何两个变量,而equals()是一个方法,需要被调用
- ==比equals效率更高,因为它不需要比较对象的内容,而只需要比较对象的地址
综上,**== 比较的是两个对象的地址,而equals()比较的是两个对象的内容**。对于基本数据类型,== 比较的是它们的值是否相等。在使用时,需要根据具体的需求选择使用哪种比较方式。如果要比较两个对象的内容,应该使用equals()方法。如果要比较两个对象是否为同一个对象,则应该使用==操作符。
Java的四种引用,强弱软虚
强引用
强引用是我们平常使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:
String str = new String("str");
弱引用
弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:
WeakReference<String> wrf = new WeakReference<String>(str);
软引用
软引用在程序内存不足时,会被回收,使用方式:
// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));
虚引用
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意
哦,其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多
被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue ,
使用例子:
PhantomReference<String> prf = new PhantomReference<String>(new String("str"),
new ReferenceQueue<>());
深拷贝和浅拷贝的区别
- 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指
向原来的对象.换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
- 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值.而那些引用其他对象的变量将指向
被复制过的新对象.而不再是原有的那些被引用的对象.换言之.深拷贝把要复制的对象所引用的
对象都复制了一遍。
Java创建对象的方式
- new创建对象
- 通过反射机制
- 采用clone机制
- 通过序列化机制
接口和抽象类的区别
- 实现方式不同:接口定义了一组公共的方法,但没有提供实现。类实现接口时,必须实现接口中定义的所有方法。抽象类是一个具有一些抽象方法的类,它可以有一些具体方法的实现
- 继承限制不同:一个类只能继承一个类(包含抽象类),但一个类能实现多个接口
- 抽象类可以包含成员变量、构造方法、非抽象方法,而接口只能包含公共静态常量和公共抽象方法
- 接口中的方法默认是公共的、抽象的和不可变的,而抽象类可以定义任意类型的方法
- 接口可以在不影响类结构的情况下增加新方法,而抽象类的修改可能会影响已有子类的实现。
泛型
泛型,即“参数化类型”,顾名思义就是将类型由原来的具体的类型参数化
好处
泛型的好处是在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率
反射
反射是在运行状态中,对于任意一个类,都能知道这个类的属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性。这种动态获取信息、动态调用方法的功能叫做Java语言的反射机制
哪里用到反射
像JDBC就是典型的反射
Class.forName('com.mysql.jdbc.Driver.class');//加载MySQL的驱动类
反射的优缺点
- 优点:可以动态执行,在运行期间根据业务功能动态执行方法、访问属性,最大限度发挥了Java的灵活性。
- 缺点:对性能有影响
序列化与反序列化
- 序列化:将对象转换为字节序列的过程
- 反序列化:将存储在磁盘或网络节点上的字节序列恢复为对象的过程
final、finally、finalize的区别
- final用于声明类、变量、方法,分别表示类不可以继承,方法不可以被重写,变量不可以被重写赋值
- finally一般作用在try-catch代码块中,表示总是执行。在处理异常的时候,通常我们将一定要执行的代码方法放入finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
- finalize是一个方法,属于gect类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System的gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾。
集合
Java集合概述
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素;另一个是 Map
接口,主要用于存放键值对。对于Collection
接口,下面又有三个主要的子接口:List
、Set
和 Queue
。
说说List、Set、Queue、Map四者的区别
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),”x” 代表 key,”y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
集合框架底层数据结构总结
Collection
List
ArrayList
:Object[]
数组Vector
:Object[]
数组LinkedList
: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Set
HashSet
(无序,唯一): 基于HashMap
实现的,底层采用HashMap
来保存元素LinkedHashSet
:LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。有点类似于我们之前说的LinkedHashMap
其内部是基于HashMap
实现一样,不过还是有一点点区别的TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)
Queue
PriorityQueue
:Object[]
数组来实现二叉堆ArrayQueue
:Object[]
数组 + 双指针
Map
HashMap
: JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间、LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。Hashtable
: 数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 红黑树(自平衡的排序二叉树)
如何选用集合
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map
接口下的集合,需要排序时选择 TreeMap
,不需要排序时就选择 HashMap
,需要保证线程安全就选用 ConcurrentHashMap
。
当我们只需要存放元素值时,就选择实现Collection
接口的集合,需要保证元素唯一时选择实现 Set
接口的集合比如 TreeSet
或 HashSet
,不需要就选择实现 List
接口的比如 ArrayList
或 LinkedList
,然后再根据实现这些接口的集合的特点来选用。
为什么要使用集合
集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。
Collection子接口之List
ArrayList、LinkedList和Vector的区别
同步性:Vector是同步的,而ArrayList和LinkedList是非同步的。因此,多个线程可以同时访问ArrayList和LinkedList,但是在访问Vector时需要进行同步。
性能:ArrayList在随机访问时性能更好,而LinkedList在插入和删除时性能更好。这是因为ArrayList使用数组实现,因此随机访问时的时间复杂度为O(1),但在插入和删除时需要移动数组中的元素,时间复杂度为O(n);而LinkedList使用链表实现,插入和删除时只需要修改节点的指针,时间复杂度为O(1),但随机访问时需要遍历链表,时间复杂度为O(n)。
容量增长:当集合的大小超过其容量时,ArrayList会自动增加其容量的一半,而Vector会将容量翻倍。LinkedList没有固定的容量。
迭代器:ArrayList和Vector使用ListIterator迭代器,而LinkedList使用Iterator迭代器。
综上所述,如果需要在单线程环境下进行随机访问操作,应该选择ArrayList;如果需要频繁插入和删除操作,应该选择LinkedList;如果需要在多线程环境下进行访问操作,应该选择Vector。
ArrayList的扩容机制
ArrayList默认的分配大小为10的容量,超过10,容器会以1.5倍扩容
ArrayList是Java中的一个动态数组类,其内部使用一个Object类型的数组来存储元素。当数组不够大来容纳更多的元素时,ArrayList会触发扩容机制,将原有的数组复制到一个更大的新数组中,然后将新元素添加到新数组的尾部。
具体的扩容机制如下:
当ArrayList需要添加元素时,首先判断当前数组是否已满。如果数组未满,则直接将元素添加到数组尾部;如果数组已满,则触发扩容机制。
扩容机制会根据当前数组的大小和增量计算出新数组的大小,并创建一个新的更大的数组。
将原有数组中的元素复制到新数组中,同时将新元素添加到新数组的尾部。
将ArrayList的内部数组引用指向新数组,原有数组会被垃圾回收器回收。
默认情况下,ArrayList的增量为原来大小的一半,即每次扩容会将数组大小增加50%。可以通过构造函数指定增量大小,或通过set方法设置ArrayList的capacity属性来控制扩容时的数组增量。
Collection子接口之Set
比较HashSet、LinkedHashSet、TreeSet三者的异同
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,都不是线程安全的。- 主要区别在于底层数据结构不同。
HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
Collection 子接口之 Queue
Map
HashMap和Hashtable的区别
线程是否安全:
HashMap
是非线程安全的,Hashtable
是线程安全的,因为Hashtable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!);效率: 因为线程安全的问题,
HashMap
要比Hashtable
效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它;对 Null key 和 Null value 的支持:
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException
。初始容量大小和每次扩充容量大小的不同 :
① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么
Hashtable
会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小。
底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable
没有这样的机制。
HashMap和HashSet的区别
HashSet
底层就是基于 HashMap
实现的。
HashMap和TreeMap的区别
TreeMap
和HashMap
都继承自AbstractMap
,但是需要注意的是TreeMap
它还实现了NavigableMap
接口和SortedMap
接口。
实现 NavigableMap
接口让 TreeMap
有了对集合内元素的搜索的能力。
实现SortedMap
接口让 TreeMap
有了对集合中的元素根据键排序的能力。
综上,相比于HashMap
来说 TreeMap
主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。
HashSet如何检查重复
在 JDK1.8 中,实际上无论HashSet
中是否已经存在了某元素,HashSet
都会直接插入,只是会在add()
方法的返回值处告诉我们插入前是否存在相同元素。
HashMap的底层实现
JDK7
HashMap由数组+链表,使用头插法。使用头插法是为了提高插入效率,但容易出现循环链表
默认容量是16,负载因子是0.75,一旦大于0.75*16,就会调用resize()进行扩容,将该链表扩大两倍,创建一个新的两倍长的链表
JDK8
HashMap有数组+链表+红黑树,使用尾插法。相对于头插法,尾插法消耗小,避免循环链表
总结
数据结构:HashMap底层使用一个数组和链表或红黑树结构实现。数组用来存储元素,链表或红黑树用来解决哈希冲突。
哈希算法:在向HashMap中插入元素时,会先根据元素的键(Key)计算哈希值(Hash Value),通过哈希值得到数组下标,然后将元素插入到对应的数组位置。Java中默认使用的哈希算法是通过key的hashCode()方法计算哈希值,并对数组长度取模得到下标。
解决哈希冲突:由于哈希算法的限制,可能会出现不同元素的哈希值相同的情况,这就是哈希冲突。HashMap使用链表或红黑树来解决哈希冲突。当链表长度大于等于8时,链表会自动转化为红黑树,以提高查询效率。
初始容量和负载因子:在初始化HashMap时,可以指定初始容量和负载因子。初始容量指定了HashMap底层数组的长度,负载因子指定了HashMap何时需要扩容。当HashMap中元素的数量超过了容量和负载因子的乘积时,HashMap会自动进行扩容操作,即创建一个新的数组,将原数组中的元素重新哈希到新数组中,以此来提高HashMap的性能。
线程安全:HashMap不是线程安全的,如果多个线程同时对一个HashMap进行操作,可能会导致数据出现问题。可以使用ConcurrentHashMap来代替HashMap,它是线程安全的。
HashMap的初始容量设置多少合适
计算公式:**(需要的容量/负载因子) + 1**
eg.假如有6个元素,则HashMap的初始化容量为 (6/0.75) +1 = 9,即new HashMap(9)
ConcurrentHashMap和Hashtable的区别
底层数据结构: JDK1.7 的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;实现线程安全的方式(重要):
- 在 JDK1.7 的时候,
ConcurrentHashMap
对整个桶数组进行了分割分段(Segment
,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
- 在 JDK1.7 的时候,
- 到了 JDK1.8 的时候,
ConcurrentHashMap
已经摒弃了Segment
的概念,而是直接用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和 CAS 来操作。(JDK1.6 以后synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
,虽然在 JDK1.8 中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;
- 到了 JDK1.8 的时候,
Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
JDK1.7和JDK1.8的ConcurrentHashMap实现有什么不同
- 线程安全实现方式 :JDK 1.7 采用
Segment
分段锁来保证安全,Segment
是继承自ReentrantLock
。JDK1.8 放弃了Segment
分段锁的设计,采用Node + CAS + synchronized
保证线程安全,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点。 - Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
- 并发度 :JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
锁
乐观锁与悲观锁
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。
- 悲观锁
- 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
- Java中,synchronized关键字和Lock的实现类都是悲观锁。
- 乐观锁
- 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
- 如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
- 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
总结
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
自旋锁与适应性自旋锁
并发
进程与线程
- 进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
线程有哪些状态
- 创建
- 就绪
- 运行
- 堵塞
- 死亡
线程安全与非线程安全
线程安全和非线程安全是指在多线程并发执行是,程序的行为是否符合预期
线程安全的程序在多线程并发执行时,可以保证每个线程的操作不会影响其他线程的结果,程序仍然可以正确地运行。线程安全的程序需要考虑线程间的同步和互斥,常用的方法有锁、信号量等。线程安全的程序通常会有更高的性能开销,因为线程间的同步需要消耗额外的时间和资源。Vector、StringBuffer、Hashtable是线程安全的
非线程安全的程序在多线程并发执行时,可能会导致不可预期的结果,例如数据竞争、死锁等。非线程安全的程序在多线程环境下使用时,需要开发者自己考虑线程安全的问题。非线程安全的程序通常会有更高的性能,因为不需要额外的同步和互斥开销。ArrayList、LinkedList、HashSet、LinkedSet、TreeSet、HashMap、TreeMap、StringBuilder都是非线程安全的
如何防止死锁
- 避免使用多个锁:减少使用多个锁可以减少死锁的可能性。如果必须使用多个锁,则应该始终以相同的顺序获取锁,以避免死锁。
- 使用超时机制:当一个进程等待太久时,可以设置一个超时机制,如果等待时间超过某个阈值,就会放弃等待并释放已获得的资源。
- 避免循环等待:进程之间应该避免循环等待资源。如果需要多个资源,应该按照相同的顺序请求这些资源。
- 预防性措施:在程序设计时,应该预先避免潜在的死锁情况。例如,使用单一的资源管理器,为每个资源分配唯一的标识符,等等。
- 死锁检测:使用死锁检测算法可以检测到死锁并采取相应的措施,如回滚事务或强制释放资源。
多线程的实现方式
继承Thread类
public class MyThread extends Thread { @Override public void run() { System.out.println("thread run ..."); } public static void main(String[] args) { new MyThread().start(); } }
实现Runnable接口
public class MyRunnable implements Runnable { @Override public void run() { System.out.println("thread run ..."); } public static void main(String[] args) { new Thread(new MyRunnable()).start(); } }
实现Callable接口
- 定义一个Callable的实现类并实现call方法。call方法是带返回值的
- 通过FutureTask的构造方法,把这个Callable实现类传进去
- 把FutureTask作为Thread类的target,创建Thread线程对象
- 通过FutureTask的get方法获取线程的执行结果
public class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { return new Random().nextInt(100); } public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable()); new Thread(futureTask).start(); Integer result = futureTask.get(); System.out.println(result); } }
线程池方式创建
public class MyThread implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "thread run ..."); } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.execute(new MyThread()); } executorService.shutdown(); } }
Runnable、Callable的区别
- Runnable中的run()方法的返回值是void
- Callable中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Runnable、Callable接口实现的优缺点
优点
线程类只是实现了Runnable或者Callable接口,还可以继承其他类。这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
缺点
编程稍微复杂一些,如果需要访问当前线程,则必须使用 Thread.currentThread() 方法
Thread类的方式创建线程的优缺点
优点
编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用this即可获取当前线程
缺点
因为线程类已经继承了Thread类,Java语言是单继承的,所以就不能再继承其他父类了。
notify()和notifyAll()的区别
- notify可能导致死锁,而notifyAll()不会
- notifyAll是唤醒所有,notify是唤醒一个
sleep()和wait()的区别
- sleep()方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。线程不会释放对象锁
- wait()方法是Object类的方法。当一个线程执行到wait方法时,它就进入到该对象的等待池中。线程会放弃对象锁,可以通过notify()和notifyAll()来唤醒,被唤醒的线程会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
Thread类中start()和run()的区别
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
- start 是让线程进入就绪状态;run是让线程运行起来
线程池
什么是线程池
线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被消耗,而是等待下一个任务。
为什么要用线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和消耗的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如何创建线程池
通过ThreadPoolExecutor
构造函数来创建(推荐)
通过Executor
框架的工具类Executors
来创建
我们可以创建多种类型的ThreadPoolExecutor
FixedThreadPool
: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。SingleThreadExecutor
: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。CachedThreadPool
: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。ScheduledThreadPool
:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
为什么不推荐使用内置线程池
在《阿里巴巴Java开发手册》中明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
为什么呢?
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors
去创建,而是通过 ThreadPoolExecutor
构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
FixedThreadPool
和SingleThreadExecutor
: 使用的是无界的LinkedBlockingQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。CachedThreadPool
:使用的是同步队列SynchronousQueue
, 允许创建的线程数量为Integer.MAX_VALUE
,可能会创建大量线程,从而导致 OOM。ScheduledThreadPool
和SingleThreadScheduledExecutor
: 使用的无界的延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
ThreadPoolExecutor
和ThreadPoolTaskExecutor
的区别
ThreadPoolExecutor
是JDK的一个线程池实现,而ThreadPoolTaskExecutor
是Spring框架中的一个线程池实现,继承自ThreadPoolExecutor
,并添加了一些扩展功能
JVM
JVM是什么
JVM(Java Virtual Machine),Java虚拟机,是JRE的一部分。JVM有自己完善的硬件架构,还具备相应的指令系统,是实现跨平台的关键
Java内存区域(运行时数据区)
- 堆(Heap)
- 方法区(Method Area)
- 本地方法栈(Native Method Stack)
- 虚拟机栈(VM Stack)
- 程序计数器(Program Counter Register)
线程私有
- 虚拟机栈
- 本地方法栈
- 程序计数器
线程共享
- 堆
- 方法区
- 直接内存
StackOverFlowError
若栈的内存大小不允许动态扩展,那么线程请求栈的深度超过Java虚拟机栈的最大深度时,就会抛出StackOverFlowError
导致原因
- 无限递归、死循环
- 执行大量方法导致线程栈空间耗尽
- 方法声明海量的局部变量
OutOfMemoryError
简称OOM。如果栈内存允许动态扩展,如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则会抛出OutOfMemory异常
出现原因
- Java堆溢出
Java堆用于存储对象实例,只要不断创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常
- 虚拟机栈、本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
- 方法区和运行时常量池溢出
- 本地直接内存溢出
堆和栈的区别
- 栈(stack):内存空间小,存取效率高;常用于保存局部变量、方法帧、基本数据类型
- 堆(Heap):内存空间大,存取效率低;所有对象实例、数组都存在堆上
GC是什么?为什么要有GC?
GC是垃圾收集的意思。内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至奔溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。
垃圾回收的优点
- Java引入垃圾回收机制,使开发时不需要考虑内存管理
- 垃圾回收可以有效防止内存泄漏
类加载机制
Java的类加载机制是Java虚拟机(JVM)在运行时动态加载类的过程。
- 加载: 查找并加载类的二进制数据
- 连接
- 验证: 确保被加载的类的正确性
- 准备: 为类的静态变量分配内存,并将其初始化为默认值
- 解析: 把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
- 使用: 类访问方法区内的数据结构的接口, 对象是Heap区的数据
- 卸载: 结束生命周期
双亲委派
双亲委派机制是Java中的一种类加载机制,它是基于父子关系的,当一个类需要被加载时,首先会委派其父类加载器去加载该类,如果父类加载器无法加载该类,再由当前类加载器去加载。这样可以保证类的唯一性和安全性,避免重复加载和类的混乱。
工作流程
- 当一个类需要被加载时,首先会委派其父类加载器去加载该类。
- 如果父类加载器无法加载该类,再由当前类加载器去加载。
- 如果当前类加载器也无法加载该类,再委派其父类加载器的父类加载器去加载,直到顶层的启动类加载器。
- 如果顶层的启动类加载器也无法加载该类,就会抛出ClassNotFoundException异常。
JDK新特性
计算机基础
操作系统
网络
OSI模型
1、OSI七层模型是什么?每一层的作用是什么?
TCP/IP模型
ICP/IP四层模型是什么?每一层的作用是什么?
为什么网络需要分层?
算法与数据结构
数据库
基础
MySQL
Redis
Redis的数据结构
Redis的线程模型
Redis的内存管理
Redis的持久化机制
RDB
在指定时间间隔内将内存中的数据集快照(snapshot)到磁盘上,生成一个 RDB 文件。RDB 文件是一个经过压缩的二进制文件,包含了 Redis 在某个时间点上的数据集,可以用于数据备份和恢复。RDB 文件可以手动触发生成,也可以设置自动保存的时间间隔。使用 RDB 持久化机制,可以最大限度地保证数据的完整性和一致性,但会有一定的数据丢失风险,因为在数据快照之后发生的任何变化都不会被保存。
AOF
将 Redis 服务器执行的所有写命令追加到 AOF 文件的末尾,通过这种方式来记录 Redis 的所有写操作,这样在 Redis 重启的时候,可以通过重新执行 AOF 文件中的命令来重建数据库的原始状态。AOF 文件是一个文本文件,包含了 Redis 的所有写操作,可以通过配置文件设置 AOF 文件的同步方式和频率,以达到最大程度的数据保护。使用 AOF 持久化机制,可以最大限度地保证数据的安全性和可靠性,但会有一定的性能损失。
Redis的淘汰机制
Redis 的内存是有限的,当内存不足时,Redis 会根据一定的淘汰策略来决定哪些数据需要被淘汰掉。Redis 目前支持 6 种淘汰策略:
- noeviction:不淘汰数据,当内存不足时,返回错误信息。
- volatile-lru:从已设置过期时间的数据集中选择最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中选择将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中随机选择数据淘汰。
- allkeys-lru:从所有数据集中选择最近最少使用的数据淘汰。
- allkeys-random:从所有数据集中随机选择数据淘汰。
其中,noeviction 策略表示不淘汰数据,当内存不足时,返回错误信息;volatile-* 策略表示只淘汰已设置过期时间的数据;allkeys-* 策略表示从所有数据集中选择数据淘汰。在实际使用中,可以根据业务需求和数据特征来选择最合适的淘汰策略。
此外,Redis 还提供了一种被动淘汰机制,即在 Redis 内存使用达到指定阈值时,Redis 会触发被动淘汰机制,根据已经设置的淘汰策略来淘汰一些数据,以释放内存空间。这种被动淘汰机制可以通过配置文件中的 maxmemory-policy
参数来设置。例如,当设置为 volatile-lru
时,Redis 会在内存使用达到 maxmemory
时,自动淘汰已设置过期时间的数据集中最近最少使用的数据,以释放内存空间。
需要注意的是,淘汰机制可能会导致数据丢失,因此在生产环境中,需要根据业务需求和数据的重要性来选择最合适的淘汰策略,并进行数据备份和监控。
Redis的事务
Redis的性能优化
Redis的生产问题
缓存穿透
大量请求的key是不合理的,根本不存在于缓存中,也不存在与数据库中。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决方案
- 缓存无效key并设置过期时间
- 布隆过滤器:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不存在,那么元素一定不存在
缓存击穿
缓存击穿,请求的key是对应的热点数据,该数据存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决方案
- 设置热点数据永不过期或者过期时间比较长
- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间
- 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库,减少数据库压力
缓存雪崩
缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
解决方案
- 针对 Redis 服务不可用的情况
- 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
- 限流,避免同时处理大量的请求。
- 针对热点缓存失效的情况
- 设置不同的失效时间比如随机设置缓存的失效时间。
- 缓存永不失效(不太推荐,实用性太差)。
- 设置二级缓存。
Redis的四种模式
Redis的单机模式
单机模式就是安装一个Redis,启动起来,业务调用即可。
优点
- 部署简单,成本低(没有备用节点,不需要其他开支)
- 高性能,单机不需要同步数据
缺点
- 可靠性不能保障,单节点有宕机的风险
- 单机高性能受限于CPU的处理能力,redis是单线程的
- 如果需要很高的性能、可靠性,单机就不合适
Redis的主从模式
主从模式就是N个redis实例,可以是1主N从,也可以N主N从(N主N从则不是严格意义上的主从模式了,后续的集群模式会说到,N主N从就是N+N个redis实例。)
主从模式的一个作用是备份数据,这样当一个节点损坏(指不可恢复的硬件损坏)时,数据因为有备份,可以方便恢复。另一个作用是负载均衡,所有客户端都访问一个节点肯定会影响Redis工作效率,有了主从以后,查询操作就可以通过查询从节点来完成。
既然主从复制,意味着master和slave的数据都是一样的,有数据冗余问题。在程序设计上,为了高可用性和高性能,是允许有冗余存在的。对于追求极致用户体验的产品,是绝对不允许有宕机存在的。
优点
- 一旦主节点宕机,从节点作为主节点的备份可以随时顶上来
- 扩展主节点的读能力,分担主节点读压力
- 主从复制还是哨兵模式和集群模式能够实施的基础
缺点
- 一旦主节点宕机,从节点晋升成主节点,同时需要修改应用方的主节点地址
- 主节点的写能力受到单机的限制
- 主节点的存储能力受到单机的限制
- 数据冗余的问题
Redis的哨兵模式
主从模式,当主节点宕机之后,从节点是可以作为主节点顶上来,继续提供服务的。但是有一个问题,主节点的IP已经变动了,此时应用服务还是拿着原主节点的地址去访问,此时就需要人工干预进行修改。哨兵恰恰就可以解决这个问题……
访问redis集群的数据都是通过哨兵集群的,哨兵监控整个redis集群。一旦发现redis集群出现了问题,比如主节点挂了,从节点会顶上来。但是主节点地址变了,这时候应用服务无感知,也不用更改访问地址,因为哨兵才是和应用服务做交互的。Sentinel 很好的解决了故障转移,在高可用方面又上升了一个台阶,当然Sentinel还有其他功能。比如 主节点存活检测、主从运行情况检测、主从切换。Redis的Sentinel最小配置是 一主一从。
Redis哨兵机制是Redis的一种高可用性解决方案,可以从主从复制架构中,实现自动故障检测和故障转移。
哨兵(sentinel)实现了什么功能
- 监控:哨兵会不断地检查主节点和从节点是否运作正常
- 自动故障转移:当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他节点改为复制新的主节点
- 配置提供者:客户端初始化时,通过连接哨兵来获得当前Redis服务的主节点地址
- 通知:哨兵可以将故障转移的结果发送给客户端
Redis哨兵集群是通过什么方式组建的
哨兵实例之间的相互发现,要归功于Redis提供的pub/sub机制(发布/订阅机制)。
在主从集群中,主库上有一个名为__sentinel__:hello
的频道,不同的哨兵就是通过它来相互发现并实现通信的。如下图,哨兵1把自己的IP和端口发布在__sentinel__:hello
频道上,哨兵2和3订阅了该频道。那此时,哨兵2和3就可以从这个频道直接获取哨兵1的ip地址和端口号。然后,哨兵2、3可以与哨兵1建立网络连接。
Redis哨兵是如何监控Redis集群的
这是由哨兵向主库发送 INFO 命令来完成的。就像下图所示,哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。
哨兵如何判断主库已经下线
主要分为主观下线和客观下线
- 主观下线:任何一个哨兵都是可以监控探测,并做出Redis节点下线的判断
- 客观下线:由哨兵集群共同决定Redis节点是否下线
当某个哨兵判断主库“主观下线”后,就会给其他哨兵发送is-master-down-by-addr
命令。接着,其他哨兵会根据自己和主库的连接情况,做出Y或N的响应,Y相当于赞成票,N相当于反对票。
如果赞成票数(这里是2)是大于等于哨兵配置文件中的 quorum
配置项(比如这里如果是quorum=2), 则可以判定主库客观下线了。
Redis哨兵的选举机制是什么样的
- 为什么必然会出现选举/共识机制?
为了避免哨兵的单点情况发生,所以需要一个哨兵的分布式集群。作为分布式集群,必然涉及共识(即选举问题);同时故障的转移和通知都只需要一个主的哨兵节点就可以了
- 哨兵的选举机制是什么样的?
Raft选举算法:选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举
任何一个想成为Leader的哨兵,要满足两个条件:
- 第一,拿到半数以上的赞成票;
- 第二,拿到的票数同时还需要大于等于哨兵配置文件中的quorum值
以 3 个哨兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以了。
主库判定客观下线了,那么如何从剩余的从库中选择一个新的主库呢?
- 过滤掉不健康的(下线或断线),没有回复过哨兵ping响应的从节点
- 选择salve-priority从节点优先级最高的
- 选择复制偏移量最大的,只复制最完整的从节点
Redis的集群模式
MongoDB
常用框架
Spring
Spring
什么是Spring?
Spring是一种轻量级应用框架,旨在提高开发人员的开发效率以及系统的可维护性。
列举一些重要的Spring模块
- Spring Core:基础,可以说Spring其他所有的功能都依赖该类库,主要提供IOC和DI功能
- Spring Aspects:该模块为与AspectsJ的集成提供支持
- Spring AOP:提供面向切面的编程实现
- Spring JDBC:Java数据库连接
- Spring JMS:Java消息服务
- Spring ORM:用于支持Hibernate等ORM工具
- Spring Web:为创建Web应用程序提供支持
- Spring Test:提供了对JUnit和TestNG测试的支持
Spring IOC
什么是IOC
IOC(Inversion Of Control,控制反转)是一种设计思想,而不是一个具体的技术实现。IOC并非Spring特有,在其他语言中也有应用。
Spring IOC指的是:将原本在程序中手动创建对象的控制权,交给IOC容器来管理,并由IOC容器完成对象的注入。
为什么叫控制反转?
- 控制:值的是对象创建(实例化、管理)的权利
- 反转:控制权交给外部环境(Spring框架、IOC容器)
IOC的好处
这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
IOC的实现
Spring中的IOC的实现原理就是工厂模式+反射机制
Bean
什么是Spring Bean?
简单来说,Bean代指的就是那些被IOC容器所管理的对象
将一个类声明为Bean的注解有哪些?
- @Component:通用的注解,可标注任意类为Spring组件,如果一个Bean不知道属于那一层,可以使用@Component注解标注
- @Repository:对应持久层,即Dao层,主要用于数据库相关操作
- @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到Dao层
- @Controller:对应SpringMVC控制层,主要用于接受用户请求并调用Service层返回给前端页面
@Component和@Bean的区别
- @Component注解作用于类,而@Bean注解作用于方法
- @Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们使用@ComponentScan注解定义要扫描的路径,从中找出标识了需要装配的类,并自动装配到Spring的Bean容器中)。
@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。 - @Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。
注入Bean的注解有哪些
Spring内置的@Autowired以及JDK内置的@Resource和@Inject都可以用于注入Bean
@Autowired和@Resource的区别是什么?
- @Autowired是Spring提供的注解,@Resource是JDK提供的注解
- @Autowired默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为byName(根据名称进行匹配)
- 当一个接口存在多个实现类的情况下,@Autowired和@Resource都需要通过名称才能正确匹配到对应的Bean。@Autowired可以通过@Qualifier注解来显式指定名称,@Resource可以通过name属性来显式指定名称。
Bean的作用域有哪些?
在Spring框架中,Bean的作用域是指在应用程序中创建和管理bean实例的生命周期和可见范围
- singleton:默认作用域。唯一bean实例,单例的;
- prototype:每次请求都会创建一个新的bean实例;
- request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效;
- session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效;
- global-session:这是一个不常用的作用域,它仅在使用基于portlet的Spring MVC时才有效。每个全局HTTP会话都会创建一个新的bean实例。
单例Bean的线程安全问题了解吗?
单例bean存在线程问题,主要是因为多个线程操作同一个对象的时候存在资源竞争
解决方案
- 在Bean中尽量避免定义可变的成员变量
- 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal中(推荐的一种方式)
Bean(JavaBean)的生命周期了解吗?
Bean(即Java Bean)是Java语言中一种标准的可重用组件,它具有一些特定的属性和方法,可以在Java应用程序中重复使用。一个Bean的生命周期可以分为以下几个阶段:
- 定义:在编写Java类的时候,可以将它定义为一个Bean。Bean通常具有一些特定的属性和方法,这些属性和方法可以通过JavaBean规范来定义。
- 实例化:一旦Java类被定义为Bean,它就可以被实例化。在Java程序中,可以通过使用Bean的构造函数来创建Bean的实例。
- 初始化:在Bean被实例化之后,需要对其进行初始化,以设置其属性和其他状态。这通常可以通过在Bean中定义初始化方法来完成,例如在Bean中定义一个名为“init”的方法。
- 使用:一旦Bean被初始化,就可以在Java应用程序中使用它。在使用Bean时,可以调用其方法并访问其属性。
- 销毁:当不再需要Bean时,需要将其销毁。在Java程序中,可以通过调用Bean的销毁方法来完成销毁操作,例如在Bean中定义一个名为“destroy”的方法。
总结:定义->实例化->初始化->使用->销毁
Spring Bean的生命周期
- 实例化:在Spring容器中定义一个Bean之后,Spring会通过反射创建该Bean的实例
- 属性注入:在实例化后,Spring会通过setter方法或者带有@Autowired注解的属性注入,将Bean所需的属性值注入到Bean实例中
- BeanPostProcessor处理:Spring容器会检查是否有实现了BeanPostProcessor接口的类,如果有,则会调用它们的postProcessBeforeInitialization()方法和postProcessAfterInitialization()方法,对Bean进行额外处理。
- 初始化:在Bean实例化并注入属性值之后,Spring容器会调用Bean的初始化方法。Bean可以通过实现InitializingBean接口或者在配置文件中使用init-method属性来定义初始化方法。
- 使用:初始化完成后,Spring容器会将Bean对象提供给应用程序使用
- 销毁:当Spring容器关闭时,会销毁所有Bean实例。Bean可以通过实现DisposableBean接口或者在配置文件中使用destroy-method属性来定义销毁方法。
在Bean的生命周期中,BeanPostProcessor是一个重要的环节。BeanPostProcessor接口中定义了两个方法:postProcessBeforeInitialization()和postProcessAfterInitialization()。
实现BeanPostProcessor接口的类可以在Bean初始化前后进行额外处理,例如修改Bean属性值、校验Bean等。
总结:实例化->属性注入->BeanPostProcessor处理->初始化->使用->销毁
什么是AOP
AOP(面向切面编程),用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块。可减少系统的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限、日志、事务等。
AOP有哪些应用场景
- 记录日志(调用方法后记录日志)
- 监控性能(统计方法运行时间)
- 权限控制(调用方法前校验是否有权限)
- 事务管理(调用方法前开启事务,调用方法后提交关闭事务)
AOP的实现方式
AOP实现的关键在于代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表是AspectJ,动态代理的代表是Spring AOP
静态代理
在编译期间,通过手动编写代理类的方式将横切关注点嵌入到目标对象的方法中。缺点是需要手动编写代理类,增加了代码量和维护难度。
动态代理
在运行期间,通过Java提供的动态代理机制,生成代理对象并将横切逻辑织入到目标对象的方法中。动态代理分为JDK动态代理和CGLib动态代理两种方式
JDK动态代理和CGLib动态代理的区别
- 实现方式不同:JDK动态代理是基于接口的代理,它要求目标对象必须实现一个或多个接口。JDK动态代理通过反射机制在运行时动态地生成代理类,代理类实现了目标对象实现的所有接口,并将所有方法的调用都委托给InvocationHandler处理。而CGLIB动态代理则是基于继承的代理,它不要求目标对象实现任何接口。CGLIB动态代理通过继承目标对象,并重写其方法来实现代理,因此可以代理非接口类型的对象。
- 性能不同:由于JDK动态代理是基于接口的代理,它需要反射调用目标对象的方法,而反射调用的效率相对较低。而CGLIB动态代理则是通过生成子类来实现代理,因此不需要反射调用目标对象的方法,可以获得更高的性能。但是由于生成子类需要消耗一定的时间和内存,因此在创建代理对象时会比JDK动态代理更慢。
- 应用场景不同:由于JDK动态代理是基于接口的代理,因此适用于代理接口的场景。而CGLIB动态代理则适用于代理类的场景,比如代理Spring的Bean时,Spring默认使用CGLIB动态代理。另外,由于CGLIB动态代理可以代理非接口类型的对象,因此它还适用于需要代理的类没有实现接口的情况。
Spring AOP和AspectJ AOP的区别
- Spring AOP属于运行时增强,而AspectJ是编译时增强
- Spring AOP已经集成AspectJ,AspectJ相比于SpringAOP功能更加强大
- 如果切面比较少,两者性能差异不大。如果切面太多,最好选择AspectJ,它比SpringAOP快的多
Spring框架中用到哪些设计模式
- 工厂模式:Spring使用工厂模式通过
BeanFactory
、ApplicationContext
创建bean对象 - 代理模式:SpringAOP功能的实现
- 单例模式:Spring中Bean模式是单例模式的
- 模板模式:Spring中
jdbcTemplate
、HibernateTemplate
等以Template
结尾的对数据库操作的类,它们就使用到了模板模式 - 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配
Controller
。
Spring的事务
Spring管理事务的方式有哪几种?
- 编程式事务:在代码中硬编码(不推荐使用)
- 声明式事务:在XML配置文件中配置或者直接基于注解(推荐使用)
SpringMVC
说说自己对SpringMVC的了解
SpringMVC是一个基于MVC架构、用来简化web应用开发的框架,通过把Model、View、Controller分离,将Web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。
SpringMVC的核心组件有哪些
- DispatcherServlet:中央处理器,负责接收请求、分发并给予客户端响应
- HandlerMapping:处理器映射器,根据uri去匹配查找能处理的Handler,并会将请求设计到的拦截器和Handler一起封装
- HandlerAdapter:处理器适配器,根据HandlerMapping找到的Handler,适配执行对应的Handler
- Handler:请求处理器,处理实际请求的处理器
- ViewResolver:视图解析器,根据Handler返回逻辑视图/视图,解析并渲染真正的视图,并传递给DispatcherServlet响应客户端
SpringMVC工作原理
1、所有请求到达DispatcherServlet
2、DispatcherServlet通过HandlerMapping确定请求所对应的Handler(也就是我们平常说的Controller
控制器)
3、DispatcherServlet通过HandlerAdapter执行Handler
4、Handler会根据请求的信息执行相应的业务逻辑
5、Handler执行完业务逻辑后返回ModelAndView
6、DispatcherServlet会根据ViewResolver(视图解析器)将视图名称解析成视图对象
7、视图对象会使用模型数据渲染视图并将最终渲染好的视图返回给客户端
8、DispatcherServlet将渲染好的视图返回给客户端,请求处理完成
SpringBoot
什么是SpringBoot?
SpringBoot是Spring开源组织下的子项目,是Spring组件一站式解决方案,旨在于简化Spring开发
为什么使用SpringBoot?
- 独立运行:SpringBoot内嵌了各种Servlet容器,tomcat、jetty等,不需要打成war包部署到容器中,只需打成jar包就可以独立运行
- 简化配置:不需要编写大量样板代码、XML配置和注释
- 自动装配
- 应用监控:提供了一系列端口可监控服务与应用
介绍一下@SpringBootApplication注解
@SpringBootApplication
可看作是@Configuration
、@EnableAutoConfiguration
、@ComponentScan
注解的集合
- @EnableAutoConfiguration:启用SpringBoot的自动装配机制
- @ComponentScan:扫描被@Component(@Service,@Controller)注解的bean,注解默认会扫描改类所在包下所有的类
- @Configuration:允许在上下文中注册额外的bean或导入其他配置类
MyBatis
什么是MyBatis
- MyBatis是一个开源的Java持久层框架(半ORM框架),它可以用来简化数据访问层的开发。
- 它的主要功能是将Java对象和数据库中的数据进行映射,以及提供一系列的查询语句来操作数据。
- 其优势在于它可以很好地支持定制化SQL语句,使得开发者可以根据实际需求自由地编写SQL语句。
- 此外,Mybatis还提供了缓存机制来提高查询效率,并且可以与Spring框架很好地集成。
MyBatis的优缺点
- 优点
- 基于SQL语句编程,灵活,不会对应用程序或数据库的现有设计造成任何影响;SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用
- 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接
- 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)
- 能够与Spring很好的集成
- 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护
- 缺点
- SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求
- SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库
为什么说MyBatis是半自动ORM框架?
因为相对于全自动ORM框架(如Hibernate)来说,需要开发者手动编写SQL语句,而不是完全由框架自动生成SQL语句
#{}和${}的区别是什么
- #{}是预编译处理,${}是字符串替换
- MyBatis处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值
- MyBatis处理${}时,就是把其替换变量的值
使用#{}可以有效防止SQL注入,提高系统安全性
MyBatis是如何分页的
- MyBatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页
- 可以在sql内直接书写带有物理分页的参数来完成物理分页功能
- 也可以使用分页插件来完成物理分页
MyBatis动态SQL的标签
<if>
标签:判断条件为真时,包含在标签内的SQL语句才会被包含在最终生成的SQL语句中。<choose>
标签:类似于Java语言中的switch语句,根据条件匹配相应的子元素。<when>
标签:<choose>
标签的子标签,用于定义匹配的条件和生成的SQL语句。<otherwise>
标签:<choose>
标签的默认分支,当所有条件不匹配时,会执行<otherwise>
标签中的SQL语句。<trim>
标签:可以删除或包含在生成的SQL语句中的部分内容。<where>
标签:可以根据条件动态生成where
语句。<set>
标签:用于根据条件动态生成set
语句,主要用于更新操作。<foreach>
标签:可以将Java集合中的元素作为SQL语句中的参数值,用于批量插入、更新、删除等操作。<bind>
标签:可以从OGNL(对象图导航语言)表达式中创建一个变量并将其绑定到上下文。
MyBatis的一级缓存、二级缓存
- 一级缓存
- 一级缓存是SqlSession级别的缓存,它是默认开启的,存储了查询结果对象的引用,避免了重复查询数据库的操作,从而提升了效率
- 一级缓存的作用域为SqlSession,只在当前的SqlSession中有效,当SqlSession关闭时,缓存也就失效了。
- 一级缓存的清除策略是默认的,即在执行任何写操作(插入、更新、删除)之后,会清空一级缓存中所有的查询结果对象
- 二级缓存
- 二级缓存是Mapper级别的缓存,它需要手动开启,存储了查询结果对象的拷贝。
- 二级缓存的作用域是Mapper,多个SqlSession可以共享同一个Mapper的二级缓存。
- 当不同的SqlSession查询相同的Mapper和SQL语句时,如果二级缓存中有缓存结果,则直接返回缓存结果,否则查询数据库并将结果放入缓存中。二级缓存的清除策略有多种,可以根据时间、大小、刷新等条件进行清除。
MyBatis-Plus
什么是MyBatis-Plus
MyBatis-Plus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生
Netty
BIO/NIO/AIO的区别
- BIO(Blocking I/O):同步阻塞I/O模式
- NIO(Non-blocking/New I/O):同步非阻塞I/O
- AIO(Asynchronous I/O):异步非阻塞I/O,AIO也就是NIO 2
Netty是什么
- Netty是一个基于NIO的client-server(客户端服务器)框架,使用它可以简单快速地开发网络应用程序
- 它极大低简化并优化了TCP和UDP套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好
- 支持多种协议,如FTP、SMTP、HTTP以及各种二进制和基于文本的传统协议
为啥不直接用NIO
- NIO的编程模型复杂且存在一些BUG,并且对编程功底要求比较高
- NIO在面对断连、重连、包丢失、粘包等问题时处理过程非常复杂。
为啥用Netty
- 统一的API,支持多种传输类型,阻塞和非阻塞的
- 简单强大的线程模型
- 自带编解码器解决TCP粘包/拆包问题
- 自带各种协议栈
- 安全性不错、社区活跃、成熟稳定
Netty应用场景了解吗
从理论上来说,NIO可以做的事情,使用Netty都可以做并且更好,Netty主要用来做网络通信
- 做为RPC框架的网络通信工具
- 实现一个自己的HTTP服务器
- 实现一个即时通讯系统
- 实现消息推送系统
哪些项目用到Netty
Dubbo、RocketMQ、ElasticSearch,gRPC等等
介绍一下Netty的核心组件
微服务
微服务与分布式的区别
分布式系统(Distributed System)是一种由多个独立计算机组成的系统,这些计算机通过网络进行通信和协作,以共同完成任务。它们共享资源、数据和功能,使得系统更加灵活、可扩展和可靠。分布式系统的重点是处理系统的复杂性,以便能够更好地管理和协调各个计算机之间的通信和资源。
微服务(Microservices)是一种软件开发模式,将应用程序分解为小型、松耦合的服务,每个服务运行在自己的进程中,并通过轻量级的通信机制进行交互。每个服务都可以单独部署、测试、维护和扩展。微服务的重点是提高软件开发的灵活性和可维护性,以便能够更快速地交付业务价值。
主要区别
- 视角不同:分布式系统从整体上看待系统的构成和功能,而微服务从单个服务的角度来看待系统的构成和功能。
- 粒度不同:分布式系统的粒度较大,涵盖整个系统,而微服务的粒度较小,每个服务都非常简单,只关注特定的业务逻辑。
- 沟通机制不同:分布式系统通常使用消息传递或共享内存等机制进行通信,而微服务则使用轻量级的HTTP/REST API进行通信。
分布式
RPC
何为RPC
RPC(Remote Procedure Call)远程过程调用,为了让你调用远程方法像调用本地方法一样简单
RPC的原理
RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,用于在网络中的不同计算机上执行远程过程调用。它的基本原理是客户端应用程序通过网络调用服务端应用程序的函数或方法,并等待函数或方法的返回结果。
- 客户端调用过程:客户端应用程序调用远程过程,就像调用本地函数一样。调用参数会被打包成网络可传输的格式,然后发送给服务端。
- 网络传输:客户端通过网络把打包好的参数传输给服务端,使用HTTP、TCP/IP、UDP等网络协议进行传输。
- 服务端接收请求:服务端接收到请求后,解包参数并调用本地的函数或方法。
- 函数执行:服务端执行本地函数或方法,使用调用参数并生成结果。
- 返回结果:服务端把函数的返回结果打包成网络可传输的格式,通过网络发送给客户端。
- 客户端接收结果:客户端接收到服务端返回的结果,解包结果并使用它。
总之,RPC使得分布式应用程序中的不同计算机之间的通信变得更加容易和可靠,通过将过程调用封装成网络通信,使得应用程序可以像调用本地函数一样调用远程函数。
常用的RPC框架
- Dubbo:是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案, 涵盖 Java、Golang 等多种语言 SDK 实现。
- Motan:是新浪微博开源的一款 RPC 框架,据说在新浪微博正支撑着千亿次调用。
- gRPC:Google 开源的一个高性能、通用的开源 RPC 框架。其由主要面向移动应用开发并基于 HTTP/2 协议标准而设计(支持双向流、消息头压缩等功能,更加节省带宽),基于 ProtoBuf 序列化协议开发,并且支持众多开发语言。
HTTP和RPC的区别
- 功能: HTTP旨在传输数据和文件,而RPC旨在在不同的计算机上调用远程过程。
- 传输协议和编码格式: HTTP使用HTTP协议进行通信,而RPC可以使用不同的协议和编码格式(例如TCP、UDP、HTTP等,并且可以支持多种编码格式,如二进制、JSON等。RPC通常使用二进制编码格式,可以更高效地传输数据)
- 性能: RPC通常比HTTP更高效,因为它可以使用二进制编码格式和更轻量级的传输协议。
Dubbo
什么是Dubbo
Apache Dubbo 是⼀个⾼性能,轻量级,基于Java的RPC框架。Dubbo提供三个关键功能,包括基 于接⼝的远程调⽤,容错和负载平衡以及⾃动服务注册和发现。
消息队列
消息队列
消息队列的作用
- 通过异步处理提高系统性能(减少响应所需时间)
- 削峰/限流
- 降低系统耦合性
- 实现分布式事务
使用消息队列会带来哪些问题
- 系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了!
- 系统复杂性提高: 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
- 一致性问题: 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!
Kafka
Kafka是什么?
Kafka是一个分布式的、基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域
流平台具有:
- 消息队列:发布和订阅消息流
- 容错的持久方式存储记录消息流:Kafka会将消息持久化到磁盘,有效避免了消息丢失的风险
- 流式处理平台:在消息发布的时候进行处理,Kafka提供了一个完整的流式处理类库
应用场景
- 消息队列:建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。
- 数据处理:构建实时的流数据处理程序来转换或处理数据流。
Kafka的优势
Kafka相比于其他消息队列,主要的优势如下:
- 极致的性能:基于Scala和Java语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息
- 生态系统兼容性无可匹敌:Kakfa于周边生态系统的兼容性是最好的,没有之一,尤其在大数据和流计算领域
RabbitMQ
RabbitMQ的工作模式
- 简单模式
- work工作模式
- pub/sub发布订阅模式
- routing路由模式
- topic主题模式
什么是死信队列?如何导致的?
DLX,全称为 Dead-Letter-Exchange
,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message
) 之后,它能被重新被发送到DLX,与DLX绑定 的队列就称之为死信队列。
导致死信的原因
- 消息被拒
- 消息TTL过期
- 队列满了,无法再添加
什么是延迟队列?RabbitMQ怎么实现延迟队列?
延迟队列指的是存储对应的延迟信息,消息被发送之后,并不想让消费者立即拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费
RabbitMQ本身是没有延迟队列的,要实现延迟队列,需要使用RabbitMQ的死信交换机(DLX)和消息的存活时间TTL(Time to Live)。模拟延迟队列的功能
设计模式
单例模式
单例模式:保证一个类只有一个实例并提供一个访问它的全局访问点
懒汉模式
线程不安全,延迟初始化
public class Singleton {
// 懒汉模式
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
饿汉模式
线程安全,比较常用,但容易产生垃圾,因为一开始就初始化
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton (){}
public static Singleton getInstance(){
return singleton;
}
}
静态内部类
只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return Inner.instance;
}
private static class Inner {
private static final Singleton instance = new Singleton();
}
}
枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
双重锁模式
双重检查模式,进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
单例模式的懒汉和饿汉的区别
- 懒汉:在类加载的时候不会被初始化,非线程安全
- 饿汉:在类加载时时候就完成初始化,线程安全
策略模式
策略模式是一种行为型设计模式,它允许在运行时动态选择算法或行为,而不必在编译时硬编码这些选择。使用策略模式可以使代码更具有可拓展性、可维护性和可重用性。
在策略模式中,一个对象或方法将根据需要选择一种算法或行为,而不必知道该算法或行为的实现细节。通常,这些算法或行为被定义为一个接口或抽象类,并且可以有多个不同的实现,称为策略类。然后,一个上下文类使用这些策略类,并根据需要动态选择一个策略来执行相应的算法或行为。
项目
为啥使用Postgresql而不用MySQL
PostgreSQL (pgsql) 和 MySQL 都是常用的关系型数据库管理系统 (RDBMS)。虽然它们都是开源软件,但在一些方面有着不同的设计和实现。
以下是一些可能使人们选择使用 PostgreSQL 而不是 MySQL 的原因:
- 数据类型支持:PostgreSQL 提供了更多的数据类型,如数组、范围类型、JSON 等,这些数据类型对于一些应用程序可能非常有用,而 MySQL 则支持较少的数据类型。
- 事务支持:PostgreSQL 在事务处理方面非常强大,支持完全的 ACID 属性,而 MySQL 在这方面的支持则有限。
- 外键约束:PostgreSQL 对于外键约束的支持更加强大和灵活,可以进行更多的操作和限制,而 MySQL 则相对简单。
- 扩展性:PostgreSQL 支持更多的扩展,如自定义类型、自定义函数、存储过程等,这些扩展对于一些特殊需求的应用程序可能非常有用。
- 性能:PostgreSQL 在某些方面的性能可能会优于 MySQL,例如复杂查询和并发读取。
当然,选择 PostgreSQL 还是 MySQL 还要考虑到具体的应用场景和需求。