Java面试总结

1. String,StringBuffer, StringBuilder 的区别是什么?String为什么是不可变的?

String StringBuffer StringBuilder
String的值是不可变的,每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量的内存空间 StringBuffer是可变的线程安全的字符串操作类,每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 可变类,速度更快
不可变 可变 可变
- 线程安全 线程不安全
- 多线程操作字符串 单线程操作字符串

初始化上的区别,String可以空赋值,后者不行,报错

小结:

(1)如果要操作少量的数据用 String;

(2)多线程操作字符串缓冲区下操作大量数据 StringBuffer;

(3)单线程操作字符串缓冲区下操作大量数据 StringBuilder。

String为什么不可变长?

String类被final修饰,不可被继承。
String的成员变量char数组value被final修饰,初始化后不可更改引用。
String的成员变量value访问修饰符为private,不对外界提供修改value数组值的方法。
String类源码

public final class String implements java.io.Serializable, Comparable, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

2. Vector、ArrayList、LinkedList的区别,ArrayList的扩容策略

  • 1、ArrayList和Vector的默认初始容量为10。Vector、ArrayList、LinkedList 是有序的。
  • 2、ArrayList和Vector底层是数组结构,查询快增删慢;LinkedList底层是链表结构,查询慢增删快。
  • 3、ArrayList、LinkedList线程不安全,如果在并发环境下,可以用Collections.synchronizedList()对ArrayList和LinkedList进行调用。
  • Vector线程安全,即它大部分的方法都包含关键字synchronized,但是Vector的效率没有ArraykList和LinkedList高。
  • 4、ArrayList在元素填满容器时会自动扩充容器大小的50%,而Vector则是100%,因此ArrayList更节省空间。

3. ConcurrentHashMap和、HashMap、HashTable的区别,ConcurrentHashMap的特点

  • 1、HashMap 的特性:
    HashMap的键和值可以为空,Hashtable的键和值不允许为空;HashMap线程不安全,HashTable线程安全;HashMap比HashTable效率高。
  • 2、HashMap的工作原理:
    HashMap基于hashing的原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,HashMap调用键对象的hashCode()方法来计算hashcode,返回的hashCode用于找到bucket位置来储存Entry对象。
  • 3、HashMap中的碰撞探测(collision detection)以及碰撞的解决方法:
    “当两个对象的hashcode相同会发生什么?” 因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。
    “如果两个键的hashcode相同,你如何获取值对象?” 当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。如果有两个值对象储存在同一个bucket,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点。
    碰撞的解决方法:
    使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。
    HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
  • 4、“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?” 默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
  • 5、“你了解重新调整HashMap大小存在什么问题吗?”
    • 当重新调整HashMap大小的时候,当多线程的情况下,可能产生条件竞争(race condition)。因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

    • 存在两个问题,一是数据丢失。二是重排可能会引起链表打结锁死,下次get的时候陷入死循环的equals。jdk1.8已经对hashmap进行了优化。resize排序规则的改变红黑树避免了死循环和最坏的碰撞性能问题。但是数据丢失问题仍然不能避免。所以多线程下不要使用hashMap。可能hashMap多线程还有我们未知的安全问题。
  • 6、为什么String, Interger这样的wrapper类适合作为键? 因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。当两个不相等的对象返回不同的hashcode时,碰撞的几率就会小些,这样就能提高HashMap的性能。
  • 7、我们可以使用自定义的对象作为键吗? 可以使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且插入到Map中之后将不再改变。作为键的条件:对象不可变,即被final修饰。
  • 8、我们可以使用CocurrentHashMap来代替Hashtable吗? 可以,HashTable是synchronized的,提供更强的线程安全性。但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。

ConcurrentHashMap 总结

ConcurrentHashMap的锁分段技术
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁住容器中的一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
Java面试总结_第1张图片

定位Segment

既然ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和获取元素的时候,必须先通过哈希算法定位到Segment。可以看到ConcurrentHashMap会首先使用Wang/Jenkins hash的变种算法对元素的hashCode进行一次哈希。
再哈希,其目的是为了减少哈希冲突,使元素能够均匀的分布在不同的Segment上,从而提高容器的存取效率。假如哈希的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。

ConcurrentHashMap的get操作

Segment的get操作实现非常简单和高效。先经过一次哈希,然后使用这个哈希值通过哈希运算定位到segment,再通过哈希算法定位到元素,代码如下:

[java] view plain copy
public V get(Object key) {  
    int hash = hash(key.hashCode());  
    return segmentFor(hash).get(key, hash);  
}  

人很聪明,真的很聪明。既然不能全锁(HashTable)又不能不锁(HashMap),所以就搞个部分锁,只锁部分,用到哪部分就锁哪部分。一个大仓库,里面有若干个隔间,每个隔间都有锁,同时只允许一个人进隔间存取东西。但是,在存取东西之前,需要有一个全局索引,告诉你要操作的资源在哪个隔间里,然后当你看到隔间空闲时,就可以进去存取,如果隔间正在占用,那你就得等着。聪明!!
https://blog.csdn.net/caisongcheng_good/article/details/79489852

4. Session, Cookie区别

  • 1,session 在服务器端,cookie 在客户端(浏览器)
  • 2,session 默认被存在在服务器的一个文件里(不是内存)
  • 3,session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id)
  • 4,session 可以放在 文件、数据库、或内存中。
  • 5,用户验证这种场合一般会用 session
    因此,维持一个会话的核心就是客户端的唯一标识,即 session id

  • 1、cookie数据存放在第三方应用的浏览器上,session数据放在服务器上。

  • 2、cookie不是很安全,别人可以分析存放在本地的Cookie,进行Cookie欺骗,考虑到安全应当使用session。

  • 3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用Cookie。

  • 4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

  • 5、所以个人建议:
    将登录信息等重要信息存放为Session
    其他信息如果需要保留,可以放在Cookie

5. Servlet的生命周期

Servlet 生命周期

Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:
Servlet的生命周期包含了下面4个阶段:

1.加载和实例化
2.初始化
3.请求处理
4.服务终止
Servlet 通过调用 init () 方法进行初始化。
Servlet 调用 service() 方法来处理客户端的请求。
Servlet 通过调用 destroy() 方法终止(结束)。
最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

init() 方法

init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化,就像 Applet 的 init 方法一样。

Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是您也可以指定 Servlet 在服务器第一次启动时被加载。

当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。

public void init() throws ServletException {
  // 初始化代码...
}

service() 方法

service() 方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。

下面是该方法的特征:

public void service(ServletRequest request, 
                    ServletResponse response) 
      throws ServletException, IOException{
}

service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,您不用对 service() 方法做任何动作,您只需要根据来自客户端的请求类型来重写 doGet() 或 doPost() 即可。

doGet() 和 doPost() 方法是每次服务请求中最常用的方法。下面是这两种方法的特征。

doGet() 方法

GET 请求来自于一个 URL 的正常请求,或者来自于一个未指定 METHOD 的 HTML 表单,它由 doGet() 方法处理。

public void doGet(HttpServletRequest request,
                  HttpServletResponse response)
    throws ServletException, IOException {
    // Servlet 代码
}

doPost() 方法

POST 请求来自于一个特别指定了 METHOD 为 POST 的 HTML 表单,它由 doPost() 方法处理。

public void doPost(HttpServletRequest request,
                   HttpServletResponse response)
    throws ServletException, IOException {
    // Servlet 代码
}

destroy() 方法

destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。

在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。destroy 方法定义如下所示:

  public void destroy() {
    // 终止化代码...
  }

架构图

下图显示了一个典型的 Servlet 生命周期方案。

第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。
Servlet 容器在调用 service() 方法之前加载 Servlet。
然后 Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。
Java面试总结_第2张图片

  1. 根据时机,Web容器加载对应的Servlet类,加载后进行init()初始化。
  • 设置了容器启动时初始化
  • 第一次请求此Servlet时初始化
  • Servlet类文件被更新后,重新装载Servlet
  1. 接收到请求,容器根据配置将请求交给对应的Servlet,同时创建HttpServletRequest 和 HttpServletResponse 对象,一并交给Servlet。
  2. 在service()中根据HttpServletRequest的请求类型等信息,调用 doGet / doPost 进行业务处理。
  3. 处理后通过HttpServletResponse获得响应信息,返回给Web容器。
  4. Web容器再将响应返回给客户端。
    https://www.cnblogs.com/lingz/p/10031309.html

6. tomcat怎么处理jsp请求

  • 翻译:编写好的jsp文件被web容器中的jsp引擎转换成java源码。
  • 编译:翻译成java源码的jsp文件会被编译成可被计算机执行的字节码文件。
  • 请求:当容器(服务器)接收了来自客户端(浏览器)的请求后,将执行编译好的字节码文件。处理请求完毕后,容器再把生成的页面返回客户端页面显示。
  • 图例:
    Java面试总结_第3张图片

7. 线程池的种类和特点

Java通过Executors提供四种线程池,分别为:
1)newCachedThreadPool可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制。 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 可能导致内存溢出,一般使用newFixedThreadPool代替 。

(2)newFixedThreadPool 定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程。

(3)newScheduledThreadPool 定长线程池,支持定时及周期性任务执行。

(4)newSingleThreadExecutor 单线程化的线程池,它只会用唯一的工作线程来执行任务,线程异常结束,会创建一个新的线程,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

5) newSingleThreadScheduledExecutor 可以在指定延迟后执行线程任务 的单线程线程池。

(6)newWorkStealingPool(这个是在jdk1.8出来的)
任务窃取线程池,不保证执行顺序,适合任务耗时差异较大。

线程池中有多个线程队列,有的线程队列中有大量的比较耗时的任务堆积,而有的线程队列却是空的,就存在有的线程处于饥饿状态,当一个线程处于饥饿状态时,它就会去其它的线程队列中窃取任务。解决饥饿导致的效率问题。

默认创建的并行 level 是 CPU 的核数。主线程结束,即使线程池有任务也会立即停止。

8. 接口和抽象类区别

https://baijiahao.baidu.com/s?id=1619289698690911502&wfr=spider&for=pc&isFailFlag=1
https://blog.csdn.net/qq_35206244/article/details/81708830
https://www.cnblogs.com/wenwenzuiniucha/p/11091187.html

相同点:

(1)都不能被实例化
(2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的所有方法后才能实例化。

不同点:

(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为public static final,必须赋初始值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
(5)接口被用于常用的功能,便于日后维护和添加删除,而抽象类更倾向于充当公共类的角色,不适用于日后重新对里面代码修改。功能需要累积时用抽象类,不需要累积时用接口。
Java面试总结_第4张图片

9. 对象的实例化顺序

1. 父类静态成员和静态初始化代码块 ,按在代码中出现的顺序依次执行
2. 子类静态成员和静态初始化代码块 ,按在代码中出现的顺序依次执行
3. 父类实例成员和实例初始化代码块 ,按在代码中出现的顺序依次执行
4. 父类构造方法
5. 子类实例成员和实例初始化代码块 ,按在代码中出现的顺序依次执行
6. 子类构造方法
结论:对象初始化的顺序,先静态方法,再构造方法,每个又是先基类后子类。

10. synchronized和Lock的区别

Java面试总结_第5张图片

  • 1、synchronized 是Java内置的关键字,使用后会自动释放锁。Lock是一个接口,必须要手动释放。特别是在发生异常时,需要在 finally 块中进行手动释放,否则会发生死锁行为;

  • 2、Lock可响应中断,而synchronized 不能响应中断;

  • 3、synchronized 是非公平锁,Lock的实现类 ReentrantLock 可通过实例化true or false 的构造参数,实现公平锁和非公平锁,默认为非公平锁;

  • 4、synchronized无法判断锁的状态,Lock可以判断是否获取到锁;

  • 5、Lock锁适合大量代码的同步问题,synchronized锁适合少量代码的同步问题;

  • 6、synchronized是一个悲观锁,Lock是一个乐观锁(底层基于volatile和cas实现);

  • 7、都是可重入锁:在执行对象中所有同步方法不用再次获得锁。

二、底层实现

  • 1、synchronznized映射成字节码指令就是增加两个指令:monitorenter、monitorexit,当一条线程执行时遇到monitorenter指令时,它会尝试去获得锁,如果获得锁,那么所计数器+1(为什么要加1,因为它是可重入锁,可根据这个锁计数器判断锁状态),如果没有获得锁,那么阻塞,当它遇到一个monitoerexit时,锁计数器会-1,当计数器为0时,就释放锁。
    (tips:节码中出现的两个monitoerexit指令的原因是:一个正常执行-1,令一个异常时执行,这两个用goto的方式只执行一个)
  • 2、Lock底层则基于volatile和cas实现。

IO部分面试题:

11. 什么是java序列化?如何实现java序列化?

序列化就是一种用来处理对象流的机制,将对象的内容进行流化。可以对流化后的对象进行读写操作,可以将流化后的对象传输于网络之间。序列化是为了解决在对象流读写操作时所引发的问题
序列化的实现:将需要被序列化的类实现Serializable接口,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。


12. 什么是IO缓冲区,有什么作用?

缓冲区就是内存里的一块区域,把数据先存内存里,然后一次性写入,类似于数据库的批量操作,这样大大提高了数据的读写速率。


缓冲区就是一段特殊的内存区域,很多情况下当程序需要频繁地操作一个资源(如文件或数据库)则性能会很低,所以为了提升性能就可以将一部分数据暂时读写到缓存区,以后直接从此区域中读写数据即可,这样就显著提升了性。
对于 Java 字符流的操作都是在缓冲区操作的,所以如果我们想在字符流操作中主动将缓冲区刷新到文件则可以使用 flush() 方法操作。

13. 什么是节点流,什么是处理流,它们各有什么用处,处理流的创建有什么特征?

  • 节点流 : 直接与数据源连接,用于数据输入输出
  • 处理流:对节点流的封装,进行功能扩展,处理流的构造方法总是传入节点流的子类

处理流的创建特征 :

  • 处理流的构造器必须传入节点流的子类

反射部分面试题:

14. 什么是反射?

反射就是动态加载对象,并对对象进行剖析。在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能称为Java反射机制。

15. 什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作?

概念

  • 序列化:把对象转换为字节序列的过程称为对象的序列化。
  • 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。

如何实现序列化

实现Serializable接口即可。
在实现这个Serializable 接口的时候,一定要给 serialVersionUID 赋值。

serialVersionUID 的值到底怎么设置才OK

private static final long serialVersionUID = 1L;
注意:L需要大写。

import java.io.*;
 
/**
 * 序列化测试
 */
public class SerializableTest {
    public static void main(String[] args) throws Exception {
        serializeFlyPig();
        FlyPig flyPig = deserializeFlyPig();
        System.out.println(flyPig.toString());
 
    }
	/**
     * 序列化
     */
    private static void serializeFlyPig() throws IOException {
        FlyPig flyPig = new FlyPig();
        flyPig.setColor("black");
        flyPig.setName("naruto");
        flyPig.setCar("0000");
        // ObjectOutputStream 对象输出流,将 flyPig 对象存储到E盘的 flyPig.txt 文件中,完成对 flyPig 对象的序列化操作
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:/flyPig.txt")));
        oos.writeObject(flyPig);
        System.out.println("FlyPig 对象序列化成功!");
        oos.close();
    }
 
    /**
     * 反序列化
     */
    private static FlyPig deserializeFlyPig() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/flyPig.txt")));
        FlyPig person = (FlyPig) ois.readObject();
        System.out.println("FlyPig 对象反序列化成功!");
        return person;
    }
}

ObjectOutputStream代表对象输出流:
它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream代表对象输入流:
它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

什么情况下需要序列化

  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
  • 当你想用套接字在网络上传送对象的时候;
  • 当你想通过RMI传输对象的时候;
    (老实说,上面的几种,我可能就用过个存数据库的)

jdk api 文档里面关于接口 Serializable 的描述

  • 类通过实现 java.io.Serializable 接口以启用其序列化功能。
  • 未实现此接口的类将无法使其任何状态序列化或反序列化。
  • 可序列化类的所有子类型本身都是可序列化的。因为实现接口也是间接的等同于继承。
  • 序列化接口没有方法或字段,仅用于标识可序列化的语义。

16. 深拷贝和浅拷贝以及零拷贝概念和区别是什么?

浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。
深拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针和堆内存中的对象。
零拷贝:是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。
Java面试总结_第6张图片
Java面试总结_第7张图片

javaweb和框架部分面试题:

17. JSP 有哪些内置对象?作用分别是什么?jsp有哪几种作用域?作用是什么?

名称 作用域
application 有效范围是整个应用
session 在当前会话中有效
request 在当前请求中有效
page 在当前页面中有效
  • 如果把变量放到application里,就说明它的作用域是application,它的有效范围是整个应用。
  • 如果把变量放到session里,就说明它的作用域是session,它的有效范围是当前会话。
  • 如果把变量放到request里,就说明它的作用域是request,它的有效范围是当前请求周期。
  • 如果把变量放到pageContext里,就说明它的作用域是page,它的有效范围只在当前jsp页面里。

JSP共有以下9种基本内置组件

  • 1、request对象 客户端请求,此请求会包含来自GET/POST请求的参数,通过它才能了解到客户的需求,然后做出响应。

  • 2、response对象 响应客户请求的有关信息。

  • 3、session对象 它指的是客户端与服务器的一次会话,从客户端连到服务器的一个WebApplication开始,直到客户端与服务器断开连接为止。

  • 4、out对象 它是JspWriter类的实例,是向客户端输出内容常用的对象。

  • 5、page对象 它是指向当前JSP页面本身,有点像类中的this指针,它是 java.lang.Object类的实例。

  • 6、application对象 它实现了用户间数据的共享,可存放全局变量。它开始于服务器的启动,直到服务器的关闭。

  • 7、exception对象 它是一个例外对象,当一个页面在运行过程中发生了例外,就产生这个对象。

  • 8、pageContext对象 它提供了对JSP页面内所有的对象及名字空间的访问。

  • 9、config对象 它是在一个Servlet初始化时,JSP引擎向它传递信息用的。

18. springbean生命周期?

首先说一下Servlet的生命周期:实例化,初始init,接收请求service,销毁destroy;

Spring上下文中的Bean生命周期也类似,如下:

  • 实例化 Instantiation [ɪnˌstænʃiˈeɪʃən]

  • 属性赋值 Populate

  • 初始化 Initialization [ɪˌnɪʃəlaɪˈzeɪʃn]

  • 销毁 Destruction
    主要逻辑都在doCreateBean()方法中,逻辑很清晰,就是顺序调用以下三个方法,这三个方法与三个生命周期阶段一一对应,非常重要。

  • createBeanInstance()-> 实例化

  • populateBean()-> 属性赋值

  • initializeBean()-> 初始化

  • 当容器关闭时调用bean的销毁方法 DisposableBean接口中的destory()方法或者 destroy-method属性

// 忽略了无关代码
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
      throws BeanCreationException {
      
   // Instantiate the bean.
   BeanWrapper instanceWrapper = null;
   if (instanceWrapper == null) {
       // 实例化阶段!
      instanceWrapper = createBeanInstance(beanName, mbd, args);
   }

   // Initialize the bean instance.
   Object exposedObject = bean;
   try {
       // 属性赋值阶段!
      populateBean(beanName, mbd, instanceWrapper);
       // 初始化阶段!
      exposedObject = initializeBean(beanName, exposedObject, mbd);
   }
}

19. SpringMVC执行流程?

Java面试总结_第8张图片

  • 1、用户发起请求到前端控制器(DispatcherServlet),该控制器会过滤出哪些请求可以访问Servlet、哪些不能访问。就是url-pattern的作用,并且会加载springmvc.xml配置文件。
  • 2、前端控制器会找到处理器映射器(HandlerMapping),通过HandlerMapping完成url到controller映射的组件,简单来说,就是将在springmvc.xml中配置的或者注解的url与对应的处理类找到并进行存储,用map这样的方式来存储。
  • 3、HandlerMapping有了映射关系,并且找到url对应的处理器,HandlerMapping就会将其处理器(Handler)返回,在返回前,会加上很多拦截器。
  • 4、DispatcherServlet拿到Handler后,找到HandlerAdapter(处理器适配器),通过它来访问处理器,并执行处理器。
  • 5、执行处理器
  • 6、处理器会返回一个ModelAndView对象给HandlerAdapter
  • 7、通过HandlerAdapter将ModelAndView对象返回给前端控制器(DispatcherServlet)
  • 8、前端控制器请求视图解析器(ViewResolver)去进行视图解析,根据逻辑视图名解析成真正的视图(jsp),其实就是将ModelAndView对象中存放视图的名称进行查找,找到对应的页面形成视图对象
  • 9、返回视图对象到前端控制器。
  • 10、视图渲染,就是将ModelAndView对象中的数据放到request域中,用来让页面加载数据的。
  • 11、通过第8步,通过名称找到了对应的页面,通过第10步,request域中有了所需要的数据,那么就能够进行视图渲染了。最后将其返回即可。

1、用户发起请求到前端控制器(DispatcherServlet),该控制器会过滤出哪些请求可以访问Servlet、哪些不能访问。
2、DispatcherServlet会找到处理器映射器(HandlerMapping),通过HandlerMapping完成url到controller映射的组件。
3、HandlerMapping根据映射关系,找到url对应的处理器(Handler对象)并返回给 DispatcherServlet
4、DispatcherServlet拿到Handler后,找到HandlerAdapter(处理器适配器),通过它来访问处理器,并执行处理器。
5、处理器会返回一个ModelAndView对象HandlerAdapter
6、通过HandlerAdapterModelAndView对象返回给DispatcherServlet
7、DispatcherServlet请求视图解析器(ViewResolver)去进行视图解析,得到 view对象
8、视图渲染。
9、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响应用户。
Java面试总结_第9张图片

20. 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作用域效果相同。

21、typeof和instanceof的区别

  • typeof是判断变量类型
    对变量或值调用typeof运算符将返回下列值之一:
    - “undefined”,如果变量是Undefined型的。
    - “boolean”,如果变量是Boolean型的。
    - “number”,如果变量是Number型的。
    - “string”,如果变量是String型的。
    - “object”,如果变量是一种引用类型或Null类型的。
  • instanceof是用来判断引用类型的具体类型的,因此判断的对象首先得是个“引用类型”。

22、Spring两大特性IOC和AOP

Spring核心容器的主要组件是Bean工厂(BeanFactory),Bean工厂使用控制反转(IoC)模式来降低程序代码之间的耦合度,并提供了面向切面编程(AOP)的实现。

DI,英文全称,Dependency Injection,意为依赖注入。

依赖注入:由IoC容器动态地将某个对象所需要的外部资源(包括对象、资源、常量数据)注入到组件(Controller, Service等)之中。简单点说,就是IoC容器会把当前对象所需要的外部资源动态的注入给我们。

Spring依赖注入的方式主要有四个,基于注解注入方式、set注入方式、构造器注入方式、静态工厂注入方式。推荐使用基于注解注入方式,配置较少,比较方便。
面向切面编程(AOP)就是纵向的编程。比如业务A和业务B现在需要一个相同的操作,传统方法我们可能需要在A、B中都加入相关操作代码,而应用AOP就可以只写一遍代码,A、B共用这段代码。并且,当A、B需要增加新的操作时,可以在不改动原代码的情况下,灵活添加新的业务逻辑实现。

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

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

AOP涉及名词

切面(Aspect):共有功能的实现。如日志切面、权限切面、验签切面等。在实际开发中通常是一个存放共有功能实现的标准Java类。当Java类使用了@Aspect注解修饰时,就能被AOP容器识别为切面。

通知(Advice):切面的具体实现。就是要给目标对象织入的事情。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际开发中通常是切面类中的一个方法,具体属于哪类通知,通过方法上的注解区分。

连接点(JoinPoint):程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出等。Spring只支持方法级的连接点。一个类的所有方法前、后、抛出异常时等都是连接点。

切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

比如,在上面所说的连接点的基础上,来定义切入点。我们有一个类,类里有10个方法,那就产生了几十个连接点。但是我们并不想在所有方法上都织入通知,我们只想让其中的几个方法,在调用之前检验下入参是否合法,那么就用切点来定义这几个方法,让切点来筛选连接点,选中我们想要的方法。切入点就是来定义哪些类里面的哪些方法会得到通知。

目标对象(Target):那些即将切入切面的对象,也就是那些被通知的对象。这些对象专注业务本身的逻辑,所有的共有功能等待AOP容器的切入。

代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象本身业务逻辑加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。目标对象被织入共有功能后产生的对象。

织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译时、类加载时、运行时。Spring是在运行时完成织入,运行时织入通过Java语言的反射机制与动态代理机制来动态实现。

23、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时会按类型装配。

    简单点说,就是,能够明确该类是一个控制器类组件的,就用@Controller;能够明确是一个服务类组件的,就用@Service;能够明确该类是一个数据访问组件的,就用@Repository;不知道他是啥或者不好区分他是啥,但是就是想让他动态装配的就用@Component。

    @Controller、@Service、@Component、@Repository都是类级别的注解,如果一个方法也想动态装配,就用@Bean。

    当我们想按类型进行自动装配时,就用@Autowired;当我们想按名称(beanID)进行自动装配时,就用@Resource;当我们需要根据比如配置信息等来动态装配不同的组件时,可以用getBean(“beanID”)。

24、Spring事务的实现方式和实现原理:

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

  • (1)Spring事务的种类

    spring支持编程式事务管理和声明式事务管理两种方式:

    ①编程式事务管理使用TransactionTemplate。

    ②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

    声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。

    声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

  • 2)spring的事务传播行为

    spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。
    [ˌprɒpə’ɡeɪʃ(ə)n]
    ① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

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

    ③ PROPAGATION_MANDATORY [ˈmændətəri](强制性的):支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

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

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

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

    ⑦ PROPAGATION_NESTED [ˈnestɪd](嵌套的):如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。

  • 3)Spring中的隔离级别
    [ˌaɪsəˈleɪʃn]
    ① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。

    ② ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可以看到这个事务未提交的数据。

    ③ ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新。

    ④ ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新。

    ⑤ ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。

数据库方面的面试题

数据库三大范式是什么

第一范式:每个列都不可以再拆分。

第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。

第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。

25、MySQL的存储引擎

存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。

常用的存储引擎:

  • InnoDB引擎(MySQL默认存储引擎 从版本5.5.5开始):
    支持事务,行级锁,以及外键,拥有高并发处理能力。但是在创建索引和加载数据时,比MyISAM慢。它的设计的目标就是处理大数据容量的数据库系统。
  • MyISAM引擎:
    不支持事务、行级锁和外键。所以速度很快,性能优秀。可以对整张表加锁,支持并发插入,支持全文索引。
  • MEMORY引擎:
    所有的数据都在内存中,数据的处理速度快,但是安全性不高。
    支持Hash索引,内存表,Memory引擎将数据存储在内存中,表结构不是存储在内存中的,查询时不需要执行磁盘I/O操作,所以要比MyISAM和InnoDB快很多倍,但是数据库断电或是重启后,表中的数据将会丢失,表结构不会丢失。

数据库引擎

数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。

  • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
  • 非聚簇索引:将数据存储与索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
    澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值。
    Java面试总结_第10张图片

MyISAM存储格式:

  • 1、静态表(默认):字段都是非变长的(每个记录都是固定长度的)。存储非常迅速、容易缓存,出现故障容易恢复;占用空间通常比动态表多。

  • 2、动态表:占用的空间相对较少,但是频繁的更新删除记录会产生碎片,需要定期执行optimize table或myisamchk -r命令来改善性能,而且出现故障的时候恢复比较困难。

  • 3、压缩表:使用myisampack工具创建,占用非常小的磁盘空间。因为每个记录是被单独压缩的,所以只有非常小的访问开支。

    使用MyISAM引擎创建数据库,将产生3个文件。文件的名字以表名字开始,扩展名之处文件类型:frm文件存储表定义、数据文件的扩展名为.MYD(MYData)、索引文件的扩展名是.MYI(MYIndex)。

26、存储引擎的选择

如果没有特别的需求,使用默认的Innodb即可。

MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。

Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。
Java面试总结_第11张图片

27、索引

什么是索引?

  • 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。

  • 索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。

  • 更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。

索引有哪些优缺点?

  • 索引的优点

    可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
    通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

  • 索引的缺点

    时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
    空间方面:索引需要占物理空间。

索引有哪几种类型?

主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。

唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。

可以通过ALTER TABLE table_name ADD UNIQUE (column);创建唯一索引

可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2);创建唯一组合索引

普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。

可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引

可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引

全文索引: 是目前搜索引擎使用的一种关键技术。

可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引

索引的基本原理

索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。

索引的原理很简单,就是把无序的数据变成有序的查询

  • 把创建了索引的列的内容进行排序

  • 对排序结果生成倒排表

  • 在倒排表内容上拼上数据地址链

  • 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

索引设计的原则?

  • 适合索引的列是出现在where子句中的列,或者连接子句中指定的列
  • 基数较小的类,索引效果较差,没有必要在此列建立索引
  • 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
  • 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

创建索引的原则(重中之重)

索引虽好,但也不是无限制的使用,最好符合一下几个原则

1) 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

2)较频繁作为查询条件的字段才去创建索引

3)更新频繁字段不适合创建索引

4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)

5)尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

6)定义有外键的数据列一定要建立索引。

7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

8)对于定义为text、image和bit的数据类型的列不要建立索引。

创建索引的三种方式,删除索引

第一种方式:在执行CREATE TABLE时创建索引

CREATE TABLE user_index2 (
	id INT auto_increment PRIMARY KEY,
	first_name VARCHAR (16),
	last_name VARCHAR (16),
	id_card VARCHAR (18),
	information text,
	KEY name (first_name, last_name),
	FULLTEXT KEY (information),
	UNIQUE KEY (id_card)
);

第二种方式: 使用ALTER TABLE命令去增加索引

ALTER TABLE table_name ADD INDEX index_name (column_list);

ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。

其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。

索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

第三种方式: 使用CREATE INDEX命令创建

CREATE INDEX index_name ON table_name (column_list);

CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)

删除索引

Java面试总结_第12张图片

28、B树和B+树的区别

  • 在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。

  • B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。
    Java面试总结_第13张图片

  • 使用B树的好处
    B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

  • 使用B+树的好处
    由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间

29、事务

什么是数据库事务?

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务的四大特性、隔离级别 和 传播行为

Java面试总结_第14张图片

Java面试总结_第15张图片
这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别 ;Oracle 默认采用的 READ_COMMITTED隔离级别

事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读) 并不会有任何性能损失。

InnoDB 存储引擎在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。

传播行为

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

2、PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作

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

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

5、PROPAGATION_REQUIRES_NEW:支持当前事务,创建新事务,无论当前存不存在事务,都创建新事务。

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

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

30、数据库的乐观锁和悲观锁是什么?怎么实现的?

  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制

  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。
    Java面试总结_第16张图片

31、超键、候选键、主键、外键分别是什么?

Java面试总结_第17张图片

32、SQL的约束有哪几种?

Java面试总结_第18张图片

33、六种关联查询

Java面试总结_第19张图片

34、varchar 和 char 的区别

Java面试总结_第20张图片

35、drop、delete与truncate的区别

Java面试总结_第21张图片

36、SQL的生命周期

Java面试总结_第22张图片
Java面试总结_第23张图片

37、SQL语句优化的一些方法

1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:

select id from t where num is null
-- 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0

3.应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。
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.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

6.下面的查询也将导致全表扫描:

select id from t where name like ‘%李%’

若要提高效率,可以考虑全文检索。
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=100*2

9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where substring(name,1,3)=’abc’
-- name以abc开头的id应改为:
select id from t where name like ‘abc%’

10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。

12.不要写一些没有意义的查询,如需要生成一个空表结构:

select col1,col2 into #t from t where 1=0    
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:    
create table #t(...)    

13.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。

14.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,
因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。
一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。

15.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

16.尽可能的使用 varchar 代替 char ,因为首先变长字段存储空间小,可以节省存储空间,
其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

17.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。

18.避免频繁创建和删除临时表,以减少系统表资源的消耗。

19.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。

20.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,
以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

21.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

22.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

23.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

24.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。
在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

25.尽量避免大事务操作,提高系统并发能力。

26.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
27.很多时候用 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)    

38、MySQL的复制原理以及流程

主从复制:将主数据库中的DDL和DML操作通过二进制日志(BINLOG)传输到从数据库上,然后将这些日志重新执行(重做);从而使得从数据库的数据与主数据库保持一致。

主从复制的作用

在这里插入图片描述

MySQL主从复制解决的问题

Java面试总结_第24张图片

MySQL主从复制工作原理

在这里插入图片描述

基本原理流程,3个线程以及之间的关联

在这里插入图片描述

复制过程

Java面试总结_第25张图片
Binary log:主数据库的二进制日志

Relay log:从服务器的中继日志

第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。

第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。

第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。

39、乐观锁和悲观锁原理及实现

一、乐观锁

总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

核心SQL代码:

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};  

CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

二、悲观锁

总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。

你可能感兴趣的:(面试)