Java面试~基础

一、Java基础

1、基础知识
1.1重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同时,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类。访问修饰符范围大于等于父类,如果父类方法访问修饰符为private则子类就不能重写该方法。
1.2 String和StringBuffer,StringBuilder的区别是什么?String为什么是不可变的?
可变性:
简单的来说:String类中使用final关键字字符数组保存字符串,private final char value[] ,所以String对象时不可变的。而StringBuilder和StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串 char value[] 但是没有用fianl修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是
AbstractStringBuilder 实现的.

AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}

线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义
了一些字符串的基本操作,如 expandCapacity.append.insert.indexOf 等公
共 方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以
是线程安全的。StringBuilder 并没有对
方法进行加同步锁,所以是非线程安全的。
性能

abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];}

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然
后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新
的对象并改变对象引用。相同情况下使用 StirngBuilder 相比使用
StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的
风险。
对于三者使用的总结:

  1. 操作少量的数据 = String
  2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据 = StringBuffer
    1.3 自动装箱与拆箱
    装箱:将基本类型用它们对应的引用类型包装起来;
    拆箱:将包装类型转换为基本数据类型;
    1.4 == 与 equals
    == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不
    是同一个对象。(基本数据类型比较的是值,引用数据类型比较的是内存
    地址)
    equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情
    况:
    情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对
    象时,等价于通过“==”比较这两个对象。
    情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两
    个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b 为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}

说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是
比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的
值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池
中重新创建一个 String 对象。
1.5 关于 final 关键字的一些总结
final 关键字主要用在三个地方:变量、方法、类。

  1. 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始
    化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  2. 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成
    员方法都会被隐式地指定为 final 方法。
  3. 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承
    类修改它的含义;第二个原因是效率。
    在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过
    于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为
    final。

    1.6 Object 类的常见方法总结
    Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
    方法 1: public final native Class getClass()
    // native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键
    字修饰,故不允许子类重写。
    方法 2: public native int hashCode()
    // native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK
    中的 HashMap。
    方法 3:public boolean equals(Object obj)
    // 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写
    用户比较字符串的值是否相等。
    方法 4:protected native Object clone() throws CloneNotSupportedException
    // naitive 方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于
    任何对象 x,表达式 x.clone() != x 为 true,x.clone().getClass() == x.getClass() 为true。Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常。
    方法 5:public String toString()
    // 返回类的名字@实例的哈希码的 16 进制的字符串。建议 Object 所有的
    子类都重写这个方法。
    方法 6:public final native void notify()
    // native 方法,并且不能重写。必须锁对象调用该方法,不会释放锁资源,唤醒一个在此对象监视器上等待的线程(监
    视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
    方法 7: public final native void notifyAll()
    // native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在
    此对象监视器上等待的所有线程,而不是一个线程。
    方法 8:public final native void wait(long timeout) throws InterruptedException
    // native 方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释
    放锁,而 wait 方法释放了锁 。timeout 是等待时间。
    方法 9:public final void wait(long time, int nanos) throws InterruptedException
    // 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是
    0-999999)。 所以超时的时间还需要加上 nanos 毫秒。
    方法 10:public final void wait() throws InterruptedException
    // 跟之前的 2 个 wait 方法一样,只不过该方法一直等待,没有超时时间这
    个概念。必须是锁对象调用,会释放锁资源。
    方法 11:protected void finalize() throws Throwable { }
    // 实例被垃圾回收器回收的时候触发的操作。
    1.7 Java 中的异常处理
    在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的
    Throwable 类。Throwable: 有两个重要的子类:
    Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要
    子类,各自都包含大量子类。
    Java面试~基础_第1张图片

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。
大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟
机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当
JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。
这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身.或者发生在虚拟机试图执行应用时,
如 Java 虚拟机运行错误(Virtual MachineError).类定义错误
(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的
控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设
计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重
要的子类 RuntimeException。
RuntimeException 异常由 Java 虚拟机抛出。NullPointerException(要
访问的变量没有引用任何对象时,抛出该 异常).ArithmeticException(算术
运算异常,一个整数除以 0 时,抛出该异常)和
ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
Throwable 类常用方法

public string getMessage():返回异常发生时的详细信息
public string toString():返回异常发生时的简要描述
public string getLocalizedMessage():返回异常对象的本地化信息。使
用 Throwable 的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖
该方法,则该方法返回的信息与 getMessage()返回的结果相同
public void printStackTrace():在控制台上打印 Throwable 对象封装的
异常信息
异常处理总结
try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,
则必须跟一个 finally 块。
catch 块:用于处理 try 捕获到的异常。
finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当
在 try 块或 catch 块中遇到 return 语句
时,finally 语句块将在方法返回之前被执行。
在以下 4 种特殊情况下,finally 块不会被执行:

  1. 在 finally 语句块中发生了异常。
  2. 在前面的代码中用了 System.exit()退出程序。
  3. 程序所在的线程死亡。
  4. 关闭 CPU。
    1.8 获取用键盘输入常用的的两种方法
    方法 1:通过 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();

方法 2:通过 BufferedReader

BufferedReader input = new BufferedReader(new
InputStreamReader(System.in));
String s = input.readLine();

1.9 接口和抽象类的区别是什么

  1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始
    接口方法可以有默认实现),抽象类可以有非抽象的方法
  2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
  3. 一个类可以实现多个接口,但最多只能实现一个抽象类
  4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
  5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口
    的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
    备注:在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实
    现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。
    1.10 JVM 调优及内存泄漏
    1.10.1 JVM 调优工具
    Jconsole,JProfile,VisualVM
    Jconsole : JDK 自带,功能简单,但是可以在系统有一定负荷的情况下使
    用。对垃圾回收算法有很详细的跟踪。
    JProfiler:商业软件,需要付费。功能强大。
    VisualVM:JDK 自带,功能强大,与 JProfiler 类似。推荐。
    1.10.2 如何调优
    观察内存释放情况.集合类检查.对象树
    上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功

    堆信息查看
    Java面试~基础_第2张图片

可查看堆空间大小分配(年轻代.年老代.持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)
Java面试~基础_第3张图片

查看堆内类.对象信息查看:数量.类型等
Java面试~基础_第4张图片

对象引用情况查看
有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:

  • 年老代年轻代大小划分是否合理
  • 内存泄漏
  • 垃圾回收法设置是否合理
    1.10.3 线程监控
    Java面试~基础_第5张图片

线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下
Java面试~基础_第6张图片

Dump 线程详细信息:查看线程内部运行情况
死锁检查
热点分析

Java面试~基础_第7张图片

CPU 热点:检查系统哪些方法占用的大量 CPU 时间
内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对
象一起统计)
这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的
进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用
眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时
刻,对象(或类.线程等)的不同,以便快速找到问题
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下
来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后
对比两次快照的对象情况。
1.10.4 内存泄漏检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一
下,而线程.热点方面的问题则是具体问题具体分析了。
内存泄漏一般可以理解为系统资源(各方面的资源,堆.栈.线程等)在错误
使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资
源分配请求无法完成,引起系统错误。
内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。
需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最
终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是
系统确实没有那么多资源可以分配了(其他的资源都在使用)。
年老代堆空间被占满
异常: java.lang.OutOfMemoryError: Java heap space
说明:Java面试~基础_第8张图片

这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对
象占满,虚拟机无法再在分配新空间。
如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都
是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有
谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间
被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄
漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小
时或者几天)
解决:这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
持久代被占满
异常:java.lang.OutOfMemoryError: PermGen space
说明:Perm 空间被占满。无法为新的 class 分配存储空间而引发的异常。
这个异常以前是没有的,但是在 Java 反射大量使用的今天这个异常比较常见了。
主要原因就是大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。
更可怕的是,不同的 classLoader 即便使用了相同的类,但是都会对其
进行加载,相当于同一个东西,如果有 N 个 classLoader 那么他将会被加载 N
次。因此,某些情况下,这个问题基本视为无解。当然,存在大量 classLoader
和大量反射类的情况其实也不多。
解决

  1. -XX:MaxPermSize=16m
  2. 换用 JDK。比如 JRocket
    堆栈溢出
    异常:java.lang.StackOverflowError
    说明:这个就不多说了,一般就是递归没返回,或者循环调用造成
    线程堆栈满
    异常:Fatal: Stack size too small
    说明:java 中一个线程的空间大小是有限制的。JDK5.0 以后这个值是 1M。
    与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上
    面异常。
    解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看
    代码部分是否有造成泄漏的部分。
    系统内存被占满
    异常:java.lang.OutOfMemoryError: unable to create new native thread
    说明:这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系
    统创建线程时,除了要在 Java 堆中分配内存外,操作系统本身也需要分配资源
    来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操
    作系统分配不出资源来了,就出现这个异常了。
    分配给 Java 虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内
    存固定时,分配给 Java 虚拟机的内存越多,那么,系统总共能够产生的线程也
    就越少,两者成反比的关系。同时,可以通过修改-Xss 来减少分配给单个线程
    的空间,也可以增加系统总共内生产的线程数。
    解决
  3. 重新设计系统减少线程数量。
  4. 线程数量不能减少的情况下,通过-Xss 减小单个线程大小。以便能生产
    更多的线程
    1.11 JVM 内存管理
    JVM 将内存划分为 6 个部分:PC 寄存器(也叫程序计数器).虚拟机栈.堆. 方法区.运行时常量池.本地方法栈
    Java面试~基础_第9张图片

PC 寄存器(程序计数器):用于记录当前线程运行时的位置,每一个线程
都有一个独立的程序计数器,线程的阻塞.恢复.挂起等一系列操作都需要程
序计数器的参与,因此必须是线程私有的。
java 虚拟机栈:在创建线程时创建的,用来存储栈帧,因此也是线程私有
。java 程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的
临时数据和中间结果,包括局部变量表.操作数栈.动态链接.方法出口等信息。
这些栈帧就存储在栈中。如果栈深度大于虚拟机允许的最大深度,则抛出 S
tackOverflowError 异常。
局部变量表:方法的局部变量列表,在编译时就写入了 class 文件
操作数栈:int x = 1; 就需要将 1 压入操作数栈,再将 1 赋值给变量 x
java 堆java 堆被所有线程共享,堆的主要作用就是存储对象。如果堆空间不够,但扩展时又不能申请到足够的内存时,则抛出 OutOfMemoryError 异常。

StackOverflowError OutOfMemoryError
java 栈 java 堆
栈深度超过范围了(比如:递归层数太多了) 内存空间不够了(需要及时释放内存)

方法区:方发区被各个线程共享,用于存储静态变量.运行时常量池等信息。
本地方法栈:本地方法栈的主要作用就是支持 native 方法,比如在 java 中
调用 C/C+
1.12 GC 回收机制

  1. 哪些内存需要回收?—— who
  2. 什么时候回收?—— when
  3. 怎么回收?—— how
    1. 哪些内存需要回收?
    Java 堆方法区的内存
    Java面试~基础_第10张图片

2. 什么时候回收?
引用计数法
可达性分析
2.1 引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一。反
之每当一个引用失效时,计数器减一。当计数器为 0 时,则表示对象不被引用。
举个例子:

Object a = new Object(); // a 的引用计数为 1
a = null; // a 的引用计数为 0,等待 GC 回收

但是,引用计数法不能解决对象之间的循环引用,见下例

Object a = new Object(); // a 的引用计数为 1
Object b = new Object(); // b 的引用计数为 1
a.next = b; // a 的引用计数为 2
b.next = a; // b 的引用计数为 2
a = null; // a 的引用计数为 1,尽管已经显示地将 a 赋值为 null,但是由于引用计数为 1,GC 无法回收 a
b = null; // b 的引用计数为 1,同理,GC 也不回收 b

2.2 可达性分析
设立若干根对象(GC Root),每个对象都是一个子节点,当一个对象找
不到根时,就认为该对象不可达。
Java面试~基础_第11张图片

没有一条从根到 Object4 和 Object5 的路径,说明这两个对象到根是不可
达的,可以被回收
补充:java 中,可以作为 GC Roots 的对象包括:
 java 虚拟机栈中引用的对象
 方法区中静态变量引用的对象
 方法区中常量引用的对象
 本地方法栈中引用的对象
3. 怎么回收?
标记-清除算法
复制算法
标记-整理算法
分代算法
3.1 标记——清除算法
遍历所有的 GC Root,分别标记处可达的对象和不可达的对象,然后将不
可达的对象回收。
缺点是:效率低.回收得到的空间不连续
Java面试~基础_第12张图片

3.2 复制算法
将内存分为两块,每次只使用一块。当这一块内存满了,就将还存活的对象
复制到另一块上,并且严格按照内存地址排列,然后把已使用的那块内存统一回
收。
优点是:能够得到连续的内存空间
缺点是:浪费了一半内存
Java面试~基础_第13张图片

3.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对
象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外
的内存,“标记-整理”算法的示意图如下图所示。
Java面试~基础_第14张图片
标记-整理算法是一种老年代的回收算法,它在标记-清除算法的基础上做了
一些优化。也首先需要从根节点开始对所有可达对象做一次标记,但之后,它并
不简单地 清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
3.3 分代算法
在 java 中,把内存中的对象按生命长短分为:
新生代:活不了多久就 go die 了,比如局部变量
老年代:老不死的,活的久但也会 go die,比如一些生命周期长的对象
永久代:千年王八万年龟,不死,比如加载的 class 信息
有一点需要注意:新生代和老年代存储在 java 虚拟机堆上 ;永久代存储
在方法区上
Java面试~基础_第15张图片
补充:java finalize()方法:
在被 GC 回收前,可以做一些操作,比如释放资源。有点像析构函数,但是
一个对象只能调用一次 finalize()方法。
2. Java 集合框架
2.1 ArrayList 与 LinkedList 异同
1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不
保证线程安全;
2. 底层数据结构: ArrayList 底层使用的是 Object 数组;LinkedList 底
层使用的是双向链表数据结构(JDK1.6 之 前为循环链表,JDK1.7 取消了循环。
注意双向链表和双向循环链表的区别:);
3. 插入和删除是否受元素位置的影响① ArrayList 采用数组存储,所
以插入和删除元素的时间复杂度受元素位置的影响。
比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而
ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于
get(int index) 方法)。
5. 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会
预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需
要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
补充内容:RandomAccess 接口

public interface RandomAccess {
}

查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,
在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这
个接口的类具有随机访问功能。
在 binarySearch()方法中,它要判断传入的 list 是否 RamdomAccess
的实例,如果是,调用
indexedBinarySearch()方法,如果不是,那么调用 iteratorBinarySearch()方法

public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为
什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而
LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所
以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间
复杂度为 O(n),所以不支持快速随机访问。,
ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功
能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现
RandomAccess 接口才具有快速随机访问功能的!
下面再总结一下 list 的遍历方式选择:
实现了 RandomAccess 接口的 list,优先选择普通 for 循环 ,其次 foreach, 未实现 RandomAccess 接口的 list, 优先选择 iterator 遍历(foreach 遍历底
层也是通过 iterator 实现的),大 size 的数据,千万不要使用普通 for 循环.补
充:数据结构基础之双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,
分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可
以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表,如
下图所示,同时下图也是 LinkedList 底层使用的是双向循环链表数据结构。
Java面试~基础_第16张图片

2.2 ArrayList 与 Vector 区别
Vector 类的所有方法都是同步的。可以由两个线程安全地访问一个 Vector
对象.但是一个线程访问 Vector 的话代码要在同步操作上耗费大量的时间。
ArrayList不是同步的,所以在不需要保证线程安全时时建议使用ArrayList。
2.3 HashMap 的底层实现
JDK1.8 之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链
表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到
hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指
的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素
的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉
链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就
是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰
动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不
变。
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是 hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以 0 补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK1.7 的 HashMap 的 hash 方法源码. static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一
点点,因为毕竟扰动了 4 次。所谓 “拉链法” 就是:将链表和数组相结合。
也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希
冲突,则将冲突的值加到链表中即可。
Java面试~基础_第17张图片

JDK1.8 之后
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链
表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。
Java面试~基础_第18张图片

2.4 HashMap 和 Hashtable 的区别
1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全
的;HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap 吧!);
2. 效率:因为线程安全的问题,HashMap 要比 HashTable 效率高一点。
另外,HashTable 基本被淘汰,不要在代码中使用它;
3. 对 Null key 和 Null value 的支持: HashMap 中,null 可以作为键,
这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个null,直接抛出 NullPointerException。
4. 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量
初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的
2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2
倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大
小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的
tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总是使
用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
5. 底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大
的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜
索时间。Hashtable 没有这样的机制。
HasMap 中带有初始容量的构造函数

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。

/**
* Returns a power of two size for the given target capacity. */
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

2.5 HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配
均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到
2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均
匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是
放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度
取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组
下标的计算方法是“ (n - 1) & hash ”。(n 代表数组长度)。这也就解释了
HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)
操作中如果除数是 2 的幂次则等价于与其
除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前
提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap的长度为什么是 2 的幂次方。
2.6 HashMap 多线程操作导致死循环问题
在多线程下,进行 put 操作会导致 HashMap 死循环,原因在于
HashMap 的扩容 resize()方法。由于扩容是新建一个数组,复制原数据到数组。
由于数组下标挂有链表,所以需要复制链表,但是多线程操作有可能导致环形链
表。复制链表过程如下:
以下模拟 2 个线程同时扩容。假设,当前 HashMap 的空间为 2(临界值
为 1),hashcode 分别为 0 和 1,在散列地 址 0 处有元素 A 和 B,这时
候要添加元素 C,C 经过 hash 运算,得到散列地址为 1,这时候由于超过了
临界值,空间不够,需要调用 resize 方法进行扩容,那么在多线程条件下,会
出现条件竞争,模拟过程如下:
线程一:读取到当前的 HashMap 情况,在准备扩容时,线程二介入
Java面试~基础_第19张图片

这个过程为,先将 A 复制到新的 hash 表中,然后接着复制 B 到链头(A
的前边:B.next=A),本来 B.next=null,到此也就结束了(跟线程二一样的
过程),但是,由于线程二扩容的原因,将 B.next=A,所以,这里继续复制 A,让 A.next=B,由此,环形链表出现:B.next=A; A.next=B
注意:JDK1.8 已经解决了死循环的问题。
2.7 HashSet 和 HashMap 区别
如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于
HashMap 实现的
。(HashSet 的源码非常非常少,因为除了 clone() 方
法.writeObject()方法.readObject()方法是 HashSet 自己不得不实现之外,其
他方法都是直接调用 HashMap 中的方法。)
Java面试~基础_第20张图片

2.8 ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的
方式上不同。
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组
+链表
实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链
表/红黑二叉树
Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类
似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为
了解决哈希冲突而存在的;
实现线程安全的方式(重要): ① 在 JDK1.7 的时候,
ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),
每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不
会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment
的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制
使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁
做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在
JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容
旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率
非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入
阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,
也不能使用 get,竞争会越来越激烈效率越低。
Java面试~基础_第21张图片
Java面试~基础_第22张图片

2.9 ConcurrentHashMap 线程安全的具体实现方式/底层
具体实现
JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程
占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结
构组成。
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演
锁的角色。HashEntry 用于存储键值对数据。
static class Segment extends ReentrantLock implements
Serializable { } 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结
构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个
HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守
护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,
必须首先获得对应的 Segment 的锁。
JDK1.8 (上面有示意图)
ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和
synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+
链表/红黑二叉树。
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不
冲突,就不会产生并发,效率又提升 N 倍。
2.10 集合框架底层数据结构总结
Collection

  1. List
    ArrayList: Object 数组
    Vector: Object 数组
    LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环) 详
    细可阅读 JDK1.7-LinkedList 循环链表优化
  2. 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: 红黑树(自平衡的排序二叉树)
    3. Java 多线程
    关于 Java 多线程,在面试的时候,问的比较多的就是①悲观锁和乐观锁
    ( 具体百度:面试必备之乐观锁与悲观锁).②synchronized
    和 lock 区别以及 volatile 和 synchronized 的区别,③可重入锁与非可重入锁
    的区别.④多线程是解决什么问题的.⑤线程池解决什么问题.⑥线程池的原理.⑦
    线程池使用时的注意事项.⑧AQS 原有没有在项目中实际使用多线程的经历
    。所以,如果你在你的项目中有实际使用 Java 多线程的经历 的话,会为你加分不少
    哦!
    3.1 synchronized 关键字的 5 连击
    3.1.1 说一说自己对于 synchronized 关键字的了解

    synchronized 关键字解决的是多个线程之间访问资源的同步性,
    synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一
    个线程执行。
    另外,在 Java 早期版本中,synchronized 属于重量级锁,效率
    低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来
    实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤
    醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要
    从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本
    相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在
    Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在
    的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的
    优化,如自旋锁.适应性自旋锁.锁消除.锁粗化.偏向锁.轻量级锁等技术来减少锁
    操作的开销。
    3.1.2 说说自己是怎么使用 synchronized 关键字,在项目中用到
    了吗?

    synchronized 关键字最主要的三种使用方式:
    修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象
    实例的锁
    修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象
    的锁
    。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属
    于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管
    new 了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果
    一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调
    用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥
    现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
    修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给
    定对象的锁。
    和 synchronized 方法一样,synchronized(this)代码块也是锁
    定当前对象的。synchronized 关键字加到 static 静态方法和
    synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:
    synchronized 关键字加到非 static 静态方法上是给对象实例上锁。另外需要注
    意的是:尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量
    池具有缓冲功能!下面我已一个常见的面试题为例讲解一下 synchronized
    关键字的具体使用。
    面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一
    下双重检验锁方式实现单例模式的原理呗!”

    双重校验锁实现对象单例(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码另外,需要注意 uniqueInstance 采用volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址
    但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回uniqueInstance,但此时uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。饿汉式本身是线程安全的,懒汉式需要加锁。
    3.1.3 讲一下 synchronized 关键字的底层原理
    synchronized 关键字底层原理属于 JVM 层面。

    ① synchronized 同步语句块的情况
    通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节
    码信息:首先切换到类的对应目录执行 javacSynchronizedDemo.java 命令生
    成编译后的 .class 文件,然后执行 javap -c -s -v -l SynchronizedDemo.class 。
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}

从上面我们可以看出:
synchronized 同步语句块的实现使用的是 monitorenter 和
monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,
monitorexit 指令则指明同步代码块的结束位置
。 当执行 monitorenter 指
令时,线程试图获取锁也就是获取 monitor(monitor 对象存在于每个 Java 对
象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java
中任意对象可以作为锁的原因) 的持有权.当计数器为 0 则可以成功获取,获取
后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计
数器设为 0,表明锁被释放。如果获取对象锁失败,那当
前线程就要阻塞等待,直到锁被另外一个线程释放为止。
② synchronized 修饰方法的的情况

public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
3.1.4 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些
优化,可以详细介绍一下这些优化吗?

JDK1.6 对锁的实现引入了大量的优化,如偏向锁.轻量级锁.自旋锁.适应性自旋锁.锁消除.锁粗化等技术来减少锁操作的开销。锁主要存在四种状态,依次是:无锁状态.偏向锁状态.轻量级锁状态.重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
3.1.5 谈谈 synchronized 和 ReenTrantLock 的区别
(1)两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
(2) synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合try/fifinally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
(3)ReenTrantLock 比 synchronized 增加了一些高级功能
相比 synchronized,ReenTrantLock 增加了一些高级功能。主要来说主要有三点:等待可中断;可实现公平锁; 可实现选择性通知(锁可以绑定多个条件)ReenTrantLock 提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以
选择放弃等待,改为处理其他事情。
ReenTrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock 默认情况是非公平的,可以通过ReenTrantLock 类的 ReentrantLock(boolean fair) 构造方法来制定是否是公平的。synchronized 关键字与 wait()和 notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock 类当然也 可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。Condition 是 JDK1.5 之后才有
的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用 notify/notifyAll()方法进行通知时,被通知的线程是由 JVM选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized 关键字就相当于整个 Lock 对象中只有一个Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll()方法 只会唤醒注册在
该 Condition 实例中的所有等待线程。如果你想使用上述功能,那么选择 ReenTrantLock 是一个不错的选择。
3.2 线程池的 4 连击
3.2.1 讲一下 Java 内存模型

在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变
量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变
量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就
可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它
在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变
量是不稳定的,每次使用它都到主存中进行读取。说白了, volatile 关键字的
主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
3.2.2 说说 synchronized 关键字和 volatile 关键字的区别
synchronized 关键字和 volatile 关键字比较
volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比
synchronized 关键字要好。但是 volatile 关键字只能用于变量而
synchronized 关键字可以修饰方法以及代码块。synchronized 关键字在
JavaSE1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引
入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发
中使用 synchronized 关键字的场景还是更多一些。
多线程访问 volatile 关键字不会发生阻塞,而 synchronized 关键字可能
会发生阻塞

volatile 关键字能保证数据的可见性,但不能保证数据的原子性。
synchronized 关键字两者都能保证。
volatile 关键字主要用于解决变量在多个线程之间的可见性,而
synchronized 关键字解决的是多个线程之间访问资源的同步性。

3.3 线程池的 2 连击
3.3.1 为什么要用线程池?

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还
维护一些基本统计信息,例如已完成任务的数量。
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消
耗。
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即
执行。
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗
系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监
控。
3.3.2 实现 Runnable 接口和 Callable 接口的区别
如果想让线程池执行任务的话需要实现的 Runnable 接口或 Callable 接口。
Runnable 接口或 Callable 接口实现类都可以被 ThreadPoolExecutor 或
ScheduledThreadPoolExecutor 执行。两者的区别在于 Runnable 接口不会
返回结果但是 Callable 接口可以返回结果。

备注: 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之
间的相互转换。
( Executors.callable(Runnable task) 或 Executors.callable
(Runnable task,Object resule) )。
3.3.3 执行 execute()方法和 submit()方法的区别是什么呢?

  1. execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被
    线程池执行成功与否;2)submit()方法用于提交需要返回值的任务。线程池会
    返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成
    功,并且可以通过 future 的 get()方法来获取返回值,get()方法会阻塞当前线
    程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞
    当前线程一段时间后立即返回,这时候有可能任务没有执行完。
    3.3.4 如何创建线程池
    《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,
    而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确
    线程池的运行规则,规避资源耗尽的风险
    Executors 返回线程池对象的弊端如下:
    FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度
    为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
    CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
    方式一:通过构造方法实现
    方式二:通过 Executor 框架的工具类 Executors 来实现 我们可以创建三
    种类型的 ThreadPoolExecutor

    FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池
    中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则
    立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,
    便处理在任务队列中的任务。
    SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余
    一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按
    先入先出的顺序执行队列中的任务。
    CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线
    程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复
    用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任
    务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
    对应 Executors 工具类中的方法如图所示:
    3.4 Atomic 原子类的 4 连击
    3.4.1 介绍一下 Atomic 原子类

    Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物
    质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操
    作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不
    会被其他线程干扰。所以,所谓原子类说简单点就是具有原子/原子操作特征的
    类。并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic 下,
    3.4.2 JUC 包中的原子类是哪 4 类?
    基本类型

    使用原子的方式更新基本类型
    AtomicInteger:整形原子类
    AtomicLong:长整型原子类
    AtomicBoolean :布尔型原子类
    数组类型
    使用原子的方式更新数组里的某个元素
    AtomicIntegerArray:整形数组原子类
    AtomicLongArray:长整形数组原子类
    AtomicReferenceArray :引用类型数组原子类
    引用类型
    AtomicReference:引用类型原子类
    AtomicStampedRerence:原子更新引用类型里的字段原子类
    AtomicMarkableReference :原子更新带有标记位的引用类型
    对象的属性修改类型
    AtomicIntegerFieldUpdater:原子更新整形字段的更新器
    AtomicLongFieldUpdater:原子更新长整形字段的更新器
    AtomicStampedReference :原子更新带有版本号的引用类型。该类将整
    数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
    3.4.3 讲讲 AtomicInteger 的使用
    AtomicInteger 类常用方法
    AtomicInteger 类的使用示例
    使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线
    程安全。
    3.4.4 能不能给我简单介绍一下 AtomicInteger 类的原理
    AtomicInteger 线程安全原理简单分析
    AtomicInteger 类的部分源码:
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用 AtomicInteger 之后,不需要对该方法加锁,也可以实现线程安全。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的
值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用
来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一
个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿
到该变量的最新值。
3.5 AQS
3.5.1 AQS 介绍

AQS 的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks 包下面。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出
应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其
他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等
皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合
我们自己需求的同步器。
3.5.2 AQS 原理分析

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;AQS 原理这部分参考了部分博客,在 5.2 节末尾放了链接。
在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于
AQS 原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一
定要假如自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来
而不是背出来。
下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃
力一点,感兴趣的话可以看看源码。
3.5.3 AQS 原理概览
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程
设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资
源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制
AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双
向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求
共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁
的分配。看个 AQS(AbstractQueuedSynchronizer)原理图:
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成
获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其
值的修改。
private volatile int state;//共享变量,使用 volatile 修饰保证线程可见性
状态信息通过 procted 类型的 getState,setState,compareAndSetState 进行操作//返回同步
状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS 操作)将同步状态值设置为给定值 update 如果当前同步状态的值等于 expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3.5.4 AQS 对资源的共享方式
AQS 定义两种资源共享方式

Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为
公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如 Semaphore/CountDownLatch。
Semaphore. CountDownLatCh. CyclicBarrier.ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为
ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要
实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如
获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。
3.5.5 AQS 底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是
这样(模板方法模式很经典的一个应用):

  1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些
    重写方法很简单,无非是对于共享资源 state 的获取和释放)
  2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模
    板方法会调用使用者重写的方法。这和我们以往通过实现接口的方式有很大区
    别,这是模板方法模式很经典的一个运用。
    AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的
    模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到 condition 才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回 true,失败则返回 false。

默认情况下,每个方法都抛出 UnsupportedOperationException 。 这
些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类
中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其
他类使用。以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线
程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再
tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,
其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此
锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始
化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线
程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到
所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程
就会从 await()函数返回,继续后余动作。一般来说,自定义同步器要么是独占
方法,要么是共享方式,他们也只需实现 tryAcquire-tryRelease . tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定
义同步器同时实现独占和共享两种方式。
如 ReentrantReadWriteLock 。
推荐两篇 AQS 原理和相关源码分析的文章:
http://www.cnblogs.com/waterystone/p/4920797.html
https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
3.5.6 AQS 组件总结
Semaphore(信号量)-允许多个线程同时访问
: synchronized 和
ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch (倒计时器): CountDownLatch 是一个同步工具
类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类
似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更
加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字
面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线
程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,
屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏
障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了
屏障,然后当前线程被阻塞。
二. Java Web
1.JDBC 技术

1.1 说下原生 JDBC 操作数据库流程?
第一步:Class.forName()加载数据库连接驱动;
第二步:DriverManager.getConnection()获取数据连接对象;
第三步:根据 SQL 获取 sql 会话对象,有 2 种方式
Statement.PreparedStatement ;
第四步:执行 SQL 处理结果集,执行 SQL 前如果有参数值就设置参数值
setXXX();
第五步:关闭结果集.关闭会话.关闭连接。
1.2 说说事务的概念,在 JDBC 编程中处理事务的步骤。

  1. 事务是作为单个逻辑工作单元执行的一系列操作。
  2. 一个逻辑工作单元必须有四个属性,称为原子性.一致性.隔离性和持久性
    (ACID) 属性,只有这样才能成为一个事务处理步骤:
  3. conn.setAutoComit(false);设置提交方式为手工提交
  4. conn.commit()提交事务
  5. 出现异常,回滚 conn.rollback();
    1.3 JDBC 的脏读是什么?哪种数据库隔离级别能防止脏
    读?

    当我们使用事务时,有可能会出现这样的情况,有一行数据刚更新,与此同
    时另一个查询读到了这个刚更新的值。这样就导致了脏读,因为更新的数据还没
    有进行持久化,更新这行数据的业务可能会进行回滚,这样这个数据就是无效的。
    数据库的 TRANSACTIONREADCOMMITTED ,TRANSACTIONREPEATABLEREAD 和 TRANSACTION_SERIALIZABLE 隔离
    级别可以防止脏读
    2.网路通讯部分
    2.1 TCP 与 UDP 区别?

    UDP:
    a.是面向无连接, 将数据及源的封装成数据包中,不需要建立连接
    b.每个数据报的大小在限制 64k 内
    c.因无连接,是不可靠协议
    d.不需要建立连接,速度快
    TCP:
    a.建立连接,形成传输数据的通道. b.在连接中进行大数据量传输,以字节流方式
    c.通过三次握手完成连接,是可靠协议
    d 必须建立连接效率会稍低.聊天.网络视频会议就是 UDP
    2.2 说一下什么是 Http 协议?
    客户端和 服务器端之间数据传输的格式规范,格式简称为“超文本传输协
    议”。 是一个基于请求与响应模式的.无状态的.应用层的协议,基于 TCP 的连
    接方式

    2.3 get 与 post 请求区别?
    区别 1:
    get 重点在从服务器上获取资源,post 重点在向服务器发送数据;
    区别 2:
    get 传输数据是通过 URL 请求,以 field(字段)= value 的形式,
    置于 URL 后,并用"?“连接,多个请求数据间用”&"连接,如
    http://127.0.0.1/Test/login.action?name=admin&password=admin,这个
    过程用户是可见的;
    post 传输数据通过 Http 的 post 机制,将字段与对应值封存在请求实体中
    发送给服务器,这个过程对用户是不可见的;
    区别 3:
    Get 传输的数据量小,因为受 URL 长度限制,但效率较高;
    Post 可以传输大量数据,所以上传文件时只能用 Post 方式;
    区别 4:
    Get 是不安全的,因为 URL 是可见的,可能会泄露私密信息,如密码等;
    Post 较 get 安全性较高;
    区别 5:
    get 方式只能支持 ASCII 字符,向服务器传的中文字符可能会乱码。
    post 支持标准字符集,可以正确传递中文字符。
    2.4 http 中重定向和请求转发的区别?
    本质区别:转发是服务器行为,重定向是客户端行为。
    重定向特点:两次请求,浏览器地址发生变化,可以访问自己 web 之外的
    资源,传输的数据会丢失。
    请求转发特点:一次请求,浏览器地址不变,访问的是自己本身的 web 资
    源,传输的数据不会丢失。
    3. Cookie 和 Session
    Cookie 是 web 服务器发送给浏览器的一块信息,浏览器会在本地一个文
    件中给每个 web 服务器存储 cookie。以后浏览器再给特定的 web 服务器发
    送请求时,同时会发送所有为该服务器存储的 cookie。
    Session 是存储在 web 服务器端的一块信息。session 对象存储特定用户
    会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储
    在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。
    Cookie 和 session 的不同点:
    1.无论客户端做怎样的设置,session 都能够正常工作。当客户端禁用
    cookie 时将无法使用 cookie。
    2.在存储的数据量方面:session 能够存储任意的 java 对象,cookie 只
    能存储 String 类型的对象。
    4.Jsp 和 Servlet
    4.1 Servlet 的执行流程

    Servlet 的执行流程也就是 servlet 的生命周期,当服务器启动的时候生
    命周期开始,然后通过 init()《启动顺序根据 web.xml 里的 startup-on-load 来
    确定加载顺序》方法初始化 servlet,再根据不同请求调用 doGet 或 doPost 方
    法,最后再通过 destroy()方法进行销毁。
    4.2 doGet 和 doPost 的区别
    doGet 和 doPost 都是接受用户请求的方法,doGet 处理 get 请求,doPost
    处理 post 请求,doGet 用于地址栏提交,doPost 用于表单提交,在页面提交
    数据时,get 的数据大小有限制 4k,post 没有限制,get 请求提交的数据会在
    地址栏显示,post 不显示,所以 post 比 get 安全。
    4.3 Jsp 和 Servlet 的区别
    你可以将 JSP 当做一个可扩充的 HTML 来对待。
    虽然在本质上 JSP 文件会被服务器自动翻译为相应的 Servlet 来执行。
    可以说 Servlet 是面向 Java 程序员而 JSP 是面向 HTML 程序员的,除此之
    外两者功能完全等价。
    4.4 JSP 九大内置对象
    pageContext :只对当前 jsp 页面有效,里面封装了基本的 request
    和 session 的对象
    Request :对当前请求进行封装
    Session :浏览器会话对象,浏览器范围内有效
    Application :应用程序对象,对整个 web 工程都有效
    Out :页面打印对象,在 jsp 页面打印字符串
    Response :返回服务器端信息给用户
    Config :单个 servlet 的配置对象,相当于 servletConfig 对象
    Page :当前页面对象,也就是 this
    Exception :错误页面的 exception 对象,如果指定的是错误页面,这
    个就是异常对象
    4.5 JSP 的三大指令:
    Page :指令是针对当前页面的指令
    Include :用于指定如何包含另一个页面
    Taglib :用于定义和指定自定义标签
    4.6 七大动作
    Forward,执行页面跳转,将请求的处理转发到另一个页面
    Param :用于传递参数
    Include :用于动态引入一个 jsp 页面
    Plugin :用于下载 javaBean 或 applet 到客户端执行
    useBean :使用 javaBean
    setProperty :修改 javaBean 实例的属性值
    getProperty :获取 javaBean 实例的属性值
    5. Ajax &Jquery
    5.1 谈谈你对 Ajax 的认识?
    Ajax 是一种创建交互式网页应用的的网页开发技术;Asynchronous
    JavaScript and XML”的缩写。
    Ajax 的优势:
    通过异步模式,提升了用户体验。
    优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带
    宽占用。
    Ajax 引擎在客户端运行,承担了一部分本来由服务器承担的工作,从而
    减少了大用户量下的服务器负载。
    Ajax 的最大特点:
    可以实现局部刷新,在不更新整个页面的前提下维护数据,提升用户体
    验度。
    5.2 使用 Jquery 手写 Ajax
$.ajax({
url:'http://www.baidu.com', 
type:'POST', 
data:data, 
cache:true, 
headers:{}, 
beforeSend:function(){},
 success:function(){}, 
 error:function(){}, 
 complete:function(){}
});

5.3 请简单介绍 Ajax 的使用
Ajax = 异步 JavaScript 和 XML。
Ajax 是一种用于创建快速动态网页的技术。
通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更
新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更
新。
传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面。
有很多使用 AJAX 的应用程序案例:新浪微博.Google 地图.开心网等
等。
5.4 Ajax 可以做异步请求么?
可以.ajax 请求默认是异步的.如果想同步 把 async 设置为 false 就可以
了默认是 true
如果是 jquery

$.ajax({
url: some.php, async: false, success : function(){
}
});

如果是原生的 js

xmlHttp.open("POST",url,false);

5.5 请介绍下 Jsonp 原理
jsonp 的最基本的原理是:动态添加一个

  1. .AVG 返回指定组中的平均值,空值被忽略;例:select prd_no,avg(qty)
    from sales group by prd_no
  2. COUNT 返回指定组中 项目的数量。 例:select count(*) from sales;
    2). MAX 返回指定数据的最大值;MIN 返回指定数据的最小值;SUM 返
    回指定数据的和,只能用于数字列,空值被忽略。 例:select prd_no,max(qty)
    from sales group by prd_no
    3)使用 group by 子句对数据进行分组;对 group by 子句形成的组运
    行 聚集函数计算每一组的值;最后用 having 子句去掉不符合条件的组;
    having 子 句中的每一个元素也必须出现在 select 列表中。有些数据库例外,
    如 oracle. 例:select prd_no,max(qty) from sales group by prd_no having
    prd_no>10
    3.3 SQL 之 SQL 注入
    举例:
    select admin from user where username=‘admin’ or ‘a’=‘a’ and
    passwd='‘or ‘a’=‘a’
    防止 SQL 注入,使用预编译语句是预防 SQL 注入的最佳方式,如
    select admin from user where username=?And password=?
    使用预编译的 SQL 语句语义不会发生改变,在 SQL 语句中,变量用问
    号? 表示。像上面例子中,username 变量传递的’admin’ or ‘a’=‘a’ 参数,
    也只会当 作 username 字符串来解释查询,从根本上杜绝了 SQL 注入攻击的
    发生。
    注意:使用 mybaits 时 mapper 中#方式能够很大程度防止 SQL 注入,
    $方式 无法防止 SQL 注入.
    3.4 SQL Select 语句完整的执行顺序:
    查询中用到的关键词主要包含六个,并且他们的顺序依次为
    select–from–where–group by–having–order by
    其中 select 和 from 是必须的,其他关键词是可选的,
    这六个关键词的执行顺序如下:
    from:需要从哪个数据表检索数据
    where:过滤表中数据的条件
    group by:如何将上面过滤出的数据分组
    having:对上面已经分组的数据进行过滤的条件
    select:查看结果集中的哪个列,或列的计算结果
    order by :按照什么样的顺序来查看返回的数据
    3.5 存储引擎
    1. 概念
    数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数
    据引擎进行创建.查询. 更新和删除数据。不同的存储引擎提供不同的存储机制. 索引技巧.锁定水平等功能,使用不同 的存储引擎,还可以 获得特定的功能。
    现在许多不同的数据库管理系统都支持多种不同的数据引 擎。
    存储引擎主要有: 1. MyIsam , 2. InnoDB, 3. Memory, 4. Archive, 5. Federated 。
    2. InnoDB
    InnoDB 底层存储结构为 B+树, B 树的每个节点对应 innodb 的一个
    page,page 大小是固定的, 一般设为 16k。其中非叶子节点只有键值,叶子
    节点包含完整数据, mysql 的默认存储引擎
    适用场景:
    1)经常更新的表,适合处理多重并发的更新请求。
    2)支持事务。
    3)可以从灾难中恢复(通过 bin-log 日志等)。
    4)外键约束。只有他支持外键。
    5)支持自动增加列属性 auto_increment。
    特点:
  1. 支持事务(事务是指逻辑上的一组操作,组成这组操作的各个单元,要么全成功, 要么全失败)
  2. 行级锁定(更新时一般是锁定当前行):通过索引实现,全表扫描仍然会是锁定整
    个表,注意间隙锁的影响.
  3. 读写阻塞与事务隔离级别相关.
  4. 具有非常高效的缓存特性,能缓存索引,也能缓存数据.
  5. 整个表和主键以 Cluster 方式存储,组成一颗平衡树.
  6. 所有 Secondary Index 都会保存主键信息.
  7. 支持分区,表空间.类似于 Oracle 数据库.
  8. 支持外键约束,不支持全文索引,5.5 之前支持,后面不再支持.
  9. 和 MyISAM 相比,InnoDB 对于硬件资源要求比较高.
    3. TokuDB
    TokuDB 底层存储结构为 Fractal Tree,Fractal Tree 的结构与 B+树有些
    类似, 在 Fractal Tree 中,每一个 child 指针除了需要指向一个 child 节点
    外,还会带有一个 Message Buffer ,这个 Message Buffer 是一个 FIFO 的
    队列,用来缓存更新操作。
    例如,一次插入操作只需要落在某节点的 Message Buffer 就可以马上返
    回了,并不需要搜索到叶 子节点。这些缓存的更新会在查询时或后台异步合并
    应用到对应的节点中。
    TokuDB 在线添加索引,不影响读写操作, 非常快的写入性能,
    Fractal-tree 在事务实现上有优 势。他主要适用于访问频率不高的数据或历史
    数据归档。
    4. MyIASM
    MyIASM 是 MySQL5.5 之前的默认的引擎,但是它没有提供对数据库事
    务的支持,也不支持行级锁和外键, 因此当 INSERT(插入)或 UPDATE(更新)
    数据时即写操作需要锁定整个表,效率便会低一些。
    ISAM 执行读取操作的速度很快,而且不占用大量的内存和存储资源。在设
    计之初就预想数据组织 成有固定长度的记录,按顺序存储的。—ISAM 是一种
    静态索引结构。
    特点:
  10. 不支持事务、不具备 AICD 特性(原子性、一致性、分离性、永久性);
  11. 表级别锁定形式(更新数据时锁定整个表、这样虽然可以让锁定的实现成本很
    小但是同时大大降低了其并发的性能);
  12. 读写相互阻塞(不仅会在写入的时候阻塞读取、还会在读取的时候阻塞写入、
    但是读取不会阻塞读取);
  13. 只会缓存索引(myisam 通过 key_buffer_size 来设置缓存索引,提高访问性
    能较少磁盘 IO 的压力、但是只缓存索引、不缓存数据);
  14. 读取速度快、占用资源比较少;
  15. 不支持外键约束、只支持全文检索;
  16. 是 MySQL5.5.5 版本之前的默认存储引擎;
    4. Memory
    Memory(也叫 HEAP)堆内存:使用存在内存中的内容来创建表。每个
    MEMORY 表只实际对应 一个磁盘文件。MEMORY 类型的表访问非常得快,因为它的数据是放在内存中的,并且默认使用 HASH 索引。但是一旦服务关闭,表中的数据就会丢失掉。 Memory 同时支持散列索引和 B 树索 引,B 树索引可以使用部分查询和通配查询,也可以使用和>=等操作符方便数据挖掘,散列索 引相等的比较快但是对于范围的比较慢很多。
    3.6 索引
    索引(Index)是帮助 MySQL 高效获取数据的数据结构。常见的查询算法, 顺序查找,二分查找,二 叉排序树查找,哈希散列法,分块查找,平衡多路搜索树 B
    树(B-tree)
    索引就是加快检索表中数据的方法。数据库的索引类似于书籍的索引。在书
    籍中,索引允许用户不必翻阅完整个书就能迅速地找到所需要的信息。在数据库
    中,索引也允许数据库程序迅速地找到表中的数据,而不必扫描整个数据库。
    MySQL 数据库几个基本的索引类型:普通索引.唯一索引.主键索引.全文索
    引.组合索引
    1.普通索引
    是最基本的索引,它没有任何限制。它有以下几种创建方式:
    (1)直接创建索引
1 CREATE INDEX index_name ON table(column[length]))

(2)修改表结构的方式添加索引

1 ALTER TABLE table_name ADD INDEX index_name ON (column[length]))

(3)创建表的时候同时创建索引

CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT , 
`title` char(255) CHARACTER NOT NULL , 
`content` text CHARACTER NULL , 
`time` int(10) NULL DEFAULT NULL , 
PRIMARY KEY (`id`), 
INDEX index_name (title[length])
)

(4)删除索引

1 DROP INDEX index_name ON table

2.唯一索引
与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。
如果是组合索引,则列值的组合必须唯一。它有以下几种创建方式:
(1)创建唯一索引

1 CREATE UNIQUE INDEX indexName ON table(column[length])

(2)修改表结构

1 ALTER TABLE table_name ADD UNIQUE indexName ON (column[length])

(3)创建表的时候直接指定

CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT , 
`title` char(255) CHARACTER NOT NULL , 
`content` text CHARACTER NULL , 
`time` int(10) NULL DEFAULT NULL , 
UNIQUE indexName (title[length])
);

3.主键索引
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在
建表的时候同时创建主键索引:

CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT , 
`title` char(255) NOT NULL ,
 PRIMARY KEY (`id`)
);

4.组合索引
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字
段,索引才会被使用。使用组合索引时遵循最左前缀集合

1 ALTER TABLE table ADD INDEX name_city_age (name,city,age);

5.全文索引
主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext
索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的 where 语句
的参数匹配。fulltext 索引配合 match against 操作使用,而不是一般的 where
语句加 like。它可以在 create table,alter table ,create index 使用,不过
目前只有 char.varchar,text 列上可以创建全文索引。值得一提的是,在数据
量较大时候,现将数据放入一个没有全局索引的表中,然后再用 CREATE index
创建 fulltext 索引,要比先为一张表建立 fulltext 然后再将数据写入的速度快很
多。
(1)创建表的时候添加全文索引

CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) CHARACTER NOT NULL ,
 `content` text CHARACTER NULL , 
 `time` int(10) NULL DEFAULT NULL ,
  PRIMARY KEY (`id`), FULLTEXT (content)
);

(2)修改表结构添加全文索引

1 ALTER TABLE article ADD FULLTEXT index_content(content)

(3)直接创建全文索引

1 CREATE FULLTEXT INDEX index_content ON article(content)

索引的优点
创建唯一性索引,保证数据库表中每一行数据的唯一性
大大加快数据的检索速度,这也是创建索引的最主要的原因
加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排
序的时间。
通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能。
索引的缺点
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加
索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定
的物理空间,如果要建立聚簇索引,那么需要的空间就会更大
当对表中的数据进行增加.删除和修改的时候,索引也要动态的维护,降低
了数据的维护速度
常见索引原则有
选择唯一性索引:唯一性索引的值是唯一的,可以更快速的通过该索引来确
定某条记录。
为经常需要排序.分组和联合操作的字段建立索引. 为常作为查询条件的字段建立索引。
限制索引的数目:越多的索引,会使更新表变得很浪费时间。
尽量使用数据量少的索引:如果索引的值很长,那么查询的速度会受到影响。
尽量使用前缀来索引:如果索引字段的值很长,最好使用值的前缀来索引。
删除不再使用或者很少使用的索引
最左前缀匹配原则,非常重要的原则。
尽量选择区分度高的列作为索引:区分度的公式是表示字段不重复的比例
索引列不能参与计算,保持列“干净”:带函数的查询不参与索引。
尽量的扩展索引,不要新建索引。
3.7 数据库三范式
范式是具有最小冗余的表结构。3 范式具体如下:
1. 第一范式(1st NF -First Normal Fromate)
第一范式的目标是确保每列的原子性:如果每列都是不可再分的最小数据单
元(也称为最小的原子 单元),则满足第一范式(1NF)
第一范式(1NF)要求数据库表的每一列都是不可分割的基本数据项,同一
列中不能有多个值。
若某一列有多个值,可以将该列单独拆分成一个实体,新实体和原实体间是
一对多的关系。
在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不
满足第一范式(1NF)的数据库就不是关系数据库。
第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原
子值,就说明该数据库表满足了第一范式。
第一范式的合理遵循需要根据系统的实际需求来定。比如某些数据库系统中
需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字
段就行。但是如果系统经常会访问“地址”属性中的“城市”部分,那么就非要
将“地址”这个属性重新拆分为省份.城市.详细地址等多个部分进行存储,这样
在对地址中某一部分操作的时候将非常方便。这样设计才算满足了数据库的第一
范式
2. 第二范式(2nd NF-Second Normal Fromate)
首先满足第一范式,并且表中非主键列不存在对主键的部分依赖。 第二范
式要求每个表只描述一 件事情。
满足第二范式(2NF)必须先满足第一范式(1NF)。
第二范式要求实体中每一行的所有非主属性都必须完全依赖于主键;即:非
主属性必须完全依赖于主键。
完全依赖:主键可能由多个属性构成,完全依赖要求不允许存在非主属性依
赖于主键中的某一部分属性。
若存在哪个非主属性依赖于主键中的一部分属性,那么要将发生部分依赖的
这一组属性单独新建一个实体,并且在旧实体中用外键与新实体关联,并且新实
体与旧实体间是一对多的关系。
第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的
每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而
言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种
数据保存在同一张数据库表中。
3. 第三范式(3rd NF- Third Normal Fromate)
第三范式定义是,满足第二范式,并且表中的列不存在对非主键列的传递
依赖。除了主键订单编号外,顾客姓名依赖于非主键顾客编号。
满足第三范式必须先满足第二范式。
第三范式要求:实体中的属性不能是其他实体中的非主属性。因为这样会出
现冗余。即:属性不依赖于其他非主属性。
如果一个实体中出现其他实体的非主属性,可以将这两个实体用外键关联,
而不是将另一张表的非主属性直接写在当前表中。
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相
关。
3.8 数据库事务
1. 事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列操作,这
些操作作为一个整体一起向 系统提交,要么都执行.要么都不执行 。事务是一
个不可分割的工作逻辑单元 事务必须具备以下四个属性,简称 ACID 属性:
A 原子性(Atomicity):事务是一个完整的操作。事务的各步操作是不可
分的(原子的);要 么都执行,要么都不执行。
C 一致性(Consistency):当事务完成时,数据必须处于一致状态。
I 隔离性(Isolation):对数据进行修改的所有并发事务是彼此隔离的,这
表明事务必须是独 立的,它不应以任何方式依赖于或影响其他事务。
D 永久性(Durability):事务完成后,它对数据库的修改被永久保持,事
务日志能够保持事务 的永久性。
2 .事务控制语句:
 BEGIN 或 START TRANSACTION 显式地开启一个事务;
 COMMIT 也可以使用 COMMIT WORK,不过二者是等价的。
COMMIT 会提交事务,并使已对数据库进行的所有修改成为永久性的;
 ROLLBACK 也可以使用 ROLLBACK WORK,不过二者是等价
的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;
 SAVEPOINT identifier,SAVEPOINT 允许在事务中创建一个
保存点,一个事务中可以有多个 SAVEPOINT;
 RELEASE SAVEPOINT identifier 删除一个事务的保存点,当没
有指定的保存点时,执行该语句会抛出一个异常;
 ROLLBACK TO identifier 把事务回滚到标记点;
 SET TRANSACTION 用来设置事务的隔离级别。InnoDB 存储
引擎提供事务的隔离级别有 READ UNCOMMITTED.READ
COMMITTED.REPEATABLE READ 和 SERIALIZABLE。
3. MySQL 事务处理主要有两种方法:

  1. 用 BEGIN, ROLLBACK, COMMIT 来实现
    a) BEGIN 开始一个事务
    b) ROLLBACK 事务回滚
    c) COMMIT 事务确认
  2. 直接用 SET 来改变 MySQL 的自动提交模式:
    a) SET AUTOCOMMIT=0 禁止自动提交
    b) SET AUTOCOMMIT=1 开启自动提交
    4. 事务的四种隔离级别
  3. Read uncommitted
    读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
  4. Read committed
    读已提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数
    据。
  5. Repeatable read
    可重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
  6. Serializable 序列化
    Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执
    行,可以避免脏读.不可重复读与幻读。但是这种事务隔离级别效率低下,比
    较耗数据库性能,一般不使用。
    在 MySQL 数据库中,支持上面四种隔离级别,默认的为 Repeatable
    read (可重复读)
    ;而在 Oracle 数据库中,只支持 Serializable (串行化)级别
    和 Read committed (读已提交)这两种级别,其中默认的为 Read
    committed 级别。
    3.9 存储过程
    一组为了完成特定功能的 SQL 语句集,存储在数据库中,经过第一次编译
    后再次调用不需要再次 编译,用户通过指定存储过程的名字并给出参数(如果
    该存储过程带有参数)来执行它。存储过 程是数据库中的一个重要对象。
    存储过程优化思路:
  1. 尽量利用一些 SQL 语句来替代一些小循环,例如聚合函数,求平均函
    数等。
    2.中间结果存放于临时表,加索引。
    3.少使用游标。SQL 是个集合语言,对于集合运算具有较高性能。而
    cursors 是过程运算。比 如对一个 100 万行的数据进行查询。游标需要读表
    100 万次,而不使用游标则只需要少量几 次读取。
    4.事务越短越好。SQLserver 支持并发操作。如果事务过多过长,或者隔
    离级别过高,都会造 成并发操作的阻塞,死锁。导致查询极慢,cpu 占用率极
    低。
    5.使用 try-catch 处理错误异常。
    6.查找语句尽量不要放在循环内。
    3.10 触发器
    触发器是一段能自动执行的程序,是一种特殊的存储过程,触发器和普通的
    存储过程的区别是: 触发器是当对某一个表进行操作时触发。诸如:
    update.insert.delete 这些操作的时候,系统 会自动调用执行该表上对应的触
    发器。SQL Server 2005 中触发器可以分为两类:DML 触发器和 DDL 触发
    器,其中 DDL 触发器它们会影响多种数据定义语言语句而激发,这些语句有
    create. alter.drop 语句。
    3.11 数据库并发策略
    并发控制一般采用三种方法,分别是乐观锁和悲观锁以及时间戳。
    乐观锁
    乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;悲观锁
    就刚好相反,觉得自 己读数据库的时候,别人可能刚好在写自己刚读的数据,其实就是持一种比较保守的态度;时间 戳就是不加锁,通过时间戳来控制并发出现的问题。
    悲观锁
    悲观锁就是在读取数据的时候,为了不让别人修改自己读取的数据,就会先
    对自己读取的数据加 锁,只有自己把数据读完了,才允许别人修改那部分数据,或者反过来说,就是自己修改某条数 据的时候,不允许别人读取该数据,只有等自己的整个事务提交了,才释放自己加上的锁,才允 许其他用户访问那部分数据。
    两种锁的使用场景
    从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另
    一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
    乐观锁常见的两种实现方式
    版本号机制

    一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次
    数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数
    据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当
    前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
    CAS 算法即 compare and swap(比较与交换),是一种有名的无锁算法。
    无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程
    被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking
    Synchronization)。CAS 算法涉及到三个操作数
    需要读写的内存值 V
    进行比较的值 A
    拟写入的新值 B
    当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,
    否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
    乐观锁的缺点
    ABA 问题
    如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它
    仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
    JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中
    的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
    循环时间长开销大
    自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,
    会给 CPU 带来非常大的执行开销。 如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPUpipeline flush),从而提高 CPU 的执行效率。
    只能保证一个共享变量的原子操作
    CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
    但是从 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作。
    CAS 与 synchronized 的使用情景
    简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),
    synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)
    对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进
    行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;
    而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
    对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,
    从而浪费更多的 CPU 资源,效率低于 synchronized。
    补充: Java 并发编程这个领域中 synchronized 关键字一直都是元老级的
    角色,很久之前很多人都会称它为 “重量级锁” 。但是,在 JavaSE 1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。
    synchronized 的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和 CAS 类似的性能;而线程冲突严重的情况下,性能远高于 CAS。
    时间戳
    时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”,每次
    读出来的时候,把该字 段也读出来,当写回去的时候,把该字段加 1,提交之
    前 ,跟数据库的该字段比较一次,如果比数 据库的值大的话,就允许保存,否
    则不允许保存,这种处理方法虽然不使用数据库系统提供的锁 机制,但是这种
    方法可以大大提高数据库处理的并发量,
    以上悲观锁所说的加“锁”,其实分为几种锁,分别是:排它锁(写锁)和
    共享锁(读锁)。
    3.12 数据库锁
    1. 行级锁
    行级锁是一种排他锁(写锁),防止其他事务修改此行;在使用以下语句时,Oracle会自动应用行级锁:
    INSERT.UPDATE.DELETE.SELECT … FOR UPDATE [OF columns] [WAIT
    n | NOWAIT];
    SELECT … FOR UPDATE 语句允许用户一次锁定多条记录进行更新
    使用 COMMIT 或 ROLLBACK 语句释放锁。
    2. 表级锁
    表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分
    MySQL 引擎支持。最常使 用的 MYISAM 与 INNODB 都支持表级锁定。表
    级锁定分为表共享读锁(共享锁)与表独占写锁 (排他锁)。
    3. 页级锁
    页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁
    速度快,但冲突多,行级 冲突少,但速度慢。所以取了折衷的页级,一次锁定
    相邻的一组记录。BDB 支持页级锁
    3.13 基于 Redis 分布式锁
  2. 获取锁的时候,使用 setnx(ETNX key val:当且仅当 key 不存在时,
    set 一个 key 为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0)加锁,锁的 value 值为一个随机生成的 UUID,在释放锁的时候进行判断。并使用 expire 命令为锁添 加一个超时时间,超过该时间则自动释放锁。
  3. 获取锁的时候调用 setnx,如果返回 0,则该锁正在被别人使用,返回 1
    则成功获取 锁。 还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  4. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete
    进行锁释放。
    3.14 分区分表
    分库分表有垂直切分和水平切分两种。
    ▪ 垂直切分:将表按照功能模块.关系密切程度划分出来,部署到不同的库上。例如,我们会 建立定义数据库 workDB.商品数据库 payDB.用户数据库
    userDB.日志数据库 logDB 等,分别用于存储项目数据定义表.商品定义表.用户数据表.日志数据表等。
    ▪ 水平切分:当一个表中的数据量过大时,我们可以把该表的数据按照某种
    规则,例如 userID 散列,进行划分,然后存储到多个结构相同的表,和不同的
    库上。例如,我们的 userDB 中的用户数据表中,每一个表的数据量都很大,就可以把 userDB 切分为结构相同的多个 userDB:part0DB.part1DB 等,再将 userDB 上的用户数据表 userTable,切分为很多 userTable:
    userTable0.userTable1 等,然后将这些表按照一定的规则存储到多个 userDB上。
    3.15 应该使用哪一种方式来实施数据库分库分表,这要看
    数据库中数据量的瓶颈 所在,并综合项目的业务类型进行
    考虑。

    如果数据库是因为表太多而造成海量数据,并且项目的各项业务逻辑划分清
    晰. 低耦合,那么规则简单明了.容易实施的垂直切分必是首选。
    而如果数据库中的表并不多,但单表的数据量很大.或数据热度很高,这种
    情况 之下就应该选择水平切分,水平切分比垂直切分要复杂一些,它将原本逻
    辑上属 于一体的数据进行了物理分割,除了在分割时要对分割的粒度做好评估,
    考虑数 据平均和负载平均,后期也将对项目人员及应用程序产生额外的数据管
    理负担。 在现实项目中,往往是这两种情况兼而有之,这就需要做出权衡,甚
    至既需要垂 直切分,又需要水平切分。我们的游戏项目便综合使用了垂直与水
    平切分,我们 首先对数据库进行垂直切分,然后,再针对一部分表,通常是用
    户数据表,进行 水平切分。
    单库多表 :
    随着用户数量的增加,user 表的数据量会越来越大,当数据量达到一定 程
    度的时候对 user 表的查询会渐渐的变慢,从而影响整个 DB 的性能。如果使用 MySQL, 还有一个更严重的问题是,当需要添加一列的时候,MySQL 会锁表, 期间所有的读写操作只能等待。可以将 user 进行水平的切分,产生两个表结构完全一样的user_0000,user_0001 等表,user_0000 + user_0001 + …的数据刚好是一份完整的数据。
    多库多表 :
    随着数据量增加也许单台 DB 的存储空间不够,随着查询量的增加单台数
    据 库服务器已经没办法支撑。这个时候可以再对数据库进行水平区分。
    分库分表规则举例: 通过分库分表规则查找到对应的表和库的过程。如分
    库分表的规则是 user_id 除以 4 的方式,当用户新注册了一个账号,账号 id
    的 123,我们可以通过 id 除以 4 的方式确定此账号应该保存到 User_0003
    表中。当用户 123 登录的时 候,我们通过 123 除以 4 后确定记录在
    User_0003 中。
    3.16 MySQL 读写分离
    在实际的应用中,绝大部分情况都是读远大于写。MySQL 提供了读写分离
    的机制,所有的写操作都必须对应到 Master,读操作可以在 Master 和 Slave机 器上进行,Slave 与 Master 的结构完全一样,一个 Master 可以有多个Slave,甚 至 Slave 下还可以挂 Slave,通过此方式可以有效的提高 DB 集群的每秒查询率. 所有的写操作都是先在 Master 上操作,然后同步更新到 Slave上,所以 从 Master 同步到 Slave 机器有一定的延迟,当系统很繁忙的时候,延迟问题会 更加严重,Slave 机器数量的增加也会使这个问题更加严重。此外,可以看出 Master 是集群的瓶颈,当写操作过多,会严重影响到Master 的 稳定性,如果 Master 挂掉,整个集群都将不能正常工作。 所以,1. 当读压力很大的时候,可以考虑添加 Slave 机器的分式解决,但是当 Slave机器达到一定的数量就得考虑分库了。 2. 当写压力很大的时候,就必须 得进行分库操作。
    3.17 MySQL 常用 30 种 SQL 查询语句优化方法
    1.应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用
    索引而进行全表扫描。
    2.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order
    by 涉及的列上建立索引。
    3.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎
    放弃使用索引而进行全表扫描。如:
    select id from t where num is null
    可以在 num 上设置默认值 0,确保表中 num 列没有 null 值,然后这样查
    询:
    select id from t where num=0
    4.尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使
    用索引而进行全表扫描,如:
    select id from t where num=10 or num=20
    可以这样查询:
    select id from t where num=10
    union all
    select id from t where num=20
    5.下面的查询也将导致全表扫描:(不能前置百分号)
    select id from t where name like ‘%c%’
    下面走索引
    select id from t where name like ‘c%’
    若要提高效率,可以考虑全文检索。
    6.in 和 not in 也要慎用,否则会导致全表扫描,如:
    select id from t where num in(1,2,3)
    对于连续的数值,能用 between 就不要用 in 了:select id from t where
    num between 1 and 3
    7.如果在 where 子句中使用参数,也会导致全表扫描。因为 SQL 只有在
    运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
    select id from t where num=@num
    可以改为强制查询使用索引:
    select id from t with(index(索引名)) where num=@num
    8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃
    使用索引而进行全表扫描。如:
    select id from t where num/2=100
    应改为:
    select id from t where num=1002
    9.应尽量避免在 where 子句中对字段进行函数操作,这将导致引擎放弃使
    用索引而进行全表扫描。如:
    select id from t where substring(name,1,3)=’abc’ –name 以 abc 开
    头的 id
    select id from t where datediff(day,createdate,’2005-11-30′)=0 –’
    2005-11-30′生成的 id
    应改为:
    select id from t where name like ‘abc%’
    select id from t where createdate>=’2005-11-30′ and createdate<’
    2005-12-1′
    10.不要在 where 子句中的“=”左边进行函数.算术运算或其他表达式运
    算,否则系统将可能无法正确使用索引。
    11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到
    该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使 用,并且应尽可能的让字段顺序与索引顺序相一致。
    12.不要写一些没有意义的查询,如需要生成一个空表结构:
    select col1,col2 into #t from t where 1=0
    这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
    create table #t(…)
    13.很多时候用 exists 代替 in 是一个好的选择:
    select num from a where num in(select num from b)
    用下面的语句替换:
    select num from a where exists(select 1 from b where num=a.num)
    14.并不是所有索引对查询都有效,SQL 是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL 查询可能不会去利用索引,如一表中有字段sex,male.female 几乎各一半,那么即使在 sex 上建了索引也对查询效率起不了作用。
    15.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同
    时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会
    重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数较好不要超过 6 个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。
    16.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数
    据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺
    序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数
    据列,那么需要考虑是否应将该索引建为 clustered 索引。
    17.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,
    这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
    18.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字
    段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
    19.任何地方都不要使用 select * from t ,用具体的字段列表代替“
    ”,
    不要返回用不到的任何字段。
    20.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引
    非常有限(只有主键索引)。
    21.避免频繁创建和删除临时表,以减少系统表资源的消耗。
    22.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,
    当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,较好使 用导出表。
    23.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select
    into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,
    为了缓和系统表的资源,应先 create table,然后 insert。
    24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
    25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过 1
    万行,那么就应该考虑改写。
    26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来
    解决问题,基于集的方法通常更有效。
    27.与临时表一样,游标并不是不可使用。对小型数据集使用
    FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。
    28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结
    束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONEINPROC 消息。
    29.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是
    否合理。
    30.尽量避免大事务操作,提高系统并发能力。
    3.18 数据库优化方案整理
    1.优化说明
    (1)有数据表明,用户可以承受的最大等待时间为 8 秒。数据库优化策略有
    很多,设计初期,建立好的数据结构对于后期性能优化至关重要。因为数据库结构是系统的基石,基础打不好,使用各种优化策略,也不能达到很完美的效果。
    (2)数据库优化的几个方面
    Java面试~基础_第23张图片

可以看出来,数据结构.SQL.索引是成本最低,且效果最好的优化手段。
(3)性能优化是无止境的,当性能可以满足需求时即可,不要过度优化。
2.优化方向
(1)SQL 以及索引的优化

首先要根据需求写出结构良好的 SQL,然后根据 SQL 在表中建立有效的索
引。但是如果索引太多,不但会影响写入的效率,对查询也有一定的影响。
(2)合理的数据库是设计
根据数据库三范式来进行表结构的设计。设计表结构时,就需要考虑如何设
计才能更有效的查询。
数据库三范式:
第一范式:数据表中每个字段都必须是不可拆分的最小单元,也就是确保每
一列的原子性;
第二范式:满足一范式后,表中每一列必须有唯一性,都必须依赖于主键;
第三范式:满足二范式后,表中的每一列只与主键直接相关而不是间接相关
(外键也是直接相关),字段没有冗余。
注意:没有最好的设计,只有最合适的设计,所以不要过分注重理论。三范
式可以作为一个基本依据,不要生搬硬套。
有时候可以根据场景合理地反规范化:
A:分割表。
B:保留冗余字段。当两个或多个表在查询中经常需要连接时,可以在其中
一个表上增加若干冗余的字段,以 避免表之间的连接过于频繁,一般在冗余列
的数据不经常变动的情况下使用。
C:增加派生列。派生列是由表中的其它多个列的计算所得,增加派生列可
以减少统计运算,在数据汇总时可以大大缩短运算时间。
数据库五大约束:
A:PRIMARY key:设置主键约束;
B:UNIQUE:设置唯一性约束,不能有重复值;
C:DEFAULT 默认值约束
D:NOT NULL:设置非空约束,该字段不能为空;
E:FOREIGN key :设置外键约束。
字段类型选择:
A:尽量使用 TINYINT.SMALLINT.MEDIUM_INT 作为整数类型而非 INT,如果非负则
加上 UNSIGNED
B:VARCHAR 的长度只分配真正需要的空间
C:使用枚举或整数代替字符串类型
D:尽量使用 TIMESTAMP 而非 DATETIME
E:单表不要有太多字段,建议在 20 以内
F:避免使用 NULL 字段,很难查询优化且占用额外索引空间
(3)系统配置的优化
例如:MySQL 数据库 my.cnf
(4)硬件优化
更快的 IO.更多的内存。一般来说内存越大,对于数据库的操作越好。但是
CPU 多就不一定了,因为他并不会用到太多的 CPU 数量,有很多的查询都是单
CPU。另外使用高的 IO(SSD.RAID),但是 IO 并不能减少数据库锁的机制。
所以说如果查询缓慢是因为数据库内部的一些锁引起的,那么硬件优化就没有什
么意义。
3. 优化方案
代码优化

之所以把代码放到第一位,是因为这一点最容易引起技术人员的忽视。很多
技术人员拿到一个性能优化的需求以后,言必称缓存.异步.JVM 等。实际上,第
一步就应该是分析相关的代码,找出相应的瓶颈,再来考虑具体的优化策略。有
一些性能问题,完全是由于代码写的不合理,通过直接修改一下代码就能解决问
题的,比如 for 循环次数过多.作了很多无谓的条件判断.相同逻辑重复多次等。
举个例子:
一个 update 操作,先查询出 entity,再执行 update,这样无疑多了一次
数据库交互。还有一个问题,update 语句可能会操作一些无需更新的字段。
我们可以将表单中涉及到的属性,以及 updateTime,updateUser 等赋值
到 entity,直接通过 pdateByPrimaryKeySelective,去 update 特定字段
定位慢 SQL,并优化
这是最常用.每一个技术人员都应该掌握基本的 SQL 调优手段(包括方法. 工具.辅助系统等)。这里以 MySQL 为例,最常见的方式是,由自带的慢查询
日志或者开源的慢查询系统定位到具体的出问题的 SQL,然后使用
explain.profile 等工具来逐步调优,最后经过测试达到效果后上线。
SqlServer 执行计划:
通过执行计划,我们能得到哪些信息:
A:哪些步骤花费的成本比较高
B:哪些步骤产生的数据量多,数据量的多少用线条的粗细表示,很直观
C:每一步执行了什么动作
具体优化手段:
A:尽量少用(或者不用)sqlserver 自带的函数
select id from t where substring(name,1,3) = ’abc’
select id from t where datediff(day,createdate,’2005-11-30′) = 0
可以这样查询:
select id from t where name like ‘abc%’
select id from t where createdate >= ‘2005-11-30’ and createdate
< ‘2005-12-1’
B:连续数值条件,用 BETWEEN 不用 IN:SELECT id FROM t WHERE num
BETWEEN 1 AND 5
C:Update 语句,如果只更改 1.2 个字段,不要 Update 全部字段,否则
频繁调用会引起明显的性能消耗
D:尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型
E:不建议使用 select * from t ,用具体的字段列表代替“”,不要返回
用不到的任何字段。尽量避免向客户 端返回大数据量,若数据量过大,应该考
虑相应需求是否合理
F:表与表之间通过一个冗余字段来关联,要比直接使用 JOIN 有更好的性

G:select count(
) from table;这样不带任何条件的 count 会引起全表
扫描
连接池调优
我们的应用为了实现数据库连接的高效获取.对数据库连接的限流等目的,
通常会采用连接池类的方案,即每一个应用节点都管理了一个到各个数据库的连
接池。随着业务访问量或者数据量的增长,原有的连接池参数可能不能很好地满
足需求,这个时候就需要结合当前使用连接池的原理.具体的连接池监控数据和
当前的业务量作一个综合的判断,通过反复的几次调试得到最终的调优参数。 合理使用索引
索引一般情况下都是高效的。但是由于索引是以空间换时间的一种策略,索
引本身在提高查询效率的同时会影响插入.更新.删除的效率,频繁写的表不宜建
索引。
选择合适的索引列,选择在 where,group by,order by,on 从句中出现
的列作为索引项,对于离散度不大的列没有必要创建索引。
主键已经是索引了,所以 primay key 的主键不用再设置 unique 唯一索引
索引类型
主键索引 (PRIMARY KEY)
唯一索引 (UNIQUE)
普通索引 (INDEX)
组合索引 (INDEX)
全文索引 (FULLTEXT)
可以应用索引的操作符
大于等于
Between
IN
LIKE 不以 % 开头
不能应用索引的操作符
NOT IN
LIKE %_ 开头
如何选择索引字段
A:字段出现在查询条件中,并且查询条件可以使用索引
B:通常对数字的索引和检索要比对字符串的索引和检索效率更高
C:语句执行频率高,一天会有几千次以上
D:通过字段条件可筛选的记录集很小
无效索引
A:尽量不要在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索
引而进行全表扫描
B:应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引
而进行全表扫描。
C:应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描
select id from t where num=10 or Name = ‘admin’
可以这样查询:
select id from t where num = 10
union
select id from t where Name = ‘admin’
union all 返回所有数据,不管是不是重复。 union 会自动压缩,去除重复数据。
D:不做列运算
where age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数.计算
表达式等
E:查询 like,如果是 ‘%aaa’ 不会使用到索引
分表
分表方式
水平分割(按行).垂直分割(按列)
分表场景
A: 根据经验,MySQL 表数据一般达到百万级别,查询效率就会很低。
B: 一张表的某些字段值比较大并且很少使用。可以将这些字段隔离成单
独一张表,通过外键关联,例如考试成绩,我们通常关注分数,不关注考试详情。
水平分表策略
按时间分表:当数据有很强的实效性,例如微博的数据,可以按月分割。
按区间分表:例如用户表 1 到一百万用一张表,一百万到两百万用一张表。
hash 分表:通过一个原始目标 id 或者是名称按照一定的 hash 算法计算出
数据存储的表名。
读写分离
当一台服务器不能满足需求时,采用读写分离【写: update/delete/add】
的方式进行集群。
一台数据库支持最大连接数是有限的,如果用户的并发访问很多,一台服务
器无法满足需求,可以集群处理。MySQL 集群处理技术最常用的就是读写分离。
主从同步:数据库最终会把数据持久化到磁盘,集群必须确保每个数据库服
务器的数据是一致的。从库读主库写,从库从主库上同步数据。
读写分离:使用负载均衡实现,写操作都往主库上写,读操作往从服务器上
读。
缓存
缓存分类
本地缓存:HashMap/ConcurrentHashMap.Ehcache.Guava Cache 等
缓存服务:Redis/Tair/Memcache 等
使用场景
短时间内相同数据重复查询多次且数据更新不频繁,这个时候可以选择先从
缓存查询,查询不到再从数据库加载并回设到缓存的方式。此种场景较适合用单
机缓存。
高并发查询热点数据,后端数据库不堪重负,可以用缓存来扛。
缓存作用:减轻数据库的压力,减少访问时间。
缓存选择:如果数据量小,并且不会频繁地增长又清空(这会导致频繁地垃
圾回收),那么可以选择本地缓存。具体的话,如果需要一些策略的支持(比如
缓存满的逐出策略),可以考虑 Ehcache;如不需要,可以考虑 HashMap;如
需要考虑多线程并发的场景,可以考虑 ConcurentHashMap。
其他情况,可以考虑缓存服务。目前从资源的投入度.可运维性.是否能动态
扩容以及配套设施来考虑,我们优先考虑 Tair。除非目前 Tair 还不能支持的场合
(比如分布式锁.Hash 类型的 value),我们考虑用 Redis。
缓存穿透一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的
value,就应该去后端系统查找(比如 DB)。如果 key 对应的 value 是一定不
存在的,并且对该 key 并发请求量很大,就会对后端系统造 成很大的压力。这
就叫做缓存穿透。
对查询结果为空的情况也进行缓存,缓存时间设置短点,或者该 key 对应
的数据 insert 了之后清理缓存。
缓存并发有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进
程同时查询 DB,同时设置缓存的情况,
如果并发确实很大,这也可能造成 DB 压力过大,还有缓存频繁更新的问题。
对缓存查询加锁,如果 KEY 不存在,就加锁,然后查 DB 入缓存,然后解
锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入 DB 查询。
缓存雪崩(失效)
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如 DB)带来很大压力。
不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀.
防止缓存空间不够用
(1) 给缓存服务,选择合适的缓存逐出算法,比如最常见的 LRU。
(2) 针对当前设置的容量,设置适当的警戒值,比如 10G 的缓存,当缓存
数据达到 8G 的时候,就开始发出报警,提前排查问题或者扩容。
(3) 给一些没有必要长期保存的 key,尽量设置过期时间。
我们看下图,在 WebServe(r Dao 层)和 DB 之间加一层 cache,这层 cache 一般选取的介质是内存,因为我们都知道存入数据库的数据都具有持久化的特
点,那么读写会有磁盘 IO 的操作,内存的读写速度远比磁盘快得多。(选用存
储介质,提高访问速度:内存>>磁盘;减少磁盘 IO 的操作,减少重复查询,提
高吞吐量)
Java面试~基础_第24张图片

常用开源的缓存工具有:ehcache.memcache.Redis。
ehcache 是一个纯 Java 的进程内缓存框架,hibernate 使用其做二级缓存。
同时,ehcache 可以通过多播的方式实现集群。本人主要用于本地的缓存,数
据库上层的缓存。
memcache 是一套分布式的高速缓存系统,提供 key-value 这样简单的数
据储存,可充分利用 CPU 多核,无持久化功能。在做 web 集群中可以用做
session 共享,页面对象缓存。
Redis 高性能的 key-value 系统,提供丰富的数据类型,单核 CPU 有抗并
发能力,有持久化和主从复制的功能。本人主要使用 Redis 的 Redis sentinel,
根据不同业务分为多组。
Redis 注意事项
A:在增加 key 的时候尽量设置过期时间,不然 Redis Server 的内存使
用会达到系统物理内存的最大值,导致 Redis 使用 VM 降低系统性能;
B:Redis Key 设计时应该尽可能短,Value 尽量不要使用复杂对象;
C:将对象转换成 JSON 对象(利用现成的 JSON 库)后存入 Redis;
D:将对象转换成 Google 开源二进制协议对象(Google Protobuf,和
JSON 数据格式类似,但是因为是二进制表现,所以性能效率以及空间占用都比
JSON 要小;缺点是 Protobuf 的学习曲线比 JSON 大得多);
E:Redis 使用完以后一定要释放连接。 读取缓存中是否有相关数据,如果缓存中有相关数据,则直接返回,这就是所谓的数据命中“hit”
如果缓存中没有相关数据,则从数据库读取相关数据,放入缓存中,再返回。
这就是所谓的数据未命中“miss”
缓存的命中率 = 命中缓存请求个数/总缓存访问请求个数 =
hit/(hit+miss)
NoSQL
与缓存的区别
先说明一下,这里介绍的和缓存不一样,虽然 Redis 等也可以用来做数据存
储方案(比如 Redis 或者 Tair),但 NoSql 是把它作为 DB 来用。如果当作 DB
来用,需要有效保证数据存储方案的可用性.可靠性。
使用场景
需要结合具体的业务场景,看这块业务涉及的数据是否适合用 NoSQL 来存
储,对数据的操作方式是否适合用 NoSQL 的方式来操作,或者是否需要用到
NoSQL 的一些额外特性(比如原子加减等)。
如果业务数据不需要和其他数据作关联,不需要事务或者外键之类的支持,
而且有可能写入会异常频繁,这个时候就比较适合用 NoSQL(比如 HBase)
比如,美团点评内部有一个对 exception 做的监控系统,如果在应用系统
发生严重故障的时候,可能会短时间产生大量 exception 数据,这个时候如果
选用 MySQL,会造成 MySQL 的瞬间写压力飙升,容易导致 MySQL 服务器的
性能急剧恶化以及主从同步延迟之类的问题,这种场景就比较适合用 Hbase 类
似的 NoSQL 来存储。
视图/存储过程
普通业务逻辑尽量不要使用存储过程,定时任务或报表统计函数可以根据团
队资源情况采用存储过程处理。
JVM 调优
通过监控系统(如没有现成的系统,自己做一个简单的上报监控的系统也很
容易)上对一些机器关键指标(gc time.gc count.各个分代的内存大小变化.机
器的 Load 值与 CPU 使用率.JVM 的线程数等)的监控报警,也可以看 gc log
和 jstat 等命令的输出,再结合线上 JVM 进程服务的一些关键接口的性能数据和
请求体验,基本上就能定位出当前的 JVM 是否有问题,以及是否需要调优。
异步/多线程
针对某些客户端的请求,在服务端可能需要针对这些请求做一些附属的事
情,这些事情其实用户并不关心或者用户不需要立即拿到这些事情的处理结果,
这种情况就比较适合用异步的方式处理这些事情。
异步作用
A:缩短接口响应时间,使用户的请求快速返回,用户体验更好。
B:避免线程长时间处于运行状态,这样会引起服务线程池的可用线程长时
间不够用,进而引起线程池任务队列长度增大,从而阻塞更多请求任务,使得更
多请求得不到技术处理。
C:线程长时间处于运行状态,可能还会引起系统 Load.CPU 使用率.机器整
体性能下降等一系列问题,甚至引发雪崩。异步的思路可以在不增加机器数和
CPU 数的情况下,有效解决这个问题。
异步实现
A:额外开辟线程,这里可以采用额外开辟一个线程或者使用线程池的做法,
在 IO 线程(处理请求响应)之外的线程来处理相应的任务,在 IO 线程中让
response 先返回。
B:使用消息队列(MQ)中间件服务
搜索引擎
例如:solr,elasticsearch
四. SpringMVC 框架
4.1 什么是 SpringMVC ?简单介绍下你对 SpringMVC
的理解?

SpringMVC 是一个基于 Java 的实现了 MVC 设计模式的请求驱动类型的轻
量级 Web 框架,通过把 Model,View,Controller 分离,将 web 层进行职责
解耦,把复杂的 web 应用分成逻辑清晰的几部分,简化开发,减少出错,方便
组内开发人员之间的配合。
4.2 SpringMVC 的流程?
Java面试~基础_第25张图片

(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 响应用户。
4.3 SpringMVC 的优点
(1)可以支持各种视图技术,而不仅仅局限于 JSP;
(2)与 Spring 框架集成(如 IoC 容器.AOP 等);
(3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映
射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器
(ViewResolver)
(4) 支持各种请求资源的映射策略。
4.4 SpringMVC 的主要组件?
(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.5 SpringMVC 和 Struts2 的区别有哪些?
(1)SpringMVC的入口是一个servlet即前端控制器(DispatchServlet),
而 struts2 入口是一个 filter 过虑器(StrutsPrepareAndExecuteFilter)。
(2)SpringMVC 是基于方法开发(一个 url 对应一个方法),请求参数传递
到方法的形参,可以设计为单例或多例(建议单例),struts2 是基于类开发,传
递参数是通过类的属性,只能设计为多例。
(3)Struts 采用值栈存储请求和响应的数据,通过 OGNL 存取数据,
SpringMVC 通过参数解析器是将 request 请求内容解析,并给方法形参赋值,
将数据和视图封装成 ModelAndView 对象,最后又将 ModelAndView 中的模
型数据通过 reques 域传输到页面。Jsp 视图解析器默认使用 jstl。
4.6 SpringMVC 怎么样设定重定向和转发的?
(1)转发:在返回值前面加"forward:“,譬如
“forward:user.do?name=method4” (2)重定向:在返回值前面加"redirect:”,譬如
“redirect:http://www.baidu.com”
4.7 SpringMVC 怎么和 Ajax 相互调用的?
通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json
对象。具体步骤如下 :
(1)加入 Jackson.jar
(2)在配置文件中配置 json 的映射
(3)在接受 Ajax 方法里面可以直接返回 Object,List 等,但方法前面要加上
@ResponseBody 注解。
4.8 如何解决 Post 请求中文乱码问题,Get 的又如何处理
呢?

(1)解决 post 请求乱码问题:
在 web.xml 中配置一个 CharacterEncodingFilter 过滤器,设置成 utf-8;

<filter>
<filter-name>CharacterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>utf-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>CharacterEncodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>

(2)get 请求中文参数出现乱码解决方法有两个:
修改 tomcat 配置文件添加编码与工程编码一致,如下:

另外一种方法对参数进行重新编码:
String userName= new String(request.getParamter(“userName”).get
Bytes(“ISO8859-1”),“utf-8”)
ISO8859-1 是 tomcat 默认编码,需要将 tomcat 编码后的内容按 utf-8 编
码。
4.9 SpringMVC 的异常处理 ?
答:可以将异常抛给 Spring 框架,由 Spring 框架来处理;我们只需要配
置简单的异常处理器,在异常处理器中添视图页面即可。
4.10 SpringMVC 的控制器是不是单例模式,如果是,有什么
问题,怎么解决

答:是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影
响性能的,解决方案是在控制器里面不能写字段。
4.11 SpringMVC 常用的注解有哪些?
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,
则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收 http 请求的 json 数据,将 json 转换为 java 对象。
@ResponseBody:注解实现将 conreoller 方法返回对象转化为 json 对象响应给客户。
4.12 SpingMVC 中的控制器的注解一般用那个,有没有别
的注解可以替代?

一般用@Conntroller 注解,表示是表现层,不能用别的注解代替。
4.13 如果在拦截请求中,我想拦截 get 方式提交的方法,怎
么配置?

可以在@RequestMapping 注解里面加
method=RequestMethod.GET。
4.14 怎样在方法里面得到 Request,或者 Session?
直接在方法的形参中声明 request,SpringMVC 就自动把 request 对象传
入。
4.15 如果想在拦截的方法里面得到从前台传入的参数,怎么
得到?

直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。
4.16 如果前台有很多个参数传入,并且这些参数都是一个对
象的,那么怎么样快速得到这个对象?

直接在方法中声明这个对象,SpringMVC 就自动会把属性赋值到这个对象
里面。
4.17 SpringMVC 中函数的返回值是什么?
返回值可以有很多类型,有 String, ModelAndView。ModelAndView 类把
视图和数据都合并的一起的,但一般用 String 比较好。
4.18 SpringMVC 用什么对象从后台向前台传递数据的?
通过 ModelMap 对象,可以在这个对象里面调用 put 方法,把对象加到里面, 前台就可以通过 el 表达式拿到。
4.19 怎么样把 ModelMap 里面的数据放入 Session 里
面?

可以在类上面加上@SessionAttributes 注解,里面包含的字符串就是要放
入 session 里面的 key。
4.20 SpringMVC 里面拦截器是怎么写的?
有两种写法,一种是实现 HandlerInterceptor 接口,另外一种是继承适配器
类,接着在接口方法当中,实现处理逻辑;然后在 SpringMVC 的配置文件中配
置拦截器即可:


<mvc:interceptors>

<bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor">bean>

<mvc:interceptor>
<mvc:mapping path="/modelMap.do" />
<bean class="com.zwp.action.MyHandlerInterceptorAdapter" />
mvc:interceptor>
mvc:interceptors>

4.21 注解原理
注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运
行时生成的动态代理类。我们通过反射获取注解时,返回的是 Java 运行时生成
的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用
AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues
这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。 五. Spring 框架
5.1 Spring 是什么?

Spring 是一个轻量级的 IoC 和 AOP 容器框架。是为 Java 应用程序提供基
础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需
要关心业务需求。常见的配置方式有三种:基于 XML 的配置.基于注解的配置. 基于 Java 的配置。
主要由以下几个模块组成:
Spring Core:核心类库,提供 IOC 服务;
Spring Context:提供框架式的 Bean 访问方式,以及企业级功能(JNDI. 定时任务等);
Spring AOP:AOP 服务;
Spring DAO:对 JDBC 的抽象,简化了数据访问异常的处理;
Spring ORM:对现有的 ORM 框架的支持;
Spring Web:提供了基本的面向 Web 的综合特性,例如多方文件上传;
Spring MVC:提供面向 Web 应用的 Model-View-Controller 实现。
5.2 Spring 的优点?
(1)Spring 属于低侵入式设计,代码的污染极低;
(2)Spring 的 DI 机制将对象之间的依赖关系交由框架处理,减低组件的
耦合性;
(3)Spring 提供了 AOP 技术,支持将一些通用任务,如安全.事务.日志. 权限等进行集中式管理,从而提供更好的复用。
(4)Spring 对于主流的应用框架提供了集成支持。
5.3 Spring 的 AOP 理解?
OOP 面向对象,允许开发者定义纵向的关系,但并适用于定义横向的关系,
导致了大量代码的重复,而不利于各个模块的重用。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务
无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的
模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低
了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证.日志.事务处
理。
AOP 实现的关键在于 代理模式,AOP 代理主要分为静态代理和动态代理。
静态代理的代表为 AspectJ;动态代理则以 Spring AOP 为代表。
(1)AspectJ 是静态代理的增强,所谓静态代理,就是 AOP 框架会在编译
阶段生成 AOP 代理类,因此也称为编译时增强,他会在编译阶段将 AspectJ(切
面)织入到 Java 字节码中,运行的时候就是增强之后的 AOP 对象。
(2)Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会
去修改字节码,而是每次运行时在内存中临时为方法生成一个 AOP 对象,这个
AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回
调原对象的方法。
Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态
代理:
(1)JDK 动态代理只提供接口的代理,不支持类的代理。核心
InvocationHandler 接口和 Proxy 类,InvocationHandler 通过 invoke()方法
反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy
利用 InvocationHandler 动态创建一个符合某一接口的的实例, 生成目标类的
代理对象。
(2)如果代理类没有实现 InvocationHandler 接口,那么 Spring AOP 会
选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是
一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖
其中特定方法并添加增强代码,从而实现 AOP。CGLIB 是通过继承的方式做的
动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代
理的。
(3)静态代理与动态代理区别在于生成 AOP 代理对象的时机不同,相对
来说 AspectJ 的静态代理方式具有更好的性能,但是 AspectJ 需要特定的编译
器进行处理,而 Spring AOP 则无需特定的编译器处理。
InvocationHandler 的
invoke(Object proxy,Method method,Object[] args):proxy 是最终生成的
代理实例; method 是被代理目标实例的某个具体方法;args 是被代理目标实
例某个方法的具体入参, 在方法反射调用时使用。
5.4 Spring 的 IOC 理解?
(1)IOC 就是控制反转,是指创建对象的控制权的转移,以前创建对象的
主动权和时机是由自己把控的,而现在这种权力转移到 Spring 容器中,并由容
器根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松
散耦合,也利于功能的复用。DI 依赖注入,和控制反转是同一个概念的不同角
度的描述,即 应用程序在运行时依赖 IoC 容器来动态注入对象需要的外部资源。
(2)最直观的表达就是,IOC 让对象的创建不用去 new 了,可以由 spring
自动生产,使用 java 的反射机制,根据配置文件在运行时动态的去创建对象以
及管理对象,并调用对象的方法的。
(3)Spring 的 IOC 有三种注入方式 :构造器注入.setter 方法注入.根据
注解注入。
IOC 让相互协作的组件保持松散的耦合,而 AOP 编程允许你把遍布于应用
各层的功能分离出来形成可重用的功能组件。
5.5 BeanFactory 和 ApplicationContext 有什么区别?
BeanFactory 和 ApplicationContext 是 Spring 的两大核心接口,都可以
当做 Spring 的容器。其中 ApplicationContext 是 BeanFactory 的子接口。
(1)BeanFactory:是 Spring 里面最底层的接口,包含了各种 Bean 的定
义,读取 bean 配置文档,管理 bean 的加载.实例化,控制 bean 的生命周期,
维护 bean 之间的依赖关系。ApplicationContext 接口作为 BeanFactory 的派
生,除了提供 BeanFactory 所具有的功能外,还提供了更完整的框架功能:
继承 MessageSource,因此支持国际化。
统一的资源文件访问方式。
提供在监听器中注册 bean 的事件。
同时加载多个配置文件。
载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的
层次,比如应用的 web 层。
(2)BeanFactroy 采用的是延迟加载形式来注入 Bean 的,即只有在使用
到某个 Bean 时(调用 getBean()),才对该 Bean 进行加载实例化。这样,我们
就不能发现一些存在的 Spring 的配置问题。如果 Bean 的某一个属性没有注入,
BeanFacotry 加载后,直至第一次使用调用 getBean 方法才会抛出异常。
ApplicationContext,它是在容器启动时,一次性创建了所有的 Bean。这
样,在容器启动时,我们就可以发现 Spring 中存在的配置错误,这样有利于检
查所依赖属性是否注入。 ApplicationContext 启动后预载入所有的单实例
Bean,通过预载入单实例 bean ,确保当你需要的时候,你就不用等待,因为它
们已经创建好了。
相对于基本的 BeanFactory,ApplicationContext 唯一的不足是占用内存
空间。当应用程序配置 Bean 较多时,程序启动较慢。
(3)BeanFactory 通常以编程的方式被创建,ApplicationContext 还能
以声明的方式创建,如使用 ContextLoader。
(4)BeanFactory 和 ApplicationContext 都支持
BeanPostProcessor.BeanFactoryPostProcessor 的使用,但两者之间的区别
是:BeanFactory 需要手动注册,而 ApplicationContext 则是自动注册。
5.6 请解释 Spring Bean 的生命周期?
首先说一下 Servlet 的生命周期:实例化,初始 init,接收请求 service,
销毁 destroy;
Spring 上下文中的 Bean 生命周期也类似,如下:
(1)实例化 Bean:
对于 BeanFactory 容器,当客户向容器请求一个尚未初始化的 bean 时,
或初始化 bean 的时候需要注入另一个尚未初始化的依赖时,容器就会调用
createBean 进行实例化。对于 ApplicationContext 容器,当容器启动结束后,
通过获取 BeanDefinition 对象中的信息,实例化所有的 bean。
(2)设置对象属性(依赖注入):
实例化后的对象被封装在 BeanWrapper 对象中,紧接着,Spring 根据
BeanDefinition 中的信息 以及 通过 BeanWrapper 提供的设置属性的接口完
成依赖注入。
(3)处理 Aware 接口:
接着,Spring 会检测该对象是否实现了 xxxAware 接口,并将相关的
xxxAware 实例注入给 Bean:
如果这个 Bean 已经实现了 BeanNameAware 接口,会调用它实现的
setBeanName(String beanId)方法,此处传递的就是 Spring 配置文件中 Bean
的 id 值;
如果这个 Bean 已经实现了 BeanFactoryAware 接口,会调用它实现的
setBeanFactory()方法,传递的是 Spring 工厂自身。
如果这个 Bean 已经实现了 ApplicationContextAware 接口,会调用
setApplicationContext(ApplicationContext)方法,传入 Spring 上下文;
(4)BeanPostProcessor:
如果想对 Bean 进行一些自定义的处理,那么可以让 Bean 实现了
BeanPostProcessor 接口,那将会调用
postProcessBeforeInitialization(Object obj, String s)方法。由于这个方法是
在 Bean 初始化结束时调用的,所以可以被应用于内存或缓存技术;
(5)InitializingBean 与 init-method:
如果 Bean 在 Spring 配置文件中配置了 init-method 属性,则会自动调
用其配置的初始化方法。
(6)如果这个 Bean 实现了 BeanPostProcessor 接口,将会调用
postProcessAfterInitialization(Object obj, String s)方法;
以上几个步骤完成后,Bean 就已经被正确创建了,之后就可以使用这个
Bean 了。
(7)DisposableBean:
当 Bean 不再需要时,会经过清理阶段,如果 Bean 实现了 DisposableBean
这个接口,会调用其实现的 destroy()方法;
(8)destroy-method:
最后,如果这个 Bean 的 Spring 配置中配置了 destroy-method 属性,会
自动调用其配置的销毁方法。
5.7 解释 Spring 支持的几种 bean 的作用域。
Spring 容器中的 bean 可以分为 5 个范围:
(1)singleton:默认,每个容器中只有一个 bean 的实例,单例的模式由
BeanFactory 自身来维护。
(2)prototype:为每一个 bean 请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean
会失效并被垃圾回收器回收。
(4)session:与 request 范围类似,确保每个 session 中有一个 bean 的
实例,在 session 过期后,bean 会随之失效。
(5)global-session:全局作用域,global-session 和 Portlet 应用相关。
当你的应用部署在 Portlet 容器中工作时,它包含很多 portlet。如果你想要声
明让所有的 portlet 共用全局的存储变量的话,那么这全局变量需要存储在
global-session 中。全局作用域与 Servlet 中的 session 作用域效果相同。
5.8 使用注解之前要开启自动扫描功能
其中 base-package 为需要扫描的包(含子包)。
1
@Configuration 把一个类作为一个 IoC 容器,它的某个方法头上如果注册了@Bean,就会作
为这个 Spring 容器中的 Bean。 @Scope 注解 作用域
@Lazy(true) 表示延迟初始化
@Service 用于标注业务层组件. @Controller 用于标注控制层组件(如 struts 中的 action)
@Repository 用于标注数据访问组件,即 DAO 组件。 @Component 泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。 @Scope 用于指定 scope 作用域的(用在类上)
@PostConstruct 用于指定初始化方法(用在方法上)
@PreDestory 用于指定销毁方法(用在方法上)
@Resource 默认按名称装配,当找不到与名称匹配的 bean 才会按类型装配。 @DependsOn:定义 Bean 初始化及销毁时的顺序
@Primary:自动装配时当出现多个 Bean 候选者时,被注解为@Primary 的 Bean 将作为首
选者,否则将抛出异常
@Autowired 默认按类型装配,如果我们想使用按名称装配,可以结合@Qualifier 注解一起使
用。如下:
@Autowired @Qualifier(“personDaoBean”) 存在多个实例配合使用
5.9 Spring 框架中的单例 Beans 是线程安全的么?
Spring 框架并没有对单例 bean 进行任何多线程的封装处理。关于单例 bean 的线程
安全和并发问题需要开发者自行去搞定。但实际上,大部分的 Spring bean 并没有可变的
状态(比如 Serview 类和 DAO 类),所以在某种程度上说 Spring 的单例 bean 是线程安全
的。如果你的 bean 有多种状态的话(比如 View Model 对象),就需要自行保证线程安
全。最浅显的解决办法就是将多态 bean 的作用域由“singleton”变更为“prototype”。
5.10 Spring 如何处理线程并发问题?
在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中,绝大
部分 Bean 都可以声明为 singleton 作用域,因为 Spring 对一些 Bean 中非线程安全状态
采用 ThreadLocal 进行处理,解决线程安全问题。
ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机
制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获
得锁的线程则需要排队。而 ThreadLocal 采用了“空间换时间”的方式。
ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的
访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。
ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装
进 ThreadLocal。
5.11 Spring 基于 xml 注入 bean 的几种方式
(1)Set 方法注入;
(2)构造器注入:1.通过 index 设置参数的位置;2.通过 type 设置参数类型;3.通过
name 注入
(3)静态工厂注入;
(4)实例工厂;
5.12 Spring 的自动装配:
在 Spring 中,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互
协作的对象引用赋予各个对象,使用 autowire 来配置自动装载模式。
在 Spring 框架 xml 配置中共有 5 种自动装配:
(1)no:默认的方式是不进行自动装配的,通过手工设置 ref 属性来进行装配 bean。
(2)byName:通过 bean 的名称进行自动装配,如果一个 bean 的 property 与另
一 bean 的 name 相同,就进行自动装配。
(3)byType:通过参数的数据类型进行自动装配。
(4)constructor:利用构造函数进行装配,并且构造函数的参数通过 byType 进行装
配。
(5)autodetect:自动探测,如果有构造方法,通过 construct 的方式自动装配,否
则使用 byType 的方式自动装配。
基于注解的方式:
使用@Autowired 注解来自动装配指定的 bean。在使用@Autowired 注解之前需要
在 Spring 配置文件进行配置,。在启动 spring IoC 时,
容器自动装载了一个 AutowiredAnnotationBeanPostProcessor 后置处理器,当容器扫描
到@Autowied.@Resource 或@Inject 时,就会在 IoC 容器自动查找需要的 bean,并装
配给该对象的属性。在使用@Autowired 时,首先在容器中查询对应类型的 bean:
如果查询结果刚好为一个,就将该 bean 装配给@Autowired 指定的数据;
如果查询的结果不止一个,那么@Autowired 会根据名称来查找;
如果上述查找的结果为空,那么会抛出异常。解决方法时,使用 required=false。 @Autowired 可用于:构造函数.成员变量.Setter 方法
注:@Autowired 和@Resource 之间的区别
(1) @Autowired 默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在
(可以设置它 required 属性为 false)。
(2) @Resource 默认是按照名称来装配注入的,只有当找不到与名称匹配的 bean 才
会按照类型来装配注入。
5.13 Spring 框架中都用到了哪些设计模式?
(1)工厂模式:BeanFactory 就是简单工厂模式的体现,用来创建对象的实例;
(2)单例模式:Bean 默认为单例模式。
(3)代理模式:Spring 的 AOP 功能用到了 JDK 的动态代理和 CGLIB 字节码生成技
术;
(4)模板方法:用来解决代码重复的问题。比
如. RestTemplate, JmsTemplate, JpaTemplate。
(5)观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,
所有依赖于它的对象都会得到通知被制动更新,如 Spring 中 listener 的实现
–ApplicationListener。
5.14 Spring 事务的实现方式和实现原理
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,
spring 是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过
binlog 或者 redo log 实现的。
(1)Spring 事务的种类:
spring 支持编程式事务管理和声明式事务管理两种方式:
A.编程式事务管理使用 TransactionTemplate。
B.声明式事务管理建立在 AOP 之上的。其本质是通过 AOP 功能,对方法
前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始
之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,
只需在配置文件中做相关的事务规则声明或通过@Transactional 注解的方式,
便可以将事务规则应用到业务逻辑中。
声明式事务管理要优于编程式事务管理,这正是 spring 倡导的非侵入式的
开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持。唯
一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以
作用到代码块级别。
(2)Spring 的事务传播行为:
Spring 事务的传播行为说的是,当多个事务同时存在的时候,Spring 如何
处理这些事务的行为。
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,
如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就
加入该事务,如果当前不存在事务,就以非事务执行。‘
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,
就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事
务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当
前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则
抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。
如果当前没有事务,则按 REQUIRED 属性执行。
(3)Spring 中的隔离级别:
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,
使用数据库默认的事务隔离级别。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可以看到这个
事务未提交的数据。
③ ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才
能被另一事务读取,而且能看到该事务对已有记录的更新。
④ ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才
能被另一事务读取,但是不能看到该事务对已有记录的更新。
⑤ ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据
库所做的更新。
5.15 Spring 框架中有哪些不同类型的事件?
Spring 提供了以下 5 种标准的事件:
(1)上下文更新事件(ContextRefreshedEvent):在调用
ConfigurableApplicationContext 接口中的 refresh()方法时被触发。
(2)上下文开始事件(ContextStartedEvent):当容器调用
ConfigurableApplicationContext 的 Start()方法开始/重新开始容器时触发该
事件。
(3)上下文停止事件(ContextStoppedEvent):当容器调用
ConfigurableApplicationContext 的 Stop()方法停止容器时触发该事件。
(4)上下文关闭事件(ContextClosedEvent):当 ApplicationContext
被关闭时触发该事件。容器被关闭时,其管理的所有单例 Bean 都被销毁。
(5)请求处理事件(RequestHandledEvent):在 Web 应用中,当一个
http 请求(request)结束触发该事件。
如果一个 bean 实现了 ApplicationListener 接口,当一个
ApplicationEvent 被发布以后,bean 会自动被通知。
5.16 解释一下 Spring AOP 里面的几个名词
(1)切面(Aspect):被抽取的公共模块,可能会横切多个对象。在 Spring
AOP 中,切面可以使用通用类(基于模式的风格) 或者在普通类中
以 @AspectJ 注解来实现。
(2)连接点(Join point):指方法,在 Spring AOP 中,一个连接点 总
是 代表一个方法的执行。
(3)通知(Advice):在切面的某个特定的连接点(Join point)上执行
的动作。通知有各种类型,其中包括“around”.“before”和“after”等通知。
许多 AOP 框架,包括 Spring,都是以拦截器做通知模型, 并维护一个以连接
点为中心的拦截器链。
(4)切入点(Pointcut):切入点是指 我们要对哪些 Join point 进行拦
截的定义。通过切入点表达式,指定拦截的方法,比如指定拦截 add*.search*。
(5)引入(Introduction):(也被称为内部类型声明(inter-type
declaration))。声明额外的方法或者某个类型的字段。Spring 允许引入新的
接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入
来使 bean 实现 IsModified 接口,以便简化缓存机制。
(6)目标对象(Target Object): 被一个或者多个切面(aspect)所通
知(advise)的对象。也有人把它叫做 被通知(adviced) 对象。 既然 Spring
AOP 是通过运行时代理实现的,这个对象永远是一个 被代理(proxied)对象。
(7)织入(Weaving):指把增强应用到目标对象来创建新的代理对象的
过程。Spring 是在运行时完成织入。
切入点(pointcut)和连接点(join point)匹配的概念是 AOP 的关键,
这使得 AOP 不同于其它仅仅提供拦截功能的旧技术。 切入点使得定位通知
(advice)可独立于 OO 层次。 例如,一个提供声明式事务管理的 around 通
知可以被应用到一组横跨多个对象中的方法上(例如服务层的所有业务操作)。
5.17 Spring 通知有哪些类型?
(1)前置通知(Before advice):在某连接点(join point)之前执行的通知,但这
个通知不能阻止连接点前的执行(除非它抛出一个异常)。
(2)返回后通知(After returning advice):在某连接点(join point)正常完成后
执行的通知:例如,一个方法没有抛出任何异常,正常返回。
(3)抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。
(4)后通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是
正常返回还是异常退出)。
(5)环绕通知(Around Advice):包围一个连接点(join point)的通知,如方法
调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它
也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。 环绕
通知是最常用的一种通知类型。大部分基于拦截的 AOP 框架,例如 Nanning 和 JBoss4,
都只提供环绕通知。
同一个 aspect,不同 advice 的执行顺序:
①没有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterReturning
②有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterThrowing:异常发生
java.lang.RuntimeException: 异常发生
六. Mybatis 框架
6.1 什么是 Mybatis?

(1)Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,
开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动.创建连接. 创建 statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql
执行性能,灵活度高。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映
射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取
结果集。
(3)通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,
并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql
语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。(从执
行 sql 到返回 result 的过程)。
6.2 Mybaits 的优点
(1)基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设
计造成任何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管
理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。
(2)与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的
代码,不需要手动开关连接;
(3)很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,
所以只要 JDBC 支持的数据库 MyBatis 都支持)。
(4)能够与 Spring 很好的集成;
(5)提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象
关系映射标签,支持对象关系组件维护。
6.3 MyBatis 框架的缺点
(1)SQL 语句的编写工作量较大,尤其当字段多.关联表多时,对开发人员
编写 SQL 语句的功底有一定要求。
(2)SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
6.4 MyBatis 框架适用场合
(1)MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案。
(2)对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis
将是不错的选择。
6.5 MyBatis 与 Hibernate 有哪些不同?
(1)Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis
需要程序员自己编写 Sql 语句。
(2)Mybatis 直接编写原生态 SQL,可以严格控制 SQL 执行性能,灵活
度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频
繁,一但需求变化要求迅速输出成果。但是灵活的前提是 Mybatis 无法做到数
据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套 SQL 映
射文件,工作量大。
(3)Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要
求高的软件,如果用 Hibernate 开发可以节省很多代码,提高效率。
6.6 #{}和KaTeX parse error: Expected 'EOF', got '#' at position 11: {}的区别是什么? #̲{}是预编译处理,{}是字符串替换。
Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用
PreparedStatement 的 set 方法来赋值;
Mybatis 在处理 时,就是把 {}时,就是把 时,就是把{}替换成变量的值。
使用#{}可以有效的防止 SQL 注入,提高系统安全性。
6.7 Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执
行的内存分页,而非物理分页。可以在 SQL 内直接书写带有物理分页的参数来
完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,
在插件的拦截方法内拦截待执行的 SQL,然后重写 SQL,根据 dialect 方言,添
加对应的物理分页语句和物理分页参数。
6.8 Mybatis是如何将 SQL 执行结果封装为目标对象并返回
的?都有哪些映射形式?
第一种是使用标签,逐一定义数据库列名和对象属性名之间
的映射关系。
第二种是使用标签和 SQL 列的别名功能,将列的别名书写为
对象属性名。
有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反
射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值
的。
6.9 Mybatis 动态 SQL 有什么用?执行原理?有哪些动
sql?
Mybatis 动态 SQL 可以在 Xml 映射文件内,以标签的形式编写动态 sql,
执行原理是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能。
Mybatis 提供了 9 种动态 sql 标签:trim | where | set | foreach | if |
choose | when | otherwise | bind。
6.10 Xml 映射文件中,除了常见的
select|insert|updae|delete 标签之外,还有哪些标签?
....,加上
动态 sql 的 9 个标签,其中为 sql 片段标签,通过标签引入 sql
片段,为不支持自增的主键生成策略标签。
6.11 Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,
id 是否可以重复?
不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果
没有配置 namespace,那么 id 不能重复;
原因就是 namespace+id 是作为 Map
key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相
覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id
自然也就不同。
6.12 为什么说 Mybatis 是半自动 ORM 映射工具?它与全自
动的区别在哪里?
Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合
对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis 在查询关联对
象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。
6.13 MyBatis 实现一对一有几种方式?具体怎么操作的?
有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在 resultMap 里
面配置 association 节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键 id,去再另外一个表里面查询
数据,也是通过 association 配置,但另外一个表的查询通过 select 属性配置。
6.14 MyBatis 实现一对多有几种方式,怎么操作的?
有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在
resultMap 里面的 collection 节点配置一对多的类就可以完成;嵌套查询是先
查一个表,根据这个表里面的 结果的外键 id,去再另外一个表里面查询数据,也是
通过配置 collection,但另外一个表的查询通过 select 节点配置。
6.15 Mybatis 是否支持延迟加载?如果支持,它的实现原理是
什么?
答:Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延
迟加载,association 指的就是一对一,collection 指的就是一对多查询。在
Mybatis 配置文件中,可以配置是否启用延迟加载
lazyLoadingEnabled=true|false。
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,
进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现
a.getB()是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把
B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成
a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原
理都是一样的。
6.16 Mybatis 的一级.二级缓存
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作
用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有
Cache 就将清空,默认打开一级缓存且不能关闭。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,
HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自
定义存储源,如 Ehcache。默认不打开二级缓存,要手动开启二级缓存,使用
二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在
它的映射文件中配置 ;
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存
Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存
将被 clear。
6.17 什么是 MyBatis 的接口绑定?有哪些实现方式?
接口绑定,就是在 MyBatis 中任意定义接口,然后把接口里面的方法和 SQL
语句绑定, 我们直接调用接口方法就可以,这样比起原来了 SqlSession 提供的方
法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加
上 @Select.@Update 等注解,里面包含 Sql 语句来绑定;另外一种就是通过
xml 里面写 SQL 来绑定, 在这种情况下,要指定 xml 映射文件里面的 namespace
必须为接口的全路径名。当 Sql 语句比较简单时候,用注解绑定, 当 SQL 语句比
较复杂时候,用 xml 绑定,一般用 xml 绑定的比较多。
6.18 使用 MyBatis 的 mapper 接口调用时有哪些要求?
Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的 id 相同;
Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的
parameterType 的类型相同;
Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的
resultType 的类型相同;
Mapper.xml 文件中的 namespace 即是 mapper 接口的类路径。
6.19 简述 Mybatis 的插件运行原理,以及如何编写一个插件。
Mybatis 仅可以编写针对
ParameterHandler.ResultSetHandler.StatementHandler.Executor 这 4 种接口的插件, 在四大对象创建的时候
1、每个创建出来的对象不是直接返回的,而是
interceptorChain.pluginAll(parameterHandler);
2、获取到所有的 Interceptor(拦截器)(插件需要实现的接口);
调用 interceptor.plugin(target);返回 target 包装后的对象
3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP(面
向切面)
我们的插件可以为四大对象创建出代理对象;
代理对象就可以拦截到四大对象的每一个执行;
Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接
口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体
就是 InvocationHandler 的 invoke()方法,当然,只会拦截那些你指定需要拦
截的方法。
编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然
后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在
配置文件中配置你编写的插件。

你可能感兴趣的:(java)