1、基础知识
1.1重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同时,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类。访问修饰符范围大于等于父类,如果父类方法访问修饰符为private则子类就不能重写该方法。
1.2 String和StringBuffer,StringBuilder的区别是什么?String为什么是不可变的?
可变性:
简单的来说:String类中使用final关键字字符数组保存字符串,private final char value[] ,所以String对象时不可变的。而StringBuilder和StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串 char value[] 但是没有用fianl修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是
AbstractStringBuilder 实现的.
AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义
了一些字符串的基本操作,如 expandCapacity.append.insert.indexOf 等公
共 方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以
是线程安全的。StringBuilder 并没有对
方法进行加同步锁,所以是非线程安全的。
性能
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];}
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然
后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新
的对象并改变对象引用。相同情况下使用 StirngBuilder 相比使用
StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的
风险。
对于三者使用的总结:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b 为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是
比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的
值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池
中重新创建一个 String 对象。
1.5 关于 final 关键字的一些总结
final 关键字主要用在三个地方:变量、方法、类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。
大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟
机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当
JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。
这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身.或者发生在虚拟机试图执行应用时,
如 Java 虚拟机运行错误(Virtual MachineError).类定义错误
(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的
控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设
计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重
要的子类 RuntimeException。
RuntimeException 异常由 Java 虚拟机抛出。NullPointerException(要
访问的变量没有引用任何对象时,抛出该 异常).ArithmeticException(算术
运算异常,一个整数除以 0 时,抛出该异常)和
ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
Throwable 类常用方法
public string getMessage():返回异常发生时的详细信息
public string toString():返回异常发生时的简要描述
public string getLocalizedMessage():返回异常对象的本地化信息。使
用 Throwable 的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖
该方法,则该方法返回的信息与 getMessage()返回的结果相同
public void printStackTrace():在控制台上打印 Throwable 对象封装的
异常信息
异常处理总结
try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,
则必须跟一个 finally 块。
catch 块:用于处理 try 捕获到的异常。
finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当
在 try 块或 catch 块中遇到 return 语句
时,finally 语句块将在方法返回之前被执行。
在以下 4 种特殊情况下,finally 块不会被执行:
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
方法 2:通过 BufferedReader
BufferedReader input = new BufferedReader(new
InputStreamReader(System.in));
String s = input.readLine();
1.9 接口和抽象类的区别是什么
可查看堆空间大小分配(年轻代.年老代.持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)
对象引用情况查看
有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下
Dump 线程详细信息:查看线程内部运行情况
死锁检查
热点分析
CPU 热点:检查系统哪些方法占用的大量 CPU 时间
内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对
象一起统计)
这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的
进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用
眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时
刻,对象(或类.线程等)的不同,以便快速找到问题
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下
来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后
对比两次快照的对象情况。
1.10.4 内存泄漏检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一
下,而线程.热点方面的问题则是具体问题具体分析了。
内存泄漏一般可以理解为系统资源(各方面的资源,堆.栈.线程等)在错误
使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资
源分配请求无法完成,引起系统错误。
内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。
需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最
终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是
系统确实没有那么多资源可以分配了(其他的资源都在使用)。
年老代堆空间被占满
异常: java.lang.OutOfMemoryError: Java heap space
说明:
这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对
象占满,虚拟机无法再在分配新空间。
如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都
是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有
谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间
被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄
漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小
时或者几天)
解决:这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
持久代被占满
异常:java.lang.OutOfMemoryError: PermGen space
说明:Perm 空间被占满。无法为新的 class 分配存储空间而引发的异常。
这个异常以前是没有的,但是在 Java 反射大量使用的今天这个异常比较常见了。
主要原因就是大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。
更可怕的是,不同的 classLoader 即便使用了相同的类,但是都会对其
进行加载,相当于同一个东西,如果有 N 个 classLoader 那么他将会被加载 N
次。因此,某些情况下,这个问题基本视为无解。当然,存在大量 classLoader
和大量反射类的情况其实也不多。
解决:
PC 寄存器(程序计数器):用于记录当前线程运行时的位置,每一个线程
都有一个独立的程序计数器,线程的阻塞.恢复.挂起等一系列操作都需要程
序计数器的参与,因此必须是线程私有的。
java 虚拟机栈:在创建线程时创建的,用来存储栈帧,因此也是线程私有
的。java 程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的
临时数据和中间结果,包括局部变量表.操作数栈.动态链接.方法出口等信息。
这些栈帧就存储在栈中。如果栈深度大于虚拟机允许的最大深度,则抛出 S
tackOverflowError 异常。
局部变量表:方法的局部变量列表,在编译时就写入了 class 文件
操作数栈:int x = 1; 就需要将 1 压入操作数栈,再将 1 赋值给变量 x
java 堆:java 堆被所有线程共享,堆的主要作用就是存储对象。如果堆空间不够,但扩展时又不能申请到足够的内存时,则抛出 OutOfMemoryError 异常。
StackOverflowError | OutOfMemoryError |
---|---|
java 栈 | java 堆 |
栈深度超过范围了(比如:递归层数太多了) | 内存空间不够了(需要及时释放内存) |
方法区:方发区被各个线程共享,用于存储静态变量.运行时常量池等信息。
本地方法栈:本地方法栈的主要作用就是支持 native 方法,比如在 java 中
调用 C/C+
1.12 GC 回收机制
2. 什么时候回收?
引用计数法
可达性分析
2.1 引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一。反
之每当一个引用失效时,计数器减一。当计数器为 0 时,则表示对象不被引用。
举个例子:
Object a = new Object(); // a 的引用计数为 1
a = null; // a 的引用计数为 0,等待 GC 回收
但是,引用计数法不能解决对象之间的循环引用,见下例
Object a = new Object(); // a 的引用计数为 1
Object b = new Object(); // b 的引用计数为 1
a.next = b; // a 的引用计数为 2
b.next = a; // b 的引用计数为 2
a = null; // a 的引用计数为 1,尽管已经显示地将 a 赋值为 null,但是由于引用计数为 1,GC 无法回收 a
b = null; // b 的引用计数为 1,同理,GC 也不回收 b
2.2 可达性分析
设立若干根对象(GC Root),每个对象都是一个子节点,当一个对象找
不到根时,就认为该对象不可达。
没有一条从根到 Object4 和 Object5 的路径,说明这两个对象到根是不可
达的,可以被回收
补充:java 中,可以作为 GC Roots 的对象包括:
java 虚拟机栈中引用的对象
方法区中静态变量引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
3. 怎么回收?
标记-清除算法
复制算法
标记-整理算法
分代算法
3.1 标记——清除算法
遍历所有的 GC Root,分别标记处可达的对象和不可达的对象,然后将不
可达的对象回收。
缺点是:效率低.回收得到的空间不连续
3.2 复制算法
将内存分为两块,每次只使用一块。当这一块内存满了,就将还存活的对象
复制到另一块上,并且严格按照内存地址排列,然后把已使用的那块内存统一回
收。
优点是:能够得到连续的内存空间
缺点是:浪费了一半内存
3.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对
象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外
的内存,“标记-整理”算法的示意图如下图所示。
标记-整理算法是一种老年代的回收算法,它在标记-清除算法的基础上做了
一些优化。也首先需要从根节点开始对所有可达对象做一次标记,但之后,它并
不简单地 清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
3.3 分代算法
在 java 中,把内存中的对象按生命长短分为:
新生代:活不了多久就 go die 了,比如局部变量
老年代:老不死的,活的久但也会 go die,比如一些生命周期长的对象
永久代:千年王八万年龟,不死,比如加载的 class 信息
有一点需要注意:新生代和老年代存储在 java 虚拟机堆上 ;永久代存储
在方法区上
补充:java finalize()方法:
在被 GC 回收前,可以做一些操作,比如释放资源。有点像析构函数,但是
一个对象只能调用一次 finalize()方法。
2. Java 集合框架
2.1 ArrayList 与 LinkedList 异同
1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不
保证线程安全;
2. 底层数据结构: ArrayList 底层使用的是 Object 数组;LinkedList 底
层使用的是双向链表数据结构(JDK1.6 之 前为循环链表,JDK1.7 取消了循环。
注意双向链表和双向循环链表的区别:);
3. 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所
以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而
ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于
get(int index) 方法)。
5. 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会
预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需
要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
补充内容:RandomAccess 接口
public interface RandomAccess {
}
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,
在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这
个接口的类具有随机访问功能。
在 binarySearch()方法中,它要判断传入的 list 是否 RamdomAccess
的实例,如果是,调用
indexedBinarySearch()方法,如果不是,那么调用 iteratorBinarySearch()方法
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为
什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而
LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所
以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间
复杂度为 O(n),所以不支持快速随机访问。,
ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功
能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现
RandomAccess 接口才具有快速随机访问功能的!
下面再总结一下 list 的遍历方式选择:
实现了 RandomAccess 接口的 list,优先选择普通 for 循环 ,其次 foreach, 未实现 RandomAccess 接口的 list, 优先选择 iterator 遍历(foreach 遍历底
层也是通过 iterator 实现的),大 size 的数据,千万不要使用普通 for 循环.补
充:数据结构基础之双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,
分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可
以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表,如
下图所示,同时下图也是 LinkedList 底层使用的是双向循环链表数据结构。
2.2 ArrayList 与 Vector 区别
Vector 类的所有方法都是同步的。可以由两个线程安全地访问一个 Vector
对象.但是一个线程访问 Vector 的话代码要在同步操作上耗费大量的时间。
ArrayList不是同步的,所以在不需要保证线程安全时时建议使用ArrayList。
2.3 HashMap 的底层实现
JDK1.8 之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链
表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到
hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指
的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素
的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉
链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就
是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰
动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不
变。
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是 hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以 0 补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK1.7 的 HashMap 的 hash 方法源码. static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一
点点,因为毕竟扰动了 4 次。所谓 “拉链法” 就是:将链表和数组相结合。
也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希
冲突,则将冲突的值加到链表中即可。
JDK1.8 之后
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链
表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。
2.4 HashMap 和 Hashtable 的区别
1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全
的;HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap 吧!);
2. 效率:因为线程安全的问题,HashMap 要比 HashTable 效率高一点。
另外,HashTable 基本被淘汰,不要在代码中使用它;
3. 对 Null key 和 Null value 的支持: HashMap 中,null 可以作为键,
这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个null,直接抛出 NullPointerException。
4. 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量
初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的
2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2
倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大
小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的
tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总是使
用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
5. 底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大
的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜
索时间。Hashtable 没有这样的机制。
HasMap 中带有初始容量的构造函数:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。
/**
* Returns a power of two size for the given target capacity. */
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
2.5 HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配
均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到
2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均
匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是
放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度
取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组
下标的计算方法是“ (n - 1) & hash ”。(n 代表数组长度)。这也就解释了
HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)
操作中如果除数是 2 的幂次则等价于与其
除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前
提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap的长度为什么是 2 的幂次方。
2.6 HashMap 多线程操作导致死循环问题
在多线程下,进行 put 操作会导致 HashMap 死循环,原因在于
HashMap 的扩容 resize()方法。由于扩容是新建一个数组,复制原数据到数组。
由于数组下标挂有链表,所以需要复制链表,但是多线程操作有可能导致环形链
表。复制链表过程如下:
以下模拟 2 个线程同时扩容。假设,当前 HashMap 的空间为 2(临界值
为 1),hashcode 分别为 0 和 1,在散列地 址 0 处有元素 A 和 B,这时
候要添加元素 C,C 经过 hash 运算,得到散列地址为 1,这时候由于超过了
临界值,空间不够,需要调用 resize 方法进行扩容,那么在多线程条件下,会
出现条件竞争,模拟过程如下:
线程一:读取到当前的 HashMap 情况,在准备扩容时,线程二介入
这个过程为,先将 A 复制到新的 hash 表中,然后接着复制 B 到链头(A
的前边:B.next=A),本来 B.next=null,到此也就结束了(跟线程二一样的
过程),但是,由于线程二扩容的原因,将 B.next=A,所以,这里继续复制 A,让 A.next=B,由此,环形链表出现:B.next=A; A.next=B
注意:JDK1.8 已经解决了死循环的问题。
2.7 HashSet 和 HashMap 区别
如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于
HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone() 方
法.writeObject()方法.readObject()方法是 HashSet 自己不得不实现之外,其
他方法都是直接调用 HashMap 中的方法。)
2.8 ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的
方式上不同。
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组
+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链
表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类
似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为
了解决哈希冲突而存在的;
实现线程安全的方式(重要): ① 在 JDK1.7 的时候,
ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),
每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不
会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment
的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制
使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁
做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在
JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容
旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率
非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入
阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,
也不能使用 get,竞争会越来越激烈效率越低。
2.9 ConcurrentHashMap 线程安全的具体实现方式/底层
具体实现
JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程
占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结
构组成。
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演
锁的角色。HashEntry 用于存储键值对数据。
static class Segment
Serializable { } 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结
构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个
HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守
护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,
必须首先获得对应的 Segment 的锁。
JDK1.8 (上面有示意图)
ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和
synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+
链表/红黑二叉树。
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不
冲突,就不会产生并发,效率又提升 N 倍。
2.10 集合框架底层数据结构总结
Collection
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码另外,需要注意 uniqueInstance 采用volatile 关键字修饰也是很有必要。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
从上面我们可以看出:
synchronized 同步语句块的实现使用的是 monitorenter 和
monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,
monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指
令时,线程试图获取锁也就是获取 monitor(monitor 对象存在于每个 Java 对
象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java
中任意对象可以作为锁的原因) 的持有权.当计数器为 0 则可以成功获取,获取
后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计
数器设为 0,表明锁被释放。如果获取对象锁失败,那当
前线程就要阻塞等待,直到锁被另外一个线程释放为止。
② synchronized 修饰方法的的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
3.1.4 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些
优化,可以详细介绍一下这些优化吗?
JDK1.6 对锁的实现引入了大量的优化,如偏向锁.轻量级锁.自旋锁.适应性自旋锁.锁消除.锁粗化等技术来减少锁操作的开销。锁主要存在四种状态,依次是:无锁状态.偏向锁状态.轻量级锁状态.重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
3.1.5 谈谈 synchronized 和 ReenTrantLock 的区别
(1)两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
(2) synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合try/fifinally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
(3)ReenTrantLock 比 synchronized 增加了一些高级功能
相比 synchronized,ReenTrantLock 增加了一些高级功能。主要来说主要有三点:等待可中断;可实现公平锁; 可实现选择性通知(锁可以绑定多个条件)ReenTrantLock 提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以
选择放弃等待,改为处理其他事情。
ReenTrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock 默认情况是非公平的,可以通过ReenTrantLock 类的 ReentrantLock(boolean fair) 构造方法来制定是否是公平的。synchronized 关键字与 wait()和 notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock 类当然也 可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。Condition 是 JDK1.5 之后才有
的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用 notify/notifyAll()方法进行通知时,被通知的线程是由 JVM选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized 关键字就相当于整个 Lock 对象中只有一个Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll()方法 只会唤醒注册在
该 Condition 实例中的所有等待线程。如果你想使用上述功能,那么选择 ReenTrantLock 是一个不错的选择。
3.2 线程池的 4 连击
3.2.1 讲一下 Java 内存模型
在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变
量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变
量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就
可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它
在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变
量是不稳定的,每次使用它都到主存中进行读取。说白了, volatile 关键字的
主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
3.2.2 说说 synchronized 关键字和 volatile 关键字的区别
synchronized 关键字和 volatile 关键字比较
volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比
synchronized 关键字要好。但是 volatile 关键字只能用于变量而
synchronized 关键字可以修饰方法以及代码块。synchronized 关键字在
JavaSE1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引
入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发
中使用 synchronized 关键字的场景还是更多一些。
多线程访问 volatile 关键字不会发生阻塞,而 synchronized 关键字可能
会发生阻塞
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。
synchronized 关键字两者都能保证。
volatile 关键字主要用于解决变量在多个线程之间的可见性,而
synchronized 关键字解决的是多个线程之间访问资源的同步性。
3.3 线程池的 2 连击
3.3.1 为什么要用线程池?
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还
维护一些基本统计信息,例如已完成任务的数量。
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消
耗。
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即
执行。
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗
系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监
控。
3.3.2 实现 Runnable 接口和 Callable 接口的区别
如果想让线程池执行任务的话需要实现的 Runnable 接口或 Callable 接口。
Runnable 接口或 Callable 接口实现类都可以被 ThreadPoolExecutor 或
ScheduledThreadPoolExecutor 执行。两者的区别在于 Runnable 接口不会
返回结果但是 Callable 接口可以返回结果。
备注: 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之
间的相互转换。
( Executors.callable(Runnable task) 或 Executors.callable
(Runnable task,Object resule) )。
3.3.3 执行 execute()方法和 submit()方法的区别是什么呢?
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用 AtomicInteger 之后,不需要对该方法加锁,也可以实现线程安全。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的
值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用
来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一
个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿
到该变量的最新值。
3.5 AQS
3.5.1 AQS 介绍
AQS 的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks 包下面。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出
应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其
他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等
皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合
我们自己需求的同步器。
3.5.2 AQS 原理分析
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;AQS 原理这部分参考了部分博客,在 5.2 节末尾放了链接。
在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于
AQS 原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一
定要假如自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来
而不是背出来。
下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃
力一点,感兴趣的话可以看看源码。
3.5.3 AQS 原理概览
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程
设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资
源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制
AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双
向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求
共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁
的分配。看个 AQS(AbstractQueuedSynchronizer)原理图:
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成
获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其
值的修改。
private volatile int state;//共享变量,使用 volatile 修饰保证线程可见性
状态信息通过 procted 类型的 getState,setState,compareAndSetState 进行操作//返回同步
状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS 操作)将同步状态值设置为给定值 update 如果当前同步状态的值等于 expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3.5.4 AQS 对资源的共享方式
AQS 定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为
公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如 Semaphore/CountDownLatch。
Semaphore. CountDownLatCh. CyclicBarrier.ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为
ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要
实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如
获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。
3.5.5 AQS 底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是
这样(模板方法模式很经典的一个应用):
isHeldExclusively()//该线程是否正在独占资源。只有用到 condition 才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回 true,失败则返回 false。
默认情况下,每个方法都抛出 UnsupportedOperationException 。 这
些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类
中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其
他类使用。以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线
程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再
tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,
其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此
锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始
化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线
程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到
所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程
就会从 await()函数返回,继续后余动作。一般来说,自定义同步器要么是独占
方法,要么是共享方式,他们也只需实现 tryAcquire-tryRelease . tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定
义同步器同时实现独占和共享两种方式。
如 ReentrantReadWriteLock 。
推荐两篇 AQS 原理和相关源码分析的文章:
http://www.cnblogs.com/waterystone/p/4920797.html
https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
3.5.6 AQS 组件总结
Semaphore(信号量)-允许多个线程同时访问: synchronized 和
ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch (倒计时器): CountDownLatch 是一个同步工具
类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类
似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更
加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字
面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线
程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,
屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏
障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了
屏障,然后当前线程被阻塞。
二. Java Web
1.JDBC 技术
1.1 说下原生 JDBC 操作数据库流程?
第一步:Class.forName()加载数据库连接驱动;
第二步:DriverManager.getConnection()获取数据连接对象;
第三步:根据 SQL 获取 sql 会话对象,有 2 种方式
Statement.PreparedStatement ;
第四步:执行 SQL 处理结果集,执行 SQL 前如果有参数值就设置参数值
setXXX();
第五步:关闭结果集.关闭会话.关闭连接。
1.2 说说事务的概念,在 JDBC 编程中处理事务的步骤。
$.ajax({
url:'http://www.baidu.com',
type:'POST',
data:data,
cache:true,
headers:{},
beforeSend:function(){},
success:function(){},
error:function(){},
complete:function(){}
});
5.3 请简单介绍 Ajax 的使用
Ajax = 异步 JavaScript 和 XML。
Ajax 是一种用于创建快速动态网页的技术。
通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更
新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更
新。
传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面。
有很多使用 AJAX 的应用程序案例:新浪微博.Google 地图.开心网等
等。
5.4 Ajax 可以做异步请求么?
可以.ajax 请求默认是异步的.如果想同步 把 async 设置为 false 就可以
了默认是 true
如果是 jquery
$.ajax({
url: some.php, async: false, success : function(){
}
});
如果是原生的 js
xmlHttp.open("POST",url,false);
5.5 请介绍下 Jsonp 原理
jsonp 的最基本的原理是:动态添加一个
1 | CREATE INDEX index_name ON table(column[length])) |
---|
(2)修改表结构的方式添加索引
1 | ALTER TABLE table_name ADD INDEX index_name ON (column[length])) |
---|
(3)创建表的时候同时创建索引
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) CHARACTER NOT NULL ,
`content` text CHARACTER NULL ,
`time` int(10) NULL DEFAULT NULL ,
PRIMARY KEY (`id`),
INDEX index_name (title[length])
)
(4)删除索引
1 | DROP INDEX index_name ON table |
---|
2.唯一索引
与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。
如果是组合索引,则列值的组合必须唯一。它有以下几种创建方式:
(1)创建唯一索引
1 | CREATE UNIQUE INDEX indexName ON table(column[length]) |
---|
(2)修改表结构
1 | ALTER TABLE table_name ADD UNIQUE indexName ON (column[length]) |
---|
(3)创建表的时候直接指定
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) CHARACTER NOT NULL ,
`content` text CHARACTER NULL ,
`time` int(10) NULL DEFAULT NULL ,
UNIQUE indexName (title[length])
);
3.主键索引
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在
建表的时候同时创建主键索引:
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) NOT NULL ,
PRIMARY KEY (`id`)
);
4.组合索引
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字
段,索引才会被使用。使用组合索引时遵循最左前缀集合
1 | ALTER TABLE table ADD INDEX name_city_age (name,city,age); |
---|
5.全文索引
主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext
索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的 where 语句
的参数匹配。fulltext 索引配合 match against 操作使用,而不是一般的 where
语句加 like。它可以在 create table,alter table ,create index 使用,不过
目前只有 char.varchar,text 列上可以创建全文索引。值得一提的是,在数据
量较大时候,现将数据放入一个没有全局索引的表中,然后再用 CREATE index
创建 fulltext 索引,要比先为一张表建立 fulltext 然后再将数据写入的速度快很
多。
(1)创建表的时候添加全文索引
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) CHARACTER NOT NULL ,
`content` text CHARACTER NULL ,
`time` int(10) NULL DEFAULT NULL ,
PRIMARY KEY (`id`), FULLTEXT (content)
);
(2)修改表结构添加全文索引
1 | ALTER TABLE article ADD FULLTEXT index_content(content) |
---|
(3)直接创建全文索引
1 | CREATE FULLTEXT INDEX index_content ON article(content) |
---|
索引的优点
创建唯一性索引,保证数据库表中每一行数据的唯一性
大大加快数据的检索速度,这也是创建索引的最主要的原因
加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排
序的时间。
通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能。
索引的缺点
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加
索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定
的物理空间,如果要建立聚簇索引,那么需要的空间就会更大
当对表中的数据进行增加.删除和修改的时候,索引也要动态的维护,降低
了数据的维护速度
常见索引原则有
选择唯一性索引:唯一性索引的值是唯一的,可以更快速的通过该索引来确
定某条记录。
为经常需要排序.分组和联合操作的字段建立索引. 为常作为查询条件的字段建立索引。
限制索引的数目:越多的索引,会使更新表变得很浪费时间。
尽量使用数据量少的索引:如果索引的值很长,那么查询的速度会受到影响。
尽量使用前缀来索引:如果索引字段的值很长,最好使用值的前缀来索引。
删除不再使用或者很少使用的索引
最左前缀匹配原则,非常重要的原则。
尽量选择区分度高的列作为索引:区分度的公式是表示字段不重复的比例
索引列不能参与计算,保持列“干净”:带函数的查询不参与索引。
尽量的扩展索引,不要新建索引。
3.7 数据库三范式
范式是具有最小冗余的表结构。3 范式具体如下:
1. 第一范式(1st NF -First Normal Fromate)
第一范式的目标是确保每列的原子性:如果每列都是不可再分的最小数据单
元(也称为最小的原子 单元),则满足第一范式(1NF)
第一范式(1NF)要求数据库表的每一列都是不可分割的基本数据项,同一
列中不能有多个值。
若某一列有多个值,可以将该列单独拆分成一个实体,新实体和原实体间是
一对多的关系。
在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不
满足第一范式(1NF)的数据库就不是关系数据库。
第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原
子值,就说明该数据库表满足了第一范式。
第一范式的合理遵循需要根据系统的实际需求来定。比如某些数据库系统中
需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字
段就行。但是如果系统经常会访问“地址”属性中的“城市”部分,那么就非要
将“地址”这个属性重新拆分为省份.城市.详细地址等多个部分进行存储,这样
在对地址中某一部分操作的时候将非常方便。这样设计才算满足了数据库的第一
范式
2. 第二范式(2nd NF-Second Normal Fromate)
首先满足第一范式,并且表中非主键列不存在对主键的部分依赖。 第二范
式要求每个表只描述一 件事情。
满足第二范式(2NF)必须先满足第一范式(1NF)。
第二范式要求实体中每一行的所有非主属性都必须完全依赖于主键;即:非
主属性必须完全依赖于主键。
完全依赖:主键可能由多个属性构成,完全依赖要求不允许存在非主属性依
赖于主键中的某一部分属性。
若存在哪个非主属性依赖于主键中的一部分属性,那么要将发生部分依赖的
这一组属性单独新建一个实体,并且在旧实体中用外键与新实体关联,并且新实
体与旧实体间是一对多的关系。
第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的
每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而
言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种
数据保存在同一张数据库表中。
3. 第三范式(3rd NF- Third Normal Fromate)
第三范式定义是,满足第二范式,并且表中的列不存在对非主键列的传递
依赖。除了主键订单编号外,顾客姓名依赖于非主键顾客编号。
满足第三范式必须先满足第二范式。
第三范式要求:实体中的属性不能是其他实体中的非主属性。因为这样会出
现冗余。即:属性不依赖于其他非主属性。
如果一个实体中出现其他实体的非主属性,可以将这两个实体用外键关联,
而不是将另一张表的非主属性直接写在当前表中。
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相
关。
3.8 数据库事务
1. 事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列操作,这
些操作作为一个整体一起向 系统提交,要么都执行.要么都不执行 。事务是一
个不可分割的工作逻辑单元 事务必须具备以下四个属性,简称 ACID 属性:
A 原子性(Atomicity):事务是一个完整的操作。事务的各步操作是不可
分的(原子的);要 么都执行,要么都不执行。
C 一致性(Consistency):当事务完成时,数据必须处于一致状态。
I 隔离性(Isolation):对数据进行修改的所有并发事务是彼此隔离的,这
表明事务必须是独 立的,它不应以任何方式依赖于或影响其他事务。
D 永久性(Durability):事务完成后,它对数据库的修改被永久保持,事
务日志能够保持事务 的永久性。
2 .事务控制语句:
BEGIN 或 START TRANSACTION 显式地开启一个事务;
COMMIT 也可以使用 COMMIT WORK,不过二者是等价的。
COMMIT 会提交事务,并使已对数据库进行的所有修改成为永久性的;
ROLLBACK 也可以使用 ROLLBACK WORK,不过二者是等价
的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;
SAVEPOINT identifier,SAVEPOINT 允许在事务中创建一个
保存点,一个事务中可以有多个 SAVEPOINT;
RELEASE SAVEPOINT identifier 删除一个事务的保存点,当没
有指定的保存点时,执行该语句会抛出一个异常;
ROLLBACK TO identifier 把事务回滚到标记点;
SET TRANSACTION 用来设置事务的隔离级别。InnoDB 存储
引擎提供事务的隔离级别有 READ UNCOMMITTED.READ
COMMITTED.REPEATABLE READ 和 SERIALIZABLE。
3. MySQL 事务处理主要有两种方法:
可以看出来,数据结构.SQL.索引是成本最低,且效果最好的优化手段。
(3)性能优化是无止境的,当性能可以满足需求时即可,不要过度优化。
2.优化方向
(1)SQL 以及索引的优化
首先要根据需求写出结构良好的 SQL,然后根据 SQL 在表中建立有效的索
引。但是如果索引太多,不但会影响写入的效率,对查询也有一定的影响。
(2)合理的数据库是设计
根据数据库三范式来进行表结构的设计。设计表结构时,就需要考虑如何设
计才能更有效的查询。
数据库三范式:
第一范式:数据表中每个字段都必须是不可拆分的最小单元,也就是确保每
一列的原子性;
第二范式:满足一范式后,表中每一列必须有唯一性,都必须依赖于主键;
第三范式:满足二范式后,表中的每一列只与主键直接相关而不是间接相关
(外键也是直接相关),字段没有冗余。
注意:没有最好的设计,只有最合适的设计,所以不要过分注重理论。三范
式可以作为一个基本依据,不要生搬硬套。
有时候可以根据场景合理地反规范化:
A:分割表。
B:保留冗余字段。当两个或多个表在查询中经常需要连接时,可以在其中
一个表上增加若干冗余的字段,以 避免表之间的连接过于频繁,一般在冗余列
的数据不经常变动的情况下使用。
C:增加派生列。派生列是由表中的其它多个列的计算所得,增加派生列可
以减少统计运算,在数据汇总时可以大大缩短运算时间。
数据库五大约束:
A:PRIMARY key:设置主键约束;
B:UNIQUE:设置唯一性约束,不能有重复值;
C:DEFAULT 默认值约束
D:NOT NULL:设置非空约束,该字段不能为空;
E:FOREIGN key :设置外键约束。
字段类型选择:
A:尽量使用 TINYINT.SMALLINT.MEDIUM_INT 作为整数类型而非 INT,如果非负则
加上 UNSIGNED
B:VARCHAR 的长度只分配真正需要的空间
C:使用枚举或整数代替字符串类型
D:尽量使用 TIMESTAMP 而非 DATETIME
E:单表不要有太多字段,建议在 20 以内
F:避免使用 NULL 字段,很难查询优化且占用额外索引空间
(3)系统配置的优化
例如:MySQL 数据库 my.cnf
(4)硬件优化
更快的 IO.更多的内存。一般来说内存越大,对于数据库的操作越好。但是
CPU 多就不一定了,因为他并不会用到太多的 CPU 数量,有很多的查询都是单
CPU。另外使用高的 IO(SSD.RAID),但是 IO 并不能减少数据库锁的机制。
所以说如果查询缓慢是因为数据库内部的一些锁引起的,那么硬件优化就没有什
么意义。
3. 优化方案
代码优化
之所以把代码放到第一位,是因为这一点最容易引起技术人员的忽视。很多
技术人员拿到一个性能优化的需求以后,言必称缓存.异步.JVM 等。实际上,第
一步就应该是分析相关的代码,找出相应的瓶颈,再来考虑具体的优化策略。有
一些性能问题,完全是由于代码写的不合理,通过直接修改一下代码就能解决问
题的,比如 for 循环次数过多.作了很多无谓的条件判断.相同逻辑重复多次等。
举个例子:
一个 update 操作,先查询出 entity,再执行 update,这样无疑多了一次
数据库交互。还有一个问题,update 语句可能会操作一些无需更新的字段。
我们可以将表单中涉及到的属性,以及 updateTime,updateUser 等赋值
到 entity,直接通过 pdateByPrimaryKeySelective,去 update 特定字段
定位慢 SQL,并优化
这是最常用.每一个技术人员都应该掌握基本的 SQL 调优手段(包括方法. 工具.辅助系统等)。这里以 MySQL 为例,最常见的方式是,由自带的慢查询
日志或者开源的慢查询系统定位到具体的出问题的 SQL,然后使用
explain.profile 等工具来逐步调优,最后经过测试达到效果后上线。
SqlServer 执行计划:
通过执行计划,我们能得到哪些信息:
A:哪些步骤花费的成本比较高
B:哪些步骤产生的数据量多,数据量的多少用线条的粗细表示,很直观
C:每一步执行了什么动作
具体优化手段:
A:尽量少用(或者不用)sqlserver 自带的函数
select id from t where substring(name,1,3) = ’abc’
select id from t where datediff(day,createdate,’2005-11-30′) = 0
可以这样查询:
select id from t where name like ‘abc%’
select id from t where createdate >= ‘2005-11-30’ and createdate
< ‘2005-12-1’
B:连续数值条件,用 BETWEEN 不用 IN:SELECT id FROM t WHERE num
BETWEEN 1 AND 5
C:Update 语句,如果只更改 1.2 个字段,不要 Update 全部字段,否则
频繁调用会引起明显的性能消耗
D:尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型
E:不建议使用 select * from t ,用具体的字段列表代替“”,不要返回
用不到的任何字段。尽量避免向客户 端返回大数据量,若数据量过大,应该考
虑相应需求是否合理
F:表与表之间通过一个冗余字段来关联,要比直接使用 JOIN 有更好的性
能
G:select count() from table;这样不带任何条件的 count 会引起全表
扫描
连接池调优
我们的应用为了实现数据库连接的高效获取.对数据库连接的限流等目的,
通常会采用连接池类的方案,即每一个应用节点都管理了一个到各个数据库的连
接池。随着业务访问量或者数据量的增长,原有的连接池参数可能不能很好地满
足需求,这个时候就需要结合当前使用连接池的原理.具体的连接池监控数据和
当前的业务量作一个综合的判断,通过反复的几次调试得到最终的调优参数。 合理使用索引
索引一般情况下都是高效的。但是由于索引是以空间换时间的一种策略,索
引本身在提高查询效率的同时会影响插入.更新.删除的效率,频繁写的表不宜建
索引。
选择合适的索引列,选择在 where,group by,order by,on 从句中出现
的列作为索引项,对于离散度不大的列没有必要创建索引。
主键已经是索引了,所以 primay key 的主键不用再设置 unique 唯一索引
索引类型
主键索引 (PRIMARY KEY)
唯一索引 (UNIQUE)
普通索引 (INDEX)
组合索引 (INDEX)
全文索引 (FULLTEXT)
可以应用索引的操作符
大于等于
Between
IN
LIKE 不以 % 开头
不能应用索引的操作符
NOT IN
LIKE %_ 开头
如何选择索引字段
A:字段出现在查询条件中,并且查询条件可以使用索引
B:通常对数字的索引和检索要比对字符串的索引和检索效率更高
C:语句执行频率高,一天会有几千次以上
D:通过字段条件可筛选的记录集很小
无效索引
A:尽量不要在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索
引而进行全表扫描
B:应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引
而进行全表扫描。
C:应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描
select id from t where num=10 or Name = ‘admin’
可以这样查询:
select id from t where num = 10
union
select id from t where Name = ‘admin’
union all 返回所有数据,不管是不是重复。 union 会自动压缩,去除重复数据。
D:不做列运算
where age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数.计算
表达式等
E:查询 like,如果是 ‘%aaa’ 不会使用到索引
分表
分表方式
水平分割(按行).垂直分割(按列)
分表场景
A: 根据经验,MySQL 表数据一般达到百万级别,查询效率就会很低。
B: 一张表的某些字段值比较大并且很少使用。可以将这些字段隔离成单
独一张表,通过外键关联,例如考试成绩,我们通常关注分数,不关注考试详情。
水平分表策略
按时间分表:当数据有很强的实效性,例如微博的数据,可以按月分割。
按区间分表:例如用户表 1 到一百万用一张表,一百万到两百万用一张表。
hash 分表:通过一个原始目标 id 或者是名称按照一定的 hash 算法计算出
数据存储的表名。
读写分离
当一台服务器不能满足需求时,采用读写分离【写: update/delete/add】
的方式进行集群。
一台数据库支持最大连接数是有限的,如果用户的并发访问很多,一台服务
器无法满足需求,可以集群处理。MySQL 集群处理技术最常用的就是读写分离。
主从同步:数据库最终会把数据持久化到磁盘,集群必须确保每个数据库服
务器的数据是一致的。从库读主库写,从库从主库上同步数据。
读写分离:使用负载均衡实现,写操作都往主库上写,读操作往从服务器上
读。
缓存
缓存分类
本地缓存:HashMap/ConcurrentHashMap.Ehcache.Guava Cache 等
缓存服务:Redis/Tair/Memcache 等
使用场景
短时间内相同数据重复查询多次且数据更新不频繁,这个时候可以选择先从
缓存查询,查询不到再从数据库加载并回设到缓存的方式。此种场景较适合用单
机缓存。
高并发查询热点数据,后端数据库不堪重负,可以用缓存来扛。
缓存作用:减轻数据库的压力,减少访问时间。
缓存选择:如果数据量小,并且不会频繁地增长又清空(这会导致频繁地垃
圾回收),那么可以选择本地缓存。具体的话,如果需要一些策略的支持(比如
缓存满的逐出策略),可以考虑 Ehcache;如不需要,可以考虑 HashMap;如
需要考虑多线程并发的场景,可以考虑 ConcurentHashMap。
其他情况,可以考虑缓存服务。目前从资源的投入度.可运维性.是否能动态
扩容以及配套设施来考虑,我们优先考虑 Tair。除非目前 Tair 还不能支持的场合
(比如分布式锁.Hash 类型的 value),我们考虑用 Redis。
缓存穿透一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的
value,就应该去后端系统查找(比如 DB)。如果 key 对应的 value 是一定不
存在的,并且对该 key 并发请求量很大,就会对后端系统造 成很大的压力。这
就叫做缓存穿透。
对查询结果为空的情况也进行缓存,缓存时间设置短点,或者该 key 对应
的数据 insert 了之后清理缓存。
缓存并发有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进
程同时查询 DB,同时设置缓存的情况,
如果并发确实很大,这也可能造成 DB 压力过大,还有缓存频繁更新的问题。
对缓存查询加锁,如果 KEY 不存在,就加锁,然后查 DB 入缓存,然后解
锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入 DB 查询。
缓存雪崩(失效)
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如 DB)带来很大压力。
不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀.
防止缓存空间不够用
(1) 给缓存服务,选择合适的缓存逐出算法,比如最常见的 LRU。
(2) 针对当前设置的容量,设置适当的警戒值,比如 10G 的缓存,当缓存
数据达到 8G 的时候,就开始发出报警,提前排查问题或者扩容。
(3) 给一些没有必要长期保存的 key,尽量设置过期时间。
我们看下图,在 WebServe(r Dao 层)和 DB 之间加一层 cache,这层 cache 一般选取的介质是内存,因为我们都知道存入数据库的数据都具有持久化的特
点,那么读写会有磁盘 IO 的操作,内存的读写速度远比磁盘快得多。(选用存
储介质,提高访问速度:内存>>磁盘;减少磁盘 IO 的操作,减少重复查询,提
高吞吐量)
常用开源的缓存工具有:ehcache.memcache.Redis。
ehcache 是一个纯 Java 的进程内缓存框架,hibernate 使用其做二级缓存。
同时,ehcache 可以通过多播的方式实现集群。本人主要用于本地的缓存,数
据库上层的缓存。
memcache 是一套分布式的高速缓存系统,提供 key-value 这样简单的数
据储存,可充分利用 CPU 多核,无持久化功能。在做 web 集群中可以用做
session 共享,页面对象缓存。
Redis 高性能的 key-value 系统,提供丰富的数据类型,单核 CPU 有抗并
发能力,有持久化和主从复制的功能。本人主要使用 Redis 的 Redis sentinel,
根据不同业务分为多组。
Redis 注意事项
A:在增加 key 的时候尽量设置过期时间,不然 Redis Server 的内存使
用会达到系统物理内存的最大值,导致 Redis 使用 VM 降低系统性能;
B:Redis Key 设计时应该尽可能短,Value 尽量不要使用复杂对象;
C:将对象转换成 JSON 对象(利用现成的 JSON 库)后存入 Redis;
D:将对象转换成 Google 开源二进制协议对象(Google Protobuf,和
JSON 数据格式类似,但是因为是二进制表现,所以性能效率以及空间占用都比
JSON 要小;缺点是 Protobuf 的学习曲线比 JSON 大得多);
E:Redis 使用完以后一定要释放连接。 读取缓存中是否有相关数据,如果缓存中有相关数据,则直接返回,这就是所谓的数据命中“hit”
如果缓存中没有相关数据,则从数据库读取相关数据,放入缓存中,再返回。
这就是所谓的数据未命中“miss”
缓存的命中率 = 命中缓存请求个数/总缓存访问请求个数 =
hit/(hit+miss)
NoSQL
与缓存的区别
先说明一下,这里介绍的和缓存不一样,虽然 Redis 等也可以用来做数据存
储方案(比如 Redis 或者 Tair),但 NoSql 是把它作为 DB 来用。如果当作 DB
来用,需要有效保证数据存储方案的可用性.可靠性。
使用场景
需要结合具体的业务场景,看这块业务涉及的数据是否适合用 NoSQL 来存
储,对数据的操作方式是否适合用 NoSQL 的方式来操作,或者是否需要用到
NoSQL 的一些额外特性(比如原子加减等)。
如果业务数据不需要和其他数据作关联,不需要事务或者外键之类的支持,
而且有可能写入会异常频繁,这个时候就比较适合用 NoSQL(比如 HBase)
比如,美团点评内部有一个对 exception 做的监控系统,如果在应用系统
发生严重故障的时候,可能会短时间产生大量 exception 数据,这个时候如果
选用 MySQL,会造成 MySQL 的瞬间写压力飙升,容易导致 MySQL 服务器的
性能急剧恶化以及主从同步延迟之类的问题,这种场景就比较适合用 Hbase 类
似的 NoSQL 来存储。
视图/存储过程
普通业务逻辑尽量不要使用存储过程,定时任务或报表统计函数可以根据团
队资源情况采用存储过程处理。
JVM 调优
通过监控系统(如没有现成的系统,自己做一个简单的上报监控的系统也很
容易)上对一些机器关键指标(gc time.gc count.各个分代的内存大小变化.机
器的 Load 值与 CPU 使用率.JVM 的线程数等)的监控报警,也可以看 gc log
和 jstat 等命令的输出,再结合线上 JVM 进程服务的一些关键接口的性能数据和
请求体验,基本上就能定位出当前的 JVM 是否有问题,以及是否需要调优。
异步/多线程
针对某些客户端的请求,在服务端可能需要针对这些请求做一些附属的事
情,这些事情其实用户并不关心或者用户不需要立即拿到这些事情的处理结果,
这种情况就比较适合用异步的方式处理这些事情。
异步作用
A:缩短接口响应时间,使用户的请求快速返回,用户体验更好。
B:避免线程长时间处于运行状态,这样会引起服务线程池的可用线程长时
间不够用,进而引起线程池任务队列长度增大,从而阻塞更多请求任务,使得更
多请求得不到技术处理。
C:线程长时间处于运行状态,可能还会引起系统 Load.CPU 使用率.机器整
体性能下降等一系列问题,甚至引发雪崩。异步的思路可以在不增加机器数和
CPU 数的情况下,有效解决这个问题。
异步实现
A:额外开辟线程,这里可以采用额外开辟一个线程或者使用线程池的做法,
在 IO 线程(处理请求响应)之外的线程来处理相应的任务,在 IO 线程中让
response 先返回。
B:使用消息队列(MQ)中间件服务
搜索引擎
例如:solr,elasticsearch
四. SpringMVC 框架
4.1 什么是 SpringMVC ?简单介绍下你对 SpringMVC
的理解?
SpringMVC 是一个基于 Java 的实现了 MVC 设计模式的请求驱动类型的轻
量级 Web 框架,通过把 Model,View,Controller 分离,将 web 层进行职责
解耦,把复杂的 web 应用分成逻辑清晰的几部分,简化开发,减少出错,方便
组内开发人员之间的配合。
4.2 SpringMVC 的流程?
(1)用户发送请求至前端控制器 DispatcherServlet;
(2) DispatcherServlet 收到请求后,调用 HandlerMapping 处理器映
射器,请求获取 Handle;
(3)处理器映射器根据请求 url 找到具体的处理器,生成处理器对象及处
理器拦截器(如果有则生成)一并返回给 DispatcherServlet;
(4)DispatcherServlet 调用 HandlerAdapter 处理器适配器;
(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控
制器);
(6)Handler 执行完成返回 ModelAndView;
(7)HandlerAdapter 将 Handler 执行结果 ModelAndView 返回给
DispatcherServlet;
(8)DispatcherServlet 将 ModelAndView 传给 ViewResolver 视图解析
器进行解析;
(9)ViewResolver 解析后返回具体 View;
(10)DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视
图中)
(11)DispatcherServlet 响应用户。
4.3 SpringMVC 的优点
(1)可以支持各种视图技术,而不仅仅局限于 JSP;
(2)与 Spring 框架集成(如 IoC 容器.AOP 等);
(3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映
射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器
(ViewResolver)
(4) 支持各种请求资源的映射策略。
4.4 SpringMVC 的主要组件?
(1)前端控制器 DispatcherServlet(不需要程序员开发)
作用:接收请求.响应结果,相当于转发器,有了 DispatcherServlet 就减
少了其它组件之间的耦合度。
(2)处理器映射器 HandlerMapping(不需要程序员开发)
作用:根据请求的 URL 来查找 Handler
(3)处理器适配器 HandlerAdapter
注意:在编写 Handler 的时候要按照 HandlerAdapter 要求的规则去编写,
这样适配器 HandlerAdapter 才可以正确的去执行 Handler。
(4)处理器 Handler(需要程序员开发)
(5)视图解析器 ViewResolver(不需要程序员开发)
作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view)
(6)视图 View(需要程序员开发 jsp)
View 是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,
pdf 等等)
4.5 SpringMVC 和 Struts2 的区别有哪些?
(1)SpringMVC的入口是一个servlet即前端控制器(DispatchServlet),
而 struts2 入口是一个 filter 过虑器(StrutsPrepareAndExecuteFilter)。
(2)SpringMVC 是基于方法开发(一个 url 对应一个方法),请求参数传递
到方法的形参,可以设计为单例或多例(建议单例),struts2 是基于类开发,传
递参数是通过类的属性,只能设计为多例。
(3)Struts 采用值栈存储请求和响应的数据,通过 OGNL 存取数据,
SpringMVC 通过参数解析器是将 request 请求内容解析,并给方法形参赋值,
将数据和视图封装成 ModelAndView 对象,最后又将 ModelAndView 中的模
型数据通过 reques 域传输到页面。Jsp 视图解析器默认使用 jstl。
4.6 SpringMVC 怎么样设定重定向和转发的?
(1)转发:在返回值前面加"forward:“,譬如
“forward:user.do?name=method4” (2)重定向:在返回值前面加"redirect:”,譬如
“redirect:http://www.baidu.com”
4.7 SpringMVC 怎么和 Ajax 相互调用的?
通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json
对象。具体步骤如下 :
(1)加入 Jackson.jar
(2)在配置文件中配置 json 的映射
(3)在接受 Ajax 方法里面可以直接返回 Object,List 等,但方法前面要加上
@ResponseBody 注解。
4.8 如何解决 Post 请求中文乱码问题,Get 的又如何处理
呢?
(1)解决 post 请求乱码问题:
在 web.xml 中配置一个 CharacterEncodingFilter 过滤器,设置成 utf-8;
<filter>
<filter-name>CharacterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>utf-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>CharacterEncodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
(2)get 请求中文参数出现乱码解决方法有两个:
修改 tomcat 配置文件添加编码与工程编码一致,如下:
另外一种方法对参数进行重新编码:
String userName= new String(request.getParamter(“userName”).get
Bytes(“ISO8859-1”),“utf-8”)
ISO8859-1 是 tomcat 默认编码,需要将 tomcat 编码后的内容按 utf-8 编
码。
4.9 SpringMVC 的异常处理 ?
答:可以将异常抛给 Spring 框架,由 Spring 框架来处理;我们只需要配
置简单的异常处理器,在异常处理器中添视图页面即可。
4.10 SpringMVC 的控制器是不是单例模式,如果是,有什么
问题,怎么解决?
答:是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影
响性能的,解决方案是在控制器里面不能写字段。
4.11 SpringMVC 常用的注解有哪些?
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,
则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收 http 请求的 json 数据,将 json 转换为 java 对象。
@ResponseBody:注解实现将 conreoller 方法返回对象转化为 json 对象响应给客户。
4.12 SpingMVC 中的控制器的注解一般用那个,有没有别
的注解可以替代?
一般用@Conntroller 注解,表示是表现层,不能用别的注解代替。
4.13 如果在拦截请求中,我想拦截 get 方式提交的方法,怎
么配置?
可以在@RequestMapping 注解里面加
method=RequestMethod.GET。
4.14 怎样在方法里面得到 Request,或者 Session?
直接在方法的形参中声明 request,SpringMVC 就自动把 request 对象传
入。
4.15 如果想在拦截的方法里面得到从前台传入的参数,怎么
得到?
直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。
4.16 如果前台有很多个参数传入,并且这些参数都是一个对
象的,那么怎么样快速得到这个对象?
直接在方法中声明这个对象,SpringMVC 就自动会把属性赋值到这个对象
里面。
4.17 SpringMVC 中函数的返回值是什么?
返回值可以有很多类型,有 String, ModelAndView。ModelAndView 类把
视图和数据都合并的一起的,但一般用 String 比较好。
4.18 SpringMVC 用什么对象从后台向前台传递数据的?
通过 ModelMap 对象,可以在这个对象里面调用 put 方法,把对象加到里面, 前台就可以通过 el 表达式拿到。
4.19 怎么样把 ModelMap 里面的数据放入 Session 里
面?
可以在类上面加上@SessionAttributes 注解,里面包含的字符串就是要放
入 session 里面的 key。
4.20 SpringMVC 里面拦截器是怎么写的?
有两种写法,一种是实现 HandlerInterceptor 接口,另外一种是继承适配器
类,接着在接口方法当中,实现处理逻辑;然后在 SpringMVC 的配置文件中配
置拦截器即可:
<mvc:interceptors>
<bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor">bean>
<mvc:interceptor>
<mvc:mapping path="/modelMap.do" />
<bean class="com.zwp.action.MyHandlerInterceptorAdapter" />
mvc:interceptor>
mvc:interceptors>
4.21 注解原理
注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运
行时生成的动态代理类。我们通过反射获取注解时,返回的是 Java 运行时生成
的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用
AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues
这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。 五. Spring 框架
5.1 Spring 是什么?
Spring 是一个轻量级的 IoC 和 AOP 容器框架。是为 Java 应用程序提供基
础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需
要关心业务需求。常见的配置方式有三种:基于 XML 的配置.基于注解的配置. 基于 Java 的配置。
主要由以下几个模块组成:
Spring Core:核心类库,提供 IOC 服务;
Spring Context:提供框架式的 Bean 访问方式,以及企业级功能(JNDI. 定时任务等);
Spring AOP:AOP 服务;
Spring DAO:对 JDBC 的抽象,简化了数据访问异常的处理;
Spring ORM:对现有的 ORM 框架的支持;
Spring Web:提供了基本的面向 Web 的综合特性,例如多方文件上传;
Spring MVC:提供面向 Web 应用的 Model-View-Controller 实现。
5.2 Spring 的优点?
(1)Spring 属于低侵入式设计,代码的污染极低;
(2)Spring 的 DI 机制将对象之间的依赖关系交由框架处理,减低组件的
耦合性;
(3)Spring 提供了 AOP 技术,支持将一些通用任务,如安全.事务.日志. 权限等进行集中式管理,从而提供更好的复用。
(4)Spring 对于主流的应用框架提供了集成支持。
5.3 Spring 的 AOP 理解?
OOP 面向对象,允许开发者定义纵向的关系,但并适用于定义横向的关系,
导致了大量代码的重复,而不利于各个模块的重用。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务
无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的
模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低
了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证.日志.事务处
理。
AOP 实现的关键在于 代理模式,AOP 代理主要分为静态代理和动态代理。
静态代理的代表为 AspectJ;动态代理则以 Spring AOP 为代表。
(1)AspectJ 是静态代理的增强,所谓静态代理,就是 AOP 框架会在编译
阶段生成 AOP 代理类,因此也称为编译时增强,他会在编译阶段将 AspectJ(切
面)织入到 Java 字节码中,运行的时候就是增强之后的 AOP 对象。
(2)Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会
去修改字节码,而是每次运行时在内存中临时为方法生成一个 AOP 对象,这个
AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回
调原对象的方法。
Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态
代理:
(1)JDK 动态代理只提供接口的代理,不支持类的代理。核心
InvocationHandler 接口和 Proxy 类,InvocationHandler 通过 invoke()方法
反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy
利用 InvocationHandler 动态创建一个符合某一接口的的实例, 生成目标类的
代理对象。
(2)如果代理类没有实现 InvocationHandler 接口,那么 Spring AOP 会
选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是
一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖
其中特定方法并添加增强代码,从而实现 AOP。CGLIB 是通过继承的方式做的
动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代
理的。
(3)静态代理与动态代理区别在于生成 AOP 代理对象的时机不同,相对
来说 AspectJ 的静态代理方式具有更好的性能,但是 AspectJ 需要特定的编译
器进行处理,而 Spring AOP 则无需特定的编译器处理。
InvocationHandler 的
invoke(Object proxy,Method method,Object[] args):proxy 是最终生成的
代理实例; method 是被代理目标实例的某个具体方法;args 是被代理目标实
例某个方法的具体入参, 在方法反射调用时使用。
5.4 Spring 的 IOC 理解?
(1)IOC 就是控制反转,是指创建对象的控制权的转移,以前创建对象的
主动权和时机是由自己把控的,而现在这种权力转移到 Spring 容器中,并由容
器根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松
散耦合,也利于功能的复用。DI 依赖注入,和控制反转是同一个概念的不同角
度的描述,即 应用程序在运行时依赖 IoC 容器来动态注入对象需要的外部资源。
(2)最直观的表达就是,IOC 让对象的创建不用去 new 了,可以由 spring
自动生产,使用 java 的反射机制,根据配置文件在运行时动态的去创建对象以
及管理对象,并调用对象的方法的。
(3)Spring 的 IOC 有三种注入方式 :构造器注入.setter 方法注入.根据
注解注入。
IOC 让相互协作的组件保持松散的耦合,而 AOP 编程允许你把遍布于应用
各层的功能分离出来形成可重用的功能组件。
5.5 BeanFactory 和 ApplicationContext 有什么区别?
BeanFactory 和 ApplicationContext 是 Spring 的两大核心接口,都可以
当做 Spring 的容器。其中 ApplicationContext 是 BeanFactory 的子接口。
(1)BeanFactory:是 Spring 里面最底层的接口,包含了各种 Bean 的定
义,读取 bean 配置文档,管理 bean 的加载.实例化,控制 bean 的生命周期,
维护 bean 之间的依赖关系。ApplicationContext 接口作为 BeanFactory 的派
生,除了提供 BeanFactory 所具有的功能外,还提供了更完整的框架功能:
继承 MessageSource,因此支持国际化。
统一的资源文件访问方式。
提供在监听器中注册 bean 的事件。
同时加载多个配置文件。
载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的
层次,比如应用的 web 层。
(2)BeanFactroy 采用的是延迟加载形式来注入 Bean 的,即只有在使用
到某个 Bean 时(调用 getBean()),才对该 Bean 进行加载实例化。这样,我们
就不能发现一些存在的 Spring 的配置问题。如果 Bean 的某一个属性没有注入,
BeanFacotry 加载后,直至第一次使用调用 getBean 方法才会抛出异常。
ApplicationContext,它是在容器启动时,一次性创建了所有的 Bean。这
样,在容器启动时,我们就可以发现 Spring 中存在的配置错误,这样有利于检
查所依赖属性是否注入。 ApplicationContext 启动后预载入所有的单实例
Bean,通过预载入单实例 bean ,确保当你需要的时候,你就不用等待,因为它
们已经创建好了。
相对于基本的 BeanFactory,ApplicationContext 唯一的不足是占用内存
空间。当应用程序配置 Bean 较多时,程序启动较慢。
(3)BeanFactory 通常以编程的方式被创建,ApplicationContext 还能
以声明的方式创建,如使用 ContextLoader。
(4)BeanFactory 和 ApplicationContext 都支持
BeanPostProcessor.BeanFactoryPostProcessor 的使用,但两者之间的区别
是:BeanFactory 需要手动注册,而 ApplicationContext 则是自动注册。
5.6 请解释 Spring Bean 的生命周期?
首先说一下 Servlet 的生命周期:实例化,初始 init,接收请求 service,
销毁 destroy;
Spring 上下文中的 Bean 生命周期也类似,如下:
(1)实例化 Bean:
对于 BeanFactory 容器,当客户向容器请求一个尚未初始化的 bean 时,
或初始化 bean 的时候需要注入另一个尚未初始化的依赖时,容器就会调用
createBean 进行实例化。对于 ApplicationContext 容器,当容器启动结束后,
通过获取 BeanDefinition 对象中的信息,实例化所有的 bean。
(2)设置对象属性(依赖注入):
实例化后的对象被封装在 BeanWrapper 对象中,紧接着,Spring 根据
BeanDefinition 中的信息 以及 通过 BeanWrapper 提供的设置属性的接口完
成依赖注入。
(3)处理 Aware 接口:
接着,Spring 会检测该对象是否实现了 xxxAware 接口,并将相关的
xxxAware 实例注入给 Bean:
如果这个 Bean 已经实现了 BeanNameAware 接口,会调用它实现的
setBeanName(String beanId)方法,此处传递的就是 Spring 配置文件中 Bean
的 id 值;
如果这个 Bean 已经实现了 BeanFactoryAware 接口,会调用它实现的
setBeanFactory()方法,传递的是 Spring 工厂自身。
如果这个 Bean 已经实现了 ApplicationContextAware 接口,会调用
setApplicationContext(ApplicationContext)方法,传入 Spring 上下文;
(4)BeanPostProcessor:
如果想对 Bean 进行一些自定义的处理,那么可以让 Bean 实现了
BeanPostProcessor 接口,那将会调用
postProcessBeforeInitialization(Object obj, String s)方法。由于这个方法是
在 Bean 初始化结束时调用的,所以可以被应用于内存或缓存技术;
(5)InitializingBean 与 init-method:
如果 Bean 在 Spring 配置文件中配置了 init-method 属性,则会自动调
用其配置的初始化方法。
(6)如果这个 Bean 实现了 BeanPostProcessor 接口,将会调用
postProcessAfterInitialization(Object obj, String s)方法;
以上几个步骤完成后,Bean 就已经被正确创建了,之后就可以使用这个
Bean 了。
(7)DisposableBean:
当 Bean 不再需要时,会经过清理阶段,如果 Bean 实现了 DisposableBean
这个接口,会调用其实现的 destroy()方法;
(8)destroy-method:
最后,如果这个 Bean 的 Spring 配置中配置了 destroy-method 属性,会
自动调用其配置的销毁方法。
5.7 解释 Spring 支持的几种 bean 的作用域。
Spring 容器中的 bean 可以分为 5 个范围:
(1)singleton:默认,每个容器中只有一个 bean 的实例,单例的模式由
BeanFactory 自身来维护。
(2)prototype:为每一个 bean 请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean
会失效并被垃圾回收器回收。
(4)session:与 request 范围类似,确保每个 session 中有一个 bean 的
实例,在 session 过期后,bean 会随之失效。
(5)global-session:全局作用域,global-session 和 Portlet 应用相关。
当你的应用部署在 Portlet 容器中工作时,它包含很多 portlet。如果你想要声
明让所有的 portlet 共用全局的存储变量的话,那么这全局变量需要存储在
global-session 中。全局作用域与 Servlet 中的 session 作用域效果相同。
5.8 使用注解之前要开启自动扫描功能
其中 base-package 为需要扫描的包(含子包)。
1
@Configuration 把一个类作为一个 IoC 容器,它的某个方法头上如果注册了@Bean,就会作
为这个 Spring 容器中的 Bean。 @Scope 注解 作用域
@Lazy(true) 表示延迟初始化
@Service 用于标注业务层组件. @Controller 用于标注控制层组件(如 struts 中的 action)
@Repository 用于标注数据访问组件,即 DAO 组件。 @Component 泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。 @Scope 用于指定 scope 作用域的(用在类上)
@PostConstruct 用于指定初始化方法(用在方法上)
@PreDestory 用于指定销毁方法(用在方法上)
@Resource 默认按名称装配,当找不到与名称匹配的 bean 才会按类型装配。 @DependsOn:定义 Bean 初始化及销毁时的顺序
@Primary:自动装配时当出现多个 Bean 候选者时,被注解为@Primary 的 Bean 将作为首
选者,否则将抛出异常
@Autowired 默认按类型装配,如果我们想使用按名称装配,可以结合@Qualifier 注解一起使
用。如下:
@Autowired @Qualifier(“personDaoBean”) 存在多个实例配合使用
5.9 Spring 框架中的单例 Beans 是线程安全的么?
Spring 框架并没有对单例 bean 进行任何多线程的封装处理。关于单例 bean 的线程
安全和并发问题需要开发者自行去搞定。但实际上,大部分的 Spring bean 并没有可变的
状态(比如 Serview 类和 DAO 类),所以在某种程度上说 Spring 的单例 bean 是线程安全
的。如果你的 bean 有多种状态的话(比如 View Model 对象),就需要自行保证线程安
全。最浅显的解决办法就是将多态 bean 的作用域由“singleton”变更为“prototype”。
5.10 Spring 如何处理线程并发问题?
在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中,绝大
部分 Bean 都可以声明为 singleton 作用域,因为 Spring 对一些 Bean 中非线程安全状态
采用 ThreadLocal 进行处理,解决线程安全问题。
ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机
制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获
得锁的线程则需要排队。而 ThreadLocal 采用了“空间换时间”的方式。
ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的
访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。
ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装
进 ThreadLocal。
5.11 Spring 基于 xml 注入 bean 的几种方式
(1)Set 方法注入;
(2)构造器注入:1.通过 index 设置参数的位置;2.通过 type 设置参数类型;3.通过
name 注入
(3)静态工厂注入;
(4)实例工厂;
5.12 Spring 的自动装配:
在 Spring 中,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互
协作的对象引用赋予各个对象,使用 autowire 来配置自动装载模式。
在 Spring 框架 xml 配置中共有 5 种自动装配:
(1)no:默认的方式是不进行自动装配的,通过手工设置 ref 属性来进行装配 bean。
(2)byName:通过 bean 的名称进行自动装配,如果一个 bean 的 property 与另
一 bean 的 name 相同,就进行自动装配。
(3)byType:通过参数的数据类型进行自动装配。
(4)constructor:利用构造函数进行装配,并且构造函数的参数通过 byType 进行装
配。
(5)autodetect:自动探测,如果有构造方法,通过 construct 的方式自动装配,否
则使用 byType 的方式自动装配。
基于注解的方式:
使用@Autowired 注解来自动装配指定的 bean。在使用@Autowired 注解之前需要
在 Spring 配置文件进行配置,
容器自动装载了一个 AutowiredAnnotationBeanPostProcessor 后置处理器,当容器扫描
到@Autowied.@Resource 或@Inject 时,就会在 IoC 容器自动查找需要的 bean,并装
配给该对象的属性。在使用@Autowired 时,首先在容器中查询对应类型的 bean:
如果查询结果刚好为一个,就将该 bean 装配给@Autowired 指定的数据;
如果查询的结果不止一个,那么@Autowired 会根据名称来查找;
如果上述查找的结果为空,那么会抛出异常。解决方法时,使用 required=false。 @Autowired 可用于:构造函数.成员变量.Setter 方法
注:@Autowired 和@Resource 之间的区别
(1) @Autowired 默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在
(可以设置它 required 属性为 false)。
(2) @Resource 默认是按照名称来装配注入的,只有当找不到与名称匹配的 bean 才
会按照类型来装配注入。
5.13 Spring 框架中都用到了哪些设计模式?
(1)工厂模式:BeanFactory 就是简单工厂模式的体现,用来创建对象的实例;
(2)单例模式:Bean 默认为单例模式。
(3)代理模式:Spring 的 AOP 功能用到了 JDK 的动态代理和 CGLIB 字节码生成技
术;
(4)模板方法:用来解决代码重复的问题。比
如. RestTemplate, JmsTemplate, JpaTemplate。
(5)观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,
所有依赖于它的对象都会得到通知被制动更新,如 Spring 中 listener 的实现
–ApplicationListener。
5.14 Spring 事务的实现方式和实现原理
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,
spring 是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过
binlog 或者 redo log 实现的。
(1)Spring 事务的种类:
spring 支持编程式事务管理和声明式事务管理两种方式:
A.编程式事务管理使用 TransactionTemplate。
B.声明式事务管理建立在 AOP 之上的。其本质是通过 AOP 功能,对方法
前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始
之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,
只需在配置文件中做相关的事务规则声明或通过@Transactional 注解的方式,
便可以将事务规则应用到业务逻辑中。
声明式事务管理要优于编程式事务管理,这正是 spring 倡导的非侵入式的
开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持。唯
一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以
作用到代码块级别。
(2)Spring 的事务传播行为:
Spring 事务的传播行为说的是,当多个事务同时存在的时候,Spring 如何
处理这些事务的行为。
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,
如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就
加入该事务,如果当前不存在事务,就以非事务执行。‘
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,
就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事
务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当
前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则
抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。
如果当前没有事务,则按 REQUIRED 属性执行。
(3)Spring 中的隔离级别:
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,
使用数据库默认的事务隔离级别。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可以看到这个
事务未提交的数据。
③ ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才
能被另一事务读取,而且能看到该事务对已有记录的更新。
④ ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才
能被另一事务读取,但是不能看到该事务对已有记录的更新。
⑤ ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据
库所做的更新。
5.15 Spring 框架中有哪些不同类型的事件?
Spring 提供了以下 5 种标准的事件:
(1)上下文更新事件(ContextRefreshedEvent):在调用
ConfigurableApplicationContext 接口中的 refresh()方法时被触发。
(2)上下文开始事件(ContextStartedEvent):当容器调用
ConfigurableApplicationContext 的 Start()方法开始/重新开始容器时触发该
事件。
(3)上下文停止事件(ContextStoppedEvent):当容器调用
ConfigurableApplicationContext 的 Stop()方法停止容器时触发该事件。
(4)上下文关闭事件(ContextClosedEvent):当 ApplicationContext
被关闭时触发该事件。容器被关闭时,其管理的所有单例 Bean 都被销毁。
(5)请求处理事件(RequestHandledEvent):在 Web 应用中,当一个
http 请求(request)结束触发该事件。
如果一个 bean 实现了 ApplicationListener 接口,当一个
ApplicationEvent 被发布以后,bean 会自动被通知。
5.16 解释一下 Spring AOP 里面的几个名词
(1)切面(Aspect):被抽取的公共模块,可能会横切多个对象。在 Spring
AOP 中,切面可以使用通用类(基于模式的风格) 或者在普通类中
以 @AspectJ 注解来实现。
(2)连接点(Join point):指方法,在 Spring AOP 中,一个连接点 总
是 代表一个方法的执行。
(3)通知(Advice):在切面的某个特定的连接点(Join point)上执行
的动作。通知有各种类型,其中包括“around”.“before”和“after”等通知。
许多 AOP 框架,包括 Spring,都是以拦截器做通知模型, 并维护一个以连接
点为中心的拦截器链。
(4)切入点(Pointcut):切入点是指 我们要对哪些 Join point 进行拦
截的定义。通过切入点表达式,指定拦截的方法,比如指定拦截 add*.search*。
(5)引入(Introduction):(也被称为内部类型声明(inter-type
declaration))。声明额外的方法或者某个类型的字段。Spring 允许引入新的
接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入
来使 bean 实现 IsModified 接口,以便简化缓存机制。
(6)目标对象(Target Object): 被一个或者多个切面(aspect)所通
知(advise)的对象。也有人把它叫做 被通知(adviced) 对象。 既然 Spring
AOP 是通过运行时代理实现的,这个对象永远是一个 被代理(proxied)对象。
(7)织入(Weaving):指把增强应用到目标对象来创建新的代理对象的
过程。Spring 是在运行时完成织入。
切入点(pointcut)和连接点(join point)匹配的概念是 AOP 的关键,
这使得 AOP 不同于其它仅仅提供拦截功能的旧技术。 切入点使得定位通知
(advice)可独立于 OO 层次。 例如,一个提供声明式事务管理的 around 通
知可以被应用到一组横跨多个对象中的方法上(例如服务层的所有业务操作)。
5.17 Spring 通知有哪些类型?
(1)前置通知(Before advice):在某连接点(join point)之前执行的通知,但这
个通知不能阻止连接点前的执行(除非它抛出一个异常)。
(2)返回后通知(After returning advice):在某连接点(join point)正常完成后
执行的通知:例如,一个方法没有抛出任何异常,正常返回。
(3)抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。
(4)后通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是
正常返回还是异常退出)。
(5)环绕通知(Around Advice):包围一个连接点(join point)的通知,如方法
调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它
也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。 环绕
通知是最常用的一种通知类型。大部分基于拦截的 AOP 框架,例如 Nanning 和 JBoss4,
都只提供环绕通知。
同一个 aspect,不同 advice 的执行顺序:
①没有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterReturning
②有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterThrowing:异常发生
java.lang.RuntimeException: 异常发生
六. Mybatis 框架
6.1 什么是 Mybatis?
(1)Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,
开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动.创建连接. 创建 statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql
执行性能,灵活度高。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映
射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取
结果集。
(3)通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,
并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql
语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。(从执
行 sql 到返回 result 的过程)。
6.2 Mybaits 的优点
(1)基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设
计造成任何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管
理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。
(2)与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的
代码,不需要手动开关连接;
(3)很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,
所以只要 JDBC 支持的数据库 MyBatis 都支持)。
(4)能够与 Spring 很好的集成;
(5)提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象
关系映射标签,支持对象关系组件维护。
6.3 MyBatis 框架的缺点
(1)SQL 语句的编写工作量较大,尤其当字段多.关联表多时,对开发人员
编写 SQL 语句的功底有一定要求。
(2)SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
6.4 MyBatis 框架适用场合
(1)MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案。
(2)对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis
将是不错的选择。
6.5 MyBatis 与 Hibernate 有哪些不同?
(1)Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis
需要程序员自己编写 Sql 语句。
(2)Mybatis 直接编写原生态 SQL,可以严格控制 SQL 执行性能,灵活
度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频
繁,一但需求变化要求迅速输出成果。但是灵活的前提是 Mybatis 无法做到数
据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套 SQL 映
射文件,工作量大。
(3)Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要
求高的软件,如果用 Hibernate 开发可以节省很多代码,提高效率。
6.6 #{}和KaTeX parse error: Expected 'EOF', got '#' at position 11: {}的区别是什么? #̲{}是预编译处理,{}是字符串替换。
Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用
PreparedStatement 的 set 方法来赋值;
Mybatis 在处理 时,就是把 {}时,就是把 时,就是把{}替换成变量的值。
使用#{}可以有效的防止 SQL 注入,提高系统安全性。
6.7 Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执
行的内存分页,而非物理分页。可以在 SQL 内直接书写带有物理分页的参数来
完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,
在插件的拦截方法内拦截待执行的 SQL,然后重写 SQL,根据 dialect 方言,添
加对应的物理分页语句和物理分页参数。
6.8 Mybatis是如何将 SQL 执行结果封装为目标对象并返回
的?都有哪些映射形式?
第一种是使用标签,逐一定义数据库列名和对象属性名之间
的映射关系。
第二种是使用标签和 SQL 列的别名功能,将列的别名书写为
对象属性名。
有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反
射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值
的。
6.9 Mybatis 动态 SQL 有什么用?执行原理?有哪些动
sql?
Mybatis 动态 SQL 可以在 Xml 映射文件内,以标签的形式编写动态 sql,
执行原理是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能。
Mybatis 提供了 9 种动态 sql 标签:trim | where | set | foreach | if |
choose | when | otherwise | bind。
6.10 Xml 映射文件中,除了常见的
select|insert|updae|delete 标签之外,还有哪些标签?
....,加上
动态 sql 的 9 个标签,其中为 sql 片段标签,通过标签引入 sql
片段,为不支持自增的主键生成策略标签。
6.11 Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,
id 是否可以重复?
不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果
没有配置 namespace,那么 id 不能重复;
原因就是 namespace+id 是作为 Map
key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相
覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id
自然也就不同。
6.12 为什么说 Mybatis 是半自动 ORM 映射工具?它与全自
动的区别在哪里?
Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合
对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis 在查询关联对
象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。
6.13 MyBatis 实现一对一有几种方式?具体怎么操作的?
有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在 resultMap 里
面配置 association 节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键 id,去再另外一个表里面查询
数据,也是通过 association 配置,但另外一个表的查询通过 select 属性配置。
6.14 MyBatis 实现一对多有几种方式,怎么操作的?
有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在
resultMap 里面的 collection 节点配置一对多的类就可以完成;嵌套查询是先
查一个表,根据这个表里面的 结果的外键 id,去再另外一个表里面查询数据,也是
通过配置 collection,但另外一个表的查询通过 select 节点配置。
6.15 Mybatis 是否支持延迟加载?如果支持,它的实现原理是
什么?
答:Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延
迟加载,association 指的就是一对一,collection 指的就是一对多查询。在
Mybatis 配置文件中,可以配置是否启用延迟加载
lazyLoadingEnabled=true|false。
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,
进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现
a.getB()是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把
B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成
a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原
理都是一样的。
6.16 Mybatis 的一级.二级缓存
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作
用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有
Cache 就将清空,默认打开一级缓存且不能关闭。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,
HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自
定义存储源,如 Ehcache。默认不打开二级缓存,要手动开启二级缓存,使用
二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在
它的映射文件中配置 ;
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存
Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存
将被 clear。
6.17 什么是 MyBatis 的接口绑定?有哪些实现方式?
接口绑定,就是在 MyBatis 中任意定义接口,然后把接口里面的方法和 SQL
语句绑定, 我们直接调用接口方法就可以,这样比起原来了 SqlSession 提供的方
法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加
上 @Select.@Update 等注解,里面包含 Sql 语句来绑定;另外一种就是通过
xml 里面写 SQL 来绑定, 在这种情况下,要指定 xml 映射文件里面的 namespace
必须为接口的全路径名。当 Sql 语句比较简单时候,用注解绑定, 当 SQL 语句比
较复杂时候,用 xml 绑定,一般用 xml 绑定的比较多。
6.18 使用 MyBatis 的 mapper 接口调用时有哪些要求?
Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的 id 相同;
Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的
parameterType 的类型相同;
Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的
resultType 的类型相同;
Mapper.xml 文件中的 namespace 即是 mapper 接口的类路径。
6.19 简述 Mybatis 的插件运行原理,以及如何编写一个插件。
Mybatis 仅可以编写针对
ParameterHandler.ResultSetHandler.StatementHandler.Executor 这 4 种接口的插件, 在四大对象创建的时候
1、每个创建出来的对象不是直接返回的,而是
interceptorChain.pluginAll(parameterHandler);
2、获取到所有的 Interceptor(拦截器)(插件需要实现的接口);
调用 interceptor.plugin(target);返回 target 包装后的对象
3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP(面
向切面)
我们的插件可以为四大对象创建出代理对象;
代理对象就可以拦截到四大对象的每一个执行;
Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接
口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体
就是 InvocationHandler 的 invoke()方法,当然,只会拦截那些你指定需要拦
截的方法。
编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然
后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在
配置文件中配置你编写的插件。