java面试笔记
文章目录
- java面试笔记
- 一、Java基础
- 1、什么是面向对象
- 2、JDK、JRE、JVM三者的区别和联系
- 3、==和equals
- 4、final
- 5、String、StringBuilder和StringBuffer区别及使用场景
- 6、重载和重写
- 7、接口和抽象类的区别
- 8、List和Set的区别
- 9、hashCode和equals
- 10、ArrayList和LinkedList
- 11、HashMap和HashTable的区别,底层实现是什么?
- 12、ConcurrentHashMap原理,jdk7和jdk8版本的区别
- 13、什么是字节码?采用字节码的好处是什么?
- 14、Java类加载器
- 15、双亲委托模型
- 16、Java中的异常体系
- 17、GC如何判断对象可以被回收
- 二、线程、并发
- 三、Spring
- 四、Spring MVC、Spring Boot
- 五、Mybatis
- 六、Redis
一、Java基础
1、什么是面向对象
面向过程:面向过程注重事情的每一个步骤和顺序。
面向对象:更注重事情的参与者(对象)以及各自需要做什么。
面向对象的三大特性:
**封装:**在于明确标识出允许外部使用的所有成员函数和数据项,增加安全性和简化编程。
**继承:**继承基类的方法,并做出自己的改变和拓展。
**多态:**基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。
要求:继承,方法重写,父类引用指向子类对象。
父类类型 变量名=new 子类对象;
变量名.方法名();
2、JDK、JRE、JVM三者的区别和联系
(1) JDK:Java Develpment Kit (Java 开发工具)
(2)JRE:Java Runtime Environment (Java 运行时环境)
(3)JVM:Java Virtual Machine (Java虚拟机)
3、==和equals
==:对比的是栈中的值,基本数据类型是变量值,引用数据类型是堆中内存对象的地址。
equals:object中默认也是采用==比较,通常会重写
Object
public boolean equals(Object obj){
return (this==object);
}
String已经默认重写了equals
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
4、final
- 修饰类:表示类不可继承;
- 修饰方法:表示方法不能被子类覆盖,当时可以重载;
- 修饰变量:表示变量一旦被赋值就不能更改它的值。
-
修饰成员变量
- 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
- 如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
-
修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示的初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)。public class FinalVar{ final static int a=0;//在声明的时候就需要赋值初始化或者静态代码块中赋值 final static int b; static{ b=0; } final int c=0;//在声明的时候就需要赋值初始化或者代码块中赋值或者构造器中赋值 final int d; static{ d=0; } FinalVar(int d){ this.d=d; } public static void mian(String[] args){ final int localA; //局部变量只声明没有初始化,不会报错,与final无关 localA=0;//在使用前一定要赋值 } }
-
修饰基本类型数据和引用类型数据
-
如果是基本数据类型的变量,则其数值一旦在初始化之后编不能修改;
-
如果是引用类型数据的变量,则在对其初始化之后便不能在让其指向另一个对象,但是引用的值是可变的。
public class FinalTest { public static void main(String[] args) { final int[] iArr={1,2,3,4}; iArr[2]=-3;//合法的 iArr=null;//非法的,对iArr不能重新赋值 final Person p=new Person(25); p.setAge(18);//合法的 p=null;//非法的 } }
-
5、String、StringBuilder和StringBuffer区别及使用场景
String是final修饰的,不可变,每次操作都会产生新的String对象。
StringBuffer和StringBuilder都是在原对象上操作。
StringBuffer是线程安全的,StrinBuilder是线程不安全的。
StringBuffer方法都是synchronized修饰的。
性能:StringBuilder>StringBuffer>String
场景:经常需要改变字符串内容时使用后面两个,优先使用StringBuilder,多线程使用共享变量时使用StringBuffer。
6、重载和重写
重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和修饰符可以不同,发生在编译时。
public int add(int a,int b);
public String add(int a,int b);
//编译时报错
重写:发生在父子类中,方法名、参数列表必须一致,返回值范围小于等于父类,抛出的异常小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
7、接口和抽象类的区别
- 抽象类可以存在普通成员函数,而接口中只能存在public abstract方法;
- 抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型的;
- 抽象类只能继承一个,接口可以有多个实现。
接口设计的目的是为了对类的行为进行约束(更准确的说是一种“有”的约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。
抽象类的设计目的是代码的复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非空真子集,记为B),可以让这些类都派生与一个抽象类。在这个抽象类中实现了B,避免让所有的子类都来实现B,这就达到了代码复用的目的。而A-B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例出来(否则当调用到A-B,无法执行)。
抽象类是对类本质的抽象,表达的是is a的关系,比如:BMW
is a car
。抽象类包含并实现了子类的通有特性,将子类存在差异化的特性进行抽象,交由子类实现。
接口是对行为的抽象,表达的是like a的关系。比如:Bird
like a Aircraft
(像飞行器一样可以飞),但其本质上is a Bird
。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是定义抽象类的代价高。因为对高级语言来说每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中实现多个接口。在设计阶段会降低难度。
8、List和Set的区别
List:有序,按对象进入的顺序保存对象,可重复,允许有多个Null元素对象,可以使用Iterator(迭代器)取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素。
List<Integer> aList=new ArrayList<>();
aList.add(1);
aList.add(2);
aList.add(3);
aList.add(4);
Iterator<Integer> iterator = aList.iterator();
//iterator.hasNext()如果存在元素的话返回true
while (iterator.hasNext()){
//iterator.next()返回迭代的下一个元素
System.out.println(iterator.next());
}
for (int i = 0; i < aList.size(); i++) {
System.out.println(aList.get(i));
}
输出结果:
1324
1324
Set:无须,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,再逐一遍历各个元素。
Set<Integer> b=new HashSet();
b.add(1);
b.add(3);
b.add(2);
b.add(4);
Iterator<Integer> iterator = b.iterator();
//iterator.hasNext()如果存在元素的话返回true
while (iterator.hasNext()){
//iterator.next()返回迭代的下一个元素
System.out.print(iterator.next());
}
输出结果:
1234
9、hashCode和equals
hashCode介绍
hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,Java中的任何类都包含有hashCode()函数。散列表存储的是键值对(key-value),它的特点是:能够根据“键”快速的检索出对应的“值”。这其中就利用到了散列码(可以快速找到所需要的对象)。
为什么要用hashCode
以“HashSet如何检查重复”为例子来说明为什么要有hashCode:
对象加入HashSet时,HashSet会计算对象的hashCode值来判断对象加入的位置,看该位置是否有值,如果没有,HashSet会假设对象没有重复出现,但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,HashSet就会让其加入操作失败。如果不同的话,就会重新散列到其它位置。这样就大大减少 了equals的次数,相应就大大提高了执行速度。
- 如果两个对象相等,则hashCode一定也是相同的;
- 如果两个对象相等,对两个对象分别调用equals方法都会返回true;
- 如果两个对象有相同的hashCode值,它们不一定是相等的;
- equals方法被覆盖过,则hashCode方法也必须被覆盖;
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
10、ArrayList和LinkedList
ArrayList:基于动态数组,连续内存储存,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据是需要新建数组,然后将老的数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象)。
LinkedList:基于链表,可以存储在分散的内存中,适合做数据的插入及删除操作,不适合查询操作(需要逐一遍历)。遍历LinkedList必须使用Iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消息极大。另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexOf对list进行了遍历,当结果为空时会遍历整个列表。
11、HashMap和HashTable的区别,底层实现是什么?
区别:
- HashMap方法没有synchronized修饰,线程不安全,HashTable线程安全;
- HashMap允许key和value为null,而HashTable不允许。
底层实现:数组+链表实现
Jdk8开始链表高度达到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在。
- 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标;
- 如果没有产生hash冲突(小标位置没有元素),则直接创建Node存入数组;
- 如果产生hash冲突,先进行equals比较,如果相同则取代该元素,不同则判断链表高度插入链表,链表高度到达8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表;
- key为null,存在下标为0的位置。
12、ConcurrentHashMap原理,jdk7和jdk8版本的区别
JDK7:
数据结构:ReentrantLock+Segment+HashEntry,一个Sement中包含一个HashEntry数组,每一个HashEntry又是一个链表结构。
元素查询:二次hash,第一次hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
锁:Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其它的Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容不影响其它的Segment。
get方法无须加锁,volatile保证。
JDK8:
数据结构:synchronized+CAS+Node+红黑树,Node的val和next都使用volatile修饰,保证可见性。
查询,替换,赋值操作都使用CAS
锁:锁链表的head节点,不影响其它元素的读写,锁粒度更细,效率更高,扩容时阻塞所有的读写操作、并发扩容。
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见。
数组用volatile修饰,保证扩容时被读写线程感知。
13、什么是字节码?采用字节码的好处是什么?
Java中的编译器和解释器:
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码就叫做字节码
(即拓展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这就是解释了Java的编译与运行并存的特点。
Java源代码–>编译器–>jvm可执行的Java字节码–>jvm–>jvm中解释–>机器可执行的二进制机器码–>程序运行
采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在不同的计算机上运行。
14、Java类加载器
JDK自带的类加载器:Bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
- Bootstrap ClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%bin下的jar包和class文件。
- ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。
- AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。
15、双亲委托模型
双亲委派模型的好处:
- 主要为了安全,避免用户自己编写的类动态替换Java的核心类,如String。
- 同时避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是把不同的两个类。
16、Java中的异常体系
- Java中的所有异常都来自顶级父类Throwable。
- Throwable下有两个子类:Exception和Error。
- Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
- Exception不会导致程序停止,又分为两个部分:RunTimeException运行时异常和CheckedException检查异常。
- RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。
17、GC如何判断对象可以被回收
可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
GC Roots的对象有:
- 虚拟机栈(栈中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
每个对象只能触发一次finalize()方法。
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。
二、线程、并发
1、线程的生命周期?线程有几种状态?
- 线程通常有五种状态:创建、就绪、运行、阻塞和死亡状态。
- 阻塞的情况由分为三种:
- 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其它线程调用notify或notifyAll方法才能被唤醒,wait是Object类的方法。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
- 其它阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或超时、或I/O处理完毕时,线程重新转入就绪状态。sleep是Tread类方法。
- 状态详解
- 新建状态(New):新创建一个线程对象;
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权;
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码;
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。知道线程进入就绪状态,才有机会转到运行状态;
- 死亡状态(Dead):线程执行完了或者因异常退出run方法,该线程结束生命周期。
2、sleep()、wait()、join()、yield的区别
-
锁池
所有需要竞争同步锁的线程都会放在锁池中,比如当前对象的锁已经被其中一个对象得到,则其它线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待CPU资源分配。
-
等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池中,而notifyAll()是将等待池的所有线程放到锁池当中。
- sleep是Thread类的静态本地方法,wait则是Object类的本地方法。
- sleep方法不会释放锁,但是wait会释放,而且会加入到等待队列中。
sleep就是把CPU的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回CPU资源,参与CPU的调度,获取到CPU资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入冻结状态,也就是说其它需要这个锁的线程根本不可能获取到这个锁,也就是说无法执行程序。如果在睡眠期间其它线程调用了这个线程的interrupt方法,那么这个线程就会抛出interruptExeception异常,这点和wait一样。
- sleep方法不依赖与同步器synchronized,但是wait需要依赖synchronized。
- sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)。
- sleep一般用于当前线程休眠,或者轮询暂停操作,wait则多用于多线程之间的通信。
- sleep会让出CPU执行时间且强行上下文交换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。
yield()执行线程后直接进入就绪状态,马上释放CPU的执行权,但是依然保留了CPU的执行资格,所以有可能CPU下次进入线程调度还会让这个线程获取到执行权继续执行。
join()执行线程后进入阻塞状态,例如在线程B中调用线程A的join(),那么线程B就会进入阻塞队列,直到线程A结束或中断。
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("22222222");
}
});
t1.start();
t1.join();
// 这行代码必须要等t1全部执行完毕,才会执行
System.out.println("1111");
}
3、线程安全的理解
不是对线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问。
当多个线程访问一个对象时,如果不用进行额外的同步控制或其它的协作操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。
在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
4、Thread和Runable的区别
Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会newThread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现Runnable。
//会卖出多一倍的票
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
static class MyThread extends Thread {
private int ticket = 5;
@Override
public void run() {
while (true) {
System.out.println("Thread ticket = " + ticket--);
if (ticket < 0) {
break;
}
}
}
}
//正常卖出
public static void main(String[] args) {
MyThread2 mt=new MyThread2();
new Thread(mt).start();
new Thread(mt).start();
}
static class MyThread2 implements Runnable {
private int ticket = 5;
@Override
public void run() {
while (true) {
System.out.println("Runnable ticket = " + ticket--);
if (ticket < 0) {
break;
}
}
}
}
原因是:MyThread创建了两个实例,自然会卖出两倍,属于用法错误
5、守护线程的理解
守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为它不靠谱;
守护线程的作用是什么?
举例, GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。应用场景:(1)来为其它线程提供服务支持的情况;(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。
Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用Java的线程池。
6、ThreadLocal的原理和使用场景
每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所有ThreadLocal对象及其对应的值。
ThreadLocalMap 由一个个 Entry 对象构成
Entry 继承自 WeakReference<ThreadLocal<?>> ,一个 Entry 由 ThreadLocal 对象和 Object 构成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种 隔离
7、ThreadLocal内存泄露原因,如何避免
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
**强引用:**使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
**弱引用:**JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)。
key 使用强引用
当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
每次使用完ThreadLocal都调用它的remove()方法清除数据
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
8、并发、并行和串行的区别
- 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着。
- 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
- 并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行
9、并发的三大特点
- 原子性
原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。
private long count = 0;
public void calc() {
count++;
}
- 将count从主存读取到工作内存的副本中;
- +1的运算
- 将结果写入工作内存
- 将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)
那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。
**关键字:**synchronized
-
可见性当多个线程访问同
一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
//线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;
如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
**关键字:**volatile、synchronized、final
-
有序性
虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
int a = 0; bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a; //4 } }
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。
**关键字:**volatile、synchronized volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则明确的。
synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。
在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。
我们判断使用volatile还是加锁的唯一依据就是volatile的语义能否满足使用的场景(原子性)
10、Volatile
-
保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
//线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;
如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
-
禁止指令重排序优化。
int a = 0; bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a; //4 } }
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。
但是用volatile修饰之后就变得不一样了。
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
inc++; 其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以volatile不能保证线程安全。
11、为什么用线程池?解释下线程池参数?
- 降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
- 提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
- 提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
- corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。
- maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数。
- keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime 来设置空闲时间。
- workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程。
- ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂。
- Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝。
12、线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?
-
一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源.
-
在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。
13、线程池中线程复用原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。
三、Spring
1、spring是什么
轻量级的开源的J2EE框架。它是一个容器框架,用来装javabean(java对象),中间层框架(万能胶)可以起一个连接作用,比如说把Struts和Hibernate粘合在一起运用,可以让我们的企业开发更快、更简洁。
Spring是一个轻量级的控制反转(IOC)和切面编程(AOP)的容器框架。
- 从大小与开销两方面而言Spring都是轻量级;
- 通过控制器反转(IOC)的技术达到耦合的目的;
- 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发;
- 包含并管理应用对象(Bean)的配置和声明周期,这个意义上是一个容器;
- 将简单的组件配置、组合成为复杂的应用,这个意义上是一个框架。
2、AOP的理解
系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。
当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。
日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。
在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情。
3、IOC的理解
IOC容器:
实际上就是个map(key,value),里面存的是各种对象(在xml里配置的bean节点、@repository、@service、@controller、@component),在项目启动的时候会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放到map里、扫描到打上上述注解的类还是通过反射创建对象放到map里。
这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入(autowired、resource等注解,xml里bean节点内的ref属性,项目启动的时候会读取xml节点ref属性根据id注入,也会扫描这些注解,根据类型或id注入;id就是对象名)。
控制反转:
没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
引入IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。
全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
依赖注入:
“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。
4、如何实现一个IOC容器
- 配置文件配置包扫描路径
- 递归包扫描获取.class文件
- 反射、确定需要交给IOC管理的类
- 对需要注入的类进行依赖注入
- 配置文件中指定需要扫描的包路径;
- 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解;
- 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以.class结尾的文件添加到一个set集合中进行存储;
- 遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象;
- 遍历这个IOC容器,获取每一个类的实例,判断里面是否有依赖其它类的实例,然后进行递归注入。
5、BeanFactory和ApplicationContext有什么区别?
ApplicationContext是BeanFactory的子接口。
ApplicationContext提供了更加完整的功能:
- 继承MessageSource,因此支持国际化。
- 统一的资源文件访问方式。
- 提供在监视器中注册bean的事件。
- 同时加载多个配置文件。
- 载入多个(有继承关系)上下文,使得每一个上下文都专注一个特定的层次,比如应用的web层。
- BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean方法),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFactory加载后,直至第一次使用调用getBean方法才会抛出异常。
- ApplicationContext它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预加载所有的单实例Bean,通过预载入单实例Bean,确保当你需要的时候,你就不同等待,因为它们已经创建好了。
- 相对于基本的BeanFactory,ApplicationContext唯一的不足是内存空间。当应用程序配置Bean较多时,程序启动较慢。
- BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
- BeanFactory和ApplicationContext都支持BeanPostProcessor,BeanFactoryPostProcessor的使用,但两者的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
6、Spring Bean的生命周期
- 解析类得到BeanDefinition;
- 如果有多个构造方法,则要推断构造方法;
- 确定好构造方法后,进行实例化得到一个对象;
- 对对象中的加了@Autowired注解的属性进行属性填充;
- 回调Aware方法,比如BeanNameAware,BeanFactoryAware;
- 调用BeanPostProcessor的初始化前的方法;
- 调用初始化方法;
- 调用BeanPostProcessor的初始化后的方法,在这里会进行AOP;
- 如果当前创建的bean是单例的则会把bean放入单例池;
- 使用bean;
- Spring容器关闭时调用DisposableBean中destory()方法;
7、Spring Bean的作用域
- singleton(单例):默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。该对象的生命周期是与Spring IOC容器一致的(但只有第一次被注入时才会被创建);
- prototype:为每一个Bean请求提供一个实例。在每一次注入时都会创建一个新的对象。
- request:Bean被定义在每一个Http请求中创建一个单例对象,也就是说在单个请求中会复用这个单例对象。
- session:与request范围类似,确保每一个session中有一个Bean的实例,在session过期后,bean会随之失效。
- application:bean被定义为在ServletContext的生命周期中复用一个单例对象。该作用域仅适用于web的Spring WebApplicationContext环境。
8、Spring框架中的单例Bean是线程安全的吗?
不安全;Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。
如果Bean是有状态的,那么就需要开发人员自己来进行线程安全的保证,最简单的方法就是改变bean的作用域。把“singleton”改为“prototype”,这样每次请求bean就相当于是new Bean(),这样就可以保证线程的安全了。
- 有状态就是有数据存储功能;
- 无状态就是不会保存数据。
controller、service和dao层本身不是线程安全的,如果只是调用里面的方法,而且多线程电泳一个实例方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。
Dao会操作数据库Connection,Connection是带有状态的,比如说数据库事务,Spring的事务管理器使用TreadLocal为不同的线程维护了一套独立的connection副本,保证线程之间不会互相影响(Spring是如何保证事务获取同一Connection的)
不要在Bean中生命任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果Bean的实例变量或类变量需要在多个线程中共享,那么就只能使用synchronized、lock、CAS等实现线程同步的方法。
9、Spring事务的实现方式和原理以及隔离级别
在使用Spring框架时,可以有两种使用事务的方式,一种是编程式,一种是声明式,@Transactional注解就是声明式。
首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行拓展,以及提供了一些能让程序员更加方便操作事务的方式。
比如我们可以通过在某个方法上增加@transactional注解,就可以开启事务,这个方法中所有的SQL都会在同一个事务中执行,统一成功或失败。
在一个方法上加了@Transactional注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为Bean,当在使用这个代理对象的方法时,如果这个方法上存在@Transactional注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑出现了异常,那么则会将事务进行回滚。
针对哪些异常回滚事务是可以配置的,可以利用@Transaction注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException和Error进行回滚。
Spring事务隔离的级别就是数据库的隔离级别:
- default(默认级别)
- read uncommitted(读未提交)
- read committed(读提交、不可重复读)
- repeatable read(可重复度)
- serializable(可串行化)
数据库的配置隔离级别是Read Commited,而Spring配置的隔离级别是Repeatable Read,请问这时隔离 级别是以哪一个为准? 以Spring配置的为准,如果spring设置的隔离级别数据库不支持,效果取决于数据库。
脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。
不可重复读 :是指在一个事务内,多次读同一数据。
幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
10、Spring事务传播机制
多个事务方法互相调用时,事务如何在这些方法间传播。
方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就 由两个方法所定义的事务传播类型所决定。
- REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
- SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
- MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
- REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
- NEVER:不使用事务,如果当前事务存在,则抛出异常
- NESTED:如果当前事务存在,则在嵌套事务中执行,否则和REQUIRED的操作一样(开启一个事务)
和REQUIRES_NEW的区别 REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我 们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。 在NESTED情况下父事务回滚时, 子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。 和REQUIRED的区别 REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于 共用一个事务,所以无论调用方是否catch其异常,事务都会回滚 而在NESTED情况下,被调用方发生异常 时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响。
11、Spring事务什么时候会失效
Spring事务的原理是AOP,进行切面增强,那么失效的根本原因是这个AOP不起作用。
-
发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是这个类对象本身。
-
方法不是public的
@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可 以开启 AspectJ 代理模式。
-
数据库不支持事务
-
没有被Spring管理
-
异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
12、@Resources和@Autowired
共同点:
- @Resource和@Autowired都可以作为注入属性的修饰,在接口仅有单一实现类时,两个注解的修饰效果相同,可以互相替换,不影响使用。
不同点:
- @Resource是Java自己的注解,@Resource有两个属性是比较重要的,分是name和type;Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。
- @Autowired是spring的注解,是spring2.5版本引入的,Autowired只根据type进行注入,不会去匹配name。如果涉及到type无法辨别注入对象时,那需要依赖@Qualifier或@Primary注解一起来修饰。
四、Spring MVC、Spring Boot
1、**Spring Boot、Spring MVC和 **Spring 有什么区别
- Spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更方便将不同类不同方法中的共同处理抽取成切面。自动注入给方法执行,比如日志、异常。
- Spring MVC是Spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接受请求,然后定义了一套路由策略(url到headle的映射)以及适配执行handle,将handle结果使用视图解析技术生成视图展示给前端。
- Spring Boot是Spring提供的一个快速开 发工具包,让程序员能够更方便、更快捷的开发Spring+Spring MVC应用,简化了配置(约定了默认配置),整合了一系列的解决方案redis、mongodb,可以开箱即用。
2、Spring MVC工作流程
- 用户发送请求 至前端控制器DispatcherServlet。
- DispatcherServlet收到请求后调用HandlerMapping处理器映射器。
- 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查询),生成处理器及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
- DispatcherServlet调用HandlerAdapter处理器适配器。
- HandlerAdapter经过适配调用具体的处理器(controller,也叫后端处理器)。
- Controller执行完成返回ModelAndView。
- HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
- DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
- ViewReslover解析后返回具体View。
- DispatcherServlet根据View进行渲染视图(即将模型数据填充到视图中)。
- DispatcherServlet响应用户。
3、Spring MVC的主要组件
-
HandlerMapping:
initHandlerMappings(context),处理器映射器,根据用户请求的资源uri来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事。
-
HandlerAdapter
initHandlerAdapters(context),适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。
-
HandlerExceptionResolver
initHandlerExceptionResolvers(context), 其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。
-
ViewResolver
initViewResolvers(context),ViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。
-
RequestToViewNameTranslator
initRequestToViewNameTranslator(context),ViewResolver是根据ViewName查找View,但有的Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。RequestToViewNameTranslator在Spring MVC容器里只可以配置一个,所以所有request到ViewName的转换规则都要在一个Translator里面全部实现。
-
LocaleResolver
initLocaleResolver(context), 解析视图需要两个参数:一是视图名,另一个是Locale。视图名是处理器返回的,Locale是从哪里来的?这就是LocaleResolver要做的事情。LocaleResolver用于从request解析出Locale,Locale就是zh-cn之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。
-
ThemeResolver
initThemeResolver(context),用于解析主题。SpringMVC中一个主题对应一个properties文件,里面 存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主 题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource 和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这 是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是 ThemeSource的工作。最后从主题中获取资源就可以了。
-
MultipartResolver
initMultipartResolver(context),用于处理上传请求。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。
-
FlashMapManager
initFlashMapManager(context),用来管理FlashMap的,FlashMap主要用在redirect中传递参数。
五、Mybatis
1、mybatis的优缺点
优点:
1、基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签, 支持编写动态 SQL 语句, 并可重用。
2、与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
3、很好的与各种数据库兼容( 因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC 支持的数据库MyBatis 都支持)。
4、能够与 Spring 很好的集成;
5、提供映射标签, 支持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签, 支持对象关系组件维护。
缺点:
1、SQL 语句的编写工作量较大, 尤其当字段多、关联表多时, 对开发人员编写SQL 语句的功底有一定要求。
2、SQL 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库。
2、MyBatis与Hibernate有哪些不同?
开发速度的对比:
Hibernate的真正掌握要比Mybatis难些。Mybatis框架相对简单很容易上手,但也相对简陋些。比起两者的开发速度,不仅仅要考虑到两者的特性及性能,更要根据项目需求去考虑究竟哪一个更适合项目开发,比如:一个项目中用到的复杂查询基本没有,就是简单的增删改查,这样选择hibernate效率就很快了,因为基本的sql语句已经被封装好了,根本不需要你去写sql语句,这就节省了大量的时间,但是对于一个大型项目,复杂语句较多,这样再去选择hibernate就不是一个太好的选择,选择mybatis就会加快许多,而且语句的管理也比较方便。
开发工作量的对比:
Hibernate和MyBatis都有相应的代码生成工具。可以生成简单基本的DAO层方法。针对高级查询,Mybatis需要手动编写SQL语句,以及ResultMap。而Hibernate有良好的映射机制,开发者无需关心SQL的生成与结果映射,可以更专注于业务流程。
sql优化方面:
Hibernate的查询会将表中的所有字段查询出来,这一点会有性能消耗。Hibernate也可以自己写SQL来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。而Mybatis的SQL是手动编写的,所以可以按需求指定查询的字段。
Hibernate HQL语句的调优需要将SQL打印出来,而Hibernate的SQL被很多人嫌弃因为太丑了。MyBatis的SQL是自己手动写的所以调整方便。但Hibernate具有自己的日志统计。Mybatis本身不带日志统计,使用Log4j进行日志记录。
对象管理的对比:
Hibernate 是完整的对象/关系映射解决方案,它提供了对象状态管理(state management)的功能,使开发者不再需要理会底层数据库系统的细节。也就是说,相对于常见的 JDBC/SQL 持久层方案中需要管理 SQL 语句,Hibernate采用了更自然的面向对象的视角来持久化 Java 应用中的数据。
换句话说,使用 Hibernate 的开发者应该总是关注对象的状态(state),不必考虑 SQL 语句的执行。这部分细节已经由 Hibernate 掌管妥当,只有开发者在进行系统性能调优的时候才需要进行了解。而MyBatis在这一块没有文档说明,用户需要对对象自己进行详细的管理。
缓存机制对比:
相同点:都可以实现自己的缓存或使用其他第三方缓存方案,创建适配器来完全覆盖缓存行为。
不同点:Hibernate的二级缓存配置在SessionFactory生成的配置文件中进行详细配置,然后再在具体的表-对象映射中配置是哪种缓存。
MyBatis的二级缓存配置都是在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且Mybatis可以在命名空间中共享相同的缓存配置和实例,通过Cache-ref来实现。
两者比较:因为Hibernate对查询对象有着良好的管理机制,用户无需关心SQL。所以在使用二级缓存时如果出现脏数据,系统会报出错误并提示。
而MyBatis在这一方面,使用二级缓存时需要特别小心。如果不能完全确定数据更新操作的波及范围,避免Cache的盲目使用。否则,脏数据的出现会给系统的正常运行带来很大的隐患。
Hibernate功能强大,数据库无关性好,O/R映射能力强,如果你对Hibernate相当精通,而且对Hibernate进行了适当的封装,那么你的项目整个持久层代码会相当简单,需要写的代码很少,开发速度很快,非常爽。
Hibernate的缺点就是学习门槛不低,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡取得平衡,以及怎样用好Hibernate方面需要你的经验和能力都很强才行。
Mybatis入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。
Mybatis的缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。
3、#{}和${}的区别是什么?
#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。
Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 来赋值;
Mybatis 在处理 时 , 就 是 把 {}时, 就是把 时,就是把{}替换成变量的值,调用 Statement 来赋值;
#{} 的变量替换是在DBMS 中、变量替换后,#{} 对应的变量自动加上单引号
的 变 量 替 换 是 在 D B M S 外 、 变 量 替 换 后 , {} 的变量替换是在 DBMS 外、变量替换后, 的变量替换是在DBMS外、变量替换后,{} 对应的变量不会加上单引号
使用#{}可以有效的防止 SQL 注入, 提高系统安全性。
六、Redis
1、RDB和AOF机制
RDB:Redis DataBase
在指定的时间间隔内将内存中的数据集快照写入磁盘中,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,在替换之前的文件,用二进制压缩存储。
优点:
- 整个Redis数据库将只包含一个文件dump.rdb,方便持久化。
- 容灾性很好,方便备份。
- 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以IO最大化。使用单独子进程进行持久化,主进程不会进行任何的IO操作,保证了redis的高性能。
- 相对于数据集大时,比AOF的启动效率更高
缺点:
- 数据安全性低。RDB是间隔一段时间进行持久化,如何持久化之间redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
- 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果数据集较大时,可能导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF:Append Only File
以日志的形式记录服务器所处理的每个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
优点:
- 数据安全,Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。实际上每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会立即记录到磁盘中。
- 通过append模式写文件,即使中途服务器宕机也不会破坏已存在的内容,可以通过redis-check-aof工具解决数据一致性的问题。
- AOF机制的rewrite模式,定期对AOF文件进行重写,以达到压缩的目的。
缺点:
- AOF文件比RDB文件大,且恢复的速度慢。
- 数据集大的时候,比RDB启动效率低。
- 运行效率没有RDB高。
AOF文件比RDB更新频率高,优先使用AOF还原数据。
AOF比RDB更安全也更大。
RDB性能比AOF好。
如果两个都配了优先加载AOF。
2、Redis的过期键的删除策略
Redis是key-value数据库,我们可以设置Redis中缓存的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
- 惰性过期:只有当访问一个key时,才会判断该key是否已经过期,过期则清除。该策略可以最大化地节约CPU资源,但是对内存非常不友好。极端情况下可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期:每隔一段时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中过期的key。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同的情况下使得CPU和内存资源达到最优的平衡效果。
expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。
Redis同时使用了惰性过期和定期过期两种过期策略。
3、Redis线程模型,单线程快的原因
Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器 file event handler。这个文件事件处理器,它是单线程的,所以 Redis 才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了 Redis 内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)。多个 Socket 可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中取出一个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。
然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理。
单线程快的原因:
- 纯内存操作
- 核心是基于非阻塞的IO多路复用机制
- 单线程反而避免了多线程的频繁上下文切换带来的性能问题
4、缓存雪崩、缓存穿透、缓存击穿
**缓存雪崩:**缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库上,数据库上瞬时压力过重雪崩。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
- 缓存预热
- 互斥锁
**缓存穿透:**缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
**缓存击穿:**缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
-
设置热点数据永远不过期。
这里的“永远不过期”包含两层意思:(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。 (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”不过期。
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
String get(final String key) { V v = redis.get(key); String value = v.getValue(); long timeout = v.getTimeout(); if (v.timeout <= System.currentTimeMillis()) { // 异步更新后台异常执行 threadPool.execute(new Runnable() { public void run() { String keyMutex = "mutex:" + key; if (redis.setnx(keyMutex, "1")) { // 3 min timeout to avoid mutex holder crash redis.expire(keyMutex, 3 * 60); String dbValue = db.get(key); redis.set(key, dbValue); redis.delete(keyMutex); } } }); } return value; }
-
加互斥锁
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
-
"提前"使用互斥锁(mutex key)
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下:
v = memcache.get(key); if (v == null) { if (memcache.add(key_mutex, 3 * 60 * 1000) == true) { value = db.get(key); memcache.set(key, value); memcache.delete(key_mutex); } else { sleep(50); retry(); } } else { if (v.timeout <= now()) { if (memcache.add(key_mutex, 3 * 60 * 1000) == true) { // extend the timeout for other threads v.timeout += 3 * 60 * 1000; memcache.set(key, v, KEY_TIMEOUT * 2); // load the latest value from db v = db.get(key); v.timeout = KEY_TIMEOUT; memcache.set(key, value, KEY_TIMEOUT * 2); memcache.delete(key_mutex); } else { sleep(50); retry(); } } }