常见面试题

面试题

零、开场介绍

面试官,您好!我叫秀儿。大学时间我主要利用课外时间学习了Java 以及 SpringBoot、 MyBatis等框架。在校期间参与过一个论坛系统的开发,这个系统的主要用了SpringBoot,MyBatis-Plus和Kafaka,Es等框架。另外,我在大学的时候参加过几次xxx比赛,成功获得了第二名的成绩。说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识。生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事!

一、JAVA基础

1. Java和C++,C#区别
  • 都是面向对象的语言,都支持封装、继承和多态
  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
  • C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
2. 面向对象和面向过程的区别

面向过程: 面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
面向对象: 面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低。

3. JDK,JRE区别

Java运行时环境(JRE)是将要执行Java程序的Java虚拟机。它同时也包含了执行applet需要的浏览器插件。Java开发工具包(JDK)是完整的Java软件开发包,包含了JRE,编译器和其他的工具(比如:JavaDoc,Java调试器),可以让开发者开发、编译、执行Java应用程序。

4. ==和 equals 的区别?
  • ==是判断两个变量或实例是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是不是相同
  • ==是指对内存地址进行比较 , equals()是对字符串的内容进行比较
  • ==指引用是否相同, equals()指的是值是否相同
5. 为什么重写equals还要重写hashcode?

如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。

6. 说说抽象类和接口
  • 抽象类可以有构造方法;接口中不能有构造方法。
  • 抽象类中可以有普通成员变量;接口中没有普通成员变量。
  • 抽象类中可以包含非抽象普通方法;JDK1.8 以前接口中的所有方法默认都是抽象的,JDK1.8 开始方法可以有 default 实现和 static 方法。
  • 抽象类中的抽象方法的访问权限可以是 public、protected 和 default;接口中的抽象方法只能是 public 类型的,并且默认即为 public abstract 类型。
  • 抽象类中可以包含静态方法;JDK1.8 前接口中不能包含静态方法,JDK1.8 及以后可以包含已实现的静态方法。
  • 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量可以是任意访问权限;接口中变量默认且只能是 public static final 类型。
  • 一个类可以实现多个接口,用逗号隔开,但只能继承一个抽象类。
  • 接口不可以实现接口,但可以继承接口,并且可以继承多个接口,用逗号隔开。
7. String,StringBuffer和StringBuilder。

相同点:

  • 都可以储存和操作字符串
  • 都使用 final 修饰,不能被继承
  • 提供的 API 相似

区别:

  • String 是只读字符串,String 对象内容是不能被改变的
  • StringBuffer 和 StringBuilder 的字符串对象可以对字符串内容进行修改,在修改后的内存地址不会发生改变
  • StringBuilder 线程不安全;StringBuffer 线程安全
  • 方法体内没有对字符串的并发操作,且存在大量字符串拼接操作,建议使用 StringBuilder,效率较高。
8. final在java中的作用。

final 语义是不可改变的。

  • 被 final 修饰的类,不能够被继承
  • 被 final 修饰的成员变量必须要初始化,赋初值后不能再重新赋值(可以调用对象方法修改属性值)。对基本类型来 说是其值不可变;对引用变量来说其引用不可变,即不能再指向其他的对象
  • 被 final 修饰的方法不能重写
9. final修饰的对象什么时候被初始化?

final类型的静态变量(即编译期常量)在类加载时就会被初始化放入常量池中,其他的非编译期常量是在运行期初始化的。

10. final finally finalize()区别
  • final 表示最终的、不可改变的。用于修饰类、方法和变量。final 修饰的类不能被继承;final 方法也同样只能使用,不能重写,但能够重载;final 修饰的成员变量必须在声明时给定初值或者在构造方法内设置初始值,只能读取,不可修改;final 修饰的局部变量必须在声明时给定初值;final 修饰的变量是非基本类型,对象的引用地址不能变,但对象的属性值可以改变
  • finally 异常处理的一部分,它只能用在 try/catch 语句中,表示希望 finally 语句块中的代码最后一定被执行(存在一些情况导致 finally 语句块不会被执行,如 jvm 结束)
  • finalize() 是在 java.lang.Object 里定义的,Object 的 finalize() 方法什么都不做,对象被回收时 finalize() 方法会被调用。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要清理工作,在垃圾收集器删除对象之前被调用的。一般情况下,此方法由JVM调用。特殊情况下,可重写 finalize() 方法,当对象被回收的时候释放一些资源,须调用 super.finalize() 。
11. 什么是反射?有什么作用?

Java 反射,就是在运行状态中

  • 获取任意类的名称、package 信息、所有属性、方法、注解、类型、类加载器、modifiers(public、static)、父类、现实接口等
  • 获取任意对象的属性,并且能改变对象的属性
  • 调用任意对象的方法
  • 判断任意一个对象所属的类
  • 实例化任意一个类的对象

Java 的动态就体现在反射。通过反射我们可以实现动态装配,降低代码的耦合度;动态代理等。反射的过度使用会严重消耗系统资源。

JDK 中 java.lang.Class 类,就是为了实现反射提供的核心类之一。

一个 jvm 中一种 Class 只会被加载一次。

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

java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。Exception 能被程序本身处理(try-catch), Error 是无法处理的(只能尽量避免)。

Exception 和 Error 二者都是 Java 异常处理的重要子类,各自都包含大量子类。

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。

  • Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

13. Java如何序列化?

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

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

当 Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。

序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。

注意事项:

  • 某个类可以被序列化,则其子类也可以被序列化
  • 对象中的某个属性是对象类型,需要序列化也必须实现 Serializable 接口
  • 声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据
  • 反序列化读取序列化对象的顺序要保持一致
14.你知道java8的新特性吗,请简单介绍一下?
  • Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中。
  • 方法引用− 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
  • 默认方法− 默认方法就是一个在接口里面有了一个实现的方法。
  • 新工具− 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。
  • Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
  • Date Time API − 加强对日期与时间的处理。
  • Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
  • Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。
15. 什么是多态?如何实现?有什么好处?

多态:
同一个接口,使用不同的实例而执行不同操作。同一个行为具有多个不同表现形式或形态的能力。

实现多态有三个条件:

  • 继承
  • 子类重写父类的方法
  • 父类引用变量指向子类对象

实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

Java 中使用父类的引用变量调用子类重写的方法,即可实现多态。

16.重写和重载的区别

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

17.public,protected,private的作用范围

public公共的。权限最大,外界可以引用

private 私有的。只能被本类自己调用,类外都不可以调用,子类也不可以

protected 受保护的。只能被子类(子类可以在其他包下面)或者同一个包下的其他类引用。其他的都不可以

18.throw和throws的区别

用户程序自定义的异常和应用程序特定的异常,必须借助于 throws 和 throw 语句来定义抛出异常。
1、throws出现在方法函数头;而throw出现在函数体。
2、throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常。
3、两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

二、JAVA容器

1. 集合了解吧,说说集合有几大类,分别介绍一下

Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collecton接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 和 Queue。

  • List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
  • Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
  • Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
  • Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
2. hashmap和concurenthashmap区别
  • HashMap和ConcurentHashMap的主要区别是HashMaP是线程不安全

  • ConcurentHashMap是线程安全

JDK1.7

  • HashMap的线程不安全主要是发生在扩容函数中,即根源是在transfer函数中,由于采用头插法,在多线程高并发环境下会造成死循环或数据丢失问题。
  • ConcurentHashMap采用分段锁,可重入锁Segment类,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

JDK1.8

  • HashMap在JDK 1.8中采用尾插法修复了1.7中由于头插法引起的线程不安全(死循环和数据丢失)。在JDK 1.8中进行put操作会引起线程不安全而导致数据覆盖。
  • ConcurentHashMap采用CAS和synchronized来保证线程安全,使用的是锁分离思想,只是锁住的是一个node,而锁住Node之前的操作是基于在volatile和CAS之上无锁并且线程安全的,并且大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现。
3. Array,ArrayList和LinkedList的区别?ArrayList如何扩容?

Array 即数组
定义一个 Array 时,必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同。
ArrayList 是动态数组,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素。
ArrayList和LinkedList的区别

  • ArrayList 基于动态数组实现的非线程安全的集合;LinkedList 基于双向链表实现的非线程安全的集合。
  • 扩容问题:ArrayList 使用数组实现,无参构造函数默认初始化长度为 10,数组扩容是会将原数组中的元素重新拷贝到新数组中,长度为原来的 1.5 倍(扩容代价高);LinkedList 不存在扩容问题,新增元素放到集合尾部,修改相应的指针节点即可。
  • LinkedList 比 ArrayList 更占内存,因为 LinkedList 为每一个节点存储了两个引用节点,一个指向前一个元素,一个指向下一个元素。
  • 对于随机 index 访问的 get 和 set 方法,一般 ArrayList 的速度要优于 LinkedList。因为 ArrayList 直接通过数组下标直接找到元素;LinkedList 要移动指针遍历每个元素直到找到为止。
  • 新增和删除元素,一般 LinkedList 的速度要优于 ArrayList。因为 ArrayList 在新增和删除元素时,可能扩容和复制数组;LinkedList 实例化对象需要时间外,只需要修改节点指针即可。
  • LinkedList 集合不支持高效的随机访问(RandomAccess)
  • ArrayList 的空间浪费主要体现在在list列表的结尾预留一定的容量空间;LinkedList 的空间花费则体现在它的每一个元素都需要消耗存储指针节点对象的空间。
  • 都是非线程安全,允许存放 null
4. hashMap底层实现了解过吗?具体讲讲
  • HashMap 基于 Hash 算法实现,通过 put(key,value) 存储,get(key) 来获取 value
  • 当传入 key 时,HashMap 会根据 key,调用 hash(Object key) 方法,计算出 hash 值,根据 hash 值将 value 保存在 Node 对象里,Node 对象保存在数组里
  • 当计算出的 hash 值相同时,称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value
  • 当 hash 冲突的个数:小于等于 8 使用链表;大于 8 且 tab length 大于等于 64 时,使用红黑树解决链表查询慢的问题

ps:

  • 上述是 JDK 1.8 HashMap 的实现原理,并不是每个版本都相同,比如 JDK 1.7 的 HashMap 是基于数组 + 链表实现,所以 hash 冲突时链表的查询效率低
  • hash(Object key) 方法的具体算法是 (h = key.hashCode()) ^ (h >>> 16),经过这样的运算,让计算的 hash 值分布更均匀
4. hashMap的put()原理

以JDK1.8为例,简要流程如下:

  1. 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;

  2. 如果数组是空的,则调用 resize 进行初始化;

  3. 如果没有哈希冲突直接放在对应的数组下标里;

  4. 如果冲突了,且 key 已经存在,就覆盖掉 value;

  5. 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;

  6. 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。
    在这里插入图片描述

5. 说说hashMap的jdk1.8的优化

JDK1.8在JDK1.7的基础上针对一个链上数据过多(即拉链过长的情况)导致性能下降,增加了红黑树来进行优化。即当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

6. HashMap 和 hashTable的区别?

JDK 1.8 中 HashMapHashtable 主要区别如下:

  • 线程安全性不同。HashMap 线程不安全;Hashtable 中的方法是 synchronized 的。
  • key、value 是否允许 null。HashMap 的 key 和 value 都是可以是 null,key 只允许一个 null;Hashtable 的 key 和 value 都不可为 null。
  • 迭代器不同。HashMap 的 Iterator 是 fail-fast 迭代器;Hashtable 还使用了 enumerator 迭代器。
  • hash的计算方式不同。HashMap 计算了 hash值;Hashtable 使用了 key 的 hashCode方法。
  • 默认初始大小和扩容方式不同。HashMap 默认初始大小 16,容量必须是 2 的整数次幂,扩容时将容量变为原来的2倍;Hashtable 默认初始大小 11,扩容时将容量变为原来的 2 倍加 1。
  • 是否有 contains 方法。HashMap 没有 contains 方法;Hashtable 包含 contains 方法,类似于 containsValue。
  • 父类不同。HashMap 继承自 AbstractMap;Hashtable 继承自 Dictionary。
7. HashSet和HashMap有什么区别?

HashMap

  • 实现 Map 接口
  • 键值对的方式存储
  • 新增元素使用 put(K key, V value) 方法
  • 底层通过对 key 进行 hash,使用数组 + 链表或红黑树对 key、value 存储

HashSet

  • 实现 Set 接口
  • 存储元素对象
  • 新增元素使用 add(E e) 方法
  • 底层是采用 HashMap 实现,大部分方法都是通过调用 HashMap 的方法来实现
8. 说说ConcurrentHashMap的底层实现

ConcurrentHashMap1.7 实现原理
ConcurrentHashMap 采用分段锁设计、将一个大的 HashMap 集合拆分成 n 多个不同的小的 HashTable(Segment),默认的情况下是分成 16 个不同的 Segment,每个Segment 中都有自己独立的 HashEntry<K,V>[] table
数组+Segments 分段锁+HashEntry 链表实现
使用 Lock 锁+CAS 乐观锁+UNSAFE 类
PUT 方法流程

  • 第一次需要计算出:key 出存放在那个 Segment 对象中
  • 还需要计算 key 存放在 Segment 对象中具体 index 位置。

ConcurrentHashMap1.8 实现原理
Put 原理 锁的粒度非常小,对每个数组 index 位置上锁 对 1.7ConcurrentHashMap 实现优化

  1. 取消 segment 分段设计,使用 synchronized 锁
  2. synchronized 在 JDK1.6 开始做了优化 默认实现锁的升级过程

JDK 1.7 到 JDK 1.8 中的 ConcurrentHashMap 最大的改动:

链表上的 Node 超过 8 个改为红黑树,查询复杂度 O(logn)
ReentrantLock 显示锁改为 synchronized,说明 JDK 1.8 中 synchronized 锁性能赶上或超过 ReentrantLock

9. Jdk中map的实现都有什么:

HashMap、TreeMap、Hashtable、LinkedHashMap。

10. LinkedHashMap跟HashMap的关系:

LinkedHashMap维护了一个双向循环链表,是有序的,保留了元素的插入顺序。

11. 红黑树和完全平衡二叉树(AVL)

红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。
平衡二叉树(AVL)的性质
它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
区别:

1、红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。

2、平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知

三、多线程

1.为什么要使用多线程?多线程可能出现什么问题 ?

由于创建和销毁线程都需要很大的开销,运用线程池就可以大大的缓解这些内存开销很大的问题;可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存 。
多线程并发编程并不总是能提高程序的执行效率和运行速度,而且可能存在一些问题,包括内存泄漏、上下文切换、死锁以及受限于硬件和软件的资源限制问题等。

2. java实现多线程的方式有几种?

有4种方式可以用来创建线程:

  • 继承Thread类
class MyThread extends Thread{
	public void run(){
		System.out.println("线程运行");
	}
}
public class Test{
	public static void main(String[] args){
		MyThread thread=new MyThread();
		thread.start();//开启线程
	}
}
  • 实现Runnable接口
class MyThread implements Runnable
{
	public void run(){
		System.out.println("线程运行");
	}
}
public class Test{
	public static void main(String[] args){
		MyThread thread=new MyThread();
		Thread t=new Thread(thread);
		t.start();//开启线程
	}
}
  • 还有一种方式是实现Callable接口
    实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
import java.util.concurrent.*;
public class CallableAndFuture{
	//创建线程
	public static class CallableTest implements Callable<String>{
		public String call() throws Exception{
			return "Hello World";
		}
	}
	public static void main(String[] args){
		ExecutorService threadPool=Executors.newSingleThreadExecutor();
		//启动线程
		Future<String> future=threadPool.submit(new CallableTest());
		try{
			System.out.println("等待线程执行完成");
			System.out.println(future.get());//等待线程结束,并获取返回结果
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
}
3. Runnable和Callable有什么区别?

主要区别

  • Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型
  • Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
4. 线程和进程的区别
  • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即为一个进程的创建、运行以及消亡的过程。

  • 线程是比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程,多个线程共享进程的堆和方法区内存资源,每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。由于线程共享进程的内存,因此系统产生一个线程或者在多个线程之间切换工作时的负担比进程小得多,线程也称为轻量级进程。

  • 进程和线程最大的区别是,各进程是独立的,而各线程则不一定独立,因为同一进程中的多个线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护,进程则相反

5.进程之间的通信方式

1、管道,匿名管道,命名管道
2、信号
3、信号量
4、消息队列
5、共享内存
6、socket

5. 线程间通讯方式

1:同步(synchronized)

2:共享变量(volatile)

2:wait/notify()机制

3:管道通信就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

5. 什么是守护线程?

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

  • 守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
  • Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法。
6. java的线程大概有几种状态?

线程在运行的生命周期中的任何时刻只能是5 种不同状态的其中一种。

  • 初始状态(NEW):线程已经构建,尚未启动。
  • 运行状态(RUNNABLE):包括就绪(READY)和运行中(RUNNING)两种状态,统称为运行状态。
  • 阻塞状态(BLOCKED):线程被锁阻塞。
  • 等待状态(WAITING):线程需要等待其他线程做出特定动作(通知或中断)。
  • 终止状态(TERMINATED):当前线程已经执行完毕。
7. 说说与线程相关的方法
  • 加锁对象的 wait() 方法,使一个线程处于等待状态,并且释放所持有的对象的锁
  • 加锁对象的 notify() 方法,由 JVM 唤醒一个处于等待状态的线程,具体哪个线程不确定,且与优先级无关
  • 加锁对象的 notityAll() 方法,唤醒所有处入等待状态的线程,让它们重新竞争对象的锁
  • 线程的 sleep() 方法,使一个正在运行的线程处于睡眠状态,是静态方法,调用此方法要捕捉 InterruptedException 异常
  • JDK 1.5 开始通过 Lock 接口提供了显式锁机制,丰富了锁的功能,可以尝试加锁和加锁超时。Lock 接口中定义了加锁 lock()、释放锁 unlock() 方法 和 newCondition() 产生用于线程之间通信的 Condition 对象的方法
  • JDK 1.5 开始提供了信号量 Semaphore 机制,信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须调用 Semaphore 对象的 acquire() 方法得到信号量的许可;在完成对资源的访问后,线程必须调用 Semaphore 对象的 release() 方法向信号量归还许可
8. sleep 和 wait方法的区别?
  • sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
  • wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
9. 线程的 run() 和 start() 有什么区别?
  • start方法用于启动线程,真正实现了多线程运行。在调用T线程的sturt方法后.线程会在后台执行,无须等待run方法体的代码执行完毕,就可以继续执行下面的代码。

  • 在通过调用Thrend 类的start方法启动一个线程时,此线程处于就绪状态,并没有运行。

  • run方法也叫作线程体。包含了要执行的线程的逻辑代码,在调用run 方法后.线程会进人运行状态,开始运行run方法中的代码。在run 方法运行结束后,该线程终止,CPU再次调度其他线程。

10. sleep()和yield()有什么区别?
  • sleep() 方法给其他线程运行机会时不考虑线程的优先级;yield() 方法只会给相同优先级或更高优先级的线程运行的机会
  • 线程执行 sleep() 方法后进入超时等待状态;线程执行 yield() 方法转入就绪状态,可能马上又得得到执行
  • sleep() 方法声明抛出 InterruptedException;yield() 方法没有声明抛出异常
  • sleep() 方法需要指定时间参数;yield() 方法出让 CPU 的执行权时间由 JVM 控制
11. 死锁的四个条件?
  • 互斥条件:一个锁一次只能由一个进程占有
  • 不可剥夺条件:一个进程占有的资源在使用完之前不可以被其他进程剥夺,只能由该进程释放之后才能被其他进程获取。
  • 请求和保持条件:一个进程在申请资源的同时保持已经占有的资源不释放。
  • 循环等待条件:同时需要A、B两个资源的进程分别占有了A和B,形成了两个进程都阻塞并等待对方释放资源的状态。
12. 怎么在开发中避免死锁?

避免死锁:
对于以上 4 个条件,只要破坏其中一个条件,就可以避免死锁的发生。

对于第一个条件 “互斥” 是不能破坏的,因为加锁就是为了保证互斥。

其他三个条件,我们可以尝试

  • 一次性申请所有的资源,破坏 “占有且等待” 条件
  • 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 “不可抢占” 条件
  • 按序申请资源,破坏 “循环等待” 条件

编程中的最佳实践:

  • 使用 Lock 的 tryLock(long timeout, TimeUnit unit)的方法,设置超时时间,超时可以退出防止死锁
  • 尽量使用并发工具类代替加锁
  • 尽量降低锁的使用粒度
  • 尽量减少同步的代码块
13. 怎么检测死锁?

jstack -l可以查看堆栈运行的状态,-l会显示锁状态,里面会报告死锁。

14. 怎么解决死锁?

1、系统重启
2、撤销代价比较低的线程,例如低优先级的线程

15.线程安全是什么?如何保证线程安全?

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。

16. 10个线程,一个线程出错,怎么通知其它的线程。

重写了自定义线程组的uncaughtException()方法后,加上相应的中断操作和判断,是可以做到当某个线程出现异常然后中断时,其他的线程也会马上运行结束,不过这里的其他线程指得是当前和出现异常的线程在同一线程组的线程们,而在异常线程之后新加入线程组的线程就不会被影响到的,从正常线程可以持续运行下去就可以证明这点,所以即使采取了异常中断的手段,但是当线程组内的某个线程出现异常,只会影响到当前在线程组内的线程的运行情况,异常之后才加入到线程组的线程就不会被停止了。

17. 如何避免指令重排序

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。
18. volatile除了避免指令重排序还有什么功能

Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性。

19. 说说volatile关键字

volatile 可以说是 JVM 提供的最轻量级的同步机制,当一个变量定义为volatile之后,它将具备两种特性:

  • 保证此变量对所有线程的可见性。
    而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。注意,volatile 虽然保证了可见性,但是 Java 里面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的。而 synchronized 关键字则是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得线程安全的。

  • 禁止指令重排序优化。
    普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

20. ThreadLocal有什么作用?有哪些使用场景?

ThreadLocal 是线程本地存储,在每个线程都创建了一个ThreadLocalMap对象,每个线程可以访问自己内部ThreadLocalMal对象内的value. 通过这种方式,避免资源在多线程见共享。

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都会获得该变量的副本
副本之间相互独立,这样每一个线程都可以随意更改自己的变量副本,而不会对其他线程产生影响。

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

21. 高并发下,如何安全地修改同一行数据?
  • 可以将数据加载到缓存中,利用 CAS 方式进行更新
  • 也可以将所有请求放到同一个消息队列里,异步返回,按顺序执行更新

注意:

  • 如果使用悲观锁,在并发请求量很大的情况下,会导致服务和数据连接数耗尽,系统卡死
22. synchronized 和 volatile 的区别是什么?

作用:

  • synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
  • volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

区别:

  • synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量。
  • synchronized 可以保证线程间的有序性(个人猜测是无法保证线程内的有序性,即线程内的代码可能被 CPU 指令重排序)、原子性和可见性;volatile 只保证了可见性和有序性,无法保证原子性。
  • synchronized 线程阻塞,volatile 线程不阻塞。
  • volatile 本质是告诉 jvm 当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到该变量其他线程被阻塞。
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
23. synchronized 和 Lock 有什么区别?
  • 实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
  • 是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁
  • 是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
  • 获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
  • 功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率
24. synchronized 和 ReentrantLock 区别是什么?
  • synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
  • synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
  • synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
  • synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
  • synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
  • synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放

补充一个相同点:都可以做到同一线程,同一把锁,可重入代码块。

25.synchronized锁升级
26.线程池的参数
27.线程池的拒绝策略

四、计算机网络

1. get 和 post的区别
  • Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。 但是这种做法也不时绝对的,大部分人的做法也是按照上面的说法来的,但是也可以在get请求加上 request body,给 post请求带上 URL 参数。
  • Get请求提交的url中的数据最多只能是2048字节,这个限制是浏览器或者服务器给添加的,http协议并没有对url长度进行限制,目的是为了保证服务器和浏览器能够正常运行,防止有人恶意发送请求。Post请求则没有大小限制。
  • Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。
  • Get执行效率却比Post方法好。Get是form提交的默认方法。
  • GET产生一个TCP数据包;POST产生两个TCP数据包。
  • 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
  • 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
2. TCP和UDP的区别?tcp拥塞控制和流量控制如何实现?

TCP,Transmission Control Protocol 的缩写,即传输控制协议。

  • 面向连接,即必须在双方建立可靠连接之后,才会收发数据
  • 信息包头 20 个字节
  • 建立可靠连接需要经过3次握手
  • 断开连接需要经过4次挥手
  • 需要维护连接状态
  • 报文头里面的确认序号、累计确认及超时重传机制能保证不丢包、不重复、按序到达
  • 拥有流量控制及拥塞控制的机制

UDP,User Data Protocol 的缩写,即用户数据报协议。

  • 不建立可靠连接,无需维护连接状态
  • 信息包头 8 个字节
  • 接收端,UDP 把消息段放在队列中,应用程序从队列读消息
  • 不受拥挤控制算法的调节
  • 传送数据的速度受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制
  • 面向数据报,不保证接收端一定能收到

流量控制
TCP 利用滑动窗口实现流量控制。
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
拥塞控制
为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。
TCP的拥塞控制采用了四种算法,即 慢开始 、 拥塞避免 、快重传 和 快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。

3. 输入一次url过程,用到哪些协议?

  1. DNS 解析:浏览器查询 DNS,获取域名对应的 IP 地址:具体过程包括浏览器搜索自身的 DNS 缓存、搜索操作系统的 DNS 缓存、读取本地的 Host 文件和向本地 DNS 服务器进行查询等。对于向本地 DNS 服务器进行查询,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析(此解析具有权威性);如果要查询的域名不由本地 DNS 服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析(此解析不具有权威性)。如果本地域名服务器并未缓存该网址映射关系,那么将根据其设置发起递归查询或者迭代查询;

  2. TCP 连接:浏览器获得域名对应的 IP 地址以后,浏览器向服务器请求建立链接,发起三次握手;

  3. 发送 HTTP 请求:TCP 连接建立起来后,浏览器向服务器发送 HTTP 请求;

  4. 服务器处理请求并返回 HTTP 报文:服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;

  5. 浏览器解析渲染页面:浏览器解析并渲染视图,若遇到对 js 文件、css 文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。

  6. 连接结束。

4. 说一说HTTP和HTTPS的区别。
  • 安全性上,HTTPS是安全超文本协议,在HTTP基础上有更强的安全性。简单来说,HTTPS是使用TLS/SSL加密的HTTP协议
  • 申请证书上,HTTPS需要使用ca申请证书
  • 传输协议上, HTTP是超文本传输协议,明文传输;HTTPS是具有安全性的 SSL 加密传输协议
  • 连接方式与端口上,http的连接简单,是无状态的,端口是 80; https 在http的基础上使用了ssl协议进行加密传输,端口是 443
5. 讲一下HTTPS原理。

(1)发起请求:客户端在通过TCP和服务器建立连接之后(默认使用443端口),发出一个请求证书的消息给服务器,在该请求消息里包含自己可实现的算法列表和其他需要的消息。

(2)证书返回:服务器端在收到消息后回应客户端并返回证书,在证书中包含服务器信息、域名、申请证书的公司、公钥、数据加密算法等。

(3)证书验证:客户端在收到证书后,判断证书签发机构是否正确,并使用该签发机构的公钥确认签名是否有效,客户端还会确保在证书中列出的域名为正在连接的域名。如果客户端确认证书有效,则生成对称密钥,并使用公钥将对称密钥加密。

(4)密钥交换:客户端将加密后的对称密钥发送给服务器,服务器在接收到对称密钥后使用私钥解密。

(5)数据传输:经过上述步骤,客户端和服务器就完成了密钥对的交换,在之后的数据传输过程中,客户端和服务端就可以基于对称加密(加密和解密使用相同密钥的加密算法)将数据加密后在网络上传输,保证了网络数据传输的安全性。

6. Https 如何防止中间人攻击?

什么是中间人攻击?
在手机或者电脑和服务器建立连接的时候,攻击者通过工具或者技术手段将自己位于两端之间,获取数据,进行监听活动的就是中间人攻击。

有哪几种攻击:
1.嗅探,监听获取连接数据包。

2.数据包注入,将恶意数据包注入常规数据包里面。因为是合法通讯流,用户不易察觉。

3.会话劫持,登录退出银行账户就是一个会话。会话中有很多重要信息,通过监听或者控制会话。

4.SSL剥离,通过攻击SSL连接,使其脱落,达到将https,转化为一般的http。

Https如何防范中间人攻击:
详见Https加密原理。主要是通过CA证书来保证安全。

7. TCP的三次握手,四次挥手

客户端–发送带有 SYN 标志的数据包–一次握手–服务端
服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端

客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送
服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号
服务器-关闭与客户端的连接,发送一个 FIN 给客户端
客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1

8. 详细介绍下TCP。

TCP:传输控制协议,是一种面向连接的可靠传输协议。TCP为应用程序提供一种面向连接的、可靠的服务。(面向连接:传输前进行沟通和协商,确保互相可以/愿意发送数据)

TCP三次握手能够保证面向连接,面向连接是可靠的,并不能保证TCP传输是可靠的,三次握手是TCP传输之前的一个过程,那么如何保证TCP是可靠的:

  • 面向连接的传输(准备好了传)
  • 最大报文段长度(一共传多少)
  • 传输确认机制(TCP发送的每一个数据都要进行确认,丢没丢)
  • 首部和数据的校验和(错没错)
  • 重传输(若没有收到ACK,则在等一定时间后重新发送数据)
  • 重排序(对分片用序列号进行重新排序)
  • 流量控制(量力而行,按需传递)

在这里插入图片描述

9. 解释一下HTTP长连接和短连接?

在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。

但从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。

HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

10. 什么是 Cookie 和 Session ?

什么是 Cookie

HTTP Cookie(也叫 Web Cookie或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

什么是 Session

Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。

11. Cookie和Session的区别?
  • 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。
  • 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
  • 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,-Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
  • 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
  • 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。
12. SQL注入是什么,如何避免SQL注入?

SQL 注入就是在用户输入的字符串中加入 SQL 语句,如果在设计不良的程序中忽略了检查,那么这些注入进去的 SQL 语句就会被数据库服务器误认为是正常的 SQL 语句而运行,攻击者就可以执行计划外的命令或访问未被授权的数据。

SQL注入的原理主要有以下 4 点

  • 恶意拼接查询
  • 利用注释执行非法命令
  • 传入非法参数
  • 添加额外条件

避免SQL注入的一些方法:

  • 限制数据库权限,给用户提供仅仅能够满足其工作的最低权限。
  • 对进入数据库的特殊字符(’”\尖括号&*;等)转义处理。
  • 提供参数化查询接口,不要直接使用原生SQL。

五、JVM

1. 如何判断一个对象是否存活?

判断一个对象是否存活,分为两种算法1:引用计数法;2:可达性分析算法;

引用计数法
给每一个对象设置一个引用计数器,当有一个地方引用该对象的时候,引用计数器就+1,引用失效时,引用计数器就-1;当引用计数器为0的时候,就说明这个对象没有被引用,也就是垃圾对象,等待回收; 缺点:无法解决循环引用的问题,当A引用B,B也引用A的时候,此时AB对象的引用都不为0,此时也就无法垃圾回收,所以一般主流虚拟机都不采用这个方法;

可达性分析法
从一个被称为GC Roots的对象向下搜索,如果一个对象到GC Roots没有任何引用链相连接时,说明此对象不可用,在java中可以作为GC Roots的对象有以下几种:

  • 虚拟机栈中引用的对象

  • 方法区类静态属性引用的变量

  • 方法区常量池引用的对象

  • 本地方法栈JNI引用的对象

但一个对象满足上述条件的时候,不会马上被回收,还需要进行两次标记;第一次标记:判断当前对象是否有finalize()方法并且该方法没有被执行过,若不存在则标记为垃圾对象,等待回收;若有的话,则进行第二次标记;第二次标记将当前对象放入F-Queue队列,并生成一个finalize线程去执行该方法,虚拟机不保证该方法一定会被执行,这是因为如果线程执行缓慢或进入了死锁,会导致回收系统的崩溃;如果执行了finalize方法之后仍然没有与GC Roots有直接或者间接的引用,则该对象会被回收;

2. 说一下finalize方法。

Java提供finalize()方法,垃圾回收器准备释放内存的时候,会先调用finalize()。
(1).对象不一定会被回收。
(2).垃圾回收不是析构函数。
(3).垃圾回收只与内存有关。
(4).垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。

3. 说一下垃圾回收机制?什么时候垃圾回收?

先描述一下Java堆内存划分。

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。 新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。

新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。

老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。

再描述它们之间转化流程。

  • 对象优先在Eden分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

  • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;

  • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;

  • 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代。GC年龄的阀值可以通过参数 -XX:MaxTenuringThreshold 设置,默认为 15;

  • 动态对象年龄判定:Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%;

  • Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。

  • 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和老年代。

4. 堆内存和栈内存有什么区别?堆和栈哪个快?什么变量存在栈里面?

栈内存和堆内存都是存储数据的地方。
栈内存中存储的值的大小是固定的,堆内存中存储值的大小不固定的。

:由系统自动分配,速度较快。但程序员是无法控制的。
:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

5. Java中类加载过程是什么样的?

类加载的步骤为,加载 -> 验证 -> 准备 -> 解析 -> 初始化。

1、加载:

  • 获取类的二进制字节流
  • 将字节流代表的静态存储结构转化为方法区运行时数据结构
  • 在堆中生成class字节码对象

2、验证:连接过程的第一步,确保 class 文件的字节流中的信息符合当前 JVM 的要求,不会危害 JVM 的安全

3、准备:为类的静态变量分配内存并将其初始化为默认值

4、解析:JVM 将常量池内符号引用替换成直接引用的过程

5、初始化:执行类构造器的初始化的过程

6. JVM 如何确定垃圾对象:

JVM 采用的是可达性分析算法,通过 GC Roots 来判定对象是否存活,从 GC Roots 向下追溯、搜索,会产生 Reference Chain。当一个对象不能和任何一个 GC Root 产生关系时,就判定为垃圾。

软引用和弱引用,也会影响对象的回收。内存不足时会回收软引用对象;GC 时会回收弱引用对象。

7. Java中的垃圾回收算法有哪些?

Java中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法;
标记清除法
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:在遍历一遍,将所有标记的对象回收掉;
特点
效率不行,标记和清除的效率都不高;
标记和清除后会产生大量的不连续的空间分片,
可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;

标记整理法
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉;
特点
适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;

复制算法
将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除;
特点
不会产生空间碎片;内存使用率极低;

分代收集算法
根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;

8. 说说JVM内存区域分为几大块,分别讲一下

Java 虚拟机在执行 Java 程序的过程中会把他所管理的内存划分为若干个不同的数据区域:

  • 程序计数器:可以看作是当前线程所执行的字节码文件(class)的行号指示器,它会记录执行痕迹,是每个线程私有的
  • 方法区:主要存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据,该区域是被线程共享的,很少发生垃圾回收
  • :栈是运行时创建的,是线程私有的,生命周期与线程相同,存储声明的变量
  • 本地方法栈:为 native 方法服务,native 方法是一种由非 java 语言实现的 java 方法,与 java 环境外交互,如可以用本地方法与操作系统交互
  • :堆是所有线程共享的一块内存,是在 java 虚拟机启动时创建的,几乎所有对象实例都在此创建,所以经常发生垃圾回收操作

JDK8 之前,Hotspot 中方法区的实现是永久代(Perm)

JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。

9. 什么是双亲委派模型?为什么需要双亲委派模型?

双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

为了防止内存中出现多个相同的字节码;因为如果没有双亲委派的话,用户就可以自己定义一个java.lang.String类,那么就无法保证类的唯一性。

10. 说一下 JVM 调优的命令?
  • jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat:jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap:jmap(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还阔以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文件。 jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
  • jhat:jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
  • jstack:jstack用于生成java虚拟机当前时刻的线程快照。jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
11. Minor GC 和 Full GC 有什么不同呢?

Minor GC:只收集新生代的GC。

Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。

Minor GC触发条件:
当Eden区满时,触发Minor GC。

Full GC触发条件:

  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。

  • 老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)。

  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

  • 调用System.gc时,系统建议执行Full GC,但是不必然执行。

12. 有哪几种垃圾回收器,各自的优缺点是什么?
  • 垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

  • Serial:单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,是client级别的默认GC方式。

  • ParNew:Serial收集器的多线程版本,也需要stop the world,复制算

  • Parallel Scavenge:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;

  • Serial Old:Serial收集器的老年代版本,单线程收集器,使用标记整理算法。

  • Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。

  • CMS:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片;

  • G1:标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿;G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率;

13. 强引用、软引用、弱引用、虚引用是什么,有什么区别?
  • 强引用,就是普通的对象引用关系,如 String s = new String(“ConstXiong”)

  • 软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。SoftReference 实现

  • 弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。WeakReference 实现

  • 虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。PhantomReference 实现

14. 谈谈对 OOM 的认识?如何排查 OOM 的问题?

除了程序计数器,其他内存区域都有 OOM 的风险。

  • 栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM

  • Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;

  • 堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;

  • 方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等;

  • 直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。

排查 OOM 的方法:

  • 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;

  • 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;

  • 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。

15. 什么情况下会发生栈内存溢出?

1、栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;
2、当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题;
3、调整参数-xss去调整jvm栈的大小

16.什么时候会发生内存泄漏

内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。具体主要有如下几大类:

  1. 静态集合类引起内存泄漏:像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
  2. 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
  3. 监听器:在java编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
  4. 各种连接:数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
  5. 内部类和外部模块的引用:内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。外部模块不经意的引用也会引起内存泄漏。
  6. 单例模式:不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏,
17.JVM调优

jps,显示系统所有虚拟机进程信息的命令行工具
jstat,监视分析虚拟机运行状态的命令行工具
jinfo,查看和调整虚拟机参数的命令行工具
jmap,生成虚拟机堆内存转储快照的命令行工具
jhat,显示和分析虚拟机的转储快照文件的命令行工具
jstack,生成虚拟机的线程快照的命令行工具
jcmd,虚拟机诊断工具,JDK 7 提供
jhsdb,基于服务性代理实现的进程外可视化调试工具,JDK 9 提供
JConsole,基于JMX的可视化监视和管理工具
jvisualvm,图形化虚拟机使用情况的分析工具
Java Mission Control,监控和管理 Java 应用程序的工具

MAT,Memory Analyzer Tool,虚拟机内存分析工具
vjtools,唯品会的包含核心类库与问题分析工具
arthas,阿里开源的 Java 诊断工具
greys,JVM进程执行过程中的异常诊断工具
GCHisto,GC 分析工具
GCViewer,GC 日志文件分析工具
GCeasy,在线版 GC 日志文件分析工具
JProfiler,检查、监控、追踪 Java 性能的工具
BTrace,基于动态字节码修改技术(Hotswap)实现的Java程序追踪与分析工具
下面可以重点体验下:

JDK 自带的命令行工具方便快捷,不是特别复杂的问题可以快速定位;
阿里的 arthas 命令行也不错;
可视化工具 MAT、JProfiler 比较强大。

18. 生产环境服务器变慢,如何诊断处理?

使用 top 指令,服务器中 CPU 和 内存的使用情况,-H 可以按 CPU 使用率降序,-M 内存使用率降序。排除其他进程占用过高的硬件资源,对 Java 服务造成影响。

如果发现 CPU 使用过高,可以使用 top 指令查出 JVM 中占用 CPU 过高的线程,通过 jstack 找到对应的线程代码调用,排查出问题代码。

如果发现内存使用率比较高,可以 dump 出 JVM 堆内存,然后借助 MAT 进行分析,查出大对象或者占用最多的对象来自哪里,为什么会长时间占用这么多;如果 dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题,需要借助操作系统指令 pmap 查出进程的内存分配情况、gdb dump 出具体内存信息、perf 查看本地函数调用等。

如果 CPU 和 内存使用率都很正常,那就需要进一步开启 GC 日志,分析用户线程暂停的时间、各部分内存区域 GC 次数和时间等指标,可以借助 jstat 或可视化工具 GCeasy 等,如果问题出在 GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调优、使用更适合的垃圾收集器;分析 jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启 jmx,使用 visualmv 等可视化工具远程监控与分析

19.生产环境 CPU 占用过高,你如何解决?
  1. top + H 指令找出占用 CPU 最高的进程的 pid

  2. top -H -p在该进程中找到,哪些线程占用的 CPU 最高的线程,记录下 tid

  3. jstack -l >threads.txt,导出进程的线程栈信息到文本,导出出现异常的话,加上 -F 参数

  4. 将 tid 转换为十六进制,在 threads.txt 中搜索,查到对应的线程代码执行栈,在代码中查找占 CPU 比较高的原因。其中
    tid 转十六进制,可以借助 Linux 的 printf “%x” tid 指令

我用上述方法查到过,jvm 多条线程疯狂 full gc 导致的CPU 100% 的问题和 JDK1.6 HashMap 并发 put 导致线程 CPU 100% 的问题

六、数据库

1. 说说mysql的存储引擎

InnoDB

  • 默认事务型引擎,被广泛使用的存储引擎
  • 数据存储在共享表空间,即多个表和索引都存储在一个表空间中,可通过配置文件修改
  • 主键查询的性能高于其他类型的存储引擎
  • 内部做了很多优化,如:从磁盘读取数据时会自动构建hash索引,插入数据时自动构建插入缓冲区
  • 通过一些机制和工具支持真正的热备份
  • 支持崩溃后的安全恢复
  • 支持行级锁
  • 支持外键

MyISAM

  • 拥有全文索引、压缩、空间函数
  • 不支持事务和行级锁、不支持崩溃后的安全恢复
  • 表存储在两个文件:MYD 和 MYI
  • 设计简单,某些场景下性能很好,例如获取整个表有多少条数据,性能很高
2. 讲下索引以及应用场景
  • 当我们使用order by将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序,这个操作是很影响性能的。但是如果我们对该字段建立索引,那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。
  • 对join语句匹配关系(on)涉及的字段建立索引能够提高效率。
  • 查找符合where条件的记录时
  • 如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在select后只写必要的查询字段,以增加索引覆盖的几率。
3. 索引的作用?索引有什么缺点?

索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。

索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。

优点 :

  • 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
  • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

缺点 :

  • 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
  • 索引需要使用物理文件存储,也会耗费一定空间。
4. 索引的设计原则

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

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的数据类型的列不要建立索引。

5. 为什么MySQL 没有使用Hash作为索引的数据结构呢?

1.Hash 冲突问题 :我们上面也提到过Hash 冲突了,不过对于数据库来说这还不算最大的缺点。

2.Hash 索引不支持顺序和范围查询(Hash 索引不支持顺序和范围查询是它最大的缺点: 假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。

Hash的存储结构是key-value形式存在数组中,对数据进行Hash(散列)运算,然后将哈希结果作为文件指针,可以从索引文件中获得数据的文件指针,再到数据文件中获取到数据,查询效率非常高,主流的Hash算法有MD5、SHA256等等。无法解决范围查询的场景,比如 select count(id) from sus_user where id >10;因此Hash这种索引结构只能针对字段名=目标值的场景使用。不适合模糊查询的场景。

6. MySQL索引使用的什么数据结构,B树和B+树的区别

目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。

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

B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。

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

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

7. 索引为什么采用B+树的数据结构,而不使用二叉树或者红黑树

红黑树也叫平衡二叉树,它不仅继承了二叉树的优点,而且解决了上面二叉树遇到的自增整形索引的问题,而且红黑树会左旋、右旋对结构进行调整,始终保证左子节点数 < 父节点数 < 右子节点数的规则。但在数据量大的时候,深度也很大。如果我们有很多数据,那么树的深度依然会很大,可能就会超过十几二十层以上,对我们的磁盘寻址不利,依然会花费很多时间查找。

B+树存储结构,只有叶子节点存储数据。B+树结构没有在所有的节点里存储记录数据,而是只在最下层的叶子节点存储,上层的所有非叶子节点只存放索引信息,这样的结构可以让单个节点存放下更多索引值,提高命中目标记录的几率。这种结构会在上层非叶子节点存储一部分冗余数据,但是这样的缺点都是可以容忍的,因为冗余的都是索引数据,不会对内存造成大的负担。

8. 聚簇索引和非聚簇索引这两个概念怎么理解?
9. mysql索引优化相关方法,联合索引应该把什么字段放在第一个位置?

索引优化的方法有以下,第一:字段选择性。查询条件含有多个字段时,不要在选择性很低字段上创建索引,可通过创建组合索引来增强低字段选择性和避免选择性很低字段创建索引带来副作用。正确索引会提高sql查询速度,过多索引会增加优化器选择索引的代价,不要滥用索引;第二:Explain优化查询检测。EXPLAIN可以帮助开发人员分析SQL问题,explain显示了mysql如何使用索引来处理select语句以及连接表,可以帮助选择更好的索引和写出更优化的查询语句。

在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。

10. mysql的最左原则吗?
  • 顾名思义,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
  • 最左前缀匹配原则,非常重要的原则,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的顺序可以任意调整。
  • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
11. 脏读、幻读、不可重复读指什么?
  • 脏读:一个事务读取另外一个事务还没有提交的数据。
  • 不可重复读:一个事务内,两次相同条件的查询返回了不同的结果。
  • 幻读:同一个事务中,一条数据出现在这次查询的结果集里,却没有出现在之前的查询结果集中。例如,在一个事务中进行了同一个查询运行了两次,期间被另外一个事务提交插入一行或修改查询条件匹配的一行。它比不可重复读更难防范,因为锁定第一个查询结果集的所有行并不能阻止导致幻象出现的更改。
12. 数据库事务的四个特性:

事务具备ACID四种特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的英文缩写。

  • 原子性(Atomicity)
    事务最基本的操作单元,要么全部成功,要么全部失败,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。

  • 一致性(Consistency)
    事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

  • 隔离性(Isolation)
    指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

  • 持久性(Durability)
    指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

13. 说说sql的事务隔离级别,具体的应用场景
  • 读未提交(Read Uncommitted):是最低的事务隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。会出现脏读,幻读,不可重复读,所有并发问题都可能遇到。
  • 读已提交(Read Committed):保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。不会出现脏读现象,但是会出现幻读,不可重复读。
  • 可重复读(Repeatable Read):这种事务隔离级别可以防止脏读,不可重复读,但是可能会出现幻象读。它除了保证一个事务不能被另外一个事务读取未提交的数据之外还避免了不可重复读。
  • 串行化(Serializable):这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。防止脏读、不可重复读、幻象读。
14. 说说数据库的乐观锁和悲观锁?

1.乐观锁

乐观锁在读数据时,认为别人不会去写其所读的数据:悲观锁就刚好相反,觉得自己读数据时,别人可能刚好在写自己刚读的数据,态度比较保守;时间戳在操作数据时不加锁,而是通过时间戳来控制并发出现的问题。

2.悲观锁

悲观锁指在其修改某条数据时,不允许别人读取该数据,直到自己的整个事务都提交并释放锁,其他用户才能访问该数据。悲观锁又可分为排它锁(写锁)和共享锁(读锁)。

15.数据库怎么进行优化
16. 数据库的回表和解决办法
17. MVCC是怎么实现的
18.主键索引和唯一索引的区别

七、设计模式

1. 你最熟悉的设计模式 ?

单例模式
保证一个类只有一个实例,并且提供一个访问该全局访问点
2.那些地方用到了单例模式
网站的计数器,一般也是采用单例模式实现,否则难以同步。
应用程序的日志应用,一般都是单例模式实现,只有一个实例去操作才好,否则内容不好追加显示。
多线程的线程池的设计一般也是采用单例模式,因为线程池要方便对池中的线程进行控制
Windows的(任务管理器)就是很典型的单例模式,他不能打开俩个
windows的(回收站)也是典型的单例应用。在整个系统运行过程中,回收站只维护一个实例。
3.单例优缺点
优点:
在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例
单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
提供了对唯一实例的受控访问。
由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
允许可变数目的实例。
避免对共享资源的多重占用。
缺点:
不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
单例类的职责过重,在一定程度上违背了“单一职责原则”。
滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
4.单例模式使用注意事项
使用时不能用反射模式创建单例,否则会实例化一个新的对象
使用懒单例模式时注意线程安全问题
饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)
5.单例创建方式
(主要使用懒汉和懒汉式)
饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。
懒汉式: 类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。
静态内部方式:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。
枚举单例: 使用枚举实现单例模式 优点:实现简单、调用效率高,枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点没有延迟加载。
双重检测锁方式 (因为JVM本质重排序的原因,可能会初始化多次,不推荐使用)

2. 懒汉式你会怎么写,懒汉式实例化在哪,构造函数的权限?

1.饿汉式
饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。

package com.lijie;

//饿汉式
public class Demo1 {

    // 类初始化时,会立即加载该对象,线程安全,调用效率高
    private static Demo1 demo1 = new Demo1();

    private Demo1() {
        System.out.println("私有Demo1构造参数初始化");
    }

    public static Demo1 getInstance() {
        return demo1;
    }

    public static void main(String[] args) {
        Demo1 s1 = Demo1.getInstance();
        Demo1 s2 = Demo1.getInstance();
        System.out.println(s1 == s2);
    }
}

2.懒汉式
懒汉式: 类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。

package com.lijie;

//懒汉式
public class Demo2 {

    //类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象。
    private static Demo2 demo2;

    private Demo2() {
        System.out.println("私有Demo2构造参数初始化");
    }

    public synchronized static Demo2 getInstance() {
        if (demo2 == null) {
            demo2 = new Demo2();
        }
        return demo2;
    }

    public static void main(String[] args) {
        Demo2 s1 = Demo2.getInstance();
        Demo2 s2 = Demo2.getInstance();
        System.out.println(s1 == s2);
    }
}
3. 单例模式的饿汉式和懒汉式及区别

饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。
懒汉式: 类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。

4. 常用的设计模式?

创建型

  • 工厂模式与抽象工厂模式 (Factory Pattern)(Abstract Factory Pattern)
  • 单例模式 (Singleton Pattern)
  • 建造者模式 (Builder Pattern)
  • 原型模式 (Prototype Pattern)

结构型

  • 适配器模式 (Adapter Pattern)
  • 装饰器模式 (Decorator Pattern)
  • 桥接模式 (Bridge Pattern)
  • 外观模式 (Facade Pattern)
  • 代理模式 (Proxy Pattern)
  • 过滤器模式 (Filter、Criteria Pattern)
  • 组合模式 (Composite Pattern)
  • 享元模式 (Flyweight Pattern)

行为型

  • 责任链模式(Chain of Responsibility Pattern)
  • 观察者模式(Observer Pattern)
  • 模板模式(Template Pattern)
  • 命令模式(Command Pattern)
  • 解释器模式(Interpreter Pattern)
  • 迭代器模式(Iterator Pattern)
  • 中介者模式(Mediator Pattern)
  • 策略模式(Strategy Pattern)
  • 状态模式(State Pattern)
  • 备忘录模式(Memento Pattern)
  • 空对象模式(Null Object Pattern)
5. 简单工厂和抽象工厂有什么区别?
  • 简单工厂模式
    是由一个工厂对象创建产品实例,简单工厂模式的工厂类一般是使用静态方法,通过不同的参数的创建不同的对象实例
    可以生产结构中的任意产品,不能增加新的产品

  • 抽象工厂模式
    提供一个创建一系列相关或相互依赖对象的接口,而无需制定他们具体的类,生产多个系列产品
    生产不同产品族的全部产品,不能新增产品,可以新增产品族

6. 说一说设计模式中的代理模式?

代理模式指为对象提供-种通过代理的方式来访问并控制该对象行为的方法。在客户端不适合或者不能够直接引用一-个对象时,可以通过该对象的代理对象来实现对该对象的访问,可以将该代理对象理解为客户端和目标对象之间的中介者。

在现实生活也能看到代理模式的身影,比如企业会把五险一金业务交给第三方人力资源公司去做,因为人力资源公司对五险一金 业务更加熟悉。

在代理模式下有两种角色,一种是被代理者,一种是代理( Proxy),在被代理者需要做一项工作时,不用自己做,而是交给代理做。比如企业在招人时,不用自己去人才市场上找,可以通过代理(猎头公司)去找,代理有候选人池,可根据企业的需求筛选出合适的候选人去返回给企业。

7.手写代理模式的实现。
8. 说一说设计模式中的适配器模式?

我们常常在开发中遇到各个系统之间的对接问题,然而每个系统的数据模型或多或少均存在差别,因此可能存在改变现有对象模型的情况,这将影响到系统的稳定。若想在不改变原有代码结构(类的结构)的情况下完成友好对接,就需要用到适配器模式。

适配器模式(Adapter Pattern)通过定义一个适配器类作为两个不兼容的接口之间的桥梁,将一个类的接口转换成用户期望的另一个接口,使得两个或多个原本不兼容的接口可以基于适配器类一起工作。

适配器模式主要通过适配器类实现各个接口之间的兼容,该类通过依赖注人或者继承实现各个接口的功能并对外统一提供服务。

在适配器模式的实现中有三种角色: Source、 Targetable、 Adapter。 Source 是待适配的类,Targetable 是目标接口,Adapter 是适配器。我们在具体应用中通过Adapter 将Source的功能扩展到Targetable,以实现接口的兼容。适配器的实现主要分为三类:类适配器模式、对象适配器模式、接口适配器模式。

9.说一说策略模式

八、框架

1. Spring的特点?

Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。

比如说 Spring 自带 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。

2.谈谈自己对于 Spring IoC 的了解

IoC(Inverse of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spirng 特有,在其他语言中也有应用。

控制:指的是对象创建(实例化、管理)的权力

反转:控制权交给外部环境(Spring 框架、IoC 容器)

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。

Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。

3.Spring AOP的实现原理?具体应用在哪些方面?举个例子?

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib ,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理

4.Spring 框架中用到了哪些设计模式?

工厂设计模式: Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。

代理设计模式: Spring AOP 功能的实现。

单例设计模式: Spring 中的 Bean 默认都是单例的。

模板方法模式: Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。

包装器设计模式: 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。

观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。

适配器模式: Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。

5. Springboot的自动装配原理
6.Springboot的常用注解

1、@SpringBootApplication

包含@Configuration、@EnableAutoConfiguration、@ComponentScan

通常用在主类上。

2、@Repository

用于标注数据访问组件,即DAO组件。

3、@Service

用于标注业务层组件。

4、@RestController

用于标注控制层组件(如struts中的action),包含@Controller和@ResponseBody

5、@ResponseBody

表示该方法的返回结果直接写入HTTP response body中

一般在异步获取数据时使用,在使用@RequestMapping后,返回值通常解析为跳转路径,加上@responsebody后返回结果不会被解析

为跳转路径,而是直接写入HTTP response body中。比如异步获取json数据,加上@responsebody后,会直接返回json数据。

6、@Component

泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。

7、@ComponentScan

组件扫描。相当于,如果扫描到有@Component @Controller @Service等这些注解的类,则把

这些类注册为bean。

8、@Configuration

指出该类是 Bean 配置的信息源,相当于XML中的,一般加在主类上。

9、@Bean

相当于XML中的,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理。

10、@EnableAutoConfiguration

让 Spring Boot 根据应用所声明的依赖来对 Spring 框架进行自动配置,一般加在主类上。

11、@AutoWired

byType方式。把配置好的Bean拿来用,完成属性、方法的组装,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。
当加上(required=false)时,就算找不到bean也不报错。

12、@Qualifier

当有多个同一类型的Bean时,可以用@Qualifier(“name”)来指定。与@Autowired配合使用

13、@Resource(name=“name”,type=“type”)

没有括号内内容的话,默认byName。与@Autowired干类似的事。

14、@RequestMapping

RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

5.Spring的Bean的创建流程
6.Spring的bean的生命周期
7.SpringMVC的原理

SpringMVC框架是以请求为驱动,围绕Servlet设计,将请求发给控制器,然后通过模型对象,分派器来展示请求结果视图。其中核心类是DispatcherServlet,它是一个Servlet,顶层是实现的Servlet接口。
在这里插入图片描述
流程说明:

(1)客户端(浏览器)发送请求,直接请求到DispatcherServlet。

(2)DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler。

(3)解析到对应的Handler后,开始由HandlerAdapter适配器处理。

(4)HandlerAdapter会根据Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑。

(5)处理器处理完业务后,会返回一个ModelAndView对象,Model是返回的数据对象,View是个逻辑上的View。

(6)ViewResolver会根据逻辑View查找实际的View。

(7)DispaterServlet把返回的Model传给View。

(8)通过View返回给请求者(浏览器)

8.forward和redirct的区别

1.forward是什么
forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容在发给浏览器,浏览器根本不知道服务器发送的内容从哪里来,所以它的地址栏还是原来的地址。

2.redirect是什么
redirect是服务器根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新的URL。所以redirct等于客户端向服务器发出两次request,同时也接收两次response。

3.应用的差别
forward:一般用于用户登录的时候,根据角色转发到响应的模块。

redirect:一般用于用户注销登录时返回和跳转到其他的网站等。

9.Mybatis的$和#的区别

#{ }可以防止Sql 注入,它会将所有传入的参数作为一个字符串来处理。
$ {} 则将传入的参数拼接到Sql上去执行,一般用于表名和字段名参数,$ 所对应的参数应该由服务器端提供,前端可以用参数进行选择,避免 Sql 注入的风险

九、Redis

1. redis都有哪些数据结构?
  • String字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,而且 其他几种数据结构都是在字符串类型基础上构建的,我们常使用的 set key value 命令就是字符串。常用在缓存、计数、共享Session、限速等。

  • Hash哈希:在Redis中,哈希类型是指键值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车。

  • List列表(双向链表):列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。

  • Set集合:集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

  • Sorted Set有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。

2. Redis的应用场景
  • 缓存
  • 共享Session
  • 消息队列系统
  • 分布式锁
3. redis的延时队列怎么实现?

1、使用zset数据结构存储,订单号为key,时间为score。
2、新增订单的时候,将订单号插入zset。
3、设定轮询,每分钟轮询一次zset,找出score小于当前秒数的数据,进行处理,然后将key在zset内删除。

4. Redis如何实现持久化?

Redis支持RDB和AOF两种持久化方式。

(1) RDB (Redis DataBase): rDB在指定的时间间隔内对数据进行快照存储。RDB的特点在于:文件格式紧凑,方便进行数据传输和数据恢复;在保存.rdb快照文件时父进程会fork 出一个子进程,由子进程完成具体的持久化工作,所以可以最大化Redis 的性能;同时,与AOF相比,在恢复大的数据集时会更快一些。

(2) AOF ( Append Of Flie): AOF记录对服务器的每次写操作,在Redis重启时会重放这些命令来恢复原数据。AOF命令以Redis 协议追加和保存每次写操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。AOF的特点有:可以使用不同的fsync 策略(无fsync、每秒fsync、每次写的时候fsync )将操作追加命令到文件中,操作效率高;同时,AOF文件是日志的格式,更容易被理解和操作。

5. 什么是Redis的事务,用来干什么?

Redis事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体,就是一个队列,当执行的时候,一次性按照添加顺序依次执行,中间不会被打断或者干扰。

一个队列中,一次性,顺序性,排他性的执行一系列命令。

6. Redis是阻塞式IO吗?怎么做到请求一个一个进行处理?

(1) 绝大部分请求是纯粹的内存操作(非常快速)
(2) 采用单线程,避免了不必要的上下文切换和竞争条件
(3) 非阻塞IO - IO多路复用

内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间 这3个条件不是相互独立的,特别是第一条,如果请求都是耗时的,采用单线程吞吐量及性能可想而知了。应该说redis为特殊的场景选择了合适的技术方案。

7. Redis缓存淘汰策略?是失效时间到了就立即淘汰吗?淘汰控制?

FIFO (First In First Out)先进先出原则
最先进入的缓存数据在缓存空间不够的情况下(超出最大元素限制时)会首先被清理出去

LFU (Less Frequently Uesd)最少使用原则
一直以来最少被使用的元素会被清理掉。意味着,要求缓存的元素有一个hit属性,在缓存空间不够的情况下,hit值最小的将会被清理出去

LRU (Least Recently Used)最近最少使用原则
缓存的元素有个时间戳,当缓存容量满了,而又要腾出新地方来缓存新的元素的时候,则现有缓存元素中时间戳离当前时间最远的元素将被清除出去

8. 什么是缓存雪崩和缓存穿透?

缓存雪崩
缓存雪崩指在同一时刻由于大量缓存失效,导致大量原本应该访问缓存的请求都去查询数据库,而对数据库的CPU和内存造成巨大压力,严重的话会导致数据库宕机,从而形成一系列连锁反应,使整个系统崩溃。

缓存穿透
缓存穿透指由于级存系统故陈或者用户频繁查询系统中不存在(在系统中不存在,在自然数据库和级存中都不存在)的数据,而这时请求穿过缓存不断被发送到数据库,导致数据库过载,进而引发一连串非发问题。

9. 如何解决 Redis 缓存雪崩问题
  • 请求加锁:对于并发量不是很多的应用,使用请求加锁排队的方案防止过多请求数据库。
  • 失效更新:为每一个缓存数据都增加过期标记来记录缓存数据是否失效,如果缓存标记失效,则更新数据缓存。
  • 设置不同的失效时间:为不同的数据设置不同的缓存失效时间,防止在同-时刻有大量的数据失效。
10. 如何解决 Redis 缓存穿透问题

常用的解决级存穿透问题的方法有布隆过滤器eache null策略。

  • 布隆过滤器: 指将所有可能存在的数据都映射列一个足够大的Bitmap中,在用户发起请求时首先经过布隆过滤器的拦截,一个一定不存在的数据会被这个布隆过泄器拦做,从而避免对底层存储系统业来企询上的压力。

  • cache null 策略:指如果一个查询返回的结果为null (可能见数据不存在,也可能是系统故陈),我们仍然级存这个nul结果,但它的过期时间会很短,通常不旭过5分钟;在用户再次青水该数据时血接返回nul,而不会继续访问数据库,从而有效保陈数据库的安全。其实cache null 策略的核心原理是:在级存中记录一个短暂的(数据过则时间内)数据在系统中是否存在的状态,如果不存在,则直接返回nl,不再在询数据内,从而避免复存穿遇列数据库上。

11.布隆过滤器的实现?怎么删除数据?
11. Redis如何实现分布式锁

实现思路与注意事项:

  • 设置合理的过期时间,解决忘记释放锁、甚至服务器宕机未释放锁的问题
  • 获取锁和设置过期时间,需要具有原子性,使用指令
SET key value NX PX milliseconds
NX 代表只有当键key不存在的时候才会设置key的值
PX 表示设置键 key 的过期时间,单位是毫秒
  • value 值随机设置,删除 value 前判断是否相等,解决当前线程可能释放其他线程加的锁的问题
  • lua 脚本可以解决,删除 value 时判断-删除,非原子操作的问题
12. Redis如何淘汰过期数据?

定期删除。在设置了过期时间的数据集中,隔一段时间挑几个检测,看是不是过期了,过期就删了;惰性删除。在get获取值的时候,会检测你要get的数据是否过期,过期就删了,返回null。如果定期删除漏掉了很多过期key,然后也没及时去查,也就没走惰性删除,如果大量过期key堆积在内存里,导致redis内存块耗尽了,则进行内存淘汰机制。
一共有六种淘汰策略:
1从设置过期时间(不一定已经过期)的数据集中挑选出最近最少使用的数据淘汰。
2从设置过期时间的数据集中挑选将要过期的数据淘汰,优先删除剩余时间短的key。
3从设置过期时间的数据集中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。
4从整个数据集中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。
5从数据集中选择任意数据淘汰。
6禁止驱逐数据,也就是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用该策略可以保证数据不被丢失,这也是系统默认的一种淘汰策略。

14. redis宕机了怎么办

分为在主从模式下区分来看:
如果是slave宕机,配置主从复制的时候才配置从的redis, 从的会从主的redis中读取主的redis的操作日志,来达到主从复制。在Redis中从库重新启动后会自动加入到主从架构中,自动完成同步数据;如果从数据库实现了持久化,可以直接连接到主的上面,只要实现增量备份(宕机到重新连接过程中,主的数据库发生数据操作,复制到从数据库),重新连接到主从架构中会实现增量同步。
如果Master宕机,要先确认是否做持久化,若没有做持久化,重新启动主的redis就会造成数据丢失。在slave数据上执行SLAVEOF ON ONE命令,来断开主从关系,并把slave升级为主库,此时重新启动主数据库,执行SLAVEOF,把它设置为从库,连接到主的redis上面做主从复制,自动备份数据。以上过程很容易配置错误,可以使用redis提供的哨兵机制来简化上面的操作。

15. zset的底层数据结构?什么是跳表?

Redis的zset为有序自动去重的集合数据类型,它的结构使用的是hash加上跳表的设计。hash结构用来存储value和score的关系,但是zset可以根据score的范围获取value的列表,这个实现就要用到跳跃表的设计。
跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度。

十、Kafka

1. Kafka做什么的?

Kafka为分布式流处理平台。流处理是指对不断产生的动态数据流实时处理,基于分布式内存,具有数据处理快速,高效,低延迟的特性。Kafka是一种消息队列,主要用来处理大量数据状态下的消息队列,一般用来做日志的处理,是一个分布式、支持分区的、多副本的,基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景。

2.Kafka 与传统 MQ 消息系统之间有三个关键区别

(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留

(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性

(3).Kafka 支持实时的流式处理

3.Kafka怎么保证顺序性?

Kafka 分布式的单位是 partition,同一个 partition 用一个 write ahead log 组织,所以可以保证 FIFO 的顺序。不同 partition 之间不能保证顺序。但是绝大多数用户都可以通过 message key 来定义,因为同一个 key 的 message 可以保证只发送到同一个 partition。

Kafka 中发送 1 条消息的时候,可以指定(topic, partition, key) 3 个参数。partiton 和 key 是可选的。如果你指定了 partition,那就是所有消息发往同 1个 partition,就是有序的。并且在消费端,Kafka 保证,1 个 partition 只能被1 个 consumer 消费。或者你指定 key( 比如 order id),具有同 1 个 key 的所有消息,会发往同 1 个 partition。

4. Kafka怎么避免数据丢失?

通过request.required.acks属性进行配置,有三个选项:

0代表:不进行消息接收是否成功的确认(默认值);

1代表:当Leader副本接收成功后,返回接收成功确认信息;

-1代表:当Leader和Follower副本都接收成功后,返回接收成功确认信息;acks设置为0时,不和Kafka集群进行消息接受确认,当网络发生异常等情况时,存在消息丢失的可能;想要不丢失消息数据就选:同步、ack=-1的策略。

5. Kafka怎么避免重复消费?

同消息不丢失伴生问题,如何避免重复消费:数据重复消费的情况,如果处理
去重:将消息的唯一标识保存到外部介质中,每次消费处理时判断是否处理过;
不管:大数据场景中,报表系统或者日志信息丢失几条都无所谓,不会影响最终的统计分析结果。

在kafka下游经常出现系统崩溃,需要回滚的问题,如何做到消息不重复消费是项目中很重要的一部分。可以修改offet从制定位置消费,也可以根据消息内容,从头消费toptic。对唯一字段进行过滤,做到消费过的字段不再消费。

6. Zookeeper对于Kafka的作用是什么?

答:

Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。Zookeeper主要用于在集群中不同节点之间进行通信在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取除此之外,它还执行其他活动,如:leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

7. 说一下kafka的内部结构,消息队列存在的意义?

一个典型的Kafka体系架构有多个Producer(可以是服务器日志,业务数据,页面前端产生的page view等等),多个Broker,多个Consumer,每个Producer可以对应多个Topic,每个Consumer只能对应一个Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在消费者组发生变化时进行rebalance。Producer使用push(推)模式将消息发布到broker,Consumer使用pull(拉)模式从broker订阅并消费消息。

消息队列意义:
解耦:生产者只负责把消息放在队列中,而不用关心谁去使用它。
异步:生产者把消息放在队列中后即可返回,而不用一个个的通知消费者去执行,消费者是异步的获取消息的。
限流:生产者一次性产生大量的数据时,不会给消费者造成压力,消费者可以根据自身的能力,去消息队列中取数据。
在项目中,发帖事件,站内通知事件以及删帖事件是分别用不同的topic来存放,里面有且只有一个partiton,而Kafka 本身是保证 partiton 中消息的顺序性的,所以单分区下不用特别考虑顺序性问题。

十一、项目

1. 校园论坛项目介绍

海大论坛社区是以贴吧、知乎这类社交平台为原型,基于SpringBoot框架实现的一个面向全体海大师生的交流平台。主要使用了MySQL数据库,Elasticsearch、Mybatis-Plus、Kafka等技术,实现了用户的注册、登录、发帖、点赞、系统通知、按热度排序、搜索等功能。另外引入了redis数据库来提升网站的整体性能,实现了用户凭证的存取、点赞关注的功能。基于 Kafka 实现了系统通知:当用户获得点赞、评论后得到通知。利用定时任务定期计算帖子的分数,并在页面上展现热帖排行榜。在这个项目中,我主要做了后台开发,后期参与部署工作。

2. 这个项目最具挑战的是什么?

难题1:在高并发的情况下,很多用户访问网页,评论网页,我们要记录浏览量,评论数,如果每一次请求后都去数据库中update字段,服务器很容易卡死。
解决:可以将值存到Redis中,每次收到请求后服务器先记录,每隔一段时间写回数据库中,这样用户看到的是一秒前的访问量,过了一秒,访问量蹭的上去了,对于用户来说一秒反应是没什么感觉的,对于服务器能防止卡死。Redis可以设置有效期,例如在登陆的时候可以用到,把用户登陆服务器下发的token存到Redis中,设置过期时间,时间到自动删除了,比存在数据库中判断expired_time更方便。

难题2:在高并发的时候,业务来不及同步处理,甚至出现请求堆积过多的情况,并且项目中有一些不需要实时执行但是是非常频繁的操作或者任务,比如点赞、评论和关注是非常频繁的操作,但是发送该操作的通知系统却不是需要立刻执行的。
解决:可以使用异步消息的形式进行发送,再用消息队列服务器kafka来实现。每次请求过来,先不去处理请求,而是放入消息队列,然后在后台布置一个监听器,分别监听不同业务的消息队列,有消息来的时候,再进行具体操作。代码中我们使用事件event来包装一个事件,事件需要记录事件实体的各种信息。事件生产者一般作为一个服务,由业务代码进行调用产生一个事件。而事件消费者我们在代码里使用了单线程循环获取队列里的事件,并且寻找对应的handler进行处理。

3. 项目中如何使用多线程?

用ThreadLocal持有用户信息。 项目中需要在代码中使用当前登录用户的信息,但是又不方便把保存用户信息的session对象传来传去,所以就使用ThreadLocal持有用户信息,用于代替session对象。当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本,这样就不存在线程安全问题,也不会影响程序的执行性能。

7. 登录注册是如何实现的?用Cookie做了什么? Cookie被窃取了该怎么办?

用户访问注册页面,输入注册的用户信息后提交表单,服务端验证账户是否已存在,邮箱是否已注册。验证通过后服务端发送激活邮件,用户点击邮件中的链接,访问服务端的激活服务。成功激活后,用户注册成功。其中用到了commons lang包来判断字符串、集合等等一些数据空值的情况,密码加密方面用到了MD5。还使用JavaMailSender实现邮件发送功能。用户访问登录页面,输入用户名密码进行登录。服务端验证账户是否可用以及密码、验证码是否正确,验证通过后生成登录凭证存入cookie,登录的用户信息和登录凭证存入Redis。登录成功得到cookie并设置过期时间,然后将cookie响应给客户端。Cookie的根本作用就是在客户端存储用户访问网站的一些信息。其中生成验证码使用了Kaptcha包,可随机生成字符和图片。由于每次请求时都要查询用户的登录凭证,根据凭证查询用户信息,访问非常频繁,所以用到Redis来存储登录凭证和用户信息。
在cookie中添加校验信息, 这个校验信息和当前用户外置环境有些关系,比如ip,user agent等有关. 这样当cookie被人劫持了, 并冒用, 但是在服务器端校验的时候, 发现校验值发生了变化, 因此要求重新登录, 去规避cookie劫持。

8. 项目Kafka使用场景。为什么要用Kafka? Kafka为什么吞吐量高?

当有点赞,评论,关注请求时,会发送系统通知点赞,评论,关注的对象。在处理系统信息时,使用到了Kafka。具体来说,先定义了生产者类和消费者类,事件生产者一般作为一个服务,由业务代码进行调用产生一个事件,比如被点赞/评论/关注。而事件消费者我们在代码里使用了单线程循环获取队列里的事件,并且寻找对应的handler进行处理。
在项目中,针对评论、点赞、关注我们可以定义三类不同的主题,一旦当事情发生时我们就能将这个消息发送到队列里,然后就不用再去关注后面的操作,后续由专门的消费者去获取消息队列里面的消息,这个过程实际上是异步的,是一个并发的过程,保证了系统的性能。
因为不管是Kafka也好还是阻塞队列也好,都是一种消息队列的框架,具有很好的封装性,都能实现这样的一个消息队列的功能,但是Kafka是目前性能最好的消息队列服务器,它能处理TB级别的海量数据,并且在我们的系统中评论、点赞、关注等等通知是十分频繁的,使用Kafka能保证一个最好的性能。
kafka吞吐量高是因为有以下特性
1 顺序写,而不是随机写。磁盘的顺序读写是磁盘使用模式中最有规律的,并且操作系统也对这种模式做了大量优化,Kafka就是使用了磁盘顺序读写来提升的性能。Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得Kafka写入吞吐量得到了显著提升 。
2 零拷贝。 linux操作系统 “零拷贝” 机制使用了sendfile方法, 允许操作系统将数据从Page Cache 直接发送到网络,只需要最后一步的copy操作将数据复制到 NIC 缓冲区, 这样避免重新复制数据 。
3 分区分段+索引。Kafka的message是按topic分类存储的,topic中的数据又是按照一个一个的partition即分区存储到不同broker节点。每个partition对应了操作系统上的一个文件夹,partition实际上又是按照segment分段存储的。 通过这种分区分段的设计,Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。
4 批量读写,批量压缩。它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度。

9. 如何做到显示首页的热度最高帖子?如何更新缓存?

给每个帖子定义一个score参数,加精、评论、点赞、收藏都会影响帖子分数,分数越高热度越高。最新、最热贴都是置顶具有优先级,通过Mybatis-plus的条件构造器按照帖子分数(热度)进行倒序输出。更新帖子分数就是采用定时任务的方式,每隔一段时间(两个小时)进行帖子分数更新。但是我们没必要时间一到将所有的帖子都更新一遍,尤其是当数据量很大的时候,因此这里是采用的一种方法,将这段时间内有评论点赞等等操作的帖子ID,先加载到缓存中(这里是存到redis中),等定时到了,将缓存中的修改过的帖子的分数进行更新,这样数据量比较小,效率也比较高。

10. Redis存了什么数据?缓存过期时间是多少?如何解决缓存一致性问题?

存放登录凭证、用户信息、用户数据、点赞信息和关注信息等。其中登录凭证的缓存过期时间为12小时。最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。(之所以这里是删除缓存而不是更新缓存,是因为有时候更新缓存的代价有时候是很高的,对于比较复杂的缓存数据计算的场景,如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新,开销过大。)

11. 如何识别热点数据?热度如何计算?如何更新热度?

给每个帖子定义一个score参数,加精、评论、点赞、收藏都会影响帖子分数,分数越高热度越高。更新帖子分数就是采用定时任务的方式,每隔一段时间(两个小时)进行帖子分数更新。但是我们没必要时间一到将所有的帖子都更新一遍,尤其是当数据量很大的时候,因此这里是采用的一种方法,将这段时间内有评论点赞等等操作的帖子ID,先加载到缓存中(这里是存到redis中),等定时到了,将缓存中的修改过的帖子的分数进行更新,这样数据量比较小,效率也比较高。

15. ES如何实现全文搜索的功能?ES的底层数据结构?

构造一个查询对象searchQuery,加入参数包括查询位置,查询顺序,分页信息,高亮设置。接着获取命中数据,创建一个集合,在集合中构建一个实体,根据命中的数据去构造这个实体,最终返回。使用消息队列(kafka)的方式,实现发帖/删帖后ES数据库的自动更新。最后创建一个控制类SearchController,这里先调用service实现数据的搜索,再用一个map聚合数据。最后返回到前端模板。
Elasticsearch使用倒排索引的数据结构,该结构支持非常快速的全文本搜索。倒排序在索引时创建,序列化到磁盘,全文搜索非常快,但不适合做排序,适合查询和全文检索。倒排索引是单词到文档映射关系的最佳实现形式。倒排索引列出了出现在任何文档中的每个唯一单词,并标识了每个单词出现的所有文档。索引可以认为是文档的优化集合,每个文档都是字段的集合,这些字段是包含数据的键值对。

16. ES倒排索引为什么能够加速搜索?

倒排索引列出了出现在任何文档中的每个唯一单词,并标识了每个单词出现的所有文档。索引可以认为是文档的优化集合,每个文档都是字段的集合,这些字段是包含数据的键值对。默认情况下,Elasticsearch 对每个字段中的所有数据建立索引,并且每个索引字段都具有专用的优化数据结构。例如,文本字段存储在倒排索引中,数字字段和地理字段存储在BKD树中。不同字段具有属于自己字段类型的特定优化数据结构,并具备快速响应返回搜索结果的能力使得 Elasticsearch 搜索飞快。

17. 项目中SpringSecurity的权限模型是怎么样的?

pringSecurity是基于RBAC模型轻量级权限控框架,RBAC权限框架基于角色进行鉴权,在该框架中具有三大模块:角色、用户、权限。项目中角色分为普通用户和管理员以及未注册的游客,所有人都有权限访问论坛首页、登录页以及注册页,普通用户能访问个人主页,评论发帖和私信页面等,管理员可以对帖子删减置顶以及加精。项目中判断当前请求访问的用户具备那些角色,该角色具备那些权限,所具备的权限中是否包含本次访问所需的权限?若具有,正常访问返回,若不具有,给予用户提示。

18 .用户的授权信息如何存储?

用户授权登录后,授权用户信息以及登录凭证会存储到Redis中,并且设置过期时间为12小时,这一段时间内进入论坛时不需要再进行登录操作。

十二、算法

1. 稳定排序有哪些?

不稳定:快排,堆排序,希尔排序,直接选择排序。
稳定:直接插入排序,冒泡,归并排序,基数排序

2. 排序的时空复杂度

在这里插入图片描述

3. 快速排序

选一个关键值作为基准值(一般选第一个元素),
从后向前比较,直到找到比基准值小的则交换位置;
从前向后比较,直到找到比基准值大的则交换位置;
重复执行,直到从前向后比较的索引大于等于从后向前比较的索引。


    public static int[] quickSort(int[] arr,int low,int high) {
        int start = low;//从前向后比较的索引
        int end= high;//从后向前比较的索引
             //基准数,默认设置为第一个值
        int key=arr[low];

        //循环
        while (end > start) {
            //从后往前比较
            while (end > start && arr[end] >= key) {
                end--;
            }
            //如果没有比基准值小的,则比较下一个,直到有比基准值小的,则交换位置,然后又从前向后比较
            if (arr[end] <= key) {
           		int temp = arr[end];
                arr[end] = arr[start];
                arr[start] = temp;
            }
            //从前往后比较
            while (end > start && arr[end] <= key) {
                start++;
            }
            //如果没有比基准值大的,则比较下一个,直到有比基准值大的,则交换位置
            if (arr[end] >= key) {
           		int temp = arr[end];
                arr[end] = arr[start];
                arr[start] = temp;
            }
        }
        //递归左边序列
        if(start>low)
        	sort(arr,low,start-1);
        //递归右边序列
      	if(end<high)
        	sort(arr,end+1,high);
        return arr;
    }

4. 冒泡排序

从左开始,依次比较相邻的两个元素,如果左边的元素大于右边的元素,二者进行交换位置,如此重复。


    public static int[]  bubbleSort(int[] arr){
    	//外层循环控制排序趟数
        for (int i = 0; i < arr.length-1; i++) { 
        	//内层循环控制每一趟排序次数
            for (int j = 0; j < arr.length-1-i; j++) { 
                if (arr[j] > arr[j+1] ){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
        return arr;
    }

5.选择排序

第一次从arr[0]到arr[n-1]中选取最小值,与arr[0]交换。第二次从arr[1]到arr[n-1]中选取最小值,与arr[1]交换…第i次从arr[i-1]到arr[n-1]中选取最小值,与arr[i-1]交换…第n-1次从arr[n-2]到arr[n-1]中选取最小值,与arr[i-2]交换。总共通过n-1次,得到一个按排序从小到大排列的有序序列。

public static int[] selectionSort(int[] sourceArray) {

        // 首先判断数组长度是否小于2, 小于2直接返回
        if (sourceArray == null || sourceArray.length < 2) {
            return sourceArray;
        }

        // 最小变量下标
        int min, temp;
        // 比较次数
        int compareNum = 0;
        // 交换次数
        int swapNum = 0;

        // 除了最后一个数,每个数都需要跟他后面的数进行比较
        for (int i = 0; i < sourceArray.length - 1; i++) {

            // 假设第i位为最小的数
            min = i;

            // 由于最小的数都排在第 i 个数前面
            // 所以只需和第 i+1 个数及后续的数进行比较
            for (int j = i + 1; j < sourceArray.length; j++) {
                compareNum++;
                if (sourceArray[j] < sourceArray[min]) {
                    // 记录目前能找到的最小值元素的下标
                    min = j;
                }
            }

            // 将找到的最小值和i位置所在的值进行交换
            if (min != i) {
                // 交换位置
                swapNum++;
                temp = sourceArray[i];
                sourceArray[i] = sourceArray[min];
                sourceArray[min] = temp;
                System.out.println("第" + swapNum + "次交换后,数组为:" + JSON.toJSONString(sourceArray));
            }

        }

        System.out.println("比较次数:" + compareNum);
        System.out.println("交换次数:" + swapNum);
        return sourceArray;
    }

6. 平衡二叉树

平衡二叉树(Balanced Binary Tree),具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

7. LRU

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

双向链表+Hash表
用哈希表存储链表结点和key值,能够做到O(1)访问链表任意结点,每次调用函数后将该结点放到链表最前方表示权重最大,最常访问,每次删除链表最后一个结点。要实现这个操作,我们需要的是有头结点和尾结点的双向链表。

public class Solution {
	//双向链表
    static class Node{
        int key , value;
        Node prev,next;
        public Node(int key , int value){
            this.key = key;
            this.value = value;
        }
    }
    
    private Map<Integer,Node> map = new HashMap<>();
    private Node head = new Node(-1,-1); //设置一个头
    private Node tail = new Node(-1,-1);//设置一个尾
    private int k;
    
    
    public int[] LRU (int[][] operators, int k) {
        // write code here
        this.k = k;
        head.next = tail;
        tail.prev = head;/先将链表首位相接,便于插入与删除
        ArrayList<Integer> list = new ArrayList<>();
        int cnt = 0;
        for(int i=0;i < operators.length ;i++){
            if(operators[i][0] == 1){
                set(operators[i][1],operators[i][2]);
            }else{
                list.add(get(operators[i][1]));
            }
        }
        int[] res = new int[list.size()];
        int i = 0;
        for(int val:list){
            res[i] = list.get(i);
            i++;
        }
        return res;
    }
    
    //插入数据
    public void set(int key,int value){
        //插入的数据已经存在,更新p节点的值,get()方法自动将节点位置调整到第一
        if(get(key) > -1){
            map.get(key).value = value;
        }else{
            if(map.size() == k ){
                int rk = tail.prev.key;
                tail.prev.prev.next = tail;
                tail.prev = tail.prev.prev;
                map.remove(rk);
            }
            Node node = new Node(key,value);
            map.put(key,node);
            removeToHead(node);
        }
    }
    
     //访问数据
    public int get(int key){
        if(map.containsKey(key)){//哈希表找到数据更新节点,并返回
            Node node = map.get(key);
            //将节点从原位置删除
            node.prev.next = node.next;
            node.next.prev = node.prev;
            //将节点插入到第一个位置
            removeToHead(node);
            return node.value;
        }
        return -1;
    }
    
    //将访问的节点放在第一个位置
    public void removeToHead(Node node){
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
        node.prev = head;
    }
    
}
8. 全排列算法
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蒙面侠1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值