工作也有好些年了,从刚毕业到前几年看过无数的面试题,在这个过程中也作为面试官面试过其他人,总想着自己写一个面试总结,随着自我认识的变化,一些知识点的理解也越来越不一样了。写下来温故而知新。很多问题可能别人也总结过,但是答案不尽相同,如有问题欢迎指正以备完善。
现在的问题还不是很全面,会持续更新,大家有遇到一些面试题的话,也可以评论出来,一起完善。
文章目录
- 1. JVM、JDK及JRE的区别
- 2. static的独特之处?
- 3. final修饰符的作用:
- 4.==与equals的区别
- 5. String str="i"与 String str=new String(“i”)
- 6. 如何将字符串翻转?
- 7. 普通类和抽象类的区别
- 8. 接口与抽象类的区别
- 9. JAVA中IO流分类及区别
- 10. 什么是反射?
- 11. 什么是,为什么要,怎么实现序列化?
- 12. throw与throws的异同
- 13. try-catch-finally-return的执行顺序
- 14. 什么是,为什么要有hashcode,hashcode与equals关系
- 15. 在 Java 中,为什么不允许从静态方法中访问非静态变量?
- 16. 实例化对象的方式
- 17. Bean的生命周期
- 18. bean初始化执行是顺序
- 19. HashMap与HashTable的区别
- 20. HashMap put及get的实现原理
- 21. HashMap相关面试题
- 22. HashSet实现原理
- 23. Java中如何确保一个集合不会被修改
- 24. HashMap 在 JDK7 和 JDK8 有哪些区别?
- 25. HashMap的线程不安全体现在哪儿,如何变成线程安全?
- 26. ConcurrentHashMap 在 JDK7 和 JDK8的区别?
- 27. concurrentHashMap和HashTable有什么区别
- 28. ArrayList与LinkedList的区别
- 29. HashMap与HashSet的区别
- 30. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法
- 31. 线程的run与start的区别
- 32. 线程的wait和sleep的区别
- 33. synchronized 这三种加锁方式(作用对象:静态方法、非静态方法、代码块)作用范围的区别?
- 34. Synchronized修饰的方法在抛出异常时,会释放锁吗?
- 35. Synchronized是公平锁还是非公平锁?
- 36. 如何提高Synchronized的并发性能?
- 37. 说下对volatile关键字的理解,使用场景是?
- 38. Volatile和Synchronized区别?
- 39. 什么是指令重排
- 40. volatile如何保证有序性?
- 41. 实现线程同步的方式?
- 42. Synchronized与ReentrantLock的区别?
- 43. 为什么要用线程池?
- 44. 线程池的创建方式有哪些?
- 45. ThreadPoolExecutor的核心参数含义是?
- 46. 线程池创建的线程会一直运行吗?
- 47. 如何向线程池中提交任务?
- 48. 如何关闭线程池?
- 49. 什么是ThreadLocal?有哪些应用场景?
- 50. 简单说下ThreadLocal原理
- 51. ThreadLocal为什么会内存溢出,如何避免?
- 52. ThreadLocalMap Entry的key为什么设计成弱引用?
- 53. CountDownLatch的用法,使用场景
- 54. 如何让多个线程顺序执行?
- 55. Cookie与Session是什么,区别是?
- 56. Http常见的响应码有哪些?
- 57. XSS与CSRF分别是什么,区别是?
- 58. Websocket与Http的异同?
- 59. 什么是设计模式?常用的设计模式在实际使用场景是什么?
- 60. 什么是spring AOP?
- 61. springAOP的实际应用场景?
- 62. SpringAOP的实现原理是?
- 63. jdk动态代理与cglib动态代理的优缺点?
- 64. Beanfactory和Applicationcontext的区别?
- 65. spring中支持几种bean的作用域?
- 66. @Autowired与@Resource的区别?
- 67. spring框架中的单例bean是线程安全的吗?
- 68. springboot的核心配置文件是什么?
- 69. JPA实体类相关注解有哪些?
- 70. 拦截器、过滤器及AOP的执行顺序?
- 71. 拦截器与过滤器的区别?
- 72. 为什么使用rabbitMQ?
- 73. RabbitMQ有哪几种交换机类型?
- 74. 如何避免RabbitMQ可能存在的消息丢失问题?
- 75. RabbitMQ中 vhost 的作用是什么?
- 76. Redis的默认数据库有几个?
- 77. Spring boot集成redis是使用的哪个依赖?有什么区别?
- 78. 为什么选择redis作为缓存?
- 79. Redis和Caffeine的区别是什么?
- 80. Redis的基础数据结构有哪些?
- 81. 简单说下Redis的过期清除方式及缓存淘汰策略?
- 82. 为什么Redis不支持回滚?
- 83. 如何做的Redis持久化?详细说一下持久化方式有哪些?
- 84. 说一下Redis的缓存穿透与缓存雪崩是什么?如何解决?
- 85. Redis6.x 之后为何引入了多线程?
- 86. Redis分布式锁的实现?有什么需要注意的?
- 87. Docker容器是什么?
- 88. 如何从Docker镜像中创建一个Docker容器?
- 89. Docker仓库、镜像及容器的关系?
- 90. 说下常用的Docker命令?
1. JVM、JDK及JRE的区别
Jdk中包括了Jre,Jre中包括了JVM.
- JDK(Java development kit)Java开发工具包,JRE(Java runtime environment)Java运行环境。JDK中包含JRE,JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM工作所需要的类库。
- JRE包含了java虚拟机、java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。JDK是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。运行java程序只需安装JRE。如果需要编写java程序,需要安装JDK。
- JVM即Java虚拟机,是一种抽象计算机,它有一个指令集,在运行时操作各种内存区域。虚拟机有很多种,不同厂商提供了不同实现,只要遵循虚拟机规范即可,目前我们所说的虚拟机一般指的是Hot Spot。JVM对Java语言一无所知,只知道一种特定的二进制格式,即类文件格式,我们写好的程序最终交给JVM执行的时候会被编译成二进制格式,JVM只认识二进制格式,所以任何语言只要编译后的格式符合要求,都可以在JVM上运行。
2. static的独特之处?
- 被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
- 怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】。
- 在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
- static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
- 被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
3. final修饰符的作用:
- 修饰一个引用
- 如果引用为基本数据类型,则该引用为常量,该值无法修改;
- 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
- 如果引用是类的成员变量,则必须当场赋值,否则编译会报错。
- 用来修饰一个方法
- 当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
- 用来修饰类
- 当用final修改类时,该类成为最终类,无法被继承。比如常用的String类就是最终类。
4.==与equals的区别
- “==”是运算符
①如果比较的对象是基本数据类型,则比较的是其存储的值是否相等;
②如果比较的是引用数据类型,则比较的是所指向对象的地址值是否相等(是否是同一个对象)。 - equals是Object的方法,用来比较两个对象的内容是否相等。equals 方法不能用于比较基本数据类型,如果没有对 equals 方法进行重写,则相当于“==”,比较的是引用类型的变量所指向的对象的地址值。
一般情况下,类会重写equals方法用来比较两个对象的内容是否相等。比如String类中的equals()是被重写了,比较的是对象的值。
5. String str="i"与 String str=new String(“i”)
不一样,String str="i"会将其分配到常量池中,常量池中没有重复的元素,如果常量池中有i,就将i的地址赋给变量,如果没有就创建一个再赋给变量。String str=new String(“i”)会将对象分配到堆中,即使内存一样,还是会重新创建一个新的对象。
6. 如何将字符串翻转?
这儿提供两种方式:stringbuilder.reverse及char[]。
public static void main(String[] args) {
String abc = "hello world";
StringBuilder stringBuilder = new StringBuilder(abc);
String reverse = stringBuilder.reverse().toString();
System.out.println(reverse);
}
public static void main(String[] args) {
String abc = "hello world";
char[] chars = abc.toCharArray();
char[] newd = new char[abc.length()];
for (int i = 0; i < chars.length; i++) {
char aChar = chars[chars.length - i - 1];
newd[i]=aChar;
}
String s = String.valueOf(newd);
System.out.println(s);
}
7. 普通类和抽象类的区别
- 抽象类的存在是为了被继承,不能实例化,而普通类存在是为了实例化一个对象。
- 抽象类的子类必须重写抽象类中的抽象方法,而普通类可以选择重写父类的方法,也可以直接调用父类的方法。
- 抽象类必须用abstract来修饰,普通类则不用。
- 普通类和抽象类都可以含有普通成员属性和普通方法。
- 普通类和抽象类都可以继承别的类或者被别的类继承。
- 普通类和抽象类的属性和方法都可以通过子类对象来调用。
8. 接口与抽象类的区别
- 抽象类和接口都不能直接实例化。如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
- 抽象类要被子类继承,接口要被类实现。
- 接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现。
- 接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
- 抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
- 抽象方法只能申明,不能实现。
- 抽象类里可以没有抽象方法
- 如果—个类里有抽象方法,那么这个类只能是抽象类
- 抽象方法要被实现,所以不能是静态的,也不能是私有的。
9. JAVA中IO流分类及区别
按照数据的流向:输入流:读数据。输出流:写数据
按照数据类型来分:
字节流:字节输入流(Inputstream),字节输出流(Outputstream)
字符流:字符输入流(Reader),字符输出流(Writer)
- 字符流和字节流是根据处理数据的类型的不同来区分的。
- 字节流按照8位传输,字节流是最基本的,所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。
- 字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;
- 字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。
- 理论上任何文件都能够用字节流读取,但当读取的是文本数据时,为了能还原成文本你必须再经过一个转换的工序,相对来说字符流就省了这个麻烦,可以有方法直接读取。所以,如果是处理纯文本数据,就要优先考虑字符流,除此之外都是用字节流。
10. 什么是反射?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法,所以先要获取到每一个字节码文件对应的Class类型的对象。
反射就是把java类中的各种成分映射成一个个的Java对象。
反射这儿比较重要,再次不详细展开,可查看相关链接:
Java基础-反射
11. 什么是,为什么要,怎么实现序列化?
如果我们需要持久化Java对象比如将Java对象保存在文件中,或者在网络传输Java对象,这些场景都需要用到序列化。简单来说:
序列化: 将数据结构或对象转换成二进制字节流的过程。
反序列化:将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程。
常见的序列化实现方式为:JDK 自带的序列化,只需实现 java.io.Serializable接口即可。
序列化号 serialVersionUID 属于版本控制的作用。序列化的时候serialVersionUID也会被写入二级制序列,当反序列化时会检查serialVersionUID是否和当前类的serialVersionUID一致。如果serialVersionUID不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的序列化号。日常使用指定为1L即可。
12. throw与throws的异同
较为基础的问题,日常开发过程中会较多用到的异常处理方式。
- 不同点:
位置不同。throws用在函数上,后边跟的是异常类,可以跟多个异常类。throw用在函数内,后面跟的是异常对象。
功能不同。①throws用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先得处理方式。throw抛出具体的问题对象,执行到throw。功能就已经结束了跳转到调用者,并将具体的问题对象抛给调用者,也就是说throw语句独立存在时,下面不要定义其他语句,因为执行不到。②throws表示出现异常的一种可能性,并不一定会发生这些异常,throw则是抛出了异常,执行throw则一定抛出了某种异常对象。 - 相同点:
两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
13. try-catch-finally-return的执行顺序
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
int abc = 1/0;
System.out.println("try");
return 0;
} catch (Exception e) {
System.out.println(e.getMessage());
return 1;
} finally {
System.out.println("finally");
}
}
/ by zero
finally
1
14. 什么是,为什么要有hashcode,hashcode与equals关系
- hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义 在 JDK 的 Object.java 中,这就意味着Java 中的任何类都包含有 hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - 为什么要有hashcode
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。
如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。当我们对比两个对象是否相等时,我们就可以先使用 hashCode 进行比较,如果比较的结果是 true,那么就可以使用 equals再次确认两个对象是否相等,如果比较的结果是true,那么这两个对象就是相等的,否则其他情况就认为两个对象不相等。这样就大大的提升了对象比较的效率,这也是为什么 Java 设计使用hashCode 和 equals 协同的方式,来确认两个对象是否相等的原因。
- 如果equals为true,hashcode一定相等(没有重写equals的情况下);
如果equals为false,hashcode不一定不相等;
如果hashcode值相等,equals不一定相等;
如果hashcode值不等,equals一定不等(没有重写equals的情况下);
15. 在 Java 中,为什么不允许从静态方法中访问非静态变量?
- 静态变量属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问;
- 非静态变量属于类的对象,只有在类的对象产生时,才会分配内存,通过类的实例去访问;
- 静态方法也属于类本身,但是此时没有类的实例,内存中没有非静态变量,所以无法调用非静态变量。
16. 实例化对象的方式
-
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();
17. Bean的生命周期
- Spring 对bean 进行实例化。
- Spring 将值和bean的引用注入到bean对应的属性中。
- 如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name() 方法。
- 如果bean 实现了BeanFactoryAware接口,Spring将调用setBeanFactory() 方法,将BeanFactory容器实例传入。
- 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext() 方法,将bean所在的应用上下文的引用传入进来。
- 如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization() 方法
- 如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用。
- 如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization() 方法。
- 此时, bean 已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁。
- 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。
18. bean初始化执行是顺序
可以参考我之前写的文章:初始化执行顺序
19. HashMap与HashTable的区别
这个知识点比较老旧,HashTable在工作中基本没用到过。但是还是写出来了,仅限于了解知道这个事儿即可,现在面试基本不会问。
- Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现。
- Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的。
- HashMap可以让你将空值作为一个表的条目的key或value。
- HashMap的默认容器是16,为2倍扩容,HashTable默认是11,为2倍+1扩容。
20. HashMap put及get的实现原理
- HashMap是基于哈希表的Map接口的非同步实现。元素以键值对的形式存放,并且允许null键和null值,因为key值唯一(不能重复),因此,null键只有一个。另外,hashmap不保证元素存储的顺序,是一种无序的,和放入的顺序并不相同(此类不保证映射的顺序,特别是它不保证该顺序恒久不变)。HashMap是线程不安全的。
- map.put(k,v)实现原理:
首先将k,v封装到Node对象当中(节点)。
然后它的底层会调用K的hashCode()方法得出hash值。
通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。 - map.get(k)实现原理:
先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
21. HashMap相关面试题
- HashMap内部的bucket数组长度为什么一直都是2的整数次幂答:这样做有两个好处,第一,可以通过(table.length - 1) & key.hash()这样的位运算快速寻址,第二,在HashMap扩容的时候可以保证同一个桶中的元素均匀地散列到新的桶中,具体一点就是同一个桶中的元素在扩容后一般留在原先的桶中,一般放到了新的桶中。
- HashMap默认的bucket数组是多大答:默认是16,即时指定的大小不是2的整数次幂,HashMap也会找到一个最近的2的整数次幂来初始化桶数组。
- HashMap什么时候开辟bucket数组占用内存答:在第一次put的时候调用resize方法。
- HashMap何时扩容?答:当HashMap中的元素熟练超过阈值时,阈值计算方式是capacity * loadFactor,在HashMap中loadFactor(负载因子)是0.75。
- 桶中的元素链表何时转换为红黑树,什么时候转回链表,为什么要这么设计?答:当同一个桶中的元素数量大于等于8的时候元素中的链表转换为红黑树,反之,当桶中的元素数量小于等于6的时候又会转为链表,这样做的原因是避免红黑树和链表之间频繁转换,引起性能损耗。
- Java 8中为什么要引进红黑树,是为了解决什么场景的问题?答:引入红黑树是为了避免hash性能急剧下降,引起HashMap的读写性能急剧下降的场景,正常情况下,一般是不会用到红黑树的,在一些极端场景下,假如客户端实现了一个性能拙劣的hashCode方法,可以保证HashMap的读写复杂度不会低于O(lgN)public int hashCode() {
return 1;
} - HashMap如何处理key为null的键值对?答:放置在桶数组中下标为0的桶中。
22. HashSet实现原理
HashSet是Set的实现,HashSet底层其实是一个HashMap实例,都是一个存放链表的数组。
HashSet是基于HashMap实现的,HashSet中所有的元素都存放在HashMap的key上,而value中的值都是统一的一个固定的对象:private static final Object PRESENT = new Object(); HashSet的add方法:
HashSet中add方法调用的是底层HashMap的put方法。如果是在HashMap中调用put方法,首先会去判断key是否已经存在,如果存在,则修改value的值,如果不存在,则插入这个k-v对。而在Set中,value是没有用的,所以也就不存在修改value的情况,故而,向HashSet中添加新的元素,首先判断元素是否存在,不存在则插入,存在则pass,这样HashSet中就不存在重复值了。
所以,判断key是否存在就需要去重写元素类的equals()和hashCode()方法。当向Set中添加元素的时候,先调用元素所在类的hashCode()方法,计算元素对象的哈希值,这个哈希值决定了这个元素在Set中存放的位置,如果这个位置是空的,没有存放其他元素,那么就直接把这个元素存放在这里;如果这个位置已经被别人占了,那么就调用元素所在类的equals()方法比较两个对象是否相同,相同就直接pass掉,保证了元素的不可重复性。
所以,在使用HashMap和HashSet的时候,如果Map的key或者Set中要存入自定义类的对象,必须重写hashCode和equals方法。
23. Java中如何确保一个集合不会被修改
我们很容易想到用final关键字进行修饰,我们都知道
final关键字可以修饰类,方法,成员变量,final修饰的类不能被继承,final修饰的方法不能被重写,final修饰的成员变量必须初始化值,如果这个成员变量是基本数据类型,表示这个变量的值是不可改变的,如果说这个成员变量是引用类型,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的。
那么,我们怎么确保一个集合不能被修改?首先我们要清楚,集合(map,set,list…)都是引用类型,所以我们如果用final修饰的话,集合里面的内容还是可以修改的。
那我们应该怎么做才能确保集合不被修改呢?
我们可以采用Collections包下的unmodifiableMap方法,通过这个方法返回的map,是不可以修改的。他会报 java.lang.UnsupportedOperationException错。
同理:Collections包也提供了对list和set集合的方法。Collections.unmodifiableList(List) Collections.unmodifiableSet(Set)
24. HashMap 在 JDK7 和 JDK8 有哪些区别?
- 数据结构:在 JDK7 及之前的版本,HashMap 的数据结构可以看成“数组+链表”,在 JDK8 及之后的版本,数据结构可以看成"数组+链表+红黑树",当链表的长度超过8时,链表就会转换成红黑树,从而降低时间复杂度(由O(n) 变成了 O(logN)),提高了效率
- 对数据重哈希:JDK8 及之后的版本,对 hash() 方法进行了优化,重新计算 hash 值时,让 hashCode 的高16位参与异或运算,目的是在 table 的 length较小的时候,在进行计算元素存储位置时,也让高位也参与运算。
- 在 JDK7 及之前的版本,在添加元素的时候,采用头插法,所以在扩容的时候,会导致之前元素相对位置倒置了,在多线程环境下扩容可能造成环形链表而导致死循环的问题。DK1.8之后使用的是尾插法,扩容是不会改变元素的相对位置
- 扩容时重新计算元素的存储位置的方式:JDK7 及之前的版本重新计算存储位置是直接使用 hash & (table.length-1);JDK8 使用节点的hash值与旧数组长度进行位与运算,如果运算结果为0,表示元素在新数组中的位置不变;否则,则在新数组中的位置下标=原位置+原数组长度。
- JDK7 是先扩容后插入,这就导致无论这次插入是否发生hash冲突都需要进行扩容,但如果这次插入并没有发生Hash冲突的话,那么就会造成一次无效扩容;JDK8是先插入再扩容的,优点是减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容
25. HashMap的线程不安全体现在哪儿,如何变成线程安全?
无论在JDK7还是JDK8的版本中,HashMap 都是线程不安全的,主要体现在以下两个方面:
- 在JDK7及以前的版本,表现为在多线程环境下进行扩容,由于采用头插法,位于同一索引位置的节点顺序会反掉,导致可能出现死循环的情况。
- 在JDK8及以后的版本,表现为在多线程环境下添加元素,可能会出现数据丢失的情况。
如果想使用线程安全的 Map 容器,可以使用以下几种方式:
- 使用线程安全的 Hashtable,它底层的每个方法都使用了 synchronized 保证线程同步,所以每次都锁住整张表,在性能方面会相对比较低。
- 使用Collections.synchronizedMap()方法来获取一个线程安全的集合,底层原理是使用synchronized来保证线程同步。
- 使用 ConcurrentHashMap 集合。
26. ConcurrentHashMap 在 JDK7 和 JDK8的区别?
- 数据结构:JDK7 的数据结构是 Segment数组 + HashEntry数组 + 链表,JDK8 的数据结构是 HashEntry数组 + 链表 + 红黑树,当链表的长度超过8时,链表就会转换成红黑树,从而降低时间复杂度(由O(n) 变成了 O(logN)),提高了效率
- 锁的实现:JDK7的锁是segment,是基于ReentronLock实现的,包含多个HashEntry;而JDK8 降低了锁的粒度,采用 table 数组元素作为锁,从而实现对每行数据进行加锁,进一步减少并发冲突的概率,并使用 synchronized 来代替 ReentrantLock,因为在低粒度的加锁方式中,synchronized 并不比 ReentrantLock 差,在粗粒度加锁中ReentrantLock 可以通过 Condition 来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。
- 统计集合中元素个数 size 的方式:JDK7 是先尝试 2次通过不锁住 segment 的方式来统计各个 segment 大小,如果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有Segment的大小;在 JDK8 中,对于size的计算,在扩容和 addCount() 方法中就已经有处理了,等到调用 size() 时直接返回元素的个数
27. concurrentHashMap和HashTable有什么区别
concurrentHashMap融合了hashmap和hashtable的优势,hashmap是不同步的,但是单线程情况下效率高,hashtable是同步的同步情况下保证程序执行的正确性。
但hashtable每次同步执行的时候都要锁住整个结构,如下图:
concurrentHashMap锁的方式是细粒度的。concurrentHashMap将hash分为16个桶(默认值),诸如get、put、remove等常用操作只锁住当前需要用到的桶。
concurrentHashMap的读取并发,因为读取的大多数时候都没有锁定,所以读取操作几乎是完全的并发操作,只是在求size时才需要锁定整个hash。
而且在迭代时,concurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,弱一致迭代器。在这种方式中,当iterator被创建后集合再发生改变就不会抛出ConcurrentModificationException,取而代之的是在改变时new新的数据而不是影响原来的数据,iterator完成后再讲头指针替代为新的数据,这样iterator时使用的是原来的数据。
28. ArrayList与LinkedList的区别
-
ArrayList 和 LinkedList 是 List 接口的两种不同实现,并且两者都不是线程安全的。
-
ArrayList 内部使用的动态数组来存储元素,LinkedList 内部使用的双向链表来存储元素,这也是 ArrayList 和 LinkedList 最本质的区别。由于内部使用的存储方式不同,导致它们的各种方法具有不同的时间复杂度。
-
ArrayList 和 LinkedList 在内存的使用上也有所不同。LinkedList 的每个元素都有更多开销,因为要存储上一个和下一个元素的地址。ArrayList 没有这样的开销。
-
ArrayList 占用的内存在声明的时候就已经确定了(默认大小为 10),不管实际上是否添加了元素,因为复杂对象的数组会通过 null 来填充。LinkedList 在声明的时候不需要指定大小,元素增加或者删除时大小随之改变(双向链表决定的)。LinkedList 允许内存进行动态分配,这就意味着内存分配是由编译器在运行时完成的,我们无需在 LinkedList 声明的时候指定大小。。
-
ArrayList 只能用作列表;LinkedList 可以用作列表或者队列,因为它还实现了 Deque 接口。
-
查询的时候,ArrayList 比 LinkedList 快。插入删除的时候LinkedList会更快些。
因为数组的元素需要连续的内存位置来存储其值。这就是 ArrayList 进行删除或者插入元素的时候成本很高的真正原因,因为我们必须移动某些元素为新的元素留出空间,比如说:现在有一个数组,10、12、15、20、4、5、100,如果需要在 12 的位置上插入一个值为 99 的元素,就必须得把 12 以后的元素往后移动,为 99 这个元素腾出位置。LinkedList 不需要在连续的位置上存储元素,因为节点可以通过引用指定下一个节点或者前一个节点。也就是说,LinkedList 在插入和删除元素的时候代价很低,因为不需要移动其他元素,只需要更新前一个节点和后一个节点的引用地址即可。。
个人看法:如果不知道该用 ArrayList 还是 LinkedList,就选择 ArrayList 。
29. HashMap与HashSet的区别
先贴一个众所周知的区别:
但其实从根本上来说,它俩本来就是同一个东西。再说的清楚明白一点, HashSet 就是个套了壳儿的 HashMap。虽然hashset是调用add()方法添加元素,但是其实HashSet的 add方法其实就是调用了HashMap的put方法。
30. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把 run 方法当作普通方法去执行。
31. 线程的run与start的区别
- 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
- start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
- run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
- 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
32. 线程的wait和sleep的区别
- 语法:wait 方法必须配合synchronized 一起使用,不然在运行时就会抛出 IllegalMonitorStateException 的异常;而 sleep 可以单独使用,无需配合 synchronized 一起使用。
- 所属类:wait 方法属于 Object 类的方法,而 sleep 属于 Thread 类的方法。
- 唤醒方式:sleep 方法必须要传递一个超时时间的参数,且过了超时时间之后,线程会自动唤醒。而 wait 方法可以不传递任何参数,不传递任何参数时表示永久休眠,直到另一个线程调用了 notify 或 notifyAll 之后,休眠的线程才能被唤醒。也就是说 sleep 方法具有主动唤醒功能,而不传递任何参数的 wait 方法只能被动的被唤醒。
- 资源释放:wait 方法会主动的释放锁,而 sleep 方法则不会。
- 线程状态:调用 sleep 方法线程会进入 TIMED_WAITING 有时限等待状态,而调用无参数的 wait 方法,线程会进入 WAITING 无时限等待状态。
33. synchronized 这三种加锁方式(作用对象:静态方法、非静态方法、代码块)作用范围的区别?
锁是加在对象上面的,重要事情再说一遍:在对象上加锁(这也是为什么 wait / notify 需要在锁定对象后执行,只有先拿到锁才能释放锁)
这三种作用范围的区别实际是被加锁的对象的区别:
34. Synchronized修饰的方法在抛出异常时,会释放锁吗?
synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁。
35. Synchronized是公平锁还是非公平锁?
非公平,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样做的目的是提高效率,但缺点是可能产生线程饥饿现象。
36. 如何提高Synchronized的并发性能?
- 减少锁竞争,是优化 Synchronized 同步锁的关键。我们应该尽量使 Synchronized 同步锁处于轻量级锁或偏向锁,这样才能提高 Synchronized 同步锁的性能;
通 - 过减小锁粒度来降低锁竞争也是一种最常用的优化方法;另外我们还可以通过减少锁的持有时间来提高 Synchronized 同步锁在自旋时获取锁资源的成功率,避免 Synchronized 同步锁升级为重量级锁。
37. 说下对volatile关键字的理解,使用场景是?
使用场景:
- 某个属性被多个线程共享,其中有⼀个线程修改了此属性,其他线程可以⽴即得到修改后的值,⽐如作为触发器,状态量标记,实现轻量级同步
- volatile可以在单例双重检查中实现可⻅性和禁⽌指令重排序,可以解决单例双重检查对象初始化代码执⾏乱序问题,从⽽保证安全性。
理解:
- volatile是java虚拟机提供的轻量级的同步机制:保证了可见性;不保证原子性;保证有序性。
- 可见性:Java就是利用volatile来提供可见性的。当有的线程修改了Volatile修饰的变量值并写回到主内存后,其他线程能立即看到最新的值。 其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。volatile更轻量级,开销低,因为它不会引起线程上下文的切换和调度。
- 无法保证原子性:volatile关键字无法保证原子性 ,更准确地说是volatile关键字只能保证单操作的原子性, 比如x=1 ,但是无法保证复合操作的原子性,比如x++。所谓原子性:即一个或者多个操作作为一个整体,要么全部执行,要么都不执行,并且操作在执行过程中不会被线程调度机制打断;而且这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换(context switch)。
- 可以通过加锁synchronized及lock保证原子性。
38. Volatile和Synchronized区别?
- volatile只能修饰实例变量和类变量,只能作⽤于属性,⽽synchronized可以修饰方法、静态方法及代码块。
- volatile保证数据的可⻅性,⽤于禁⽌指令重排序,但是不保证原⼦性;⽽synchronized是⼀种互斥的机制。
- volatile与synchronized相比属性的读写操作都是⽆锁的,不需要花费时间在获取锁和释放锁上,volatile是低成本的,更轻量级,开销低,因为它不会引起线程上下文的切换和调度。
- volatile可以看做是轻量版的synchronized,volatile不保证原⼦性,但是如果是对⼀个共享变量进⾏多个线程的赋值,⽽没有其他的操作,那么就可以⽤volatile来代替synchronized,因为赋值本身是有原⼦性的,⽽volatile⼜保证了可⻅性,所以就可以保证线程安全了。
39. 什么是指令重排
Java编译器是可以通过指令重排来优化性能。然而,重排可能会影响本地处理器缓存与主内存交互的方式,可能导致在多线程的情况下发生"细微"的BUG。
指令重排一般可以分为如下三种类型:
- 编译器优化重排序,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行重排序,现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排序,由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。这并不是显式的将指令进行重排序,只是因为缓存的原因,让指令的执行看起来像乱序。
从 Java 源代码到最终执行的指令序列,一般会经历下面三种重排序:编译器优化重排序 - 指令级并行重排序 - 内存系统重排序 - 最终执行的指令排序。
40. volatile如何保证有序性?
我个人认为应该不会问这个问题,简单列一下概念知识:
Volatile通过设置内存屏障(Memory Barrier),可以禁止指令重排,避免多线程环境下程序出现乱序执行的现象
通过插入内存屏障,禁止在内存屏障前后的指令执行重排序优化。
内存屏障(Memory Barrier)的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。
41. 实现线程同步的方式?
线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
- 使用Synchronized关键字
- 使用ReentrantLock
- 使用原子变量(Atomic)实现
- ThreadLocal实现线程同步
42. Synchronized与ReentrantLock的区别?
- ReentrantLock显示地获得,释放锁,synchronized隐式获得释放锁。
- synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
- synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是API层面的互斥锁,需要lock和unlock()方法配合try/finally代码块来完成。
- ReentrantLock可以实现公平锁,既可以是公平锁也可以是非公平锁,默认为非公平锁。
- ReentrantLock通过Condition可以绑定多个条件。
- 底层实现不一样,synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略。
- Lock是一个接口,而synchronized是java中的关键字,synchronized是内置的语言实现。
- synchronized 在发生异常时,会自动释放线程占有的锁;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象, 因此使用 Lock 时需要在 finally 块中释放锁。
43. 为什么要用线程池?
可以在代码里面显式的创建线程,但是如果线程多的话会消耗系统资源,效率低下并且还会降低系统稳定性。可以看到阿里巴巴编码规范提示:
所以我们使用线程池提前创建好一些固定的线程数一直在运行状态实现复用,从而可以减少就绪到运行状态的切换。优势是:(不用记知道即可)
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
44. 线程池的创建方式有哪些?
细化来说总共有七种,总体来说的话通过两种方式:一种是通过Executors创建,另一种是通过ThreadPoolExecutor来创建。
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
可以看到newSingleThreadExecutor 创建的单线程线程池,虽然是单线程但是提供了工作队列,生命周期管理,工作线程维护等功能,是有意义的。
应该选择那种方式创建线程池?
阿里巴巴编码规范给出了提示,如果在代码中使用Executors创建线程池的话会提示:
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。推荐使用 ThreadPoolExecutor 的方式进行线程池的创建,因为这种创建方式更可控,并且更加明确了线程池的运行规则,可以规避一些未知的风险。
45. ThreadPoolExecutor的核心参数含义是?
查看ThreadPoolExecutor的构造函数可知,最多可以设置7个参数:
- corePoolSize
核心线程数,线程池中始终存活的线程数。 - maximumPoolSize
最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。 - keepAliveTime
最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。 - unit:
单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:
TimeUnit.DAYS:天
TimeUnit.HOURS:小时
TimeUnit.MINUTES:分
TimeUnit.SECONDS:秒
TimeUnit.MILLISECONDS:毫秒
TimeUnit.MICROSECONDS:微妙
TimeUnit.NANOSECONDS:纳秒 - workQueue
一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。 - threadFactory
线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。 - handler
拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
默认策略为 AbortPolicy。
ThreadPoolExecutor执行流程:
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。
46. 线程池创建的线程会一直运行吗?
不会。
例如:配置核心线程数 corePoolSize 为 2 (一直运行中)、最大线程数 maximumPoolSize 为 5 (不一定一直运行);我们可以通过配置超出corePoolSize 核心线程数创建的线程的存活时间为60s,在60s内核心线程一直没有任务执行,则会停止该线程。
47. 如何向线程池中提交任务?
可以通过execute()或submit()两个方法向线程池提交任务。
execute()方法没有返回值,所以无法判断任务知否被线程池执行成功。
ExecutorService executor = Executors.newCachedThreadPool();
Runnable runnable = () -> System.out.println("runnable excute");
executor.execute(runnable);
submit()方法返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值。
ExecutorService executor = Executors.newCachedThreadPool();
Callable<String> callable = () -> {
System.out.println("runnable execute");
return "finished";
};
Future<String> submit = executor.submit(callable);
//阻塞主线程,等待任务执行完毕,返回值为call()的返回值
System.out.println(submit.get());
48. 如何关闭线程池?
可以通过shutdown()或shutdownNow()方法来关闭线程池。
- shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程,不接受新任务,但是在关闭前会将之前提交的任务处理完毕。
- shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
49. 什么是ThreadLocal?有哪些应用场景?
ThreadLocal,JDK java.lang 包下的一个类,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。
ThreadLocal 的应用场景主要有以下几个方面:
- 保存线程上下文信息,在需要的地方可以获取
- 线程间数据隔离,各线程的 ThreadLocal 互不影响
- Spring 事务管理器采用了 ThreadLocal
- Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
- 数据库连接
- 日期转换工具类等
50. 简单说下ThreadLocal原理
- Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
- ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
- 并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。
51. ThreadLocal为什么会内存溢出,如何避免?
ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。
那么如何避免内存泄漏呢?
在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收,其中 remove 源码如下所示:
/**
* 清理当前 ThreadLocal 对象关联的键值对
*/
public void remove() {
// 返回当前线程持有的 map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 从 map 中清理当前 ThreadLocal 对象关联的键值对
m.remove(this);
}
}
remove 方法是先获取到当前线程的 ThreadLocalMap,并且调用了它的 remove 方法,从 map 中清理当前 ThreadLocal 对象关联的键值对,这样 value 就可以被 GC 回收了。
52. ThreadLocalMap Entry的key为什么设计成弱引用?
当ThreadLocal的对象被回收了,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set、get、remove的时候会被清除。因此可以发现,使用弱引用作为Entry的Key,可以多一层保障:弱引用ThreadLocal不会轻易内存泄漏,对应的value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
实际上,我们的内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:
- 一种就是,使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除
- 另外一种方式就是:ThreadLocalMap的自动清除机制去清除过期Entry.(ThreadLocalMap的get(),set()时都会触发对过期Entry的清除)
Entry的key被设计为弱引用就是为了让程序自动的对访问不到的数据进行回收提醒,所以,在访问不到的数据被回收之前,内存泄漏确实是存在的,但是我们不用担心,就算我们不调用remove,ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry,内存泄漏完全没必要过于担心。
所以,ThreadLocal的建议使用方法:
- 设计为static的,被class对象给强引用,线程存活期间就不会被回收,也不用remove,完全不用担心内存泄漏
- 设计为非static的,长对象(比如被spring管理的对象)的内部,也不会被回收
- 没必要在方法中创建ThreadLocal对象
53. CountDownLatch的用法,使用场景
简单描述下使用场景,知道咋回事儿,具体原理不做表述。
CountDownLatch是java中一个协调多线程的工具类,假如多线程在执行后,需要等待所有都执行完再执行下一步,那么就可以使用CountDownLatch。
CountDownLatch 提供了一个构造方法,必须指定其初始值(int),提供了 await及countDown 方法,countDown方法的作用主要用来减小计数器的值,当计数器变为 0 时,在 CountDownLatch 上 await 的线程就会被唤醒,继续执行其他任务。当然也可以延迟唤醒,给 CountDownLatch 加一个延迟时间就可以实现。
//countdownlatch测试
CountDownLatch countDownLatch = new CountDownLatch(10);
Thread abc = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
System.out.println("abc"+i);
}
});
abc.start();
System.out.println("等待执行之前");
countDownLatch.await();
System.out.println("子线程已经执行完成,继续主线程");
主线程await,子线程执行逻辑并调用countDown。
用法:
- 让主线程await,业务线程进行业务处理,处理完成时调用countdownLatch.countDown(),CountDownLatch实例化的时候需要根据业务去选择CountDownLatch的count;
- 让业务线程await,主线程处理完数据之后进行countdownLatch.countDown(),此时业务线程被唤醒,然后去主线程拿数据,或者执行自己的业务逻辑。
54. 如何让多个线程顺序执行?
- join()方法指定线程顺序。通过join()方法使当前线程“阻塞”,等待指定线程执行完毕后继续执行。举例:在线程second中,加上一句first.join(),其意义在于,当前线程second运行到此行代码时会进入阻塞状态,直到线程first执行完毕后,线程second才会继续运行,这就保证了线程first与线程second的运行顺序。
Thread first = new Thread(() -> {
System.out.println("first");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread second = new Thread(() -> {
try {
first.join();
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("second");
});
Thread third = new Thread(() -> {
try {
second.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("third");
});
first.start();
second.start();
third.start();
third.join();
System.out.println("main");
- 倒数计时器CountDownLatch实现。CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器为0当前线程就可以往下执行而不用管相应的thread是否执行完毕。(可以查看53. CountDownLatch的用法,使用场景)
- 创建单一化线程池newSingleThreadExecutor()实现。串行执行所有任务。
- FutureTask。
ExecutorService executorService = Executors.newFixedThreadPool(3);
//实际上 ExecutorService.submit还是用的FutureTask
Object first = executorService.submit(() -> System.out.println("first")).get();
Object second = executorService.submit(() -> System.out.println("second")).get();
Object third = executorService.submit(() -> System.out.println("third")).get();
System.out.println(("main "));
- CompletableFuture。强烈推荐,好使。
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runnable first = () -> {
System.out.println("first");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable second = () -> System.out.println("second");
Runnable third = () -> System.out.println("third");
//异步执行
CompletableFuture.runAsync(first, executorService).thenRun(second).thenRun(third);
System.out.println("main");
55. Cookie与Session是什么,区别是?
56. Http常见的响应码有哪些?
我更多的是做后端,所以列举一下我常见的状态码:
57. XSS与CSRF分别是什么,区别是?
XSS是指恶意攻击者在Web页面中插入恶意javascript代码(也可能包含html代码),当用户浏览网页之时,嵌入其中Web里面的javascript代码会被执行,从而达到恶意攻击用户的目的。XSS是攻击客户端,最终受害者是用户,当然,网站管理员也是用户之一。
CSRF,攻击者利用服务器对用户的信任,从而欺骗受害者去服务器上执行受害者不知情的请求。
- XSS是利用用户对服务端的信任,CSRF是利用服务端对用户的信任。
- XSS的攻击,主要是让脚本在用户浏览器上执行,服务器端仅仅只是脚本的载体,本身服务器端不会受到攻击利用。
- CSRF攻击,攻击者会伪造一个用户发送给服务器的正常链接,其核心主要是要和已登录(已认证)的用户去发请求。CSRF不需要知道用户的Cookie,CSRF自己并不会发请求给服务器,一切交给用户。
58. Websocket与Http的异同?
相同点:
- 都是基于tcp的,都是可靠性传输协议。
- 都是应用层协议。
不同点:
- Http是短连接,请求之后会关闭连接,Websocket是长连接,只需通过一次请求初始化连接,然后所有的请求和响应都是通过这个TCP连接进行通讯。
- WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,HTTP是单向的。
- WebSocket是需要浏览器和服务器握手进行建立连接的,而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接。
59. 什么是设计模式?常用的设计模式在实际使用场景是什么?
设计模式:
- 设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
- 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。
使用场景:
- 单例模式:JDK种的runtime,Spring种的singeton。
- 简单工厂模式:Spring的BeanFactory,根据传入一个唯一标识来获得bean对象。
- 代理模式:Spring的AOP中,Spring实现AOP功能的原理就是代理模式,①JDK动态代理。②CGLIB动态代理,使用Advice(通知)对类进行方法级别的切面增强。
- 原型模式:clone()。
- 策略模式:从本质上讲,策略模式就是一个接口下有多个实现类,而每种实现类会处理某一种情况。多个if-else的处理。
60. 什么是spring AOP?
一般称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
61. springAOP的实际应用场景?
- 记录日志,方法请求操作日志,参数信息等。
- 监控方法运行时间 (监控性能)。
- 权限控制,数据权限等。
- 缓存优化 (第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库 )。
- 事务管理 (调用方法前开启事务, 调用方法后提交关闭事务 )。
62. SpringAOP的实现原理是?
Spring的AOP实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
JDK动态代理
- Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java语言的应该会对JDK动态代理有所了解。JDK实现动态代理需要两个组件,首先第一个就是InvocationHandler接口。我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。然后JDK动态代理需要使用的第二个组件就是Proxy这个类,我们可以通过这个类的newProxyInstance方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke方法。
- JDK的动态代理是基于反射实现。JDK通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。并且这个代理类是Proxy类的子类
CGLib动态代理
- JDK的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK的动态代理将没有办法使用,于是Spring会使用CGLib的动态代理来生成代理对象。CGLib直接操作字节码,生成类的子类,重写类的方法完成代理。
- CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。
63. jdk动态代理与cglib动态代理的优缺点?
jdk动态代理
- 优点:
JDK动态代理是JDK原生的,不需要任何依赖即可使用;
通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快; - 缺点:
如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;
JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;
cglib动态代理
- 优点
使用CGLib代理的类,不需要实现接口,因为CGLib生成的代理类是直接继承自需要被代理的类;
CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;
CGLib生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib执行代理方法的效率要高于JDK的动态代理; - 缺点
由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类,则无法使用CGLib代理;
由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法,或者private方法进行代理,因为子类无法重写这些方法;
CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢;
64. Beanfactory和Applicationcontext的区别?
我个人认为应该不会问这个问题,因为基本上都是使用Applicationcontext,但是还是要了解一下相关知识的。
首先,这两个都是接口,Applicationcontext继承自Beanfactory,是Beanfactory的子接口。
区别:
- BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化。
- ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化。
以上为最主要区别,其他的区别也自行了解查看。
65. spring中支持几种bean的作用域?
- singleton:单例,spring默认的作用域。默认每个容器只有一个bean的作用域。bean以单例的形式存在,在创建容器的时候就会同时创建一个bean的对象。该对象的生命周期和IOC容器是一样的。
- prototype:原型类型。每次从容器中调用bean时,都会创建一个新的对象。注意,在创建容器的时候并没有实例化,当我们获取bean的时候才会去创建一个对象,每次获取的对象不是同一个对象。
- request:request域。每次HTTP请求的时候都会创建一个单例bean,单个请求中都会复用这一个单例对象。
- session:和request类似,确保每个session中有一个bean的实例,session过期后,bean会随之失效。
- global-session:一般用于Portlet应用环境,该运用域仅适用于WebApplicationContext环境。
66. @Autowired与@Resource的区别?
- @Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解,它来自于 JSR-250(Java 250 规范提案)。
- @Autowired 先根据类型(byType)查找,如果存在多个(Bean)再根据名称(byName)进行查找;@Resource 先根据名称(byName)查找,如果(根据名称)查找不到,再根据类型(byType)进行查找。
- @Autowired 支持属性注入、构造方法注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入。但是用@Resource实现构造方法注入的话会有错误:
- idea中提示不同
67. spring框架中的单例bean是线程安全的吗?
不是安全的。
Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。注:单例bean是指IOC容器中就只有这么一个bean,是全局共享的,有多少个线程来访问用的都是这个bean。
如果Bean是有状态的,那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域 把 "singleton"改为’‘protopyte’ 这样每次请求Bean就相当于是 new Bean() 这样就可以保证线程的安全了。
- 有状态就是有数据存储功能。比如:一个Service里有个count的变量计数。
- 无状态就是不会保存数据,可以无新增更新删除加减等操作,仅有查询。
- 尽量不要在@Controller或者@Service等容器中定义静态变量,不论是单例(singleton)还是多实例(prototype)他都是线程不安全的。一定要定义变量的话,用ThreadLocal来封装,这个是线程安全的:ThreadLocal threadLocal = new ThreadLocal<>(); // 用ThreadLocal来封装变量
68. springboot的核心配置文件是什么?
spring boot 核心的两个配置文件:
- bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,
比 applicaton 优先加载,且 boostrap 里面的属性不能被覆盖; - application (. yml 或者 . properties):用于 spring boot 项目的自动化配置。
69. JPA实体类相关注解有哪些?
-
@Entity:被Entity标注的实体类将会被JPA管理控制,在程序运行时,JPA会识别并映射到指定的数据库表唯一参数name:指定实体类名称,默认为当前实体类的非限定名称。若给了name属性值即@Entity(name=“XXX”),则jpa在仓储层(数据层)进行自定义查询时,所查的表名应是XXX。
-
@Table:当你想生成的数据库表名与实体类名称不同时,使用 @Table(name=“数据库表名”),与@Entity标注并列使用,置于实体类声明语句之前。
-
@Id:用于实体类的一个属性或者属性对应的getter方法的标注,被标注的的属性将映射为数据库主键。
-
@GeneratedValue:与@Id一同使用,用于标注主键的生成策略,通过 strategy 属性指定。默认是JPA自动选择合适的策略,在 javax.persistence.GenerationType 中定义了以下几种可供选择的策略:
- IDENTITY:采用数据库ID自增长的方式产生主键,Oracle 不支持这种方式。
- AUTO: JPA 自动选择合适的策略,是默认选项。
- SEQUENCE:通过序列产生主键,通过@SequenceGenerator标注指定序列名,MySQL 不支持这种方式。
- TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略更易于做数据库移植。
-
@Column:通常置于实体的属性声明之前,可与 @Id 标注一起使用,@Column参数:
- name: 指定映射到数据库中的字段名
- unique: 是否唯一,默认为false
- nullable: 是否允许为null,默认为true
- insertable: 是否允许插入,默认为true
- updatetable: 是否允许更新,默认为true
- columnDefinition: 指定该属性映射到数据库中的实际类型,通常是自动判断。
-
@Transient:JPA会忽略该属性,不会映射到数据库中,即程序运行后数据库中将不会有该字段。
-
@Embedded 和 @Embeddable:用于一个实体类要在多个不同的实体类中进行使用,而本身又不需要独立生成一个数据库表。
-
@JoinColumn:定义表关联的外键字段名。常用参数有:
- name: 指定映射到数据库中的外键的字段名。
- unique: 是否唯一,默认为false。
- nullable: 是否允许为null,默认为true。
- insertable: 是否允许插入,默认为true。
- updatetable: 是否允许更新,默认为true。
- columnDefinition: 指定该属性映射到数据库中的实际类型,通常是自动判断。
- foreignKey = @ForeignKey(name = “none”,value = ConstraintMode.NO_CONSTRAINT):指定外键相关信息,这里用法是指定外联关系但是不建立数据库外键。
-
@OneToOne、@OneToMany、@ManyToOne、@ManyToMany
-
@Enumerated:当实体类中有枚举类型的属性时,默认情况下自动生成的数据库表中对应的字段类型是枚举的索引值,是数字类型的,若希望数据库中存储的是枚举对应的String类型,在属性上加入@Enumerated(EnumType.STRING)注解即可。
70. 拦截器、过滤器及AOP的执行顺序?
过滤前=> 拦截前=> AOP=> Controller=> AOP=> 拦截后=> 过滤后。如果过滤器、 拦截器、 AOP都存在,则它们的执行顺序为:过滤器=> 拦截器=> AOP。
71. 拦截器与过滤器的区别?
- 实现原理:过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。
- 使用范围:过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。而拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于Application、Swing等程序中。
- 触发节点:过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。
- 过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。
- 实际应用中需要在拦截器及过滤器注入组件等,以实现相关业务逻辑。过滤器注入bean正常,拦截器注入bean为null,这是因为拦截器加载的时间点在springcontext之前,即在bean实例化之前,所以在拦截器中注入自然为null 。那么我们就让拦截器执行的时候实例化拦截器Bean,在拦截器配置类里面先实例化拦截器,然后再获取就能解决这个问题。
72. 为什么使用rabbitMQ?
结合自己的使用场景,从异步、错峰、解耦来回答即可。
73. RabbitMQ有哪几种交换机类型?
- 直连交换机(Direct Exchange):当一个交换机绑定一个队列,再绑定一个routing key ,当发送消息时,指定一个bindingkey,这样消息发送到交换机时,会被送到指定的队列里面去。是一种发布/订阅模式。
- 扇形交换机(Fanout Exchange):发所有接收到的消息全部发送的交换机所绑定的队列,不需要思考,速度最快。是一种广播模式。
- 主题交换机(Topic Exchange):routing key根据一定的规则进行匹配,*:表示一个单词;#表示任意数量单词。当一个队列的绑定键为#的时候,这个队列将会无视消息的路由键,接收所有的消息。
- 首部交换机(Header Exchange,这个用的比较少,知道即可):忽略了routing key ,通过headers匹配。将一个交换机声明成首部交换机,绑定一个队列的时候,定义一个 Hash 的数据结构,消息发送的时候,会携带一组 hash 数据结构的信息,当 Hash 的内容匹配上的时候,消息就会被写入队列。
74. 如何避免RabbitMQ可能存在的消息丢失问题?
可以查看我之前的文章:点我查看
75. RabbitMQ中 vhost 的作用是什么?
每一个RabbitMQ服务器都能创建虚拟消息服务器,我们称为虚拟主机Vhost。每一个vhost本质上是一个mini版的RabbitMq。拥有自己的队列、交换器和绑定。更重要的是,它拥有自己的权限机制,vhost 是AMQP 概念基础,你必须在连接时进行制定,由于RabbitMq包含了开箱即用的默认vhost:“/”,因此使用起来非常方便。可以通过默认的用户guest 和 密码 guest 访问默认的vhost。
在RabbitMQ里创建一个用户时,用户通常会被指派至少一个vhost,并且只能访问被指派vhost内的队列,交换器和绑定。vhost之间是绝对隔离的。
76. Redis的默认数据库有几个?
redis默认支持16个数据库,可以通过调整redis的配置文件 redis/redis.conf 中的 databases 来修改这一个值,设置完毕后重启redis便完成配置。默认使用0库,可以通过select 命令用于切换到指定的数据库:
77. Spring boot集成redis是使用的哪个依赖?有什么区别?
redis继承一般通过spring-data-redis和jedis进行整合。两者有以下区别:
- 依赖不同:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>jedis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
- 管理方式不同:spring-data-redis与spring的整合,更像mybatis与spring整合,通过工厂,创建实例,再操作实例。而jedis,更像spring与MySQL结合,通过操作连接池,获取实例操作数据库。
- 性能速度:大数据量查询的话jedis效率会高些。
- 可扩展性:jedis可扩展性高。
78. 为什么选择redis作为缓存?
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
- 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 使用多路 I/O 复用模型,非阻塞 IO;
79. Redis和Caffeine的区别是什么?
Caffeine 是一个基于Java8开发的高性能,高命中率,低内存占用的本地缓存,简单来说它是 Guava Cache 的优化加强版,有些文章把 Caffeine 称为“新一代的缓存”、“现代缓存之王”。
区别:
- redis是将数据存储到内存里。
- caffeine是将数据存储在本地应用里。
- caffeine和redis相比,没有了网络IO上的消耗。
- caffeine本地应用重启后,缓存会丢失(我的认知是这样,是否有持久化的方案有待认证,大家如果知道的话可以提出来);redis可以持久化。
80. Redis的基础数据结构有哪些?
Redis有以下这五种基本类型:
- String(字符串)
- Hash(哈希)
- List(列表)
- Set(集合)
- zset(有序集合)
它还有三种特殊的数据结构类型
- Geospatial(地理位置)
- Hyperloglog
- Bitmap(位图)
81. 简单说下Redis的过期清除方式及缓存淘汰策略?
过期清除
我们可以通过EXPIRE key seconds来设置过期时间,当缓存中的数据过期之后,Redis就需要将这些数据进行清除,释放占用的内存空间。Redis中主要使用定期删除 + 惰性删除两种数据过期清除策略。
- 定期删除:redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果有过期就删除。注意这里是随机抽取的。
- 惰性删除:定期删除可能导致很多过期的key 到了时间并没有被删除掉。这时就要使用到惰性删除。在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且过期了,是的话就删除。
缓存淘汰
Redis共提供了8中缓存淘汰策略(没必要全记住,了解两个就行):
- noeviction:不进行淘汰数据。一旦缓存被写满,再有写请求进来,Redis就不再提供服务,而是直接返回错误。Redis 用作缓存时,实际的数据集通常都是大于缓存容量的,总会有新的数据要写入缓存,这个策略本身不淘汰数据,也就不会腾出新的缓存空间,我们不把它用在 Redis 缓存中。
- volatile-ttl:在设置了过期时间的键值对中,移除即将过期的键值对。
- volatile-random:在设置了过期时间的键值对中,随机移除某个键值对。
- volatile-lru:在设置了过期时间的键值对中,移除最近最少使用的键值对。
- volatile-lfu:在设置了过期时间的键值对中,移除最近最不频繁使用的键值对。
- allkeys-random:在所有键值对中,随机移除某个key。
- allkeys-lru:在所有的键值对中,移除最近最少使用的键值对。
- allkeys-lfu:在所有的键值对中,移除最近最不频繁使用的键值对。
一般场景下建议使用allkeys-lru策略。
82. 为什么Redis不支持回滚?
先了解下事务
MULTI 、EXEC 、DISCARD和WATCH是Redis事务的基础。事务可以一次执行多个命令, 并且带有以下两个重要的保证:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
- EXEC命令负责触发并执行事务中的所有命令:
- 如果客户端在使用MULTI开启了一个事务之后,却因为断线而没有成功执行EXEC,那么事务中的所有命令都不会被执行。
- 另一方面,如果客户端成功在开启事务之后执行EXEC,那么事务中的所有命令都会被执行。
用法
-
MULTI命令用于开启一个事务,它总是返回OK 。
-
MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
-
另一方面,通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务。
以下是一个事务例子,它原子地增加了foo和bar两个键的值:> MULTI OK > INCR foo QUEUED > INCR bar QUEUED > EXEC 1) (integer) 1 2) (integer) 1
EXEC命令的回复是一个数组,数组中的每个元素都是执行事务中的命令所产生的回复。其中,回复元素的先后顺序和命令发送的先后顺序一致。
当客户端处于事务状态时,所有传入的命令都会返回一个内容为QUEUED的状态回复(status reply),这些被入队的命令将在EXEC命令被调用时执行。
不支持回滚(roll back)?
“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。
以下是这种做法的优点:
- Redis命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
- 因为不需要对回滚进行支持,所以Redis的内部可以保持简单且快速。
有种观点认为Redis处理事务的做法会产生bug,然而需要注意的是,在通常情况下,回滚并不能解决编程错误带来的问题。举个例子,如果你本来想通过INCR key命令将键的值加上1,却不小心加上了2,又或者对错误类型的键执行了INCR key,回滚是没有办法处理这些情况的。
鉴于没有任何机制能避免程序员自己造成的错误,并且这类错误通常不会在生产环境中出现,所以Redis选择了更简单、更快速的无回滚方式来处理事务。
83. 如何做的Redis持久化?详细说一下持久化方式有哪些?
Redis提供了两种持久化方式:RDB快照、只进行追加操作的文件(append-only file,AOF)。
RDB 快照
-
在默认情况下, Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中。
-
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。
-
你也可以通过调用 SAVE 或者 BGSAVE , 手动让 Redis 进行数据集保存操作。
比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集:save 60 1000
这种持久化方式被称为快照(snapshot)。
快照运作方式
当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:
- Redis 调用 fork() ,同时拥有父进程和子进程。
- 子进程将数据集写入到一个临时 RDB 文件中。
- 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。
RDB的优点
- RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。
- RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心,或者亚马逊 S3 中。
- RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
- RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
RDB的缺点
- 如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
- 每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。
AOF(append only file)
- 快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。
- 尽管对于某些程序来说, 数据的耐久性并不是最重要的考虑因素, 但是对于那些追求完全耐久能力(full durability)的程序来说, 快照功能就不太适用了。
- 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
- 可以通过修改配置文件来打开 AOF 功能:
从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET key value [EX seconds] [PX milliseconds] [NX|XX]), 这个命令就会被追加到 AOF 文件的末尾。appendonly yes
这样的话, 当 Redis 重启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。
AOF重写
- 因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。
- 举个例子, 如果你对一个计数器调用了 100 次 INCR key , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET key value [EX seconds] [PX milliseconds] [NX|XX] 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
- 为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。
- Redis 2.2 需要自己手动执行 BGREWRITEAOF 命令; Redis 2.4 则可以自动触发 AOF 重写, 具体信息请查看 2.4 的示例配置文件。
如果 AOF 文件出错了,怎么办?
服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。
当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
- 为现有的 AOF 文件创建一个备份。
- 使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复。
$ redis-check-aof --fix
- (可选)使用 diff -u 对比修复后的 AOF 文件和原始 AOF 文件的备份,查看两个文件之间的不同之处。
- 重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。
AOF 的运作方式
AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制。
以下是 AOF 重写的执行步骤:
- Redis 执行 fork() ,现在同时拥有父进程和子进程。
- 子进程开始将新 AOF 文件的内容写入到临时文件。
- 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾: 这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
- 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
- 现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。
AOF的优点
- 使用 AOF 持久化会让 Redis 变得非常耐久(much more durable):你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。
- AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。
- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
- AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
AOF的缺点
- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
- 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
- AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH source destination timeout 就曾经引起过这样的 bug 。) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。
RDB 和 AOF,应该用哪一个?
- 一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
- 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
- 有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避 之前提到的 AOF 程序的 bug 。
84. 说一下Redis的缓存穿透与缓存雪崩是什么?如何解决?
一直以来有疑问,这种穿透、雪崩这种是术语还是行话?如果后者的话,名字就不能起的好区分点吗?
正常的请求流程是请求缓存,然后保护DB,不论是雪崩还是穿透造成的结果都是请求没有到缓存上而是直接打到数据库了。
雪崩:是指在短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能会导致数据库宕机的情况叫做缓存雪崩。解决办法如下:
- 加锁排队,可以起到缓冲的作用,防止大量的请求同时操作数据库,但它的缺点是增加了系统的响应时间,降低了系统的吞吐量,牺牲了一部分用户体验。当缓存未查询到时,对要请求的 key 进行加锁,只允许一个线程去数据库中查,其他线程等候排队。如果为集群架构的话,不要用本地锁,用分布式锁。
- 分散过期时间,为 key 设置不同的缓存失效时间。避免缓存同时失效。
- 设置本地缓存,当 Redis 失效之后,先去查询本地缓存。本地缓存可以使用Caffeine。
穿透:是指查询数据库和缓存都无数据,因为数据库查询无数据,出于容错考虑,不会将结果保存到缓存中,因此每次请求都会去查询数据库,这种情况就叫做缓存穿透。解决方法如下:
- 使用布隆过滤器来减少对数据库的请求,布隆过滤器的原理是将数据库的数据哈希到 bitmap 中,每次查询之前,先使用布隆过滤器过滤掉一定不存在的无效请求,从而避免了无效请求给数据库带来的查询压力。
- 另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。
看很多文章说还有一个缓存击穿?起名高手,我服了,我理解的跟穿透相差不差,感兴趣的可以了解下。
85. Redis6.x 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能(Redis 的瓶颈并不在 CPU,而在内存和网络。),虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,不需要担心线程安全问题。
多线程默认不开启,只是用主线程。
86. Redis分布式锁的实现?有什么需要注意的?
redis分布式锁可以直接查看:查看大佬文章—细说redis分布式锁
87. Docker容器是什么?
Docker 容器在应用层创建了一个抽象,并将应用程序及其所有依赖项打包在一起。这使我们能够快速可靠地部署应用程序。容器不需要我们安装不同的操作系统。相反,它们使用底层系统的 CPU 和内存来执行任务。这意味着任何容器化应用程序都可以在任何平台上运行,而不管底层操作系统如何。我们也可以将容器视为 Docker 镜像的运行时实例。
88. 如何从Docker镜像中创建一个Docker容器?
为了从镜像中创建一个容器,我们从Docker资源库中拉出我们想要的镜像并创建一个容器。我们可以使用以下命令。可以用如下命令:
$ docker run -it -d <image_name>
89. Docker仓库、镜像及容器的关系?
简而言之:仓库存放镜像,主机通过仓库下载镜像,通过镜像创建容器。
90. 说下常用的Docker命令?
docker pull 拉取镜像
docker create 创建容器
docker rm 删除容器
docker ps 列出正在运行的容器列表
docker run 创建容器并运行指定命令
docker start 启动容器
docker stop 停止运行容器
docker restart 重启容器
docker rm 删除容器
docker exec 容器执行指定命令
docker rmi 删除镜像