Java面试题整理(带答案)

这是我自己整理的Java面试题以及答案。题目来源是https://blog.csdn.net/sufu1065/article/details/88051083

我删除了自己不使用的框架技术栈,像Hibernate、Kafka、Zookeeper,所以题目列表有些不连贯是正常的。

本次整理包含了Java 基础、容器、多线程、反射、对象拷贝、Java Web 模块、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Mybatis、RabbitMQ、MySql、Redis、JVM,比较全面,希望大家都能找到自己理想的工作。

文章目录

    • 一、Java基础
      • 1.JDK 和 JRE 有什么区别?
      • 2.== 和 equals 的区别是什么?
      • 3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
      • 4.final 在 java 中有什么作用?
      • 5.java 中的 Math.round(-1.5) 等于多少?
      • 6.String 属于基础的数据类型吗?
      • 7.java 中操作字符串都有哪些类?它们之间有什么区别?
      • 8.String str="i"与 String str=new String(“i”)一样吗?
      • 9.如何将字符串反转?
      • 10.String 类的常用方法都有那些?
      • 11.抽象类必须要有抽象方法吗?
      • 12.普通类和抽象类有哪些区别?
      • 13.抽象类能使用 final 修饰吗?
      • 14.接口和抽象类有什么区别?
      • 15.java 中 IO 流分为几种?
      • 16.BIO、NIO、AIO 有什么区别?
      • 17.Files的常用方法都有哪些?
    • 二、容器
      • 18.java 容器都有哪些?
      • 19.Collection 和 Collections 有什么区别?
      • 20.List、Set、Map 之间的区别是什么?
      • 21.HashMap 和 Hashtable 有什么区别?
      • 22.如何决定使用 HashMap 还是 TreeMap?
      • 23.说一下 HashMap 的实现原理?
      • 24.说一下 HashSet 的实现原理?
      • 25.ArrayList 和 LinkedList 的区别是什么?
      • 26.如何实现数组和 List 之间的转换?
      • 27.ArrayList 和 Vector 的区别是什么?
      • 28.Array 和 ArrayList 有何区别?
      • 29.在 Queue 中 poll()和 remove()有什么区别?
      • 30.哪些集合类是线程安全的?
      • 31.迭代器 Iterator 是什么?
      • 32.Iterator 怎么使用?有什么特点?
      • 33.Iterator 和 ListIterator 有什么区别?
      • 34.怎么确保一个集合不能被修改?
    • 三、多线程
      • 35.并行和并发有什么区别?
      • 36.线程和进程的区别?
      • 37.守护线程是什么?
      • 38.创建线程有哪几种方式?
      • 39.说一下 runnable 和 callable 有什么区别?
      • 40.线程有哪些状态?
      • 41.sleep() 和 wait() 有什么区别?
      • 42.notify()和 notifyAll()有什么区别?
      • 43.线程的 run()和 start()有什么区别?
      • 44.创建线程池有哪几种方式?
      • 45.线程池都有哪些状态?
      • 46.线程池中 submit()和 execute()方法有什么区别?
      • 47.在 java 程序中怎么保证多线程的运行安全?
      • 48.多线程锁的升级原理是什么?
      • 49.什么是死锁?
      • 50.怎么防止死锁?
      • 51.ThreadLocal 是什么?有哪些使用场景?
      • 52.说一下 synchronized 底层实现原理?
      • 53.synchronized 和 volatile 的区别是什么?
      • 54.synchronized 和 Lock 有什么区别?
      • 55.synchronized 和 ReentrantLock 区别是什么?
      • 56.说一下 atomic 的原理?
    • 四、反射
      • 57.什么是反射?
      • 58.什么是 java 序列化?什么情况下需要序列化?
      • 59.动态代理是什么?有哪些应用?
      • 60.怎么实现动态代理?
    • 五、对象拷贝
      • 61.为什么要使用克隆?
      • 62.如何实现对象克隆?
      • 63.深拷贝和浅拷贝区别是什么?
    • 六、Java Web
      • 64.jsp 和 servlet 有什么区别?
      • 65.jsp 有哪些内置对象?作用分别是什么?
      • 66.说一下 jsp 的 4 种作用域?
      • 67.session 和 cookie 有什么区别?
      • 68.说一下 session 的工作原理?
      • 69.如果客户端禁止 cookie 能实现 session 还能用吗?
      • 70.Spring mvc 和 struts 的区别是什么?
      • 71.如何避免 sql 注入?
      • 72.什么是 XSS 攻击,如何避免?
      • 73.什么是 CSRF 攻击,如何避免?
    • 七、异常
      • 74.throw 和 throws 的区别?
      • 75.final、finally、finalize 有什么区别?
      • 76.try-catch-finally 中哪个部分可以省略?
      • 77.try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
      • 78.常见的异常类有哪些?
    • 八、网络
      • 79.http 响应码 301 和 302 代表的是什么?有什么区别?
      • 80.forward 和 redirect 的区别?
      • 81.简述 tcp 和 udp的区别?
      • 82.tcp 为什么要三次握手,两次不行吗?为什么?
      • 83.说一下 tcp 粘包是怎么产生的?
      • 84.OSI 的七层模型都有哪些?
      • 85.get 和 post 请求有哪些区别?
      • 86.如何实现跨域?
      • 87.说一下 JSONP 实现原理?
    • 九、设计模式
      • 88.说一下你熟悉的设计模式?
      • 89.简单工厂和抽象工厂有什么区别?
    • 十、Spring/Spring MVC
      • 90.为什么要使用 Spring?
      • 91.解释一下什么是 aop?
      • 92.解释一下什么是 ioc?
      • 93.Spring 有哪些主要模块?
      • 94.Spring 常用的注入方式有哪些?
      • 95.Spring 中的 bean 是线程安全的吗?
      • 96.Spring 支持几种 bean 的作用域?
      • 97.Spring 自动装配 bean 有哪些方式?
      • 98.Spring 事务实现方式有哪些?
      • 99.说一下 Spring 的事务隔离?
      • 100.说一下 Spring mvc 运行流程?
      • 101.Spring mvc 有哪些组件?
      • 102.@RequestMapping 的作用是什么?
      • 103.@Autowired 的作用是什么?
    • 十一、Spring Boot/Spring Cloud
      • 104.什么是 Spring boot?
      • 105.为什么要用 Spring boot?
      • 106.Spring boot 核心配置文件是什么?
      • 107.Spring boot 配置文件有哪几种类型?它们有什么区别?
      • 108.Spring boot 有哪些方式可以实现热部署?
      • 109.jpa 和 hibernate 有什么区别?
      • 110.什么是 Spring cloud?
      • 111.Spring cloud 断路器的作用是什么?
      • 112.Spring cloud 的核心组件有哪些?
    • 十二、Hibernate
      • 113.为什么要使用 hibernate?
      • 114.什么是 ORM 框架?
      • 115.hibernate 中如何在控制台查看打印的 sql 语句?
      • 116.hibernate 有几种查询方式?
      • 117.hibernate 实体类可以被定义为 final 吗?
      • 118.在 hibernate 中使用 Integer 和 int 做映射有什么区别?
      • 119.hibernate 是如何工作的?
      • 120.get()和 load()的区别?
      • 121.说一下 hibernate 的缓存机制?
      • 122.hibernate 对象有哪些状态?
      • 123.在 hibernate 中 getCurrentSession 和 openSession 的区别是什么?
      • 124.hibernate 实体类必须要有无参构造函数吗?为什么?
    • 十三、Mybatis
      • 125.mybatis 中 #{}和 ${}的区别是什么?
      • 126.mybatis 有几种分页方式?
      • 127.RowBounds 是一次性查询全部结果吗?为什么?
      • 128.mybatis 逻辑分页和物理分页的区别是什么?
      • 129.mybatis 是否支持延迟加载?延迟加载的原理是什么?
      • 130.说一下 mybatis 的一级缓存和二级缓存?
      • 131.mybatis 和 hibernate 的区别有哪些?
      • 132.mybatis 有哪些执行器(Executor)?
      • 133.mybatis 分页插件的实现原理是什么?
      • 134.mybatis 如何编写一个自定义插件?
    • 十四、RabbitMQ
      • 135.rabbitmq 的使用场景有哪些?
      • 136.rabbitmq 有哪些重要的角色?
      • 137.rabbitmq 有哪些重要的组件?
      • 138.rabbitmq 中 vhost 的作用是什么?
      • 139.rabbitmq 的消息是怎么发送的?
      • 140.rabbitmq 怎么保证消息的稳定性?
      • 141.rabbitmq 怎么避免消息丢失?
      • 142.要保证消息持久化成功的条件有哪些?
      • 143.rabbitmq 持久化有什么缺点?
      • 144.rabbitmq 有几种广播类型?
      • 145.rabbitmq 怎么实现延迟消息队列?
      • 146.rabbitmq 集群有什么用?
      • 147.rabbitmq 节点的类型有哪些?
      • 148.rabbitmq 集群搭建需要注意哪些问题?
      • 149.rabbitmq 每个节点是其他节点的完整拷贝吗?为什么?
      • 150.rabbitmq 集群中唯一一个磁盘节点崩溃了会发生什么情况?
      • 151.rabbitmq 对集群节点停止顺序有要求吗?
    • 十七、MySql
      • 164.数据库的三范式是什么?
      • 165.一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?
      • 166.如何获取当前数据库版本?
      • 167.说一下事务及其四大特性(ACID)是什么?
      • 168.char 和 varchar 的区别是什么?
      • 169.float 和 double 的区别是什么?
      • 170.mysql 的内连接、左连接、右连接有什么区别?
      • 171.mysql 索引是怎么实现的?
      • 172.怎么验证 mysql 的索引是否满足需求?
      • 173.说一下数据库的事务隔离
      • 174.说一下 mysql 常用的引擎?
      • 175.说一下 mysql 的行锁和表锁?
      • 176.说一下乐观锁和悲观锁?
      • 177.mysql 问题排查都有哪些手段?
      • 178.如何做 mysql 的性能优化?
    • 十八、Redis
      • 179.redis 是什么?都有哪些使用场景?
      • 180.redis 有哪些功能?
      • 181.redis 和 memecache 有什么区别?
      • 182.redis 为什么是单线程的?
      • 183.什么是缓存雪崩、缓存穿透、缓存击穿?怎么解决?
      • 184.redis 支持的数据类型有哪些?
      • 185.redis 支持的 java 客户端都有哪些?
      • 186.jedis 和 redisson 有哪些区别?
      • 187.怎么保证缓存和数据库数据的一致性?
      • 188.redis 持久化有几种方式?
      • 189.redis 怎么实现分布式锁?
      • 190.redis 分布式锁有什么缺陷?
      • 191.redis 如何做内存优化?
      • 192.redis 淘汰策略有哪些?
      • 193.redis 常见的性能问题有哪些?该如何解决?
    • 十九、JVM
      • 194.说一下 jvm 的主要组成部分?及其作用?
      • 195.说一下 jvm 运行时数据区?
      • 196.说一下堆栈的区别?
      • 197.队列和栈是什么?有什么区别?
      • 198.什么是双亲委派模型?
      • 199.说一下类加载的执行过程?
      • 200.怎么判断对象是否可以被回收?
      • 201.java 中都有哪些引用类型?
      • 202.说一下 jvm 有哪些垃圾回收算法?
      • 203.说一下 jvm 有哪些垃圾回收器?
      • 204.详细介绍一下 CMS 垃圾回收器?
      • 205.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
      • 206.简述分代垃圾回收器是怎么工作的?
      • 207.说一下 jvm 调优的工具?
      • 208.常用的 jvm 调优的参数都有哪些?

一、Java基础

1.JDK 和 JRE 有什么区别?

JDK:它是Java开发运行环境,在程序员的电脑上当然要安装JDK;

JRE:Java Runtime Environment它是Java运行环境,如果你不需要开发只需要运行Java程序,那么你可以安装JRE。例如程序员开发出的程序最终卖给了用户,用户不用开发,只需要运行程序,所以用户在电脑上安装JRE即可。

JDK包含了JRE。

JRE中包含虚拟机JVM

JRE顾名思义是java运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。
JDK顾名思义是java开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。
如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。

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

==号是内存地址的比较,equals是基于内容的比较。常见的Java类都重写了equals方法,因此我们可以直接通过equals比较对象内容。但是我们自己编写的实体类,如果没有重写equals方法,那么比较的也是内存地址。

3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

不对,hashCode相同,不代表内容也相同。

        String str1="通话";
        String str2="重地";
        System.out.println("str1.hashCode() = "+str1.hashCode());
        System.out.println("str2.hashCode() = "+str2.hashCode());
        System.out.println(str1.equals(str2));
		/*
		输出:
		str1.hashCode() = 1179395
		str2.hashCode() = 1179395
		false
		*/

代码解读:很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 却为 false

因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。

4.final 在 java 中有什么作用?

final关键字可以修饰类,修饰方法,修饰变量。

修饰类:当前类是一个final类,不可以被继承

修饰方法:不可被重写

修饰变量:不可改变值,如果是引用类型,final修饰的变量的地址不可改变,但是可以改变引用对象的属性。

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

Math.round的实现是加0.5然后向下取整,所以Mathround(-1.5),-1.5+0.5=-1,-1向下取整还是-1

6.String 属于基础的数据类型吗?

不是,基础的数据类型只有byte,char,short,int,long,floag,double,boolean

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

String,StringBuilder,StringBuffer

其中String类是不可变的字符串,而另外两个类都是可以对字符串进行追加的.看一下追加扩容的方法

String str="hello";
        str=str+"world";
//首先会在常量池中生成“hello”字符串,然后会生成“world”字符串,运算后会生成一个“hello world”字符串
        StringBuilder stringBuilder=new StringBuilder("hello");
        stringBuilder.append(" world");
        System.out.println(stringBuilder);
//StringBuilder和StringBuffer区别不大,唯一的区别是StringBuffer是线程安全的(同步的)

8.String str="i"与 String str=new String(“i”)一样吗?

不一样,使用String str=“i”,java虚拟机会把它分配到常量池中,而 String str=new String(“i”)创建了一个对象,会被分到堆内存中。

Java为了避免产生大量的String对象,设计了一个字符串常量池。工作原理是这样的,创建一个字符串时,JVM首先为检查字符串常量池中是否有值相等的字符串,如果有,则不再创建,直接返回该字符串的引用地址,若没有,则创建,然后放到字符串常量池中,并返回新创建的字符串的引用地址。

所以,当你创建一使用String str="i"创建一个字符串时,str指向的是常量池中的这个字段。

String str=new String(“i”)使用的是标准的对象创建方式

一个对象创建时,在虚拟机中的执行过程如下:Object obj会反映到java虚拟机栈的变量表中,作为一个引用类型数据出现,“new Object()”会反映到java堆中,在java堆上创建一个Object类型的实例数据值的结构化内存,这块内存的长度是不固定的。在java堆中还存放了了能查到此对象类型数据(对象类型、父类、接口、方法等)的地址信息,这些信息存放在方法区中。

简单来讲,String str=new String(“i”)把对象分到了堆内存中,String str="i"将对象分配到了字符串常量池中。

9.如何将字符串反转?

  • 方法一:将字符串转换为字符数组,使用首位指针对数组进行反转
//思路:将字符串通过toCharArray()方法转换为字符数组,用首位指针遍历整个数组并进行交换,通过String的构造方法,将字符数组转为字符串
public class TestStringReverse {
    public static void reverse(String str) {
        char[] char=str.toCharArray();
        for (int i = 0, j = char.length; i < j; i++, j--) {
            char temp = char[i];
            char[i]=char[j];
            char[j]=temp;
        }
        system.out.println(new String( char));
    }
}
  • 方法二:将字符串转换为StringBuffer,通过StringBuffer的构造器将String转为StringBuffer,调用StringBuffer的reverse()
    方法,最后调用StringBuffer的toString方法将StringBuffer转换为String。
  public class TestStringReverse {
    public static void reverse(Stirng str) {
        StringBuffer sb = new StringBuffer(str);
        StirngBuffer str = sb.reverse();
        system.out.println(str.toString);
    }
}

10.String 类的常用方法都有那些?

  • .indexOf():返回指定字符的索引。
  • .charAt():返回指定索引处的字符。
  • .replace():字符串替换。
  • .trim():去除字符串两端空白。
  • .split():分割字符串,返回一个分割后的字符串数组。
  • .getBytes():返回字符串的 byte 类型数组。
  • .length():返回字符串长度。
  • .toLowerCase():将字符串转成小写字母。
  • .toUpperCase():将字符串转成大写字符。
  • .substring():截取字符串。
  • .equals():字符串比较。

11.抽象类必须要有抽象方法吗?

不需要

abstract class Cat {
    public static void sayHi() {
        System.out.println("hi~");
    }
}

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

请点击跳转

  • 普通类可以实例化,接口都不能被实例化(它没有构造方法),抽象类如果要实例化,抽象类必须指向实现所有抽象方法的子类对象(抽象类可以直接实例化,直接重写自己的抽象方法),接口必须指向实现所有所有接口方法的类对象。

  • 抽象类要被子类继承,接口要被子类实现。

  • 接口只能做方法的声明,抽象类可以做方法的声明,也可以做方法的实现。

  • 接口里定义的变量只能是公共的静态常量,抽象类中定义的变量是普通变量。

  • 抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类的抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如果不能全部实现接口方法,那么该类只能是抽象类。

  • 抽象方法只能声明,不能实现。接口是设计的结果,抽象类是重构的结果。

  • 抽象类里可以没有抽象方法。

  • 如果一个类里有抽象方法,那么该类只能是抽象类。

  • 抽象方法要被实现,所以不能是静态的,也不能是私有的。

  • 接口可以继承接口,并可多继承接口,但类只能单继承。(重要啊)

  • 接口中的常量:有固定的修饰符-public static final(不能用private和protected修饰/本质上都是static的而且是final类型的,不管加不加static修饰)。

  • 接口中的抽象方法:有固定的修饰符-public abstract 。

  • 接口细节:

    • 若接口中方法或变量没有写public,static,final / public,abstract ,会自动补齐 。

    • 接口中的成员都是共有的。

    • 接口与接口之间是继承关系,而且可以多继承。

    • 接口不能被实例化

    • 一个类可以实现多个接口

    • 在java开发中,我们经常把常用的变量,定义在接口中,作为全局变量使用,访问形式:接口名.变量名。

    • 一个接口不能继承其它的类,但是可以继承别的接口

    • 一个重要的原则:当一个类实现了一个接口,要求该类把这个接口的所有方法全部实现

  • 注意:

  • 抽象类和接口都是用来抽象具体的对象的,但是接口的抽象级别更高。

  • 抽象类可以有具体的方法和属性,接口只能有抽象方法和静态常量。

  • 抽象类主要用来抽象级别,接口主要用来抽象功能。

  • 抽象类中,且不包含任何的实现,派生类必须覆盖它们。接口中所有方法都必须是未实现的。

  • 接口方法,访问权限必须是公共的 public。

  • 接口内只能有公共方法,不能存在成员变量。

  • 接口内只能包含未被实现的方法,也叫抽象方法,但是不能用 abstract 关键字。

  • 抽象类的访问速度比接口要快,接口是稍微有点慢,因为它需要时间去寻找在类中实现的方法。

  • 抽象类,除了不能被实例化外,与普通 java 类没有任何区别。

  • 抽象类可以有 main 方法,接口没有 main 方法。

  • 抽象类可以用构造器,接口没有。

  • 抽象方法可以有 public、protected 和 default 这些修饰符,接口只能使用默认 public。

  • 抽象类,添加新方法可以提供默认的实现,不需要改变原有代码。接口添加新方法,子类必须实现。

  • 抽象类的子类用 extends 关键字继承,接口用 implements 来实现。

  • 什么时候用抽象类和接口

    • 若果你拥有一些方法并且想让他们中的一些有默认实现,那就用抽象类。
    • 如果你想实现多重继承,那么必须使用接口。由于 java 不支持多继承,子类不能继承多个父类,但是可以实现多个接口,因此你可以使用接口来实现它。
    • 如果基本基本功能在不断变化,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么所有实现类都需要改变。

13.抽象类能使用 final 修饰吗?

final修饰的类不能被继承,没有子类。如果类中有抽象的方法也是没有意义的。abstract类为抽象类。即该类只关心子类具有的功能,而不是功能的具体实现。如果 用final修饰方法,那么该方法则不能再被重写。final
是不能修饰abstract所修饰的方法的。

14.接口和抽象类有什么区别?

见12题

15.java 中 IO 流分为几种?

  • 按照流的流向分,可以分为输入流和输出流;

  • 按照操作单元划分,可以划分为字节流和字符流;

  • 按照流的角色划分为节点流和处理流。

Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO流的40多个类都是从如下4个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

Java面试题整理(带答案)_第1张图片

16.BIO、NIO、AIO 有什么区别?

IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。

  • BIO

    在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。

  • NIO

    NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题:在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。如果客户端的请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题。HTTP/1.1出现后,有了Http长连接,这样除了超时和指明特定关闭的httpheader外,这个链接是一直打开的状态的,这样在NIO处理中可以进一步的进化,在后端资源中可以实现资源池或者队列,当请求来的话,开启的线程把请求和请求数据传送给后端资源池或者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接的哪个请求等),这样前面的线程还是可以去接受其他的请求,而后端的应用的处理只需要执行队列里面的就可以了,这样请求处理和后端应用是异步的.当后端处理完,到全局地方得到现场,产生响应,这个就实现了异步处理。

  • AIO

    与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:

    • AsynchronousSocketChannel
    • AsynchronousServerSocketChannel
    • AsynchronousFileChannel
    • AsynchronousDatagramChannel

    其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。

  • 总结:

    • BIO是一个连接一个线程。
    • NIO是一个请求一个线程。
    • AIO是一个有效请求一个线程。
  • 例子: 先来个例子理解一下概念,以银行取款为例:

    • 同步:自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);

    • 异步:委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);

    • 阻塞:ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);

    • 非阻塞:柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)

Java对BIO、NIO、AIO的支持:

Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,

  • BIO、NIO、AIO适用场景分析:

    • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
    • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
    • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

    另外,I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。

  • Reactor模式和Proactor模式

    在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。在比较这两个模式之前,我们首先的搞明白几个概念,什么是阻塞和非阻塞,什么是同步和异步,同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。

  • 四种I/O模型

  1. 同步阻塞IO: 在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式!

  2. 同步非阻塞IO: 在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。

  3. 异步阻塞IO:此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!

  4. 异步非阻塞IO:在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型。

17.Files的常用方法都有哪些?

  • Files.exists():检测文件路径是否存在。
  • Files.createFile():创建文件。
  • Files.createDirectory():创建文件夹。
  • Files.delete():删除一个文件或目录。
  • Files.copy():复制文件。
  • Files.move():移动文件。
  • Files.size():查看文件个数。
  • Files.read():读取文件。
  • Files.write():写入文件。

二、容器

18.java 容器都有哪些?

  • Collection
    • List
      • ArrayList
      • LinkedList
      • Vector
        • Stack
    • Set
      • HashSet
      • LinkedHashSet
      • TreeSet
  • Map
    • HashMap
      • LinkedHashMap
    • TreeMap
    • ConcurrentHashMap
    • Hashtable

19.Collection 和 Collections 有什么区别?

  • Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
  • Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。 Collections.unmodifiableCollection(
    Collection c)可以实现禁止修改一个Collection集合 Collections.synchronizedCollection(Collection c)可以实现集合类的线程同步

20.List、Set、Map 之间的区别是什么?

  • 元素重复性:

    • List允许有重复的元素。任何数量的重复元素都可以在不影响现有重复元素的值及其索引的情况下插入到List集合中;
    • Set集合不允许元素重复。Set以及所有实现了Set接口的类都不允许重复值的插入,若多次插入同一个元素时,在该集合中只显示一个;
    • Map以键值对的形式对元素进行存储。Map不允许有重复键,但允许有不同键对应的重复的值;
  • 元素的有序性:

    • List及其所有实现类保持了每个元素的插入顺序;
    • Set中的元素都是无序的;但是某些Set的实现类以某种殊形式对其中的元素进行排序,如:LinkedHashSet按照元素的插入顺序进行排序;
    • Map跟Set一样对元素进行无序存储,但其某些实现类对元素进行了排序。如:TreeMap根据键对其中的元素进行升序排序;
  • 元素是否为空值:

    • List允许任意数量的空值;
    • Set最多允许一个空值的出现;当向Set集合中添加多个null值时,在该Set集合中只会显示一个null元素
    • Map只允许出现一个空键,但允许出现任意数量的空值;(HashTable不允许空键和空值)
  • 总结:

    • List中的元素,有序、可重复、可为空;
    • Set中的元素,无序、不重复、只有一个空元素;
    • Map中的元素,无序、键不重,值可重、可一个空键、多可空值;

21.HashMap 和 Hashtable 有什么区别?

  • HashMap时HashTable的轻量级实现(非线程安全的实现),它们都实现了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上高于HashTable。
  • HashMap允许将null作为一个entry的key或者value,而HashTable不允许。
  • HashMap去掉了HashTable的contains方法,改成containsValue和containsKey方法。
  • 二者最大的不同是,HashTable的方法是synchronized(线程安全的),而HashMap不是,在多个线程访问HashTable时,不需要自己为它的方法实现同步,而HashMap就必须为之提供外同步。

22.如何决定使用 HashMap 还是 TreeMap?

  • HashMap基于散列桶(数组和链表)实现;TreeMap基于红黑树实现。
  • HashMap不支持排序;TreeMap默认是按照Key值升序排序的,可指定排序的比较器,主要用于存入元素时对元素进行自动排序。
  • HashMap大多数情况下有更好的性能,尤其是读数据。在没有排序要求的情况下,使用HashMap。

23.说一下 HashMap 的实现原理?

  • 存储结构
    HashMap的主干是一个Entry数组(JDK8开始使用Node数组,Node是实现了Entry接口)。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。JDK8以前采用数组加链表的形式实现,JDK8开始采用数组加链表加红黑树的方式实现

  • 存放过程

    public V put(K key,V value){
        return putVal(hash(key),key,value,false,true);
        }
  • 参数解读 putVal(hash(key),key,value,false,true);

    • hash:根据key生成hash值。

      static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }  
      
    • key、value就不用说了

    • onlyIfAbsent:当要插入的key已存在时是否替换value,false则替换,true则不替换。

    • evict:在HashMap中无作用,afterNodeInsertion方法在LinkedHashMap实现。

      if (++size > threshold)
      resize();
      afterNodeInsertion(evict);
      return null;
      
  • 总体流程

    • 根据key生成hash值。
    • 根据key的hash和Node数组长度生成下标,找到对应数组元素。
    • 对应元素为空时直接new一个Node(Entry)塞进去。
    • 对应元素不为空且key正好和插入的key相同时,替换元素的值。
    • 对应元素不为空且key和要插入的key不同时判断对应元素是不是TreeNode(红黑树节点),是的话插入元素。
    • 不是的话说明后面的是NULL或链表,找对应元素的nextNode。
    • nextNode为NULL时new一个Node塞进去。
    • nextNode不为NULL但是key和要插入的key相同时,修改nextNode的值。
    • nextNode不为NULL且key和要插入的key不同时就去找nextNode的nextNode知道找到NULL或key相同的nextNode。
    • 在n个nextNode之后找到NULL后,要判断长度是否超过默认的链表允许长度,超过了则把链表替换为红黑树。

24.说一下 HashSet 的实现原理?

首先,我们需要知道它是Set的一个实现,所以保证了当中没有重复的元素。一方面Set中最重要的一个操作就是查找。而且通常我们会选择

  1. 基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

  2. 当我们试图把某个类的对象当成 HashMap的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的equals(Object obj)方法和 hashCode()方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为equals() 比较的标准。

  3. HashSet的其他操作都是基于HashMap的。

25.ArrayList 和 LinkedList 的区别是什么?

  • ArrayList基于数组实现,LinkedList基于双端队列实现
  • ArrayList由于是基于数组实现的,它的随机访问要快于LinkedList
    • 常规说法ArrayList的插入要慢于LinkedList,需要考虑头部插入和尾部插入两种情况
import java.util.ArrayList;
import java.util.LinkedList;

/**
 * @Author: zhaodi
 * @Company: GUET
 * @Date: 2022/3/18
 * @Description:
 */
public class ListTest {
    public static void main(String[] args) {
        endInsert();
        headInsert();
    }

    public static void headInsert() {
        System.out.println("----------头插法---------");
        ArrayList<Integer> arrayList = new ArrayList<>();
        LinkedList<Integer> linkedList = new LinkedList<>();
        long startTime = System.currentTimeMillis();
        int n = 100000;
        for (int i = 0; i < n; i++) {
            arrayList.add(0, i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("arrayList添加" + n + "条记录总耗时" + (endTime - startTime));

        startTime = System.currentTimeMillis();
        for (int i = 0; i < n; i++) {
            linkedList.addFirst(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("linkedList添加" + n + "条记录总耗时" + (endTime - startTime));
    }

    public static void endInsert() {
        System.out.println("----------尾插法---------");
        ArrayList<Integer> arrayList = new ArrayList<>();
        LinkedList<Integer> linkedList = new LinkedList<>();
        long startTime = System.currentTimeMillis();
        int n = 10000000;
        for (int i = 0; i < n; i++) {
            arrayList.add(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("arrayList添加" + n + "条记录总耗时" + (endTime - startTime));

        startTime = System.currentTimeMillis();
        for (int i = 0; i < n; i++) {
            linkedList.addLast(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("linkedList添加" + n + "条记录总耗时" + (endTime - startTime));
    }
}
----------尾插法---------
arrayList添加10000000条记录总耗时3028
linkedList添加10000000条记录总耗时5661
----------头插法---------
arrayList添加100000条记录总耗时413
linkedList添加100000条记录总耗时3

结果分析:

  • 采用尾插法,ArrayList的速度要快于LinkedList,这是因为ArrayList直接往下标index的位置进行添加数据,采用LinkedList,每次添加数据要构建一个Node对象,将数据放入才能建立链表连接,所以LinkedList速度要慢

  • 同理分析,LinkedList删除也不一定比ArrayList快,因为LinkedList删除需要定位元素,需要查找

26.如何实现数组和 List 之间的转换?

public class ArrayToList {

    public static void main(String[] args) {

        //数组转list
        String[] str = new String[]{"hello", "world"};
        //方式一:使用for循环把数组元素加进list
        List<String> list = new ArrayList<String>();
        for (String string : str) {
            list.add(string);
        }
        System.out.println(list);

        //方式二:
        List<String> list2 = new ArrayList<String>(Arrays.asList(str));
        System.out.println(list2);

        //方式三:
        //同方法二一样使用了asList()方法。这不是最好的,
        //因为asList()返回的列表的大小是固定的。
        //事实上,返回的列表不是java.util.ArrayList类,而是定义在java.util.Arrays中一个私有静态类java.util.Arrays.ArrayList
        //我们知道ArrayList的实现本质上是一个数组,而asList()返回的列表是由原始数组支持的固定大小的列表。
        //这种情况下,如果添加或删除列表中的元素,程序会抛出异常UnsupportedOperationException。
        //java.util.Arrays.ArrayList类具有 set(),get(),contains()等方法,但是不具有添加add()或删除remove()方法,所以调用add()方法会报错。
        List<String> list3 = Arrays.asList(str);
        //list3.remove(1);
        //boolean contains = list3.contains("s");
        //System.out.println(contains);
        System.out.println(list3);

        //方式四:使用Collections.addAll()
        List<String> list4 = new ArrayList<String>(str.length);
        Collections.addAll(list4, str);
        System.out.println(list4);

        //方式五:使用Stream中的Collector收集器
        //转换后的List 属于 java.util.ArrayList 能进行正常的增删查操作
        List<String> list5 = Stream.of(str).collect(Collectors.toList());
        System.out.println(list5);
    }
}

package com.guet.se.list;

import java.util.ArrayList;
import java.util.List;

public class ListToArray {

    public static void main(String[] args) {
        //list转数组
        List<String> list = new ArrayList<>();
        list.add("hello");
        list.add("world");

        //方式一:使用for循环
        String[] str1 = new String[list.size()];
        for (int i = 0; i < list.size(); i++) {
            str1[i] = list.get(i);
        }
        for (String string : str1) {
            System.out.println(string);
        }

        //方式二:使用toArray()方法
        //list.toArray(T[]  a); 将list转化为你所需要类型的数组
        String[] str2 = list.toArray(new String[list.size()]);
        for (String string : str2) {
            System.out.println(string);
        }

        //错误方式:易错   list.toArray()返回的是Object[]数组,怎么可以转型为String
        //String[]  strings= (String[]) list.toArray();
    }
}

27.ArrayList 和 Vector 的区别是什么?

  • 相同点
    • ArrayList和Vector都是基于数组实现的,都支持下标索引
  • 不同点
    • ArrayList是线程不安全的,Vector是线程安全的
    • ArrayList由于不是同步,所以性能要优于Vector
    • ArrayList扩容时会变为原来的1.5倍,Vector如果没有设置capacityIncrement,会默认扩充两倍,如果设置了capacityIncrement,则会每次增加capacityIncrement

28.Array 和 ArrayList 有何区别?

  • Array是数组,不支持动态扩容,需要在初始化时为其进行分配空间大小,ArrayList是动态数组,支持动态扩容
  • ArrayList本质上是使用数组来实现存储的
  • Array支持基本数据类型的存储,也支持引用类型的存储,ArrayList不支持基本数据类型

29.在 Queue 中 poll()和 remove()有什么区别?

poll方法和remove方法都会移除队列中的对头元素,如果队列为空,remove会抛出一个异常,而poll会返回空

30.哪些集合类是线程安全的?

ConcurrentHashMap、Vector、Stack、CopyOnWriteArrayList、ConcurrentLinkedQueue Map中的HashTable虽然不是集合类,但是也是线程安全的

31.迭代器 Iterator 是什么?

Java集合框架的集合类,我们有时候称之为容器。容器的种类有很多种,比如ArrayList、LinkedList、HashSet...,每种容器都有自己的特点,ArrayList底层维护的是一个数组;LinkedList是链表结构的;HashSet依赖的是哈希表,每种容器都有自己特有的数据结构。因为容器的内部结构不同,很多时候可能不知道该怎样去遍历一个容器中的元素。所以为了使对容器内元素的操作更为简单,Java引入了迭代器模式!把访问逻辑从不同类型的集合类中抽取出来,从而避免向外部暴露集合的内部结构。

32.Iterator 怎么使用?有什么特点?

        List<Integer> list=new ArrayList<>();
        Iterator iterator=list.iterator();
        while(iterator.hasNext()){
        Integer next=iterator.next();
        }

特点:

  • 不可回退

  • 在使用Iterator的时候禁止对所遍历的容器进行改变集合的大小结构的操作。在使用Iterator进行迭代时,如果对集合进行了add、remove操作就会出现ConcurrentModificationException异常。因为在你迭代之前,迭代器已经被通过list.itertor()创建出来了,如果在迭代的过程中,又对list进行了改变其容器大小的操作,那么Java就会给出异常。因为此时Iterator对象已经无法主动同步list做出的改变,Java会认为你做出这样的操作是线程不安全的,就会给出善意的提醒(抛出ConcurrentModificationException异常)

33.Iterator 和 ListIterator 有什么区别?

我们在使用List,Set的时候,为了实现对其数据的遍历,我们经常使用到了Iterator(迭代器)。使用迭代器,你不需要干涉其遍历的过程,只需要每次取出一个你想要的数据进行处理就可以了。但是在使用的时候也是有不同的。List和Set都有iterator()来取得其迭代器。对List来说,你也可以通过listIterator()取得其迭代器,两种迭代器在有些时候是不能通用的,Iterator和ListIterator主要区别在以下方面:

  • ListIterator有add()方法,可以向List中添加对象,而Iterator不能

  • ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。

  • ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。

  • 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。

因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。其实,数组对象也可以用迭代器来实现。

34.怎么确保一个集合不能被修改?

Collections工具类下有Collections.unmodifiableCollection(Collection c)方法

public static<T> Collection<T> unmodifiableCollection(Collection<?extends T> c){
        return new UnmodifiableCollection<>(c);
        }

三、多线程

35.并行和并发有什么区别?

  • 并行:同时在运行
  • 并发:交替执行

36.线程和进程的区别?

类似”进程是资源分配的最小单位,线程是CPU调度的最小单位“这样的回答感觉太抽象,都不太容易让人理解。 做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-> “互斥锁”
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-> “信号量”

37.守护线程是什么?

Java线程分为用户线程和守护线程。

守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。

Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法。

38.创建线程有哪几种方式?

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 线程池方式 new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
    BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

39.说一下 runnable 和 callable 有什么区别?

  • runnable不支持线程返回值,callable可以结合FutureTask拿到线程的返回值
  • runnable的线程体是run方法,callable的线程体是call方法
  • 实现了runnable接口异常无法通过throws抛出异常,实现了callable接口后可以直接抛出Exception异常

40.线程有哪些状态?

  • 新建(NEW)

    new Thread() 新创建了一个线程对象。

  • 可运行(RUNNABLE)

    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权

  • 运行(RUNNING)

    被CPU选中运行

  • 阻塞(BLOCKED)

    阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

    • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

    • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

    • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或thread.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

  • 死亡(DEAD)

    线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

Java面试题整理(带答案)_第2张图片

41.sleep() 和 wait() 有什么区别?

  • 这两个方法来自不同的类分别是Thread和Object,sleep方法属于Thread类中的静态方法,wait属于Object的成员方法。

  • sleep()是线程类(Thread)的方法,不涉及线程通信,调用时会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,用于线程间的通信,调用时会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才进入对象锁定池准备获得对象锁进入运行状态。

  • wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)。

  • sleep()方法必须捕获异常InterruptedException,而wait()、notify()以及notifyAll()不需要捕获异常。

注意:

  • sleep方法只让出了CPU,而并不会释放同步资源锁。
  • 线程执行sleep()方法后会转入阻塞状态。
  • sleep()方法指定的时间为线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。
  • notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度。

42.notify()和 notifyAll()有什么区别?

notify()随机唤醒一个线程,notifyAll()唤醒所有线程去抢锁

43.线程的 run()和 start()有什么区别?

  • run()方法是线程的线程体,start()方法是线程开始执行,等待CPU调度
  • 如果通过thread.run()方法调用线程体,那么run()就是一个普通的方法
  • 如果通过调用thread.start()方法,则会以线程的方式运行run()方法

44.创建线程池有哪几种方式?

通常开发者都是利用Executors提供的通用线程池创建方法,去创建不同配置的线程池,Executors目前提供了5种不同的线程池创建配置:

  • newCachedThreadPool(),它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置时间超过60秒,则被终止并移除缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列,没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者。

  • newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads。

  • newSingleThreadExecutor(),它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目。

  • newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。

  • newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

阿里不建议采用以下几种方式创建线程池

  • newCachedThreadPool()会创建无限的非核心线程,不会存储任务队列。如果瞬时过来很多任务,会创建大量的非核心线程,当这些任务执行完成之后,仍然会占用CPU,形成CPU的空转

  • newFixedThreadPool(int nThreads)和newSingleThreadExecutor()的区别是核心线程的数目略有不同,前者可以自定义核心线程的数目,后者只有一个。二者的缓冲队列都是无限大的,如果任务过多堆积,会导致OOM错误

45.线程池都有哪些状态?

  • RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。

  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。

  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

  • TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。

    • 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
    • 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
  • TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

46.线程池中 submit()和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在Executor 接口中。 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。

47.在 java 程序中怎么保证多线程的运行安全?

线程的安全性问题体现在:

  • 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
  • 有序性:程序执行的顺序按照代码的先后顺序执行

导致原因:

  • 缓存导致的可见性问题
  • 线程切换带来的原子性问题
  • 编译优化带来的有序性问题

解决办法:

  • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、lock,可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题

Happens-Before 规则如下:

  • 程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

48.多线程锁的升级原理是什么?

锁的级别从低到高:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

锁分级别原因

没有优化以前,synchronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。

分级后的锁的级别

  • 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

  • 偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。

  • 轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

  • 重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

锁的状态对比

Java面试题整理(带答案)_第3张图片

49.什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

50.怎么防止死锁?

  • 避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定。主线程要对 A、B 两个对象的 Lock 进行锁定,副线程也要对 A、B 两个对象的 Lock 进行锁定,这就埋下了导致死锁的隐患。

  • 具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。比如上面的死锁程序,主线程先对 A 对象的 Lock 加锁,再对 B 对象的 Lock 加锁;而副线程则先对 B 对象的 Lock 加锁,再对 A 对象的 Lock 加锁。这种加锁顺序很容易形成嵌套锁定,进而导致死锁。如果让主线程、副线程按照相同的顺序加锁,就可以避免这个问题。

  • 使用定时锁。程序在调用 acquire() 方法加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。

  • 死锁检测。死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。

51.ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

52.说一下 synchronized 底层实现原理?

我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的:

package com.paddx.test.concurrent;

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

Java面试题整理(带答案)_第4张图片
关于这两条指令的作用,我们直接参考JVM规范中描述:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes
monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

这段话的大概意思为:

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by
objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of
the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to
enter the monitor are allowed to attempt to do so.

这段话的大概意思为:

  • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
  • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

我们再来看一下同步方法的反编译结果:

package com.paddx.test.concurrent;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

Java面试题整理(带答案)_第5张图片

从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

总结:

Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用Synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。

53.synchronized 和 volatile 的区别是什么?

java线程抽象内存模型

java的线程抽象内存模型中定义了每个线程都有一份自己的私有内存,里面存放自己私有的数据,其他线程不能直接访问,而一些共享数据则存在主内存中,供所有线程进行访问。
上图中,如果线程A和线程B要进行通信,就要经过主内存,比如线程B要获取线程A修改后的共享变量的值,要经过下面两步:

  • 线程A修改自己的共享变量副本,并刷新到了主内存中。
  • 线程B读取主内存中被A更新过的共享变量的值,同步到自己的共享变量副本中。

java多线程中的原子性、可见性、有序性

  • 原子性:是指线程的多个操作是一个整体,不能被分割,要么就不执行,要么就全部执行完,中间不能被打断。
  • 可见性:是指线程之间的可见性,就是一个线程修改后的结果,其他的线程能够立马知道。
  • 有序性:为了提高执行效率,java中的编译器和处理器可以对指令进行重新排序,重新排序会影响多线程并发的正确性,有序性就是要保证不进行重新排序(保证线程操作的执行顺序)。

volatile关键字的作用

其实volatile关键字的作用就是保证了可见性和有序性(不保证原子性),如果一个共享变量被volatile关键字修饰,那么如果一个线程修改了这个共享变量后,其他线程是立马可知的。为什么是这样的呢?比如,线程A修改了自己的共享变量副本,这时如果该共享变量没有被volatile修饰,那么本次修改不一定会马上将修改结果刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是没有被A修改之前的值。如果该共享变量被volatile修饰了,那么本次修改结果会强制立刻刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是被A修改之后的值了。volatile能禁止指令重新排序,在指令重排序优化时,在volatile变量之前的指令不能在volatile之后执行,在volatile之后的指令也不能在volatile之前执行,所以它保证了有序性。

synchronized关键字的作用

synchronized提供了同步锁的概念,被synchronized修饰的代码段可以防止被多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。因为synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作了,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了。

volatile关键字和synchronized关键字的区别

  • volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
  • volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。
  • volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

54.synchronized 和 Lock 有什么区别?

Java面试题整理(带答案)_第6张图片

55.synchronized 和 ReentrantLock 区别是什么?

共同点:

  • 都是用来协调多线程对共享对象、变量的访问
  • 都是可重入锁,同一线程可以多次获得同一个锁
  • 都保证了可见性和互斥性

不同点:

  • ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁

  • ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性

  • ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的

  • ReentrantLock 可以实现公平锁

  • ReentrantLock 通过 Condition 可以绑定多个条件

  • 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略

  • Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。

  • synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

  • Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。

  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  • Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等

56.说一下 atomic 的原理?

JDK Atomic开头的类,是通过 CAS 原理解决并发情况下原子性问题。 CAS 包含 3 个参数,CAS(V, E, N)。V 表示需要更新的变量,E 表示变量当前期望值,N 表示更新为的值。只有当变量 V 的值等于 E 时,变量 V 的值才会被更新为 N。如果变量 V 的值不等于 E ,说明变量 V 的值已经被更新过,当前线程什么也不做,返回更新失败。 当多个线程同时使用 CAS 更新一个变量时,只有一个线程可以更新成功,其他都失败。失败的线程不会被挂起,可以继续重试 CAS,也可以放弃操作。 CAS 操作的原子性是通过 CPU 单条指令完成而保障的。JDK 中是通过 Unsafe 类中的 API 完成的。 在并发量很高的情况,会有大量 CAS 更新失败,所以需要慎用。

四、反射

57.什么是反射?

Java反射? 在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法 Java反射机制主要提供了以下功能:

  • 1.在运行时判断任意一个对象所属的类。
  • 2.在运行时构造任意一个类的对象。
  • 3.在运行时判断任意一个类所具有的成员变量和方法。
  • 4.在运行时调用任意一个对象的方法。

58.什么是 java 序列化?什么情况下需要序列化?

  • 序列化:将 Java 对象转换成字节流的过程。
  • 反序列化:将字节流转换成 Java 对象的过程。

当 Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。 序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉
jvm 这个类的对象可以被序列化。

注意事项:

  • 某个类可以被序列化,则其子类也可以被序列化
  • 声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据
  • 反序列化读取序列化对象的顺序要保持一致

59.动态代理是什么?有哪些应用?

动态代理:
当想要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事务等。可以给这个类创建一个代理,故名思议就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活,扩展性强。
动态代理实现:
首先必须定义一个接口,还要有一个InvocationHandler(将实现接口的类的对象传递给它)处理类。再有一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()
可以产生代理对象,其实他只是一个产生代理对象的工具类)。利用到InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。
动态代理的应用:

  • Spring的AOP
  • 事务
  • 权限
  • 日志

60.怎么实现动态代理?

  • JDK原生动态代理:动态代理类和被代理类必须继承同一个接口。动态代理只能对接口中声明的方法进行代理。

    • Proxy类

      Proxy是所有动态代理的父类。它通过静态方法newProxyInstance()来创建动态代理的class对象和实例。

      • InvocationHandler

      每一个动态代理实例都有一个关联的InvocationHandler。通过代理实例调用方法,方法调用请求会被转发给InvocationHandler的invoke方法。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class TestJdkDynamicProxy {

    public static void main(String[] args) {
        IRegisterService iRegisterService = new RegisterServiceImpl();
        InsertDataHandler insertDataHandler = new InsertDataHandler();
        IRegisterService proxy = (IRegisterService) insertDataHandler.getProxy(iRegisterService);
        proxy.register("RyanLee", "123");
    }
}


class InsertDataHandler implements InvocationHandler {
    Object obj;

    public Object getProxy(Object obj) {
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        doBefore();
        Object result = method.invoke(obj, args);
        doAfter();
        return result;
    }

    private void doBefore() {
        System.out.println("[Proxy]一些前置处理");
    }

    private void doAfter() {
        System.out.println("[Proxy]一些后置处理");
    }
}
  • CGLib动态代理:CGLib(Code Generation Library)是一个基于ASM的字节码生成库。它允许我们在运行时对字节码进行修改或动态生成。CGLib通过继承被代理类的方式实现代理。
    • Enhancer:Enhancer指定要代理的目标对象。通过create方法得到代理对象。通过代理实例调用非final方法,方法调用请求会首先转发给MethodInterceptor的intercept
    • MethodInterceptor:通过代理实例调用方法,调用请求都会转发给intercept方法进行增强。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TestCGLibDynamicProxy {

    public static void main(String[] args) {
        IRegisterService iRegisterService = new RegisterServiceImpl();
        InsertDataInterceptor interceptor = new InsertDataInterceptor();
        RegisterServiceImpl proxy = (RegisterServiceImpl) interceptor.getProxy(iRegisterService);
        proxy.register("RyanLee", "123");
    }
}

class InsertDataInterceptor implements MethodInterceptor {
    Object target;

    public Object getProxy(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        // 回调方法
        enhancer.setCallback(this);
        // 创建代理对象
        return enhancer.create();
    }

    private void doBefore() {
        System.out.println("[Proxy]一些前置处理");
    }

    private void doAfter() {
        System.out.println("[Proxy]一些后置处理");
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        doBefore();
        Object result = methodProxy.invoke(target, objects);
        doAfter();
        return result;
    }
}

五、对象拷贝

61.为什么要使用克隆?

想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆了。克隆分浅克隆和深克隆,浅克隆后的对象中非基本对象和原对象指向同一块内存,因此对这些非基本对象的修改会同时更改克隆前后的对象。深克隆可以实现完全的克隆,可以用反射的方式或序列化的方式实现。

62.如何实现对象克隆?

  • 常规写法:new一个新对象,将被克隆对象的值挨个赋值给新对象
  • 实现Cloneable接口并重写Object类中的clone()方法
  • 使用序列化和反序列化可以完成对象的克隆(深克隆)

63.深拷贝和浅拷贝区别是什么?

浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝(例:assign()) 深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse()和JSON.stringify(),但是此方法无法复制函数类型)

六、Java Web

64.jsp 和 servlet 有什么区别?

Servlet:

Servlet 是一种服务器端的Java应用程序,具有独立于平台和协议的特性,可以生成动态的Web页面。它担当客户请求(Web浏览器或其他HTTP客户程序)与服务器响应(HTTP服务器上的数据库或应用程序)的中间层。

Servlet是位于Web服务器内部的服务器端的Java应用程序,与传统的从命令行启动的Java应用程序不同,Servlet由Web服务器进行加载,该Web服务器必须包含支持Servlet的Java虚拟机。

Jsp:

JSP 全名为Java Server Pages,中文名叫java服务器页面,其根本是一个简化的Servlet设计。JSP技术使用Java编程语言编写类XML的tags和scriptlets,来封装产生动态网页的处理逻辑。网页还能通过tags和scriptlets访问存在于服务端的资源的应用逻辑。JSP将网页逻辑与网页设计的显示分离,支持可重用的基于组件的设计,使基于Web的应用程序的开发变得迅速和容易。

JSP(JavaServer Pages)是一种动态页面技术,它的主要目的是将表示逻辑从Servlet中分离出来。

相同点

jsp经编译后就变成了servlet,jsp本质就是servlet,jvm只能识别java的类,不能识别jsp代码,web容器将jsp的代码编译成jvm能够识别的java类。

分析

其实就是当你通过 http 请求一个 JSP 页面是,首先 Tomcat 会调用 service()方法将JSP编译成为 Servlet,然后执行 Servlet。

详细理解:

当服务器启动后,当Web浏览器端发送过来一个页面请求时,Web服务器先判断是否是JSP页面请求。如果该页面只是一般的HTML/XML页面请求,则直接将HTML/XML页面代码传给Web浏览器端。如果请求的页面是JSP页面,则由JSP引擎检查该JSP页面,如果该页面是第一次被请求、或不是第一次被请求但已被修改,则JSP引擎将此JSP页面代码转换成Servlet代码,然后JSP引擎调用服务器端的Java编译器javac.exe对Servlet代码进行编译,把它变成字节码(.class)文件,然后再调用JAVA虚拟机执行该字节码文件,然后将执行结果传给Web浏览器端。如果该JSP页面不是第一次被请求,且没有被修改过,则直接由JSP引擎调用JAVA虚拟机执行已编译过的字节码.class文件,然后将结果传送Web浏览器端。

Java面试题整理(带答案)_第7张图片

不同点

  • JSP侧重视图,Servlet主要用于控制逻辑。
  • Servlet中没有内置对象。
  • JSP中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到。

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

  • HttpServletRequest类的Request对象:代表请求对象,主要用于接受客户端通过HTTP协议连接传输服务器端的数据。
  • HttpServletResponse类的Response对象:代表响应对象,主要用于向客户端发送数据。
  • JspWriter类的out对象:主要用于向客户端输出数据,out的基类是jspWriter
  • HttpSession类的session对象:主要用来分别保存每个用户的信息与请求关联的会话;会话状态的维持是web应用开发者必须面对的问题。
  • ServletContext类的application对象:主要用于保存用户信息,代码片段的运行环境;它是一个共享的内置对象,即一个容器中的多个用户共享一个application,故其保存的信息被所有用户所共享。
  • PageContext类的PageContext对象:管理网页属性,为jsp页面包装页面的上下文,管理对属于jsp的特殊可见部分中已经命名对象的访问,它的创建和初始化都是由容器来完成的。
  • ServletConfig类的Config对象:代码片段配置对象,标识Servlet的配置。
  • Object类的Page对象,处理jsp页面,是object类的一个实例,指的是jsp实现类的实例。
  • Exception对象:处理jsp文件执行时发生的错误和异常,只有在错误页面里才使用,前提是在页面指令里要有isErrorPage=true。

66.说一下 jsp 的 4 种作用域?

  • application 在所有应用程序中有效
  • session 在当前会话中有效
  • request 在当前请求中有效
  • page 在当前页面有效

67.session 和 cookie 有什么区别?

存储角度:

Session是服务器端的数据存储技术,cookie是客户端的数据存储技术

解决问题角度:

Session解决的是一个用户不同请求的数据共享问题,cookie解决的是不同请求的请求数据的共享问题

生命周期角度:

  • Session的id是依赖于cookie来进行存储的,浏览器关闭id就会失效
  • Cookie可以单独的设置其在浏览器的存储时间。

68.说一下 session 的工作原理?

其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。

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

一般默认情况下,在会话中,服务器存储 session 的 sessionid 是通过 cookie 存到浏览器里。

如果浏览器禁用了 cookie,浏览器请求服务器无法携带 sessionid,服务器无法识别请求中的用户身份,session失效。但是可以通过其他方法在禁用 cookie 的情况下,可以继续使用session。

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

70.Spring mvc 和 struts 的区别是什么?

71.如何避免 sql 注入?

  • 严格限制 Web 应用的数据库的操作权限,给连接数据库的用户提供满足需要的最低权限,最大限度的减少注入攻击对数据库的危害
  • 校验参数的数据格式是否合法(可以使用正则或特殊字符的判断)
  • 对进入数据库的特殊字符进行转义处理,或编码转换
  • 预编译 SQL(Java 中使用 PreparedStatement),参数化查询方式,避免 SQL 拼接
  • 发布前,利用工具进行 SQL 注入检测
  • 报错信息不要包含 SQL 信息输出到 Web 页面

72.什么是 XSS 攻击,如何避免?

攻击者往 web 页面里插入恶意的 HTML 代码(Javascript、css、html 标签等),当用户浏览该页面时,嵌入其中的 HTML 代码会被执行,从而达到恶意攻击用户的目的, 导致用户安全信息泄露(获取用户的敏感信息),危害数据安全例如盗取各类用户帐号、网站挂马、盗窃企业重要信息等。它与SQL注入攻击类似,SQL注入攻击中以SQL语句作为用户输入,从而达到查询/修改/删除数据的目的,而在xss攻击中,通过插入恶意脚本,实现对用户游览器的控制,获取用户的一些信息。

73.什么是 CSRF 攻击,如何避免?

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

攻击细节

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

例子

假如一家银行用以运行转账操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName那么,一个恶意攻击者可以在另一个网站上放置如下代码: 一个按钮或者图片,指向了如下链接:http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman,如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000的资金。这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。

七、异常

74.throw 和 throws 的区别?

  • throws:跟在方法声明后面,后面跟的是异常类名
  • throw:用在方法体内,后面跟的是异常类对象名

75.final、finally、finalize 有什么区别?

  • final:权限修饰符,用来修饰类,方法,变量,并具有不同的涵义

    • 修饰类: 代表此类不可以继承扩展
    • 修饰方法:代表此方法不可以重写
    • 修饰变量:变量不可以修改
  • finally:可以和try和try catch结构联合使用,在抛出异常后也会执行,通常用来关闭流或者其他无论是否发生异常都要完成的操作

  • finalize:Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没被引用时对这个对象调用的。它是在Object类中定义的,因此所的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

76.try-catch-finally 中哪个部分可以省略?

  • try{}catch(){}
  • try{}finally{}
  • try{}catch(){}finally{}

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

  • finally的作用就是,无论出现什么状况,finally里的代码一定会被执行。
  • 如果在catch中return了,也会在return之前,先执行finally代码块。
  • 而且如果finally代码块中含有return语句,会覆盖其他地方的return。
  • 对于基本数据类型的数据,在finally块中改变return的值对返回值没有影响,而对引用数据类型的数据会有影响。

finally不一定会被执行的情况:

  • 没有进入try代码块;
  • System.exit()强制退出程序;
  • 守护线程被终止;

78.常见的异常类有哪些?

  • NullPointerException 当应用程序试图访问空对象时,则抛出该异常。

  • SQLException 提供关于数据库访问错误或其他错误信息的异常。

  • IndexOutOfBoundsException指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。

  • NumberFormatException当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。

  • FileNotFoundException当试图打开指定路径名表示的文件失败时,抛出此异常。

  • IOException当发生某种I/O异常时,抛出此异常。此类是失败或中断的I/O操作生成的异常的通用类。

  • ClassCastException当试图将对象强制转换为不是实例的子类时,抛出该异常。

  • ArrayStoreException试图将错误类型的对象存储到一个对象数组时抛出的异常。

  • IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。

  • ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。

  • NegativeArraySizeException如果应用程序试图创建大小为负的数组,则抛出该异常。

  • NoSuchMethodException无法找到某一特定方法时,抛出该异常。

  • SecurityException由安全管理器抛出的异常,指示存在安全侵犯。

  • UnsupportedOperationException当不支持请求的操作时,抛出该异常。

  • RuntimeExceptionRuntimeException 是那些可能在Java虚拟机正常运行期间抛出的异常的超类。

  • OOM错误

  • StackOverflow错误

八、网络

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

301 Moved Permanently
被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。

302 Found
请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。

区别:

301 表示被请求 url 永久转移到新的 url;302 表示被请求 url 临时转移到新的 url。 301 搜索引擎会索引新 url 和新 url 页面的内容;302 搜索引擎可能会索引旧 url 和 新 url 的页面内容。

80.forward 和 redirect 的区别?

forward:请求转发
服务器内部转发,比如定位jsp
redirect:重定向
外部转发,由浏览器发起两次访问,地址栏会改变

81.简述 tcp 和 udp的区别?

tcp是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立起来。使用TCP协议传输数据,TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。当数据从A端传到B端后,B端会发送一个确认包(ACK包)给A端,告知A端数据我已收到!

UDP协议就没有这种确认机制,这就是为什么说TCP协议可靠,UDP协议不可靠,提供这种可靠服务,会加大网络带宽的开销,因为“虚拟信道”是持续存在的,同时网络中还会出现大量的ACK和FIN包。TCP协议提供了可靠的数据传输,但是其拥塞控制、数据校验、重传机制的网络开销很大,不适合实时通信,所以选择开销很小的UDP协议来传输数据。UDP协议是无连接的数据传输协议并且无重传机制,会发生丢包、收到重复包、乱序等情况。

区别:

  • 基于连接与无连接。
  • UDP不提供可靠性,不能保证数据能够到达目的地。
  • 对系统资源的要求(TCP较多,UDP少)。
  • UDP结构较简单。
  • TCP面向字节流模式,TCP会保证服务端按顺序接收到全部的字节流,UDP面向数据报模式,不保证顺序性。

很明显,当数据传输的性能必须让位于数据传输的完整性、可控制性和可靠性时,选择TCP协议。当强调传输性能而不是传输的完整性时,如音频和多媒体应用,UDP是最好的选择。在数据传输时间很短,以至于此前的连接过程成为整个流量主体的情况下,UDP也是一个好的选择,如DNS交换。UDP较低的开销使其有更好的机会去传送管理数据。TCP丰富的功能有时会导致不可预料的性能低下。

82.tcp 为什么要三次握手,两次不行吗?为什么?

tcp是可靠连接,三次握手保证了连接的可靠性。

  • 第一次握手,客户端告诉服务器,我要跟你建立连接,我要告诉你我现在发送的数据包的syn是多少号
  • 第二次握手,服务器告诉客户端,我收到了你的数据包,我现在渴望收到你的下一个包的是你发过来的编号+1,我也要告诉你我现在发送的包的编号是多少
  • 第三次握手,客户端告诉服务器,我收到了你的数据包号,我接下来期望收到你的下一个包是你的编号+1,咱们开始传输

83.说一下 tcp 粘包是怎么产生的?

发生TCP粘包或拆包有很多原因,现列出常见的几点:

  • 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

粘包/拆包的解决办法

通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

  • 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
  • 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  • 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

84.OSI 的七层模型都有哪些?

第一层到第三层,负责创建网络通信连接的链路。 第四层到第七层,负责端到端的数据通信。

  • 物理层:规定通信设备,通信链路的特性。
  • 数据链路层:在物理层提供的比特流的基础上,建立相邻节点之间的数据链路,不可靠的物理介质提供可靠传输 ppp协议。
  • 网络层:选择合适的网间路由完成两个计算机之间的多个数据链路,通过路由协议和地址解析协议(ARP)。IP,RIP(路由信息协议),OSPF(最短路径优先协议)
  • 传输层:为应用程序之间提供端对端的逻辑通信。
  • 会话层:验证访问和会话管理。
  • 表示层:信息格式和语法的转化。
  • 应用层:为操作系统或者应用程序提供可用的网络接口。

85.get 和 post 请求有哪些区别?

Get和Post在面试中一般都会问到,一般的区别:

  • post更安全(不会作为url的一部分,不会被缓存、保存在服务器日志、以及浏览器浏览记录中)
  • post发送的数据更大(get有url长度限制)
  • post能发送更多的数据类型(get只能发送ASCII字符)
  • post比get慢
  • post用于修改和写入数据,get一般用于搜索排序和筛选之类的操作(淘宝,支付宝的搜索查询都是get提交),目的是资源的获取,读取数据

虽然在开发中经常用get或者post请求,但是由于我们资历经验的欠缺,或许就重来没有深究过什么场合用get请求,什么场合用post请求,我相信不止我一个人当看到第4,5条的时候,就会明白为什么面试官对我们的回答不满意,也明白了自己对get或post用法理解的欠缺,那么get比post更快,究竟快多少呢?表现在那些方面?

post请求包含更多的请求头

因为post需要在请求的body部分包含数据,所以会多了几个数据描述部分的首部字段(如:content-type),这其实是微乎其微的。

最重要的一条,post在真正接收数据之前会先将请求头发送给服务器进行确认,然后才真正发送数据

post请求的过程:

  • 浏览器请求tcp连接(第一次握手)
  • 服务器答应进行tcp连接(第二次握手)
  • 浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
  • 服务器返回100 Continue响应
  • 浏览器发送数据
  • 服务器返回200 OK响应

get请求的过程:

  • 浏览器请求tcp连接(第一次握手)
  • 服务器答应进行tcp连接(第二次握手)
  • 浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
  • 服务器返回200 OK响应 也就是说,目测get的总耗是post的2/3左右,这个口说无凭,网上已经有网友进行过测试。

86.如何实现跨域?

jsonp

  • 利用script标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
  • JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
  • JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

cors

CORS(Cross-origin resource sharing),跨域资源共享。CORS 其实是浏览器制定的一个规范,浏览器会自动进行 CORS 通信,它的实现则主要在服务端,它通过一些 HTTP Header 来限制可以访问的域,例如页面 A 需要访问 B 服务器上的数据,如果 B 服务器 上声明了允许 A 的域名访问,那么从 A 到 B 的跨域请求就可以完成。对于那些会对服务器数据产生副作用的 HTTP 请求,浏览器会使用 OPTIONS 方法发起 一个预检请求(preflight request),从而可以获知服务器端是否允许该跨域请求,服 务器端确认允许后,才会发起实际的请求。在预检请求的返回中,服务器端也可以告知客 户端是否需要身份认证信息。我们只需要设置响应头,即可进行跨域请求。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。

  • 只要同时满足以下两大条件,就属于简单请求:

    • 使用GET、HEAD、POST方法之一;

    • Content-Type 的值仅限于:text/plain、multipart/form-data、application/x-www-form-urlencoded,请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问

  • 复杂请求:

    不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。我们用PUT向后台请求时,属于复杂请求,后台需被请求的Servlet中添加Header设置,Access-Control-Allow-Origin这个Header在W3C标准里用来检查该跨域请求是否可以被通过,如果值为*则表明当前页面可以跨域访问。默认的情况下是不允许的。

一般我们可以写一个过滤器:

@WebFilter(filterName = "corsFilter", urlPatterns = "/*",
        initParams = {@WebInitParam(name = "allowOrigin", value = "*"),
                @WebInitParam(name = "allowMethods", value = "GET,POST,PUT,DELETE,OPTIONS"),
                @WebInitParam(name = "allowCredentials", value = "true"),
                @WebInitParam(name = "allowHeaders", value = "Content-Type,X-Token")})
public class CorsFilter implements Filter {

    private String allowOrigin;
    private String allowMethods;
    private String allowCredentials;
    private String allowHeaders;
    private String exposeHeaders;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        allowOrigin = filterConfig.getInitParameter("allowOrigin");
        allowMethods = filterConfig.getInitParameter("allowMethods");
        allowCredentials = filterConfig.getInitParameter("allowCredentials");
        allowHeaders = filterConfig.getInitParameter("allowHeaders");
        exposeHeaders = filterConfig.getInitParameter("exposeHeaders");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if (!StringUtils.isEmpty(allowOrigin)) {
            if (allowOrigin.equals("*")) {
                // 设置哪个源可以访问
                response.setHeader("Access-Control-Allow-Origin", allowOrigin);
            } else {
                List<String> allowOriginList = Arrays.asList(allowOrigin.split(","));
                if (allowOriginList != null && allowOriginList.size() > 0) {
                    String currentOrigin = request.getHeader("Origin");
                    if (allowOriginList.contains(currentOrigin)) {
                        response.setHeader("Access-Control-Allow-Origin", currentOrigin);
                    }
                }
            }
        }
        if (!StringUtils.isEmpty(allowMethods)) {
            //设置哪个方法可以访问
            response.setHeader("Access-Control-Allow-Methods", allowMethods);
        }
        if (!StringUtils.isEmpty(allowCredentials)) {
            // 允许携带cookie
            response.setHeader("Access-Control-Allow-Credentials", allowCredentials);
        }
        if (!StringUtils.isEmpty(allowHeaders)) {
            // 允许携带哪个头
            response.setHeader("Access-Control-Allow-Headers", allowHeaders);
        }
        if (!StringUtils.isEmpty(exposeHeaders)) {
            // 允许携带哪个头
            response.setHeader("Access-Control-Expose-Headers", exposeHeaders);
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}
@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        /*是否允许请求带有验证信息*/
        corsConfiguration.setAllowCredentials(true);
        /*允许访问的客户端域名*/
        corsConfiguration.addAllowedOrigin("*");
        /*允许服务端访问的客户端请求头*/
        corsConfiguration.addAllowedHeader("*");
        /*允许访问的方法名,GET POST等*/
        corsConfiguration.addAllowedMethod("*");
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }
}

87.说一下 JSONP 实现原理?

九、设计模式

88.说一下你熟悉的设计模式?

工厂模式、迭代器模式、原型模式、单例模式…

89.简单工厂和抽象工厂有什么区别?

简单工厂:

Java面试题整理(带答案)_第8张图片

public interface Keyboard {
    void print();

    void input(Context context);
}

class HPKeyboard implements Keyboard {

    @Override
    public void print() {
        //...输出逻辑;
    }

    @Override
    public void input(Context context) {
        //...输入逻辑;
    }

}

class DellKeyboard implements Keyboard {

    @Override
    public void print() {
        //...输出逻辑;
    }

    @Override
    public void input(Context context) {
        //...输入逻辑;
    }

}

class LenovoKeyboard implements Keyboard {

    @Override
    public void print() {
        //...输出逻辑;
    }

    @Override
    public void input(Context context) {
        //...输入逻辑;
    }

}

/**
 * 工厂
 */
public class KeyboardFactory {
    public Keyboard getInstance(int brand) {
        if (BrandEnum.HP.getCode() == brand) {
            return new HPKeyboard();
        } else if (BrandEnum.LENOVO.getCode() == brand) {
            return new LenovoKeyboard();
        } else if (BrandEnum.DELL.getCode() == brand) {
            return new DellKeyboard();
        }
        return null;
    }

    public static void main(String[] args) {
        KeyboardFactory keyboardFactory = new KeyboardFactory();
        Keyboard lenovoKeyboard = KeyboardFactory.getInstance(BrandEnum.LENOVO.getCode());
        //...
    }

}

上面的工厂实现是一个具体的类KeyboardFactory,而非接口或者抽象类,getInstance()方法利用if-else创建并返回具体的键盘实例,如果增加新的键盘子类,键盘工厂的创建方法中就要增加新的if-else。这种做法扩展性差,违背了开闭原则,也影响了可读性。所以,这种方式使用在业务较简单,工厂类不会经常更改的情况。

工厂方法:

为了解决上面提到的"增加if-else"的问题,可以为每一个键盘子类建立一个对应的工厂子类,这些工厂子类实现同一个抽象工厂接口。这样,创建不同品牌的键盘,只需要实现不同的工厂子类。当有新品牌加入时,新建具体工厂继承抽象工厂,而不用修改任何一个类。

Java面试题整理(带答案)_第9张图片

public interface IKeyboardFactory {
    Keyboard getInstance();
}

public class HPKeyboardFactory implements IKeyboardFactory {
    @Override
    public Keyboard getInstance() {
        return new HPKeyboard();
    }
}

public class LenovoFactory implements IKeyboardFactory {
    @Override
    public Keyboard getInstance() {
        return new LenovoKeyboard();
    }
}

public class DellKeyboardFactory implements IKeyboardFactory {
    @Override
    public Keyboard getInstance() {
        return new DellKeyboard();
    }
}

每一种品牌对应一个工厂子类,在创建具体键盘对象时,实例化不同的工厂子类。但是,如果业务涉及的子类越来越多,难道每一个子类都要对应一个工厂类吗?这样会使得系统中类的个数成倍增加,增加了代码的复杂度。

抽象工厂:

为了缩减工厂实现子类的数量,不必给每一个产品分配一个工厂类,可以将产品进行分组,每组中的不同产品由同一个工厂类的不同方法来创建。

例如,键盘、主机这2种产品可以分到同一个分组——电脑,而不同品牌的电脑由不同的制造商工厂来创建。

Java面试题整理(带答案)_第10张图片

Java面试题整理(带答案)_第11张图片

public interface Keyboard {
    void print();
}

public class DellKeyboard implements Keyboard {
    @Override
    public void print() {
        //...dell...dell;
    }
}

public class HPKeyboard implements Keyboard {
    @Override
    public void print() {
        //...HP...HP;
    }
}

public interface Monitor {
    void play();
}

public class DellMonitor implements Monitor {
    @Override
    public void play() {
        //...dell...dell;
    }
}

public class HPMonitor implements Monitor {
    @Override
    public void play() {
        //...HP...HP;
    }
}

public interface MainFrame {
    void run();
}

public class DellMainFrame implements MainFrame {
    @Override
    public void run() {
        //...dell...dell;
    }
}

public class HPMainFrame implements MainFrame {
    @Override
    public void run() {
        //...HP...HP;
    }
}

//工厂类。工厂分为Dell工厂和HP工厂,各自负责品牌内产品的创建
public interface IFactory {
    MainFrame createMainFrame();

    Monitor createMainFrame();

    Keyboard createKeyboard();
}

public class DellFactory implements IFactory {
    @Override
    public MainFrame createMainFrame() {
        MainFrame mainFrame = new DellMainFrame();
        //...造一个Dell主机;
        return mainFrame;
    }

    @Override
    public Monitor createMonitor() {
        Monitor monitor = new DellMonitor();
        //...造一个Dell显示器;
        return monitor;
    }

    @Override
    public Keyboard createKeyboard() {
        Keyboard keyboard = new DellKeyboard();
        //...造一个Dell键盘;
        return Keyboard;
    }
}

public class HPFactory implements IFactory {
    @Override
    public MainFrame createMainFrame() {
        MainFrame mainFrame = new HPMainFrame();
        //...造一个HP主机;
        return mainFrame;
    }

    @Override
    public Monitor createMonitor() {
        Monitor monitor = new HPMonitor();
        //...造一个HP显示器;
        return monitor;
    }

    @Override
    public Keyboard createKeyboard() {
        Keyboard keyboard = new HPKeyboard();
        //...造一个HP键盘;
        return Keyboard;
    }
}

//客户端代码。实例化不同的工厂子类,可以通过不同的创建方法创建不同的产品
public class Main {
    public static void main(String[] args) {
        IFactory dellFactory = new DellFactory();
        IFactory HPFactory = new HPFactory();
        //创建戴尔键盘
        Keyboard dellKeyboard = dellFactory.createKeyboard();
        //...
    }
}

总结:

  • 简单工厂:唯一工厂类,一个产品抽象类,工厂类的创建方法依据入参判断并创建具体产品对象。
  • 工厂方法:多个工厂类,一个产品抽象类,利用多态创建不同的产品对象,避免了大量的if-else判断。
  • 抽象工厂:多个工厂类,多个产品抽象类,产品子类分组,同一个工厂实现类创建同组中的不同产品,减少了工厂子类的数量。

十、Spring/Spring MVC

90.为什么要使用 Spring?

Spring,英文翻译是春天的意思,而在Java中,是一个开放源代码的设计层面框架(手动滑稽,程序员的春天),他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。

优点

  • 低侵入式设计,代码污染极低
  • 独立于各种应用服务器,基于Spring框架的应用,可以真正实现Write Once,Run Anywhere的承诺
  • Spring的DI机制降低了业务对象替换的复杂性,提高了组件之间的解耦
  • Spring的AOP支持允许将一些通用任务如安全、事务、日志等进行集中式管理,从而提供了更好的复用
  • Spring的ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问
  • Spring并不强制应用完全依赖于Spring,开发者可自由选用Spring框架的部分或全部

91.解释一下什么是 aop?

这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了将不同的关注点分离出来的效果。

92.解释一下什么是 ioc?

IoC (Inversion of control )控制反转/反转控制。它是一种思想不是一个技术实现。描述的是:Java 开发领域对象的创建以及管理的问题。

例如:现有类 A 依赖于类 B

  • 传统的开发方式 :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来

  • 使用 IoC 思想的开发方式 :

    不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架)来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面过去即可。 从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)

为什么叫控制反转

  • 控制 :指的是对象创建(实例化、管理)的权力
  • 反转 :控制权交给外部环境(Spring 框架、IoC 容器

93.Spring 有哪些主要模块?

1、核心模块

SpringCore模块是Spring的核心容器,它实现了IOC模式,提供了Spring框架的基础功能。此模块中包含的BeanFactory类是Spring的核心类,负责JavaBean的配置与管理。它采用Factory模式实现了IOC即依赖注入。

2、Context模块

SpringContext模块继承BeanFactory(或者说Spring核心)类,并且添加了事件处理、国际化、资源装载、透明装载、以及数据校验等功能。它还提供了框架式的Bean的访问方式和很多企业级的功能,如JNDI访问、支持EJB、远程调用、集成模板框架、Email和定时任务调度等。

3、AOP模块

Spring集成了所有AOP功能。通过事务管理可以使任意Spring管理的对象AOP化。Spring提供了用标准Java语言编写的AOP框架,它的大部分内容都是基于AOP联盟的API开发的。它使应用程序抛开EJB的复杂性,但拥有传统EJB的关键功能。

4、DAO模块

DAO是DataAccessObject的缩写,DAO模式思想是将业务逻辑代码与数据库交互代码分离,降低两者耦合。通过DAO模式可以使结构变得更为清晰,代码更为简洁。DAO模块提供了JDBC的抽象层,简化了数据库厂商的异常错误(不再从SQLException继承大批代码),大幅度减少代码的编写,并且提供了对声明式事务和编程式事务的支持。

5、ORM映射模块

SpringORM模块提供了对现有ORM框架的支持,各种流行的ORM框架已经做得非常成熟,并且拥有大规模的市场,Spring没有必要开发新的ORM工具,它对Hibernate提供了完美的整合功能,同时也支持其他ORM工具。注意这里Spring是提供各类的接口(support),目前比较流行的下层数据库封闭映射框架,如ibatis,Hibernate等。

6、Web模块

此模块建立在SpringContext基础之上,它提供了Servlet监听器的Context和Web应用的上下文。对现有的Web框架,如JSF、Tapestry、Structs等,提供了集成。Structs是建立在MVC这种公认的好的模式上的,Struts在M、V和C上都有涉及,但它主要是提供一个好的控制器和一套定制的标签库上,也就是说它的着力点在C和V上,因此,它天生就有MVC所带来的一系列优点,如:结构层次分明,高可重用性,增加了程序的健壮性和可伸缩性,便于开发与设计分工,提供集中统一的权限控制、校验、国际化、日志等等。

7、MVC模块

SpringWebMVC模块建立在Spring核心功能之上,这使它能拥有Spring框架的所有特性,能够适应多种多视图、模板技术、国际化和验证服务,实现控制逻辑和业务逻辑的清晰分离。实践证明,MVC模式为大型程序的开发及维护提供了巨大的便利。

94.Spring 常用的注入方式有哪些?

  • 构造方法注入
  • set方法注入
  • 注解注入

95.Spring 中的 bean 是线程安全的吗?

Spring框架中的单例Bean默认是单例模式,不是线程安全的。Spring框架并没有对单例Bean进行多线程的封装处理。

关于线程是否安全,可以从Bean的状态来考虑是否要进行处理,有状态的Bean就是有数据存储功能,例如VO视图对象,无状态的Bean是不会保存数据的,例如DAO类。实际上大部分时候Spring Bean都是无状态的,因此某种程度上来说,Bean也是安全的,但如果Bean有状态的话,那就要开发者自己去保证线程安全了,可以通过把Bean的作用域改为“prototype”,这样可以保证线程安全。

96.Spring 支持几种 bean 的作用域?

  • singleton:单例模式,在整个Spring IoC容器中,使用 singleton 定义的 bean 只有一个实例
  • prototype:原型模式,每次通过容器的getbean方法获取 prototype 定义的 bean 时,都产生一个新的 bean 实例
  • request:对于每次 HTTP 请求,使用 request 定义的 bean 都将产生一个新实例,即每次 HTTP 请求将会产生不同的 bean 实例。
  • session:同一个 Session 共享一个 bean 实例。
  • global-session:同 session 作用域不同的是,所有的Session共享一个Bean实例。

只有在 Web 应用中使用Spring时,request、session、global-session 作用域才有效

97.Spring 自动装配 bean 有哪些方式?

Spring装配方式

  • 在XML中进行显式配置。
  • 在Java中进行显式配置。
  • 隐式的bean发现机制和自动装配

本题讨论自动装配bean实现,Spring从两个角度来实现自动化装配:

  • 组件扫描(ComponentScan):自动发现应用上下文中所创建的bean
  • 自动装配(Autowired):自动满足bean之间的依赖

98.Spring 事务实现方式有哪些?

  • 编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理。
  • 基于 TransactionProxyFactoryBean的声明式事务管理
  • 基于 @Transactional 的声明式事务管理
  • 基于Aspectj AOP配置事务

编程式事务两种实现方式:

编程式事务实现第一种:使用 TransactionTemplate 事务模板对象

首先:因为我们使用的是特定的平台,所以,我们需要创建一个合适我们的平台事务管理PlateformTransactionManager。如果使用的是JDBC的话,就用DataSourceTransactionManager。注意需要传入一个DataSource,这样,平台才知道如何和数据库打交道。

第二:为了使得平台事务管理器对我们来说是透明的,就需要使用TransactionTemplate。使用TransactionTemplat需要传入一个PlateformTransactionManager 进入,这样,我们就得到了一个TransactionTemplate,而不用关心到底使用的是什么平台了。

第三:TransactionTemplate 的重要方法就是 execute 方法,此方法就是调用TransactionCallback 进行处理。实际上我们需要处理的事情全部都是在 TransactionCallback 中编码的。

第四:也就是 TransactionCallback 接口,我们可以定义一个类并实现此接口,然后作为TransactionTemplate.execute 的参数。把需要完成的事情放到 doInTransaction中,并且传入一个TransactionStatus 参数。此参数是来调用回滚的。 也就是说 ,PlateformTransactionManager 和 TransactionTemplate 只需在程序中定义一次,而TransactionCallback 和 TransactionStatus 就要针对不同的任务多次定义了。

这就是Spring的编程式事务管理。下面贴出例子代码:

步骤: 1.配置事务管理器


<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource">property>
bean>

2.配置事务模板对象


<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager">property>
bean>

3.Test

@Controller
@RequestMapping("/tx")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TransactionController {

    @Resource
    public TransactionTemplate transactionTemplate;

    @Resource
    public PlatformTransactionManager transactionManager;

    @Resource
    public DataSource dataSource;

    private static JdbcTemplate jdbcTemplate;

    private static final String INSERT_SQL = "insert into cc(id) values(?)";
    private static final String COUNT_SQL = "select count(*) from cc";

    @Test
    public void TransactionTemplateTest() {
        //获取jdbc核心类对象,进而操作数据库
        jdbcTemplate = new JdbcTemplate(dataSource);
        //通过注解 获取xml中配置的 事务模板对象
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        //重写execute方法实现事务管理
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                jdbcTemplate.update(INSERT_SQL, "33");   //字段sd为int型,所以插入肯定失败报异常,自动回滚,代表TransactionTemplate自动管理事务
            }
        });
        int i = jdbcTemplate.queryForInt(COUNT_SQL);
        System.out.println("表中记录总数:" + i);
    }
}

编程式事务第二种实现:使用事务管理器 PlatformTransactionManager 对象

1.只需要:配置事务管理


<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource">property>
bean>

2.Test

@Controller
@RequestMapping("/tx")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TransactionController {

    @Resource
    public PlatformTransactionManager transactionManager;

    @Resource
    public DataSource dataSource;

    private static JdbcTemplate jdbcTemplate;

    private static final String INSERT_SQL = "insert into cc(id) values(?)";
    private static final String COUNT_SQL = "select count(*) from cc";

    @Test
    public void showTransaction() {
        //定义使用隔离级别,传播行为
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        //事务状态类,通过PlatformTransactionManager的getTransaction方法根据事务定义获取;获取事务状态后,Spring根据传播行为来决定如何开启事务
        TransactionStatus transaction = transactionManager.getTransaction(def);
        jdbcTemplate = new JdbcTemplate(dataSource);
        int i = jdbcTemplate.queryForInt(COUNT_SQL);
        System.out.println("表中记录总数:" + i);
        try {
            jdbcTemplate.update(INSERT_SQL, "2");
            jdbcTemplate.update(INSERT_SQL, "是否");//出现异常,因为字段为int类型,会报异常,自动回滚
            transactionManager.commit(transaction);
        } catch (Exception e) {
            e.printStackTrace();
            transactionManager.rollback(transaction);
        }
        int i1 = jdbcTemplate.queryForInt(COUNT_SQL);
        System.out.println("表中记录总数:" + i1);
    }
}

声明式事务实现方式第一种:基于Aspectj AOP开启事务

1.配置事务通知


<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED" rollback-for="Exception"/>
    tx:attributes>
tx:advice>

2.配置织入


<aop:config>
    
    <aop:pointcut id="tx" expression="execution(* cn.sys.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="tx"/>
aop:config>

声明式事务实现方式第一种:基于注解的 @Transactional 的声明式事务管理

@Transactional
public int saveRwHist(List<RwHist> list){
        return rwDao.saveRwHist(list);
        }

99.说一下 Spring 的事务隔离?

spring 有五大隔离级别,默认值为 ISOLATION_DEFAULT(使用数据库的设置),其他四个隔离级别和数据库的隔离级别一致:

  • ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么我就用什么;
  • ISOLATION_READ_UNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读);
  • ISOLATION_READ_COMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL server 的默认级别;
  • ISOLATION_REPEATABLE_READ:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL 的默认级别;
  • ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。

什么是脏读、不可重复读、幻读?

  • 脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。

  • 不可重复读 :是指在一个事务内,多次读同一数据。

  • 幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。

100.说一下 Spring mvc 运行流程?

Java面试题整理(带答案)_第12张图片

  • 用户向服务器发送请求,请求被 Spring 前端控制 Servelt DispatcherServlet 捕获(捕获)

  • DispatcherServlet对请求 URL进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 HandlerMapping获得该Handler配置的所有相关的对象(包括 Handler对象以及 Handler对象对应的拦截器),最后以 HandlerExecutionChain对象的形式返回;(查找 handler)

  • DispatcherServlet 根据获得的 Handler,选择一个合适的 HandlerAdapter。 提取Request 中的模型数据,填充 Handler 入参,开始执行 Handler(Controller), Handler执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象(执行 handler)

  • DispatcherServlet 根据返回的 ModelAndView,选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的 ViewResolver) (选择 ViewResolver)

  • 通过 ViewResolver 结合 Model 和 View,来渲染视图,DispatcherServlet 将渲染结果返回给客户端。(渲染返回)

101.Spring mvc 有哪些组件?

  • 前端控制器(DispatcherServlet):主要负责捕获来自客户端的请求和调度各个组件。
  • 处理器映射器(HandlerMapping):根据url查找后端控制器Handler。
  • 处理器适配器(HandlerAdapter):执行后端控制器(Handler),拿到后端控制器返回的结果ModelAndView后将结果返回给前端控制器DispatcherServlet。
  • 后端控制器(处理器)(Handler):主要负责处理前端请求,完成业务逻辑,生成ModelAndView对象返回给HandlerAdapter。
  • 视图解析器(ViewResolver):主要负责将从DispatcherServlet中拿到的ModelAndView对象进行解析,生成View对象返回给DispatcherServlet。

102.@RequestMapping 的作用是什么?

@RequestMapping 是一个注解,用来标识 http 请求地址与 Controller 类的方法之间的映射。可作用于类和方法上,方法匹配的完整是路径是 Controller 类上 @RequestMapping 注解的 value
值加上方法上的 @RequestMapping 注解的 value 值。

103.@Autowired 的作用是什么?

  • @Autowired 是一个注释,它可以对类成员变量、方法及构造函数进行标注,让 spring 完成 bean 自动装配的工作。
  • @Autowired 默认是按照类去匹配,配合 @Qualifier 指定按照名称去装配 bean。

十一、Spring Boot/Spring Cloud

104.什么是 Spring boot?

在过去的两年时间里,最让人兴奋、回头率最高、最能改变游戏规则的东西,大概就是Spring Boot了。Spring Boot提供了一种新的编程范式,能在最小的阻力下开发Spring应用程序。有了它, 你可以更加敏捷地开发Spring应用程序,专注于应用程序的功能,不用在Spring的配置上多花功 夫,甚至完全不用配置。实际上,Spring Boot的一项重要工作就是让Spring配置不再成为你成功路上的绊脚石。

105.为什么要用 Spring boot?

以前在写spring项目的时候,要配置各种xml文件,还记得曾经被ssh框架支配的恐惧。随着spring3,spring4的相继推出,约定大于配置逐渐成为了开发者的共识,大家也渐渐的从写xml转为写各种注解,在spring4的项目里,你甚至可以一行xml都不写。 虽然spring4已经可以做到无xml,但写一个大项目需要茫茫多的包,maven配置要写几百行,也是一件很可怕的事。现在,快速开发一个网站的平台层出不穷,nodejs,php等虎视眈眈,并且脚本语言渐渐流行了起来(Node JS,Ruby,Groovy,Scala等),spring的开发模式越来越显得笨重。在这种环境下,spring boot伴随着spring4一起出现了。

那么,spring boot可以做什么呢?spring boot并不是一个全新的框架,它不是spring解决方案的一个替代品,而是spring的一个封装。所以,你以前可以用spring做的事情,现在用spring boot都可以做。现在流行微服务与分布式系统,springboot就是一个非常好的微服务开发框架,你可以使用它快速的搭建起一个系统。同时,你也可以使用spring cloud(Spring Cloud是一个基于Spring Boot实现的云应用开发工具)来搭建一个分布式的网站。

106.Spring boot 核心配置文件是什么?

Spring Boot 有两种类型的配置文件,application 和 bootstrap 文件 Spring Boot会自动加载classpath目前下的这两个文件,文件格式为 properties 或 yml 格式

  • properties 文件是 key=value 的形式
  • yml 是 key: value 的形式
  • yml 加载的属性是有顺序的,但不支持 @PropertySource 注解来导入配置,一般推荐用yml文件,看下来更加形象

bootstrap 配置文件是系统级别的,用来加载外部配置,如配置中心的配置信息,也可以用来定义系统不会变化的属性.bootstatp 文件的加载先于application文件,application配置文件是应用级别的,是当前应用的配置文件

107.Spring boot 配置文件有哪几种类型?它们有什么区别?

  • properties文件:key=value
  • yml文件:key: value

108.Spring boot 有哪些方式可以实现热部署?

1、模板热部署

在SpringBoot中,模板引擎的页面默认是开启缓存的,如果修改了页面的内容,则刷新页面是得不到修改后的页面的,因此我们可以在application.properties中关闭模版引擎的缓存,如下:

Thymeleaf的配置:

spring.thymeleaf.cache=false

FreeMarker的配置:

spring.freemarker.cache=false

Groovy的配置:

spring.groovy.template.cache=false

Velocity的配置:

spring.velocity.cache=false

2、使用调试模式Debug实现热部署

此种方式为最简单最快速的一种热部署方式,运行系统时使用Debug模式,无需装任何插件即可,但是无发对配置文件,方法名称改变,增加类及方法进行热部署,使用范围有限。

3、spring-boot-devtools

在Spring Boot 项目中添加 spring-boot-devtools依赖即可实现页面和代码的热部署。如下:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-devtoolsartifactId>
dependency>

此种方式的特点是作用范围广,系统的任何变动包括配置文件修改、方法名称变化都能覆盖,但是后遗症也非常明显,它是采用文件变化后重启的策略来实现了,主要是节省了我们手动点击重启的时间,提高了实效性,在体验上回稍差。spring-boot-devtools 默认关闭了模版缓存,如果使用这种方式不用单独配置关闭模版缓存。

4、Spring Loaded

此种方式与Debug模式类似,适用范围有限,但是不依赖于Debug模式启动,通过Spring Loaded库文件启动,即可在正常模式下进行实时热部署。此种需要在 run confrgration 中进行配置。

5、JRebel

Jrebel是Java开发最好的热部署工具,对Spring Boot 提供了极佳的支持,JRebel为收费软件,试用期14天。,可直接通过插件安装。

109.jpa 和 hibernate 有什么区别?

110.什么是 Spring cloud?

在了解SpringCloud之前,我们先来大致了解下微服务这个概念吧。

传统单体架构

单体架构在小微企业比较常见,典型代表就是一个应用、一个数据库、一个web容器就可以跑起来。

Java面试题整理(带答案)_第13张图片

可以从上图看出,单体架构基本上就是如上所说的:一个应用,一个数据库,一个web容器,里面集成了所有的功能。这在小型项目里面时比较好维护的,毕竟功能不多,也不复杂,但扩展性和可靠性比较差,因为所有功能集成在一个服务或者一个war包中,修改某个功能时,需要所有服务重新打包。可能前期开发比较快,后期随着功能的增长,交互的周期会越变越长的。

服务化架构

服务化架构,也可以称之为SOA架构。SOA代表面向服务的架构,将应用程序根据不同的职责划分为不同的模块,不同的模块直接通过特定的协议和接口进行交互。这样使整个系统切分成很多单个组件服务来完成请求,当流量过大时通过水平扩展相应的组件来支撑,所有的组件通过交互来满足整体的业务需求。SOA服务化的优点是,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用。服务层是SOA的基础,可以直接被应用调用,从而有效控制系统中与软件代理交互的人为依赖性。

服务化架构是一套松耦合的架构,服务的拆分原则是服务内部高内聚,服务之间低耦合。一般上我们使用dubbo来进行服务的治理功能,没有使用SpringCloud之前,基本上都是使用dubbo来拆分服务,进行服务间的调用。看下服务化架构的架构图:

Java面试题整理(带答案)_第14张图片

可以发现从单体架构到服务化架构,应用数量都在不断的增加,慢慢的下沉的就成了基础组建,上浮的就成为业务系统。从上述也可以看出架构的本质就是不断的拆分重构:分的过程是把系统拆分为各个子系统/模块/组件,拆的时候,首先要解决每个组件的定位问题,然后才能划分彼此的边界,实现合理的拆分。合就是根据最终要求,把各个分离的组件有机整合在一起。拆分的结果使开发人员能够做到业务聚焦、技能聚焦,实现开发敏捷,合的结果是系统变得柔性,可以因需而变,实现业务敏捷。

其实,我觉得最核心还是边界拆分。如何拆,拆的颗粒度要多细,就很考验一个架构师对业务和底层技术的掌控程度。一个好的架构师,会让整个系统边界清晰,泾渭分明,功能没有多少重叠的。(不知道何时才能成长为一名架构师(┬_┬)

微服务架构

简单来说,微服务架构是 SOA 架构思想的一种扩展,更加强调服务个体的独立性、拆分粒度更小。下面这段话我觉得可以能好的概括出何为微服务:

简单来说,微服务架构风格想要开发一种由多个小服务组成的应用。每个服务运行于独立的进程,并且采用轻量级交互。多数情况下是一个HTTP的资源API这些服务具备独立业务能力并可以通过自动化部署方式独立部署。这种风格使最小化集中管理,从而可以使用多种不同的编程语言和数据存储技术。

111.Spring cloud 断路器的作用是什么?

当一个服务调用另一个服务由于网络原因或者自身原因出现问题时,调用者就会等被调用者的响应,当更多的服务请求到这些资源时,导致更多的请求等待,这素以会发生连锁效应,断路器就是解决这一问题的。

断路器有三种状态,完全打开状态、半开状态、关闭态。

  • 完全打开态:一定时间内,达到一定的次数无法调用,并且多次检测没有恢复的迹象,断路器完全打开,那么下次的请求不会到该服务;
  • 半开:短时间内有回复迹象,断路器会将部分请求发送给服务,当能正常调用时,断路器关闭。
  • 关闭:当服务一直处于正常状态,能正常调用,断路器关闭。

112.Spring cloud 的核心组件有哪些?

  • Eureka
  • Zuul/Gateway
  • Ribbon
  • Feign
  • Hystrix
  • SpringCloud Config
  • SpringCloud Bus
  • SpringCloud Sleuth

Java面试题整理(带答案)_第15张图片

十二、Hibernate

113.为什么要使用 hibernate?

114.什么是 ORM 框架?

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

116.hibernate 有几种查询方式?

117.hibernate 实体类可以被定义为 final 吗?

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

119.hibernate 是如何工作的?

120.get()和 load()的区别?

121.说一下 hibernate 的缓存机制?

122.hibernate 对象有哪些状态?

123.在 hibernate 中 getCurrentSession 和 openSession 的区别是什么?

124.hibernate 实体类必须要有无参构造函数吗?为什么?

十三、Mybatis

125.mybatis 中 #{}和 ${}的区别是什么?

  • #{}是预编译处理,$ {}是字符串替换。

  • mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;mybatis在处理时,就是把{}替换成变量的值。使用#{}可以有效的防止SQL注入,提高系统安全性。

126.mybatis 有几种分页方式?

  • 手动分页,查询结果后根据list集合手动subList
  • SQL分页,limit offset,rows 注意,offset=(页码-1)*每页的条数,rows为每页的条数
  • 实现interceptor接口
  • 使用mybatis的RowBounds实现分页

127.RowBounds 是一次性查询全部结果吗?为什么?

是,因Mysql中可以使用limit语句,但limit并不是标准SQL中的,如果是其它的数据库,则需要使用其它语句。MyBatis提供了RowBounds类,用于实现分页查询。RowBounds中有两个数字,offset和limit。

128.mybatis 逻辑分页和物理分页的区别是什么?

  • 逻辑分页:查询出来之后在内存进行分页
  • 物理分页:在数据库中查询分页

129.mybatis 是否支持延迟加载?延迟加载的原理是什么?

MyBatis 实现一对一有几种方式?具体怎么操作的?

有联合查询和嵌套查询 联合查询是几个表联合查询,只查询一次, 通过在resultMap 里面配置 association 节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键 id,去再另外一个表里面查询数据,也是通过 association 配置,但另外一个表的查询通过 select 属性配置。

<mapper namespace="com.lcb.mapping.userMapper">
    
    <select id="getClass" parameterType="int"
            resultMap="ClassesResultMap">
        select * from class c,teacher t where c.teacher_id=t.t_id and
        c.c_id=#{id}
    select>
    <resultMap type="com.lcb.user.Classes" id="ClassesResultMap">
        
        <id property="id" column="c_id"/>
        <result property="name" column="c_name"/>
        <association property="teacher"
                     javaType="com.lcb.user.Teacher">
            <id property="id" column="t_id"/>
            <result property="name" column="t_name"/>
        association>
    resultMap>

MyBatis 实现一对多有几种方式,怎么操作的?

有联合查询和嵌套查询。 联合查询是几个表联合查询,只查询一次,通过在resultMap 里面的 collection 节点配置一对多的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键 id,再去另外一个表里面查询数据,也是通过配置 collection,但另外一个表的查询通过 select 节点配置


<select id="getClass2" parameterType="int"
        resultMap="ClassesResultMap2">
    select * from class c,teacher t,student s where c.teacher_id=t.t_id
    and c.c_id=s.class_id and c.c_id=#{id}
select>
<resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher"
             javaType="com.lcb.user.Teacher">
    <id property="id" column="t_id"/>
    <result property="name" column="t_name"/>
association>
<collection property="student"
            ofType="com.lcb.user.Student">
    <id property="id" column="s_id"/>
    <result property="name" column="s_name"/>
collection>
resultMap>
        mapper>

Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?

Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。

130.说一下 mybatis 的一级缓存和二级缓存?

一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。Mybatis默认开启一级缓存。一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。

二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是多个SqlSession共享的。UserMapper有一个二级缓存区域(按namespace分,如果namespace相同则使用同一个相同的二级缓存区),其它mapper也有自己的二级缓存区域(按namespace分)。也是就是说拥有相同的namespace的UserMapper共享一个二级缓存

131.mybatis 和 hibernate 的区别有哪些?

132.mybatis 有哪些执行器(Executor)?

mybatis有三种executor执行器,分别为simpleExecutor、reuseExecutor、batchExecutor。

  • simpleExecutor执行器:在每执行一次update或select,就开启一个statement对象,用完后就关闭。

  • reuseExecutor执行器:在执行update或select时以sql作为key去查找statement,有就直接使用,没有就创建,使用完毕后不关闭,放入Map,Map的key是String类型,Value是Statement类型,供下次使用。重复使用statement。

  • batchExecutor执行器:执行update(jdbc批处理不支持select),会把所有sql添加到批处理中addbatch();等待统一批处理executorbatch();它缓存了·多个statement,每一个statement都是addbatch(),后等待进行executorbatch()批处理。

作用范围:统一限制在sqlSession生命周期范围内。

133.mybatis 分页插件的实现原理是什么?

实现interceptor接口,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

134.mybatis 如何编写一个自定义插件?

1、新建类实现 Interceptor 接口,并指定想要拦截的方法签名

/**
 * MyBatis 插件
 */
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class ExamplePlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        for (Object arg : invocation.getArgs()) {
            System.out.println("参数:" + arg);
        }
        System.out.println("方法:" + invocation.getMethod());
        System.out.println("目标对象:" + invocation.getTarget());
        Object result = invocation.proceed();

        //只获取第一个数据
        if (result instanceof List) {
            System.out.println("原集合数据:" + result);
            System.out.println("只获取第一个对象");
            List list = (List) result;
            return Arrays.asList(list.get(0));
        }
        return result;
    }
}

2、MyBatis 配置文件中添加该插件

<plugins>
    <plugin interceptor="constxiong.plugin.ExamplePlugin">
    plugin>
plugins>

3.测试

        System.out.println("------userMapper.deleteUsers()------");
        //删除 user
        userMapper.deleteUsers();

        System.out.println("------userMapper.insertUser()------");
        //插入 user
        for(int i=1;i<=5;i++){
        userMapper.insertUser(new User(i,"ConstXiong"+i));
        }

        System.out.println("------userMapper.selectUsers()------");
        //查询所有 user
        List<User> users=userMapper.selectUsers();
        System.out.println(users);
------userMapper.deleteUsers()------
------userMapper.insertUser()------
------userMapper.selectUsers()------
参数:org.apache.ibatis.mapping.MappedStatement@58c1c010
参数:null
参数:org.apache.ibatis.session.RowBounds@b7f23d9
参数:null
方法:public abstract java.util.List org.apache.ibatis.executor.Executor.query(org.apache.ibatis.mapping.MappedStatement,java.lang.Object,org.apache.ibatis.session.RowBounds,org.apache.ibatis.session.ResultHandler) throws java.sql.SQLException
目标对象:org.apache.ibatis.executor.CachingExecutor@61d47554
原集合数据:[User{id=1, name='ConstXiong1', mc='null'}, User{id=2, name='ConstXiong2', mc='null'}, User{id=3, name='ConstXiong3', mc='null'}, User{id=4, name='ConstXiong4', mc='null'}, User{id=5, name='ConstXiong5', mc='null'}]
只获取第一个对象
[User{id=1, name='ConstXiong1', mc='null'}]

十四、RabbitMQ

135.rabbitmq 的使用场景有哪些?

  • 跨系统的异步通信,所有需要异步交互的地方都可以使用消息队列。就像我们除了打电话(同步)以外,还需要发短信,发电子邮件(异步)的通讯方式。

  • 多个应用之间的耦合,由于消息是平台无关和语言无关的,而且语义上也不再是函数调用,因此更适合作为多个应用之间的松耦合的接口。基于消息队列的耦合,不需要发送方和接收方同时在线。在企业应用集成(EAI)中,文件传输,共享数据库,消息队列,远程过程调用都可以作为集成的方法。

  • 应用内的同步变异步,比如订单处理,就可以由前端应用将订单信息放到队列,后端应用从队列里依次获得消息处理,高峰时的大量订单可以积压在队列里慢慢处理掉。由于同步通常意味着阻塞,而大量线程的阻塞会降低计算机的性能。

  • 消息驱动的架构(EDA),系统分解为消息队列,和消息制造者和消息消费者,一个处理流程可以根据需要拆成多个阶段(Stage),阶段之间用队列连接起来,前一个阶段处理的结果放入队列,后一个阶段从队列中获取消息继续处理。

  • 应用需要更灵活的耦合方式,如发布订阅,比如可以指定路由规则。

  • 跨局域网,甚至跨城市的通讯(CDN行业),比如北京机房与广州机房的应用程序的通信。

136.rabbitmq 有哪些重要的角色?

  • 代理:就是MQ本身,扮演快递员的角色,本身不产生消息
  • 生产者:生产消息
  • 消费者:消费消息

137.rabbitmq 有哪些重要的组件?

  • ConnectionFactory(连接管理器):应用程序与RabbitMQ之间建立连接的管理器

  • Channel(信道):消息推送使用的通道

  • Exchange(交换器):用于接受、分配消息

  • Queue(队列):用于存储生产者的消息

  • RoutingKey(路由键):生产者将消息发送给交换器的时候,会指定一个RoutingKey,用来指定这个消息的路由规则,这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。

  • BindKey(绑定键):用于把交换器的消息绑定到队列上

138.rabbitmq 中 vhost 的作用是什么?

  • vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的队列、绑定、交换器和权限控制;

  • vhost通过在各个实例间提供逻辑上分离,允许你为不同应用程序安全保密地运行数据;

  • vhost是AMQP概念的基础,必须在连接时进行指定,RabbitMQ包含了默认vhost:“/”;

  • 当在RabbitMQ中创建一个用户时,用户通常会被指派给至少一个vhost,并且只能访问被指派vhost内的队列、交换器和绑定,vhost之间是绝对隔离的。

  • vhost可以理解为虚拟broker,即mini-RabbitMQ server,其内部均含有独立的queue、bind、exchange等,最重要的是拥有独立的权限系统,可以做到vhost范围内的用户控制。当然,从RabbitMQ全局角度,vhost可以作为不同权限隔离的手段(一个典型的例子,不同的应用可以跑在不同的vhost中)。

139.rabbitmq 的消息是怎么发送的?

首先客户端必须连接到RabbitMQ服务器才能发布和消费消息,客户端和rabbit server 之间会创建一个 tcp 连接,一旦 tcp 打开并通过了认证(认证就是你发送给 rabbit 服务器的用户名和密码),你的客户端和 RabbitMQ 就创建了一条 amqp 信道(channel),信道是创建在“真实”tcp 上的虚拟连接,amqp 命令都是通过信道发送 出去的,每个信道都会有一个唯一的 id,不论是发布消息,订阅队列都是通过这个信道完成的。

140.rabbitmq 怎么保证消息的稳定性?

  • 提供了事务的功能。
  • 通过将 channel 设置为 confirm(确认)模式。

141.rabbitmq 怎么避免消息丢失?

  • 消息持久化
  • ACK确认机制
  • 设置集群镜像模式
  • 消息补偿机制

142.要保证消息持久化成功的条件有哪些?

  • 声明队列必须设置持久化 durable 设置为 true.
  • 消息推送投递模式必须设置持久化,deliveryMode 设置为 2(持久)。
  • 消息已经到达持久化交换器。
  • 消息已经到达持久化队列。

以上四个条件都满足才能保证消息持久化成功。

143.rabbitmq 持久化有什么缺点?

持久化的缺地就是降低了服务器的吞吐量,因为使用的是磁盘而非内存存储,从而降低了吞吐量。可尽量使用 ssd 硬盘来缓解吞吐量的问题。

144.rabbitmq 有几种广播类型?

direct(默认方式):最基础最简单的模式,发送方把消息发送给订阅方,如果有多个订阅者,默认采取轮询的方式进行消息发送。 headers:与 direct 类似,只是性能很差,此类型几乎用不到。
fanout:分发模式,把消息分发给所有订阅者。 topic:匹配订阅模式,使用正则匹配到消息队列,能匹配到的都能接收到。

145.rabbitmq 怎么实现延迟消息队列?

  • 通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能;
  • 使用 RabbitMQ-delayed-message-exchange 插件实现延迟功能

146.rabbitmq 集群有什么用?

  • 高可用:某个服务器出现问题,整个 RabbitMQ 还可以继续使用;
  • 高容量:集群可以承载更多的消息量。

147.rabbitmq 节点的类型有哪些?

  • DISK磁盘节点:消息会存储到磁盘。
  • RAM内存节点:消息都存储在内存中,重启服务器消息丢失,性能高于磁盘类型

148.rabbitmq 集群搭建需要注意哪些问题?

  • 各节点之间使用“–link”连接,此属性不能忽略。
  • 各节点使用的 erlang cookie 值必须相同,此值相当于“秘钥”的功能,用于各节点的认证。
  • 整个集群中必须包含一个磁盘节点。

149.rabbitmq 每个节点是其他节点的完整拷贝吗?为什么?

不是,原因有以下两个:

  • 存储空间的考虑:如果每个节点都拥有所有队列的完全拷贝,这样新增节点不但没有新增存储空间,反而增加了更多的冗余数据;
  • 性能的考虑:如果每条消息都需要完整拷贝到每一个集群节点,那新增节点并没有提升处理消息的能力,最多是保持和单节点相同的性能甚至是更糟。

150.rabbitmq 集群中唯一一个磁盘节点崩溃了会发生什么情况?

如果唯一磁盘的磁盘节点崩溃了,不能进行以下操作:

  • 不能创建队列
  • 不能创建交换器
  • 不能创建绑定
  • 不能添加用户
  • 不能更改权限
  • 不能添加和删除集群节点

唯一磁盘节点崩溃了,集群是可以保持运行的,但你不能更改任何东西

151.rabbitmq 对集群节点停止顺序有要求吗?

RabbitMQ 对集群的停止的顺序是有要求的,应该先关闭内存节点,最后再关闭磁盘节点。如果顺序恰好相反的话,可能会造成消息的丢失。

十七、MySql

164.数据库的三范式是什么?

Java面试题整理(带答案)_第16张图片

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

  • 表类型如果是 MyISAM ,那 id 就是 18。
  • 表类型如果是 InnoDB,那 id 就是 15。
  • MyISAM 只管记录,之前记录的是17,就算增删数据,当添加数据时,还是18
  • InnoDB 表只会把自增主键的最大 id 记录在内存中,所以重启之后会导致最大 id 丢失。

166.如何获取当前数据库版本?

使用 select version() 获取当前 MySQL 数据库版本。

167.说一下事务及其四大特性(ACID)是什么?

  • Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。

  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。 这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。

  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

168.char 和 varchar 的区别是什么?

char(n) :固定长度类型,比如订阅 char(10),当你输入"abc"三个字符的时候, 它们占的空间还是 10 个字节,其他 7 个是空字节。

char 优点:效率高;缺点:占用空间; 适用场景:存储密码的 md5 值,固定长度的,使用 char 非常合适。

varchar(n) :可变长度,存储的值是每个值占用的字节再加上一个用来记录其长度的字节的长度。 所以,从空间上考虑 varchar 比较合适; 从效率上考虑 char 比较合适,二者使用需要权衡。

169.float 和 double 的区别是什么?

float 最多可以存储 8 位的十进制数,并在内存中占 4 字节。 double 最可可以存储 16 位的十进制数,并在内存中占 8 字节。

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

  • 内连接关键字:inner join;
  • 左连接:left join;
  • 右连接:right join。

区别:

  • 内连接是把匹配的关联数据显示出来;
  • 左连接是左边的表全部显示出来,右边的表显示出符合条件的数据;
  • 右连接正好相反。

171.mysql 索引是怎么实现的?

索引是满足某种特定查找算法的数据结构,而这些数据结构会以某种方式指向数据,从而实现高效查找

具体来说 MySQL 中的索引,不同的数据引擎实现有所不同,但目前主流的数据库引擎 的索引都是 B+ 树实现的,B+ 树的搜索效率,可以到达二分法的性能,找到数据区域 之后就找到了完整的数据结构了,所有索引的性能也是更好的。

172.怎么验证 mysql 的索引是否满足需求?

使用 explain查看SQL是如何执行查询语句的,从而分析你的索引是否满足需求。

explain 语法:explain select * from table where type=1。

173.说一下数据库的事务隔离

脏读、不可重复读、幻读
脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。

当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下

update account
set money=money + 100
where name =’B’;
    (此时A通知B)

update account
set money=money - 100
where name =’A’;

当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。

不可重复读

不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……

虚读(幻读)

幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

数据库隔离级别

  • Read uncommitted (读未提交):最低级别,以上问题均无法解决。
  • Read committed (读已提交):读已提交,可避免脏读情况发生。
  • Repeatable Read(可重复读):确保事务可以多次从一个字段中读取相同的值,在此事务持续期间,禁止其他事务对此字段的更新,可以避免脏读和不可重复读,仍会出现幻读问题。(mysql默认)
  • Serializable (串行化):最严格的事务隔离级别,要求所有事务被串行执行,不能并发执行,可避免脏读、不可重复读、幻读情况的发生。

Java面试题整理(带答案)_第17张图片

174.说一下 mysql 常用的引擎?

  • InnoDB支持事务,MyISAM不支持,这一点是非常之重要。事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而MyISAM就不可以了。

  • MyISAM适合查询以及插入为主的应用,InnoDB适合频繁修改以及涉及到安全性较高的应用

  • InnoDB支持外键,MyISAM不支持

  • 从MySQL5.5.5以后,InnoDB是默认引擎

  • InnoDB不支持FULLTEXT类型的索引

  • InnoDB中不保存表的行数,如select count() from table时,InnoDB需要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含where条件时MyISAM也需要扫描整个表

  • 对于自增长的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中可以和其他字段一起建立联合索引

  • 清空整个表时,InnoDB是一行一行的删除,效率非常慢。MyISAM则会重建表

  • InnoDB支持行锁(某些情况下还是锁整表,如 update table set a=1 where user like ‘%lee%’。

175.说一下 mysql 的行锁和表锁?

MyISAM 只支持表锁,InnoDB 支持表锁和行锁,默认为行锁。

  • 表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低。
  • 行级锁:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高。

176.说一下乐观锁和悲观锁?

  • 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
  • 悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。

数据库的乐观锁需要自己实现,在表里面添加一个 version 字段,每次修改成功值加1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version 是否一致, 如果不一致就不修改,这样就实现了乐观锁

177.mysql 问题排查都有哪些手段?

  • 使用 show processlist 命令查看当前所有连接信息。
  • 使用 explain 命令查询 SQL 语句执行计划。
  • 开启慢查询日志,查看慢查询的 SQL。

178.如何做 mysql 的性能优化?

  • 为搜索字段创建索引。
  • 避免使用 select *,列出需要查询的字段。
  • 垂直分割分表。
  • 选择正确的存储引擎。

十八、Redis

179.redis 是什么?都有哪些使用场景?

Redis是什么

首先要说redis,应该先说一下nosql,NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题,包括超大规模数据的存储。例如谷歌或Facebook每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。

Redis:REmote DIctionary Server(远程字典服务器)是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构服务器。

Redis的使用场景

  1. 热点数据的缓存

    由于redis访问速度块、支持的数据类型比较丰富,所以redis很适合用来存储热点数据,另外结合expire,我们可以设置过期时间然后再进行缓存更新操作,这个功能最为常见,我们几乎所有的项目都有所运用。

  2. 限时业务的运用

    redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。

  3. 计数器相关问题

    redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。

  4. 排行榜相关问题

    关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。

  5. 分布式锁

    这个主要利用redis的setnx命令进行,setnx:"set if not exists"就是如果不存在则成功设置缓存同时返回1,否则返回0,这个特性在俞你奔远方的后台中有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock,如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。当然我们可以将这个特性运用于其他需要分布式锁的场景中,结合过期时间主要是防止死锁的出现。

  6. 延时操作

    比如在订单生产后我们占用了库存,10分钟后去检验用户是够真正购买,如果没有购买将该单据设置无效,同时还原库存。由于redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。所以我们对于上面的需求就可以用以下解决方案,我们在订单生产时,设置一个key,同时设置10分钟后过期, 我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。当然我们也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。

  7. 分页、模糊搜索

    redis的set集合中提供了一个zrangebylex方法,语法如下:ZRANGEBYLEX key min max [LIMIT offset count]。通过ZRANGEBYLEX zset - + LIMIT 0 10 可以进行分页数据查询,其中- +表示获取全部数据 zrangebylex key min max 这个就可以返回字典区间的数据,利用这个特性可以进行模糊查询功能,这个也是目前我在redis中发现的唯一一个支持对存储内容进行模糊查询的特性。

  8. 点赞、好友等相互关系的存储

    Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。又或者在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。

  9. 队列

    由于redis有list push和list pop这样的命令,所以能够很方便的执行队列操作。

180.redis 有哪些功能?

缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。

  • 排行榜,在使用传统的关系型数据库(mysql oracle 等)来做这个事儿,非常的麻烦,而利用Redis的SortSet(有序集合)数据结构能够简单的搞定;

  • 计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;

  • 好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;

  • 简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;

  • Session共享,以PHP为例,默认Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。

  • 一些频繁被访问的数据,经常被访问的数据如果放在关系型数据库,每次查询的开销都会很大,而放在redis中,因为redis 是放在内存中的可以很高效的访问

181.redis 和 memecache 有什么区别?

什么是memecache?

memcached是一套分布式的高速缓存系统,与redis相似。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失。另外,内容容量达到指定值之后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。memcached本身是为缓存而设计的服务器,因此并没有过多考虑数据的永久性问题。

redis 和 memecache 有什么区别?

Redis的作者Salvatore Sanfilippo曾经对这两种基于内存的数据存储系统进行过比较:

  • Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。

  • 内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。

  • 性能对比:由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。

182.redis 为什么是单线程的?

官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。

183.什么是缓存雪崩、缓存穿透、缓存击穿?怎么解决?

缓存雪崩

由于Redis中大批的key同时过期,导致大量的请求绕过了Redis直接请求到数据库,造成数据库压力激增,最终可能导致数据库不可用。

解决方案:

  • 对一批key可以设置随机的过期时间

缓存穿透

常见于攻击手段,攻击者通过查询不存在的key,在Redis中找不到,只能去数据库查找,但是数据库中也查找不到,每次攻击者的请求都会打到数据库上,造成数据库压力激增,可能导致数据库不可用

解决方案:

  • 后端服务器对于访问者的身份权限做出校验
  • 对于请求的数据进行格式校验,比如id=-1这种类型的请求直接做出限制
  • 采用布隆过滤器,对于一定不存在值直接过滤拦截
  • 对于redis中查询不到的key,去查询数据库,如果数据库也查询不到,那么可以在Redis中存放该key的值为空

缓存击穿

热点问题,一个热点key如果过期了,会导致访问该key的请求同时进入数据库,造成数据库的压力剧增,可能导致数据库不可用

解决方案:

  • 热点数据设置不过期
  • 采用互斥锁的形式,代码参考如下:
 public class RedisTest {

    static ReentrantLock reentrantLock = new ReentrantLock();

    public static String getData(String key) throws InterruptedException {
        //调用getDataFromRedis方法从Redis缓存中读取数据,getDataFromRedis方法看自己需求实现
        String result = getDataFromRedis(key);
        //缓存中不存在数据
        if (result == null) {
            //去获取锁,获取成功,去数据库取数据
            if (reentrantLock.tryLock()) {
                //调用getDataFromMysql方法从数据获取数据,getDataFromMysql方法看自己需求实现
                result = getDataFromMysql(key);
                //从数据库查出之后在调用setDataToRedis方法写入到Redis中
                if ((result != null)) {
                    setDataToRedis(key, result);
                }
                //释放锁
                reentrantLock.unlock();
            }
        }
        //其他同时并发的线程会获取锁失败
        else {
            //让它们暂停100ms再重新去获取数据
            Thread.sleep(100);
            result = getData(key);
        }
        return result;
    }
}

184.redis 支持的数据类型有哪些?

string(字符串)

与memcached一样,一个key对应一个value,key的最大存储值为512MB,value的最大存储值也为512MB。string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。使用设置和获取的命令为SET和GET。命令为【SET key value】【GET key】

hash(哈希)

键值(key=>value)对集合。 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象(每个hash可以存储2的32次方 -1 键值对(40多亿))。使用设置和获取的命令为 HMSET, HGET。命令为【HMSET key key1 value1 key2 value2】【HGET key key1】

list(列表)

列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部或者尾部(列表最多可存储2的32次方 - 1 元素 (4294967295, 每个列表可存储40多亿))。进值命令为LPUSH或者RPUSH,获取值命令为LRANGE。命令为【LPUSH key value】【LRANGE key 0 10】获取key列表从左边开始0到10个value。

set(集合)

Set 是 string 类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。集合中最大的成员数为 2的32次方 - 1(4294967295, 每个集合可存储40多亿个成员)。SADD添加一个 string 元素到 key 对应的 set 集合中,成功返回 1,如果元素已经在集合中返回 0。 命令为【SADD key value】【SMEMBERS key】

zset(有序集合)

和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。ZADD添加元素到集合,元素在集合中存在则更新对应score。

185.redis 支持的 java 客户端都有哪些?

  • jedis
  • redisson
  • lettuce

186.jedis 和 redisson 有哪些区别?

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

187.怎么保证缓存和数据库数据的一致性?

第一种方案:采用延时双删策略

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。 伪代码如下:

public void write(String key,Object data){
        redis.delKey(key);
        db.updateData(data);
        Thread.sleep(500);
        redis.delKey(key);
        }

具体的步骤就是:

  • 先删除缓存;
  • 再写数据库;
  • 休眠500毫秒(休眠时间要保证数据库更新完成);
  • 再次删除缓存。

第二种方案:异步更新缓存(基于订阅binlog的同步机制)

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

  • 读Redis:热数据基本都在Redis
  • 写MySQL:增删改都是操作MySQL
  • 更新Redis数据:MySQ的数据操作binlog,来更新到Redis

Redis更新

1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)
  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析,利用消息队列,推送更新各台的redis缓存数据。

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

188.redis 持久化有几种方式?

由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。那么这两种持久化方式有什么区别呢,改如何选择呢?

二者的区别

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

Java面试题整理(带答案)_第18张图片

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

AOF

二者优缺点

RDB存在哪些优势呢?

  • 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

  • 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

  • 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

  • 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

RDB又存在哪些劣势呢?

  • 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
  • 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF的优势有哪些呢?

  • 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。

  • 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。

  • 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。

  • AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。

AOF的劣势有哪些呢

  • 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
  • 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually
consistent的意思了。

189.redis 怎么实现分布式锁?

我们从最简单的开始讲起。

想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX 命令,这个命令表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。

两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。

客户端 1 申请加锁,加锁成功:

127.0.0.1:6379> SETNX lock 1
(integer) 1 // 客户端1,加锁成功

客户端 2 申请加锁,因为后到达,加锁失败:

127.0.0.1:6379> SETNX lock 1
(integer) 0 // 客户端2,加锁失败

此时,加锁成功的客户端,就可以去操作「共享资源」,例如,修改 MySQL 的某一行数据,或者调用一个API 请求。操作完成后,还要及时释放锁,给后来者让出操作共享资源的机会。

如何释放锁呢?也很简单,直接使用 DEL 命令删除这个 key 即可:

127.0.0.1:6379> DEL lock // 释放锁
(integer)1

但是,它存在一个很大的问题,当客户端 1 拿到锁后,如果发生下面的场景,就会造成「死锁」:

  • 程序处理业务逻辑异常
  • 没及时释放锁进程挂了,没机会释放锁

这时,这个客户端就会一直占用这个锁,而其它客户端就「永远」拿不到这把锁了。

如何避免死锁

为了解决以上死锁问题,最容易想到的方案是在申请锁时,在Redis中实现时,给锁设置一个过期时间,假设操作共享资源的时间不会超过10s,那么加锁时,给这个key设置10s过期即可。

但以上操作还是有问题,加锁、设置过期时间是2条命令,有可能只执行了第一条,第二条却执行失败,例如:

  • SETNX执行成功,执行EXPIRE时由于网络问题,执行失败
  • SETNX执行成功,Redis异常宕机,EXPIRE没有机会执行
  • SETNX执行成功,客户端异常崩溃,EXPIRE没有机会执行

总之这两条命令如果不能保证是原子操作,就有潜在的风险导致过期时间设置失败,依旧有可能发生死锁问题。幸好在Redis
2.6.12之后,Redis扩展了SET命令的参数,可以在SET的同时指定EXPIRE时间,这条操作是原子的,例如以下命令是设置锁的过期时间为10秒。

SET lock_key 1 EX 10 NX

至此,解决了死锁问题,但还是有其他问题。想像下面这个这样一种场景:

Java面试题整理(带答案)_第19张图片

  • 客户端1加锁成功,开始操作共享资源
  • 客户端1操作共享资源耗时太久,超过了锁的过期时间,锁失效(锁被自动释放)
  • 客户端2加锁成功,开始操作共享资源
  • 客户端1操作共享资源完成,在finally块中手动释放锁,但此时它释放的是客户端2的锁。

这里存在两个严重的问题:

  • 锁过期
  • 释放了别人的锁

第1个问题是评估操作共享资源的时间不准确导致的,如果只是一味增大过期时间,只能缓解问题降低出现问题的概率,依旧无法彻底解决问题。原因在于客户端在拿到锁之后,在操作共享资源时,遇到的场景是很复杂的,既然是预估的时间,也只能是大致的计算,不可能覆盖所有导致耗时变长的场景。

第2个问题是释放了别人的锁,原因在于释放锁的操作是无脑操作,并没有检查这把锁的归属,这样解锁不严谨。如何解决呢?

锁被别人给释放了

解决办法是,客户端在加锁时,设置一个只有自己知道的唯一标识进去,例如可以是自己的线程ID,如果是redis实现,就是SET key unique_value EX 10 NX。之后在释放锁时,要先判断这把锁是否归自己持有,只有是自己的才能释放它。

//释放锁 比较unique_value是否相等,避免误释放
if redis.get("key")==unique_value then
        return redis.del("key")

这里释放锁使用的是GET + DEL两条命令,这时又会遇到原子性问题了。

  • 客户端1执行GET,判断锁是自己的
  • 客户端2执行了SET命令,恰好1释放了锁,客户端2获取到锁(虽然发生概念很低,但要严谨考虑锁的安全性)
  • 客户端1执行DEL,却释放了客户端2的锁

由此可见,以上GET + DEL两个命令还是必须原子的执行才行。怎样原子执行两条命令呢?答案是Lua脚本,可以把以上逻辑写成Lua脚本,让Redis执行。因为Redis处理每个请求是单线程执行的,在执行一个Lua脚本时其它请求必须等待,直到这个Lua脚本处理完成,这样一来GET+DEL之间就不会有其他命令执行了。

以下是使用Lua脚本(unlock.script)实现的释放锁操作的伪代码,其中,KEYS[1]表示lock_key,ARGV[1]是当前客户端的唯一标识,这两个值都是我们在执行 Lua脚本时作为参数传入的。

//Lua脚本语言,释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

最后我们执行以下命令,即可

redis-cli  --eval  unlock.script lock_key , unique_value 

这样一路优先下来,整个加锁、解锁流程就更严谨了,先小结一下,基于Redis实现的分布式锁,一个严谨的流程如下:

  • 加锁时要设置过期时间SET lock_key unique_value EX expire_time NX
  • 操作共享资源
  • 释放锁:Lua脚本,先GET判断锁是否归属自己,再DEL释放锁

有了这个严谨的锁模型,我们还需要重新思考之前的那个问题,锁的过期时间不好评估怎么办。

前面提到过,过期时间如果评估得不好,这个锁就会有提前过期的风险,一种妥协的解决方案是,尽量冗余过期时间,降低锁提前过期的概率,但这个方案并不能完美解决问题。是否可以设置这样的方案,加锁时,先设置一个预估的过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。

这是一种比较好的方案,已经有一个库把这些工作都封装好了,它就是Redisson。Redisson是一个Java语言实现的Redis SDK客户端,在使用分布式锁时,它就采用了自动续期的方案来避免锁过期,这个守护线程我们一般叫它看门狗线程。这个SDK提供的API非常友好,它可以像操作本地锁一样操作分布式锁。客户端一旦加锁成功,就会启动一个watch dog看门狗线程,它是一个后台线程,会每隔一段时间(这段时间的长度与设置的锁的过期时间有关)检查一下,如果检查时客户端还持有锁key(也就是说还在操作共享资源),那么就会延长锁key的生存时间。

Java面试题整理(带答案)_第20张图片

那如果客户端在加锁成功后就宕机了呢?宕机了那么看门狗任务就不存在了,也就无法为锁续期了,锁到期自动失效。

Redis的部署方式对锁的影响
上面讨论的情况,都是锁在单个Redis 实例中可能产生的问题,并没有涉及到Redis的部署架构细节。

Redis发展到现在,几种常见的部署架构有:

  • 单机模式;
  • 主从模式;
  • 哨兵(sentinel)模式;
  • 集群模式;

我们使用Redis时,一般会采用主从集群+哨兵的模式部署,哨兵的作用就是监测redis节点的运行状态。普通的主从模式,当master崩溃时,需要手动切换让slave成为master,使用主从+哨兵结合的好处在于,当master异常宕机时,哨兵可以实现故障自动切换,把slave提升为新的master,继续提供服务,以此保证可用性。那么当主从发生切换时,分布式锁依旧安全吗?

Java面试题整理(带答案)_第21张图片

想像这样的场景:

  • 客户端1在master上执行SET命令,加锁成功
  • 此时,master异常宕机,SET命令还未同步到slave上(主从复制是异步的)
  • 哨兵将slave提升为新的master,但这个锁在新的master上丢失了,导致客户端2来加锁成功了,两个客户端共同操作共享资源

可见,当引入Redis副本后,分布式锁还是可能受到影响。即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况。

集群模式+Redlock实现高可靠的分布式锁

为了避免Redis实例故障而导致的锁无法工作的问题,Redis的开发者 Antirez提出了分布式锁算法Redlock。Redlock算法的基本思路,是让客户端和多个独立的Redis实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个Redis实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

来具体看下Redlock算法的执行步骤。Redlock算法的实现要求Redis采用集群部署模式,无哨兵节点,需要有N个独立的Redis实例(官方推荐至少5个实例)。接下来,我们可以分成3步来完成加锁操作

Java面试题整理(带答案)_第22张图片

第一步是,客户端获取当前时间。

第二步是,客户端按顺序依次向N个Redis实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,使用SET命令,带上NX、EX/PX选项,以及带上客户端的唯一标识。当然,如果某个Redis实例发生故障了,为了保证在这种情况下,Redlock算法能够继续运行,我们需要给加锁操作设置一个超时时间。如果客户端在和一个Redis实例请求加锁时,一直到超时都没有成功,那么此时,客户端会和下一个Redis实例继续请求加锁。加锁操作的超时时间需要远远地小于锁的有效时间,一般也就是设置为几十毫秒。

第三步是,一旦客户端完成了和所有Redis实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

客户端只有在满足两个条件时,才能认为是加锁成功,条件一是客户端从超过半数(大于等于 N/2+1)的Redis实例上成功获取到了锁;条件二是客户端获取锁的总耗时没有超过锁的有效时间。

为什么大多数实例加锁成功才能算成功呢?多个Redis实例一起来用,其实就组成了一个分布式系统。在分布式系统中总会出现异常节点,所以在谈论分布式系统时,需要考虑异常节点达到多少个,也依旧不影响整个系统的正确运行。这是一个分布式系统的容错问题,这个问题的结论是:如果只存在故障节点,只要大多数节点正常,那么整个系统依旧可以提供正确服务。

在满足了这两个条件后,我们需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成共享资源操作,锁就过期了的情况。

当然,如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端就要向所有Redis节点发起释放锁的操作。为什么释放锁,要操作所有的节点呢,不能只操作那些加锁成功的节点吗?因为在某一个Redis节点加锁时,可能因为网络原因导致加锁失败,例如一个客户端在一个Redis实例上加锁成功,但在读取响应结果时由于网络问题导致读取失败,那这把锁其实已经在Redis上加锁成功了。所以释放锁时,不管之前有没有加锁成功,需要释放所有节点上的锁以保证清理节点上的残留的锁。

在Redlock算法中,释放锁的操作和在单实例上释放锁的操作一样,只要执行释放锁的 Lua脚本就可以了。这样一来,只要N个Redis实例中的半数以上实例能正常工作,就能保证分布式锁的正常工作了。所以,在实际的业务应用中,如果你想要提升分布式锁的可靠性,就可以通过Redlock算法来实现。

原文链接:https://blog.csdn.net/fuzhongmin05/article/details/119251590

190.redis 分布式锁有什么缺陷?

详细见上题

191.redis 如何做内存优化?

缩减键值对象

缩减键(key)和值(value)的长度,

  • key长度:如在设计键时,在完整描述业务情况下,键值越短越好。

  • value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。以JAVA为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等,下图是JAVA常见序列化工具空间压缩对比。

共享对象池

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

字符串优化

编码优化

控制key的数量

192.redis 淘汰策略有哪些?

  • noeviction : Redis 默认淘汰策略,对于写请求不再提供服务,直接返回错误( DEL 请求和部分特殊请求除外)
  • allkeys - lru : 从所有 key 中使用 LRU 算法进行淘汰
  • volatile - lru : 从设置了过期时间的 key 中使用 LRU 算法进行淘汰
  • allkeys - random : 从所有 key 中随机淘汰数据
  • volatile - random : 从设置了过期时间的 key 中随机淘汰
  • volatile - ttl : 从设置了过期时间的 key 中,根据 key 的过期时间进行淘汰,越早过期的越优先被淘汰

193.redis 常见的性能问题有哪些?该如何解决?

主服务器写内存快照,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以主服务器最好不要写内存快照。 Redis 主从复制的性能问题,为了主从复制的速度和连接的稳定性,主从库最好在同一个局域网内。

十九、JVM

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

  • 类加载器:负责加载字节码文件,即java编译后的 .class 文件

  • 运行时数据区:负责存放.class 文件,分配内存

    • 方法区:负责存放.class 文件,方法区里有一块区域是运行时常量池,用来存放程序的常量。
    • 堆:分配给对象的内存空间。
    • 方法区和堆是所有线程共享的内存空间。
    • java虚拟机栈:每个线程独享的内存空间。
    • 本地方法栈:本地native 方法独享的内存空间。
    • 程序计数器:记录线程执行的位置,方便线程切换后再次执行。

    java虚拟机栈,本地方法栈,程序计数器是每个线程独享的。

  • 执行引擎

195.说一下 jvm 运行时数据区?

方法区(也有人叫永久代)

和堆一样所有线程共享,主要用于存储已被jvm加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(在JDK1.7发布的HotSpot中,已经把字符串常量池移除方法区了。)

  • 常量池:

    • 运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

    • Java虚拟机对class文件每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范才会被jvm认可。但对于运行时常量池,Java虚拟机规范没做任何细节要求。

    • 运行时常量池有个重要特性是动态性,Java语言不要求常量一定只在编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也有可能将新的常量放入池中,这种特性使用最多的是String类的intern()方法。

    • 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制。当常量池无法再申请到内存时会抛出outOfMemoryError异常。

Java堆

对于大多数应用来说,堆空间是jvm内存中最大的一块。Java堆是被所有线程共享,虚拟机启动时创建,此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也就变得不那么绝对了。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。从内存回收角度看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。(如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。)

Java虚拟机栈

线程私有,生命周期和线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本类型数据(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型(指向了一条字节码指令的地址)。

其中64位长度的long和double类型的数据会占用2个局部变量表空间(slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法所需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出Stack OverflowError异常;如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机中使用到的native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样也会抛出Stack OverflowError异常和OutOfMemoryError异常。

程序计数器

占据一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此未来线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们成这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java方法,这个计数器记录的则是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空(undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

196.说一下堆栈的区别?

  • 堆:用来存放我们构建出来的对象
  • 栈:保存方法调用,每调用一次方法,压入一次栈帧,栈帧存放的是临时变量表,操作数栈,返回地址(方法出口)

197.队列和栈是什么?有什么区别?

  • 队列:先入先出
  • 栈:后入先出

198.什么是双亲委派模型?

类加载器的类别

  • BootstrapClassLoader(启动类加载器): C++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

  • ExtClassLoader(标准扩展类加载器): java编写,加载扩展库,如classpath中的jre ,javax.*或者java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

  • AppClassLoader(系统类加载器): java编写,加载程序所在的目录,如user.dir所在的位置的class

  • CustomClassLoader(用户自定义类加载器): java编写,用户自定义的类加载器,可加载指定路径的class文件

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

Java面试题整理(带答案)_第23张图片

从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

199.说一下类加载的执行过程?

类什么时候才被加载

  • 创建类的实例,也就是new一个对象
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(Class.forName(“com.lyj.load”))
  • 初始化一个类的子类(会首先初始化子类的父类)
  • JVM启动时标明的启动类,即文件名和类名相同的那个类

类加载过程(顺序)

  • 加载:加载我们正常编译好的.class文件,jar包中的.class文件,网络上的.class文件

  • 链接:

    • 验证:class文件的来源很多,需要检验它的文件格式、元数据、符号引用
    • 准备:为静态常量和静态成员变量进行内存分配。如果是静态成员(非常量),会初始化为默认值,如int类型的默认值为0
    • 解析:将符号引用转为直接引用
  • 初始化:执行类构造方法,这里的类构造方法不是我们写的有参构造无参构造的方法,而是编译后生成的CLInit方法,会对静态成员变量进行具体赋值,执行static{}静态代码块。初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法

  • 使用

  • 卸载

200.怎么判断对象是否可以被回收?

引用计数算法(已被淘汰的算法)

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象之间相互循环引用的问题。尽管该算法执行效率很高。

可达性分析算法

目前主流的编程语言(java,C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象

Java中的GC Roots:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

201.java 中都有哪些引用类型?

  • 强引用:Java中默认声明的就是强引用,比如:
        // 只要强引用存在,垃圾回收器将永远不会回收被引用的对象,
// 哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。
// 如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
        Object obj=new Object(); //只要obj还指向Object对象,Object对象就不会被回收
                obj=null;  //手动置null后会回收刚刚创建的对象
  • 软引用:软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。

  • 弱引用:弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。

  • 虚引用:虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

引用队列(ReferenceQueue)

引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。与软引用、弱引用不同,虚引用必须和引用队列一起使用。

202.说一下 jvm 有哪些垃圾回收算法?

标记-清除算法

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

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

此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。在Java堆中被分为了新生代和老年代,这样的划分是方便GC。Java堆中的新生代就使用了GC复制算法。在新生代中又分为了三个区域:Eden 空间、To Survivor空间、From Survivor空间。不妨将注意力回到这张图的左边新生代部分:

Java面试题整理(带答案)_第24张图片

新的对象实例被创建的时候通常在Eden空间,发生在Eden空间上的GC称为Minor GC,当在新生代发生一次GC后,会将Eden和其中一个Survivor空间的内存复制到另外一个Survivor中,如果反复几次有对象一直存活,此时内存对象将会被移至老年代。可以看到新生代中Eden占了大部分,而两个Survivor实际上占了很小一部分。这是因为大部分的对象被创建过后很快就会被GC(这里也许运用了是二八原则)。

标记-整理算法(或称为标记-压缩算法,Java堆中老年代的垃圾回收算法)

对于新生代,大部分对象都不会存活,所以在新生代中使用复制算法较为高效,而对于老年代来讲,大部分对象可能会继续存活下去,如果此时还是利用复制算法,效率则会降低。标记-压缩算法首先还是“标记”,标记过后,将不用回收的内存对象压缩到内存一端,此时即可直接清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。老年代的垃圾回收称为“Major GC”。

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

  • 串行垃圾回收器
  • 并行垃圾回收器
  • CMS垃圾回收器
  • G1垃圾回收器

204.详细介绍一下 CMS 垃圾回收器?

CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能知道它是标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:

  • 初始标记:标记一下GC Roots能直接关联到的对象,会"Stop The World"。
  • 并发标记:GC Roots Tracing,可以和用户线程并发执行。
  • 重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
  • 并发清除:清除对象,可以和用户线程并发执行。

由于垃圾回收线程可以和用户线程同时运行,也就是说它是并发的,那么它会对CPU的资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/ 4,当CPU<4个时,并发回收是垃圾收集线程就不会少于25%,而且随着CPU减少而增加,这样会影响用户线程的执行。而且由于它是基于标记-清除算法的,那么就无法避免空间碎片的产生。CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

所谓浮动垃圾,在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留待下一次GC时再清理掉。

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

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代垃圾回收器一般采用的是标记-复制算法,标记-复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

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

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,

执行流程如下:

  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  • 清空 Eden 和 From Survivor 分区; From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
  • 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
  • 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。
  • 以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

207.说一下 jvm 调优的工具?

常用调优工具分为两类:

  • jdk自带监控工具:
    • jconsole
    • jvisualvm
  • 第三方工具:
    • MAT(Memory AnalyzerTool)
    • GChisto。

jconsole全称Java Monitoring and Management Console,是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存, 线程和类等的监控。

jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。

MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富 的Javaheap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。

GChisto,一款专业分析gc日志的工具。

208.常用的 jvm 调优的参数都有哪些?

  • jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap,JVM Memory Map命令用于生成heap dump文件
  • jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
  • jstack,用于生成java虚拟机当前时刻的线程快照。
  • jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

你可能感兴趣的:(java,面试)