1、JDK 和 JRE 有什么区别?
JDK(Java Development Kit),Java开发工具包
JRE(Java Runtime Environment),Java运行环境
JDK中包含JRE,JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM工作所需要的类库。
2、== 和 equals 的区别是什么?
- 对于基本类型,==比较的是值;
- 对于引用类型,==比较的是地址;
- equals不能用于基本类型的比较;
- 如果没有重写equals,equals就相当于==;
- 如果重写了equals方法,equals比较的是对象的内容;
3、final 在 java 中有什么作用?
(1)用来修饰一个引用
- 如果引用为基本数据类型,则该引用为常量,该值无法修改;
- 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
- 如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
(2)用来修饰一个方法
当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
(3)用来修饰类
当用final修改类时,该类成为最终类,无法被继承。
比如常用的String类就是最终类。
4、java 中的 Math.round(-1.5) 等于多少?
Math提供了三个与取整有关的方法:ceil、floor、round
(1)ceil:向上取整;
Math.ceil(11.3) = 12;
Math.ceil(-11.3) = 11;
(2)floor:向下取整;
Math.floor(11.3) = 11;
Math.floor(-11.3) = -12;
(3)round:四舍五入;
加0.5然后向下取整。
Math.round(11.3) = 11;
Math.round(11.8) = 12;
Math.round(-11.3) = -11;
Math.round(-11.8) = -12;
5、String 属于基础的数据类型吗?
不属于。
八种基本数据类型:byte、short、char、int、long、double、float、boolean。
6、String str="i"与 String str=new String(“i”)一样吗?
String str="i"会将起分配到常量池中,常量池中没有重复的元素,如果常量池中存中i,就将i的地址赋给变量,如果没有就创建一个再赋给变量。
String str=new String(“i”)会将对象分配到堆中,即使内存一样,还是会重新创建一个新的对象。
7、如何将字符串反转?
将对象封装到stringBuilder中,调用reverse方法反转。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-goXo7f18-1682099301755)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097172804.png)]
8、String 类的常用方法都有那些?
(1)常见String类的获取功能
length:获取字符串长度;
charAt(int index):获取指定索引位置的字符;
indexOf(int ch):返回指定字符在此字符串中第一次出现处的索引;
substring(int start):从指定位置开始截取字符串,默认到末尾;
substring(int start,int end):从指定位置开始到指定位置结束截取字符串;
(2)常见String类的判断功能
equals(Object obj): 比较字符串的内容是否相同,区分大小写;
contains(String str): 判断字符串中是否包含传递进来的字符串;
startsWith(String str): 判断字符串是否以传递进来的字符串开头;
endsWith(String str): 判断字符串是否以传递进来的字符串结尾;
isEmpty(): 判断字符串的内容是否为空串"";
(3)常见String类的转换功能
byte[] getBytes(): 把字符串转换为字节数组;
char[] toCharArray(): 把字符串转换为字符数组;
String valueOf(char[] chs): 把字符数组转成字符串。valueOf可以将任意类型转为字符串;
toLowerCase(): 把字符串转成小写;
toUpperCase(): 把字符串转成大写;
concat(String str): 把字符串拼接;
(4)常见String类的其他常用功能
replace(char old,char new) 将指定字符进行互换
replace(String old,String new) 将指定字符串进行互换
trim() 去除两端空格
int compareTo(String str) 会对照ASCII 码表 从第一个字母进行减法运算 返回的就是这个减法的结果,如果前面几个字母一样会根据两个字符串的长度进行减法运算返回的就是这个减法的结果,如果连个字符串一摸一样 返回的就是0。
9、new String("a") + new String("b") 会创建几个对象?
对象1:new StringBuilder()
对象2:new String("a")
对象3:常量池中的"a"
对象4:new String("b")
对象5:常量池中的"b"
深入剖析:StringBuilder中的toString():
对象6:new String("ab")
强调一下,toString()的调用,在字符串常量池中,没有生成"ab"
附加题
String s1 = new String("1") + new String("1");//s1变量记录的地址为:new String
s1.intern();//在字符串常量池中生成"11"。如何理解:jdk6:创建了一个新的对象"11",也就有新的地址;jdk7:此时常量池中并没有创建"11",而是创建了一个指向堆空间中new String("11")的地址;
String s2 = "11";
System.out.println(s1 == s2);//jdk6:false;jdk7:true
10、如何将字符串反转?
添加到StringBuilder中,然后调用reverse()。
11、String 类的常用方法都有那些?
equals、length、contains、replace、split、hashcode、indexof、substring、trim、toUpperCase、toLowerCase、isEmpty等等。
12、普通类和抽象类有哪些区别?
抽象类不能被实例化;
抽象类可以有抽象方法,只需申明,无须实现;
有抽象方法的类一定是抽象类;
抽象类的子类必须实现抽象类中的所有抽象方法,否则子类仍然是抽象类;
抽象方法不能声明为静态、不能被static、final修饰。
13、接口和抽象类有什么区别?
(1)接口
接口使用interface修饰;
接口不能实例化;
类可以实现多个接口;
①java8之前,接口中的方法都是抽象方法,省略了public abstract。②java8之后;接口中可以定义静态方法,静态方法必须有方法体,普通方法没有方法体,需要被实现;
(2)抽象类
抽象类使用abstract修饰;
抽象类不能被实例化;
抽象类只能单继承;
抽象类中可以包含抽象方法和非抽象方法,非抽象方法需要有方法体;
如果一个类继承了抽象类,①如果实现了所有的抽象方法,子类可以不是抽象类;②如果没有实现所有的抽象方法,子类仍然是抽象类。
14、java 中 IO 流分为几种?
(1)按流划分,可以分为输入流和输出流;
(2)按单位划分,可以分为字节流和字符流;
字节流:inputStream、outputStream;
字符流:reader、writer;
15、BIO、NIO、AIO 有什么区别?
(1)同步阻塞BIO
一个连接一个线程。
JDK1.4之前,建立网络连接的时候采用BIO模式,先在启动服务端socket,然后启动客户端socket,对服务端通信,客户端发送请求后,先判断服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话会等待请求结束后才继续执行。
(2)同步非阻塞NIO
NIO主要是想解决BIO的大并发问题,BIO是每一个请求分配一个线程,当请求过多时,每个线程占用一定的内存空间,服务器瘫痪了。
JDK1.4开始支持NIO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中。
一个请求一个线程。
(3)异步非阻塞AIO
一个有效请求一个线程。
JDK1.7开始支持AIO,适用于连接数目多且连接比较长的结构,比如相册服务器,充分调用OS参与并发操作。
16、Files的常用方法都有哪些?
exist
createFile
createDirectory
write
read
copy
size
delete
move
17、什么是反射?
所谓反射,是java在运行时进行自我观察的能力,通过class、constructor、field、method四个方法获取一个类的各个组成部分。
在Java运行时环境中,对任意一个类,可以知道类有哪些属性和方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于反射机制。
18、什么是 java 序列化?什么情况下需要序列化?
序列化就是一种用来处理对象流的机制。将对象的内容流化,将流化后的对象传输于网络之间。
序列化是通过实现serializable接口,该接口没有需要实现的方法,implement Serializable只是为了标注该对象是可被序列化的,使用一个输出流(FileOutputStream)来构造一个ObjectOutputStream对象,接着使用ObjectOutputStream对象的writeObejct(Object object)方法就可以将参数的obj对象到磁盘,需要恢复的时候使用输入流。
序列化是将对象转换为容易传输的格式的过程。
例如,可以序列化一个对象,然后通过HTTP通过Internet在客户端和服务器之间传输该对象。在另一端,反序列化将从流中心构造成对象。
一般程序在运行时,产生对象,这些对象随着程序的停止而消失,但我们想将某些对象保存下来,这时,我们就可以通过序列化将对象保存在磁盘,需要使用的时候通过反序列化获取到。
对象序列化的最主要目的就是传递和保存对象,保存对象的完整性和可传递性。
譬如通过网络传输或者把一个对象保存成本地一个文件的时候,需要使用序列化。
19、为什么要使用克隆?如何实现对象克隆?深拷贝和浅拷贝区别是什么?
(1)什么要使用克隆?
想对一个对象进行复制,又想保留原有的对象进行接下来的操作,这个时候就需要克隆了。
(2)如何实现对象克隆?
实现Cloneable接口,重写clone方法;
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。
BeanUtils,apache和Spring都提供了bean工具,只是这都是浅克隆。
(3)深拷贝和浅拷贝区别是什么?
浅拷贝:仅仅克隆基本类型变量,不克隆引用类型变量;
深克隆:既克隆基本类型变量,又克隆引用类型变量;
(4)代码实例
20、throw 和 throws 的区别?
(1)throw
作用在方法内,表示抛出具体异常,由方法体内的语句处理;
一定抛出了异常;
(2)throws
作用在方法的声明上,表示抛出异常,由调用者来进行异常处理;
可能出现异常,不一定会发生异常;
21、final、finally、finalize 有什么区别?
final可以修饰类,变量,方法,修饰的类不能被继承,修饰的变量不能重新赋值,修饰的方法不能被重写
finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。
finalize方法用于垃圾回收。
一般情况下不需要我们实现finalize,当对象被回收的时候需要释放一些资源,比如socket链接,在对象初始化时创建,整个生命周期内有效,那么需要实现finalize方法,关闭这个链接。
但是当调用finalize方法后,并不意味着gc会立即回收该对象,所以有可能真正调用的时候,对象又不需要回收了,然后到了真正要回收的时候,因为之前调用过一次,这次又不会调用了,产生问题。所以,不推荐使用finalize方法。
22、try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xlbTyjac-1682099301757)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097198113.png)]
23、常见的异常类有哪些?
- NullPointerException:空指针异常;
- SQLException:数据库相关的异常;
- IndexOutOfBoundsException:数组下角标越界异常;
- FileNotFoundException:打开文件失败时抛出;
- IOException:当发生某种IO异常时抛出;
- ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出此异常;
- NoSuchMethodException:无法找到某一方法时,抛出;
- ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常;
- NumberFormatException:当试图将字符串转换成数字时,失败了,抛出;
- IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
- ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。
24、hashcode是什么?有什么作用?
Java中Object有一个方法:
public native int hashcode();
(1)hashcode()方法的作用
hashcode()方法主要配合基于散列的集合一起使用,比如HashSet、HashMap、HashTable。
当集合需要添加新的对象时,先调用这个对象的hashcode()方法,得到对应的hashcode值,实际上hashmap中会有一个table保存已经存进去的对象的hashcode值,如果table中没有改hashcode值,则直接存入,如果有,就调用equals方法与新元素进行比较,相同就不存了,不同就存入。
(2)equals和hashcode的关系
如果equals为true,hashcode一定相等;
如果equals为false,hashcode不一定不相等;
如果hashcode值相等,equals不一定相等;
如果hashcode值不等,equals一定不等;
(3)重写equals方法时,一定要重写hashcode方法
(4)百度百科
hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
(5)小白解释
1.hashcode是用来查找的,如果你学过数据结构就应该知道,在查找和排序这一章有
例如内存中有这样的位置
0 1 2 3 4 5 6 7
而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,如果不用hashcode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。
但如果用hashcode那就会使效率提高很多。
我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除 8求余数直接找到存放的位置了。2.但是如果两个类有相同的hashcode怎么办那(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,回答是:可以这样。那么如何判断呢?在这个时候就需要定义 equals了。
也就是说,我们先通过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过 equals 来在这个桶里找到我们要的类。
那么。重写了equals(),为什么还要重写hashCode()呢?
想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶,光重写equals()有什么用啊。
25、java 中操作字符串都有哪些类?它们之间有什么区别?
(1)String
String是不可变对象,每次对String类型的改变时都会生成一个新的对象。
(2)StringBuilder
线程不安全,效率高,多用于单线程。
(3)StringBuffer
线程安全,由于加锁的原因,效率不如StringBuilder,多用于多线程。
不频繁的字符串操作使用String,操作频繁的情况不建议使用String。
StringBuilder > StringBuffer > String。
26、java 中都有哪些引用类型?
(1)强引用
Java中默认声明的就是强引用,比如:
Object obj = new Object();
obj = null;
只要强引用存在,垃圾回收器将永远不会回收被引用的对象。如果想被回收,可以将对象置为null;
(2)软引用(SoftReference)
在内存足够的时候,软引用不会被回收,只有在内存不足时,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会跑出内存溢出异常。
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
(3)弱引用(WeakReference)
进行垃圾回收时,弱引用就会被回收。
(4)虚引用(PhantomReference)
(5)引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用、虚引用一起配合使用。
当垃圾回收器准备回收一个对象时,如果发现它还有引用,就会在回收对象之前,把这个引用加入到引用队列中。
程序可以通过判断引用队列中是否加入了引用,来判断被引用的对象是否将要被垃圾回收,这样可以在对象被回收之前采取一些必要的措施。
27、在 Java 中,为什么不允许从静态方法中访问非静态变量?
- 静态变量属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问;
- 非静态变量属于类的对象,只有在类的对象产生时,才会分配内存,通过类的实例去访问;
- 静态方法也属于类本身,但是此时没有类的实例,内存中没有非静态变量,所以无法调用。
28、说说Java Bean的命名规范
- JavaBean 类必须是一个公共类,并将其访问属性设置为 public
- JavaBean 类必须有一个空的构造函数:类中必须有一个不带参数的公用构造器,此构造器也应该通过调用各个特性的设置方法来设置特性的缺省值。
- 一个javaBean类不应有公共实例变量,类变量都为private
- 持有值应该通过一组存取方法(getXxx 和 setXxx)来访问:对于每个特性,应该有一个带匹配公用 getter 和 setter 方法的专用实例变量。
属性为布尔类型,可以使用 isXxx() 方法代替 getXxx() 方法。
通常属性名是要和 包名、类名、方法名、字段名、常量名作出区别的:
首先:必须用英文,不要用汉语拼音
(1)包(package)
用于将完成不同功能的类分门别类,放在不同的目录(包)下,包的命名规则:将公司域名反转作为包名。比如www.sohu.com 对于包名:每个字母都需要小写。比如:com.sohu.test;该包下的Test类的全名是:com.sohu.Test.Java 。
如果定义类的时候没有使用package,那么java就认为我们所定义的类位于默认包里面(default package)。
(2)类
首字母大写,如果一个类由多个单词构成,那么每个单词的首字母都大写,而且中间不使用任何的连接符。尽量使用英文。如ConnectionFactory
(3)方法
首单词全部小写,如果一个方法由多个单词构成,那么从第二个单词开始首字母大写,不使用连接符。addPerson
(4)字段
与方法相同。如ageOfPerson
(5)常量
所有单词的字母都是大写,如果有多个单词,那么使用下划线链接即可。
如:public static final int AGE_OF_PERSON = 20; //通常加上static
29、Java Bean 属性命名规范问题分析
public class User {
private String busName;
private String pCount;
private Boolean isRunning;
//正确的命名方式,驼峰式的
public String getBusName() {
return busName;
}
public void setBusName(String busName) {
this.busName = busName;
}
//这是什么?
public String getpCount() {
return pCount;
}
public void setpCount(String pCount) {
this.pCount = pCount;
}
//这个也是不允许的
public Boolean getIsRunning() {
return isRunning;
}
public void setIsRunning(Boolean isRunning) {
this.isRunning = isRunning;
}
}
\1. javabean属性命名尽量使用常规的驼峰式命名规则
\2. 属性名第一个单词尽量避免使用一个字母:如eBook, eMail。
\3. boolean属性名避免使用 “is” 开头的名称
\4. 随着jdk, eclipse, spring 等软件版本的不断提高, 底版本的出现的问题可能在高版本中解决了, 低版本原来正常的代码可能在高版本环境下不再支持。
30、什么是 Java 的内存模型?
在了解什么是 Java 内存模型之前,先了解一下为什么要提出 Java 内存模型。
之前提到过并发编程有三大问题
CPU 缓存,在多核 CPU 的情况下,带来了可见性问题
操作系统对当前执行线程的切换,带来了原子性问题
译器指令重排优化,带来了有序性问题
为了解决并发编程的三大问题,提出了 JSR-133,新的 Java 内存模型,JDK 5 开始使用。
简单总结下
Java 内存模型是 JVM 的一种规范
定义了共享内存在多线程程序中读写操作行为的规范
屏蔽了各种硬件和操作系统的访问差异,保证了 Java 程序在各种平台下对内存的访问效果一致
解决并发问题采用的方式:限制处理器优化和使用内存屏障
增强了三个同步原语(synchronized、volatile、final)的内存语义
定义了 happens-before 规则
31、在 Java 中,什么时候用重载,什么时候用重写?
(1)重载是多态的集中体现,在类中,要以统一的方式处理不同类型数据的时候,可以用重载。
(2)重写的使用是建立在继承关系上的,子类在继承父类的基础上,增加新的功能,可以用重写。
(3)简单总结:
重载是多样性,重写是增强剂;
目的是提高程序的多样性和健壮性,以适配不同场景使用时,使用重载进行扩展;
目的是在不修改原方法及源代码的基础上对方法进行扩展或增强时,使用重写;
生活例子:
你想吃一碗面,我给你提供了拉面,炒面,刀削面,担担面供你选择,这是重载;
你想吃一碗面,我不但给你端来了面,还给你加了青菜,加了鸡蛋,这个是重写;
设计模式:
cglib实现动态代理,核心原理用的就是方法的重写;
详细解答:
Java的重载(overload) 最重要的应用场景就是构造器的重载,构造器重载后,提供多种形参形式的构造器,可以应对不同的业务需求,加强程序的健壮性和可扩展性,比如我们最近学习的Spring源码中的ClassPathXmlApplicationContext,它的构造函数使用重载一共提供了10个构造函数,这样就为业务的选择提供了多选择性。在应用到方法中时,主要是为了增强方法的健壮性和可扩展性,比如我们在开发中常用的各种工具类,比如我目前工作中的短信工具类SMSUtil, 发短信的方法就会使用重载,针对不同业务场景下的不同形参,提供短信发送方法,这样提高了工具类的扩展性和健壮性。
总结:重载必须要修改方法(构造器)的形参列表,可以修改方法的返回值类型,也可以修改方法的异常信息即访问权限;使用范围是在同一个类中,目的是对方法(构造器)进行功能扩展,以应对多业务场景的不同使用需求。提高程序的健壮性和扩展性。
java的重写(override) 只要用于子类对父类方法的扩展或修改,但是在我们开发中,为了避免程序混乱,重写一般都是为了方法的扩展,比如在cglib方式实现的动态代理中,代理类就是继承了目标类,对目标类的方法进行重写,同时在方法前后进行切面织入。
总结:方法重写时,参数列表,返回值得类型是一定不能修改的,异常可以减少或者删除,但是不能抛出新的异常或者更广的异常,方法的访问权限可以降低限制,但是不能做更严格的限制。
(4)在里氏替换原则中,子类对父类的方法尽量不要重写和重载。(我们可以采用final的手段强制来遵循)
32、举例说明什么情况下会更倾向于使用抽象类而不是接口?
接口和抽象类都遵循”面向接口而不是实现编码”设计原则,它可以增加代码的灵活性,可以适应不断变化的需求。下面有几个点可以帮助你回答这个问题:在 Java 中,你只能继承一个类,但可以实现多个接口。所以一旦你继承了一个类,你就失去了继承其他类的机会了。
接口通常被用来表示附属描述或行为如: Runnable 、 Clonable 、 Serializable 等等,因此当你使用抽象类来表示行为时,你的类就不能同时是 Runnable 和 Clonable( 注:这里的意思是指如果把 Runnable 等实现为抽象类的情况 ) ,因为在 Java 中你不能继承两个类,但当你使用接口时,你的类就可以同时拥有多个不同的行为。
在一些对时间要求比较高的应用中,倾向于使用抽象类,它会比接口稍快一点。如果希望把一系列行为都规范在类继承层次内,并且可以更好地在同一个地方进行编码,那么抽象类是一个更好的选择。有时,接口和抽象类可以一起使用,接口中定义函数,而在抽象类中定义默认的实现。
33、实例化对象有哪几种方式
- new
- clone()
- 通过反射机制创建
//用 Class.forName方法获取类,在调用类的newinstance()方法
Class<?> cls = Class.forName("com.dao.User");
User u = (User)cls.newInstance();
- 序列化反序列化
//将一个对象实例化后,进行序列化,再反序列化,也可以获得一个对象(远程通信的场景下使用)
ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream("D:/data.txt"));
//序列化对象
out.writeObject(user1);
out.close();
//反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:/data.txt"));
User user2 = (User) in.readObject();
System.out.println("反序列化user:" + user2);
in.close();
34、byte类型127+1等于多少
byte的范围是-128~127。
字节长度为8位,最左边的是符号位,而127的二进制为01111111,所以执行+1操作时,01111111变为10000000。
大家知道,计算机中存储负数,存的是补码的兴衰。左边第一位为符号位。
那么负数的补码转换成十进制如下:
一个数如果为正,则它的原码、反码、补码相同;一个正数的补码,将其转化为十进制,可以直接转换。
已知一个负数的补码,将其转换为十进制数,步骤如下:
- 先对各位取反;
- 将其转换为十进制数;
- 加上负号,再减去1;
例如10000000,最高位是1,是负数,①对各位取反得01111111,转换为十进制就是127,加上负号得-127,再减去1得-128;
35、Java 容器都有哪些?
(1)Collection
① set
HashSet、TreeSet
② list
ArrayList、LinkedList、Vector
(2)Map
HashMap、HashTable、TreeMap
36、Collection 和 Collections 有什么区别?
(1)Collection是最基本的集合接口,Collection派生了两个子接口list和set,分别定义了两种不同的存储方式。
(2)Collections是一个包装类,它包含各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等)。
此类不能实例化,就像一个工具类,服务于Collection框架。
37、list与Set区别
(1)List简介
实际上有两种List:一种是基本的ArrayList,其优点在于随机访问元素,另一种是LinkedList,它并不是为快速随机访问设计的,而是快速的插入或删除。
ArrayList:由数组实现的List。允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。
LinkedList :对顺序访问进行了优化,向List中间插入与删除的开销并不大。随机访问则相对较慢。
还具有下列方 法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 这些方法 (没有在任何接口或基类中定义过)使得LinkedList可以当作堆栈、队列和双向队列使用。
(2)Set简介
Set具有与Collection完全一样的接口,因此没有任何额外的功能。实际上Set就是Collection,只是行为不同。这是继承与多态思想的典型应用:表现不同的行为。Set不保存重复的元素(至于如何判断元素相同则较为负责)
Set : 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
HashSet:为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。
TreeSet: 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
(3)list与Set区别
① List,Set都是继承自Collection接口
② List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
③ Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
38、HashMap 和 Hashtable 有什么区别?
- HashMap是线程不安全的,HashTable是线程安全的;
- HashMap中允许键和值为null,HashTable不允许;
- HashMap的默认容器是16,为2倍扩容,HashTable默认是11,为2倍+1扩容;
39、说一下 HashMap 的实现原理?
(1)简介
HashMap基于map接口,元素以键值对方式存储,允许有null值,HashMap是线程不安全的。
(2)基本属性
初始化大小,默认16,2倍扩容;
负载因子0.75;
初始化的默认数组;
size
threshold。判断是否需要调整hashmap容量
(3)HashMap的存储结构
JDK1.7中采用数组+链表的存储形式。
HashMap采取Entry数组来存储key-value,每一个键值对组成了一个Entry实体,Entry类时机上是一个单向的链表结构,它具有next指针,指向下一个Entry实体,以此来解决Hash冲突的问题。
HashMap实现一个内部类Entry,重要的属性有hash、key、value、next。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2FFUAUCN-1682099301758)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097256355.png)]
JDK1.8中采用数据+链表+红黑树的存储形式。当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Az8BusWK-1682099301759)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097273963.png)]
40、set有哪些实现类?
(1)HashSet
HashSet是set接口的实现类,set下面最主要的实现类就是HashSet(也就是用的最多的),此外还有LinkedHashSet和TreeSet。
HashSet是无序的、不可重复的。通过对象的hashCode和equals方法保证对象的唯一性。
HashSet内部的存储结构是哈希表,是线程不安全的。
(2)TreeSet
TreeSet对元素进行排序的方式:
元素自身具备比较功能,需要实现Comparable接口,并覆盖compareTo方法。
元素自身不具备比较功能,需要实现Comparator接口,并覆盖compare方法。
(3)LinkedHashSet
LinkedHashSet是一种有序的Set集合,即其元素的存入和输出的顺序是相同的。
41、说一下 HashSet 的实现原理?
HashSet实际上是一个HashMap实例,数据存储结构都是数组+链表。
HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value都是一个统一的对象PRESENT。
private static final Object PRESENT = new Object();
HashSet中add方法调用的是底层HashMap中的put方法,put方法要判断插入值是否存在,而HashSet的add方法,首先判断元素是否存在,如果存在则插入,如果不存在则不插入,这样就保证了HashSet中不存在重复值。
通过对象的hashCode和equals方法保证对象的唯一性。
42、ArrayList 和 LinkedList 的区别是什么?
ArrayList是动态数组的数据结构实现,查找和遍历的效率较高;
LinkedList 是双向链表的数据结构,增加和删除的效率较高;
43、如何实现数组和 List 之间的转换?
String[] arr = {"zs","ls","ww"};
List<String> list = Arrays.asList(arr);
System.out.println(list);
ArrayList<String> list1 = new ArrayList<String>();
list1.add("张三");
list1.add("李四");
list1.add("王五");
String[] arr1 = list1.toArray(new String[list1.size()]);
System.out.println(arr1);
for(int i = 0; i < arr1.length; i++){
System.out.println(arr1[i]);
}
44、在 Queue 中 poll()和 remove()有什么区别?
(1)offer()和add()区别:
增加新项时,如果队列满了,add会抛出异常,offer返回false。
(2)poll()和remove()区别:
poll()和remove()都是从队列中删除第一个元素,remove抛出异常,poll返回null。
(3)peek()和element()区别:
peek()和element()用于查询队列头部元素,为空时element抛出异常,peek返回null。
45、哪些集合类是线程安全的
Vector:就比Arraylist多了个同步化机制(线程安全)。
Stack:栈,也是线程安全的,继承于Vector。
Hashtable:就比Hashmap多了个线程安全。
ConcurrentHashMap:是一种高效但是线程安全的集合。
46、迭代器 Iterator 是什么?
为了方便的处理集合中的元素,Java中出现了一个对象,该对象提供了一些方法专门处理集合中的元素.例如删除和获取集合中的元素.该对象就叫做迭代器(Iterator)。
47、Iterator 怎么使用?有什么特点?
Iterator 接口源码中的方法:
- java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象
- next() 方法获得集合中的下一个元素
- hasNext() 检查集合中是否还有元素
- remove() 方法将迭代器新返回的元素删除
48、Iterator 和 ListIterator 有什么区别?
(1)ListIterator 继承 Iterator
(2)ListIterator 比 Iterator多方法
- add(E e) 将指定的元素插入列表,插入位置为迭代器当前位置之前
- set(E e) 迭代器返回的最后一个元素替换参数e
- hasPrevious() 迭代器当前位置,反向遍历集合是否含有元素
- previous() 迭代器当前位置,反向遍历集合,下一个元素
- previousIndex() 迭代器当前位置,反向遍历集合,返回下一个元素的下标
- nextIndex() 迭代器当前位置,返回下一个元素的下标
(3)使用范围不同,Iterator可以迭代所有集合;ListIterator 只能用于List及其子类
- ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能
- ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator不可以
- ListIterator 有 nextIndex() 和previousIndex() 方法,可定位当前索引的位置;Iterator不可以
- ListIterator 有 set()方法,可以实现对 List 的修改;Iterator 仅能遍历,不能修改。
49、怎么确保一个集合不能被修改?
我们很容易想到用final关键字进行修饰,我们都知道
final关键字可以修饰类,方法,成员变量,final修饰的类不能被继承,final修饰的方法不能被重写,final修饰的成员变量必须初始化值,如果这个成员变量是基本数据类型,表示这个变量的值是不可改变的,如果说这个成员变量是引用类型,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的。
那么,我们怎么确保一个集合不能被修改?首先我们要清楚,集合(map,set,list…)都是引用类型,所以我们如果用final修饰的话,集合里面的内容还是可以修改的。
我们可以做一个实验:
可以看到:我们用final关键字定义了一个map集合,这时候我们往集合里面传值,第一个键值对1,1;我们再修改后,可以把键为1的值改为100,说明我们是可以修改map集合的值的。
那我们应该怎么做才能确保集合不被修改呢?
我们可以采用Collections包下的unmodifiableMap方法,通过这个方法返回的map,是不可以修改的。他会报 java.lang.UnsupportedOperationException错。
同理:Collections包也提供了对list和set集合的方法。
Collections.unmodifiableList(List)
Collections.unmodifiableSet(Set)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lHSiEFeO-1682099301760)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097297081.png)]
50、队列和栈是什么?有什么区别?
(1)队列先进先出,栈先进后出。
(2)遍历数据速度不同。
栈只能从头部取数据 也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性;
队列则不同,他基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间,因为在遍历的过程中不影像数据结构,速度要快的多。
51、Java8开始ConcurrentHashMap,为什么舍弃分段锁?
ConcurrentHashMap的原理是引用了内部的 Segment ( ReentrantLock ) 分段锁,保证在操作不同段 map 的时候, 可以并发执行, 操作同段 map 的时候,进行锁的竞争和等待。从而达到线程安全, 且效率大于 synchronized。
但是在 Java 8 之后, JDK 却弃用了这个策略,重新使用了 synchronized+CAS。
弃用原因
通过 JDK 的源码和官方文档看来, 他们认为的弃用分段锁的原因由以下几点:
加入多个分段锁浪费内存空间。
生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
为了提高 GC 的效率
新的同步方案
既然弃用了分段锁, 那么一定由新的线程安全方案, 我们来看看源码是怎么解决线程安全的呢?(源码保留了segment 代码, 但并没有使用)。
52、ConcurrentHashMap(JDK1.8)为什么要使用synchronized而不是如ReentranLock这样的可重入锁?
我想从下面几个角度讨论这个问题:
(1)锁的粒度
首先锁的粒度并没有变粗,甚至变得更细了。每当扩容一次,ConcurrentHashMap的并发度就扩大一倍。
(2)Hash冲突
JDK1.7中,ConcurrentHashMap从过二次hash的方式(Segment -> HashEntry)能够快速的找到查找的元素。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。
JDK1.8中,在ConcurrentHashmap进行扩容时,其他线程可以通过检测数组中的节点决定是否对这条链表(红黑树)进行扩容,减小了扩容的粒度,提高了扩容的效率。
下面是我对面试中的那个问题的一下看法。
为什么是synchronized,而不是ReentranLock
(1)减少内存开销
假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
(2)获得JVM的支持
可重入锁毕竟是API这个级别的,后续的性能优化空间很小。
synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。
53、concurrentHashMap和HashTable有什么区别
concurrentHashMap融合了hashmap和hashtable的优势,hashmap是不同步的,但是单线程情况下效率高,hashtable是同步的同步情况下保证程序执行的正确性。
但hashtable每次同步执行的时候都要锁住整个结构,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zXXRjpqC-1682099301762)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097358849.png)]
concurrentHashMap锁的方式是细粒度的。concurrentHashMap将hash分为16个桶(默认值),诸如get、put、remove等常用操作只锁住当前需要用到的桶。
concurrentHashMap的读取并发,因为读取的大多数时候都没有锁定,所以读取操作几乎是完全的并发操作,只是在求size时才需要锁定整个hash。
而且在迭代时,concurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,弱一致迭代器。在这种方式中,当iterator被创建后集合再发生改变就不会抛出ConcurrentModificationException,取而代之的是在改变时new新的数据而不是影响原来的数据,iterator完成后再讲头指针替代为新的数据,这样iterator时使用的是原来的数据。
54、HasmMap和HashSet的区别
(1)先了解一下HashCode
Java中的集合有两类,一类是List,一类是Set。
List:元素有序,可以重复;
Set:元素无序,不可重复;
要想保证元素的不重复,拿什么来判断呢?这就是Object.equals方法了。如果元素有很多,增加一个元素,就要判断n次吗?
显然不现实,于是,Java采用了哈希表的原理。哈希算法也称为散列算法,是将数据依特定算法直接指定到一根地址上,初学者可以简单的理解为,HashCode方法返回的就是对象存储的物理位置(实际上并不是)。
这样一来,当集合添加新的元素时,先调用这个元素的hashcode()方法,就一下子能定位到他应该放置的物理位置上。如果这个位置上没有元素,他就可以直接存储在这个位置上,不用再进行任何比较了。如果这个位置上有元素,就调用它的equals方法与新元素进行比较,想同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际上调用equals方法的次数就大大降低了,几乎只需要一两次。
简而言之,在集合查找时,hashcode能大大降低对象比较次数,提高查找效率。
Java对象的equals方法和hashCode方法时这样规定的:
相等的对象就必须具有相等的hashcode。
- 如果两个对象的hashcode相同,他们并不一定相同。
- 如果两个对象的hashcode相同,他们并不一定相同。
如果两个Java对象A和B,A和B不相等,但是A和B的哈希码相等,将A和B都存入HashMap时会发生哈希冲突,也就是A和B存放在HashMap内部数组的位置索引相同,这时HashMap会在该位置建立一个链接表,将A和B串起来放在该位置,显然,该情况不违反HashMap的使用规则,是允许的。当然,哈希冲突越少越好,尽量采用好的哈希算法避免哈希冲突。
equals()相等的两个对象,hashcode()一定相等;equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。
(2)HashMap和HashSet的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wj7UgL6t-1682099301764)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097484801.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Y864sNx-1682099301765)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097493854.png)]
55、请谈谈 ReadWriteLock 和 StampedLock
ReadWriteLock包括两种子锁
(1)ReadWriteLock
ReadWriteLock 可以实现多个读锁同时进行,但是读与写和写于写互斥,只能有一个写锁线程在进行。
(2)StampedLock
StampedLock是Jdk在1.8提供的一种读写锁,相比较ReentrantReadWriteLock性能更好,因为ReentrantReadWriteLock在读写之间是互斥的,使用的是一种悲观策略,在读线程特别多的情况下,会造成写线程处于饥饿状态,虽然可以在初始化的时候设置为true指定为公平,但是吞吐量又下去了,而StampedLock是提供了一种乐观策略,更好的实现读写分离,并且吞吐量不会下降。
StampedLock包括三种锁:
(1)写锁writeLock:
writeLock是一个独占锁写锁,当一个线程获得该锁后,其他请求读锁或者写锁的线程阻塞, 获取成功后,会返回一个stamp(凭据)变量来表示该锁的版本,在释放锁时调用unlockWrite方法传递stamp参数。提供了非阻塞式获取锁tryWriteLock。
(2)悲观读锁readLock:
readLock是一个共享读锁,在没有线程获取写锁情况下,多个线程可以获取该锁。如果有写锁获取,那么其他线程请求读锁会被阻塞。悲观读锁会认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据进行加锁,这是在读少写多的情况下考虑的。请求该锁成功后会返回一个stamp值,在释放锁时调用unlockRead方法传递stamp参数。提供了非阻塞式获取锁方法tryWriteLock。
(3)乐观读锁tryOptimisticRead:
tryOptimisticRead相对比悲观读锁,在操作数据前并没有通过CAS设置锁的状态,如果没有线程获取写锁,则返回一个非0的stamp变量,获取该stamp后在操作数据前还需要调用validate方法来判断期间是否有线程获取了写锁,如果是返回值为0则有线程获取写锁,如果不是0则可以使用stamp变量的锁来操作数据。由于tryOptimisticRead并没有修改锁状态,所以不需要释放锁。这是读多写少的情况下考虑的,不涉及CAS操作,所以效率较高,在保证数据一致性上需要复制一份要操作的变量到方法栈中,并且在操作数据时可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性得到了保证。
56、线程的run()和start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
57、为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
58、Synchronized 用过吗,其原理是什么?
(1)可重入性
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁;
- 可重入的好处:
- 可以避免死锁;
- 可以让我们更好的封装代码;
synchronized是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。
(2)不可中断性
- 一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;
- synchronized 属于不可被中断;
- Lock lock方法是不可中断的;
- Lock tryLock方法是可中断的;
59、JVM 对 Java 的原生锁做了哪些优化?
(1)自旋锁
在线程进行阻塞的时候,先让线程自旋等待一段时间,可能这段时间其它线程已经解锁,这时就无需让线程再进行阻塞操作了。
自旋默认次数是10次。
(2)自适应自旋锁
自旋锁的升级,自旋的次数不再固定,由前一次自旋次数和锁的拥有者的状态决定。
(3)锁消除
在动态编译同步代码块的时候,JIT编译器借助逃逸分析技术来判断锁对象是否只被一个线程访问,而没有其他线程,这时就可以取消锁了。
4、锁粗化
当JIT编译器发现一系列的操作都对同一个对象反复加锁解锁,甚至加锁操作出现在循环中,此时会将加锁同步的范围粗化到整个操作系列的外部。
锁粒度:不要锁住一些无关的代码。
锁粗化:可以一次性执行完的不要多次加锁执行。
60、为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。
wait(), notify()和 notifyAll()这些方法在同步代码块中调用
有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。
综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。
61、Java 如何实现多线程之间的通讯和协作?
可以通过中断 和 共享变量的方式实现线程间的通讯和协作
比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。
Java中线程通信协作的最常见的两种方式:
1、syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
2、ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
线程间直接的数据交换:
通过管道进行线程间通信:1)字节流;2)字符流
62、Thread 类中的 yield 方法有什么作用?
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
63、为什么说 Synchronized 是非公平锁?
当锁被释放后,任何一个线程都有机会竞争得到锁,这样做的目的是提高效率,但缺点是可能产生线程饥饿现象。
64、请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?
volatile只能作用于变量,保证了操作可见性和有序性,不保证原子性。
在Java的内存模型中分为主内存和工作内存,Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存。
主内存和工作内存之间的交互分为8个原子操作:
- lock
- unlock
- read
- load
- assign
- use
- store
- write
volatile修饰的变量,只有对volatile进行assign操作,才可以load,只有load才可以use,,这样就保证了在工作内存操作volatile变量,都会同步到主内存中。
65、为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?
Synchronized的并发策略是悲观的,不管是否产生竞争,任何数据的操作都必须加锁。
乐观锁的核心是CAS,CAS包括内存值、预期值、新值,只有当内存值等于预期值时,才会将内存值修改为新值。
66、乐观锁一定就是好的吗?
乐观锁认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果冲突则再试一遍,直至成功为止,这个尝试的过程称为自旋。
乐观锁没有加锁,但乐观锁引入了ABA问题,此时一般采用版本号进行控制;
也可能产生自旋次数过多问题,此时并不能提高效率,反而不如直接加锁的效率高;
只能保证一个对象的原子性,可以封装成对象,再进行CAS操作;
67、请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。
(1)相似点
它们都是阻塞式的同步,也就是说一个线程获得了对象锁,进入代码块,其它访问该同步块的线程都必须阻塞在同步代码块外面等待,而进行线程阻塞和唤醒的代码是比较高的。
(2)功能区别
Synchronized是java语言的关键字,是原生语法层面的互斥,需要JVM实现;ReentrantLock 是JDK1.5之后提供的API层面的互斥锁,需要lock和unlock()方法配合try/finally代码块来完成。
Synchronized使用较ReentrantLock 便利一些;
锁的细粒度和灵活性:ReentrantLock强于Synchronized;
(3)性能区别
Synchronized引入偏向锁,自旋锁之后,两者的性能差不多,在这种情况下,官方建议使用Synchronized。
① Synchronized
Synchronized会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。
在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计数器+1,相应的执行monitorexit时,计数器-1,当计数器为0时,锁就会被释放。如果获取锁失败,当前线程就要阻塞,知道对象锁被另一个线程释放为止。
② ReentrantLock
ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有如下三项:
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized避免出现死锁的情况。通过lock.lockInterruptibly()来实现这一机制;
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁是非公平锁;ReentrantLock默认也是非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好;
锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像Synchronized要么随机唤醒一个线程,要么唤醒全部线程。
68、ReentrantLock 是如何实现可重入性的?
(1)什么是可重入性
一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明该锁是可重入的,反之则不可重入。
(2)synchronized是如何实现可重入性
synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。每个锁对象内部维护一个计数器,该计数器初始值为0,表示任何线程都可以获取该锁并执行相应的方法。根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了对象的锁,把锁的计数器+1,相应的在执行monitorexit指令后锁计数器-1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
(3)ReentrantLock如何实现可重入性
ReentrantLock使用内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计算重入次数,避免频繁的持有释放操作带来的线程问题。
(4)ReentrantLock代码实例
// Sync继承于AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
// ReentrantLock默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 可以通过向构造方法中传true来实现公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
protected final boolean tryAcquire(int acquires) {
// 当前想要获取锁的线程
final Thread current = Thread.currentThread();
// 当前锁的状态
int c = getState();
// state == 0 此时此刻没有线程持有锁
if (c == 0) {
// 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
// 看看有没有别人在队列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
// 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=
// 因为刚刚还没人的,我判断过了
compareAndSetState(0, acquires)) {
// 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
setExclusiveOwnerThread(current);
return true;
}
}
// 会进入这个else if分支,说明是重入了,需要操作:state=state+1
// 这里不存在并发问题
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁
return false;
}
(5)代码分析
当一个线程在获取锁过程中,先判断state的值是否为0,如果是表示没有线程持有锁,就可以尝试获取锁。
当state的值不为0时,表示锁已经被一个线程占用了,这时会做一个判断current==getExclusiveOwnerThread(),这个方法返回的是当前持有锁的线程,这个判断是看当前持有锁的线程是不是自己,如果是自己,那么将state的值+1,表示重入返回即可。
69、什么是锁消除和锁粗化?
(1)锁消除
所消除就是虚拟机根据一个对象是否真正存在同步情况,若不存在同步情况,则对该对象的访问无需经过加锁解锁的操作。
比如StringBuffer的append方法,因为append方法需要判断对象是否被占用,而如果代码不存在锁竞争,那么这部分的性能消耗是无意义的。于是虚拟机在即时编译的时候就会将上面的代码进行优化,也就是锁消除。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
从源码可以看出,append方法用了 synchronized关键字,它是线程安全的。但我们可能仅在线程内部把StringBuffer当做局部变量使用;StringBuffer仅在方法内作用域有效,不存在线程安全的问题,这时我们可以通过编译器将其优化,将锁消除,前提是Java必须运行在server模式,同时必须开启逃逸分析;
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。
public static String createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);// append方法是同步操作
sBuf.append(str2);
return sBuf.toString();
}
逃逸分析:比如上面的代码,它要看sBuf是否可能逃出它的作用域?如果将sBuf作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题,这时就可以说sBuf这个对象发生逃逸了,因而不应将append操作的锁消除,但我们上面的代码没有发生锁逃逸,锁消除就可以带来一定的性能提升。
(2)锁粗化
锁的请求、同步、释放都会消耗一定的系统资源,如果高频的锁请求反而不利于系统性能的优化,锁粗化就是把多次的锁请求合并成一个请求,扩大锁的范围,降低锁请求、同步、释放带来的性能损耗。
70、跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?
(1)都是可重入锁;
(2)ReentrantLock内部是实现了Sync,Sync继承于AQS抽象类。Sync有两个实现,一个是公平锁,一个是非公平锁,通过构造函数定义。AQS中维护了一个state来计算重入次数,避免频繁的持有释放操作带来的线程问题。
(3)ReentrantLock只能定义代码块,而Synchronized可以定义方法和代码块;
4、Synchronized是JVM的一个内部关键字,ReentrantLock是JDK1.5之后引入的一个API层面的互斥锁;
5、Synchronized实现自动的加锁、释放锁,ReentrantLock需要手动加锁和释放锁,中间可以暂停;
6、Synchronized由于引进了偏向锁和自旋锁,所以性能上和ReentrantLock差不多,但操作上方便很多,所以优先使用Synchronized。
71、那么请谈谈 AQS 框架是怎么回事儿?
(1)AQS是AbstractQueuedSynchronizer的缩写,它提供了一个FIFO队列,可以看成是一个实现同步锁的核心组件。
AQS是一个抽象类,主要通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取和释放的方法来提供自定义的同步组件。
(2)AQS的两种功能:独占锁和共享锁
(3)AQS的内部实现
AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的范文前驱和后继节点。每个Node其实是由线程封装,当线程争抢锁失败后会封装成Node加入到AQS队列中。
72、AQS 对资源的共享方式?
AQS定义两种资源共享方式
(1)Exclusive(独占)
只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
(2)Share(共享)
多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
73、如何让 Java 的线程彼此同步?
- synchronized
- volatile
- ReenreantLock
- 使用局部变量实现线程同步
74、你了解过哪些同步器?请分别介绍下。
(1)Semaphore同步器
特征:
经典的信号量,通过计数器控制对共享资源的访问
Semaphore(int count):创建拥有count个许可证的信号量
acquire()/acquire(int num) : 获取1/num个许可证
release/release(int num) : 释放1/num个许可证
(2)CountDownLatch同步器
特征:
必须发生指定数量的事件后才可以继续运行(比如赛跑比赛,裁判喊出3,2,1之后大家才同时跑)
CountDownLatch(int count):必须发生count个数量才可以打开锁存器
await:等待锁存器
countDown:触发事件
(3)CyclicBarrier同步器
特征:
适用于只有多个线程都到达预定点时才可以继续执行(比如斗地主,需要等齐三个人才开始)
CyclicBarrier(int num) :等待线程的数量
CyclicBarrier(int num, Runnable action) :等待线程的数量以及所有线程到达后的操作
await() : 到达临界点后暂停线程
(4)交换器(Exchanger)同步器
(5)Phaser同步器
75、Java 中的线程池是如何实现的
创建一个阻塞队列来容纳任务,在第一次执行任务时创建足够多的线程,并处理任务,之后每个工作线程自动从任务队列中获取线程,直到任务队列中任务为0为止,此时线程处于等待状态,一旦有工作任务加入任务队列中,即刻唤醒工作线程进行处理,实现线程的可复用性。
线程池一般包括四个基本组成部分:
(1)线程池管理器
用于创建线程池,销毁线程池,添加新任务。
(2)工作线程
线程池中线程,可循环执行任务,在没有任务时处于等待状态。
(3)任务队列
用于存放没有处理的任务,一种缓存机制。
(4)任务接口
每个任务必须实现的接口,供工作线程调度任务的执行,主要规定了任务的开始和收尾工作,和任务的状态。
76、创建线程池的几个核心构造参数
// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 线程池长期维持的最小线程数,即使线程处于Idle状态,也不会回收。
int maximumPoolSize, // 线程数的上限
long keepAliveTime, // 线程最大生命周期。
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增加,则需要一个容器来容纳这些任务,这就是任务队列。
ThreadFactory threadFactory, // 线程工厂。定义如何启动一个线程,可以设置线程名称,并且可以确认是否是后台线程等。
RejectedExecutionHandler handler // 拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。
)
77、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
线程池中的线程是在第一次提交任务submit时创建的
创建线程的方式有继承Thread和实现Runnable,重写run方法,start开始执行,wait等待,sleep休眠,shutdown停止。
(1)newSingleThreadExecutor:单线程池。
顾名思义就是一个池中只有一个线程在运行,该线程永不超时,而且由于是一个线程,当有多个任务需要处理时,会将它们放置到一个无界阻塞队列中逐个处理,它的实现代码如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable()));
}
它的使用方法也很简单,下面是简单的示例:
public static void main(String[] args) throws ExecutionException,InterruptedException {
// 创建单线程执行器
ExecutorService es = Executors.newSingleThreadExecutor();
// 执行一个任务
Future<String> future = es.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "";
}
});
// 获得任务执行后的返回值
System.out.println("返回值:" + future.get());
// 关闭执行器
es.shutdown();
}
(2)newCachedThreadPool:缓冲功能的线程。
建立了一个线程池,而且线程数量是没有限制的(当然,不能超过Integer的最大值),新增一个任务即有一个线程处理,或者复用之前空闲的线程,或者重亲启动一个线程,但是一旦一个线程在60秒内一直处于等待状态时(也就是一分钟无事可做),则会被终止,其源码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这里需要说明的是,任务队列使用了同步阻塞队列,这意味着向队列中加入一个元素,即可唤醒一个线程(新创建的线程或复用空闲线程来处理),这种队列已经没有队列深度的概念了。
(3)newFixedThreadPool:固定线程数量的线程池。
在初始化时已经决定了线程的最大数量,若任务添加的能力超出了线程的处理能力,则建立阻塞队列容纳多余的任务,其源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
上面返回的是一个ThreadPoolExecutor,它的corePoolSize和maximumPoolSize是相等的,也就是说,最大线程数量为nThreads。如果任务增长的速度非常快,超过了LinkedBlockingQuene的最大容量(Integer的最大值),那此时会如何处理呢?会按照ThreadPoolExecutor默认的拒绝策略(默认是DiscardPolicy,直接丢弃)来处理。
以上三种线程池执行器都是ThreadPoolExecutor的简化版,目的是帮助开发人员屏蔽过得线程细节,简化多线程开发。当需要运行异步任务时,可以直接通过Executors获得一个线程池,然后运行任务,不需要关注ThreadPoolExecutor的一系列参数时什么含义。当然,有时候这三个线程不能满足要求,此时则可以直接操作ThreadPoolExecutor来实现复杂的多线程计算。
newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool是线程池的简化版,而ThreadPoolExecutor则是旗舰版___简化版容易操作,需要了解的知识相对少些,方便使用,而旗舰版功能齐全,适用面广,难以驾驭。
78、volatile 关键字的作用
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
79、既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?
volatile修饰的变量在各个线程的工作内存中不存在一致性的问题(在各个线程工作的内存中,volatile修饰的变量也会存在不一致的情况,但是由于每次使用之前都会先刷新主存中的数据到工作内存,执行引擎看不到不一致的情况,因此可以认为不存在不一致的问题),但是java的运算并非原子性的操作,导致volatile在并发下并非是线程安全的。
80、ThreadLocal 是什么?有哪些使用场景?
ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
81、请谈谈 ThreadLocal 是怎么解决并发安全的?
在java程序中,常用的有两种机制来解决多线程并发问题,一种是sychronized方式,通过锁机制,一个线程执行时,让另一个线程等待,是以时间换空间的方式来让多线程串行执行。而另外一种方式就是ThreadLocal方式,通过创建线程局部变量,以空间换时间的方式来让多线程并行执行。两种方式各有优劣,适用于不同的场景,要根据不同的业务场景来进行选择。
在spring的源码中,就使用了ThreadLocal来管理连接,在很多开源项目中,都经常使用ThreadLocal来控制多线程并发问题,因为它足够的简单,我们不需要关心是否有线程安全问题,因为变量是每个线程所特有的。
82、很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?
ThreadLocal 变量解决了多线程环境下单个线程中变量的共享问题,使用名为ThreadLocalMap的哈希表进行维护(key为ThreadLocal变量名,value为ThreadLocal变量的值);
使用时需要注意以下几点:
- 线程之间的threadLocal变量是互不影响的,
- 使用private final static进行修饰,防止多实例时内存的泄露问题
- 线程池环境下使用后将threadLocal变量remove掉或设置成一个初始值
83、为什么代码会重排序?
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
- 在单线程环境下不能改变程序运行的结果;
- 存在数据依赖关系的不允许重排序
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。
84、什么是自旋
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
85、多线程中 synchronized 锁升级的原理是什么?
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
86、synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
主要区别如下:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
- 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
87、Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
88、jsp 和 servlet 有什么区别?
(1)servlet是服务器端的Java程序,它担当客户端和服务端的中间层。
(2)jsp全名为Java server pages,中文名叫Java服务器页面,其本质是一个简化的servlet设计。JSP是一种动态页面设计,它的主要目的是将表示逻辑从servlet中分离出来。
(3)JVM只能识别Java代码,不能识别JSP,JSP编译后变成了servlet,web容器将JSP的代码编译成JVM能够识别的Java类(servlet)。
(4)JSP有内置对象、servlet没有内置对象。
89、jsp 有哪些内置对象?作用分别是什么?
JSP九大内置对象:
- pageContext,页面上下文对象,相当于页面中所有功能的集合,通过它可以获取JSP页面的out、request、response、session、application对象。
- request
- response
- session
- application,应用程序对象,application实现了用户间数据的共享,可存放全局变量,它开始于服务器启动,知道服务器关闭。
- page,就是JSP本身。
- exception
- out,out用于在web浏览器内输出信息,并且管理应用服务器上的输出缓冲区,作用域page。
- config,取得服务器的配置信息。
90、forward 和 redirect 的区别?
- forward是直接请求转发;redirect是间接请求转发,又叫重定向。
- forward,客户端和浏览器执行一次请求;redirect,客户端和浏览器执行两次请求。
- forward,经典的MVC模式就是forward;redirect,用于避免用户的非正常访问。(例如用户非正常访问,servlet就可以将HTTP请求重定向到登录页面)。
- forward,地址不变;redirect,地址改变。
- forward常用方法:RequestDispatcher类的forward()方法;redirect常用方法:HttpServletRequest类的sendRedirect()方法。
91、说一下 jsp 的 4 种作用域?
application、session、request、page
92、session 和 cookie 有什么区别?
(1)存储位置不同
- cookie在客户端浏览器;
- session在服务器;
(2)存储容量不同
- cookie<=4K,一个站点最多保留20个cookie;
- session没有上线,出于对服务器的保护,session内不可存过多东西,并且要设置session删除机制;
(3)存储方式不同
- cookie只能保存ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据;
- session中能存储任何类型的数据,包括并不局限于String、integer、list、map等;
(4)隐私策略不同
- cookie对客户端是可见的,不安全;
- session存储在服务器上,安全;
(5)有效期不同
- 开发可以通过设置cookie的属性,达到使cookie长期有效的效果;
- session依赖于名为JESSIONID的cookie,而cookie JSESSIONID的过期时间默认为-1,只需关闭窗口该session就会失效,因而session达不到长期有效的效果;
(6)跨域支持上不同
- cookie支持跨域;
- session不支持跨域;
93、如果客户端禁止 cookie 能实现 session 还能用吗?
一般默认情况下,在会话中,服务器存储 session 的 sessionid 是通过 cookie 存到浏览器里。
如果浏览器禁用了 cookie,浏览器请求服务器无法携带 sessionid,服务器无法识别请求中的用户身份,session失效。
但是可以通过其他方法在禁用 cookie 的情况下,可以继续使用session。
- 通过url重写,把 sessionid 作为参数追加的原 url 中,后续的浏览器与服务器交互中携带 sessionid 参数。
- 服务器的返回数据中包含 sessionid,浏览器发送请求时,携带 sessionid 参数。
- 通过 Http 协议其他 header 字段,服务器每次返回时设置该 header 字段信息,浏览器中 js 读取该 header 字段,请求服务器时,js设置携带该 header 字段。
94、什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
95、cookie、session、token
1、session机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7wYFJxle-1682099301770)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097776507.png)]
session是服务端存储的一个对象,主要用来存储所有访问过该服务端的客户端的用户信息(也可以存储其他信息),从而实现保持用户会话状态。但是服务器重启时,内存会被销毁,存储的用户信息也就消失了。
不同的用户访问服务端的时候会在session对象中存储键值对,“键”用来存储开启这个用户信息的“钥匙”,在登录成功后,“钥匙”通过cookie返回给客户端,客户端存储为sessionId记录在cookie中。当客户端再次访问时,会默认携带cookie中的sessionId来实现会话机制。
(1)session是基于cookie的。
- cookie的数据4k左右;
- cookie存储数据的格式:字符串key=value
- cookie存储有效期:可以自行通过expires进行具体的日期设置,如果没设置,默认是关闭浏览器时失效。
- cookie有效范围:当前域名下有效。所以session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上(前后端项目协议、域名、端口号都一致,即在一个项目下)
(2)session持久化
用于解决重启服务器后session消失的问题。在数据库中存储session,而不是存储在内存中。通过包:express-mysql-session。
当客户端存储的cookie失效后,服务端的session不会立即销毁,会有一个延时,服务端会定期清理无效session,不会造成无效数据占用存储空间的问题。
2、token机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eKYI1R14-1682099301771)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097901468.png)]
适用于前后端分离的项目(前后端代码运行在不同的服务器下)
请求登录时,token和sessionid原理相同,是对key和key对应的用户信息进行加密后的加密字符,登录成功后,会在响应主体中将{token:“字符串”}返回给客户端。
客户端通过cookie都可以进行存储。再次请求时不会默认携带,需要在请求拦截器位置给请求头中添加认证字段Authorization携带token信息,服务器就可以通过token信息查找用户登录状态。
96、说一下 session 的工作原理?
当客户端登录完成后,会在服务端产生一个session,此时服务端会将sessionid返回给客户端浏览器。客户端将sessionid储存在浏览器的cookie中,当用户再次登录时,会获得对应的sessionid,然后将sessionid发送到服务端请求登录,服务端在内存中找到对应的sessionid,完成登录,如果找不到,返回登录页面。
97、http 响应码 301 和 302 代表的是什么?有什么区别?
- 301和302状态码都表示重定向,当浏览器拿到服务器返回的这个状态码后悔自动跳转到一个新的URL地址。
- 301代表永久性重定向,旧地址被永久移除,客户端向新地址发送请求。
- 302代表暂时性重定向,旧地址还在,客户端继续向旧地址发送请求。
- 303代表暂时性重定向,重定向到新地址时,必须使用GET方法请求新地址。
- 307代表暂时性重定向,与302的区别在于307不允许从POST改为GET。
- 307代表永久性重定向,与301的区别在于308不允许从POST改为GET。
98、简述 tcp 和 udp的区别?
- TCP是传输控制协议,UDP是用户数据表协议;
- TCP长连接,UDP无连接;
- UDP程序结构较简单,只需发送,无须接收;
- TCP可靠,保证数据正确性、顺序性;UDP不可靠,可能丢数据;
- TCP适用于少量数据,UDP适用于大量数据传输;
- TCP速度慢,UDP速度快;
99、tcp 为什么要三次握手,两次不行吗?为什么?
因为客户端和服务端都要确认连接,①客户端请求连接服务端;②针对客户端的请求确认应答,并请求建立连接;③针对服务端的请求确认应答,建立连接;
两次无法确保A能收到B的数据;
100、OSI 的七层模型都有哪些?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7lzGD41A-1682099301772)(C:\Users\CHENQI~1\AppData\Local\Temp\1682097923082.png)]
101、get 和 post 请求有哪些区别?
- get请求参数是连接在url后面的,而post请求参数是存放在requestbody内的;
- get请求因为浏览器对url长度有限制,所以参数个数有限制,而post请求参数个数没有限制;
- 因为get请求参数暴露在url上,所以安全方面post比get更加安全;
- get请求只能进行url编码,而post请求可以支持多种编码方式;
- get请求参数会保存在浏览器历史记录内,post请求并不会;
- get请求浏览器会主动cache,post并不会,除非主动设置;
- get请求产生1个tcp数据包,post请求产生2个tcp数据包;
- 在浏览器进行回退操作时,get请求是无害的,而post请求则会重新请求一次;
- 浏览器在发送get请求时会将header和data一起发送给服务器,服务器返回200状态码,而在发送post请求时,会先将header发送给服务器,服务器返回100,之后再将data发送给服务器,服务器返回200 OK;
102、什么是 XSS 攻击,如何避免?
xss(Cross Site Scripting),即跨站脚本攻击,是一种常见于web应用程序中的计算机安全漏洞。指的是在用户浏览器上,在渲染DOM树的时候,执行了不可预期的JS脚本,从而发生了安全问题。
XSS就是通过在用户端注入恶意的可运行脚本,若服务端对用户的输入不进行处理,直接将用户的输入输出到浏览器,然后浏览器将会执行用户注入的脚本。 所以XSS攻击的核心就是浏览器渲染DOM的时候将文本信息解析成JS脚本从而引发JS脚本注入,那么XSS攻击的防御手段就是基于浏览器渲染这一步去做防御。只要我们使用HTML编码将浏览器需要渲染的信息编码后,浏览器在渲染DOM元素的时候,会自动解码需要渲染的信息,将上述信息解析成字符串而不是JS脚本,这就是我们防御XSS攻击的核心想法。
预防:
1、获取用户的输入,不用innerHtml,用innerText.
2、对用户的输入进行过滤,如对& < > " ' /等进行转义;
103、什么是 CSRF 攻击,如何避免?
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
1、攻击细节
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
例子
假如一家银行用以运行转账操作的URL地址如下:http: //www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成信息的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。
2、防御措施
检查Referer字段
HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。
3、添加校验token
由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。
104、如何实现跨域?说一下 JSONP 实现原理?
1、不知道jsonp是什么的同学可以先去了解下
2、最流行的跨域方案cors
cors是目前主流的跨域解决方案,跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
3、最方便的跨域方案Nginx
nginx是一款极其强大的web服务器,其优点就是轻量级、启动快、高并发。
现在的新项目中nginx几乎是首选,我们用node或者java开发的服务通常都需要经过nginx的反向代理。
反向代理的原理很简单,即所有客户端的请求都必须先经过nginx的处理,nginx作为代理服务器再讲请求转发给node或者java服务,这样就规避了同源策略。
105、websocket应用的是哪个协议
WebSocket是一个允许Web应用程序(通常指浏览器)与服务器进行双向通信的协议。HTML5的WebSocket API主要是为浏览器端提供了一个基于TCP协议实现全双工通信的方法。
WebSocket优势: 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,双方可以在任意时刻,相互推送信息。同时,服务器与客户端之间交换的头信息很小。
106、说一下 tcp 粘包是怎么产生的?
发送方需要等缓冲区满才能发送出去,造成粘包;
接收方不及时接收缓冲区的包,造成粘包;
107、请列举出在 JDK 中几个常用的设计模式?
1、单例模式
作用:保证类只有一个实例。
JDK中体现:Runtime类。
2、静态工厂模式
作用:代替构造函数创建对象,方法名比构造函数清晰。
JDK中体现:Integer.valueOf、Class.forName
3、抽象工厂
作用:创建某一种类的对象。
JDK中体现:Java.sql包。
4、原型模式
clone();
原型模式的本质是拷贝原型来创建新的对象,拷贝是比new更快的创建对象的方法,当需要大批量创建新对象而且都是同一个类的对象的时候考虑使用原型模式。
一般的克隆只是浅拷贝(对象的hash值不一样,但是对象里面的成员变量的hash值是一样的)。
有些场景需要深拷贝,这时我们就要重写clone方法,以ArrayList为例:
5、适配器模式
作用:使不兼容的接口相容。
JDK中体现:InputStream、OutputStream。
6、装饰器模式
作用:为类添加新的功能,防止类继承带来的类爆炸。
JDK中体现:io类、Collections、List。
7、外观模式
作用:封装一组交互类,一直对外提供接口。
JDK中体现:logging包。
8、享元模式
作用:共享对象、节省内存。
JDK中体现:Integer.valueOf、String常量池。
9、代理模式
作用:
(1)透明调用被代理对象,无须知道复杂实现细节;
(2)增加被代理类的功能;
JDK中体现:动态代理。
10、迭代器模式
作用:将集合的迭代和集合本身分离。
JDK中体现:Iterator
11、命令模式
作用:封装操作,使接口一致。
JDK中体现:Runable、Callable、ThreadPoolExecutor。
108、什么是设计模式?你是否在你的代码里面使用过任何设计模式?
1、什么是设计模式?
设计模式是解决软件开发某些特定问题而提出的一些解决方案,也可以理解为解决问题的一些固定思路。
通过设计模式可以帮助我们增强代码的可复用性、可扩展性、灵活性。
我们使用设计模式的最终目的是实现代码的高内聚、低耦合。
2、设计模式的七大原则
- 单一职责原则
- 接口隔离原则
- 依赖倒转原则
- 里式替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
3、你是否在你的代码里面使用过任何设计模式?
(1)单例模式
JDK种的runtime,Spring种的singeton。
(2)简单工厂模式
Spring的BeanFactory,根据传入一个唯一标识来获得bean对象。
(3)原型模式
clone()
(4)代理模式
Spring的AOP中,Spring实现AOP功能的原理就是代理模式,①JDK动态代理。②CGLIB动态代理,使用Advice(通知)对类进行方法级别的切面增强。
(5)装饰器模式
为类添加新的功能,防止类爆炸;
IO流、数据源包装,Spring中用到的装饰器模式表现在Wrapper。
109、Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
- 保证程序只有一个对象的实例,叫做单例模式;
- 内部类的方式实现单例模式,是线程安全的;
- 双重验证方式实现单例模式也是线程安全的;
110、在 Java 中,什么叫观察者设计模式(observer design pattern)?
1、观察者模式是一种一对多的依赖关系,让多个观察者同时监听某一主题对象。当这个主题对象发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
2、JAVA提供的对观察者模式的支持
在JAVA语言的java.util库里面,提供了一个Observable类以及一个Observer接口,构成JAVA语言对观察者模式的支持。
(1)Observer接口
这个接口只定义了一个方法,即update()方法,当被观察者对象的状态发生变化时,被观察者对象的notifyObservers()方法就会调用这一方法。
public interface Observer {
void update(Observable o, Object arg);
}
(2)Observable类
被观察者类都是java.util.Observable类的子类。java.util.Observable提供公开的方法支持观察者对象,这些方法中有两个对Observable的子类非常重要:一个是setChanged(),另一个是notifyObservers()。第一方法setChanged()被调用之后会设置一个内部标记变量,代表被观察者对象的状态发生了变化。第二个是notifyObservers(),这个方法被调用时,会调用所有登记过的观察者对象的update()方法,使这些观察者对象可以更新自己。
111、使用工厂模式最主要的好处是什么?在哪里使用?
1、工厂模式好处
- 良好的封装性、代码结构清晰;
- 扩展性好,如果想增加一个产品,只需扩展一个工厂类即可;
- 典型的解耦框架;
2、在哪里使用?
- 需要生成对象的地方;
- 不同数据库的访问;
112、请解释自动装配模式的区别?
有五种自动装配的方式,可以用来指导 Spring 容器用自动装配方式来进行依赖注入。
1、no
默认的方式是不进行自动装配,通过显式设置 ref 属性来进行装配。第 402 页 共 485 页
2、byName
通过参数名 自动装配,Spring 容器在配置文件中发现 bean
的 autowire 属性被设置成 byname,之后容器试图匹配、装配和该 bean 的属
性具有相同名字的 bean。
3、byType:
通过参数类型自动装配,Spring 容器在配置文件中发现 bean
的 autowire 属性被设置成 byType,之后容器试图匹配、装配和该 bean 的属
性具有相同类型的 bean。如果有多个 bean 符合条件,则抛出错误。
4、constructor
这个方式类似于 byType, 但是要提供给构造器参数,如
果没有确定的带参数的构造器参数类型,将会抛出异常。
5、autodetect
首先尝试使用 constructor 来自动装配,如果无法工作,
则使用 byType 方式。
113、举一个用 Java 实现的装饰模式(decorator design pattern)?它是作用于对象层次还是类层次?
在Java IO中运用了装饰器模式,inputStream作为抽象类,其下有几个实现类,表示从不同的数据源输入:
- byteArrayInputStream
- fileInputStream
- StringBufferInputStream
- PipedInputStream,从管道产生输入;
- SequenceInputStream,可将其他流收集合并到一个流内;
FilterInputStream作为装饰器在JDK中是一个普通类,其下面有多个具体装饰器比如BufferedInputStream、DataInputStream等。
FilterInputStream内部封装了基础构件:
protected volatile InputStream in;
而BufferedInputStream在调用其read()读取数据时会委托基础构件来进行更底层的操作,而它自己所起的装饰作用就是缓冲,在源码中可以很清楚的看到这一切。
114、什么是 Spring 框架?Spring 框架有哪些主要模块?
Spring是一个控制反转和面向切面的容器框架。
Spring有七大功能模块:
1、Core
Core模块是Spring的核心类库,Core实现了IOC功能。
2、AOP
Apring AOP模块是Spring的AOP库,提供了AOP(拦截器)机制,并提供常见的拦截器,供用户自定义和配置。
3、orm
提供对常用ORM框架的管理和支持,hibernate、mybatis等。
4、Dao
Spring提供对JDBC的支持,对JDBC进行封装。
5、Web
对Struts2的支持。
6、Context
Context模块提供框架式的Bean的访问方式,其它程序可以通过Context访问Spring的Bean资源,相当于资源注入。
7、MVC
MVC模块为spring提供了一套轻量级的MVC实现,即Spring MVC。
115、使用 Spring 框架能带来哪些好处?
1、轻量级框架、容器
Spring是一个容器,管理对象的生命周期和配置。基于一个可配置原型prototype,你的bean可以使单利的,也可以每次需要时都生成一个新的实例。
2、控制反转IOC
Spring通过控制反转实现松耦合。
3、支持AOP
Spring提供对AOP的支持,它允许将一些通用任务,如安全、事务、日志等进行集中式处理,从而提高了程序的复用性。
4、轻量级框架
5、方便测试
Spring提供Junit4的支持,可以通过注解方便测试spring程序。
6、对Java中很多API进行了封装
7、方便集成各种优秀框架
如Struts、hibernate、mybstis。
8、支持声明式事务处理
只需通过配置就可以完成对事务的管理,而无须手动编程。
116、Spring IOC、AOP举例说明
1、IOC理论的背景
我们都知道,在采用面向对象方法设计的软件系统中,它的底层实现都是由N个对象组成的,所有的对象通过彼此的合作,最终实现系统的业务逻辑。
如果我们打开机械式手表的后盖,就会看到与上面类似的情形,各个齿轮分别带动时针、分针和秒针顺时针旋转,从而在表盘上产生正确的时间。图1中描述的就是这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项任务。我们可以看到,在这样的齿轮组中,如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转。
齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。
耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家Michael Mattson提出了IOC理论,用来实现对象之间的“解耦”,目前这个理论已经被成功地应用到实践当中,很多的J2EE项目均采用了IOC框架产品Spring。
2、什么是控制反转
IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”,还有些书籍翻译成为“控制反向”或者“控制倒置”。
1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IOC 这个概念。对于面向对象设计及编程的基本思想,前面我们已经讲了很多了,不再赘述,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kow9sDii-1682099301774)(]
大家看到了吧,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
我们再来做个试验:把上图中间的IOC容器拿掉,然后再来看看这套系统(拿掉IoC容器后的系统):
我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,A、B、C、D这4个对象之间已经没有了耦合关系,彼此毫无联系,这样的话,当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现IOC容器,对于系统开发而言,这将是一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系!
我们再来看看,控制反转(IOC)到底为什么要起这么个名字?我们来对比一下:
软件系统在没有引入IOC容器之前,如图1所示,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
软件系统在引入IOC容器之后,这种情形就完全改变了,如图3所示,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。
3、IOC的别名:依赖注入(DI)
2004年,Martin Fowler探讨了同一个问题,既然IOC是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现IOC的方法:注入。所谓依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。
所以,依赖注入(DI)和控制反转(IOC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。
我们举一个生活中的例子,来帮助理解依赖注入的过程。大家对USB接口和USB设备应该都很熟悉吧,USB为我们使用电脑提供了很大的方便,现在有很多的外部设备都支持USB接口。
现在,我们利用电脑主机和USB接口来实现一个任务:从外部USB设备读取一个文件。
电脑主机读取文件的时候,它一点也不会关心USB接口上连接的是什么外部设备,而且它确实也无须知道。它的任务就是读取USB接口,挂接的外部设备只要符合USB接口标准即可。所以,如果我给电脑主机连接上一个U盘,那么主机就从U盘上读取文件;如果我给电脑主机连接上一个外置硬盘,那么电脑主机就从外置硬盘上读取文件。挂接外部设备的权力由我作主,即控制权归我,至于USB接口挂接的是什么设备,电脑主机是决定不了,它只能被动的接受。电脑主机需要外部设备的时候,根本不用它告诉我,我就会主动帮它挂上它想要的外部设备,你看我的服务是多么的到位。这就是我们生活中常见的一个依赖注入的例子。在这个过程中,我就起到了IOC容器的作用。
通过这个例子,依赖注入的思路已经非常清楚:当电脑主机读取文件的时候,我就把它所要依赖的外部设备,帮他挂接上。整个外部设备注入的过程和一个被依赖的对象在系统运行时被注入另外一个对象内部的过程完全一样。
我们把依赖注入应用到软件系统中,再来描述一下这个过程:
对象A依赖于对象B,当对象 A需要用到对象B的时候,IOC容器就会立即创建一个对象B送给对象A。IOC容器就是一个对象制造工厂,你需要什么,它会给你送去,你直接使用就行了,而再也不用去关心你所用的东西是如何制成的,也不用关心最后是怎么被销毁的,这一切全部由IOC容器包办。
在传统的实现中,由程序内部代码来控制组件之间的关系。我们经常使用new关键字来实现两个组件之间关系的组合,这种实现方式会造成组件之间耦合。IOC很好地解决了该问题,它将实现组件间关系从程序内部提到外部容器,也就是说由容器在运行期将组件间的某种依赖关系动态注入组件中。
4、IOC为我们带来了什么好处
我们还是从USB的例子说起,使用USB外部设备比使用内置硬盘,到底带来什么好处?
第一、USB设备作为电脑主机的外部设备,在插入主机之前,与电脑主机没有任何的关系,只有被我们连接在一起之后,两者才发生联系,具有相关性。所以,无论两者中的任何一方出现什么的问题,都不会影响另一方的运行。这种特性体现在软件工程中,就是可维护性比较好,非常便于进行单元测试,便于调试程序和诊断故障。代码中的每一个Class都可以单独测试,彼此之间互不影响,只要保证自身的功能无误即可,这就是组件之间低耦合或者无耦合带来的好处。
第二、USB设备和电脑主机的之间无关性,还带来了另外一个好处,生产USB设备的厂商和生产电脑主机的厂商完全可以是互不相干的人,各干各事,他们之间唯一需要遵守的就是USB接口标准。这种特性体现在软件开发过程中,好处可是太大了。每个开发团队的成员都只需要关心实现自身的业务逻辑,完全不用去关心其它的人工作进展,因为你的任务跟别人没有任何关系,你的任务可以单独测试,你的任务也不用依赖于别人的组件,再也不用扯不清责任了。所以,在一个大中型项目中,团队成员分工明确、责任明晰,很容易将一个大的任务划分为细小的任务,开发效率和产品质量必将得到大幅度的提高。
第三、同一个USB外部设备可以插接到任何支持USB的设备,可以插接到电脑主机,也可以插接到DV机,USB外部设备可以被反复利用。在软件工程中,这种特性就是可复用性好,我们可以把具有普遍性的常用组件独立出来,反复利用到项目中的其它部分,或者是其它项目,当然这也是面向对象的基本特征。显然,IOC不仅更好地贯彻了这个原则,提高了模块的可复用性。符合接口标准的实现,都可以插接到支持此标准的模块中。
第四、同USB外部设备一样,模块具有热插拔特性。IOC生成对象的方式转为外置方式,也就是把对象生成放在配置文件里进行定义,这样,当我们更换一个实现子类将会变得很简单,只要修改配置文件就可以了,完全具有热插拨的特性。
以上几点好处,难道还不足以打动我们,让我们在项目开发过程中使用IOC框架吗?
5、IOC容器的技术剖析
IOC中最基本的技术就是“反射(Reflection)”编程,目前.Net C#、Java和PHP5等语言均支持,其中PHP5的技术书籍中,有时候也被翻译成“映射”。有关反射的概念和用法,大家应该都很清楚,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象。这种编程方式可以让对象在生成时才决定到底是哪一种对象。反射的应用是很广泛的,很多的成熟的框架,比如象Java中的Hibernate、Spring框架,.Net中 NHibernate、Spring.Net框架都是把“反射”做为最基本的技术手段。
反射技术其实很早就出现了,但一直被忽略,没有被进一步的利用。当时的反射编程方式相对于正常的对象生成方式要慢至少得10倍。现在的反射技术经过改良优化,已经非常成熟,反射方式生成对象和通常对象生成方式,速度已经相差不大了,大约为1-2倍的差距。
我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言的的反射编程,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
6、IOC容器的一些产品
Sun ONE技术体系下的IOC容器有:轻量级的有Spring、Guice、Pico Container、Avalon、HiveMind;重量级的有EJB;不轻不重的有JBoss,Jdon等等。Spring框架作为Java开发中SSH(Struts、Spring、Hibernate)三剑客之一,大中小项目中都有使用,非常成熟,应用广泛,EJB在关键性的工业级项目中也被使用,比如某些电信业务。
.Net技术体系下的IOC容器有:Spring.Net、Castle等等。Spring.Net是从Java的Spring移植过来的IOC容器,Castle的IOC容器就是Windsor部分。它们均是轻量级的框架,比较成熟,其中Spring.Net已经被逐渐应用于各种项目中。
7、使用IOC框架应该注意什么
使用IOC框架产品能够给我们的开发过程带来很大的好处,但是也要充分认识引入IOC框架的缺点,做到心中有数,杜绝滥用框架。
(1)软件系统中由于引入了第三方IOC容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用IOC框架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同样的知识体系。
(2)由于IOC容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
(3)、具体到IOC框架产品(比如:Spring)来讲,需要进行大量的配制工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
(4)IOC框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。
我们大体可以得出这样的结论:一些工作量不大的项目或者产品,不太适合使用IOC框架产品。另外,如果团队成员的知识能力欠缺,对于IOC框架产品缺乏深入的理解,也不要贸然引入。最后,特别强调运行效率的项目或者产品,也不太适合引入IOC框架产品,象WEB2.0网站就是这种情况。
117、什么是控制反转(IOC)?什么是依赖注入?
借助Spring实现具有依赖关系的对象之间的解耦。
对象A运行需要对象B,由主动创建变为IOC容器注入,这便是控制反转。
获得依赖对象的过程被反转了,获取依赖对象的过程由自身创建变为由IOC容器注入,这便是依赖注入。
118、BeanFactory 和 ApplicationContext 有什么区别?
1、BeanFactory是Spring的最底层接口,包含bean的定义,管理bean的加载,实例化,控制bean的生命周期,特点是每次获取对象时才会创建对象。
ApplicationContext是BeanFactory的子接口,拥有BeanFactory的全部功能,并且扩展了很多高级特性,每次容器启动时就会创建所有的对象。
- ApplicationContext的额外功能:
- 继承MessageSource,支持国际化;
- 统一的资源文件访问方式;
- 提供在监听器中注册bean;
- 同时加载过个配置文件;
- 载入多个(有继承关系)上下文,使得每个上下文都专注于一个特定的层次,比如应用的web层;
2、BeanFactory通常以编程的方式被创建,ApplicationContext可以以声明的方式创建,如使用ContextLoader。
3、BeanFactory 和 ApplicationContext都支持BeanPostProcessor,BeanFactoryPostProcessor,但BeanFactory需要手动注册,ApplicationContext则是自动注册。
119、什么是 JavaConfig?
JavaConfig是Spring3.0新增的概念,就是以注解的形式取代Spring中繁琐的xml文件。
JavaConfig结合了xml的解耦和java编译时检查的优点。
- @Configuration,表示这个类是配置类;
- @ComponentScan,相当于xml的<context:componentScan basepackage=>;
- @Bean,相当于xml的;
- @EnableWebMvc,相当于xml的<mvc:annotation-driven>;
- @ImportResource,相当于xml的;
- @PropertySource,用于读取properties配置文件;
- @Profile,一般用于多环境配置,激活时可用@ActiveProfile("dev")注解;
120、什么是 ORM 框架?
ORM(Object-relational mapping),对象关系映射。
是为了解决面向对象与关系型数据库存在的不匹配问题。
ORM框架的优点:
- 开发效率更高
- 数据访问更抽象、轻便
- 支持面向对象封装
121、Spring 有几种配置方式?
1、xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="jackma" class="com.tyq.dto.User">
<property name="name" value="jackma" />
<property name="age" value="55" />
<property name="dog" ref="jm" />
</bean>
<bean id="jm" class="com.tyq.dto.Dog">
<property name="name" value="jack" />
<property name="breed" value="金毛" />
<property name="age" value="2" />
</bean>
</beans>
2、基于注解的方式
项目越来越大,基于xml配置太麻烦,Spring 2.x时代提供了声明bean的注解。
(1)Bean的定义
@Component、@Controller、@Service、@Repository。
(2)Bean的注入
@Autowire
3、基于Java的方式
Spring 3.x以后,可以通过Java代码装配Bean。
@Configuration
public class DemoConfig {
@Bean
public User zs(){
return new User();
}
@Bean
public Dog dog(){
return new Dog();
}
@Bean //两个狗
public Dog haqi(){
return new Dog();
}
}
@Component("zs")
public class User {
private String name;
private int age;
private Dog dog;
//get,set方法略
}
原来就是配置类啊,通过@Bean、@Component、getBean方式进行Bean的注册和发现。
122、请解释 Spring Bean 的生命周期?
- 通过构造器或工厂方法创建bean实例;
- 为bean的属性赋值;
- 调用bean的初始化方法;
- 使用bean;
- 当容器关闭时,调用bean的销毁方法;
123、Spring Bean 的作用域之间有什么区别? Spring容器中的bean可以分为5个范围:
- singleton:这种bean范围是默认的,这种范围确保不管接受多少请求,每个容器中只哟一个bean的实例,单例模式;
- prototype:为每一个bean提供一个实例;
- request:在请求bean范围内为每一个来自客户端的网络请求创建一个实例,在请求完毕后,bean会失效并被垃圾回收器回收;
- session:为每个session创建一个实例,session过期后,bean会随之消失;
- global-session:global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet公用全局的存储变量的话,那么全局变量需要存储在global-session中。
124、如何在 Spring Boot 中禁用 Actuator 端点安全性?
默认情况下,所有敏感的HTTP端点都是安全的,只有具有Actuator角色的用户才能访问它们。安全性是使用标准的HTTPServletRequest.isUserInRole方法实施的。我们可以使用management.security.enable = false来禁用安全性。只有在执行机构端点在防火墙后访问时,才建议禁用安全性。
125、什么是 Spring inner beans?
在Spring框架中,无论何时bean被使用时,当仅被调用一个属性。可以将这个bean声明为内部bean。内部bean可以用setter注入“属性”和构造方法注入“构造参数”的方式来实现。比如,在我们的应用程序中,一个Customer类引用了一个Person类,我们要做的是创建一个Person实例,然后再Customer内部使用。
package com;
public class Customer {
private Person person;
}
class Person{
private int id;
private String name;
private int age;
}
<bean id="CustomerBean" class="com.Customer">
<property name="person">
<bean class="com.person">
<property name="id" value=1 />
<property name="name" value="素小暖" />
<property name="age" value=18 />
</bean>
</property>
</bean>
126、Spring 框架中的单例 Beans 是线程安全的么?
Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态,所以在某种程度上说Spring的单例bean时线程安全的。如果你的bean有多种状态的话,比如view model,就需要自行保证线程安全啦。
最浅显的解决办法就是将多态bean的作用域由singleton变更为prototype。
127、请解释 Spring Bean 的自动装配?
Spring支持IOC,自动装配不用类实例化,直接从bean容器中取。
1、配置在xml中
2、@Autowired自动装配
128、如何开启基于注解的自动装配?
要使用 @Autowired
,需要注册 AutowiredAnnotationBeanPostProcessor
,可以有以下两种方式来实现:
引入配置文件中的<bean>
下引入 <context:annotation-config>
<beans>
<context:annotation-config />
</beans>
在bean配置文件中直接引入AutowiredAnnotationBeanPostProcessor
<beans>
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
</beans>
129、什么是 Spring Batch?
1、什么是spring batch?
spring batch是一个轻量级的、完善的批处理框架,它主要的目的在于帮助企业建立健壮、高效的批处理应用。
spring batch是Spring的一个子项目,它使用java语言并基于spring框架作为基础开发,使得已经使用Spring框架的开发者或者是企业可以更加容易访问和利用企业服务。
spring batch提供了大量可重用的组件,包括了日志、追踪、事务、任务作业统计、任务重启、跳过、重复、资源管理。
对大数据量和高性能的批处理任务,spring batch同样提供了高级功能和特性来支持。
例如:分区功能、远程功能。
总的来说,spring batch可以支持简单的、复杂的和大数据量的批处理作业。
2、spring batch业务场景
周期性的提交批处理
把一个任务并行处理
消息驱动应用分级处理
大规模并行批处理
手工或调度使任务失败之后重新启动
有依赖步骤的顺序执行(使用工作流驱动扩展)
处理时跳过部分记录
成批事务:为小批量的或有的存储过程/脚本的场景使用
130、spring mvc 和 struts 的区别是什么?
1、拦截机制的不同
Struts2是类级别的拦截,每次请求就会创建一个Action,和Spring整合时Struts2的ActionBean注入作用域是原型模式prototype,然后通过setter,getter吧request数据注入到属性。Struts2中,一个Action对应一个request,response上下文,在接收参数时,可以通过属性接收,这说明属性参数是让多个方法共享的。Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了,只能设计为多例。
SpringMVC是方法级别的拦截,一个方法对应一个Request上下文,所以方法直接基本上是独立的,独享request,response数据。而每个方法同时又何一个url对应,参数的传递是直接注入到方法中的,是方法所独有的。处理结果通过ModeMap返回给框架。在Spring整合时,SpringMVC的Controller Bean默认单例模式Singleton,所以默认对所有的请求,只会创建一个Controller,有应为没有共享的属性,所以是线程安全的,如果要改变默认的作用域,需要添加@Scope注解修改。
Struts2有自己的拦截Interceptor机制,SpringMVC这是用的是独立的Aop方式,这样导致Struts2的配置文件量还是比SpringMVC大。
2、底层框架的不同
Struts2采用Filter(StrutsPrepareAndExecuteFilter)实现,SpringMVC(DispatcherServlet)则采用Servlet实现。Filter在容器启动之后即初始化;服务停止以后坠毁,晚于Servlet。Servlet在是在调用时初始化,先于Filter调用,服务停止后销毁。
3、性能方面
Struts2是类级别的拦截,每次请求对应实例一个新的Action,需要加载所有的属性值注入,SpringMVC实现了零配置,由于SpringMVC基于方法的拦截,有加载一次单例模式bean注入。所以,SpringMVC开发效率和性能高于Struts2。
4、配置方面
spring MVC和Spring是无缝的。从这个项目的管理和安全上也比Struts2高。
131、请举例解释@Required 注解?
@Required注解应用于bean属性的setter方法,它表明影响的bean属性在配置时必须放在XML配置文件中。
十九、请举例说明@Qualifier 注解?
如果在xml中定义了一种类型的多个bean,同时在java注解中又想把其中一个bean对象作为属性,那么此时可以使用@Qualifier加@Autowired来达到这一目的,若不加@Qualifier这个注解,在运行时会出现“ No qualifying bean of type [com.tutorialspoint.Student] is defined: expected single matching bean but found 2: student1,student2”这个异常。
133、项目中是如何实现权限验证的,权限验证需要几张表
通过了解,现在最普遍的权限管理模型就是RBAC(Role-Based Access Control)。
1、权限控制分类
菜单功能
url控制(控制访问不同的控制器)
2、RBAC的优缺点
(1)优点
简化了用户和权限的关系
易扩展、易维护
(2)缺点
RBAC模型没有提供操作顺序的控制机制,这一缺陷使得RBAC模型很难适应哪些对操作次序有严格要求的系统。
3、RBAC支持的安全原则
(1)最小权限原则
RBAC可以将角色配置成其完成任务所需的最小权限集合。
(2)责任分离原则
可以通过调用相互独立互斥的角色来共同完成敏感的任务,例如要求一个记账员和财务管理员共同参与统一过账操作。
(3)数据抽象原则
可以通过权限的抽象来体现,例如财务操作用借款、存款等抽象权限,而不是使用典型的读写权限。
4、远古时代的权限控制
当时还没有RBAC,也没有这个概念,就是一堆程序员在那鼓捣,觉得登录这块该做点什么。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-855b2REq-1682099301775)(C:\Users\CHENQI~1\AppData\Local\Temp\1682096515131.png)]
1、新建一个用户,对这个用户进行赋予权限。2、但是一旦用户多了,权限复杂了,这工作量也是蛮大的。
5、RBAC
RBAC 1.0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A4TRf94L-1682099301776)(C:\Users\CHENQI~1\AppData\Local\Temp\1682096524737.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ed6PwWzv-1682099301778)(C:\Users\CHENQI~1\AppData\Local\Temp\1682096541231.png)]
直接上图,一目了然,当程序不是很复杂的时候,RBAC就是这样设计的,我们公司的权限验证模块就是这样设计的。
简简单单,五张表,解
RBAC 2.0
基于RBAC 1.0模型的基础上,进行了角色的访问控制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZDnxiDeh-1682099301779)(C:\Users\CHENQI~1\AppData\Local\Temp\1682096616428.png)]
RBAC2中的一个基本限制是互斥角色的限制,互斥角色是指各自权限可以互相制约的两个角色。对于这类角色一个用户在某一次活动中只能被分配其中的一个角色,不能同时获得两个角色的使用权。
该模型有以下几种约束
- 互斥角色 :同一用户只能分配到一组互斥角色集合中至多一个角色,支持责任分离的原则。互斥角色是指各自权限互相制约的两个角色。对于这类角色一个用户在某一次活动中只能被分配其中的一个角色,不能同时获得两个角色的使用权。常举的例子:在审计活动中,一个角色不能同时被指派给会计角色和审计员角色。
- 基数约束 :一个角色被分配的用户数量受限;一个用户可拥有的角色数目受限;同样一个角色对应的访问权限数目也应受限,以控制高级权限在系统中的分配。
- 先决条件角色 :可以分配角色给用户仅当该用户已经是另一角色的成员;对应的可以分配访问权限给角色,仅当该角色已经拥有另一种访问权限。指要想获得较高的权限,要首先拥有低一级的权限。
- 运行时互斥 :例如,允许一个用户具有两个角色的成员资格,但在运行中不可同时激活这两个角色。
6、rbac的实现理论分析
进入登录页面;
拿到通过post传过来的用户名和密码;
使用orm进行过滤查找;
如果能找到值,则说明登录成功:登录成功后调用rbac初始化函数,初始化函数的主要功能是获取用户的权限和菜单保存到session中,并跳转客户列表页面;如果失败,页面进行友好提示;
7、url权限控制关键代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEwRlkQ9-1682099301780)(C:\Users\CHENQI~1\AppData\Local\Temp\1682096738016.png)]
134、谈谈controller,接口调用的路径问题
1、Spring MVC如何匹配请求路径
@RequestMapping是用来映射请求的,比如get请求、post请求、或者REST风格与非REST风格的。该注解可以用在类上或方法上,如果用在类上,表示是该类中所有方法的父路径。
@RequestMapping("/springmvc")
@Controller
public class SpringMVCTest {
@RequestMapping("/testRequestMapping")
public String testRequestMapping(){
System.out.println("testRequestMapping");
return SUCCESS;
}
}
在类上还添加了一个@Controller注解,该注解在SpringMVC中负责处理由DispatcherServlet分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个model,然后再把该model返回给对应的view进行展示。
我们可以通过“springmvc/testRequestMapping”这个路径来定位到testRequestMapping这个方法,然后执行方法内的方法体。
RequestMapping可以实现模糊匹配路径,比如:
- ?表示一个字符;
- *表示任意字符;
- **匹配多层路径;
/springmvc/**/testRequestMapping 就可以匹配/springmvc/stu/getStudentInfo/testRequestMapping 这样的路径了。
2、SpringMVC如何获取请求的参数
(1)@PathVariable
该注解用来映射请求URL中绑定的占位符。通过@PathVariable可以将URL中占位符的参数绑定到controller处理方法的入参中。
@RequestMapping("/testPathVariable/{id}")
public String testPathVariable(@PathVariable(value="id") Integer id){
System.out.println("testPathVariable:" + id);
return SUCCESS;
}
在index.jsp中我们添加一条连接,用来触发一个请求:
<a href="springmvc/testPathVariable/1">testPathVariable</a>
(2) @RequestParam
该注解也是用来获取请求参数的,那么该注解和@PathVariable有什么不同呢?
@RequestMapping(value="/testRequestParam")
public String testRequestParam(@RequestParam(value="username") String username, @RequestParam(value="age", required=false, defaultValue="0") int age){
System.out.println("testRequestParam" + " username:" + username + " age:" +age);
return SUCCESS;
}
在index.jsp添加超链接标签
<a href="springmvc/testRequestParam?username=jackie&age=12">testRequestParam</a>
3、REST风格的请求
在SpringMVC中业务最多的应该是CRUD了
@RequestMapping(value="/testRest/{id}", method=RequestMethod.PUT)
public String testRestPut(@PathVariable(value="id") Integer id){
System.out.println("test put:" + id);
return SUCCESS;
}
@RequestMapping(value="/testRest/{id}", method=RequestMethod.DELETE)
public String testRestDelete(@PathVariable(value="id") Integer id){
System.out.println("test delete:" + id);
return SUCCESS;
}
@RequestMapping(value="/testRest", method=RequestMethod.POST)
public String testRest(){
System.out.println("test post");
return SUCCESS;
}
@RequestMapping(value="/testRest/{id}", method=RequestMethod.GET)
public String testRest(@PathVariable(value="id") Integer id){
System.out.println("test get:" + id);
return SUCCESS;
}
135、如何防止表单重复提交
1、通过JavaScript屏蔽提交按钮(不推荐)
2、给数据库增加唯一键约束(简单粗暴)
3、利用Session防止表单重复提交(推荐)
4、使用AOP自定义切入实现
136、Spring中都应用了哪些设计模式
1、简单工厂模式
简单工厂模式的本质就是一个工厂类根据传入的参数,动态的决定实例化哪个类。
Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象。
2、工厂方法模式
应用程序将对象的创建及初始化职责交给工厂对象,工厂Bean。
定义工厂方法,然后通过config.xml配置文件,将其纳入Spring容器来管理,需要通过factory-method指定静态方法名称。
3、单例模式
Spring用的是双重判断加锁的单例模式,通过getSingleton方法从singletonObjects中获取bean。
/**
* Return the (raw) singleton object registered under the given name.
* <p>Checks already instantiated singletons and also allows for an early
* reference to a currently created singleton (resolving a circular reference).
* @param beanName the name of the bean to look for
* @param allowEarlyReference whether early references should be created or not
* @return the registered singleton object, or {@code null} if none found
*/
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
4、代理模式
Spring的AOP中,使用的Advice(通知)来增强被代理类的功能。Spring实现AOP功能的原理就是代理模式(① JDK动态代理,② CGLIB字节码生成技术代理。)对类进行方法级别的切面增强。
5、装饰器模式
装饰器模式:动态的给一个对象添加一些额外的功能。
Spring的ApplicationContext中配置所有的DataSource。这些DataSource可能是不同的数据库,然后SessionFactory根据用户的每次请求,将DataSource设置成不同的数据源,以达到切换数据源的目的。
在Spring中有两种表现:
一种是类名中含有Wrapper,另一种是类名中含有Decorator。
6、观察者模式
定义对象间的一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
Spring中观察者模式一般用在listener的实现。
7、策略模式
策略模式是行为性模式,调用不同的方法,适应行为的变化 ,强调父类的调用子类的特性 。
getHandler是HandlerMapping接口中的唯一方法,用于根据请求找到匹配的处理器。
8、模板方法模式
Spring JdbcTemplate的query方法总体结构是一个模板方法+回调函数,query方法中调用的execute()是一个模板方法,而预期的回调doInStatement(Statement state)方法也是一个模板方法。
137、请举例说明如何在 Spring 中注入一个 Java Collection?
Spring注入有四种方式,
- set注入;
- 构造器注入;
- 基于注解的注入;
- xml配置文件注入;
想要注入java collection,就是注入集合类:
- list
- set
- map
- props:该标签支持注入键和值都是字符串类型的键值对。
list和set都使用value标签;map使用entry标签;props使用prop标签;
138、mybatis 中 #{}和 ${}的区别是什么?
- \#{}带引号,${}不带引号;
- \#{}可以防止SQL注入;
- ${}常用于数据库表名、order by子句;
- 一般能用#{}就不要使用${};
139、mybatis 是否支持延迟加载?延迟加载的原理是什么?
1、mybatis 是否支持延迟加载?
延迟加载其实就是讲数据加载时机推迟,比如推迟嵌套查询的时机。
延迟加载可以实现先查询主表,按需实时做关联查询,返回关联表结果集,一定程度上提高了效率。
mybatis仅支持关联对象association和关联集合对象collection的延迟加载,association是一对一,collection是一对多查询,在mybatis配置文件中可以配置lazyloadingEnable=true/false。
2、延迟加载的原理是什么?
使用CGLIB为目标对象建立代理对象,当调用目标对象的方法时进入拦截器方法。
比如调用a.getB().getName(),拦截器方法invoke()发现a.getB()为null,会单独发送事先准备好的查询关联B对象的sql语句,把B查询出来然后调用a.setB(b),也是a的对象的属性b就有值了,然后调用getName(),这就是延迟加载的原理。
140、说一下 mybatis 的一级缓存和二级缓存?
一级缓存是session级别的缓存,默认开启,当查询一次数据库时,对查询结果进行缓存,如果之后的查询在一级缓存中存在,则无需再访问数据库;
二级缓存是sessionFactory级别的缓存,需要配置才会开启。当进行sql语句查询时,先查看一级缓存,如果不存在,访问二级缓存,降低数据库访问压力。
141、mybatis 有哪些执行器(Executor)?
1、mybatis有三种基本的Executor执行器:
(1)、SimpleExecutor
每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
(2)、PauseExecutor
执行update或select,以sql做为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而且放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
(3)、BatchExecutor
执行update,将所有sql通过addBatch()都添加到批处理中,等待统一执行executeBatch(),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
2、作用范围:
Executor的这些特点,都严格限制在SqlSession生命周期范围内。
3、Mybatis中如何指定使用哪一种Executor执行器?
在mybatis的配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数。
142、mybatis 和 hibernate 的区别有哪些?
1、两者最大的区别
针对简单逻辑,都有对应的代码生成工具,可以生成简单基本的dao层方法;
针对高级查询,mybatis要手动编写sql语句和resultMap,而hibernate有良好的映射机制;
2、开发难度对比
hibernate > mybatis
3、日志统计
hibernate有自己的日志统计功能,而mybatis需要借助log4j来记录日志。
4、数据库扩展比较
hibernate > mybatis
5、缓存机制比较
因为hibernate对查询对象有良好的管理机制,用户无需关心sql,所以使用二级缓存如果出现脏数据,系统会报错。
而mybatis,如果不能获取最新数据,应该避免缓存的使用,脏数据的出现会给系统的正常运行带来很大的隐患。
6、如何选择
- mybatis需要编写sql和映射规则,工作量大于hibernate;
- mybatis支持的工具也有限,不能像hibernate那样有许多插件可以帮助生成映射代码和关联关系;
- 对于性能要求不太苛刻的系统,比如管理系统、ERP等推荐hibernate;
- 对于性能要求高、响应快、灵活的系统,比如电商系统,推荐使用mybatis;
143、myBatis查询多个id、myBatis常用属性
myBatis查询多个id(我居然回答用对象来传递...)
Page<UserPoJo> getUserListByIds(@Param("ids") List<Integer> ids);
<!--根据id列表批量查询user-->
<select id="getUserListByIds" resultType="com.guor.UserPoJo">
select * from student
where id in
<foreach collection="ids" item="userid" open="(" close=")" separator=",">
#{userid}
</foreach>
</select>
144、mybatis一级缓存、二级缓存
1、一级缓存:指的是mybatis中sqlSession对象的缓存,当我们执行查询以后,查询的结果会同时存入sqlSession中,再次查询的时候,先去sqlSession中查询,有的话直接拿出,当sqlSession消失时,mybatis的一级缓存也就消失了,当调用sqlSession的修改、添加、删除、commit()、close()等方法时,会清空一级缓存。
2、二级缓存:指的是mybatis中的sqlSessionFactory对象的缓存,由同一个sqlSessionFactory对象创建的sqlSession共享其缓存,但是其中缓存的是数据而不是对象。当命中二级缓存时,通过存储的数据构造成对象返回。查询数据的时候,查询的流程是二级缓存 > 一级缓存 > 数据库。
3、如果开启了二级缓存,sqlSession进行close()后,才会把sqlSession一级缓存中的数据添加到二级缓存中,为了将缓存数据取出执行反序列化,还需要将要缓存的pojo实现Serializable接口,因为二级缓存数据存储介质多种多样,不一定只存在内存中,也可能存在硬盘中。
4、mybatis框架主要是围绕sqlSessionFactory进行的,具体的步骤:
- 定义一个configuration对象,其中包含数据源、事务、mapper文件资源以及影响数据库行为属性设置settings。
- 通过配置对象,则可以创建一个sqlSessionFactoryBuilder对象。
- 通过sqlSessionFactoryBuilder获得sqlSessionFactory实例。
- 通过sqlSessionFactory实例创建qlSession实例,通过sqlSession对数据库进行操作。
5、代码实例
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 加载类路径下的属性文件 -->
<properties resource="db.properties"/>
<!-- 设置类型别名 -->
<typeAliases>
<typeAlias type="cn.itcast.javaee.mybatis.app04.Student" alias="student"/>
</typeAliases>
<!-- 设置一个默认的连接环境信息 -->
<environments default="mysql_developer">
<!-- 连接环境信息,取一个任意唯一的名字 -->
<environment id="mysql_developer">
<!-- mybatis使用jdbc事务管理方式 -->
<transactionManager type="jdbc"/>
<!-- mybatis使用连接池方式来获取连接 -->
<dataSource type="pooled">
<!-- 配置与数据库交互的4个必要属性 -->
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
<!-- 连接环境信息,取一个任意唯一的名字 -->
<environment id="oracle_developer">
<!-- mybatis使用jdbc事务管理方式 -->
<transactionManager type="jdbc"/>
<!-- mybatis使用连接池方式来获取连接 -->
<dataSource type="pooled">
<!-- 配置与数据库交互的4个必要属性 -->
<property name="driver" value="${oracle.driver}"/>
<property name="url" value="${oracle.url}"/>
<property name="username" value="${oracle.username}"/>
<property name="password" value="${oracle.password}"/>
</dataSource>
</environment>
</environments>
<!-- 加载映射文件-->
<mappers>
<mapper resource="cn/itcast/javaee/mybatis/app14/StudentMapper.xml"/>
</mappers>
</configuration>
public class MyBatisTest {
public static void main(String[] args) {
try {
//读取mybatis-config.xml文件
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
//初始化mybatis,创建SqlSessionFactory类的实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//创建session实例
SqlSession session = sqlSessionFactory.openSession();
/*
* 接下来在这里做很多事情,到目前为止,目的已经达到得到了SqlSession对象.通过调用SqlSession里面的方法,
* 可以测试MyBatis和Dao层接口方法之间的正确性,当然也可以做别的很多事情,在这里就不列举了
*/
//插入数据
User user = new User();
user.setC_password("123");
user.setC_username("123");
user.setC_salt("123");
//第一个参数为方法的完全限定名:位置信息+映射文件当中的id
session.insert("com.cn.dao.UserMapping.insertUserInformation", user);
//提交事务
session.commit();
//关闭session
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
145、mybatis如何防止sql注入
注意:但凡是sql注入漏洞的程序,都是因为程序要接受来自客户端用户输入的变量或URL传递的参数,并且这个变量或参数是组成sql语句的一部分,对于用户输入的内容或传递的参数,我们应该要时刻保持警惕,这是安全领域里的【外部数据不可信任】的原则,纵观web安全领域的各种攻击方式,大多数都是因为开发者违反了这个原则而导致的,所以自然能想到,就是变量的检测、过滤、验证下手,确保变量是开发者所预想的。
1、检查变量数据类型和格式
数据类型检查,sql执行前,要进行数据类型检查,如果是邮箱,参数就必须是邮箱的格式,如果是日期,就必须是日期格式;
只要是有固定格式的变量,在SQL语句执行前,应该严格按照固定格式去检查,确保变量是我们预想的格式,这样很大程度上可以避免SQL注入攻击。
如果上述例子中id是int型的,效果会怎样呢?无法注入,因为输入注入参数会失败。比如上述中的name字段,我们应该在用户注册的时候,就确定一个用户名规则,比如5-20个字符,只能由大小写字母、数字以及汉字组成,不包含特殊字符。此时我们应该有一个函数来完成统一的用户名检查。不过,仍然有很多场景并不能用到这个方法,比如写博客,评论系统,弹幕系统,必须允许用户可以提交任意形式的字符才行,否则用户体验感太差了。
2、过滤特殊符号
3、绑定变量,使用预编译语句
146、为什么要使用 hibernate?
- hibernate对jdbc进行了封装,简化了JDBC的重复性代码;
- hibernate对dao有一个封装类hibernateTemplate,可以继承它,实现简单的CRUD接口。
- hibernate使用注解和配置文件,可以对实体类和映射文件进行映射;
- hibernate有事务管理机制,保证了数据的安全性;
- hibernate有一级缓存和二级缓存;
146、hibernate 中如何在控制台查看打印的 sql 语句?
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
147、hibernate 有几种查询方式?
1、导航对象图查询:根据已加载的对象,导航到其他对象。
例如,对于已经加载的Customer对象,调用它的getOrders().iterator()方法就可以导航到所有关联的Order对象,假如在关联级别使用了延迟加载检索策略,那么首次执行此方法时,hibernate会从数据库中加载关联的Order对象,否则就从缓存中获得Order对象。
2、OID方式:按照对象的OID来检索对象
Session的get()和load()方法提供了这种功能,如果在应用程序中先知道了OID,就可以使用这种方式检索对象。
get()和load()的用法完全一样,都需要两个参数,一个是持久化对象类名class,一个是行号OID,返回固定的某一行的数据,但是需要注意的是,当输入的OID不存在时,get()会返回一个空对象,load()则直接报错。
3、HQL检索方式:(hibernate query language)
使用面向对象的HQL查询语言,session的find()方法用于执行HQL查询语句。此外,hibernate还提供了query接口,它是hibernate提供的专门的HQL查询接口,能够执行各种复杂的HQL查询语句。
它具备以下功能:
- 在查询语句中设定各种查询条件;
- 支持投影查询,即仅检索出对象的部分属性;
- 支持分页查询;
- 支持连接查询;
- 支持分组查询;
- 提供内置函数;
- 能够调用用户自定义的SQL函数;
- 支持子查询;
- 支持动态绑定参数;
例如:
Query query = session.createQuery(“from UserPo”);
获得一个query对象,注意参数字符串中不是一个SQL语句,from后面的是持久化对象名称;
List list = query.list();
就可以获取数据库中对应表的数据集合。
4、QBC检索方式:Query By Criteria的API来检索对象
这种API封装了基于字符串形式的查询语句,提供了更加面向对象的接口。
例:Criteria criteria = session.createCriteria(UserPo.class);
创建一个Criteria对象,参数是所关联的持久化对象,criteria.add(Restrictions.ge("id",2));将查询条件加入对象中,后面的操作就和Query对象一样了。
5、本地SQL
使用本地数据库的SQL查询语句,hibernate会负责把检索到的JDBC ResultSet结果映射为持久化对象图。
148、hibernate 实体类可以被定义为 final 吗?
可以将hibernate的实体类定义为final,但这种做法不好。
因为hibernate会使用代理模式在延迟关联的情况下提高性能,如果你把实体类定义成final类之后,因为Java不允许对final类进行扩展,所以hibernate就无法再使用代理了,如此一来就限制了使用可以提升性能的手段。
不过,如果你的持久化类实现了一个接口,而且在该接口中声明了所有定义于实体类中的所有public的方法的话,就能避免出现前面所说的不利后果。
149、在 hibernate 中使用 Integer 和 int 做映射有什么区别?
hibernate是面向对象的ORM,所以一般定义成封装类型,要看数据库中的定义,如果数据库中有对应字段存在null值,就要定义Integer。也可以定义基本类型,在配置文件中写清楚即可。
150、什么是 Spring Boot?Spring Boot 有哪些优点?
1、Spring Boot简介
基于Spring4.0设计,不仅继承了Spring框架原有的优秀特性,而且还通过简化配置来进一步简化spring应用的整个搭建和开发过程。另外SpringBoot通过集成大量的框架使得依赖包的版本冲突、引用的不稳定性得到了解决。
2、Spring Boot 有哪些优点?
- 快速构建项目,可以选一些必要的组件;
- 对主流框架的无配置集成;
- 内嵌Tomcat容器,项目可独立运行;
- 删除了繁琐的xml配置文件;
- 极大地提高了开发和部署效率;
- 提供starter,简化maven配置;
3、SpringBoot有哪些缺点?
- 版本迭代速度快,一些模块改动很大;
- 由于无须配置,报错时很难定位;
151、Spring Boot 中的监视器是什么?
监听器也叫listener,是servlet的监听器,可以用于监听web应用程序中某些对象的创建、销毁、增加、修改、删除等动作的发生,然后做出相应的响应处理。当范围对象的状态发生变化时,服务器自动调用监听器对象中的方法,常用于系统加载时进行信息初始化,统计在线人数和在线用户,统计网站的访问量。
配置监听器的方法:
通过@Component把监听器加入Spring容器中管理;
在application.properties中添加context.listener.classes配置;
在方法上加@EventListener注解;
152、什么是 YAML?
YAML是JSON的一个超集,可以非常方便地将外部配置以层次结构形式存储起来。YAML可以作为properties配置文件的替代。
YAML使用的注意事项:
- 在properties文件中是以"."进行分割的,在yml中是用"."进行分割的;
- yml的数据格式和json的格式很像,都是K-V格式,并且通过":"进行赋值;
- 每个冒号后面一定要加一个空格;
153、如何使用 Spring Boot 实现分页和排序?
使用Spring Data Jpa可以实现将可分页的传递给存储库方法。
154、如何使用 Spring Boot 实现异常处理?
1、使用 @ExceptionHandler 注解处理局部异常(只能处理当前controller中的ArithmeticException和NullPointerException异常,缺点就是只能处理单个controller的异常)
@Controller
public class ExceptionHandlerController {
@RequestMapping("/excep")
public String exceptionMethod(Model model) throws Exception {
String a=null;
System.out.println(a.charAt(1));
int num = 1/0;
model.addAttribute("message", "没有抛出异常");
return "index";
}
@ExceptionHandler(value = {ArithmeticException.class,NullPointerException.class})
public String arithmeticExceptionHandle(Model model, Exception e) {
model.addAttribute("message", "@ExceptionHandler" + e.getMessage());
return "index";
}
}
2、使用 @ControllerAdvice + @ExceptionHandler 注解处理全局异常(value后面可以填写数组)
@ControllerAdvice
public class ControllerAdviceException {
@ExceptionHandler(value = {NullPointerException.class})
public String NullPointerExceptionHandler(Model model, Exception e) {
model.addAttribute("message", "@ControllerAdvice + @ExceptionHandler :" + e.getMessage());
return "index";
}
}
3、配置 SimpleMappingExceptionResolver 类处理异常(配置类)
@Configuration
public class SimpleMappingException {
@Bean
public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver(){
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
//第一个参数为异常全限定名,第二个为跳转视图名称
mappings.put("java.lang.NullPointerException", "index");
mappings.put("java.lang.ArithmeticException", "index");
//设置异常与视图映射信息的
resolver.setExceptionMappings(mappings);
return resolver;
}
}
4、实现 HandlerExceptionResolver 接口处理异常
@Configuration
public class HandlerException implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("message", "实现HandlerExceptionResolver接口");
//判断不同异常类型,做不同视图跳转
if(ex instanceof NullPointerException){
modelAndView.setViewName("index");
}
if(ex instanceof ArithmeticException){
modelAndView.setViewName("index");
}
return modelAndView;
}
}
155、单点登录
1、概念
单点登录SSO,说的是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。
2、单点登录的要点
存储信任;
验证信任;
3、实现单点登录的三种方式
(1)以cookie作为凭证
最简单的单点登录实现方式,是使用cookie作为媒介,存放用户凭证。
用户登录父应用之后,应用返回一个加密的cookie,当用户访问子应用的时候,携带上这个cookie,授权应用解密cookie进行校验,校验通过则登录当前用户。
缺点:
cookie不安全
通过加密可以保证安全性,但如果对方掌握了解密算法就完蛋了。
不能跨域实现免登
(2)通过JSONP实现
对于跨域问题,可以使用JSONP实现。用户在父应用中登录后,跟session匹配的cookie会存到客户端中,当用户需要登录子应用的时候,授权应用访问父应用提供的JSONP接口,并在请求中带上父应用域名下的cookie,父应用接收到请求,验证用户的登录状态,返回加密的信息,子应用通过解析返回来的加密信息来验证用户,如果通过验证则登录用户。
缺点:
这种方法虽然能解决跨域问题,但是治标不治本,没有解决cookie安全性的问题。
(3)通过页面重定向的方式
最后一种介绍的方式,是通过父应用和子应用来回重定向进行通信,实现信息的安全传递。
父应用提供一个GET方式的登录接口A(此时的父应用接口固定,攻击者无法去伪造),用户通过子应用重定向连接的方式访问这个接口,如果用户还没有登录,则返回一个登录页面,用户输入账号密码进行登录,如果用户已经登录了,则生成加密的token,并且重定向到子应用提供的验证token的接口B(此时的子应用接口固定,攻击者无法去伪造),通过解密和校验之后,子应用登录当前用户。
缺点:
这种方式较前面的两种方式,是解决了安全性和跨域的问题,但是并没有前面两种方式简单,安全与方便,本来就是矛盾的。
4、使用独立登录系统
一般来说,大型应用会把授权的逻辑和用户信息的相关逻辑独立成一个应用,称为用户中心。用户中心不处理业务逻辑,只是处理用户信息的管理以及授权给第三方应用。第三方应用需要登录的时候,则把用户的登录请求转发给用户中心进行处理,用户处理完毕后返回凭证,第三方应用验证凭证,通过后就登录用户。
5、sso(单点登录)与OAuth2.0(授权)的区别?
(1)sso(单点登录)
通常处理的是一个公司的不同应用间的访问登录问题,如企业应用有很多子系统,只需登录一个系统,就可以实现不同子系统间的跳转,而避免了登录操作;
通过cookie、jsonp、重定向来实现;
(2)OAuth2.0(授权)
解决的是服务提供方(如微信)给第三方应用授权的问题,简称微信登录;
是一种具体的协议,只是为用户资源的授权提供了一个安全的、开放的而又简易的标准,OAuth2.0(授权)为客户开发者开发web应用,桌面应用程序,移动应用及客厅设备提供特定的授权流程。
156、Spring Boot比Spring多哪些注解
157、打包和部署
Spring和Spring Boot都支持maven和Gradle通用打包管理技术。
Spring Boot相对Spring的一些优点:
- 提供嵌入式容器支持;
- 使用命令java -jar独立运行jar;
- 部署时可以灵活指定配置文件;
最近项目是分布式的项目,都是通过分项目打包部署,然后部署在docker中运行。
158、Spring Boot如何访问不同的数据库
可以使用druidDataSource创建DataSource,然后通过jdbcTemplate执行sql。
159、查询网站在线人数
通过监听session对象的方式来实现在线人数的统计和在线人信息展示,并且让超时的自动销毁。
对session对象实现监听,首先必须继承HttpSessionListener类,该程序的基本原理就是当浏览器访问页面的时候必定会产生一个session对象,当关闭该页面的时候必然会删除session对象。所以每当产生一个新的session对象就让在线人数+1,当删除一个session对象就让在线人数-1。还要继承一个HttpSessionAttributeListener,来实现对其属性的监听。分别实现attributeAdded方法,attributeReplace方法以及attributeRemove方法。
sessionCreated//新建一个会话的时候触发,也可以说是客户端第一次喝服务器交互时触发。
sessionDestroyed//销毁会话的时候,一般来说只有某个按钮触发进行销毁,或者配置定时销毁。
HttpSessionAttributeListener有三个方法需要实现
attributeAdded//在session中添加对象时触发此操作 笼统的说就是调用setAttribute这个方法时候会触发的
attributeRemoved//修改、删除session中添加对象时触发此操作 笼统的说就是调用 removeAttribute这个方法时候会触发的
attributeReplaced//在Session属性被重新设置时。
160、easyExcel如何实现
异步读取
新建一个 ExcelModelListener 监听类出来,并且 继承 AnalysisEventListener 类
package com.zh.oukele.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.zh.oukele.model.ExcelMode;
import java.util.ArrayList;
import java.util.List;
/***
* 监听器
*/
public class ExcelModelListener extends AnalysisEventListener<ExcelMode> {
/**
* 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 5;
List<ExcelMode> list = new ArrayList<ExcelMode>();
private static int count = 1;
@Override
public void invoke(ExcelMode data, AnalysisContext context) {
System.out.println("解析到一条数据:{ "+ data.toString() +" }");
list.add(data);
count ++;
if (list.size() >= BATCH_COUNT) {
saveData( count );
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData( count );
System.out.println("所有数据解析完成!");
System.out.println(" count :" + count);
}
/**
* 加上存储数据库
*/
private void saveData(int count) {
System.out.println("{ "+ count +" }条数据,开始存储数据库!" + list.size());
System.out.println("存储数据库成功!");
}
}
161、什么是 Swagger?你用 Spring Boot 实现了它吗?
Swagger是用于生成RestFul Web服务的可视化表示工具,它使文档和服务器可视化更新;
当定义好Swagger后,可以调用服务端接口,来查看接口的返回值,验证返回数据的正确性;
162、数据库的三范式是什么?
1、列不可再分;
2、每一行数据只做一件事,只与一列相关,主键;
3、每个属性都与主键有直接关系,而不是间接关系;
三大范式只是设计数据库的基本理念,可以建立冗余较小、结构合理的数据库。如果有特殊情结,当然要特殊对待,数据库设计最重要的是看需求和性能,需求>性能>表结构。
所以不能一味的追求三范式建立数据库。
163、一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?
一般情况下,我们创建的表类型是InnoDB。
不重启MySQL,如果新增一条记录,id是8;
重启,ID是6;因为InnoDB表只把自增主键的最大ID记录在内存中,如果重启,已删除的最大ID会丢失。
如果表类型是MyISAM,重启之后,最大ID也不会丢失,ID是8;
InnoDB必须有主键(建议使用自增主键,不用UUID,自增主键索引查询效率高)、支持外键、支持事务、支持行级锁。
系统崩溃后,MyISAM很难恢复;
综合考虑,优先选择InnoDB,MySQL默认也是InnoDB。
164、如何获取当前数据库版本?
//MySQL,,mysql -v
select version();
//Oracle
select * from v$version;
165、说一下 ACID 是什么?
ACID是数据库事务执行的四大基本要素,包括原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
1、原子性
整个事务中的所有操作,要么全部完成,要不全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被roolback回滚到事务开始前的状态,就像这个事务从未执行过一样。
2、一致性
事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
3、隔离性
隔离状态执行事务,使他们好像是系统在给定时间内执行的唯一操作。
如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性确保每一个事务在系统中认为只有自己在使用系统。这种属性称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。
4、持久性
一个成功的事务将永久的改变系统的状态。
166、char 和 varchar 的区别是什么?
- char的长度是固定的,varchar的长度的可变的;
- char的效率比varchar的效率高;
- char占用空间比varchar大,char在查询时需要使用trim;
167、float 和 double 的区别是什么?
1、float 和 double 的区别是什么?
(1)内存中占有的字节数不同
单精度浮点数在内存中占有4个字节;
双精度浮点数在内存中占有8个字节;
(2)有效数字位数不同
单精度浮点数有效数字8位;
双精度浮点数有效数字16位;
(3)数值取值范围不同
单精度浮点数的表示范围:-3.40E+38~3.40E+38
双精度浮点数的表示范围:-1.79E+308~-1.79E+308
(4)在程序中处理速度不同
一般来说,CPU处理单精度浮点数的速度比双精度浮点数的速度快
如果不声明,默认小数是double类型,如果想用float,要进行强转;
2、例如
float f = 1.3;会编译报错,正确的写法是float f = (float)1.3;或者float a = 1.3f;(f或F都可以不区分大小写)
3、注意
float是八位有效数字,第七位会四舍五入;
4、面试题
(1)java中3*0.1==0.3将会返回什么?true还是false?
答:返回false,因为浮点数不能完全精确的表示出来,一般会损失精度;
(2)java中float f = 3.4;是否正确?
答:不正确。因为3.4是双精度浮点数,将双精度赋给单精度属于向下转型,会造成精度损失,因此需要强制类型转换float=(float)3.4;或者写成float f = 3.4f;
168、Oracle分页sql
#不带排序的
SELECT * FROM (
SELECT ROWNUM AS rowno, t.* FROM worker t where ROWNUM <=20) table_alias
WHERE table_alias.rowno > 10;
#带排序的
SELECT * FROM (
SELECT tt.*, ROWNUM AS rowno FROM (
SELECT t.* FROM worker t ORDER BY wkid aSC) tt WHERE ROWNUM <= 20) table_alias
WHERE table_alias.rowno >= 10;
169、数据库如何保证主键唯一性
1、主键约束
主键列上没有任何两行具有相同值(即重复值),不允许空(NULL);
2、唯一性约束
保证一个字段或者一组字段里的数据都与表中其它行的对应数据不同。和主键约束不同,唯一性约束允许为null,但是只能有一行;
3、唯一性索引
不允许具有索引值相同的行,从而禁止重复的索引和键值;
4、三者的区别
- 约束是用来检查数据的正确性;
- 索引是用来优化查询的;
- 创建唯一性约束会创建一个约束和一个唯一性索引;
- 创建唯一性索引只会创建一个唯一性索引;
- 主键约束和唯一性约束都会创建一个唯一性索引。
170、如何设计数据库
1、数据库设计最起码要占用这个项目开发的40%以上的时间
2、数据库设计不仅仅停留在页面demo的表面
页面内容所需字段,在数据库设计中只是一部分,还有系统运转、模块交互、中转数据、表之间的联系等等所需要的字段,因此数据库设计绝对不是简单的基本数据存储,还有逻辑数据存储。
3、数据库设计完成后,项目80%的设计开发都要存在你的脑海中
每个字段的设计都要有他存在的意义,要清楚的知道程序中如何去运用这些字段,多张表的联系在程序中是如何体现的。
4、数据库设计时就要考虑效率和优化问题
数据量大的表示粗粒度的,会冗余一些必要字段,达到用最少的表,最弱的表关系去存储海量的数据。大数据的表要建立索引,方便查询。对于含有计算、数据交互、统计这类需求时,还有考虑是否有必要采用存储过程。
5、添加必要的冗余字段
像创建时间、修改时间、操作用户IP、备注这些字段,在每张表中最好都有,一些冗余的字段便于日后维护、分析、拓展而添加。
6、设计合理的表关联
若两张表之间的关系复杂,建议采用第三张映射表来关联维护两张表之间的关系,以降低表之间的直接耦合度。
7、设计表时不加主外键等约束关联,系统编码阶段完成后再添加约束性关联
8、选择合适的主键生成策略
数据库的设计难度其实比单纯的技术实现难很多,他充分体现了一个人的全局设计能力和掌控能力,最后说一句,数据库设计,很重要,很复杂。
171、性别是否适合做索引
区分度不高的字段不适合做索引,因为索引页是需要有开销的,需要存储的,不过这类字段可以做联合索引的一部分。
172、如何查询重复的数据
1、查询重复的单个字段(group by)
select 重复字段A, count(*) from 表 group by 重复字段A having count(*) > 1
2、查询重复的多个字段(group by)
select 重复字段A, 重复字段B, count(*) from 表 group by 重复字段A, 重复字段B having count(*) > 1
173、数据库一般会采取什么样的优化方法?
1、选取适合的字段属性
- 为了获取更好的性能,可以将表中的字段宽度设得尽可能小。
- 尽量把字段设置成not null
- 执行查询的时候,数据库不用去比较null值。
- 对某些省份或者性别字段,将他们定义为enum类型,enum类型被当做数值型数据来处理,而数值型数据被处理起来的速度要比文本类型块很多。
2、使用join连接代替子查询
3、使用联合union来代替手动创建的临时表
注意:union用法中,两个select语句的字段类型要匹配,而且字段个数要相同。
4、事务
要么都成功,要么都失败。
可以保证数据库中数据的一致性和完整性。事务以begin开始,commit关键字结束。
如果出错,rollback命令可以将数据库恢复到begin开始之前的状态。
事务的另一个重要作用是当多个用户同时使用相同的数据源时,它可以利用锁定数据库的方式为用户提供一种安全的访问方式,这样就可以保证用户的操作不被其他的用户干扰。
5、锁定表
尽管事务是维护数据库完整性的一个非常好的方法,但却因为它的独占性,有时会影响数据库的性能,尤其是在大应用中。
由于在事务执行的过程中,数据库会被锁定,因此其它用户只能暂时等待直到事务结束。
有的时候可以用锁定表的方法来获得更好的性能,
共享锁:其它用户只能看,不能修改
lock table person in share mode;
对于通过lock table 命令主动添加的锁来说,如果要释放它们,只需发出rollback命令即可。
6、使用外键
锁定表的方法可以维护数据的完整性,但是它却不能保证数据的关联性,这个时候可以使用外键。
7、使用索引
索引是提高数据库查询速度的常用方法,尤其是查询语句中包含max()、min()、order by这些命令的时候,性能提高更为显著。
一般来说索引应该建在常用于join、where、order by的字段上。尽量不要对数据库中含有大量重复的值得字段建立索引。
8、优化的查询语句
在索引的字段上尽量不要使用函数进行操作。
尽量不要使用like关键字和通配符,这样做法很简单,但却是以牺牲性能为代价的。
避免在查询中进行自动类型转换,因为类型转换也会使索引失效。
174、索引怎么定义,分哪几种
- b-tree索引,如果不建立索引的情况下,oracle就自动给每一列都加一个B 树索引;
- normal:普通索引
- unique:唯一索引
- bitmap:位图索引,位图索引特定于只有几个枚举值的情况,比如性别字段;
- 基于函数的索引
175、mysql 的内连接、左连接、右连接有什么区别?
- 内连接,显示两个表中有联系的所有数据;
- 左链接,以左表为参照,显示所有数据,右表中没有则以null显示
- 右链接,以右表为参照显示数据,,左表中没有则以null显示
176、RabbitMQ的使用场景有哪些?
1、解决异步问题
例如用户注册,发送邮件和短信反馈注册成功,可以使用RabbitMQ消息队列,用户无需等待反馈。
2、服务间解耦
订单系统和库存系统,中间加入RabbitMQ消息队列,当库存系统出现问题时,订单系统依旧能正常使用,降低服务间耦合度。
3、秒杀系统
利用RabbitMQ的最大值,实现秒杀系统。
177、RabbitMQ有哪些重要的角色?有哪些重要的组件?
1、RabbitMQ有哪些重要的角色?
客户端、RabbitMQ、服务端。
2、有哪些重要的组件?
(1)connectionFactory(连接管理器)
应用程序与RabbitMQ之间建立连接的管理器。
(2)Channel(信道)
消息推送使用的信道。
(3)RoutingKey(路由键)
用于把生产者的数据分配到交换机上。
(4)Exchange(交换机)
用于接受和分配消息。
(5)BindKey(绑定键)
用于把交换机的消息绑定到队列上
(6)Queue(队列)
用于存储生产者消息。
178、RabbitMQ中 vhost 的作用是什么?
vhost可以理解为mini版的RabbitMQ,其内部均含有独立的交换机、绑定、队列,最重要的是拥有独立的权限系统,可以做到vhost范围内的用户控制。从RabbitMQ全局考虑,不同的应用可以跑在不同的vhost上,作为不同权限隔离的手段。
179、说一下 jvm 的主要组成部分?及其作用?
JVM包括类加载子系统、堆、方法区、栈、本地方法栈、程序计数器、直接内存、垃圾回收器、执行引擎。
1、类加载子系统
类加载子系统负责加载class信息,加载的类信息存放于方法区中。
2、直接内存
直接内存是在Java堆外的、直接向系统申请的内存空间。访问直接内存的速度会由于Java堆。出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。
3、垃圾回收器
垃圾回收器可以对堆、方法区、直接内存进行回收。
4、执行引擎
执行引擎负责执行虚拟机的字节码,虚拟机会使用即时编译技术将方法编译成机器码后再执行。
180、说一下 jvm 运行时数据区?
运行时数据区包括堆、方法区、栈、本地方法栈、程序计数器。
1、堆
堆解决的是对象实例存储的问题,垃圾回收器管理的主要区域。
2、方法区
方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,常量、静态变量、即时编译器编译后的代码。
3、栈
栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。
(1)栈帧
每个方法从调用到执行的过程就是一个栈帧在虚拟机栈中入栈到出栈的过程。
(2)局部变量表
用于保存函数的参数和局部变量。
(3)操作数栈
操作数栈又称操作栈,大多数指令都是从这里弹出数据,执行运算,然后把结果压回操作数栈。
4、本地方法栈
与栈功能相同,本地方法栈执行的是本地方法,一个Java调用非Java代码的接口。
5、程序计数器(PC寄存器)
程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
181、什么是类加载器,类加载器有哪些?
1、什么是类加载器?
类加载器负责加载所有的类,其为所有被载入内存的类生成一个java.lang.Class实例对象。
2、类加载器有哪些?
JVM有三种类加载器:
(1)启动类加载器
该类没有父加载器,用来加载Java的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自java.lang.classLoader。
(2)扩展类加载器
它的父类为启动类加载器,扩展类加载器是纯java类,是ClassLoader类的子类,负责加载JRE的扩展目录。
(3)应用程序类加载器
它的父类为扩展类加载器,它从环境变量classpath或者系统属性java.lang.path所指定的目录中加载类,它是自定义的类加载器的父加载器。
182、说一下类加载的执行过程?
当程序主动使用某个类时,如果该类还未被加载到内存中,JVM会通过加载、连接、初始化3个步骤对该类进行类加载。
1、加载
加载指的是将类的class文件读入到内存中,并为之创建一个java.lang.Class对象。
类的加载由类加载器完成,类加载器由JVM提供,开发者也可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器可以从不同来源加载类的二进制数据,通常有如下几种来源:
- 从本地文件系统加载
- 从jar包加载
- 通过网络加载
- 把一个Java源文件动态编译,并执行加载
2、连接
当类被加载之后,系统为之生成一个对应的Class对象,接着进入连接阶段,连接阶段负责将类的二进制数据合并到JRE中。
类连接又可分为三个阶段:
(1)验证
文件格式验证
元数据验证
字节码验证
符号引用验证
(2)准备
为类的静态变量分配内存,并设置默认初始值。
(3)解析
将类的二进制数据中的符号引用替换成直接引用。
3、初始化
为类的静态变量赋予初始值。
183、JVM的类加载机制是什么?
JVM类加载机制主要有三种:
1、全盘负责
类加载器加载某个class时,该class所依赖的和引用其它的class也由该类加载器载入。
2、双亲委派
先让父加载器加载该class,父加载器无法加载时才考虑自己加载。
3、缓存机制
缓存机制保证所有加载过的class都会被缓存,当程序中需要某个class时,先从缓存区中搜索,如果不存在,才会读取该类对应的二进制数据,并将其转换成class对象,存入缓存区中。
这就是为什么修改了class后,必须重启JVM,程序所做的修改才会生效的原因。
184、什么是双亲委派模型?
如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器执行,如果父加载器还存在其父加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器,如果父类加载器可以完成父加载任务,就成功返回,如果父加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型。
双亲委派模式的优势:
- 避免重复加载;
- 考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派模式传递到启动加载器,而启动加载器在核心Java API中发现同名的类,发现该类已经被加载,就不会重新加载网络传递的Integer类,而直接返回已加载过的Integer.class,这样可以防止核心API库被随意篡改。
185、怎么判断对象是否可以被回收?
1、引用计数算法
(1)判断对象的引用数量
通过判断对象的引用数量来决定对象是否可以被回收;
每个对象实例都有一个引用计数器,被引用+1,完成引用-1;
任何引用计数为0的对象实例可以被当做垃圾回收;
(2)优缺点
优点:执行效率高,程序受影响较小;
缺点:无法检测出循环引用的情况,导致内存泄漏;
2、可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。
如果程序无法再引用该对象,那么这个对象肯定可以被回收,这个状态称为不可达。
那么不可达状态如何判断呢?
答案是GC roots,也就是根对象,如果一个对象无法到达根对象的路径,或者说从根对象无法引用到该对象,该对象就是不可达的。
以下三种对象在JVM中被称为GC roots,来判断一个对象是否可以被回收。
(1)虚拟机栈的栈帧
每个方法在执行的时候,JVM都会创建一个相应的栈帧(操作数栈、局部变量表、运行时常量池的引用),当方法执行完,该栈帧就从栈中弹出,这样一来,方法中临时创建的独享就不存在了,或者说没有任何GC roots指向这些临时对象,这些对象在下一次GC的时候便会被回收。
(2)方法区中的静态属性
静态属性数据类属性,不属于任何实例,因此该属性自然会作为GC roots。这要这个class在,该引用指向的对象就一直存在,class也由被回收的时候。
class何时会被回收?
- 堆中不存在该类的任何实例
- 加载该类的classLoader已经被回收
- 该类的java.lang.class对象没有在任何地方被引用,也就是说无法通过反射访问该类的信息
(3)本地方法栈引用的对象
186、说一下 jvm 有哪些垃圾回收算法?
1、对象是否已死算法
- 引用计数器算法
- 可达性分析算法
2、GC算法
(1)标记清除算法
如果对象被标记后进行清除,会带来一个新的问题--内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。
(2)复制算法(Java堆中新生代的垃圾回收算法)
- 先标记待回收内存和不用回收内存;
- 将不用回收的内存复制到新的内存区域;
- 就的内存区域就可以被全部回收了,而新的内存区域也是连续的;
缺点是损失部分系统内存,因为腾出部分内存进行复制。
(3)标记压缩算法(Java堆中老年代的垃圾回收算法)
对于新生代,大部分对象都不会存活,所以复制算法较高效,但对于老年代,大部分对象可能要继续存活,如果此时使用复制算法,效率会降低。
标记压缩算法首先还是标记,将不用回收的内存对象压缩到内存一端,此时即可清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。
老年代的垃圾回收算法称为“Major GC”。
188、JVM栈堆概念,何时销毁对象
- 类在程序运行的时候就会被加载,方法是在执行的时候才会被加载,如果没有任何引用了,Java自动垃圾回收,也可以用System.gc()开启回收器,但是回收器不一定会马上回收。
- 静态变量在类装载的时候进行创建,在整个程序结束时按序销毁;
- 实例变量在类实例化对象时创建,在对象销毁的时候销毁;
- 局部变量在局部范围内使用时创建,跳出局部范围时销毁;
189、新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
新生代回收器一般采用的是复制算法,复制算法效率较高,但是浪费内存;
老生代回收器一般采用标记清楚算法,比如最常用的CMS;
190、详细介绍一下 CMS 垃圾回收器?
CMS 垃圾回收器是Concurrent Mark Sweep,是一种同步的标记-清除,CMS分为四个阶段:
初始标记,标记一下GC Root能直接关联到的对象,会触发“Stop The World”;
并发标记,通过GC Roots Tracing判断对象是否在使用中;
重新标记,标记期间产生对象的再次判断,执行时间较短,会触发“Stop The World”;
并发清除,清除对象,可以和用户线程并发进行;
191、简述分代垃圾回收器是怎么工作的?
分代回收器分为新生代和老年代,新生代大概占1/3,老年代大概占2/3;
新生代包括Eden、From Survivor、To Survivor;
Eden区和两个survivor区的 的空间比例 为8:1:1 ;
垃圾回收器的执行流程:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden + From Survivor 分区,From Survivor 和 To Survivor 分区交换;
- 每次交换后存活的对象年龄+1,到达15,升级为老年代,大对象会直接进入老年代;
- 老年代中当空间到达一定占比,会触发全局回收,老年代一般采取标记-清除算法;
192、Redis是什么?
Redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string、list、set、zset(sorted set --有序集合)和hash。这些数据结构都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中,Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
193、Redis都有哪些使用场景?
- Redis是基于内存的nosql数据库,可以通过新建线程的形式进行持久化,不影响Redis单线程的读写操作
- 通过list取最新的N条数据
- 模拟类似于token这种需要设置过期时间的场景
- 发布订阅消息系统
- 定时器、计数器
194、Redis有哪些功能?
1、基于本机内存的缓存
当调用api访问数据库时,假如此过程需要2秒,如果每次请求都要访问数据库,那将对服务器造成巨大的压力,如果将此sql的查询结果存到Redis中,再次请求时,直接从Redis中取得,而不是访问数据库,效率将得到巨大的提升,Redis可以定时去更新数据(比如1分钟)。
2、如果电脑重启,写入内存的数据是不是就失效了呢,这时Redis还提供了持久化的功能。
3、哨兵(Sentinel)和复制
Sentinel可以管理多个Redis服务器,它提供了监控、提醒以及自动的故障转移功能;
复制则是让Redis服务器可以配备备份的服务器;
Redis也是通过这两个功能保证Redis的高可用;
4、集群(Cluster)
单台服务器资源总是有上限的,CPU和IO资源可以通过主从复制,进行读写分离,把一部分CPU和IO的压力转移到从服务器上,但是内存资源怎么办,主从模式只是数据的备份,并不能扩充内存;
现在我们可以横向扩展,让每台服务器只负责一部分任务,然后将这些服务器构成一个整体,对外界来说,这一组服务器就像是集群一样。
195、Redis支持的数据类型有哪些?
- 字符串
- hash
- list
- set
- zset
196、Redis取值存值问题
1、先把Redis的连接池拿出来
JedisPool pool = new JedisPool(new JedisPoolConfig(),"127.0.0.1");
Jedis jedis = pool.getResource();
2、存取值
jedis.set("key","value");
jedis.get("key");
jedis.del("key");
//给一个key叠加value
jedis.append("key","value2");//此时key的值就是value + value2;
//同时给多个key进行赋值:
jedis.mset("key1","value1","key2","value2");
3、对map进行操作
Map<String,String> user = new HashMap();
user.put("key1","value1");
user.put("key2","value2");
user.put("key3","value3");
//存入
jedis.hmset("user",user);
//取出user中key1
List<String> nameMap = jedis.hmget("user","key1");
//删除其中一个键值
jedis.hdel("user","key2");
//是否存在一个键
jedis.exists("user");
//取出所有的Map中的值:
Iterator<String> iter = jedis.hkeys("user").iterator();
while(iter.next()){
jedis.hmget("user",iter.next());
}
197、Redis为什么是单线程的?
- 代码更清晰,处理逻辑更简单;
- 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;
- 不存在多线程切换而消耗CPU;
- 无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善;
198、Redis真的是单线程的吗?
Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;
redis内部使用了基于epoll的多路服用,也可以多部署几个redis服务器解决单线程的问题;
redis主要的性能瓶颈是内存和网络;
内存好说,加内存条就行了,而网络才是大麻烦,所以redis6内存好说,加内存条就行了;
而网络才是大麻烦,所以redis6.0引入了多线程的概念,
redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。
199、Redis持久化有几种方式?
redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。
RDB,简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上;
AOF,则是换了一个角度来实现持久化,那就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF方式来进行数据恢复,这是因为AOF方式的数据恢复完整度更高。
如果你没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库,就像memcache一样。
200、Redis和 memecache 有什么区别?
1、Redis相比memecache,拥有更多的数据结构和支持更丰富的数据操作。
(1)Redis支持key-value,常用的数据类型主要有String、Hash、List、Set、Sorted Set。
(2)memecache只支持key-value。
2、内存使用率对比,Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于memecache。
3、性能对比:Redis只使用单核,memecache使用多核。
4、Redis支持磁盘持久化,memecache不支持。
Redis可以将一些很久没用到的value通过swap方法交换到磁盘。
5、Redis支持分布式集群,memecache不支持。
201、Redis支持的 java 客户端都有哪些?
Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。
202、jedis 和 redisson 有哪些区别?
Jedis 和 Redisson 都是Java中对Redis操作的封装。Jedis 只是简单的封装了 Redis 的API库,可以看作是Redis客户端,它的方法和Redis 的命令很类似。Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于Jedis 更加大。但Jedis相比于Redisson 更原生一些,更灵活。
203、什么是缓存穿透?怎么解决?
1、缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对用的value,就应该去后端系统查找(比如DB数据库)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
2、怎么解决?
对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert之后清理缓存。
对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该Bitmap过滤。
3、缓存雪崩
当缓存服务器重启或者大量缓存集中在某一时间段失效,这样在失效的时候,会给后端系统带来很大的压力,导致系统崩溃。
4、如何解决?
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其它线程等待;
- 做二级缓存;
- 不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀;
204、怎么保证缓存和数据库数据的一致性?
1、淘汰缓存
数据如果为较为复杂的数据时,进行缓存的更新操作就会变得异常复杂,因此一般推荐选择淘汰缓存,而不是更新缓存。
2、选择先淘汰缓存,再更新数据库
假如先更新数据库,再淘汰缓存,如果淘汰缓存失败,那么后面的请求都会得到脏数据,直至缓存过期。
假如先淘汰缓存再更新数据库,如果更新数据库失败,只会产生一次缓存穿透,相比较而言,后者对业务则没有本质上的影响。
3、延时双删策略
如下场景:同时有一个请求A进行更新操作,另一个请求B进行查询操作。
- 请求A进行写操作,删除缓存
- 请求B查询发现缓存不存在
- 请求B去数据库查询得到旧值
- 请求B将旧值写入缓存
- 请求A将新值写入数据库
次数便出现了数据不一致问题。采用延时双删策略得以解决。
public void write(String key,Object data){
redisUtils.del(key);
db.update(data);
Thread.Sleep(100);
redisUtils.del(key);
}
这么做,可以将1秒内所造成的缓存脏数据,再次删除。这个时间设定可根据俄业务场景进行一个调节。
4、数据库读写分离的场景
两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
- 请求A进行写操作,删除缓存
- 请求A将数据写入数据库了,
- 请求B查询缓存发现,缓存没有值
- 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
- 请求B将旧值写入缓存
- 数据库完成主从同步,从库变为新值
依旧采用延时双删策略解决此问题。
205、Redis,什么是缓存穿透?怎么解决?
1、缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对用的value,就应该去后端系统查找(比如DB数据库)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
2、怎么解决?
对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert之后清理缓存。
对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该Bitmap过滤。
3、缓存雪崩
当缓存服务器重启或者大量缓存集中在某一时间段失效,这样在失效的时候,会给后端系统带来很大的压力,导致系统崩溃。
4、如何解决?
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其它线程等待;
- 做二级缓存;
- 不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀;
206、Redis怎么实现分布式锁?
使用Redis实现分布式锁
redis命令:set users 10 nx ex 12 原子性命令
//使用uuid,解决锁释放的问题
@GetMapping
public void testLock() throws InterruptedException {
String uuid = UUID.randomUUID().toString();
Boolean b_lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
if(b_lock){
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)){
return;
}
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",++num);
Object lockUUID = redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockUUID.toString())){
redisTemplate.delete("lock");
}
}else{
Thread.sleep(100);
testLock();
}
}
备注:可以通过lua脚本,保证分布式锁的原子性。
207、Redis分布式锁有什么缺陷?
Redis 分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。
Redis容易产生的几个问题:
- 锁未被释放
- B锁被A锁释放了
- 数据库事务超时
- 锁过期了,业务还没执行完
- Redis主从复制的问题
208、Redis如何做内存优化?
1、缩短键值的长度
- 缩短值的长度才是关键,如果值是一个大的业务对象,可以将对象序列化成二进制数组;
- 首先应该在业务上进行精简,去掉不必要的属性,避免存储一些没用的数据;
- 其次是序列化的工具选择上,应该选择更高效的序列化工具来降低字节数组大小;
- 以JAVA为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等
2、共享对象池
对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。
3、字符串优化
4、编码优化
5、控制key的数量