10万字208道Java经典面试题总结(附答案)

本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

🍅 作者简介:哪吒,CSDN2021 博客之星亚军🏆、新星计划导师✌、博客专家💪

🍅 哪吒多年工作总结:Java 学习路线总结,搬砖工逆袭 Java 架构师

🍅 技术交流:定期更新 Java 硬核干货,不定期送书活动

🍅 关注公众号【哪吒编程】,回复 面试题 ,获取《10 万字 208 道 Java 经典面试题总结 (附答案)》pdf,背题更方便,一文在手,面试我有

前言

最近有很多粉丝问我,有什么方法能够快速提升自己,通过阿里、腾讯、字节跳动、京东等互联网大厂的面试,我觉得短时间提升自己最快的手段就是背面试题,最近总结了 Java 常用的面试题,分享给大家,希望大家都能圆梦大厂,加油,我命由我不由天。

目录

1、JDK 和 JRE 有什么区别?

2、== 和 equals 的区别是什么?

3、final 在 java 中有什么作用?

4、java 中的 Math.round(-1.5) 等于多少?

5、String 属于基础的数据类型吗?

6、String str=“i” 与 String str=new String(“i”) 一样吗?

7、如何将字符串反转?

8、String 类的常用方法都有那些?

9、new String(“a”) + new String(“b”) 会创建几个对象?

10、如何将字符串反转?

11、String 类的常用方法都有那些?

12、普通类和抽象类有哪些区别?

13、接口和抽象类有什么区别?

14、java 中 IO 流分为几种?

15、BIO、NIO、AIO 有什么区别?

16、Files 的常用方法都有哪些?

17、什么是反射?

18、什么是 java 序列化?什么情况下需要序列化?

19、为什么要使用克隆?如何实现对象克隆?深拷贝和浅拷贝区别是什么?

20、throw 和 throws 的区别?

21、final、finally、finalize 有什么区别?

22、try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

23、常见的异常类有哪些?

24、hashcode 是什么?有什么作用?

25、java 中操作字符串都有哪些类?它们之间有什么区别?

26、java 中都有哪些引用类型?

27、在 Java 中,为什么不允许从静态方法中访问非静态变量?

28、说说 Java Bean 的命名规范

29、Java Bean 属性命名规范问题分析

30、什么是 Java 的内存模型?

31、在 Java 中,什么时候用重载,什么时候用重写?

32、举例说明什么情况下会更倾向于使用抽象类而不是接口?

33、实例化对象有哪几种方式

34、byte 类型 127+1 等于多少

35、Java 容器都有哪些?

36、Collection 和 Collections 有什么区别?

37、list 与 Set 区别

38、HashMap 和 Hashtable 有什么区别?

39、说一下 HashMap 的实现原理?

40、set 有哪些实现类?

41、说一下 HashSet 的实现原理?

42、ArrayList 和 LinkedList 的区别是什么?

43、如何实现数组和 List 之间的转换?

44、在 Queue 中 poll() 和 remove() 有什么区别?

45、哪些集合类是线程安全的

46、迭代器 Iterator 是什么?

47、Iterator 怎么使用?有什么特点?

48、Iterator 和 ListIterator 有什么区别?

49、怎么确保一个集合不能被修改?

50、队列和栈是什么?有什么区别?

51、Java8 开始 ConcurrentHashMap, 为什么舍弃分段锁?

52、ConcurrentHashMap(JDK1.8) 为什么要使用 synchronized 而不是如 ReentranLock 这样的可重入锁?

53、concurrentHashMap 和 HashTable 有什么区别

54、HasmMap 和 HashSet 的区别

55、请谈谈 ReadWriteLock 和 StampedLock

56、线程的 run() 和 start() 有什么区别?

57、为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

58、Synchronized 用过吗,其原理是什么?

59、JVM 对 Java 的原生锁做了哪些优化?

60、为什么 wait(), notify() 和 notifyAll() 必须在同步方法或者同步块中被调用?

61、Java 如何实现多线程之间的通讯和协作?

62、Thread 类中的 yield 方法有什么作用?

63、为什么说 Synchronized 是非公平锁?

64、请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?

65、为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?

66、乐观锁一定就是好的吗?

67、请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。

68、ReentrantLock 是如何实现可重入性的?

69、什么是锁消除和锁粗化?

70、跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?

71、那么请谈谈 AQS 框架是怎么回事儿?

72、AQS 对资源的共享方式?

73、如何让 Java 的线程彼此同步?

74、你了解过哪些同步器?请分别介绍下。

75、Java 中的线程池是如何实现的

76、创建线程池的几个核心构造参数

77、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?

78、volatile 关键字的作用

79、既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?

80、ThreadLocal 是什么?有哪些使用场景?

81、请谈谈 ThreadLocal 是怎么解决并发安全的?

82、很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?

83、为什么代码会重排序?

84、什么是自旋

85、多线程中 synchronized 锁升级的原理是什么?

86、synchronized 和 ReentrantLock 区别是什么?

87、Java Concurrency API 中的 Lock 接口 (Lock interface) 是什么?对比同步它有什么优势?

88、jsp 和 servlet 有什么区别?

89、jsp 有哪些内置对象?作用分别是什么?

90、forward 和 redirect 的区别?

91、说一下 jsp 的 4 种作用域?

92、session 和 cookie 有什么区别?

93、如果客户端禁止 cookie 能实现 session 还能用吗?

94、什么是上下文切换?

95、cookie、session、token

96、说一下 session 的工作原理?

97、http 响应码 301 和 302 代表的是什么?有什么区别?

98、简述 tcp 和 udp 的区别?

99、tcp 为什么要三次握手,两次不行吗?为什么?

100、OSI 的七层模型都有哪些?

101、get 和 post 请求有哪些区别?

102、什么是 XSS 攻击,如何避免?

103、什么是 CSRF 攻击,如何避免?

104、如何实现跨域?说一下 JSONP 实现原理?

105、websocket 应用的是哪个协议

106、说一下 tcp 粘包是怎么产生的?

107、请列举出在 JDK 中几个常用的设计模式?

108、什么是设计模式?你是否在你的代码里面使用过任何设计模式?

109、Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式

110、在 Java 中,什么叫观察者设计模式(observer design pattern)?

111、使用工厂模式最主要的好处是什么?在哪里使用?

112、请解释自动装配模式的区别?

113、举一个用 Java 实现的装饰模式 (decorator design pattern)?它是作用于对象层次还是类层次?

114、什么是 Spring 框架?Spring 框架有哪些主要模块?

115、使用 Spring 框架能带来哪些好处?

116、Spring IOC、AOP 举例说明

117、什么是控制反转 (IOC)?什么是依赖注入?

118、BeanFactory 和 ApplicationContext 有什么区别?

119、什么是 JavaConfig?

120、什么是 ORM 框架?

121、Spring 有几种配置方式?

122、请解释 Spring Bean 的生命周期?

123、Spring Bean 的作用域之间有什么区别?Spring 容器中的 bean 可以分为 5 个范围:

124、如何在 Spring Boot 中禁用 Actuator 端点安全性?

125、什么是 Spring inner beans?

126、Spring 框架中的单例 Beans 是线程安全的么?

127、请解释 Spring Bean 的自动装配?

128、如何开启基于注解的自动装配?

129、什么是 Spring Batch?

130、spring mvc 和 struts 的区别是什么?

131、请举例解释 @Required 注解?

132、Spring 常用注解

133、项目中是如何实现权限验证的,权限验证需要几张表

134、谈谈 controller,接口调用的路径问题

135、如何防止表单重复提交

136、Spring 中都应用了哪些设计模式

137、请举例说明如何在 Spring 中注入一个 Java Collection?

138、mybatis 中 #{} 和 ${} 的区别是什么?

139、mybatis 是否支持延迟加载?延迟加载的原理是什么?

140、说一下 mybatis 的一级缓存和二级缓存?

141、mybatis 有哪些执行器(Executor)?

142、mybatis 和 hibernate 的区别有哪些?

143、myBatis 查询多个 id、myBatis 常用属性

144、mybatis 一级缓存、二级缓存

145、mybatis 如何防止 sql 注入

146、hibernate 中如何在控制台查看打印的 sql 语句?

147、hibernate 有几种查询方式?

148、hibernate 实体类可以被定义为 final 吗?

149、在 hibernate 中使用 Integer 和 int 做映射有什么区别?

150、什么是 Spring Boot?Spring Boot 有哪些优点?

151、Spring Boot 中的监视器是什么?

152、什么是 YAML?

153、如何使用 Spring Boot 实现分页和排序?

154、如何使用 Spring Boot 实现异常处理?

155、单点登录

156、Spring Boot 比 Spring 多哪些注解

157、打包和部署

158、Spring Boot 如何访问不同的数据库

159、查询网站在线人数

160、easyExcel 如何实现

161、什么是 Swagger?你用 Spring Boot 实现了它吗?

162、数据库的三范式是什么?

163、一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?

164、如何获取当前数据库版本?

165、说一下 ACID 是什么?

166、char 和 varchar 的区别是什么?

167、float 和 double 的区别是什么?

168、Oracle 分页 sql

169、数据库如何保证主键唯一性

170、如何设计数据库

171、性别是否适合做索引

172、如何查询重复的数据

173、数据库一般会采取什么样的优化方法?

174、索引怎么定义,分哪几种

175、mysql 的内连接、左连接、右连接有什么区别?

176、RabbitMQ 的使用场景有哪些?

177、RabbitMQ 有哪些重要的角色?有哪些重要的组件?

178、RabbitMQ 中 vhost 的作用是什么?

179、说一下 jvm 的主要组成部分?及其作用?

180、说一下 jvm 运行时数据区?

181、什么是类加载器,类加载器有哪些?

182、说一下类加载的执行过程?

183、JVM 的类加载机制是什么?

184、什么是双亲委派模型?

185、怎么判断对象是否可以被回收?

186、说一下 jvm 有哪些垃圾回收算法?

187、说一下 jvm 有哪些垃圾回收器?

188、JVM 栈堆概念,何时销毁对象

189、新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

190、详细介绍一下 CMS 垃圾回收器?

191、简述分代垃圾回收器是怎么工作的?

192、Redis 是什么?

193、Redis 都有哪些使用场景?

194、Redis 有哪些功能?

195、Redis 支持的数据类型有哪些?

196、Redis 取值存值问题

197、Redis 为什么是单线程的?

198、Redis 真的是单线程的吗?

199、Redis 持久化有几种方式?

200、Redis 和 memecache 有什么区别?

201、Redis 支持的 java 客户端都有哪些?

202、jedis 和 redisson 有哪些区别?

203、什么是缓存穿透?怎么解决?

204、怎么保证缓存和数据库数据的一致性?

205、Redis,什么是缓存穿透?怎么解决?

206、Redis 怎么实现分布式锁?

207、Redis 分布式锁有什么缺陷?

208、Redis 如何做内存优化?

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 的区别是什么?

  1. 对于基本类型,== 比较的是值;
  2. 对于引用类型,== 比较的是地址;
  3. equals 不能用于基本类型的比较;
  4. 如果没有重写 equals,equals 就相当于 ==;
  5. 如果重写了 equals 方法,equals 比较的是对象的内容;

3、final 在 java 中有什么作用?

(1)用来修饰一个引用

  1. 如果引用为基本数据类型,则该引用为常量,该值无法修改;
  2. 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
  3. 如果引用时类的成员变量,则必须当场赋值,否则编译会报错。

(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 方法反转。

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 还会执行吗?

23、常见的异常类有哪些?

  1. NullPointerException:空指针异常;
  2. SQLException:数据库相关的异常;
  3. IndexOutOfBoundsException:数组下角标越界异常;
  4. FileNotFoundException:打开文件失败时抛出;
  5. IOException:当发生某种 IO 异常时抛出;
  6. ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出此异常;
  7. NoSuchMethodException:无法找到某一方法时,抛出;
  8. ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常;
  9. NumberFormatException:当试图将字符串转换成数字时,失败了,抛出;
  10. IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
  11. 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 求余数直接找到存放的位置了。

  1. 但是如果两个类有相同的 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 中,为什么不允许从静态方法中访问非静态变量?

  1. 静态变量属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问;
  2. 非静态变量属于类的对象,只有在类的对象产生时,才会分配内存,通过类的实例去访问;
  3. 静态方法也属于类本身,但是此时没有类的实例,内存中没有非静态变量,所以无法调用。

28、说说 Java Bean 的命名规范

  1. JavaBean 类必须是一个公共类,并将其访问属性设置为 public
  2. JavaBean 类必须有一个空的构造函数:类中必须有一个不带参数的公用构造器,此构造器也应该通过调用各个特性的设置方法来设置特性的缺省值。
  3. 一个 javaBean 类不应有公共实例变量,类变量都为 private
  4. 持有值应该通过一组存取方法(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. 先对各位取反;
  2. 将其转换为十进制数;
  3. 加上负号,再减去 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 有什么区别?

  1. HashMap 是线程不安全的,HashTable 是线程安全的;
  2. HashMap 中允许键和值为 null,HashTable 不允许;
  3. 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。

JDK1.8 中采用数据 + 链表 + 红黑树的存储形式。当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。

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 接口源码中的方法:

  1. java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象
  2. next() 方法获得集合中的下一个元素
  3. hasNext() 检查集合中是否还有元素
  4. remove() 方法将迭代器新返回的元素删除

48、Iterator 和 ListIterator 有什么区别?

(1)ListIterator 继承 Iterator

(2)ListIterator 比 Iterator 多方法

  1. add(E e)  将指定的元素插入列表,插入位置为迭代器当前位置之前
  2. set(E e)  迭代器返回的最后一个元素替换参数 e
  3. hasPrevious()  迭代器当前位置,反向遍历集合是否含有元素
  4. previous()  迭代器当前位置,反向遍历集合,下一个元素
  5. previousIndex()  迭代器当前位置,反向遍历集合,返回下一个元素的下标
  6. nextIndex()  迭代器当前位置,返回下一个元素的下标

(3)使用范围不同,Iterator 可以迭代所有集合;ListIterator 只能用于 List 及其子类

  1. ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能
  2. ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator 不可以
  3. ListIterator 有 nextIndex() 和 previousIndex() 方法,可定位当前索引的位置;Iterator 不可以
  4. 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)

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 每次同步执行的时候都要锁住整个结构,如下图:

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。

  1. 如果两个对象的 hashcode 相同,他们并不一定相同。
  2. 如果两个对象的 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 的区别

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 变量)会记录线程获得几次锁;

  1. 可重入的好处:
  2. 可以避免死锁;
  3. 可以让我们更好的封装代码;

synchronized 是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会 - 1,直到计数器的数量为 0,就释放这个锁。

(2)不可中断性

  1. 一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;
  2. synchronized 属于不可被中断;
  3. Lock lock 方法是不可中断的;
  4. 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 个原子操作:

  1. lock
  2. unlock
  3. read
  4. load
  5. assign
  6. use
  7. store
  8. 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 的线程彼此同步?

  1. synchronized
  2. volatile
  3. ReenreantLock
  4. 使用局部变量实现线程同步

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 九大内置对象:

  1. pageContext,页面上下文对象,相当于页面中所有功能的集合,通过它可以获取 JSP 页面的 out、request、response、session、application 对象。
  2. request
  3. response
  4. session
  5. application,应用程序对象,application 实现了用户间数据的共享,可存放全局变量,它开始于服务器启动,知道服务器关闭。
  6. page,就是 JSP 本身。
  7. exception
  8. out,out 用于在 web 浏览器内输出信息,并且管理应用服务器上的输出缓冲区,作用域 page。
  9. config,取得服务器的配置信息。

90、forward 和 redirect 的区别?

  1. forward 是直接请求转发;redirect 是间接请求转发,又叫重定向。
  2. forward,客户端和浏览器执行一次请求;redirect,客户端和浏览器执行两次请求。
  3. forward,经典的 MVC 模式就是 forward;redirect,用于避免用户的非正常访问。(例如用户非正常访问,servlet 就可以将 HTTP 请求重定向到登录页面)。
  4. forward,地址不变;redirect,地址改变。
  5. 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。

  1. 通过 url 重写,把 sessionid 作为参数追加的原 url 中,后续的浏览器与服务器交互中携带 sessionid 参数。
  2. 服务器的返回数据中包含 sessionid,浏览器发送请求时,携带 sessionid 参数。
  3. 通过 Http 协议其他 header 字段,服务器每次返回时设置该 header 字段信息,浏览器中 js 读取该 header 字段,请求服务器时,js 设置携带该 header 字段。

94、什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

95、cookie、session、token

1、session 机制

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 机制

适用于前后端分离的项目(前后端代码运行在不同的服务器下)

请求登录时,token 和 sessionid 原理相同,是对 key 和 key 对应的用户信息进行加密后的加密字符,登录成功后,会在响应主体中将 {token:“字符串”} 返回给客户端。

客户端通过 cookie 都可以进行存储。再次请求时不会默认携带,需要在请求拦截器位置给请求头中添加认证字段 Authorization 携带 token 信息,服务器就可以通过 token 信息查找用户登录状态。

96、说一下 session 的工作原理?

当客户端登录完成后,会在服务端产生一个 session,此时服务端会将 sessionid 返回给客户端浏览器。客户端将 sessionid 储存在浏览器的 cookie 中,当用户再次登录时,会获得对应的 sessionid,然后将 sessionid 发送到服务端请求登录,服务端在内存中找到对应的 sessionid,完成登录,如果找不到,返回登录页面。

97、http 响应码 301 和 302 代表的是什么?有什么区别?

  1. 301 和 302 状态码都表示重定向,当浏览器拿到服务器返回的这个状态码后悔自动跳转到一个新的 URL 地址。
  2. 301 代表永久性重定向,旧地址被永久移除,客户端向新地址发送请求。
  3. 302 代表暂时性重定向,旧地址还在,客户端继续向旧地址发送请求。
  4. 303 代表暂时性重定向,重定向到新地址时,必须使用 GET 方法请求新地址。
  5. 307 代表暂时性重定向,与 302 的区别在于 307 不允许从 POST 改为 GET。
  6. 307 代表永久性重定向,与 301 的区别在于 308 不允许从 POST 改为 GET。

98、简述 tcp 和 udp 的区别?

  1. TCP 是传输控制协议,UDP 是用户数据表协议;
  2. TCP 长连接,UDP 无连接;
  3. UDP 程序结构较简单,只需发送,无须接收;
  4. TCP 可靠,保证数据正确性、顺序性;UDP 不可靠,可能丢数据;
  5. TCP 适用于少量数据,UDP 适用于大量数据传输;
  6. TCP 速度慢,UDP 速度快;

99、tcp 为什么要三次握手,两次不行吗?为什么?

因为客户端和服务端都要确认连接,①客户端请求连接服务端;②针对客户端的请求确认应答,并请求建立连接;③针对服务端的请求确认应答,建立连接;

两次无法确保 A 能收到 B 的数据;

100、OSI 的七层模型都有哪些?

101、get 和 post 请求有哪些区别?

  1. get 请求参数是连接在 url 后面的, 而 post 请求参数是存放在 requestbody 内的;
  2. get 请求因为浏览器对 url 长度有限制,所以参数个数有限制,而 post 请求参数个数没有限制;
  3. 因为 get 请求参数暴露在 url 上, 所以安全方面 post 比 get 更加安全;
  4. get 请求只能进行 url 编码, 而 post 请求可以支持多种编码方式;
  5. get 请求参数会保存在浏览器历史记录内, post 请求并不会;
  6. get 请求浏览器会主动 cache,post 并不会, 除非主动设置;
  7. get 请求产生 1 个 tcp 数据包, post 请求产生 2 个 tcp 数据包;
  8. 在浏览器进行回退操作时, get 请求是无害的, 而 post 请求则会重新请求一次;
  9. 浏览器在发送 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 原理详解——终于搞清楚 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、设计模式的七大原则

  1. 单一职责原则
  2. 接口隔离原则
  3. 依赖倒转原则
  4. 里式替换原则
  5. 开闭原则
  6. 迪米特法则
  7. 合成复用原则

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 写出线程安全的单例模式

  1. 保证程序只有一个对象的实例,叫做单例模式;
  2. 内部类的方式实现单例模式,是线程安全的;
  3. 双重验证方式实现单例模式也是线程安全的;

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 作为抽象类,其下有几个实现类,表示从不同的数据源输入:

  1. byteArrayInputStream
  2. fileInputStream
  3. StringBufferInputStream
  4. PipedInputStream,从管道产生输入;
  5. 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 理论提出的观点大体是这样的:借助于 “第三方” 实现具有依赖关系的对象之间的解耦,如下图:

大家看到了吧,由于引进了中间位置的 “第三方”,也就是 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 的全部功能,并且扩展了很多高级特性,每次容器启动时就会创建所有的对象。

  1. ApplicationContext 的额外功能:
  2. 继承 MessageSource,支持国际化;
  3. 统一的资源文件访问方式;
  4. 提供在监听器中注册 bean;
  5. 同时加载过个配置文件;
  6. 载入多个(有继承关系)上下文,使得每个上下文都专注于一个特定的层次,比如应用的 web 层;

2、BeanFactory 通常以编程的方式被创建,ApplicationContext 可以以声明的方式创建,如使用 ContextLoader。

3、BeanFactory 和 ApplicationContext 都支持 BeanPostProcessor,BeanFactoryPostProcessor,但 BeanFactory 需要手动注册,ApplicationContext 则是自动注册。

119、什么是 JavaConfig?

JavaConfig 是 Spring3.0 新增的概念,就是以注解的形式取代 Spring 中繁琐的 xml 文件。

JavaConfig 结合了 xml 的解耦和 java 编译时检查的优点。

  1. @Configuration,表示这个类是配置类;
  2. @ComponentScan,相当于 xml 的 <context:componentScan basepackage=>;
  3. @Bean,相当于 xml 的 ;
  4. @EnableWebMvc,相当于 xml 的 mvc:annotation-driven;
  5. @ImportResource,相当于 xml 的 ;
  6. @PropertySource,用于读取 properties 配置文件;
  7. @Profile,一般用于多环境配置,激活时可用 @ActiveProfile(“dev”) 注解;

120、什么是 ORM 框架?

ORM(Object-relational mapping),对象关系映射。

是为了解决面向对象与关系型数据库存在的不匹配问题。

ORM 框架的优点:

  1. 开发效率更高
  2. 数据访问更抽象、轻便
  3. 支持面向对象封装

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  />
        <property  />
        <property  />
     </bean>
 
    <bean id="jm" class="com.tyq.dto.Dog">
        <property  />
        <property  />
        <property  />
    </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 的生命周期?

  1. 通过构造器或工厂方法创建 bean 实例;
  2. 为 bean 的属性赋值;
  3. 调用 bean 的初始化方法;
  4. 使用 bean;
  5. 当容器关闭时,调用 bean 的销毁方法;

123、Spring Bean 的作用域之间有什么区别?
Spring 容器中的 bean 可以分为 5 个范围:

  1. singleton:这种 bean 范围是默认的,这种范围确保不管接受多少请求,每个容器中只哟一个 bean 的实例,单例模式;
  2. prototype:为每一个 bean 提供一个实例;
  3. request:在请求 bean 范围内为每一个来自客户端的网络请求创建一个实例,在请求完毕后,bean 会失效并被垃圾回收器回收;
  4. session:为每个 session 创建一个实例,session 过期后,bean 会随之消失;
  5. 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 >
		<bean class="com.person">
			<property  value=1 />
			<property  />
			<property  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” 这个异常。

132、Spring 常用注解

Spring 常用注解(绝对经典)

133、项目中是如何实现权限验证的,权限验证需要几张表

通过了解,现在最普遍的权限管理模型就是 RBAC(Role-Based Access Control)。

1、权限控制分类

菜单功能
url 控制(控制访问不同的控制器)
2、RBAC 的优缺点

(1)优点

简化了用户和权限的关系
易扩展、易维护

(2)缺点

RBAC 模型没有提供操作顺序的控制机制,这一缺陷使得 RBAC 模型很难适应哪些对操作次序有严格要求的系统。

3、RBAC 支持的安全原则

(1)最小权限原则

RBAC 可以将角色配置成其完成任务所需的最小权限集合。

(2)责任分离原则

可以通过调用相互独立互斥的角色来共同完成敏感的任务,例如要求一个记账员和财务管理员共同参与统一过账操作。

(3)数据抽象原则

可以通过权限的抽象来体现,例如财务操作用借款、存款等抽象权限,而不是使用典型的读写权限。

4、远古时代的权限控制

当时还没有 RBAC,也没有这个概念,就是一堆程序员在那鼓捣,觉得登录这块该做点什么。

1、新建一个用户,对这个用户进行赋予权限。

2、但是一旦用户多了,权限复杂了,这工作量也是蛮大的。

5、RBAC

RBAC 1.0

直接上图,一目了然,当程序不是很复杂的时候,RBAC 就是这样设计的,我们公司的权限验证模块就是这样设计的。

简简单单,五张表,解

RBAC 2.0

基于 RBAC 1.0 模型的基础上,进行了角色的访问控制

RBAC2 中的一个基本限制是互斥角色的限制,互斥角色是指各自权限可以互相制约的两个角色。对于这类角色一个用户在某一次活动中只能被分配其中的一个角色,不能同时获得两个角色的使用权。

该模型有以下几种约束

  • 互斥角色 :同一用户只能分配到一组互斥角色集合中至多一个角色,支持责任分离的原则。互斥角色是指各自权限互相制约的两个角色。对于这类角色一个用户在某一次活动中只能被分配其中的一个角色,不能同时获得两个角色的使用权。常举的例子:在审计活动中,一个角色不能同时被指派给会计角色和审计员角色。
  • 基数约束 :一个角色被分配的用户数量受限;一个用户可拥有的角色数目受限;同样一个角色对应的访问权限数目也应受限,以控制高级权限在系统中的分配。
  • 先决条件角色 :可以分配角色给用户仅当该用户已经是另一角色的成员;对应的可以分配访问权限给角色,仅当该角色已经拥有另一种访问权限。指要想获得较高的权限,要首先拥有低一级的权限。
  • 运行时互斥 :例如,允许一个用户具有两个角色的成员资格,但在运行中不可同时激活这两个角色。

6、rbac 的实现理论分析

进入登录页面;
拿到通过 post 传过来的用户名和密码;
使用 orm 进行过滤查找;
如果能找到值,则说明登录成功:登录成功后调用 rbac 初始化函数,初始化函数的主要功能是获取用户的权限和菜单保存到 session 中,并跳转客户列表页面;如果失败,页面进行友好提示;

7、url 权限控制关键代码

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 可以实现模糊匹配路径,比如:

  1. ?表示一个字符;
    • 表示任意字符;
  2. ** 匹配多层路径;

/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?user>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 注入有四种方式,

  1. set 注入;
  2. 构造器注入;
  3. 基于注解的注入;
  4. xml 配置文件注入;

想要注入 java collection,就是注入集合类:

  1. list
  2. set
  3. map
  4. props:该标签支持注入键和值都是字符串类型的键值对。

list 和 set 都使用 value 标签;map 使用 entry 标签;props 使用 prop 标签;

138、mybatis 中 #{} 和 ${} 的区别是什么?

  1. #{} 带引号,${} 不带引号;
  2. #{} 可以防止 SQL 注入;
  3. ${} 常用于数据库表名、order by 子句;
  4. 一般能用 #{} 就不要使用 ${};

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、如何选择

  1. mybatis 需要编写 sql 和映射规则,工作量大于 hibernate;
  2. mybatis 支持的工具也有限,不能像 hibernate 那样有许多插件可以帮助生成映射代码和关联关系;
  3. 对于性能要求不太苛刻的系统,比如管理系统、ERP 等推荐 hibernate;
  4. 对于性能要求高、响应快、灵活的系统,比如电商系统,推荐使用 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 进行的,具体的步骤:

  1. 定义一个 configuration 对象,其中包含数据源、事务、mapper 文件资源以及影响数据库行为属性设置 settings。
  2. 通过配置对象,则可以创建一个 sqlSessionFactoryBuilder 对象。
  3. 通过 sqlSessionFactoryBuilder 获得 sqlSessionFactory 实例。
  4. 通过 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 ${mysql.driver}"/>  
                <property ${mysql.url}"/>  
                <property ${mysql.username}"/>  
                <property ${mysql.password}"/>  
            </dataSource>  
        </environment>  
 
        <!-- 连接环境信息,取一个任意唯一的名字 -->  
        <environment id="oracle_developer">  
            <!-- mybatis使用jdbc事务管理方式 -->  
            <transactionManager type="jdbc"/>  
            <!-- mybatis使用连接池方式来获取连接 -->  
            <dataSource type="pooled">  
                <!-- 配置与数据库交互的4个必要属性 -->  
                <property ${oracle.driver}"/>  
                <property ${oracle.url}"/>  
                <property ${oracle.username}"/>  
                <property ${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?

  1. hibernate 对 jdbc 进行了封装,简化了 JDBC 的重复性代码;
  2. hibernate 对 dao 有一个封装类 hibernateTemplate,可以继承它,实现简单的 CRUD 接口。
  3. hibernate 使用注解和配置文件,可以对实体类和映射文件进行映射;
  4. hibernate 有事务管理机制,保证了数据的安全性;
  5. 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 查询语句。

它具备以下功能:

  1. 在查询语句中设定各种查询条件;
  2. 支持投影查询,即仅检索出对象的部分属性;
  3. 支持分页查询;
  4. 支持连接查询;
  5. 支持分组查询;
  6. 提供内置函数;
  7. 能够调用用户自定义的 SQL 函数;
  8. 支持子查询;
  9. 支持动态绑定参数;

例如:

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 有哪些优点?

  1. 快速构建项目,可以选一些必要的组件;
  2. 对主流框架的无配置集成;
  3. 内嵌 Tomcat 容器,项目可独立运行;
  4. 删除了繁琐的 xml 配置文件;
  5. 极大地提高了开发和部署效率;
  6. 提供 starter,简化 maven 配置;

3、SpringBoot 有哪些缺点?

  1. 版本迭代速度快,一些模块改动很大;
  2. 由于无须配置,报错时很难定位;

151、Spring Boot 中的监视器是什么?

监听器也叫 listener,是 servlet 的监听器,可以用于监听 web 应用程序中某些对象的创建、销毁、增加、修改、删除等动作的发生,然后做出相应的响应处理。当范围对象的状态发生变化时,服务器自动调用监听器对象中的方法,常用于系统加载时进行信息初始化,统计在线人数和在线用户,统计网站的访问量。

配置监听器的方法:

通过 @Component 把监听器加入 Spring 容器中管理;
在 application.properties 中添加 context.listener.classes 配置;
在方法上加 @EventListener 注解;

152、什么是 YAML?

YAML 是 JSON 的一个超集,可以非常方便地将外部配置以层次结构形式存储起来。YAML 可以作为 properties 配置文件的替代。

YAML 使用的注意事项:

  1. 在 properties 文件中是以 “.” 进行分割的,在 yml 中是用 “.” 进行分割的;
  2. yml 的数据格式和 json 的格式很像,都是 K-V 格式,并且通过 “:” 进行赋值;
  3. 每个冒号后面一定要加一个空格;

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 多哪些注解

Spring Boot 常用注解(绝对经典)

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 的区别是什么?

  1. char 的长度是固定的,varchar 的长度的可变的;
  2. char 的效率比 varchar 的效率高;
  3. 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、索引怎么定义,分哪几种

  1. b-tree 索引,如果不建立索引的情况下,oracle 就自动给每一列都加一个 B 树索引;
  2. normal:普通索引
  3. unique:唯一索引
  4. bitmap:位图索引,位图索引特定于只有几个枚举值的情况,比如性别字段;
  5. 基于函数的索引

175、mysql 的内连接、左连接、右连接有什么区别?

  1. 内连接, 显示两个表中有联系的所有数据;
  2. 左链接, 以左表为参照, 显示所有数据, 右表中没有则以 null 显示
  3. 右链接, 以右表为参照显示数据,, 左表中没有则以 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 基类来创建自己的类加载器。

通过使用不同的类加载器可以从不同来源加载类的二进制数据,通常有如下几种来源:

  1. 从本地文件系统加载
  2. 从 jar 包加载
  3. 通过网络加载
  4. 把一个 Java 源文件动态编译,并执行加载

2、连接

当类被加载之后,系统为之生成一个对应的 Class 对象,接着进入连接阶段,连接阶段负责将类的二进制数据合并到 JRE 中。

类连接又可分为三个阶段:

(1)验证

文件格式验证
元数据验证
字节码验证
符号引用验证

(2)准备

为类的静态变量分配内存,并设置默认初始值。

(3)解析

将类的二进制数据中的符号引用替换成直接引用。

3、初始化

为类的静态变量赋予初始值。

183、JVM 的类加载机制是什么?

JVM 类加载机制主要有三种:

1、全盘负责

类加载器加载某个 class 时,该 class 所依赖的和引用其它的 class 也由该类加载器载入。

2、双亲委派

先让父加载器加载该 class,父加载器无法加载时才考虑自己加载。

3、缓存机制

缓存机制保证所有加载过的 class 都会被缓存,当程序中需要某个 class 时,先从缓存区中搜索,如果不存在,才会读取该类对应的二进制数据,并将其转换成 class 对象,存入缓存区中。

这就是为什么修改了 class 后,必须重启 JVM,程序所做的修改才会生效的原因。

184、什么是双亲委派模型?

如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器执行,如果父加载器还存在其父加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器,如果父类加载器可以完成父加载任务,就成功返回,如果父加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型。

双亲委派模式的优势:

  1. 避免重复加载;
  2. 考虑到安全因素,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 何时会被回收?

  1. 堆中不存在该类的任何实例
  2. 加载该类的 classLoader 已经被回收
  3. 该类的 java.lang.class 对象没有在任何地方被引用,也就是说无法通过反射访问该类的信息

(3)本地方法栈引用的对象

186、说一下 jvm 有哪些垃圾回收算法?

1、对象是否已死算法

  • 引用计数器算法
  • 可达性分析算法

2、GC 算法

(1)标记清除算法

如果对象被标记后进行清除,会带来一个新的问题 – 内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。

(2)复制算法(Java 堆中新生代的垃圾回收算法)

  1. 先标记待回收内存和不用回收内存;
  2. 将不用回收的内存复制到新的内存区域;
  3. 就的内存区域就可以被全部回收了,而新的内存区域也是连续的;

缺点是损失部分系统内存,因为腾出部分内存进行复制。

(3)标记压缩算法(Java 堆中老年代的垃圾回收算法)

对于新生代,大部分对象都不会存活,所以复制算法较高效,但对于老年代,大部分对象可能要继续存活,如果此时使用复制算法,效率会降低。

标记压缩算法首先还是标记,将不用回收的内存对象压缩到内存一端,此时即可清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。

老年代的垃圾回收算法称为 “Major GC”。

187、说一下 jvm 有哪些垃圾回收器?

说一下 jvm 有哪些垃圾回收器?

188、JVM 栈堆概念,何时销毁对象

  1. 类在程序运行的时候就会被加载,方法是在执行的时候才会被加载,如果没有任何引用了,Java 自动垃圾回收,也可以用 System.gc() 开启回收器,但是回收器不一定会马上回收。
  2. 静态变量在类装载的时候进行创建,在整个程序结束时按序销毁;
  3. 实例变量在类实例化对象时创建,在对象销毁的时候销毁;
  4. 局部变量在局部范围内使用时创建,跳出局部范围时销毁;

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 ;

垃圾回收器的执行流程:

  1. 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  2. 清空 Eden + From Survivor 分区,From Survivor 和 To Survivor 分区交换;
  3. 每次交换后存活的对象年龄 + 1,到达 15,升级为老年代,大对象会直接进入老年代;
  4. 老年代中当空间到达一定占比,会触发全局回收,老年代一般采取标记 - 清除算法;

192、Redis 是什么?

Redis 是一个 key-value 存储系统,它支持存储的 value 类型相对更多,包括 string、list、set、zset(sorted set – 有序集合)和 hash。这些数据结构都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis 支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中,Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了 master-slave(主从)同步。

193、Redis 都有哪些使用场景?

  1. Redis 是基于内存的 nosql 数据库,可以通过新建线程的形式进行持久化,不影响 Redis 单线程的读写操作
  2. 通过 list 取最新的 N 条数据
  3. 模拟类似于 token 这种需要设置过期时间的场景
  4. 发布订阅消息系统
  5. 定时器、计数器

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 支持的数据类型有哪些?

  1. 字符串
  2. hash
  3. list
  4. set
  5. 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 为什么是单线程的?

  1. 代码更清晰,处理逻辑更简单;
  2. 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;
  3. 不存在多线程切换而消耗 CPU;
  4. 无法发挥多核 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、如何解决?

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其它线程等待;
  2. 做二级缓存;
  3. 不同的 key,设置不同的过期时间,让缓存失效的时间尽量均匀;

204、怎么保证缓存和数据库数据的一致性?

1、淘汰缓存

数据如果为较为复杂的数据时,进行缓存的更新操作就会变得异常复杂,因此一般推荐选择淘汰缓存,而不是更新缓存。

2、选择先淘汰缓存,再更新数据库

假如先更新数据库,再淘汰缓存,如果淘汰缓存失败,那么后面的请求都会得到脏数据,直至缓存过期。

假如先淘汰缓存再更新数据库,如果更新数据库失败,只会产生一次缓存穿透,相比较而言,后者对业务则没有本质上的影响。

3、延时双删策略

如下场景:同时有一个请求 A 进行更新操作,另一个请求 B 进行查询操作。

  1. 请求 A 进行写操作,删除缓存
  2. 请求 B 查询发现缓存不存在
  3. 请求 B 去数据库查询得到旧值
  4. 请求 B 将旧值写入缓存
  5. 请求 A 将新值写入数据库

次数便出现了数据不一致问题。采用延时双删策略得以解决。

public void write(String key,Object data){
    redisUtils.del(key);
    db.update(data);
    Thread.Sleep(100);
    redisUtils.del(key);
}

这么做,可以将 1 秒内所造成的缓存脏数据,再次删除。这个时间设定可根据俄业务场景进行一个调节。

4、数据库读写分离的场景

两个请求,一个请求 A 进行更新操作,另一个请求 B 进行查询操作。

  1. 请求 A 进行写操作,删除缓存
  2. 请求 A 将数据写入数据库了,
  3. 请求 B 查询缓存发现,缓存没有值
  4. 请求 B 去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
  5. 请求 B 将旧值写入缓存
  6. 数据库完成主从同步,从库变为新值

依旧采用延时双删策略解决此问题。

205、Redis,什么是缓存穿透?怎么解决?

1、缓存穿透

一般的缓存系统,都是按照 key 去缓存查询,如果不存在对用的 value,就应该去后端系统查找(比如 DB 数据库)。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

2、怎么解决?

对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 之后清理缓存。
 
对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 Bitmap 过滤。

3、缓存雪崩

当缓存服务器重启或者大量缓存集中在某一时间段失效,这样在失效的时候,会给后端系统带来很大的压力,导致系统崩溃。

4、如何解决?

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其它线程等待;
  2. 做二级缓存;
  3. 不同的 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 容易产生的几个问题:

  1. 锁未被释放
  2. B 锁被 A 锁释放了
  3. 数据库事务超时
  4. 锁过期了,业务还没执行完
  5. Redis 主从复制的问题

208、Redis 如何做内存优化?

1、缩短键值的长度

  1. 缩短值的长度才是关键,如果值是一个大的业务对象,可以将对象序列化成二进制数组;
  2. 首先应该在业务上进行精简,去掉不必要的属性,避免存储一些没用的数据;
  3. 其次是序列化的工具选择上,应该选择更高效的序列化工具来降低字节数组大小;
  4. 以 JAVA 为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo 等

2、共享对象池

对象共享池指 Redis 内部维护 [0-9999] 的整数对象池。创建大量的整数类型 redisObject 存在内存开销,每个 redisObject 内部结构至少占 16 字节,甚至超过了整数自身空间消耗。所以 Redis 内存维护一个 [0-9999] 的整数对象池,用于节约内存。 除了整数值对象,其他类型如 list,hash,set,zset 内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

3、字符串优化

4、编码优化

5、控制 key 的数量

🍅 作者简介:哪吒,CSDN2021 博客之星亚军🏆、新星计划导师✌、博客专家💪

🍅 哪吒多年工作总结:Java 学习路线总结,搬砖工逆袭 Java 架构师

🍅  技术交流:定期更新 Java 硬核干货,不定期送书活动

🍅 关注公众号【哪吒编程】,回复 面试题 ,获取《10 万字 208 道 Java 经典面试题总结 (附答案)》pdf,背题更方便,一文在手,面试我有

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值