推广 热搜: 公司  快速  上海  未来  中国    企业  政策  教师  系统 

优化后老程序猿 最新面试总结(附加答案,最细,最全面)

   日期:2024-10-06       caijiyuan   评论:0    移动:http://qyn41e.riyuangf.com/news/18276.html
核心提示:简单易学(语法简单,上手容易);面向对象(封装,继承,多态);平台

  1. 简单易学(语法简单,上手容易
  2. 面向对象(封装,继承,多态
  3. 平台无关性( Java 虚拟机实现平台无关性
  4. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持
  5. 可靠性(具备异常处理和自动内存管理机制
  6. 安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源
  7. 高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的
  8. 支持网络编程并且很方便
  9. 编译与解释并存

🌈 拓展一下

“Write Once, Run Anywhere(一次编写,随处运行)”这句宣传口号,真心经典,流传了好多年!以至于,直到今天,依然有很多人觉得跨平台是 Java 语言最大的优势。实际上,跨平台已经不是 Java 最大的卖点了,各种 JDK 新特性也不是。目前市面上虚拟化技术已经非常成熟,比如你通过 Docker 就很容易实现跨平台了。在我看来,Java 强大的生态才是

1.1、Java SE vs Java EE


Java SE(Java Platform,Standard Edition): Java 平台标准版,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建桌面应用程序或简单的服务器应用程序。
Java EE(Java Platform, Enterprise Edition :Java 平台企业版,建立在 Java SE 的基础上,包含了支持企业级应用程序开发和部署的标准和规范(比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序。
简单来说,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。除了 Java SE 和 Java EE,还有一个 Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。

1.2、JVM vs JDK vs JRE


JVM
Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS,目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。


运行在 Java 虚拟机之上的编程语言JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。 也就是说我们平时接触到的 HotSpot VM 仅仅是是 JVM 规范的一种实现而已。除了我们平时最常用的 HotSpot VM 外,还有 J9 VM、Zing VM、JRockit VM 等 JVM 。维基百科上就有常见 JVM 的对比:Comparison of Java virtual machinesopen in new window ,感兴趣的可以去看看。并且,你可以在 Java SE Specificationsopen in new window 上找到各个版本的 JDK 对应的 JVM 规范。

JDK 和 JRE
JDK(Java Development Kit,它是功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 Java 程序的开发套件。它包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控?具)、javap(反编译工具)等等。

JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。

也就是说,JRE 是 Java 运行时环境,仅包含 Java 应用程序的运行时环境和必要的类库。而 JDK 则包含了 JRE,同时还包括了 javac、javadoc、jdb、jconsole、javap 等工具,可以用于 Java 应用程序的开发和调试。如果需要进行 Java 编程工作,比如编写和编译 Java 程序、使用 Java API 文档等,就需要安装 JDK。而对于某些需要使用 Java 特性的应用程序,如 JSP 转换为 Java Servlet、使用反射等,也需要 JDK 来编译和运行 Java 代码。因此,即使不打算进行 Java 应用程序的开发工作,也有可能需要安装 JDK。

不过,从 JDK 9 开始,就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块+ jlinkopen in new window 工具 (随 Java 9 一起发布的新命令行工具,用于生成自定义 Java 运行时映像,该映像仅包含给定应用程序所需的模块) 。并且,从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载。

在 Java 9 新特性概览open in new window这篇文章中,我在介绍模块化系统的时候提到

在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。

也就是说,可以用 jlink 根据自己的需求,创建一个更小的 runtime(运行时,而不是不管什么应用,都是同样的 JRE。

定制的、模块化的 Java 运行时映像有助于简化 Java 应用的部署和节省内存并增强安全性和可维护性。这对于满足现代应用程序架构的需求,如虚拟化、容器化、微服务和云原生开发,是非常重要的。

1.3、什么是字节码?采用字节码的好处是什么?


在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件,它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

Java 程序从源代码到运行的过程如下图所示


我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言 。

1.4、为什么说 Java 语言“编译与解释并存”


其实这个问题我们讲字节码的时候已经提到过,因为比较重要,所以我们这里再提一下。

我们可以将高级编程语言按照程序的执行方式分为两种

  • 编译型:编译型语言open in new window 会通过编译器open in new window将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
  • 解释型:解释型语言open in new window会通过解释器open in new window一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、Javascript、PHP 等等。

为什么说 Java 语言“编译与解释并存”
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件,这种字节码必须由 Java 解释器来解释执行。


定义:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类 型,在内存中分配了不同大小的内存空间。

分类

基本数据类型

  • ? 数值型
  • ? 整数类型(byte,short,int,long)
  • ? 浮点类型(float,double)
  • ? 字符型(char)
  • ? 布尔型(boolean)

引用数据类型

  • ? 类(class)
  • ? 接口(interface)
  • ? 数组([])

Java基本数据类型图


封装: 使用private关键字,让对象私有,防止无关的程序去使用。
继承: 继承某个类,使子类可以使用父类的属性和方法。
多态: 同一个行为,不同的子类具有不同的表现形式。


1.重写(Override)


从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。

例如

 


重写 总结

  • 发生在父类与子类之间
  • 方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
  • 访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
  • 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常

2.重载(Overload)


在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。

例如

 


重载 总结

  • 重载Overload是一个类中多态性的一种表现
  • 重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
  • 重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准

面试时,问:重载(Overload)和重写(Override)的区别

:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。

5、访问修饰符 public,private,protected,以及不写(默认)时的 区别


定义Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访 问。Java 支持 4 种不同的访问权限。

分类

  • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部 类
  • default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用 任何修饰符。使用对象:类、接口、变量、方法。
  • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意: 不能修饰类(外部类)。
  • public : 对所有类可见。使用对象:类、接口、变量、方法

访问修饰符图


6、==和equals的区别


== 对于基本类型和引用类型的作用效果是不同的

对于基本数据类型来说== 比较的是值。
对于引用数据类型来说== 比较的是对象的内存地址。


因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

Object 类 equals() 方法

 



equals() 方法存在两种使用情况

  • 类没有重写 equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
  • 类重写了 equals()方法:一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 == 换成 equals()

 


String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。

当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

String类equals()方法

 


hashCode() 有什么用
hashCode() 的作用是获取哈希码(int 整数,也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。


hashCode() 方法hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是:Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的。

散列表存储的是键值对(key-value),它的特点是能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码(可以快速找到所需要的对象

为什么要有 hashCode
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode
下面这段内容摘自我的 Java 启蒙书《Head First Java》:

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

其实, hashCode() 和 equals()都是用于比较两个对象是否相等。

那为什么 JDK 还要同时提供这两个方法呢

  • 这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程
  • 我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。

那为什么不只提供 hashCode() 方法呢

  • 这是因为两个对象的hashCode 值相等并不代表两个对象就相等。

那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的
因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。

总结下来就是

  • 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
  • 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
  • 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。

相信大家看了我前面对 hashCode() 和 equals() 的介绍之后,下面这个问题已经难不倒你们了。

为什么重写 equals() 时必须重写 hashCode() 方法
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

思考:重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。

总结

  • equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
  • 两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。

7、&和&&的区别


用法 &&和&都是表示 与
区别是&&若第一个条件不满足,后面条件就不再判断。而&要对所有的条件都进行判断
返回值true/false

8、 | 和 || 的区别


用法|| 和 | 都是表示 或
区别 || 若第一个条件满足,后面条件就不再判断。而 | 要对所有的条件都进行判断
返回值true/false

9、String、StringBuffer、StringBuilder的区别


1、String

String是不可变的,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。

可以看出来,再次给s赋值时,并不是对原来堆中实例对象进行重新赋值,而是生成一个新的实例对象,并且指向“def”这个字符串,s则指向最新生成的实例对象,之前的实例对象仍然存在,如果没有被再次引用,则会被垃圾回收。


2、StringBuffer

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。StringBuffer对象是一个字符序列可变的字符串,它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串。


3、StringBuilder

StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。

4、StringBuffer类中实现的方法,StringBuffer是如何实现线程安全的呢

StringBuffer类中的方法都添加了synchronized关键字,也就是给这个方法添加了一个锁,用来保证线程安全。

5、StringBuilder类中实现的方法。

6、String、StringBuffer、StringBuilder比较。

三者共同之处:都是final类,不允许被继承,主要是从性能和安全性上考虑的,因为这几个类都是经常被使用着,且考虑到防止其中的参数被参数修改影响到其他的应用。

  • StringBuffer是线程安全,可以不需要额外的同步用于多线程中;
  • StringBuilder是非同步,运行于多线程中就需要使用着单独同步处理,但是速度就比StringBuffer快多了;
  • StringBuffer与StringBuilder两者共同之处:可以通过append、indert进行字符串的操作。
  • String实现了三个接口:Serializable、Comparable、CarSequence
  • StringBuilder只实现了两个接口Serializable、CharSequence,相比之下String的实例可以通过compareTo方法进行比较,其他两个不可以。

7、运行速度
执行速度由快到慢:StringBuilder > StringBuffer > String

8、小结

  • 如果要操作少量的数据用 String
  • 多线程操作字符串缓冲区下操作大量数据 StringBuffer
  • 单线程操作字符串缓冲区下操作大量数据 StringBuilder。


在运行过程中,对于任何一个类都能获取它的属性和方法,任何一个对象都能调用其方法,动态获取信息和动态调用,就是反射。

Java获取反射的三种方法

1.通过new对象实现反射机制

2.通过路径实现反射机制

3.通过类名实现反射机制


浅拷贝: 基础数据类型复制值,引用类型复制引用地址,修改一个对象的值,另一个对象也随之改变。
深拷贝: 基础数据类型复制值,引用类型在新的内存空间复制值,新老对象不共享内存,修改一个值,不影响另一个。

深拷贝和浅拷贝的示意图大致如下


抽象类只能单继承,接口可以实现多个。
抽象类有构造方法,接口没有构造方法。
抽象类可以有实例变量,接口中没有实例变量,有常量。
抽象类可以包含非抽象方法,接口在java7之前所有方法都是抽象的,java8之后可以包含非抽象方法。
抽象类中方法可以是任意修饰符,接口中java8之前都是public,java9支持private。
扩展:普通类是亲爹,手把手教你怎么学,抽象类(多个类具有相同的东西,拿出来放抽象类)是师傅,教你一部分秘籍,然后告诉你怎么学。接口(规范了某些行为)是干爹,只给你秘籍,怎么学全靠你。

13、Error和Exception有什么区别
Error: 程序无法处理,比较严重的问题,程序会立即崩溃,jvm停止运行。
Exception: 程序本身可以处理(向上抛出或者捕获)。编译时异常和运行时异常

14、final关键字有哪些用法
修饰类: 不能被继承。
修饰方法: 不能被重写。
修饰变量: 声明时给定初始值,只能读取不能修改。如果是对象引用不能改,但是对象的属性可以修改。
15、jdk1.8的新特性
1、lambda 表达式
Lambda是一个匿名函数,我们可以将Lambda表达式理解为一段可以传递的代码(将代码像数据一样传递)。使用它可以写出简洁、灵活的代码。作为一种更紧凑的代码风格,使java语言表达能力得到提升。

2、方法引用
当要传递给Lambda体的操作已经有实现方法,可以直接使用方法引用(实现抽象方法的列表,必须要和方法引用的方法参数列表一致)

方法引用:使用操作符“:”将方法名和(类或者对象)分割开来。

有下列三种情况

对象:实例方法
:实例方法
:静态方法
3、函数式接口
函数式接口:只包含一个抽象方法的接口,称为函数式接口,并且可以使用lambda表达式来创建该接口的对象,可以在任意函数式接口上使用@FunctionalInterface注解,来检测它是否是符合函数式接口。同时javac也会包含一条声明,说明这个接口是否符合函数式接口。

4、Optional类
optional类是一个容器,代表一个值存在或者不存在,原来使用null表示一个值存不存在,现在使用optional可以更好的表达这个概念,并且可以避免空指针异常。

Optional常用的方法

Optional.of(T t) : 创建一个Optional实例

Optional.empty() : 创建一个空的Optional实例

Optional.ofNullable(T t) :若t不为空创建一个Optional实例否则创建一个空实例

isPresent() : 判断是否包含值

orElse(T t) :如果调用对象包含值,返回该值,否则返回t

orElseGet(Supplier s) : 如果调用对象包含值,返回该值,否则返回s获取的值

map(Function f: 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty();

flatMap(Function mapper) : 与map类似,要求返回值必须是Optional。

其他等等

16、Java和C++的区别
我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀! 没办法!就算没学过C++,也要记下来

都是面向对象的语言,都支持封装、继承和多态
Java不提供指针来直接访问内存,程序内存更加安全
Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是 接口可以多继承。
Java有自动内存管理机制,不需要程序员手动释放无用内存
17、Oracle JDK 和 OpenJDK 的对比
Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一 次
OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是 OpenJDK的一个实现,并不是完全开源的
Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎 相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企 业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳 定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程 序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题
在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的 性能
Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过 更新到最新版本获得支持来获取最新版本
Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。
18、 用最有效率的方法计算 2 乘以 8
2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次 方)。

19、 loat f=3.4;是否正确
不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于 下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转 换float f =(float)3.4; 或者写成 float f =3.4F;。

20、Java语言采用何种编码方案?有何特点
Java语言采用Unicode编码标准,Unicode(标准码,它为每个字符制订了一 个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。

21、 final、finally、finalize区别
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
22、this关键字的用法
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。

this的用法在java中大体可以分为3种

普通的直接引用,this相当于是指向当前对象本身。
形参与成员名字重名,用this来区分
引用本类的构造函数
23、 super关键字的用法
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。

super也有三种用法

1.普通的直接引用与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。

2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分

3.引用父类构造函数

super(参数:调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。

this(参数:调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

24、break ,continue ,return 的区别及作用
break 跳出总上一层循环,不再执行循环(结束当前的循环体)
continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
25、在 Java 中,如何跳出当前的多重嵌套循环
在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如

1 public static void main(String[] args) {
2 ok:
3 for (int i = 0; i < 10; i++) {
4 for (int j = 0; j < 10; j++) {
5 System.out.println("i=" + i + ",j=" + j);
6 if (j == 5) {
7 break ok;
8 }
10 }
11 }
12 }
1
2
3
4
5
6
7
8
9
10
11
26、面向对象三大特性
1、 面向对象的特征有哪些方面
面向对象的特征主要有以下几个方面

抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行 为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是 什么。

封装 : 封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如 果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没 有提供给外界访问的方法,那么这个类也没有什么意义了。

继承 : 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新 的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用 继承我们能够非常方便地复用以前的代码。

?关于继承如下 3 点请记住

? 1.子类拥有父类非 private 的属性和方法。

? 2.子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

? 3.子类可以用自己的方式实现父类的方法。

多态 : 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出 的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到 底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的 方法,必须在由程序运行期间才能决定。

在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口 (实现接口并覆盖接口中同一方法)。

其中Java 面向对象编程三大特性:封装 继承 多态

封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便 于使用,提高复用性和安全性。

继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以 增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通 过使用继承可以提高代码复用性。继承是多态的前提。

关于继承如下 3 点请记住

子类拥有父类非 private 的属性和方法。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提 高了程序的拓展性。

在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口 (实现接口并覆盖接口中同一方法)。

方法重载(overload)实现的是编译时的多态性(也称为前绑定,而方法重 写(override)实现的是运行时的多态性(也称为后绑定)。

一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是 哪个类中实现的方法,必须在由程序运行期间才能决定。运行时的多态是面向对 象精髓的东西,要实现多态需要做两件事

方法重写(子类继承父类并重写父类中已有的或抽象的方法
对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据 子类对象的不同而表现出不同的行为)。
2、 什么是多态机制?Java语言是如何实现多态的
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出 的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒 底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的 方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这 样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而 导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时 所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的 重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不 同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来 实现的,也就是我们所说的多态性

3、多态的实现
Java实现多态有三个必要条件:继承、重写、向上转型。

继承:在多态中必须存在有继承关系的子类和父类。

重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的 方法。

向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具 备技能调用父类的方法和子类的方法。

只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现 代码处理不同的对象,从而达到执行不同的行为。

对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类 对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但 是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

4、 面向对象五大基本原则是什么(可选
单一职责原则SRP(Single Responsibility Principle)类的功能要单一,不能包罗万象,跟杂货铺似的。

开放封闭原则OCP(Open-Close Principle)一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。

里式替换原则LSP(the Liskov Substitution Principle LSP)子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~

依赖倒置原则DIP(the Dependency Inversion Principle DIP)高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。

接口分离原则ISP(the Interface Segregation Principle ISP)
设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有
打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的
多。

27、静态变量和实例变量区别
静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会 有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。

实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量 是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。

28、 什么是内部类
在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。

29、 内部类的分类有哪些
在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内 部类本身就是类的一个属性,与其他属性定义方式一致。

内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类。

30、IO流
1、 java 中 IO 流分为几种?
按照流的流向分,可以分为输入流和输出流; 按照操作单元划分,可以划分为字节流和字符流; 按照流的角色划分为节点流和处理流。 Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼 此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类 中派生出来的。

InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符 输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输 出流。
2、 BIO,NIO,AIO 有什么区别?
简答

BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。

NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过
Channel(通道)通讯,实现了多路复用。

AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO
,异步 IO 的操作基于事件和回调机制。

详细回答

BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的 N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发

AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

31、 Files的常用方法都有哪些
Files. exists():检测文件路径是否存在。
Files. createFile():创建文件。
Files. createDirectory():创建文件夹。
Files. delete():删除一个文件或目录。
Files. copy():复制文件。
Files. move():移动文件。
Files. size():查看文件个数。
Files. read():读取文件。
Files. write():写入文件。
32、 在使用 HashMap 的时候,用 String 做 key 有什么好处
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。

二、集合
1、 什么是集合
集合框架: 用于存储数据的容器。

集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。 任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算 法。

接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现, 从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。

实现:集合接口的具体实现,是重用性很高的数据结构。

算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方 法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个 接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。 它减少了程序设计的辛劳。

集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部 分上,而不是为了让程序能正常运转而将注意力于底层设计上。

通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以 便联合这些API而去写大量的代码。 它提高了程序速度和质量。

集合的特点
集合的特点主要有如下两点

对象封装数据,对象多了也需要存储。集合用于存储对象。
对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因 为集合是可变长度的。
集合和数组的区别
数组是固定长度的;集合可变长度的。
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存 储引用数据类型。
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同 数据类型。
数据结构:就是容器中存储数据的方式。

对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个 容器的内部数据结构不同。

集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参 阅顶层内容。建立底层对象。

使用集合框架的好处
容量自增长
提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质 量; 3
允许不同 API 之间的互操作,API之间可以来回传递集合
可以方便地扩展或改写集合,提高代码复用性和可操作性。
通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。
2、常用的集合类有哪些
Map接口和Collection接口是所有集合框架的父接口

Collection接口的子接口包括:Set接口和List接口
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、 ConcurrentHashMap以及Properties等
Set接口的实现类主要有:HashSet、TreeSet、linkedHashSet等
List接口的实现类主要有:ArrayList、linkedList、Stack以及Vector等
3、List,Set,Map三者的区别?List、Set、Map 是否继 承自 Collection 接口?List、Map、Set 三个接口存取 元素时,各有什么特点
Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、 List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是 collection的子接口。

Collection集合主要有List和Set两大接口

List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重 复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、linkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素, 只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、 linkedHashSet 以及 TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不 要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元 素时,只要给出键对象,就会返回对应的值对象。

Map 的常用实现类:HashMap、TreeMap、HashTable、linkedHashMap、 ConcurrentHashMap

4、集合框架底层数据结构
List

Arraylist: Object数组
Vector: Object数组
linkedList: 双向循环链表
Set

HashSet(无序,唯一:基于 HashMap 实现的,底层采用 HashMap 来保存元素
linkedHashSet: linkedHashSet 继承与 HashSet,并且其内部是通过 linkedHashMap 来实现的。有点类似于我们之前说的linkedHashMap 其内部是基 于 Hashmap 实现一样,不过还是有一点点区别的。
TreeSet(有序,唯一: 红黑树(自平衡的排序二叉树。) Map
HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主 体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转 化为红黑树,以减少搜索时间
linkedHashMap:linkedHashMap 继承自 HashMap,所以它的底层仍然是 基于拉链式散列结构即由数组和链表或红黑树组成。另外,linkedHashMap 在上面 结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。 同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为 了解决哈希冲突而存在的
TreeMap: 红黑树(自平衡的排序二叉树
5、哪些集合类是线程安全的
vector:就比arraylist多了个同步化机制(线程安全,因为效率较低,现在已 经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优 先考虑的。
statck:堆栈类,先进后出。
hashtable:就比hashmap多了个线程安全。
enumeration:枚举,相当于迭代器。
6、Java集合的快速失败机制 “fail-fast”
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作 时,有可能会产生 fail-fast 机制。

例如:假设存在两个线程(线程1、线程2,线程1通过Iterator在遍历集合A中 的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简 单的修改集合元素的内容,那么这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount 的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测 modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出 异常,终止遍历。

解决办法

在遍历过程中,所有涉及到改变modCount值得地方全部加上 synchronized。
使用CopyOnWriteArrayList来替换ArrayList
7、 怎么确保一个集合不能被修改
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。 示例代码如下

?List<String> list = new ArrayList<>();?
?list. add("x");?
?Collection<String> clist = Collections. unmodifiableCollection(list);?
?clist. add("y"); // 运行时此行报错?
?System. out. println(list. size());?
1
2
3
4
5
8、 说一下 ArrayList 的优缺点
ArrayList的优点如下

ArrayList 底层以数组实现,是一种随机访问模式。
ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。
ArrayList 的缺点如下

删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList 比较适合顺序添加、随机访问的场景。

9、 ArrayList 和 linkedList 的区别是什么
数据结构实现:ArrayList 是动态数组的数据结构实现,而 linkedList 是双向链表的数据结构实现。
随机访问效率:ArrayList 比 linkedList 在随机访问的时候效率要高,因为 linkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增加和删除操作,linkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
内存空间占用:linkedList 比 ArrayList 更占内存,因为 linkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全:ArrayList 和 linkedList 都是不同步的,也就是不保证线程安全
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 linkedList。

补充:数据结构基础之双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

10、 ArrayList 和 Vector 的区别是什么
这两个类都实现了 List 接口(List 接口继承了 Collection 接口,他们都是有序集合
线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而ArrayList 是非线程安全的。
性能:ArrayList 在性能方面要优于 Vector
扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
11、 插入数据时,ArrayList、linkedList、Vector谁速度较快?阐述 ArrayList、Vector、linkedList 的存储性能和特性
ArrayList、linkedList、Vector底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
Vector 中的方法由于加了 synchronized 修饰,因此 Vector是线程安全容器,但性能上较ArrayList差。
linkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以linkedList插入速度较快。
12、 多线程场景下如何使用 ArrayList
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样

1?? ?List<String> synchronizedList = Collections.synchronizedList(list);
2?? ?synchronizedList.add("aaa");
3?? ?synchronizedList.add("bbb");
4
5?? ?for (int i = 0; i < synchronizedList.size(); i++) {
6?? ?System.out.println(synchronizedList.get(i));
7?? ?}
1
2
3
4
5
6
7
13、 List 和 Set 的区别
List , Set 都是继承自Collection 接口
List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有ArrayList、linkedList 和 Vector。
Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是HashSet、linkedHashSet 以及 TreeSet。
另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

Set和List对比

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

14、说一下 HashSet 的实现原理
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成。

15、 HashSet如何检查重复?HashSet是如何保证数据不可重复的
向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。HashSet 中的add ()方法会使用HashMap 的put()方法。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为 HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较 hashcode 再比较equals )。
16、 BlockingQueue是什么
Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。

BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。

Java提供了集中 BlockingQueue的实现,比如ArrayBlockingQueue、linkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。

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

相同点:都是返回第一个元素,并在队列中删除返回的对象。
不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
17、 Map接口
1、说一下 HashMap 的实现原理
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap的数据结构: 在Java编程语言中, 基本的结构就是两种,一个是数组,另外一个是模拟指针(引用,所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap 基于 Hash 算法实现的

当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标

存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突,则将当前的key-value 放入链表中

获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。

理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

2、HashMap在JDK1.7和JDK1.8中有哪些不同? HashMap的底层实现
在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

JDK1.8之前
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

JDK1.7 VS JDK1.8 比较
JDK1.8主要解决或优化了一下问题

resize 扩容优化

引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考

解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

3、 HashMap的put方法的具体流程
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向

,这里的相同指的是hashCode以及equals

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可

⑥.插入成功后,判断实际存在的键值对数量size是否超多了 大容量threshold,如果超过,进行扩容。

4、 HashMap是怎么解决哈希冲突的
:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;什么是哈希

Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值;这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同**。

什么是哈希冲突
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

HashMap的数据结构
在Java中,保存数据有两种比较简单的数据结构:数组和链表数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突,这样我们就可以将拥有相同哈希值的对象(img)组织成一个链表放在hash值所对应的 bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化 hash()函数。

上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让 hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下

1?? ?static final int hash(Object key) {
2?? ?int h;
3?? ?return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或
4?? ?}
1
2
3
4
这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动,在1.8中,只进行了1次位运算和1次异或运算(2次扰动

5、 HashMap 的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

这个算法应该如何设计呢?我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

那为什么是两次扰动呢

:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性, 终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的。

6、 HashMap 与 HashTable 有什么区别
线程安全: HashMap 是非线程安全的,HashTable 是线程安全的
HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧

效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它

对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。

初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2 的幂次方。

底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

7、如何决定使用 HashMap 还是TreeMap
对于在Map中插入、删除和定位元素这类操作,HashMap是 好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

8、 HashMap 和 ConcurrentHashMap 的区别
ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized 锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启了一种全新的方式实现,利用CAS算法。

HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

三、并发编程
1、并发编程的优缺点为什么要使用并发编程(并发编程的优点
充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU 的计算能力发挥到极致,性能得到提升
方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
2、并发编程有什么缺点
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题

比如:内存泄漏、上下文切换、线程安全、死锁等问题。

3、并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全
并发编程三要素(线程的安全性问题体现在

原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么 全部执行成功要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。 (synchronized,volatile
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行 重排序
出现线程安全问题的原因

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

JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题
4、并行和并发有什么区别
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑 上来看那些任务是同时执行。
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上 的“同时进行”。
串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行 所以不存在线程不安全情况,也就不存在临界区的问题。
做一个形象的比喻

并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
串行 = 一个队列和一台咖啡机。

5、什么是多线程,多线程的优劣
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个 不同的线程来执行不同的任务。

多线程的好处: 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可 以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单 个程序创建多个并行执行的线程来完成各自的任务。

多线程的劣势

线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
多线程需要协调和管理,所以需要 CPU 时间跟踪线程
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
6、线程和进程区别 什么是线程和进程?
进程

一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进 程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进 程。

线程

进程中的一个执行任务(控制单元,负责当前进程中程序的执行。一个进程至 少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

进程与线程的区别

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process), 它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有 若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执 行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文,程序之间的切 换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空 间,每个线程都有自己独立的运行栈和程序计数器(PC,线程之间切换的开 销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线 (线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻 量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空 间和资源是相互独立的

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个 线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控 制,两者均可并发执行

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

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

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

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

8、守护线程和用户线程有什么区别呢
守护线程和用户线程

用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网 络的子线程等都是用户线程
守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护 线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程 会随 JVM 一起结束工作
main 函数所在的线程就是一个用户线程,main 函数启动的同时在 JVM 内部 同时还启动了好多守护线程,比如垃圾回收线程。 比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线 程运行。而守护线程不会影响 JVM 的退出。

注意事项

setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
在守护线程中产生的新线程也是守护线程
不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算 逻辑
守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清 理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守 护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语 句块可能无法被执行。
9、如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高
windows上面用任务管理器看,linux下可以用 top 这个工具看。

找出cpu耗用厉害的进程pid, 终端执行top命令,然后按下shift+p 查 找出cpu利用厉害的pid号
根据上面第一步拿到的pid号,top -H -p pid 。然后按下shift+p,查 找出cpu利用率厉害的线程号,比如top -H -p 1328
将获取到的线程号转换成16进制,去百度转换一下就行
使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat,比 如jstack 31365 > /tmp/t.dat
编辑/tmp/t.dat文件,查找线程号对应的信息
10、 什么是线程死锁
百度百科:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资 源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推 进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进 程(线程)称为死锁进程(线程)。

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线 程被无限期地阻塞,因此程序不可能正常终止。

线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方 的资源,所以这两个线程就会互相等待而进入死锁状态。

形成死锁的四个必要条件是什么
互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只 能被一个线程(进程)占用,直到被该线程(进程)释放
请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对 已获得的资源保持不放。
不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程 强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环 路(类似于死循环,造成永久阻塞
如何避免线程死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。

破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源 需要互斥访问)。

破坏请求与保持条件

一次性申请所有的资源。

破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占 有的资源。

破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环 等待条件。

11、 创建线程有哪几种方式
创建线程有四种方式

继承 Thread 类
实现 Runnable 接口
实现 Callable 接口
使用 Executors 工具类创建线程池继承 Thread 类
12、 线程的 run()和 start()有什么区别
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的, run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。 start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。 start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待 run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

13、 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

14、 什么是 Callable 和 Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。

Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future 用于获取结果。

15、什么是 FutureTask
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

16、说说线程的生命周期及五种基本状态
网上对线程状态的描述很多,有5种,6种,7种,都可以接受。

5中状态一般是针对传统的线程状态来说(操作系统层面


Java中给线程准备的6种状态


NEW:Thread对象被创建出来了,但是还没有执行start方法。
RUNNABLE:Thread对象调用了start方法,就为RUNNABLE状态(CPU调度/没有调度
BLOCKED、WAITING、TIME_WAITING:都可以理解为是阻塞、等待状态,因为处在这三种状态下,CPU不会调度当前线程
BLOCKED:synchronized没有拿到同步锁,被阻塞的情况
WAITING:调用wait方法就会处于WAITING状态,需要被手动唤醒
TIME_WAITING:调用sleep方法或者join方法,会被自动唤醒,无需手动唤醒
TERMINATED:run方法执行完毕,线程生命周期到头了
在Java代码中验证一下效果
NEW

public static void main(String[] args) throws InterruptedException {
? ? Thread t1 = new Thread(() -> {
??
? ? });
? ? System.out.println(t1.getState());
}
1
2
3
4
5
6
RUNNABLE

public static void main(String[] args) throws InterruptedException {
? ? Thread t1 = new Thread(() -> {
? ? ? ? while(true){

? ? ? ? }
? ? });
? ? t1.start();
? ? Thread.sleep(500);
? ? System.out.println(t1.getState());
}
1
2
3
4
5
6
7
8
9
10
BLOCKED

public static void main(String[] args) throws InterruptedException {
? ? Object obj = new Object();
? ? Thread t1 = new Thread(() -> {
? ? ? ? // t1线程拿不到锁资源,导致变为BLOCKED状态
? ? ? ? synchronized (obj){

? ? ? ? }
? ? });
? ? // main线程拿到obj的锁资源
? ? synchronized (obj) {
? ? ? ? t1.start();
? ? ? ? Thread.sleep(500);
? ? ? ? System.out.println(t1.getState());
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WAITING

public static void main(String[] args) throws InterruptedException {
? ? Object obj = new Object();
? ? Thread t1 = new Thread(() -> {
? ? ? ? synchronized (obj){
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? obj.wait();
? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }
? ? });
? ? t1.start();
? ? Thread.sleep(500);
? ? System.out.println(t1.getState());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TIMED_WAITING

public static void main(String[] args) throws InterruptedException {
? ? Thread t1 = new Thread(() -> {
? ? ? ? try {
? ? ? ? ? ? Thread.sleep(1000);
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? });
? ? t1.start();
? ? Thread.sleep(500);
? ? System.out.println(t1.getState());
}
1
2
3
4
5
6
7
8
9
10
11
12
TERMINATED

public static void main(String[] args) throws InterruptedException {
? ? Thread t1 = new Thread(() -> {
? ? ? ? try {
? ? ? ? ? ? Thread.sleep(500);
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? });
? ? t1.start();
? ? Thread.sleep(1000);
? ? System.out.println(t1.getState());
}
1
2
3
4
5
6
7
8
9
10
11
12
17、 sleep() 和 wait() 有什么区别
两者都可以暂停线程的执行

类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
18、 如何停止一个正在运行的线程
在java中有以下3种方法可以终止正在运行的线程

使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及 resume一样都是过期作废的方法。
使用interrupt方法中断线程。
19、 Java 中 interrupted 和 isInterrupted 方法的区别
interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。

isInterrupted:查看当前中断信号是true还是false

20、 notify() 和 notifyAll() 有什么区别
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。

notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。如何在两个线程间共享数据?在两个线程间共享变量即可实现共享。

21、 Java 线程数过多会造成什么异常
线程的生命周期开销非常高消耗过多的

CPU资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。

降低稳定性JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。

22、synchronized 的作用
synchronized 可以修饰类、方法、变量。

在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。

另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。

庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

23、 synchronized、volatile、CAS 比较
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。

(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。

(3)CAS 是基于冲突检测的乐观锁(非阻塞

24、synchronized 和 Lock 有什么区别
首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
25、 synchronized 和 ReentrantLock 区别是什么
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

相同点
两者都是可重入锁两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。

同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0 时才能释放锁。主要区别如下

ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: 普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
26、 volatile 关键字的作用
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。
volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
27、什么是AQS
AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。

首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。

其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象。

AQS内部结构和属性


28、ThreadLocal 是什么?有哪些使用场景
hreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。

任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

29、 ThreadLocal造成内存泄漏的原因
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key 为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完ThreadLocal方法后手动调用remove()方法。

ThreadLocal内存泄漏解决方案
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
30、 线程池有什么优点
降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
提高响应速度。可有效的控制 大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
31、 线程池都有哪些状态
RUNNING:这是 正常的状态,接受新的任务,处理等待队列中的任务。 SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
? TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为
TIDYING 状态时,会执行钩子方法 terminated()。
? TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
32、 Executors和ThreaPoolExecutor创建线程池的区别
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 各个方法的弊端

newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数 大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定。

33、ThreadPoolExecutor参数说明
1、corePoolSize:核心线程数
? ? * 核心线程会一直存活,及时没有任务需要执行
? ? * 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
? ? * 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

2、queueCapacity:任务队列容量(阻塞队列
? ? * 当核心线程数达到最大时,新任务会放在队列中排队等待执行

3、maxPoolSize:最大线程数
? ? * 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
? ? * 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

4、 keepAliveTime:线程空闲时间
? ? * 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
? ? * 如果allowCoreThreadTimeout=true,则会直到线程数量=0

5、allowCoreThreadTimeout:允许核心线程超时
6、rejectedExecutionHandler:任务拒绝处理器
? ? * 两种情况会拒绝处理任务
? ? ? ? - 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
? ? ? ? - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
? ? * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
? ? * ThreadPoolExecutor类有几个内部实现类来处理这类情况
? ? ? ? - AbortPolicy 丢弃任务,抛运行时异常
? ? ? ? - CallerRunsPolicy 执行任务
? ? ? ? - DiscardPolicy 忽视,什么都不会发生
? ? ? ? - DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
? ? * 实现RejectedExecutionHandler接口,可自定义处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
34、 ThreadPoolExecutor执行顺序
线程池按以下行为执行任务
1. 当线程数小于核心线程数时,创建线程。
2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
3. 当线程数大于等于核心线程数,且任务队列已满
? ? -1 若线程数小于最大线程数,创建线程
? ? -2 若线程数等于最大线程数,抛出异常,拒绝任务
1
2
3
4
5
6
35、 并发工具
1、CycliBarriar 和 CountdownLatch 有什么区别
CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行
CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入 barrierAction,指定当所有线程都到达时执行的业务功能
CountDownLatch是不能复用的,而CyclicLatch是可以复用的。
2、 Semaphore
Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。

Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码 多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

Semaphore(信号量)-允许多个线程同时访问: synchronized 和

ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量) 可以指定多个线程同时访问某个资源。

3、 Exchanger
Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过 exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。

36、常用的并发工具类有哪些
Semaphore(信号量)-允许多个线程同时访问: synchronized 和ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到 后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉
CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
四、Spinrg
1、什么是spring?
Spring是一个轻量级Java开发框架,最早由Rod Johnson创建,目的是为了解 决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的 JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发Java应用程序提 供全面的基础架构支持。Spring负责基础架构,因此Java开发者可以专注于应 用程序的开发。 Spring最根本的使命是解决企业级应用开发的复杂性,即简化Java开发。

Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能 的底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming, AOP)。

为了降低Java开发的复杂性,Spring采取了以下4种关键策略

基于POJO的轻量级和最小侵入性编程
通过依赖注入和面向接口实现松耦合
基于切面和惯例进行声明式编程
通过切面和模板减少样板式代码。
2、Spring框架的设计目标,设计理念,和核心是什么
Spring设计目标:Spring为开发者提供一个一站式轻量级应用开发平台

Spring设计理念:在JavaEE开发中,支持POJO和JavaBean开发方式,使应用 面向接口开发,充分支持OO(面向对象)设计方法;Spring通过IoC容器实现 对象耦合关系的管理,并实现依赖反转,将对象之间的依赖关系交给IoC容器, 实现解耦

Spring框架的核心:IoC容器和AOP模块。通过IoC容器管理POJO对象以及他 们之间的耦合关系;通过AOP以动态非侵入的方式增强服务。 IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的 功能分离出来形成可重用的功能组件。

3、Spring的优缺点是什么
优点

方便解耦,简化开发
Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护,交给 Spring管理。
AOP编程的支持
Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等 功能。
声明式事务的支持
只需要通过配置就可以完成对事务的管理,而无需手动编程。
方便程序的测试
Spring对Junit4支持,可以通过注解方便的测试Spring程序。
方便集成各种优秀框架
Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持 (如:Struts、Hibernate、MyBatis等)。
降低JavaEE API的使用难度
Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用 等,都提供了封装,使这些API应用难度大大降低。
缺点

Spring明明一个很轻量级的框架,却给人感觉大而全
Spring依赖反射,反射影响性能
使用门槛升高,入门Spring需要较长时间
4、Spring有哪些应用场景
应用场景:JavaEE企业应用开发,包括SSH、SSM等

Spring价值

Spring是非侵入式的框架,目标是使应用程序代码对框架依赖最小化
Spring提供一个一致的编程模型,使应用直接使用POJO开发,与运行环境隔离 开来
Spring推动应用设计风格向面向对象和面向接口开发转变,提高了代码的重用性 和可测试性
5、Spring由哪些模块组成
Spring 总共大约有 20 个模块, 由 1300 多个不同的文件构成。 而这些组件被 分别整合在核心容器(Core Container) 、 AOP(Aspect Oriented Programming) 和设备支持(Instrmentation) 、数据访问与集成(Data Access/Integeration) 、 Web、 消息(Messaging) 、 Test等 6 个模块中。

spring core:提供了框架的基本组成部分,包括控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)功能。
spring beans:提供了BeanFactory,是工厂模式的一个经典实现,Spring将管 理对象称为Bean。
spring context:构建于 core 封装包基础上的 context 封装包,提供了一种框 架式的对象访问方法。
spring jdbc:提供了一个JDBC的抽象层,消除了烦琐的JDBC编码和数据库厂 商特有的错误代码解析, 用于简化JDBC。
spring aop:提供了面向切面的编程实现,让你可以自定义拦截器、切点等。
spring Web:提供了针对 Web 开发的集成特性,例如文件上传,利用 servlet listeners 进行 ioc 容器初始化和针对 Web 的 ApplicationContext。
spring test:主要为测试提供支持的,支持使用JUnit或TestNG对Spring组件进 行单元测试和集成测试。
6、Spring 框架中都用到了哪些设计模式
工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实 例
单例模式:Bean默认为单例模式。
代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生 成技术
模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中 listener的实现–ApplicationListener。
7、什么是Spring IOC
控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。

Spring IOC 负责创建对象,管理对象(通过依赖注入(DI,装配对象,配置对象,并且管理这些对象的整个生命周期。

8、什么是Spring AOP
面向切面编程(AOP)就是纵向的编程。比如业务A和业务B现在需要一个相同的操作,传统方法我们可能需要在A、B中都加入相关操作代码,而应用AOP就可以只写一遍代码,A、B共用这段代码。并且,当A、B需要增加新的操作时,可以在不改动原代码的情况下,灵活添加新的业务逻辑实现。

在实际开发中,比如商品查询、促销查询等业务,都需要记录日志、异常处理等操作,AOP把所有共用代码都剥离出来,单独放置到某个类中进行集中管理,在具体运行时,由容器进行动态织入这些公共代码。

AOP主要一般应用于签名验签、参数校验、日志记录、事务控制、权限控制、性能统计、异常处理等。

9、Spring常用注解
1、@Controller:用于标注控制器层组件

2、@Service:用于标注业务层组件

3、@Component : 用于标注这是一个受 Spring 管理的组件,组件引用名称是类名,第一个字母小写。可以使用@Component(“beanID”) 指定组件的名称

4、@Repository:用于标注数据访问组件,即DAO组件

5、@Bean:方法级别的注解,主要用在@Configuration和@Component注解的类里@Bean注解的方法会产生一个Bean对象,该对象由Spring管理并放到IoC容器中。引用名称是方法名,也可以用@Bean(name = “beanID”)指定组件名

6、@Scope(“prototype”):将组件的范围设置为原型的(即多例)。保证每一个请求有一个单独的action来处理,避免action的线程问题。

由于Spring默认是单例的,只会创建一个action对象,每次访问都是同一个对象,容易产生并发问题,数据不安全。

7、@Autowired:默认按类型进行自动装配。在容器查找匹配的Bean,当有且仅有一个匹配的Bean时,Spring将其注入@Autowired标注的变量中。

8、@Resource:默认按名称进行自动装配,当找不到与名称匹配的Bean时会按类型装配。

10、 @Autowired和@Resource之间的区别
@Autowired可用于:构造函数、成员变量、Setter方法

@Autowired和@Resource之间的区别

@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。

@Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。

11、@Qualifier 注解有什么作用
当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时,您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。

12、说一下Spring的事务传播行为
spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。

① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。

③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。

13、 说一下 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 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。

五、Spring MVC
1、什么是Spring MVC?简单介绍下你对Spring MVC的理解
Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级 Web框架,通过把模型-视图-控制器分离,将web层进行职责解耦,把复杂的 web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间 的配合。

2、Spring MVC的优点
(1)可以支持各种视图技术,而不仅仅局限于JSP

(2)与Spring框架集成(如IoC容器、AOP等

(3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射 (handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器 (ViewResolver)。

(4) 支持各种请求资源的映射策略。

3、Spring MVC的主要组件
(1)前端控制器 DispatcherServlet(不需要程序员开发
作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了 其它组件之间的耦合度。

(2)处理器映射器HandlerMapping(不需要程序员开发
作用:根据请求的URL来查找Handler

(3)处理器适配器HandlerAdapter
注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。

(4)处理器Handler(需要程序员开发

(5)视图解析器 ViewResolver(不需要程序员开发
作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view

(6)视图View(需要程序员开发jsp
View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等 等

4、什么是DispatcherServlet
Spring的MVC框架是围绕DispatcherServlet来设计的,它用来处理所有的 HTTP请求和响应。

5、什么是Spring MVC框架的控制器
控制器提供一个访问应用程序的行为,此行为通常通过服务接口实现。控制器解 析用户输入并将其转换为一个由视图呈现给用户的模型。Spring用一个非常抽 象的方式实现了一个控制层,允许用户创建多种用途的控制器。

6、Spring MVC的控制器是不是单例模式,如果是,有什么问题,怎么解决
:是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性 能的,解决方案是在控制器里面不能写字段。

7、请描述Spring MVC的工作流程?描述一下 DispatcherServlet 的工作流程
(1)用户发送请求至前端控制器DispatcherServlet

(2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器, 请求获取Handle

(3)处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦 截器(如果有则生成)一并返回给DispatcherServlet

(4)DispatcherServlet 调用 HandlerAdapter处理器适配器

(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制 器)

(6)Handler执行完成返回ModelAndView

(7)HandlerAdapter将Handler执行结果ModelAndView返回给 DispatcherServlet

(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行 解析

(9)ViewResolver解析后返回具体View

(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中

(11)DispatcherServlet响应用户。

8、MVC是什么?MVC设计模式的好处有哪些
mvc是一种设计模式(设计模式就是日常开发中编写代码的一种好的方法和经验 的总结)。模型(model)-视图(view)-控制器(controller,三层架构的 设计模式。用于实现前端页面的展现与后端业务数据处理的分离。

mvc设计模式的好处

1.分层设计,实现了业务系统各个组件之间的解耦,有利于业务系统的可扩展 性,可维护性。

2.有利于系统的并行开发,提升开发效率。

9、 Spring MVC常用的注解有哪些
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用 于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。

@RequestBody:注解实现接收http请求的json数据,将json转换为java对 象。

@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给 客户。

10、 @Controller注解的作用
在Spring MVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model 返回给对应的View 进行展示。

在Spring MVC 中提供了一个非常简便的定义Controller 的方法,你无需继承特定的类或实现特定的接口,只需使用@Controller 标记一个类是Controller ,然后使用@RequestMapping 和 @RequestParam 等一些注解用以定义URL 请求和Controller 方法之间的映射,这样的Controller 就能被外界访问到。

此外Controller 不会直接依赖于HttpServletRequest 和HttpServletResponse 等HttpServlet 对象,它们可以通过Controller 的方法参数灵活的获取到。

@Controller 用于标记在一个类上,使用它标记的类就是一个Spring MVC

Controller 对象。分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping 注解。@Controller 只是定义了一个控制器类,而使用@RequestMapping 注解的方法才是真正处理请求的处理器。单单使用@Controller 标记在一个类上还不能真正意义上的说它就是Spring MVC 的一个控制器类,因为这个时候Spring 还不认识它。那么要如何做Spring 才能认识它呢

这个时候就需要我们把这个控制器类交给Spring 来管理。有两种方式

在Spring MVC 的配置文件中定义MyController 的bean 对象。
在Spring MVC 的配置文件中告诉Spring 该到哪里去找标记为@Controller 的Controller 控制器。
11、@RequestMapping注解的作用
RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。

用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

RequestMapping注解有六个属性,下面我们把她分成三类进行说明(下面有相应示例)。

value: 指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明
method: 指定请求的method类型, GET、POST、PUT、DELETE等; consumes,produces
consumes: 指定处理请求的提交内容类型(Content-Type,例如 application/json, text/html;
produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回
params: 指定request中必须包含某些参数值是,才让该方法处理。
headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求。
12、@ResponseBody注解的作用
作用: 该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

使用时机:返回的数据不是html标签的页面,而是其他某种格式的数据时(如json、xml等)使用

13、@PathVariable和@RequestParam的区别
请求路径上有个id的变量值,可以通过@PathVariable来获取

@RequestMapping(value = “/page/{id}”, method = RequestMethod.GET)

@RequestParam用来获得静态的URL请求入参 spring注解时action里用到。

六、MyBatis
1. MyBatis是什么
MyBatis 是一款优秀的持久层框架,一个半 ORM(对象关系映射)框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及 获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

2. ORM是什么
ORM(Object Relational Mapping,对象关系映射,是一种为了解决关系型数据库数 据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和 数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。

3. 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里
Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。

而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半 自动ORM映射工具。

4. 传统JDBC开发存在的问题
频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能。可以使用连接池 解决这个问题。但是使用jdbc需要自己实现连接池。

sql语句定义、参数设置、结果集处理存在硬编码。实际项目中sql语句变化的可能性较大, 一旦发生变化,需要修改java代码,系统需要重新编译,重新发布。不好维护。

使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一 定,可能多也可能少,修改sql还要修改代码,系统不易维护。

结果集处理存在重复代码,处理麻烦。如果可以映射成Java对象会比较方便。

5. JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的
数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接 池可解决此问题。
解决:在mybatis-config.xml中配置数据链接池,使用连接池管理数据库连接。
Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变 java代码。
解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需 要和参数一一对应。
解决: Mybatis自动将java对象映射至sql语句。
对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记 录封装成pojo对象解析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象。

6. Mybatis优缺点
优点

与传统的数据库访问技术相比,ORM有以下优点

基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态 SQL语句,并可重用与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数 据库MyBatis都支持

不同点

能够与Spring很好的集成

缺点

SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底 有一定要求

SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库

7. MyBatis编程步骤是什么样的
创建SqlSessionFactory
通过SqlSessionFactory创建SqlSession
通过sqlsession执行数据库操作
调用session.commit()提交事务
调用session.close()关闭会话
8.请说说MyBatis的工作原理
在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作原理如下图

上面中流程就是MyBatis内部核心流程,每一步流程的详细说明如下文所述

(1)读取MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。

(2)加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。

(3)构造会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。

(4)创建会话对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

(5)Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

(6)MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

(7)输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

(8)输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

9.MyBatis的功能架构是怎样的
我们把Mybatis的功能架构分为三层

API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层 一接收到调用请求就会调用数据处理层来完成具体的数据处理。

数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的 目的是根据调用的请求完成一次数据库操作。

基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这 些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的 支撑。

10.为什么需要预编译
定义

SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编 译,这样 DBMS 执行 SQL 时,就不需要重新编译。

为什么需要预编译

JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以 优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedState 对象。Mybatis 默认情况下,将对所有的 SQL 进行预编译。

11.#{}和${}的区别
#{}是占位符:预编译处理

${}是拼接符:字符串替换,没有预编译处理。

Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。

Mybatis在处理时,是原值传入,就是把 {}时,是原值传入,就是把时,是原值传入,就是 把{}替换成变量的值,相当于JDBC中的Statement编译变量替换后,#{} 对应的变量自动加上单引号 ‘’;变量替换后,${} 对应的变量不会加上 单引号 ‘’

#{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入。

#{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外。

12.Mybatis的一级、二级缓存
一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session, 当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓 存。
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储, 不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默 认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接 口(可用来保存对象的状态),可在它的映射文件中配置
对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的 进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
七、JVM
1. 内存模型以及分区,需要详细到每个区放什么。
· 方法区:主要是存储类信息,常量池(static 常量和 static 变量,编译后的代码(字

节码)等数据

· 堆:初始化的对象,成员变量 (那种非 static 的变量,所有的对象实例和数组都要

在堆上分配

· 栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操

作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所以还是一个指向地址的指针

· 本地方法栈:主要为 Native 方法服务

· 程序计数器:记录当前线程执行的行号

2. 堆里面的分区:Eden,survival (from+ to,老年代,各自的特点
堆里面分为新生代和老生代(java8 取消了永久代,采用了 metaspace,新生代包含Eden+Survivor 区,survivor 区里面分为 from 和 to 区,内存回收时,如果用的是复制算法,从 from 复制到 to,当经过一次或者多次 GC 之后,存活下来的对象会被移动到老年区,当 JVM 内存不够用的时候,会触发 Full GC,清理 JVM 老年区当新生区满了之后会触发 YGC,先把存活的对象放到其中一个 Survice区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把 Eden 进行完全的清理,然后整理内存。那么下次 GC 的时候,就会使用下一个 Survive,这样循环使用。如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。因为 JVM 认为,一般大对象的存活时间一般比较久远。

3.GC 的两种判定方法
引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为 0 就会回收,但是 JVM 没有用这种方式,因为无法判定相互循环引用(A 引用 B,B 引用 A)的情况。

引用链法: 通过一种 GC ROOT 的对象(方法区中静态变量引用的对象等-static 变量)来判断,如果有一条链能够到达 GC ROOT 就说明,不能到达 GC ROOT 就说明可以回收。

4. SafePoint 是什么
比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始执行 GC

循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入safepoint)
方法返回前
调用方法的 call 之后
抛出异常的位置
5.Java New对象分配内存流程
点击这里参见详情

6.常见的垃圾回收器?
点击这里参见详情

7.类加载的几个过程
java 类加载需要经历一下 7 个过程

加载

加载时类加载的第一个过程,在这个阶段,将完成一下三件事情

通过一个类的全限定名获取该类的二进制流。

将该二进制流中的静态存储结构转化为方法去运行时数据结构。

在内存中生成该类的 Class 对象,作为该类的数据访问入口。

验证

验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成

以下四钟验证:

文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。

public static int value=123;*/usr **: 用于存放系统应用程序
/opt: 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把
tomcat等都安装到这里
/proc: 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息
/root: 超级用户(系统管理员)的主目录(特权阶级o
/sbin: 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如ifconfig等
/dev: 用于存放设备文件
/mnt: 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统
/boot: 存放用于系统引导时使用的各种文件
**/lib **: 存放着和系统运行相关的库文件
/tmp: 用于存放各种临时文件,是公用的临时文件存储点
/var: 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等
/lost+found: 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在这里。
9.什么是 inode
一般来说,面试不会问 inode 。但是 inode 是一个重要概念,是理解 Unix/Linux 文件系统和硬盘储存的基础。

理解inode,要从文件储存说起。

文件储存在硬盘上,硬盘的 小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。

操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的 小单位。"块"的大小, 常见的是4KB,即连续八个 sector组成一个 block。

文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。

每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

10.一台 Linux 系统初始化环境后需要做一些什么安全工作
添加普通用户登陆,禁止 root 用户登陆,更改 SSH 端口号。修改 SSH 端口不一定绝对哈。

当然,如果要暴露在外网,建议改下:

服务器使用密钥登陆,禁止密码登陆。
开启防火墙,关闭 SElinux ,根据业务需求设置相应的防火墙规则。
装 fail2ban 这种防止 SSH 暴力破击的软件。
设置只允许公司办公网出口 IP 能登陆服务器(看公司实际需要)
也可以安装 VPN 等软件,只允许连接 VPN 到服务器上。
只允许有需要的服务器可以访问外网,其它全部禁止。
做好软件层面的防护。
设置 nginx_waf 模块防止 SQL 注入。
把 Web 服务使用 www 用户启动,更改网站目录的所有者和所属组为 www 。
11.什么叫 CC 攻击?什么叫 DDOS 攻击
攻击:即是通过大量合法的请求占用大量网络资源,以达到瘫痪网络的目的。

CC 攻击:主要是用来攻击页面的,模拟多个用户不停的对你的页面进行访问,从而使你的系统资源消耗殆尽。

DDOS 攻击:DDoS攻击一般指分布式拒绝攻击,是一种攻击者操纵大量计算机,或位于不同位置的多个攻击者,在短时间内通过将攻击伪装成大量的合法请求,向服务器资源发动的攻击,导致请求数量超过了服务器的处理能力,造成服务器运行缓慢或者宕机。通常由僵尸网络用于执行此恶意任务,由于攻击的发出点是分布在不同地方的,这类攻击称为分布式拒绝服务攻击。

怎么预防 CC 攻击和 DDOS 攻击
防 CC、DDOS 攻击,这些只能是用硬件防火墙做流量清洗,将攻击流量引入黑洞。

流量清洗这一块,主要是买 ISP 服务商的防攻击的服务就可以,机房一般有空余流量,我们一般是买服务,毕竟攻击不会是持续长时间。

12.请问当用户反馈网站访问慢,你会如何处理
有哪些方面的因素会导致网站网站访问慢
1、服务器出口带宽不够用

本身服务器购买的出口带宽比较小。一旦并发量大的话,就会造成分给每个用户的出口带宽就小,访问速度自然就会慢。
跨运营商网络导致带宽缩减。例如,公司网站放在电信的网络上,那么客户这边对接是长城宽带或联通,这也可能导致带宽的缩减。
2、服务器负载过大,导致响应不过来

可以从两个方面入手分析

分析系统负载,使用 w 命令或者 uptime 命令查看系统负载。如果负载很高,则使用 top 命令查看 CPU ,MEM 等占用情况,要么是 CPU 繁忙,要么是内存不够。
如果这二者都正常,再去使用 sar 命令分析网卡流量,分析是不是遭到了攻击。一旦分析出问题的原因,采取对应的措施解决,如决定要不要杀死一些进程,或者禁止一些访问等。
3、数据库瓶颈

如果慢查询比较多。那么就要开发人员或 DBA 协助进行 SQL 语句的优化。
如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等。然后,也可以搭建 MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。
4、网站开发代码没有优化好

例如 SQL 语句没有优化,导致数据库读写相当耗时。
针对网站访问慢,怎么去排查
1、首先要确定是用户端还是服务端的问题。当接到用户反馈访问慢,那边自己立即访问网站看看,如果自己这边访问快,基本断定是用户端问题,就需要耐心跟客户解释,协助客户解决问题。不要上来就看服务端的问题。一定要从源头开始,逐步逐步往下。

2、如果访问也慢,那么可以利用浏览器的调试功能,看看加载那一项数据消耗时间过多,是图片加载慢,还是某些数据加载慢。

3、针对服务器负载情况。查看服务器硬件(网络、CPU、内存)的消耗情况。如果是购买的云主机,比如阿里云,可以登录阿里云平台提供各方面的监控,比如 CPU、内存、带宽的使用情况。

4、如果发现硬件资源消耗都不高,那么就需要通过查日志,比如看看 MySQL慢查询的日志,看看是不是某条 SQL 语句查询慢,导致网站访问慢。怎么去解决

如果是出口带宽问题,那么久申请加大出口带宽。
如果慢查询比较多,那么就要开发人员或 DBA 协助进行 SQL 语句的优化。
如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等等。然后也可以搭建MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。
申请购买 CDN 服务,加载用户的访问。
如果访问还比较慢,那就需要从整体架构上进行优化咯。做到专角色专用,多台服务器提供同一个服务。
13. 命令
1、文件管理命令 cat 命令
cat 命令用于连接文件并打印到标准输出设备上。

cat 主要有三大功能

1.一次显示整个文件:

cat filename

2.从键盘创建一个文件

cat > filename

只能创建新文件,不能编辑已有文件

3.将几个文件合并为一个文件:

cat file1 file2 > file

2. chmod 命令
Linux/Unix 的文件调用权限分为三级 : 文件拥有者、群组、其他。利用 chmod 可以控制文件如何被他人所调用。

用于改变 linux 系统文件或目录的访问权限。用它控制文件或目录的访问权限。该命令有两种用法。一种是包含字母和操作符表达式的文字设定法;另一种是包含数字的数字设定法。

每一文件或目录的访问权限都有三组,每组用三位表示,分别为文件属主的读、写和执行权限;与属主同组的用户的读、写和执行权限;系统中其他用户的读、写和执行权限。可使用 ls -l test.txt 查找。

3. chown 命令
chown 将指定文件的拥有者改为指定的用户或组,用户可以是用户名或者用户 ID;组可以是组名或者组 ID;文件是以空格分开的要改变权限的文件列表,支持通配符。

1?? ?‐c 显示更改的部分的信息
2?? ?‐R 处理指定目录及子目录
1
2
实例

(1) 改变拥有者和群组 并显示改变信息

1 chown ‐c mail:mail log2012.log

(1) 改变文件群组

1 chown ‐c :mail t.log

(2) 改变文件夹及子文件目录属主及属组为 mail

1 chown ‐cR mail: test/

4. cp 命令
将源文件复制至目标文件,或将多个源文件复制至目标目录。

注意:命令行复制,如果目标文件已经存在会提示是否覆盖,而在 shell 脚本中,如果不加 -i 参数,则不会提示,而是直接覆盖

1?? ?‐i 提示
2?? ?‐r 复制目录及目录内所有项目
3?? ?‐a 复制的文件与原文件时间一样
1
2
3
实例

(1) 复制 a.txt 到 test 目录下,保持原文件时间,如果原文件存在提示是否覆盖。

1 cp ‐ai a.txt test

(2) 为 a.txt 建议一个链接(快捷方式

1 cp ‐s a.txt link_a.txt

5.find 命令
用于在文件树中查找文件,并作出相应的处理。

6. head 命令
head 用来显示档案的开头至标准输出中,默认 head 命令打印其相应文件的开头 10 行。常用参数

1 ‐n<行数> 显示的行数(行数为复数表示从最后向前数

实例

(1) 显示 1.log 文件中前 20 行

1 head 1.log ‐n 20

(2) 显示 1.log 文件前 20 字节

1 head ‐c 20 log2014.log

(3) 显示 t.log 后 10 行

1 head ‐n ‐10 t.log

7.mv 命令
移动文件或修改文件名,根据第二参数类型(如目录,则移动文件;如为文件则重命令该文件)。

当第二个参数为目录时,第一个参数可以是多个以空格分隔的文件或目录,然后移动第一个参数指定的多个文件到第二个参数指定的目录中。

实例

(1) 将文件 test.log 重命名为 test1.txt

1 mv test.log test1.txt

(2) 将文件 log1.txt,log2.txt,log3.txt 移动到根的 test3 目录中

1 mv llog1.txt log2.txt log3.txt /test3

(3) 将文件 file1 改名为 file2,如果 file2 已经存在,则询问是否覆盖

1 mv ‐i log1.txt log2.txt

(4) 移动当前文件夹下的所有文件到上一级目录

1 mv * https://blog.csdn.net/s050106/article/

8. rm 命令
删除一个目录中的一个或多个文件或目录,如果没有使用 -r 选项,则 rm 不会删除目录。如果使用 rm 来删除文件,通常仍可以将该文件恢复原状。

1 rm[选项] 文件…

实例

(1) 删除任何 .log 文件,删除前逐一询问确认

1 rm ‐i *.log

(2) 删除 test 子目录及子目录中所有档案删除,并且不用一一确认

1 rm ‐rf test

(3) 删除以 -f 开头的文件

1 rm ‐‐ ‐f*

9.tail 命令
用于显示指定文件末尾内容,不指定文件时,作为输入信息进行处理。常用查看日志文件。常用参数

1?? ?‐f 循环读取(常用于查看递增的日志文件
2?? ?‐n<行数> 显示行数(从后向前
1
2
(1)循环读取逐渐增加的文件内容

1 ping 127.0.0.1 > ping.log &

后台运行:可使用 jobs -l 查看,也可使用 fg 将其移到前台运行。

1 tail ‐f ping.log

10. vim 命令
Vim是从 vi 发展出来的一个文本编辑器。代码补完、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。

11. mkdir 命令
mkdir 命令用于创建文件夹。

可用选项

-m: 对新建目录设置存取权限,也可以用 chmod 命令设置;

-p: 可以是一个路径名称。此时若路径中的某些目录尚不存在,加上此选项后,系统将自动建立好那些尚不在的目录,即一次可以建立多个目录。

实例

(1) 当前工作目录下创建名为 t的文件夹

1 mkdir t

(2) 在 tmp 目录下创建路径为 test/t1/t 的目录,若不存在,则创建

1 mkdir ‐p /tmp/test/t1/t

12.pwd 命令
pwd 命令用于查看当前工作目录路径。

实例

(1) 查看当前路径

1 pwd

(2) 查看软链接的实际路径

1 pwd

13.rmdir 命令
从一个目录中删除一个或多个子目录项,删除某目录时也必须具有对其父目录的写权限。

注意:不能删除非空目录实例

(1)当 parent 子目录被删除后使它也成为空目录的话,则顺便一并删除

1 rmdir ‐p parent/child/child11

14. netstat 命令
Linux netstat命令用于显示网络状态。

15. ping 命令
Linux ping命令用于检测主机。

执行ping指令会使用ICMP传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。指定接收包的次数

ping ‐c 2 www.baidu.com

十、Redis
1.什么是Redis
Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许 可)高性能非关系型(NoSQL)的键值对数据库。

Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值 支持五种数据类型:字符串、列表、集合、散列表、有序集合。

与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快, 因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能快的Key-Value DB。

另外,Redis 也经常用来做分布式锁。除此之 外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

2.Redis有哪些优缺点
优点

读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
支持数据持久化,支持AOF和RDB两种持久化方式。
支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并 后的原子性执行。
数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等 数据结构。
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点

数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求 失败,需要等待机器重启或者手动切换前端的IP才能恢复。
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一 致的问题,降低了系统的可用性。
Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避 免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的 浪费。
3. 为什么要用 Redis /为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。

高性能

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上 读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候 就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如 果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可
高并发

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑 把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这 里而不用经过数据库。

4.为什么要用 Redis 而不用 map/guava 做缓存?
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,主要的特点是轻量以及快速,生命周期随着 jvm 的销毁 而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实 例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached 服务的高可用,整个程序架构上较为复杂。

5. Redis为什么这么快
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存 中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1)
数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计 的
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者 多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁 操作,没有因为可能出现死锁而导致的性能消耗
使用多路 I/O 复用模型,非阻塞 IO
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协 议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的 话,会浪费一定的时间去移动和请求
6.Redis有哪些数据类型
Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分 的使用要求 数据类型 可以存储 的值 操作 应用场景

数据类型?? ?可以存储 的值?? ?操作?? ?应用场景
STRING?? ?字符串、 整数或者 浮点数?? ?对整个字 符串或者 字符串的 其中一部 分执行操 作 对整数和 浮点数执 行自增或 者自减操 作?? ?做简单的 键值对缓 存
LIST?? ?列表?? ?从两端压 入或者弹 出元素 对单个或 者多个元 素进行修 剪, 只保留一 个范围内 的元素?? ?存储一些 列表型的 数据结 构,类似 粉丝列 表、文章 的评论列 表之类的 数据
SET?? ?无序集合?? ?添加、获 取、移除 单个元素 检查一个 元素是否 存在于集 合中?? ?交集、并 集、差集 的操作, 比如交 集,可以 把两个人 的粉丝列
HASH?? ?包含键值 对的无序 散列表?? ?添加、获 取、移除 单个键值 对 获取所有 键值对 检查某个 键是否存 在?? ?结构化的 数据,比 如一个对 象
ZSET?? ?有序集合?? ?添加、获 取、删除 元素 根据分值 范围或者 成员来获 取元素 计算一个 键的排名?? ?去重但可 以排序, 如获取排 名前几名 的用户
7.Redis的应用场景
总结一
计数器

可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数 据库的读写性能非常高,很适合存储频繁读写的计数量。

缓存

将热点数据放到内存中,设置内存的大使用量以及淘汰策略来保证缓存的命中率。

会话缓存

可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存 储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务 器,从而更容易实现高可用性以及可伸缩性。

全页缓存(FPC

除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例, Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以 快速度加载你曾浏览过的页面。

查找表

例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效, 因为缓存不作为可靠的数据来源。

消息队列(发布/订阅功能)

List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过好使用 Kafka、RabbitMQ 等消息中间件。

分布式锁实现 在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可 以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提 供的 RedLock 分布式锁实现。

其它

Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有 序性操作,从而实现排行榜等功能。

总结二
Redis相比其他缓存,有一个非常大的优势,就是支持多种数据类型。

数据类型说明string字符串,简单的k-v存储hashhash格式,value为field和 value,适合ID-Detail这样的场景。list简单的list,顺序列表,支持首位或者末 尾插入数据set无序list,查找速度快,适合交集、并集、差集处理sorted set有 序的set

其实,通过上面的数据类型的特性,基本就能想到合适的应用场景了。

string——适合简单的k-v存储,类似于memcached的存储结构,短信验证 码,配置信息等,就用这种类型来存储。

hash——一般key为ID或者唯一标示,value对应的就是详情了。如商品详情, 个人信息详情,新闻详情等。

list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省 市区表、字典表等。因为list是有序的,适合根据写入的时间来排序,如:新 的***,消息队列等。

set——可以简单的理解为ID-List的模式,如微博中一个人有哪些好友,set 牛的地方在于,可以对两个set提供交集、并集、差集操作。例如:查找两个人 共同的好友等。

Sorted Set——是set的增强版本,增加了一个score参数,自动会根据score的 值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据。 如上所述,虽然Redis不像关系数据库那么复杂的数据结构,但是,也能适合很 多场景,比一般的缓存数据结构要多。了解每种数据结构适合的业务场景,不仅 有利于提升开发效率,也能有效利用Redis的性能。

8.什么是Redis持久化
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

9.Redis 的持久化机制是什么?各自的优缺点
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:

RDB:是Redis Database缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保 存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来 定义快照的周期。

优点
只有一个文件 dump.rdb,方便持久化。
容灾性好,一个文件可以保存到安全的磁盘。
性能大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
相对于数据集大时,比 AOF 的启动效率更高。
缺点
数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发 生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)
AOF(Append-only file)持久化方式: 是指所有的命令行记录以 redis 命 令请 求协议的格式完全持久化存储)保存为 aof 文件。
AOF:持久化
AOF持久化(即Append only File持久化),则是将Redis执行的每次写命令记录 到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。 当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

优点
数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一 次 命令操作就记录到 aof 文件中一次。
通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-checkaof 工具解决数据一致性问题。
AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命 令 进行合并重写,可以删除其中的某些命令(比如误操作的 flushall))
缺点
AOF 文件比 RDB 文件大,且恢复速度慢。
数据集大的时候,比 rdb 启动效率低。
优缺点是什么
AOF文件比RDB更新频率高,优先使用AOF还原数据。
AOF比RDB更安全也更大
RDB性能比AOF好
如果两个都配了优先加载AOF
10.如何选择合适的持久化方式
一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载 入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集 要比RDB文件保存的数据集要完整。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用RDB持久化。
有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生 成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据 集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免 AOF程序的bug。
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任 何持久化方式。
11.Redis持久化数据和缓存怎么做扩容
如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。

如果Redis被当做一个持久化存储使用,必须使用固定的keys-tonodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况,必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。

12.Redis的过期键的删除策略
我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

过期策略通常有以下三种

定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以 大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到 优的平衡效果。

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略。

13.Redis key的过期时间和永久有效分别怎么设置
EXPIRE和PERSIST命令。

14.我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择,我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种

定时去清理过期的缓存

当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

15. Redis的内存淘汰策略有哪些
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除:

noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除近少使用的key。(这个是最常用的
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。(不建议)
设置过期时间的键空间选择性移除:

volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除 近 少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

16. Redis事务的概念
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。

事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

17.Redis事务的三个阶段
1. 事务开始 MULTI

2. 命令入队

3. 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

18.说说Redis哈希槽的概念
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384 个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

19.Redis集群会有写操作丢失吗?为什么
Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

20.Redis集群之间是如何复制的
异步复制

21.Redis集群最大节点个数是多少
16384个

22.Redis是单线程的,如何提高多核CPU的利用率
可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。

23.为什么要做Redis分区
分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你 多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

24.Redis分区有什么缺点
涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
同时操作多个key,则不能使用Redis事务.
分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集
当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis 实例和主机同时收集RDB / AOF文件。
分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis 节点,能做到 大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。
25.什么是 RedLock
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性

安全特性:互斥访问,即永远只有一个 client 能拿到锁

避免死锁: 终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区

容错性:只要大部分 Redis 节点存活就可以正常提供服务缓存异常缓存雪崩

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

一般并发量不是特别多的时候,使用 多的解决方案是加锁排队。

给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

26.缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截

从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力附加对于空间的利用到达了一种极致

27.缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期,这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

1.设置热点数据永远不过期。

2.加互斥锁,互斥锁缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据

解决方案

直接写个缓存刷新页面,上线时手工操作一下

数据量不大,可以在项目启动的时候自动进行加载

定时刷新缓存

28.缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的 终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案

一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级

警告:有些服务在一段时间内成功率有波动(如在95~100%之间,可以自动降级或人工降级,并发送告警

错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的 大阀值,此时可以根据情况自动降级或者人工降级

严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

29.热点数据和冷数据
热点数据,缓存才有价值。对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。

再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

数据更新前至少读取两次,缓存才有意义。这个是基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢

!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

30.缓存热点key
缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

31.Redis与Memcached的区别
两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同

对比参数?? ?Redis?? ?Memcac hed
类型?? ?1. 支持内存 2. 非关系型数据库?? ?1. 支持内存 2. 键值对形式 3. 缓存形式
数据存储类型?? ?1. String 2. List 3. Set 4. Hash 5. Sort Set 【俗称 ZSet】?? ?1. 文本型 2. 二进制类型
查询【操作】类型?? ?1. 批量操作 2. 事务支持 3. 每个类型不同的 CRUD?? ?1.常用的 CRUD 2. 少量的其他命令
附加功能?? ?1. 发布/订阅模式 2. 主从分区 3. 序列化 支持 4. 脚本支持 【Lua脚本】?? ?1. 多线程服务支持
网络IO模型?? ?1. 单线程的多路 IO 复用模型?? ?1. 多线程,非阻塞IO模式
事件库?? ?自封转简易事件库 AeEvent?? ?贵族血统的 LibEvent 事件库
持久化支持?? ?1. RDB 2. AOF?? ?不支持
集群模式?? ?原生支持 cluster 模式,可以实现主从复制,读写分离?? ?没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据
内存管理机制?? ?在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用 的 value 交换到磁盘?? ?Memcach ed 的数据则会一直在内存中, Memcach ed 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
复杂数据?? ?纯key- value,数据量非常大,并发量非常大的业务
适用场景?? ?结构,有持久化,高可用需求,value 存储内容较大?? ?
(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

(2) redis的速度比memcached快很多

(3) redis可以持久化其数据

32.如何保证缓存与数据库双写时的数据一致性
问题场景?? ?描述?? ?解决
先写缓存,再写数据库,缓存写成功,数据库写失败?? ?缓存写成功,但写数据库失败或者响应延迟,则下次读取(并发读)缓存时,就出现脏读?? ?这个写缓存的方式,本身就是错误的,需要改为先写数据库,把旧缓存置为失效;读取数据的时候,如果缓存不存在,则读取数据库再写缓存
先写数据库,再写缓存,数据库写成功,缓存写失败?? ?写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据?? ?缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现
需要缓存异步刷新?? ?指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候?? ?确定哪些数据适合此类场景,根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔
33.Redis常见性能问题和解决方案
Master 好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。

如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。

为了主从复制的速度和连接的稳定性,Slave和Master 好在同一个局域网内。

尽量避免在压力较大的主库上增加从库

Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。

为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果

Master挂了,可以立马启用Slave1做Master,其他不变。

34.假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来
使用keys指令可以扫出指定模式的key列表。

对方接着追问
如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题

这个时候你要回答redis关键的一个特性

redis是单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

35.使用Redis做过异步队列吗,是如何实现的
使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。

redis可以通过 pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

36.Redis如何实现延时队列
使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。

37.Redis回收进程如何工作的
一个客户端运行了新的命令,添加了新的数据。

Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。

一个新的命令被执行,等等。

所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键,不用多久内存限制就会被这个内存使用量超越。

38.Redis回收使用的是什么算法
LRU算法

十一、Rabbit MQ
1.什么是RabbitMQ
RabbitMQ是一款开源的,Erlang编写的,基于AMQP协议的消息中间件

2.Rabbitmq 的使用场景
异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。
应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。
流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请 求。
日志处理 - 解决大量日志传输。
消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通 讯。比如实现点对点消息队列,或者聊天室等。
3.RabbitMQ基本概念
Broker: 简单来说就是消息队列服务器实体
Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列
Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来
Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
VHost: vhost 可以理解为虚拟 broker ,即 mini-RabbitMQ server。其内部 均含有独立的 queue、exchange 和 binding 等,但最最重要的是,其拥有独立的 权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度, vhost 可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同 的 vhost 中)。
Producer: 消息生产者,就是投递消息的程序
Consumer: 消息消费者,就是接受消息的程序
Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个 channel代表一个会话任务
由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的 唯一的线路。

4. MQ优缺点
优点
应用解耦
生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可,而不需要和其他系统有耦合,这显然也提高了系统的扩展性。

异步提速
将用户的请求数据存储到消息队列之后就立即返回结果。随后,系统再对消息进行消费。

削峰限流
先将短时间内高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。

缺点
系统可用性降低

系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。

系统复杂度提高

MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。

一致性问题

A 系统处理完业务,通过 MQ 给 B、C、D 三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败,则会造成数据处理的不一致。

5.Kafka、ActiveMQ、RabbitMQ、RocketMQ 有 什么优缺点
ActiveMQ?? ?RabbitMQ?? ?RocketMQ?? ?Kafka?? ?ZeroMQ
单机吞吐 量?? ?比 RabbitM Q低?? ?2.6w/s( 消息做持 久化)?? ?11.6w/s?? ?17.3w/s?? ?29w/s
开发语言?? ?Java?? ?Erlang?? ?Java?? ?Scala/Java?? ?C
主要维护者?? ?Apache?? ?Mozilla/Spring?? ?Alibaba?? ?Apache?? ?iMatix创始人已去世
成熟度?? ?成熟?? ?成熟?? ?开源版本不够成熟?? ?比较成熟?? ?只有C、PHP等版本成熟
订阅形式?? ?点对点 (p2p)、广 播(发布订阅)?? ?提供了4 种: direct, topic,Headers 和 fanout。 fanout就 是广播模 式?? ?基于 topic/me ssageTag 以及按照消息类 型、属性 进行正则 匹配的发 布订阅模 式?? ?基于topic 以及按照 topic进行 正则匹配的发布订 阅模式?? ?点对点(P2P)
持久化?? ?支持少量 堆积?? ?支持少量 堆积?? ?支持大量 堆积?? ?支持大量 堆积?? ?不支持
顺序消息?? ?不支持?? ?不支持?? ?支持?? ?支持?? ?不支持
性能稳定 性?? ?好?? ?好?? ?一般?? ?较差?? ?很好
集群方式?? ?支持简单 集群模 式,比 如’主备’,对 高级集群 模式支持 不好。?? ?支持简单 集群'复 制’模 式,对高 级集群模 式支持不 好。?? ?常用 多 对’Mast erSlave’ 模 式,开源 版本需手 动切换 Slave变成 Master?? ?天然 的‘Lead erSlave’无 状态集 群,每台 服务器既 是Master 也是Slave?? ?不支持
管理界面?? ?一般?? ?较好?? ?一般?? ?无?? ?无
综上,各种对比之后,有如下建议

一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用 的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是 算了吧,我个人不推荐用这个了

后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师 去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开 源的,比较稳定的支持,活跃度也高

不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出 品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝 对不会黄。

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是 不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选 择。

如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝 对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性 规范。

6.RabbitMQ的工作模式
1.simple模式(即最简单的收发模式


特点

一个生产者对应一个消费者,通过队列进行消息传递。
该模式使用 direct 交换机,direct 交换机是 RabbitMQ 默认交换机。

2. 工作队列模式(Work Queue)


与简单模式相比,工作队列模式(Work Queue)多了一些消费者,该模式也使用 direct 交换机,应用于处理消息较多的情况。特点如下

一个队列对应多个消费者。
一条消息只会被一个消费者消费。
消息队列默认采用 轮询 的方式将消息平均发送给消费者。

3. 发布订阅模式(Publish/Subscribe)


在开发过程中,有一些消息需要不同消费者进行不同的处理,如电商网站的同一条促销信息需要短信发送、邮件发送、站内信发送等。此时可以使用发布订阅模式(Publish/Subscribe),特点如下

生产者将消息发送给交换机,交换机将消息转发到绑定此交换机的 每个队列 中。
工作队列模式的交换机只能将消息发送给一个队列,发布订阅模式的交换机能将消息发送给多个队列。
发布订阅模式使用 fanout 交换机。
4. 路由模式(Routing)


使用发布订阅模式时,所有消息都会发送到绑定的队列中,但很多时候,不是所有消息都无差别的发布到所有队列中。比如电商网站的促销活动,618大促可能会发布到所有队列;而一些小的促销活动为了节约成本,只发布到站内信队列。此时需要使用 路由模式 (Routing)完成这一需求。特点如下

每个队列绑定路由关键字 RoutingKey
生产者将带有 RoutingKey 的消息发送给交换机,交换机根据 RoutingKey 转发到指定队列。
路由模式使用 direct 交换机。
能按照路由键将消息发送给指定队列
5. 通配符模式(Topics)


通配符模式(Topics)是在路由模式的基础上,给队列绑定带通配符的路由关键字,只要消息的 RoutingKey 能实现通配符匹配,就会将消息转发到该队列。通配符模式比路由模式更灵活,通配符模式使用 topic 交换机。 能按照通配符规则将消息发送给指定队列

通配符规则如下

队列设置 RoutingKey 时,“#” 可以匹配任意多个单词,“*” 可以匹配任意一个单词。
消息设置 RoutingKey 时,RoutingKey 由多个单词构成,中间以 “.” 分割。
7.如何保证RabbitMQ消息的顺序性
拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确 实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

8.如何保证消息不被重复消费?或者说,如何保证消息消费时的幂等性
1.什么情况会导致消息被重复消费呢
生产者:生产者可能会重复推送一条数据到 MQ 中,比如 Controller 接口被重复调用了 2 次,没有做接口幂等性导致的

MQ:在消费者消费完准备响应 ack 消息消费成功时,MQ 突然挂了,导致 MQ 以为消费者还未消费该条数据,MQ 恢复后再次推送了该条消息,导致了重复消费。

消费者:消费者已经消费完消息,正准备但是还未响应给ack消息到时,此时消费者挂了,服务重启后 MQ 以为消费者还没有消费该消息,再次推送了该条消息。

2.解决方案
2.1使用数据库唯一键约束
缺点:局限性很大,仅仅只能用在我们数据新增场景,并且性能也比较低

2.2使用乐观锁
假设是更新订单状态,在发送的消息的时候带上修改字段的版本号

缺点:如果说更新字段比较多,并且更新场景比较多,可能会导致数据库字段增加并且还有可能出现多条消息同时在队列中此时他们修改字段版本号一致,排在后续的消息无法被消费

2.3 简单的消息去重,插入消费记录,增加数据库判断

优点:很多场景下的确能起到不错的效果

缺点

这个消费者的代码执行需要1秒,重复消息在执行期间(假设100毫秒)内到达(例如生产者快速重发,Broker重启等,增加校验的地方是不是还是没数据(因为上一条消息还没消费完,没有记录
那么就会穿透掉检查的挡板,最后导致重复的消息消费逻辑进入到非幂等安全的业务代码中,从而引发重复消费的问题

2.4并发消息去重基于消息幂等表

缺点:如果说第一次消息投递异常没有消费成功,并且没有将消息状态给置为成功或者没有删除消息表记录,此时延时消费每次执行下列都是一直处于消费中,最后消费就会被视为消费失败而被投递到死信Topic中

方案:插入的消息表必须要带一个最长消费过期时间,例如10分钟.

上述方案只需要一个存储的中心媒介,那我们可以选择更灵活的存储中心媒介,比如Redis。使用Redis有两个好处

性能上损耗更低
上面我们讲到的超时时间可以直接利用Redis本身的ttl实现
3.总结
利用数据库唯一键约束
可以利用我们的乐观锁
插入消费记录
不丢和不重是矛盾的(在分布式场景下,总的来说,开发者根据业务的实际需求来选择相应的方式即可。
9.如何保证RabbitMQ消息的可靠传输
一、可靠传输
本篇文章主要讲 RabbitMQ 如何保证消息的可靠传输,所以在讲RabbitMQ的实现之前,我们需要先来搞懂一个问题,就是什么是消息的可靠传输。

在 RabbitMQ 中,一个消息从产生到被消费大致需要经过三个步骤,即生产者生产消息,消息投递到 RabbitMQ,RabbitMQ 再将消息推送消费者(或者是消费者拉取,最终消费者将这条消息成功消费。

所以消息丢失也可以划分为三种情况——生产者、消息队列、消费者,如下图所示

所以消息的可靠传输,就是确保消息能够百分百从生产者发送到服务器,再从服务器发送到消费者。接下来我们就针对这三种情况分别讨论解决方案。

二、生产者投递消息失败
1. 事务机制

使用 RabbitMQ 的事务功能,此时可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务 channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务 channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务 channel.txCommit。

// 开启事务
channel.txSelect();
try {
// 发送消息
} catch(Exception e) {
channel.txRollback();
// 重发消息
}
// 提交事务
channel.txCommit();
1
2
3
4
5
6
7
8
9
10
事务机制可以基本确保生产者投递消息成功,但是这种方式有比较大的缺点,基本上 RabbitMQ 事务机制(同步)一搞,基本上吞吐量会下来,因为太耗性能了。

2. /confirm/i机制

针对上述问题,如果还要确保 RabbitMQ 生产者的消息正确投递,可以开启 confirm 模式,在生产者端设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ACK 消息,告诉你说这个消息 ok 了。

如果 RabbitMQ 没能处理这个消息,会回调你一个 nack 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。

try {
?? ?channel.confirmSelect(); //将信道置为 publisher confirm 模式
?? ?// 之后正常发送消息
?? ?channel.basicPushlish("exchange", "routingKey", null,
?? ?"publisher confirm test".getBytes());
?? ?if (!channel.waitFormConfirms()) {
?? ?System.out.println("Send message failed");
?? ?// do something else...
?? ?} catch (InterruptedException e) {
?? ?e.printStackTrace();
?? ?}
}
1
2
3
4
5
6
7
8
9
10
11
12
事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm 机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收之后会异步回调你一个接口通知你这个消息接收到了。

所以一般生产者到 RabbitMQ 这块避免数据丢失,会采用 confirm 机制更多些。

三、消息队列自身丢失
就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,可能导致少量数据丢失,但是这个概率较小。

设置持久化有两个步骤

创建 queue 的时候将其设置为持久化
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是不会持久化 queue 里的数据。

发送消息时将消息的 deliveryMode 设置为 2
就是将消息设置为持久化,此时 RabbitMQ 就会将消息持久化到磁盘上去。

必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。

同时持久化也可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘后,才会通知生产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ACK,你也是可以自己重发的。

注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存中的数据丢失。

四、消费者宕机
对于消费者端所产生的情况就是:消费者成功接收到消息,但是还未将消息处理完毕就宕机了。针对这种情况,可以利用 RabbitMQ 提供的 消息确认机制。

消息确认机制
为了保证消息从队列可靠地到达消费者,RabbitMQ 提供了消息确认机制(message acknowledgement)。消费者在订阅队列时,可以指定 autoAck 参数
当 autoAck 等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息

当 autoAck 等于 false 时,RabbitMQ 会等待消费者显示地回复确认信号后才从内存(后者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。

所以对于消费者可能发生宕机地情况,我们可以将 autoAck 参数置为 false,消费者就有足够的时间处理这条消息,不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为 RabbitMQ 会一直等待并且持有这条消息,直到消费者显示调用 Basic.Ack 命令为止。

当 autoAck 参数设置为 false,对于 RabbitMQ 服务端而言,队列中的消息分成了两个部分:一部分是等待投递给消费者的消息,另一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果 RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则 RabbitMQ 会安排该消息重新进入队列,等待投递给下一个消费者,当然也可能还是原来的那个消费者。

十二、 Spring Boot
1.什么是 Spring Boot
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。

2.Spring Boot 有哪些优点
Spring Boot 主要有如下优点

容易上手,提升开发效率,为 Spring 开发提供一个更快、更广泛的入门体验。

开箱即用,远离繁琐的配置。

提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。

没有代码生成,也不需要XML配置。

避免大量的 Maven 导入和各种版本冲突。

3.Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解

@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

@ComponentScan:Spring组件扫描。

4.Spring Boot 自动配置原理是什么
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心

@EnableAutoConfiguration 给容器导入meta-INF/spring.factories 里定义的自动配置类。筛选有效的自动配置类。

每一个自动配置类结合对应的 xxxProperties.java 读取配置文件进行自动配置功能。

5.Spring Boot 中如何解决跨域问题 ?
跨域可以在前端通过 JSONP 来解决,但是 JSONP 只可以发送 GET 请求,无法发送其他类型的请求,在 RESTful 风格的应用中,就显得非常鸡肋,因此我们推荐在后端通过 (CORS,Cross-origin resource sharing) 来解决跨域问题。这种解决方案并非 Spring Boot 特有的,在传统的 SSM 框架中,就可以通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。

1 ?@Configuration
2 ?public class CorsConfig implements WebMvcConfigurer { 3
4 ? @Override
5 ? public void addCorsMappings(CorsRegistry registry) {
6 ? registry.addMapping(" ? ? ? private static Singleton instance = null; ?
? ? ? ? ?
? ? private Singleton() { ? ?
? ? } ?
? ? ? ? ? public static Singleton getInstance() { ? ? ? ? ? if (instance == null) { ? ? ? ? ? ? ? instance = new Singleton(); ? ? ? ? ?
? ??? ? ?? ?} ? ? ??
? ??? ??? ?return instance; ? ??
? ??? ? ?? ?} ?
? ??? ? }?
? ? (2)饿汉式?
? ? public class Singleton { ?
? ? ?? ? ? ? ? ?? ??? ??? ?
? ? ?? ?private static Singleton instance = new Singleton(); ?
? ? ?? ? ? ? ?
? ??? ? ?? ?private Singleton() { ??
? ? ?? ?} ?
? ? ? ? ? public static Singleton getInstance() { ? ? ? ??
? ? ?? ?return instance; ? ??
? ? } ??
?}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用场景

要求生成唯一序列号的环境
在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数 器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并 确保是线程安全的
创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源
需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式 (当然,也可以直接声明为static的方式)。
2. 工厂模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂 方法使一个类的实例化延迟到其子类。
接口

public interface Fruit { ? ? ??
public void print(); ? } ??
2个实现类
public class Apple implements Fruit{ ?
? ? @Override ? ? ??
? ? public void print() { ? ? ? ? ??
??? ? ? System.out.println("我是一个苹果"); ? ? ??
??? ?} ?
} ??
public class Orange implements Fruit{ ?
? ? @Override ? ? ??
? ? public void print() { ? ? ? ? ??
??? ? ? System.out.println("我是一个橘子"); ?
? ? } ?
} ??

工厂类

public class FruitFactory { ? ? ??
?? ?public Fruit produce(String type){ ? ? ? ? ??
?? ??? ?if(type.equals("apple")){ ? ? ? ? ? ? ??
?? ??? ??? ?return new Apple(); ? ? ? ? ??
?? ??? ?}else if(type.equals("orange")){ ? ? ? ? ? ? ??
?? ??? ??? ?return new Orange(); ? ? ? ? ??
?? ??? ?}else{ ? ? ? ? ? ? ??
?? ??? ??? ?System.out.println("请输入正确的类型!"); ? ? ? ? ? ? ? ??
?? ??? ?return null; ? ? ? ? ? ?
?? ??? ?} ? ? ??
?? ?} ??
} ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
使用场景:jdbc连接数据库,硬件访问,降低对象的产生和销毁

3. 抽象工厂模式:为创建一组相关或相互依赖的对象提供一个接口,而且无须指 定它们的具体类。
相对于工厂模式,我们可以新增产品类(只需要实现产品接口,只需要同时新 增一个工厂类,客户端就可以轻松调用新产品的代码。

interface food{}?
class A implements food{}?
class B implements food{}?
interface produce{ food get();}?
class FactoryForA implements produce{ ? ??
?? ?@Override ? ??
?? ?public food get() { ? ? ? ??
?? ??? ?return new A(); ? ?
? ? }?
}?
class FactoryForB implements produce{ ? ??
?? ?@Override ? ??
?? ?public food get() { ? ? ? ??
?? ?return new B(); ? ??
?? ?}?
}?
public class AbstractFactory { ? ??
?? ?public void ClientCode(String name){ ? ? ? ??
?? ??? ?food x= new FactoryForA().get(); ? ? ? ??
?? ??? ?x = new FactoryForB().get(); ? ??
?? ?}?
}?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用场景:一个对象族(或是一组没有任何关系的对象)都有相同的约束。 涉及不同操作系统的时候,都可以考虑使用抽象工厂模式

4.建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可 以创建不同的表示。
public class Build { ? ??
?? ?static class Student{ ? ??
?? ?String name = null ; ? ? ? ??
?? ??? ?int number = -1 ; ? ? ? ??
?? ??? ?String sex = null ; ? ??
?? ?public Student(Builder builder) ? ? ? ? { ? ??
?? ?this.name=builder.name; ? ??
?? ?this.number=builder.number; ? ??
?? ?this.sex=builder.sex; ? ??
?? ?} ? ? ? ??
?? ??? ?static class Builder{
? ? ? ? ? ??
?? ??? ?String name = null ; ? ? ? ? ? ??
?? ??? ?int number = -1 ; ? ? ? ? ? ??
?? ??? ?String sex = null ; ? ? ? ? ? ??
?? ??? ?public Builder setName(String name){ ? ? ? ? ? ? ? ??
?? ??? ?this.name=name; ? ? ? ? ? ? ? ??
?? ??? ?return this; ? ? ? ? ? ??
?? ??? ?} ? ? ? ? ? ??
?? ??? ?public Builder setNumber(int number){ ? ? ? ? ? ? ? ??
?? ??? ?this.number=number; ? ? ? ? ? ? ? ??
?? ??? ?return this; ? ? ? ? ? ??
?? ??? ?} ? ? ? ? ? ??
?? ??? ?public Builder setSex(String sex){ ? ? ? ? ? ? ? ??
?? ??? ?this.sex=sex; ? ? ? ? ? ? ? ??
?? ??? ?return this; ? ? ? ? ? ??
?? ??? ?} ? ? ? ? ? ??
?? ??? ?public Student build(){ ? ? ? ? ? ? ? ??
?? ??? ?return new Student(this); ? ? ? ? ? ?
?? ??? ?}
?} ? ? ? ? ? ? ? ??
} ? ??
public static void main(String[] args) { ? ??
Student A=new Student.Builder().setName("张 三").setNumber(1).build(); ? ? Student B=new Student.Builder().setSex("男").setName("李四").build(); ? ? ?
?? ?System.out.println(A.name+" "+A.number+" "+A.sex); ? ? ? ? ?? ??? ??? ??? ?System.out.println(B.name+" "+B.number+" "+B.sex); ? ??
?? ?}?
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
使用场景

相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模 式。
多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同 时,则可以使用该模式。
产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候 使用建造者模式非常合适。
5. 原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的 对象。
public class Prototype implements Cloneable{ ? ??
?? ?private String name; ? ??
?? ?public String getName() { ? ? ? ??
?? ??? ?return name; ? ??
?? ?} ? ??
?? ?public void setName(String name) { ? ? ? ??
?? ?this.name = name; ? ??
?? ?} ? ??
?? ?@Override ? ??
?? ?protected Object clone() ? { ? ? ? ??
?? ??? ?try { ? ? ? ? ? ??
?? ??? ??? ?return super.clone(); ? ? ? ??
?? ??? ?} catch (CloneNotSupportedException e) { ? ? ? ? ? ??
?? ??? ??? ?e.printStackTrace(); ? ? ? ??
?? ??? ?}finally { ? ? ? ? ? ??
?? ??? ??? ?return null; ? ? ? ??
?? ??? ?} ? ??
?? ?} ? ??
?? ?public static void main ( String[] args){ ? ? ? ??
?? ??? ?Prototype pro = new Prototype(); ? ? ? ??
?? ??? ?Prototype pro1 = (Prototype)pro.clone(); ? ??
?? ?}
}?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
原型模式实际上就是实现Cloneable接口,重写clone)方法。

使用原型模式的优点

1.性能优良

原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是 要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。

2.逃避构造函数的约束

这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的(参见 13.4节)。

使用场景

资源优化场景 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。

性能和安全要求的场景 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模 式。

一个对象多个修改者的场景 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可 以考虑使用原型模式拷贝多个对象供调用者使用。

浅拷贝和深拷贝

浅拷贝:Object类提供的方法clone只是拷贝本对象,其对象内部的数组、引用 对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝, 其他的原始类型比如int、long、char、string(当做是原始类型)等都会被拷 贝。

注意: 使用原型模式时,引用的成员变量必须满足两个条件才不会被拷贝:一 是类的成员变量,而不是方法内变量;二是必须是一个可变的引用对象,而不是 一个原始类型或不可变对象。

深拷贝:对私有的类变量进行独立的拷贝

:this.arrayList = (ArrayList)this.arrayList.clone();

6. 适配器模式:将一个类的接口变换成客户端所期待的另一种接口,从而使原本 因接口不匹配而无法在一起工作的两个类能够在一起工作。
主要可分为3种

类适配:创建新类,继承源类,并实现新接口,例如 class adapter extends oldClass implements newFunc{}
对象适配:创建新类持源类的实例,并实现新接口,例如 class adapter implements newFunc { private oldClass oldInstance ;}
接口适配:创建新的抽象类实现旧接口方法。例如 abstract class adapter implements oldClassFunc { void newFunc();}
使用场景

你有动机修改一个已经投产中的接口时,适配器模式可能是适合你的模式。比 如系统扩展了,需要使用一个已有或新建立的类,但这个类又不符合系统的接 口,怎么办?使用适配器模式,这也是我们例子中提到的。

7. 装饰器模式:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰 器模式相比生成子类更为灵活 。
interface Source{ void method();}?
public class Decorator implements Source{ ? ??
?? ?private Source source ; ? ??
?? ?public void decotate1(){ ? ? ? ??
?? ??? ?System.out.println("decorate"); ? ??
?? ?} ? ??
?? ?@Override ? ??
?? ?public void method() { ? ? ? ??
?? ??? ?decotate1(); ? ? ? ??
?? ??? ?source.method(); ? ??
?? ?}?
}?
1
2
3
4
5
6
7
8
9
10
11
12
使用场景

需要扩展一个类的功能,或给一个类增加附加功能。
需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式。
8. 代理模式:为其他对象提供一种代理以控制对这个对象的访问。
interface Source{ void method();}?
class OldClass implements Source{ ? ??
?? ?@Override ? ??
?? ?public void method() { ? ??
?? ?}?
}?
class Proxy implements Source{ ? ??
?? ?private Source source = new OldClass(); ? ??
?? ?void doSomething(){} ? ??
?? ?@Override ? ?
?? ?public void method() { ? ? ? ??
?? ??? ?new Class1().Func1(); ? ? ? ??
?? ??? ?source.method(); ? ? ? ??
?? ??? ?new Class2().Func2(); ? ? ? ??
?? ??? ?doSomething(); ? ??
?? ?}?
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
9. 中介者模式:用一个中介对象封装一系列的对象交互,中介者使各对象不需要 显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
public abstract class Mediator { ? ? ?
?? ?//定义同事类 ? ? ?
?? ?protected ConcreteColleague1 c1; ? ? ?
?? ?protected ConcreteColleague2 c2; ? ? ?
?? ?//通过getter/setter方法把同事类注入进来 ? ? ?
?? ?public ConcreteColleague1 getC1() { ? ? ? ? ? ? ?
?? ??? ?return c1; ? ? ?
?? ?}
? public void setC1(ConcreteColleague1 c1) { ? ? ? ? ? ? ?
??? ? this.c1 = c1; ? ? ?
? } ? ? ?
? public ConcreteColleague2 getC2() { ? ? ? ? ? ??
??? ? return c2; } ? ??
? public void setC2(ConcreteColleague2 c2) { ? ? ??
?? ? ?this.c2 = c2; ? ??
? } ? ?
? //中介者模式的业务逻辑 ? ??
? public abstract void doSomething1(); ? ??
? public abstract void doSomething2();?
}?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
使用场景: 中介者模式适用于多个对象之间紧密耦合的情况,紧密耦合的标准是:在类图中 出现了蜘蛛网状结构,即每个类都与其他的类有直接的联系。

10. 命令模式:将一个请求封装成一个对象,从而让你使用不同的请求把客户端 参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
Receiver接受者角色:该角色就是干活的角色,命令传递到这里是应该被执行的

Command命令角色:需要执行的所有命令都在这里声明

Invoker调用者角色:接收到命令,并执行命令

//通用Receiver类?
public abstract class Receiver { ? ??
?? ?public abstract void doSomething();?
}?
//具体Receiver类?
public class ConcreteReciver1 extends Receiver{ ? ? ?
?? ?//每个接收者都必须处理一定的业务逻辑 ? ? ?
?? ?public void doSomething(){ } ?
}?
public class ConcreteReciver2 extends Receiver{ ? ? ?
?? ?//每个接收者都必须处理一定的业务逻辑 ? ? ?
?? ?public void doSomething(){ } ?
}?
//抽象Command类 public abstract class Command { ? ??
?? ?public abstract void execute();?
}?
//具体的Command类?
public class ConcreteCommand1 extends Command { ? ? ?
?? ?//对哪个Receiver类进行命令处理 ? ? ?
?? ?private Receiver receiver; ??
?? ?//构造函数传递接收者 ??
?? ?public ConcreteCommand1(Receiver _receiver){ ??
?? ??? ?this.receiver = _receiver; ? ??
?? ?} ?
?? ?//必须实现一个命令?
?? ?public void execute() { ??
?? ?//业务处理 ? ? ? ?
?? ??? ?this.receiver.doSomething(); ? ?
?? ?} ?? ?
}?
public class ConcreteCommand2 extends Command { ?
?? ?//哪个Receiver类进行命令处理 ??
?? ?private Receiver receiver;?
?? ?//构造函数传递接收者
?? ?public ConcreteCommand2(Receiver _receiver){ ? ?
?? ??? ?this.receiver = _receiver; ??
} ? ??
//必须实现一个命令?
?public void execute() { ? ? ?
?? ? //业务处理 ? ? ?
?? ? this.receiver.doSomething();?
?? ? } ?
?}?
?//调用者Invoker类 public class Invoker { ??
?private Command command;
??? ?public void setCommand(Command _command){ ? ?
??? ?this.command = _command; ?
?}
public void action() { ? ? ?
?? ?this.command.execute(); ?
?? ?}?
}?
?? ?//场景类?
public class Client {?
?? ?public static void main(String[] args){ ? ? ?
?? ??? ?Invoker invoker = new Invoker(); ? ? ??
?? ??? ?Receiver receiver = new ConcreteReceiver1();
? ? ?? ?Command command = new ConcreteCommand1(receiver); ? ? ? ? ?? ??? ??? ??? ?invoker.setCommand(command); ? ?
? ? ??? ?invoker.action();?
? ? ?} ?? ?
}?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
使用场景: 认为是命令的地方就可以采用命令模式,例如,在GUI开发中,一个按钮的点击 是一个命令,可以采用命令模式;模拟DOS命令的时候,当然也要采用命令模
;触发-反馈机制的处理等。

11.责任链模式:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
public abstract class Handler {
? ? ?private Handler nextHandler;
? ? ?//每个处理者都必须对请求做出处理
? ? ?public final Response handleMessage(Request request){
? ? ? ? ? ? ?Response response = null; ?
? ? ? ? ? ? ?//判断是否是自己的处理级别
? ? ? ? ? ? ?if(this.getHandlerLevel().equals(request.getRequestLevel())){
? ? ? ? ? ? ? ? ? ? response = this.echo(request);
? ? ? ? ? ? ?}else{ ?//不属于自己的处理级别
? ? ? ? ? ? ? ? ? ? //判断是否有下一个处理者
? ? ? ? ? ? ? ? ? ? if(this.nextHandler != null){
? ? ? ? ? ? ? ? ? ? ? ? ? ? response = this.nextHandler.handleMessage(request);
? ? ? ? ? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? ? ? ? ? ? ? //没有适当的处理者,业务自行处理
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ?}
? ? ? ? ? ? ?return response;
? ? ?}
? ? ?//设置下一个处理者是谁
? ? ?public void setNext(Handler _handler){
? ? ? ? ? ? ?this.nextHandler = _handler;
? ? ?}
? ? ?//每个处理者都有一个处理级别
? ? ?protected abstract Level getHandlerLevel();
? ? ?//每个处理者都必须实现处理任务
? ? ?protected abstract Response echo(Request request);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
12.策略模式:定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。
使用场景

多个类只有在算法或行为上稍有不同的场景。

算法需要自由切换的场景。

需要屏蔽算法规则的场景。

13.迭代器模式:它提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。
迭代器模式已经被淘汰,java中已经把迭代器运用到各个聚集类(collection)中了,使用java自带的迭代器就已经满足我们的需求了。

14.组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
public class Composite extends Component {
? ? ?//构件容器
? ? ?private ArrayList componentArrayList = new ArrayList();
? ? ?//增加一个叶子构件或树枝构件
? ? ?public void add(Component component){
? ? ? ? ? ? ?this.componentArrayList.add(component);
? ? ?}
? ? ?//删除一个叶子构件或树枝构件
? ? ?public void remove(Component component){
this.componentArrayList.remove(component);
? ? ?}
? ? ?//获得分支下的所有叶子构件和树枝构件
? ? ?public ArrayList getChildren(){
? ? ? ? ? ? ?return this.componentArrayList;
? ? ?}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用场景

维护和展示部分-整体关系的场景,如树形菜单、文件和文件夹管理。

从一个整体中能够独立出部分模块或功能的场景。

15.观察者模式:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
public abstract class Subject {
? ? ?//定义一个观察者数组
? ? ?private Vector obsVector = new Vector();
? ? ?//增加一个观察者
? ? ?public void addObserver(Observer o){
? ? ? ? ? ? ?this.obsVector.add(o);
? ? ?}
? ? ?//删除一个观察者
? ? ?public void delObserver(Observer o){
? ? ? ? ? ? ?this.obsVector.remove(o);
? ? ?}
? ? ?//通知所有观察者
? ? ?public void notifyObservers(){
? ? ? ? ? ? ?for(Observer o:this.obsVector){
? ? ? ? ? ? ? ? ? ? ?o.update();
}
? ? ?}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用场景

关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。

事件多级触发场景。

跨系统的消息交换场景,如消息队列的处理机制

16.门面模式:要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
public class Facade {
? ? private subSystem1 subSystem1 = new subSystem1();
? ? private subSystem2 subSystem2 = new subSystem2();
? ? private subSystem3 subSystem3 = new subSystem3();
? ? public void startSystem(){
? ? ? ? subSystem1.start();
? ? ? ? subSystem2.start(); ? ? ? ??
? ? ? ? subSystem3.start();
? ? }
? ? public void stopSystem(){
? ? ? ? subSystem1.stop();
? ? ? ? subSystem2.stop(); ? ? ? ??
? ? ? ? subSystem3.stop();
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用场景

为一个复杂的模块或子系统提供一个供外界访问的接口

子系统相对独立——外界对子系统的访问只要黑箱操作即可

预防低水平人员带来的风险扩散

17.备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
public class Originator {
? ? private String state;
? ?
? ? public Memento createMemento(){
? ? ? ? return new Memento(state);
? ? }
? ?
? ? public void restoreMemento(Memento memento){
? ? ? ? this.state = memento.getState();
? ? }
? ? public String getState() {
? ? ? ? return state;
? ? }
? ? public void setState(String state) {
? ? ? ? this.state = state;
? ? ? ? System.out.println("当前状态" + this.state);
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用场景

需要保存和恢复数据的相关状态场景。

提供一个可回滚(rollback)的操作。

需要监控的副本场景中。

数据库连接的事务管理就是用的备忘录模式。

18.访问者模式:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
使用场景

一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。

需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。

19.状态模式:当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。
使用场景

行为随状态改变而改变的场景这也是状态模式的根本出发点,例如权限设计,人员的状态不同即使执行相同的行为结果也会不同,在这种情况下需要考虑使用状态模式。

条件、分支判断语句的替代者

20.解释器模式:给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
使用场景

重复发生的问题可以使用解释器模式

一个简单语法需要解释的场景

21.享元模式:使用共享对象的方法,用来尽可能减少内存使用量以及分享资讯。
abstract class flywei{ }
public class Flyweight extends flywei{
? ? Object obj ;
? ? public Flyweight(Object obj){
? ? ? ? this.obj = obj;
? ? }
}
class ?FlyweightFactory{ ? ? private HashMap data;
? ? public FlyweightFactory(){ data = new HashMap<>();}
? ? public Flyweight getFlyweight(Object object){
? ? ? ? if ( data.containsKey(object)){
? ? ? ? ? ? return data.get(object);
? ? ? ? }else {
? ? ? ? ? ? Flyweight flyweight = new Flyweight(object);
? ? ? ? ? ? data.put(object,flyweight);
? ? ? ? ? ? return flyweight;
? ? ? ? }
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用场景

系统中存在大量的相似对象。

细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。

需要缓冲池的场景。

22.桥梁模式:将抽象和实现解耦,使得两者可以独立地变化。
Circle类将DrwaApi与Shape类进行了桥接

interface DrawAPI {
? ? public void drawCircle(int radius, int x, int y);
}
class RedCircle implements DrawAPI {
? ? @Override
? ? public void drawCircle(int radius, int x, int y) {
? ? ? ? System.out.println("Drawing Circle[ color: red, radius: "
? ? ? ? ? ? ? ? + radius +", x: " +x+", "+ y +"]");
? ? }
}
class GreenCircle implements DrawAPI {
? ? @Override
? ? public void drawCircle(int radius, int x, int y) {
? ? ? ? System.out.println("Drawing Circle[ color: green, radius: "
? ? ? ? ? ? ? ? + radius +", x: " +x+", "+ y +"]");
? ? }
}
abstract class Shape {
? ? protected DrawAPI drawAPI;
? ? protected Shape(DrawAPI drawAPI){
? ? ? ? this.drawAPI = drawAPI;
? ? }
? ? public abstract void draw();
}
class Circle extends Shape {
? ? private int x, y, radius;
? ? public Circle(int x, int y, int radius, DrawAPI drawAPI) {
? ? ? ? super(drawAPI);
? ? ? ? this.x = x;
? ? ? ? this.y = y;
? ? ? ? this.radius = radius;
? ? }
? ? public void draw() {
? ? ? ? drawAPI.drawCircle(radius,x,y);
? ? }
}
//客户端使用代码
Shape redCircle = new Circle(100,100, 10, new RedCircle()); Shape greenCircle = new Circle(100,100, 10, new GreenCircle()); redCircle.draw(); greenCircle.draw();?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
使用场景

不希望或不适用使用继承的场景

接口或抽象类不稳定的场景

重用性要求较高的场景

23.模板方法模式:定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
使用场景

多个子类有公有的方法,并且逻辑基本相同时。

重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。

重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见“模板方法模式的扩展”)约束其行为。

本文地址:http://fmiwue.riyuangf.com/news/18276.html    迅易网 http://fmiwue.riyuangf.com/ , 查看更多
 
 
更多>同类行业资讯
0相关评论

新闻列表
企业新闻
推荐企业新闻
推荐图文
推荐行业资讯
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  鄂ICP备2020018471号