Java面试知识点总结,面对底层问题不怂不怕----持续更新

笔记 专栏收录该内容
4 篇文章 0 订阅

一、基础

1.1 JVM

1.1.1 JVM五大内存区域

虚拟机在执行java程序时,会将自己管理的内存划分为几个区域,每个区域都有自己的用途,并且创建时间和销毁时间也不一样。在程序运行时的内存区域主要可以划分为五个,分别是:方法区、堆、虚拟机栈、本地方法栈、程序计数器。可以用下面的图来描述:

Java虚拟机运行时数据区域

1. Java堆

Java堆是java虚拟机所管理的内存中最大的一块,是被所有线程都共享的内存区域。存在的唯一目的就是存放对象实例,几乎所有的对象实例都在这里进行分配内存。java虚拟机的垃圾回收机制主要管理的就是此区域。JVM规范中规定堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。并且可以通过-Xmx和-Xms来扩展堆的内存大小,如果在堆中没有足够的内存为实例分配,并且堆也无法在扩展时,就会报OutOfMemoryError异常。

2 方法区

跟Java堆一样,方法区是各个线程共享的内存区域,此区域是用来存储类的信息(类的名称、字段信息、方法信息)、静态变量、常量以及编译器编译后的代码

运行时常量池

运行时常量池是方法区的一个部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是**常量池,用于存放编译期间生成的各种字面量和符号引用,**这部分内容会在类加载后进入方法区的运行时常量池中。Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

3. 程序计数器

虽然在上图中程序计数器的面积很大,但实际上它是一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器。字节码解释器在工作中时下一步该干啥、到哪了,就是通过它来确定的。很明显,程序计数器就是线程私有的。如果线程正在执行的是一个java方法,**程序计数器记录的是正在执行的虚拟机字节码指令地址;**如果执行的Native方法,程序计数器记录的值为空(Undefined),此内存区域是java中唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

4. Java虚拟机栈

我们经常会把java内存粗糙的分为两个部分,堆和栈,Java虚拟机栈就是栈这一部分,或者说是虚拟机栈中局部变量表部分。跟程序计数器一样,虚拟机栈也是线程私有的,它的生命周期跟线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame),每个栈帧对应一个被调用的方法,栈帧中用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每一个方法从开始执行到结束就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 局部变量表:顾名思义,他就是用来存储方法中的局部变量(包括在方法中生命的非静态变量以及函数形参),对于基本数据类型,直接存值,对于引用类型的变量,存储指向该对象的引用。由于它只存放基本数据类型的变量、引用类型的地址和返回值的地址,这些类型所需空间大小已知且固定,所以当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全可以确定的,在方法运行期间也不会改变局部变量表的大小。
  • 指向运行常量池的引用:在方法执行过程中难免会使用到类中定义的常量,因此栈帧中要存放一个指向运行时常量池的引用。
  • 方法返回地址:当一个方法执行结束后,要返回到之前调用它的地方,因此在栈帧中需要保存一个方法返回地址。

5. 本地方法栈

本地方法栈与虚拟机栈的功能非常的相似,区别不过是虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机执行Native方法服务。有的虚拟机并不会区分本地方法栈和虚拟机栈,比如Sun HotSpot虚拟机直接将两个合二为一。Native方法是一个Java调用非Java代码的接口。

用一张图总结:

JVM内存总结

1.1.2 新生代和老年代

jvm内存中的线程私有区区域划分如下图所示:在这里插入图片描述
java中最常见的问题之一就是堆内存溢出,所以了解jvm堆工作及GC的原理非常重要。jvm堆内存从GC的角度划分可分为:新生代(eden区、survivor form区和survivor to区)和老年代。

  1. 新生代
    新生代也是顾名思义就是用来存放新生的对象。新生代通常占据着堆内存的1/3空间。因为java对象频繁的创建,所以新生代会频繁的触发Minor GC进行垃圾回收。新生区分为Eden区、Survivor form区和Survivor to区。
  • Eden区:java新对象的出生地(当然如果新创建的对象占用的内存非常大,则直接将其分配至老年代),当Eden区中的内存不足时,就会触发Minor GC对新生代区进行一次垃圾回收。
  • Survivor To区:用于保留Minor GC中的幸存者。
  • Survivor From 区:用于存放上一次 Minor GC中幸存者,并且作为本次Minor GC的被扫描者。
    Minor GC过程:Minor GC通常采用复制算法。首先将Eden区和Survivor From 区中存活的对象复制到Survivor To区之中(如果对象的年龄到达了老年代的标准时则赋值到老年代(通常年龄大于15即可));然后清空Eden区和Survivor From 区,最后将 Survivor To区和Survivor From 区互换,原来的 Survivor To区变成下一次的Survivor From 区。

2.老年代
老年代主要存放在程序中生命周期长的对象。老年代因为其中对象比较稳定,所以Major GC不会频繁的执行。在进行Major GC之前通常都会先执行一次Minor GC,Minor GC执行完后可能会有新生代的对象晋升到老年代之中,然后导致老年代的空间不足才触发Major GC。当无法找到足够大的连续空间分配给新创建的较大的对象时也会提前触发一次Major GC进行垃圾回收来腾出空间。当触发Major GC时,GC期间会停止一切线程等待至GC完成。
Major GC过程:因为老年代每次只回收少量的对象,所以Major GC通常推荐采用标记整理算法。首先扫描一次所有的老年代,标记出所有存活的对象和需要回收的对象,将存活的对象移向内存的一端,然后清除边界外的对象。

3.永久代
永久代顾名思义就是就是指永久保存内存的区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候放入永久代。他和存放实例的区域不同,GC不会在主程序运行期间对永久区进行清理。而这也导致了永久区会随着不断增加的Class而膨胀,最终导致OOM异常。所以在JAVA8之中,移除了永久代,用一个叫元数据区的代替了永久代。元空间和永久代之间最大差异就是元空间使用的不是虚拟机中的内存,而是使用本地内存,这样的好处在于其元空间内存的大小仅仅受限于本地内存的大小,这样就可以避免永久代OOM的问题了。

1.1.3 加载类的过程

一个java类的完整的生命周期会经历加载、连接、初始化、使用、卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示:

img

过程一:Loading(加载)阶段

所谓加载,简而言之就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象。

#####过程二:Linking(链接)阶段

环节1:链接阶段之 Verification (验证)

它的目的是保证加载的字节码是合法、合理并符合规范的

img

具体说明:

  1. 格式验证:是否以魔数 0xCAFEBABE 开头,主版本和副版本号是否在当前 Java 虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等
  2. Java 虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如:
  3. 是否所有的类都有父类的存在(在 Java 里,除了 Object 外,其他类都应该有父类)
  4. 是否一些被定义为 final 的方法或者类被重写或继承了
  5. 非抽象类是否实现了所有抽象方法或者接口方法
  6. 是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样,这种方法会让虚拟机无从下手调度;absract 情况下的方法,就不能是final 的了)
  7. Java 虚拟机还会进行字节码验证,字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:
  8. 在字节码的执行过程中,是否会跳转到一条不存在的指令
  9. 函数的调用是否传递了正确类型的参数
  10. 变量的赋值是不是给了正确的数据类型等
环节2:链接阶段之 Preparation (准备)

准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值

当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。

Java 虚拟机为各类型变量默认的初始值如表所示:

img

注意:Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故对应的,boolean 的默认值就是 false

环节3:链接阶段之 Resolution (解析)

在准备阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用

  1. 具体描述:

符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存分布无关。比较容理解的就是在 Class 类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下 println() 方法被调用时,系统需要明确知道该方法的位置

过程三:Initialization(初始化)阶段

初始化阶段,简言之,为类的静态变量赋予正确的初始值

  1. 具体描述

类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行 Java 字节码。(即:到了初始化阶段,才真正开始执行类中定义的 Java 程序代码)

初始化阶段的重要工作是执行类的初始化方法:() 方法

  • 该方法仅能由 Java 编译器生成并由 JVM 调用,程序开发者无法自定义一个同名的方法,更无法直接在 Java 程序中调用该方法,虽然该方法也是由字节码指令所组成
  • 它是类静态成员的赋值语句以及 static 语句块合并产生的
  • 说明
  1. 在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的加载总是在子类加载之前被调用,也就是说,父类的 static 块优先级高于子类
  2. Java 编译器并不会为所有的类都产生 () 初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含 () 方法?
  3. 一个类中并没有声明任何的类变量,也没有静态代码块时
  4. 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
  5. 一个类中包含 static final 修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
过程四:类的Using(使用)

任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“万事俱备,只欠东风”,就等着开发者使用了

开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用 new 关键字为其创建对象实例

过程五:类的Unloading(卸载)

类的卸载

  1. 启动类加载器加载的类型在整个运行期间是不可能被卸载的(JVM 和 JSL 规范)
  2. 被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到 unreachable 的可能性极小
  3. 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景(比如:很多时候用户在开发自定义类的加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)

1.1.4 OOM

1、OOM类型

OOM,即OutOfMemory,内存溢出,原因是:分配的太少;用的太多;用完没释放。

内存泄漏:内存用完没有被释放。大量的内存泄漏就会导致OOM,也就是内存溢出。

常见的OOM情况有三种:

1)java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。

2java.lang.OutOfMemoryError: PermGen space/Metaspace------>java永久代(元数据)溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize/MetaspaceSize=64m -XX:MaxPermSize/MaxMetaspaceSize =256m的形式修改。另外,过多的常量也会导致方法区溢出。

3) java.lang.StackOverflowError ----->不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

2、OOM处理

根据抛出的三种OOM异常,分别进行处理。

2.1 OutOfMemoryError: Java heap space异常(堆溢出)

将堆信息dump出来,进行分析,dump的方法有两种:

——设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息。

——用JDK自带的jmap导出dump文件

导出hprof文件后,使用MAT进行内存镜像分析,MAT进行分析主要看4个界面:

HistoGram:列出内存中的对象,对象的个数和大小

img

Retained Heap比Shallow Heap多了当前对象引用的对象的Shallow Heap。

Dominator Tree:列出线程,以及线程下各对象占用的内存

通过Path to GC roots可以查看线程中完整的对象引用链

Top Consumers:通过图形列出最大的Object,便于定位问题

Leak Suspects:自动分析内存泄漏原因

导致堆内存泄漏的常见原因:

  • 静态集合类引起的内存泄漏:像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

  • 当集合Set里面的对象属性被修改后,再调用remove()方法时不起作用:之所以不起作用,是因为对象属性修改后,对象的hashcode就变了,remove的时候就找不到了。

  • 监听器:Listener未删除。

  • 各种连接:比如数据库连接、Socket连接、IO连接等,没有显式close掉。

  • 非静态内部类:非静态内部类会自动生成构造函数,并把外部类作为构造函数的参数,这样才能在内部类里使用外部类的属性和方法。但是这样内部类会保留外部类的引用,如果内部类与外部类的生命周期不一致,就可能回收不了。

  • 单例模式:在单例对象中引用了其它对象,被引用对象永远不会回收。

2.2 OutOfMemoryError: PermGen space/ Metaspace异常(方法区溢出)

方法区里面是类的类型信息,同样借助MAT分析Dump下来的内存镜像。

一般产生的原因:

  • 不正确地使用反射生成大量的Class
  • 有大量的JSP
  • 类中过多地使用常量
  • 对类加载器使用缓存

2.3 OutOfMemoryError:GC overhead limit exceeded

用98%的时间去GC,但只能回收2%的内存,说明内存已经没法回收了,很有可能是内存中的对象都是不可回收对象,导致无法GC。

2.4 StackOverflowError异常(栈溢出

这一类是线程异常,处理的方法是使用jstack输出.out文件,然后分析每个线程栈的运行情况。

一般产生的原因:

——出现无限递归或死循环,局部变量不停地创建,导出栈溢出。

1.1.5 JVM调优

1、设定堆内存大小
-Xmx:堆内存最大限制。
2、设定新生代大小。新生代不宜太小,否则会有大量对象涌入老年代-XX:NewSize:新生代大小
-XX:NewRatio新生代和老生代占比
-xx:SurvivorRatio:伊甸园空间和幸存者空间的占比
3、设定垃圾回收器年轻代用-XX:+UseParNewGC年老代用-XX:+UseConcMarkSweepGC

1.2 GC

1.2.1 可达性分析

img

可达性分析算法的基本思路就是通过一系列称为**“GC Roots”的对象作为起点,从这些结点开始向下搜索**,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的,为可回收对象。 图中,右边部分都是不可达的对象,都是可回收对象。

在Java语言中,可以作为GC Roots的对象包括以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量变量引用的对象
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象
  5. 活跃线程的引用对象

1.2.2 Java中的引用

1、强引用
如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JvM也不会回收它,而是抛出 OutOfMemoryError错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
2、软引用
在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收;只有在内存空间不足时,软引用才会被垃圾回收器回收
3、弱引用
具有弱引用的对象拥有的生命周期更短暂。因为当JVM进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
4、虚引用
顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收,只要进行垃圾回收,虚引用就会被回收 。
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

1.2.3 GC回收算法

1.标记-清除算法

标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;

清除阶段:清除所有未被标记的对象。

2.复制算法----新生代的GC

将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象 。

3.标记-整理算法----老年代的GC

标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。

整理阶段:将所有的存活对象压缩到内存的一端之后,清理边界外所有的空间。

4.分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代,在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以使用标记整理算法。

1.2.4 GC回收器

  1. Serial串行单线程回收器,串行收集器是最古老,优点最稳定以及效率高的收集器,可能会缺点是产生较长的停顿
  2. ParNew 串行多线程回收器,ParNew收集器其实就是 Serial收集器的多线程版本
  3. Parallel并行多线程收集器,Parallel Scavenge收集器类似ParNew收集器,优点是更高系统的吞吐量
  4. Parallel 并行多线程老年代收集器,使用多线程和“标记―整理”算法
  5. CMS收集器,CMS (Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器
  6. G1收集器,G1(Garbage-First)是一款面向服务器的垃圾收集器基于“标记-整理”算法实现收集器非常精确地控制停顿主要针对配备多颗处理器及大容量内存的机器.以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

1.2.5 Full GC和Major GC

虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间

1、如果大于的话,直接执行minorGC

2、如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC

3、如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升(晋级老年代对象的平均大小)平均值的大小,如果小于直接执行FullGC

4、如果大于的话,执行minorGC

Minor GC

  1. Minor GC是指从年轻代空间(包括 Eden 和 Survivor 区域)回收内存。当 JVM 无法为一个新的对象分配空间时会触发Minor GC,比如当 Eden 区满了。
  2. Eden区满了触发MinorGC,这时会把Eden区存活的对象复制到Survivor区,当对象在Survivor区熬过一定次数的Minor GC之后,就会晋升到老年代(当然并不是所有的对象都是这样晋升的到老年代的),当老年代满了,就会报OutofMemory异常。
  3. 所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。 执行 Minor GC 操作时,不会影响到永久代。

Major GC vs Full GC

  • Major GC清理Tenured区(老年代),出现了 Major GC,经常 会伴随至少一次的 Minor GC(非绝对) 。MajorGC 的速度一般会比 Minor GC 慢 10 倍以上。
  • Full GC清理整个heap区,包括Yong区和Tenured区。

Full GC触发条件
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小 > 老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。即老年代无法存放下新年代过度到老年代的对象的时候,会触发Full GC。

1.2.6 防止Full GC

1、 System.gc()方法的调用
调用此方法是建议JVM进行Full GC,只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。可通过设置参数-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

2、老年代空间不足
a:连续空间碎片不足:
当有大对象、大数组进入老年代时,老年代的连续空间碎片放不下,此时会发生Full GC,Full GC之后任然放不下就会抛出内存溢出错误
出现这种情况:1、尽量不然创建这种大对象;2、万一创建了尽量在新生代多保存一段时间(增大默认参数:15),最好在新生代被回收掉;3、使用CMS垃圾收集器提供了一个可配置的参数-XX:+UseCMSCompactAtFullCollection开关参数,就是在Full GC之后会有一个碎片整理的过程,但是此过程内存无法并发的,停顿时间较长;还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,用于设置在执行多少次不进行内存碎片压缩的Full GC后,跟着来一次带压缩的。

b : 总空间不足
通常经过minor GC之后晋升到老年代的对象大于老年代剩余空间的容纳量就会进行Full GC
为了由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,先做一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
对于 RPC(远程过程调用)和 RMI(远程方法调用)管理的JDK而言,默认情况下会一小时执行一次Full GC。可在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

3、永生区(元空间)空间不足
Permanet Generation(永久代)中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用CMS GC的情况下也会执行
Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免永久代占满造成Full GC现象,可采用的方法为增大永久代空间或转为使用CMS GC。

4、CMS GC日志出现promotion failed和concurrent mode failure
采用CMS进行老年代GC时,尤其注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;
concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。
对措施为:增大survivor space和老年代空间,或调低触发并发GC的比率。

1.2.7 GC调优

1. -XX:NewSize and -XX:MaxNewSize
就像可以通过参数(-Xms and -Xmx) 指定堆大小一样,可以通过参数指定年轻代大小。

2. -XX:NewRatio

可以设置年轻代和老年代的相对大小。这种方式的优点是年轻代大小会随着整个堆大小动态扩展。

3. -XX:SurvivorRatio

作用于年轻代内部区域。-XX:SurvivorRatio 指定Eden与Survivor大小比。

1.3 集合

用一张图来辅助对Java集合的记忆
在这里插入图片描述

1.3.1 ArrayList 源码分析 初始容量 扩容原理

创建一个集合时,集合的初始容量为0,在第一次添加元素的时候,会对集合进行扩容,扩容之后,集合容量为10;之后,当向集合中添加元素达到集合的上限(也就是minCapacity大于elementData.length)时,会对集合再次扩容,扩容为原来的3/2。

list扩容的底层是容量的二进制码向右移一位为原来的1/2 + 原来的容量,从而扩容为原来的1.5倍,扩容步骤为:
a. 获取原数组容量
b. 计算新数组容量
新数组容量 = 原数组容量 + (原数组容量 >> 1);
c. 判断新数组容量是否满足最小要求
d. 判断新数组容量是否超出最大限制
e. 创建新数组
f. 移动数据到新数组中
g. 保存新数组地址
T[] Arrays.copyOf(T[] src, int newCapacity);

(1) ArrayList是一种变长的集合类,基于定长数组实现,使用默认构造方法初始化出来的容量是10 (1.7之后都是延迟初始化,即第一次调用add方法添加元素的时候才将elementData容量初始化为10)。
( 2)ArrayList允许空值和重复元素,当往 ArrayList中添加的元素数量大于其底层数组容量时,其会通过扩容机制重新生成一个更大的数组。ArrayList扩容的长度是原长度的1.5倍

(3)由于ArrayList底层基于数组实现,所以其可以保证在 o(1)复杂度下完成随机查找操作。
(4) ArrayList是非线程安全类,并发环境下,多个线程同时操作ArrayList,会引发不可预知的异常或错误。
(5)顺序添加很方便
(6)删除和插入需要复制数组,性能差(可以使用LinkindList>
(7) Integer.MAX_VALUE-8:主要是考虑到不同的JVM,有的JVM会在加入一些数据头,当扩容后的容量大于MAX_ARRAY_SIZE,我们会去比较最小需要容量和MAX_ARRAY_SIZE做比较,如果比它大,只能取Integer.MAX_VALUE,否则是Integer.MAx_VALUE-8。这个是从jdk1.7开始才有的

1.3.2 HashMap 源码分析 1.7和1.8 扩容 冲突 长度>8转为红黑二叉树 为什么不是6或者7

  • 如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
  • 还有一点重要的就是由于treenodes的大小大约是常规节点的两倍,因此我们仅在容器包含足够的节点以保证使用时才使用它们,当它们变得太小(由于移除或调整大小)时,它们会被转换回普通的node节点,容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值

jdk1.8之前list+链表
jdk1.8之后 list +链表(当链表长度到8时,转化为红黑树)HashMap 的负载因子默认0.75,也就是会浪费1/4的空间,达到扩容因子时,会将list扩容一倍,0.75是时间与空间一个平衡值;
默认的容量是16,hashMap每次扩容后的容量一定是2的n次幂,即使是初始化指定一个容量,也还是会通过tableSizeFor(int cap)方法去寻找最接近指定容量的2的n次幂数值

面试题扩容之后,需要对HashMap中原有元素进行rehash,即将原来通中的元素重新分配到新的桶中。hashMap的链表是可以无限插入的,为什么还要那么麻烦对数组进行扩容?

:为了解决哈希碰撞,对存储的值都会进行哈希值的计算,存在哈希值相同的情况,产生哈希碰撞。数组中的链表越容易长,发生碰撞的几率越大,造成查询或插入时的比较次数增多,性能会下降。数组中的链表也就越短,发生碰撞的几率越小,查询和插入时比较的次数也越小,性能会更高。

源码中有一些常量值下面附加说明,是hashMap结构关键的数值

    //默认初始化容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //容量最大值 左移30位,即1073741824
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //树化的长度
    static final int TREEIFY_THRESHOLD = 8;
    //非树化的长度
    static final int UNTREEIFY_THRESHOLD = 6;
    //最小树化的数组容量
    static final int MIN_TREEIFY_CAPACITY = 64;

1.3.3 ConCurrenntHashMap(JUC 源码分析 1.7和1.8 分段式锁和CAS

在ConcurrentHashMap中,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中。ConcurrentHashMap将数据分为多个segment(段),默认长度16个(concurrency level),然后每次操作对一个segment(段)加锁,避免多线程锁的几率,提高并发效率。

jdk1.7底层结构:

数组(Segment) + 数组(HashEntry) + 链表(HashEntry节点)
一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,Segment 的结构和 HashMap 类似,是一个数组和链表结构。可以将1.7版本看作是多个hashMap构成的底层结构。
img

jdk1.8 底层结构:

Node数组+链表 / 红黑树: 类似hashMap<jdk1.8>
并发控制使用Synchronized 和 CAS来操作,整个看起来就像是优化过且线程安全的 HashMap。JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
在这里插入图片描述

  1. :实现线程安全的方式

    hashMap是线程不安全的,

    hashTable是线程安全的,实现线程安全的机制是使用Synchronized关键字修饰方法

    ConcurrentHashMap :

    <JDK1.7>, ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段Segment(继承了ReentrantLock)调用父类的锁对象加锁来实现,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问。
    <jdk1.8> ,使用的是优化的synchronized 关键字 和 CAS操作了维护并发。

    CAS是基于冲突检测的乐观锁(非阻塞)
    CAS自旋式锁:全称叫“Compare And Swap”,也就是比较与交换,它具有三个操作数,内存位置V、预期值A和新值B。如果在执行过程中,发现内存中的值V与预期值A相匹配,那么他会将V更新为新值B。如果预期值A和内存中的值V不相匹配,那么处理器就不会执行任何操作。

  2. :底层数据结构:

    hashMap同hashTable,都是使用数组 + 链表结构 。

    ConcurrentHashMap :

    <jdk1.7> :使用 Segment数组 + HashEntry数组 + 链表。

    <jdk1.8> :使用 Node数组+链表+ 红黑树 。

  3. :效率

    hashMap只能单线程操作,效率低下

    hashTable使用的是synchronized方法锁,若一个线程抢夺了锁,其他线程只能等到持锁线程操作完成之后才能抢锁操作

    ConcurrentHashMap:

    <1.7> 使用的分段锁,如果一个线程占用一段,别的线程可以操作别的部分 。

    <1.8>简化结构,put和get不用二次哈希,一把锁只锁住一个链表或者一棵树,并发效率更加提升。

1.3.4 Hashtable 和HashMap的区别

1、继承的父类不同

HashMap继承自AbstractMap类。但二者都实现了Map接口。
Hashtable继承自Dictionary类,Dictionary类是一个已经被废弃的类(见其源码中的注释)。父类都被废弃,自然而然也没人用它的子类Hashtable了。

2、HashMap线程不安全,HashTable线程安全

javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
Hashtable 中的方法大多是Synchronize的,而HashMap中的方法在一般情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。HashTable实现线程安全的代价就是效率变低,因为会锁住整个HashTable,而ConcurrentHashMap做了相关优化,因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定,效率比HashTable高很多。
HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
HashMap的put方法:

void addEntry(int hash, K key, V value, int bucketIndex) { //新增Entry,将“key-value”插入指定位置,bucketIndex是位置索引。
        Entry<K,V> e = table[bucketIndex];  保存“bucketIndex”位置的值到“e”中
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
        if (size++ >= threshold)       // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小2倍
            resize(2 * table.length);
    }

在hashmap的put方法调用addEntry()方法,假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。
故解决方法就是使用 使用ConcurrentHashMap。
这里要说一下 就是HashMap的迭代器(Iterator)是fail-fast迭代器,故当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException异常,而Hashtable的enumerator迭代器不是fail-fast的。但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

3.包含的contains方法不同

HashMap是没有contains方法的,而包括containsValue和containsKey方法;

hashtable则保留了contains方法,效果同containsValue,还包括containsValue和containsKey方法。

4.是否允许null值

Hashmap是允许key和value为null值的,用containsValue和containsKey方法判断是否包含对应键值对;HashTable键值对都不能为空,否则包空指针异常。

5.计算hash值方式不同

为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。

①:HashMap有个hash方法重新计算了key的hash值,因为hash冲突变高,所以通过一种方法重算hash值的方法:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

注意这里计算hash值,先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值
**②:Hashtable通过计算key的hashCode()**来得到hash值就为最终hash值。

它们计算索引位置方法不同:
HashMap在求hash值对应的位置索引时,index = (n - 1) & hash。将哈希表的大小固定为了2的幂,因为是取模得到索引值,故这样取模时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。

HashTable在求hash值位置索引时计算index的方法:

int index = (hash & 0x7FFFFFFF) % tab.length;
1

&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号位改变,而后面的位都不变。

6.扩容方式不同(容量不够)

当容量不足时要进行resize方法,而resize的两个步骤:
①扩容;
②rehash:这里HashMap和HashTable都会会重新计算hash值而这里的计算方式就不同了(看5);
HashMap 哈希扩容必须要求为原容量的2倍,而且一定是2的幂次倍扩容结果,而且每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入;
而Hashtable扩容为原容量2倍加1;

7.解决hash冲突方式不同(地址冲突)

先看jdk8之前:
在这里插入图片描述
查找时间复杂度慢慢变高;
Java8,HashMap中,当出现冲突时可以:

1.如果冲突数量小于8,则是以链表方式解决冲突。
2.而当冲突大于等于8时,就会将冲突的Entry转换为**红黑树进行存储**。
3.而又当数量小于6时,则又转化为链表存储。

而在HashTable中, 都是以链表方式存储。

1.3.5 HashSet 实现过程 重写hashcode和equals

HashSet是对HashMap的一层封装,HashMap它可以看成是一个一维数组,而一维数组里的元素又是一个单链表,类似下图所示: 这里写图片描述

不能保证元素的排列顺序,顺序有可能发生变化

集合元素可以是null,但只能放入一个null

HashSet底层是采用HashMap实现的

HashSet底层是哈希表实现的

HashMap根据哈希表保证元素唯一性,是根据元素的hash值以及调用equals()方法保证元素唯一的。

重写equals方法:

  1. 如果调用当前方法的对象和传入参数对象是同一个对象,也就是空间首地址一致,这里无需其他判断,直接返回true
  2. 这里需要一个判定
    a. 当前传入参数对象不为null
    b. 当前传入参数对象和调用方法类对象为同一个类型对象。
  3. 这里需要对比两个对象的数据内容是否一致。

Java中的规定:如果两个对象equals方法比较结果为true,强制要求这里两个对象的hashCode值一致!!!

重写HashCode方法:objects.hash()方法

@Override
public int hashCode(Object... args) {
return Objects.hash(id, name, gender);
}

1.3.6.TreeSet 实现原理 红黑二叉树 比较器接口(Compable Compator)

Treeset中的数据是排好序的,不允许放入null值。

TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key。

TreeSet的底层实现是采用二叉树(红-黑树)的数据结构。

TreeSet在添加元素时,元素必须有可比较性 :

TreeSet的底层实现是采用红-黑树的数据结构,采用这种结构可以从Set中获取有序的序列,但是前提条件是: 有序性的实现原理

//1. 自定义类遵从Comparable<T>接口, 实现compareTo方法。
   interface Comparable<T> {
   	// 0 标示两个元素一致
   	// 大于零或者小于零,两者不一致,如果需要完成排序算法,可以尝试使用。
   	int compareTo(T t);
   }
//2. 提供自定义比较器作为TreeSet构造方法参数,需要完成compare方法
   TreeSet(Comparator<T> compare);
   interface Comparator<T> {
   	// 0 标示两个元素一致
   	// 大于零或者小于零,两者不一致,如果需要完成排序算法,可以尝试使用。
   	int compare(T o1, T o2);
   	// boolean compare(Worker w1, Worker w2);
   }



1.4线程

1.创建方式 4种

  1. 继承Thread类,重写run()方法

    1. 创建一个类继承Thread类,重写run()方法,将所要完成的任务代码写进run()方法中;
    2. 创建Thread类的子类的对象;
    3. 调用该对象的start()方法,该start()方法表示先开启线程,然后调用run()方法;
    
  2. 实现Runnable接口,实现run()方法

    1.创建一个类并实现Runnable接口
    2. 重写run()方法,将所要完成的任务代码写进run()方法中
    3. 创建实现Runnable接口的类的对象,将该对象当做Thread类的构造方法中的参数传进去
    4. 使用Thread类的构造方法创建一个对象,并调用start()方法即可运行该线程
    
  3. 实现Callable接口,实现call()方法

    1. 创建一个类并实现Callable接口
    2. 重写call()方法,将所要完成的任务的代码写进call()方法中,需要注意的是call()方法有返回值,并且可以抛出异常
    3. 如果想要获取运行该线程后的返回值,需要创建Future接口的实现类的对象,即FutureTask类的对象,调用该对象的get()方法可获取call()方法的返回值
    4. 使用Thread类的有参构造器创建对象,将FutureTask类的对象当做参数传进去,然后调用start()方法开启并运行该线程。
    
  4. 使用线程池创建启动线程。

    1. 使用Executors类中的newFixedThreadPool(int num)方法创建一个线程数量为num的线程池
    2. 调用线程池中的execute()方法执行由实现Runnable接口创建的线程;调用submit()方法执行由实现Callable接口创建的线程
    3. 调用线程池中的shutdown()方法关闭线程池
    
    使用线程池的优点?
    
    1.重用存在的线程,减少对象创建销毁的开销。
    2.可有效的控制最大并发线程数,提高系统资源的使用率,避免过多资源竞争,避免堵塞
    3.提供定时执行,定期执行,单线程,并发数控制的等功能。 
    

runnable和callable的区别:

(1)Callable可以在任务结束之后提供一个返回值。Runnable不可以。

(2)Callable中call()方法可以抛出异常。Runnable中的run()方法不可以。

(3)Callable运行过程中可以拿到一个Future对象,可以用来监视目标线程调用call()方法的情况。 runnable 访问当前线程,必须使用Thread.currentThread()方法。

public class MyThread implements Callable<String>{//Callable是一个泛型接口
 
@Override
public String call() throws Exception {//返回的类型就是传递过来的V类型
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" : "+i);
}

return "Hello Tom";
}
public static void main(String[] args) throws Exception {
MyThread myThread=new MyThread();
FutureTask<String> futureTask=new FutureTask<>(myThread);
Thread t1=new Thread(futureTask,"线程1");
Thread t2=new Thread(futureTask,"线程2");
Thread t3=new Thread(futureTask,"线程3");
t1.start();
t2.start();
t3.start();
System.out.println(futureTask.get());

}

线程池的创建方式:

第一种方式,构建一个线程池

ExecutorService threadPool = Executors.newFixedThreadPool(10);

第二种方式,使用ThreadPoolExecutor构建一个线程池

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class test {
    public static void main(String args[]) {
        ExecutorService executorService = new ThreadPoolExecutor(5,10,
                10,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(5));
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("开始执行线程池中的任务");
            }
        });
    }
}

如果只是简单的想要改变线程名称的前缀的话可以自定义ThreadFactory来实现,在Executors.new…中有一个ThreadFactory的参数,如果没有指定则用的是DefaultThreadFactory。

第三种方式,使用工具来创建线程池,Apache的guava中ThreadFactoryBuilder()来创建线程池,不仅可以避免OOM问题,还可以自定义线程名称,方便出错时溯源

2.线程生命周期

  1. 新建状态(New):新创建了一个线程对象。

  2. **就绪状态(Runnable ):**调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,随时等待CPU调度执行,并不是说执行了start()此线程立即就会执行;

  3. 运行状态(Running):就绪状态(runnable)的线程获得了cpu 使用权 ,执行程序代码。

  4. 阻塞状态(Blocked):阻塞状态是指线程因为某种原因放弃了cpu 使用权,暂时停止运行。直到线程进入就绪状态(runnable),才有机会再次获得cpu转到运行(running)状态。

    阻塞的情况分三种:

(一). 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行的线程执行Thread.sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  1. 死亡状态(Dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。

3.线程的交互 (join yeild sleep wait notify\notifyall)

线程交互是指两个线程之间通过通信联系对锁的获取与释放,从而达到较好的线程运行结果,避免引起混乱的结果。一般来说synchronized块的锁会让代码进入同步状态,即一个线程运行的同时让其它线程进行等待,那么如果需要进行实现更复杂的交互,则需要学习以下几个方法:

void notify(): 唤醒在锁对象上等待的单个线程。

void notifyAll(): 唤醒在锁对象上等待的所有线程。

void wait(): 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。

join(): 方法指的是等待调用join()方法的线程执行结束,程序才会继续执行下去。

sleep(): 方法让当前正在执行的线程先暂停一定的时间,并进入阻塞状态,不会释放锁。

Yield(): 就是暂停当前的线程,让给其他线程(包括它自己)执行,具体由谁执行由CPU决定。

4.线程安全 synchronized 和 lock

1.来源:
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

2.异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

3.是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

4.是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

5.Lock以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

6.在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

7.synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

8.synchronized是悲观锁,lock是乐观锁

img

2种机制的具体区别:
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

Lock接口有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。

1、可重入锁

如果锁具备可重入性,则称作为 可重入锁 。像 synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了 锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

5.死锁 写出死锁

死锁概述

线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。

死锁产生的必要条件

互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个线程所占有。此时若有线程请求该资源,则请求线程只能等待。

不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程倾向夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。

请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该线程已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。

避免死锁

1)加锁顺序,线程按照一定的顺序加锁

2)加锁时限,线程获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁

3) 死锁检测,每当一个线程获得了锁,会在线程和锁相关的数据结构中(例如map)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

6.线程池 7个参数的含义

线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler

一、corePoolSize 线程池核心线程大小

线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。

二、maximumPoolSize 线程池最大线程数量

一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。

三、keepAliveTime 空闲线程存活时间

一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定

四、unit 空闲线程存活时间单位

keepAliveTime的计量单位

五、workQueue 工作队列

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

六、threadFactory 线程工厂

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程(守护线程)等等

七、handler 拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:

①CallerRunsPolicy

该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。

②AbortPolicy(默认拒绝策略)

该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。

③DiscardPolicy

该策略下,直接丢弃任务,什么都不做。

④DiscardOldestPolicy

该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

7.阻塞队列

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

8.拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:

①CallerRunsPolicy

该策略下,由调用线程(提交任务的线程)直接执行此任务 ,除非线程池已经shutdown,则直接抛弃任务。

②AbortPolicy(默认拒绝策略)

该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。

③DiscardPolicy

该策略下,直接丢弃任务,什么都不做。

④DiscardOldestPolicy

该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

9.锁的分类 公平 非公平 乐观 悲观 轻重

一、公平锁/非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
  • 对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
  • 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

二、可重入锁

  • 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
  • 说的有点抽象,下面会有一个代码的示例。
  • 对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
  • 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

三、独享锁/共享锁

  • 独享锁是指该锁一次只能被一个线程所持有。
  • 共享锁是指该锁可被多个线程所持有。
  • 对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
  • 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
  • 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
  • 对于Synchronized而言,当然是独享锁。

四、互斥锁/读写锁

  • 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
  • 互斥锁在Java中的具体实现就是ReentrantLock
  • 读写锁在Java中的具体实现就是ReadWriteLock

五、乐观锁/悲观锁

  • 乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
  • 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
  • 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
  • 从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
  • 悲观锁在Java中的使用,就是利用各种锁。
  • 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

六、分段锁

  • 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
  • 我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
  • 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
  • 但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
  • 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

七、偏向锁/轻量级锁/重量级锁

  • 这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
  • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

八、自旋锁

  • 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

10.锁的优化 锁消除 锁粗化 自旋 CAS

  1. 减少锁的持有时间

例如避免给整个方法加锁

  1. 减小锁的粒度

将大对象,拆成小对象,大大增加并行度,降低锁竞争. 如此一来偏向锁,轻量级锁成功率提高.
一个简单的例子就是jdk内置的ConcurrentHashMap与SynchronizedMap.
Collections.synchronizedMap
其本质是在读写map操作上都加了锁, 在高并发下性能一般.

  1. 锁分离

顾名思义, 用ReadWriteLock将读写的锁分离开来, 尤其在读多写少的场合, 可以有效提升系统的并发能力

  1. 锁粗化

通常情况下, 为了保证多线程间的有效并发, 会要求每个线程持有锁的时间尽量短,

如果对同一个锁不停的进行请求 同步和释放, 其本身也会消耗系统宝贵的资源, 反而不利于性能的优化

一个极端的例子如下, 在一个循环中不停的请求同一个锁,将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。以此来减少在锁操作上的开销。

  1. 锁消除

JVM检测到不可能存在共享数据竞争,JVM会对这些同步锁进行锁消除,也就是取消加锁操作。

CAS

它具有三个操作数,内存位置V、预期值A和新值B。如果在执行过程中,发现内存中的值V与预期值A相匹配,那么他会将V更新为新值B。如果预期值A和内存中的值V不相匹配,那么处理器就不会执行任何操作。

ABA问题: 如果内存地址V初次读取的值是A,在CAS等待期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。

解决方案:
jdk1.5后有个类,atomicStampedReference
使用atomicStampedReference修饰变量,调用compareAndSet传入(期望值,写入的新值,期望标记,新标记值)

自旋锁

当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。

自旋锁缺点:

1.循环时间长,CPU开销很大

2.只能保证一个共享变量的原子操作

11.锁的过程 锁的升级过程(膨胀过程) 1.无锁 2.偏向锁 3.轻 4.重

  • 这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
  • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

12.ThreadLocal实现过程

ThreadLocal类用来提供线程内部的共享变量,在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。

ThreadLocal常用方法介绍

img

get()方法:获取与当前线程关联的ThreadLocal值。

set(T value)方法:设置与当前线程关联的ThreadLocal值。

initialValue()方法:设置与当前线程关联的ThreadLocal初始值。

当调用get()方法的时候,若是与当前线程关联的ThreadLocal值已经被设置过,则不会调用initialValue()方法;否则,会调用initialValue()方法来进行初始值的设置。通常initialValue()方法只会被调用一次,除非调用了remove()方法之后又调用get()方法,此时,与当前线程关联的ThreadLocal值处于没有设置过的状态(其状态体现在源码中,就是线程的ThreadLocalMap对象是否为null),initialValue()方法仍会被调用。

initialValue()方法是protected类型的,很显然是建议在子类重载该函数的,所以通常该方法都会以匿名内部类的形式被重载,以指定初始值,例如:

remove()方法:将与当前线程关联的ThreadLocal值删除。

13.Volatile 变量内存的可见性

volatile是通过内存屏障和禁止指令重排序来保证内存可见性的,一个线程对volatile变量的修改,能够立刻被其他线程所见。

volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。

volatile重排序规则表(针对编译器重排序):

在这里插入图片描述从这张表我们可以看出:
当第一个操作是Volatile读时,不管第二个操作是什么,都不能重排序;
当第一个操作是Volatile写时,第二个操作是Volatile读或写,不能重排序;
当第一个操作是普通读写,第二个操作是Volatile写时,不能重排序。

内存屏障(针对处理器重排序):

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。(首先保证了正确性,再去追求执行效率)

(1)在每一个volatile写操作前面插入一个StoreStore屏障。这确保了在进行volatile写之前前面的所有普通的写操作都已经刷新到了内存。

(2)在每一个volatile写操作后面插入一个StoreLoad屏障。这样可以避免volatile写操作与后面可能存在的volatile读写操作发生重排序。

(3)在每一个volatile读操作后面插入一个LoadLoad屏障。这样可以避免volatile读操作和后面普通的读操作进行重排序。

(4)在每一个volatile读操作后面插入一个LoadStore屏障。这样可以避免volatile读操作和后面普通的写操作进行重排序。

在这里插入图片描述

14.AQS(aqs)AbstractQueuedSynchronizer 锁生效的核心抽象类

AQS的功能可以分为两类:独占锁和共享锁。它的所有子类中,要么实现并使用了它独占锁的API,要么使用了共享锁的API

15.synchronized实现原理(monitor)

1.5杂项

1.反射

指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法.这种动态获取信息,以及动态调用对象方法的功能叫java语言的反射机制

这里需要将.class字节码文件占用的【方法区】内存空间看作是一个【对象】

当前.class字节码文件占用的内存空间,可以看做是一个对象,重点是这是一个什么数据类型???
每一个.class对应的都是一个Java类
占用的内存空间对应的就是一个Java类
该数据类型是Class类型
class 类名 {
成员变量
成员方法
构造方法
注解
}

当前.class字节码文件占用的【方法区】内存空间,我们可以看做是一个Class类型对象占用空间。
这里是对于数据类型的封装过程。

所有的方法都有返回值类型,方法名和形式参数列表。所以所有的方法,都可以看做是一个类 Method类型
所有的成员变量都有数据类型,成员变量名字。所有的成员变量都可以看做是一个类
Field类型
所有的构造方法,方法名一致!!!形式参数列表不一致,所有的构造方法都可以看做一个类 Constructor类
Annotation 注解类

static Class Class.forName(String packageAndClassName) 
throw ClassNotFoundException;
根据指定的完整包名.类名获取对应的Class对象,并且当前方法有加载指定类的能力。

Class 类名.class;
通过类名获取Class对象,这里相对于获取的是对应数据类型的Class属性,通常用于数据类型约束。

Class 类对象.getClass();
通过类对象调用getClass()方法,获取当前类对象对应的Class对象

Constructor[] getConstructors();
获取类内的所有非私有化构造方法类对象数组,返回值类型是Constructor数组类型

Constructor[] getDeclaredConstructors();
【暴力反射】
获取类内所有构造方法类对象数组,包含私有化构造方法。

Constructor getConstructor(Class... initParameterTypes);
获取类内指定数据类型的非私有化构造方法类对象
Class... initParameterTypes  不定长参数,要求数据类型是Class类型
例如:
cls.getConstructor();
==> 无参数构造方法 Person();
cls.getConstructor(int.class);
==> int类型参数构造方法 Person(int);
cls.getConstructor(int.class, String.class);
==> int类型和String类型参数构造方法 Person(int, String);

Constructor getDeclaredConstructor(Class... initParameterTypes);
【暴力反射】
获取类内的指定数据类型构造方法,包括私有化构造方法
Class... initParameterTypes  不定长参数,要求数据类型是Class类型
例如:
cls.getDeclaredConstructor(String.class);
==> 私有化构造方法 private Person(String);

Object newInstance(Object... initParameters);
通过Constructor对象调用,需要参数是一个不定长参数Object类型,是针对当前Constructor对象,对应构造方法的实际参数。
    Object... initParameters 不定长参数,要求是Object类型,参数个数不确定
    例如:
Constructor con1 = cls.getConstructor();
==> 无参数构造方法 Person();
con1.newInstance();

Constructor con2 = cls.getConstructor(int.class, String.class);
==> int类型和String类型参数构造方法 Person(int, String);
con2.newInstance(10, "苟磊");


Method[] getMethods();
获取当前类内的所有非私有化成员方法,包含从父类继承而来子类可以使用的成员方法

Method[] getDeclaredMethods();
【暴力反射】
获取当前类内的所有自有成员方法,包含私有化成员方法,但是不包含从父类继承而来的方法。

Method getMethod(String methodName, Class... parameterTypes);
根据指定的方法名和对应的形式参数列表数据类型,选择当前类内的非私有化成员方法类对象。
String methodName 方法名
Class... parameterTypes 当前方法运行需要的形式参数列表数据类型
例如:
cls.getMethod("game");
==> public void game();
cls.getMethod("game", String.class);
==> public void game(String);

Method getDeclaredMethod(String methodName, Class... parameterTypes);
【暴力反射】
根据指定的方法名和对应的形式参数列表数据类型,获取类内的成员方法,包含私有化成员方法
String methodName 方法名
Class... parameterTypes 当前方法运行需要的形式参数列表数据类型

Object invoke(Object obj, Object... values);
通过Method类对象调用,执行Method类对象对应的成员方法
Object obj 是执行当前方法的类对象,谁的方法
Object... values 是当前方法执行所需参数列表


Field[] getFields();
获取类内的所有非私有化成员变量

Field[] getDeclaredFields();
【暴力反射】
获取类内的所有成员变量,包括私有化成员变量

Field getField(String fieldName);
根据指定的成员变量名字获取对应的非私有化成员变量对象
        
Field getDeclaredField(String fieldName);
【暴力反射】
根据指定的成员变量名字获取对应的成员变量对象,包括私有化成员变量


Object get(Object obj);
获取指定类对象中对应成员变量的数据
void set(Object obj, Object value);
使用指定数据,赋值对应类对象中的成员变量数据


 给予暴力反射权限操作方法
AccessibleObject对象.setAccessible(boolean flag);
给予单个的AccessibleObject对象给予操作权限
public static void setAccessible(AccessibleObject[] array, boolean flag);
给予AccessibleObject对象数组全体操作权限。
AccessibleObject
是Field, Method, Constructor这些类的父类,表示可以被授予权限的

2.注解 自定义注解

自定义注解的作用:在反射中获取注解,以取得注解修饰的类、方法或属性的相关解释

元注解:

  元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
    1.@Target,
    2.@Retention,
    3.@Documented,
    4.@Inherited
    public @interface 注解名 {定义体}

   @Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
   
   
   @Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
   
    @Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员
    
    @Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

3.BIO NIO AIO

1.同步:使用同步IO时,Java自己处理IO读写。

2.异步:使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,完成后OS通知Java处理(回调)。

3.阻塞:使用阻塞IO时,Java调用会一直阻塞到读写完成才返回。

4.非阻塞:使用非阻塞IO时,如果不能立马读写,Java调用会马上返回,当IO事件分发器通知可读写时在进行读写,不断循环直到读写完成。

下面是重点了(敲黑板!)!

BIO是连接一个线程。

NIO是请求一个线程。

AIO是有效请求一个线程。

1.BIO:同步并阻塞,服务器的实现模式是一个连接一个线程,这样的模式很明显的一个缺陷是:由于客户端连接数与服务器线程数成正比关系,可能造成不必要的线程开销,严重的还将导致服务器内存溢出。当然,这种情况可以通过线程池机制改善,但并不能从本质上消除这个弊端。

2.NIO:在JDK1.4以前,Java的IO模型一直是BIO,但从JDK1.4开始,JDK引入的新的IO模型NIO,它是同步非阻塞的。而服务器的实现模式是多个请求一个线程,即请求会注册到多路复用器Selector上,多路复用器轮询到连接有IO请求时才启动一个线程处理。

3.AIO:JDK1.7发布了NIO2.0,这就是真正意义上的异步非阻塞,服务器的实现模式为多个有效请求一个线程,客户端的IO请求都是由OS先完成再通知服务器应用去启动线程处理(回调)。

4.网络编程(Socket)

5.TCP和UDP

网络协议是每个前端工程师都必须要掌握的知识,TCP/IP 中有两个具有代表性的传输层协议,分别是 TCP 和 UDP,本文将介绍下这两者以及它们之间的区别。
小结TCP与UDP的区别:
1、基于连接与无连接;

2、对系统资源的要求(TCP较多,UDP少);

3、UDP程序结构较简单;

4、流模式与数据报模式 ;

5、TCP保证数据正确性,UDP可能丢包;

6、TCP保证数据顺序,UDP不保证。

6.TCP的三次握手四次挥手

TCP建立连接要进行3次握手,而断开连接要进行4次
第一次: 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求 ;

第二次: 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1;

第三次: 由B 端再提出反方向的关闭请求,将FIN置1 ;

第四次: 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束.。

由TCP的三次握手和四次断开可以看出,TCP使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据正式传输前就有了交互, 为数据正式传输打下了可靠的基础。

第一次握手:主机A通过向主机B 发送一个含有同步序列号的标志位的数据段给主机B,向主机B 请求建立连接,通过这个数据段, 主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我。

第二次握手:主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;你要用那个序列号作为起始数据段来回应我

第三次握手:主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:"我已收到回复,我现在要开始传输实际数据了,这样3次握手就完成了,主机A和主机B 就可以传输数据了。

7.TCP的粘包和拆包

8.00P的理(特征)

1.6JDK的新特性

1.JDK8的新特性

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应用

2.JDK9

3.JDK10

4.JDK11

二、框架

2.1Spring

1.创建对象的三种方式

第一种方式:使用默认构造函数创建。在spring的配置文件中使用bean标签,配以id和class属性之后,且没有其他属性和标签时。采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。
<bean id="userDao" class="com.qf.dao.UserDaoImpl"></bean>

第二种方式: 实例工厂创建对象(使用某个类中的方法创建对象,并存入spring容器)
<bean id="userFactory" class="com.qf.factory.UserFactory"></bean>
<bean id="userDao" factory-bean="userFactory" factory-method="getUserDao"></bean>

第三种方式:使用静态工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)
<bean id="userDao" class="com.qf.factory.UserStaticFactory" factory-method="getUserDao"></bean>

实例工厂需要获得工厂的实例对象才能创建bean对象,静态工厂直接使用静态方法创建

2.属性注入的方式

方式一:通过set方法注入

    <bean id="user" class="com.fuke.domain.User">
        <property name="username" value="张三"/>
        <property name="password" value="1234"/>
    </bean>
    //在类内需要提供set方法,使用的是<property>标签

方式二:通过构造方法注入

<bean id="user" class="com.fuke.domain.User">
        <constructor-arg name="username" value="张三"/>
        <constructor-arg name="password" value="1234"/>
 </bean>
  //在类内需要提供构造方法,使用的是<constructor-arg>标签

方式三:使用注解提供

用于创建对象的
	他们的作用就和在XML配置文件中编写一个<bean>标签实现的功能是一样的
	Component:用于把当前类对象存入spring容器中
	(Component的属性)@value:指定bean的id。当我们不写时,它的默认值是当前类名,且首字母改小写。
	        
	Controller:一般用在表现层
	Service:一般用在业务层 
	Repository:一般用在持久层
	以上三个注解的作用和属性与Component相同,是spring框架为我们提供明确的三层使用的注解
        
用于注入数据的
	他们的作用就和在xml配置文件中的bean标签中写一个<property>标签的作用是一样的
	Autowired:自动按照类型注入
	Qualifier:配合Autowired在类型上再按照名称注入,value属性:用于指定注入bean的id,
	Resource:直接按照bean的id注入。它可以独立使用,name属性:用于指定bean的id

3.IOC和DI

IOC:
“控制反转”,是一种设计思想。就是对象之间的依赖关系由容器来创建,对象之间的关系本来是由我们开发者自己创建和维护的,在我们使用Spring框架后,对象之间的关系由容器来创建和维护,将开发者做的事让容器做,这就是控制反转。BeanFactory接口是Spring loc容器的核心接口。
DI:
我们在使用Spring容器的时候,容器通过调用set方法或者是构造器来建立对象之间的依赖关系。
控制反转是目标,依赖注入是我们实现控制反转的一种手段,控制反转包括依赖注入。

这就要讲到控制反转 创建bean的三种方式:bean+有/无构造方法,普通工厂,静态工厂
和依赖注入的三种方式:set、构造器、注解

4.IOC的创建过程(Spring Bean的生命周期)

1、Spring容器根据配置中的 bean定义中实例化 bean。
2、Spring使用依赖注入填充所有属性,如 bean中所定义的配置。
3、如果bean实现BeanNameAware接口,则工厂通过传递bean 的ID来调用setBeanName()。
4、如果bean实现 BeanFactoryAware 接口,工厂通过传递自身的实例来调用setBeanFactory()。
5、如果存在与 bean关联的任何 BeanPostProcessors,则调用 preProcessBeforeInitialization()方法。
6、如果为 bean指定了init方法 ( 的init-method 属性),那么将调用它。
7、最后,如果存在与 bean关联的任何 BeanPostProcessors ,则将调用postProcessAfterInitialization()方法。
8、如果 bean实现 DisposableBean接口,当spring容器关闭时,会调用destory()。
9、如果为 bean 指定了destroy方法( 的 destroy-method 属性),那么将调用它。

5.AOP(动态代理)

AOP底层原理:就是代理机制
动态代理:
    特点:字节码随用随创建,随用随加载
    作用:不修改源码的基础上对方法增强
分类:
    基于接口的动态代理
    基于子类的动态代理

Spring的AOP代理:
    JDK动态代理:被代理对象必须要实现接口,才能产生代理对象.如果没有接口将不能使用动态代理技术。
    CGLib动态代理:第三方代理技术,cglib代理.可以对任何类生成代理.代理的原理是对目标对象进行继承代理. 如果目标对象被final修饰.那么该类无法被cglib代理.

    结论:Spring框架,如果类实现了接口,就使用JDK的动态代理生成代理对象,如果这个类没有实现任何接口,使用CGLIB生成代理对象。
    jdk动态代理:
    (1)实现InvocationHandler接口
    (2)创建代理类Proxy.newProxyInstance(动态加载代理类,代理类实现接口);
    (3)重写invoke方法(虚拟机自动调用方法)
    cglib:
    (1)实现MethodInterceptor接口
    (3)重写intercept方法
    
AOP的术语:
    Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点.
    Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义.
    Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
    Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field.
    Target(目标对象):代理的目标对象
    Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程.
    spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
    Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类
    Aspect(切面): 是切入点和通知(引介)的结合

6.Spring三级缓存(循环依赖)

一级缓存:singletonObjects:用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用
二级缓存:earlySingletonObjects:提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖
三级缓存:singletonFactories:单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖、


先从一级缓存singletonObjects中去获取。(如果获取到就直接return)
如果获取不到或者对象正在创建中(isSingletonCurrentlyInCreation()),那就再从二级缓存earlySingletonObjects中获取。(如果获取到就直接return)
如果还是获取不到,且允许singletonFactories(allowEarlyReference=true)通过getObject()获取。就从三级缓存singletonFactory.getObject()获取。(如果获取到了就从singletonFactories中移除,并且放进earlySingletonObjects。其实也就是从三级缓存移动(是剪切、不是复制哦~)到了二级缓存

7.scope的值

1、singleton:一个Spring容器中只有一个Bean的实例,此为Spring的默认配置,全容器共享一个实例
2、prototype:每次调用新建一个Bean的实例
Spring2.0以后增加了三个作用域
3、Request:Web项目中,给每一个http request新建一个Bean实例
4、Session:Web项目中,给每一个http session新建一个Bean实例。
5、GlobalSession:这个只在portal应用中有用,给每一个global http session新建一个Bean实例。

8.Spring Bean的初始化顺序 :@Order @DependOn

@Order:
注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级;
默认是最低优先级,值越小执行顺序优先级越高
@DependOn:
bean A上使用@DependsOn注解,告诉容器bean B应该先被初始化
@PostConstruct: 用的不多

9.@Autowire和@Resource区别

首先,这个两个注解都是用来完成组件的装配的,即利用依赖注入(DI),完成对ioc容器当中各个组件之间依赖的装配赋值

@Autowire

这是spring提供的一个注解,,默认是按照类型装配(by-type),要求容器中一定要有这个类型的对象,如果没有将会报错,抛出异常。也可以通过设置可以@Autowired(required = false),来告诉容器,如果没有,可以不注入。

当容器中有多个相同类型的对象,会报错,可以配合@Qualifier("beanname"),来指定装配哪个对象的id。

@Resources注解是属于J2EE的一个注解,他可以设置 by-name 和by-type来进行自动装配。

  • 当设置了 name和type 即:@Resource(name = "employee", type = "Employee.class"),根据设置的条件到ioc中注入唯一的 对象
  • 当只设置了name,则按照name,装配,如果没有那么抛出异常。
  • 当只设置了type,那么按照类型装配,如果ioc容器当中存在多个,或不存在,抛出异常。
  • 如果name和type都没有指定,那么先按by-name查找,(如果 @Autowire注解标注在对象上 by-name查找的值 是对象的字段名,在方法上,则是参数的名),如果by-name 没查找到,那么就进行 by-type查找。如果都没查找到,那么抛出异常。

10.spring常用的设计模式

1)工厂设计模式(简单工厂和工厂方法)

Spring使用工厂模式可以通过BeanFactory或ApplicationContext创建bean对象。

两者对比:

- BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory来说会占用更少的内存,程序启动速度更快。
- ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。

2)单例设计模式

Spring中bean的默认作用域就是singleton。除了singleton作用域,Spring bean还有下面几种作用域:

- prototype : 每次请求都会创建一个新的 bean 实例。
- request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
- session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
- GlobalSession:这个只在portal应用中有用,给每一个global http session新建一个Bean实例。

3)代理设计模式
  Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDK Proxy去进行代理了,这时候Spring AOP会使用Cglib,这时候Spring AOP会使用Cglib生成一个被代理对象的子类来作为代理。
4)模板方法设计模式
模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
5)观察者设计模式
6)适配器设计模式
适配器设计模式将一个接口转换成客户希望的另一个接口,适配器模式使得接口不兼容的那些类可以一起工作,其别名为包装器。在Spring MVC中,DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler,解析到对应的Handler(也就是我们常说的Controller控制器)后,开始由HandlerAdapter适配器处理。为什么要在Spring MVC中使用适配器模式?Spring MVC中的Controller种类众多不同类型的Controller通过不同的方法来对请求进行处理,有利于代码的维护拓展。

7)装饰者设计模式
装饰者设计模式可以动态地给对象增加些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。Spring 中配置DataSource的时候,DataSource可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下切换不同的数据源,这个时候据需要用到装饰者模式。

8)策略设计模式

2.2 SpringMVC

1.SpringMVC的运行流程(实现原理)

img

  1. 用户发送请求至前端控制器DispatcherServlet(前端控制器)。
  2. DispatcherServlet(前端控制器)收到请求调用HandlerMapping(处理器映射器)。
  3. 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet(前端控制器)。
  4. DispatcherServlet(前端控制器)通过HandlerAdapter(处理器适配器)调用处理器。
  5. 执行处理器(Controller层,也叫后端控制器)。
  6. Controller执行完成返回数据和视图(ModelAndView)。
  7. HandlerAdapter(处理器适配器)将controller执行结果ModelAndView返回给DispatcherServlet(前端控制器)。
  8. DispatcherServlet(前端控制器))将ModelAndView传给ViewReslover视图解析器。
  9. ViewReslover(视图解析器)解析后返回具体的View视图(JSP / HTML)。
  10. DispatcherServlet(前端控制器)对View进行渲染视图(即将模型数据填充至视图中)。
  11. DispatcherServlet(前端控制器)响应用户,用户看到界面和数据。

2.Restful @PathVariable

PathVariable注解
作用:拥有绑定url中的占位符的。url中有/delete/{id},{id}就是占位符
属性
value:指定url中的占位符名称

R
请求路径一样,可以根据不同的请求方式去执行后台的不同方法
restful风格的URL优点
结构清晰
符合标准
易于理解
扩展方便

例如:
jsp:
	<a href="user/testPathVariable/10">testPathVariable</a>

controller:
    @RequestMapping(value="/testPathVariable/{uid}")
    public String testPathVariable(@PathVariable(name="uid") String id){
        System.out.println(id);
        return "success";
    }

3.@ResponseBody @RequestBody @RequestParam

@RequestBody和@ResponseBody一般用于ajax中获取请求json类型的数据和返回响应json类型的数据

@RequestBody :用于获取请求体json的数据(注意:GET方式请求不可以)
controller方法的参数是json类型

@ResponseBody:用于响应json数据
返回的参数为json类型

RequestParam注解
作用:把请求中的指定名称的参数传递给控制器中的形参赋值,用于指定前端的参数名和参数列表中相对应
属性
value:请求参数中的名称
required:请求参数中是否必须提供此参数,默认值是true,必须提供

2.3 Mybatis

1.Mybatis动态SQL

实现动态SQL的元素主要有:

  • if 可以对标签内的语句判断,满足条件就生效,不满足就什么也不做
  • where 可以省略 and 和or 前缀
  • set 可以省略()前后缀
  • choose(when,otherwise)
  • trim 可以代替所有
  • foreach 批量操作的标签,可以在整体的前后设置前后缀,以及单个分隔符

2.Mybatis $和#区别

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

Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement 的 set方法来赋值;

Mybatis在处理${}时,就是把${}替换成变量的值。
使用#{}可以有效的防止sQL注入,提高系统安全性。

3.Mybatis批处理

//批量查询,更新,插入
<insert id="addResource" parameterType="java.util.List">
insert into resource (object_id, res_id, res_detail_value, 
res_detail_name)
values
<foreach collection="list" item=" ResourceList " index="index" separator=",">
(#{ResourceList.objectId,jdbcType=VARCHAR},
#{ResourceList.resId,jdbcType=VARCHAR},
#{ResourceList.resDetailValue,jdbcType=VARCHAR},
#{ResourceList.resDetailName,jdbcType=VARCHAR}
)
</foreach>
</insert>

MyBatis 三种批量插入方式的比较:

1. 普通for循环批量插入数据测试

2. 使用MyBatis提供的BATCH模式
重复使用已经预处理的语句,并且批量执行所有更新语句. 原理:其实底层就是使用jdbc的批量插入操作。 1、预编译PreparedStatement ps = conn.prepareStatement(sql) 2、for循环,使用ps设置n条数据,每设置一条就ps.addBatch();当积攒的数据量到了指定的值时,就执行一次ps.executeBatch并清空缓存; 非batch模式,就是不积攒,有一条数据就,编译一次,然后设置参数,执行一次
3. mybatis中直接使用foreach插入数据

mybatis中直接使用foreach插入数据,就相当于将所有的sql预先拼接到一起,然后一起提交,但是数据量过大,会引起,栈内存溢出了,mysql对语句的长度有限制,默认是4M。动态sql,通过foreach标签进行了sql拼接出一个大sql,交由数据库执行,只需一次调用。将多条数据拼接在sql语句后,一次执行只获取一次session,提交一条sql语句。减少了程序和数据库交互的准备时间。

4.Mybatis实现过程(实现原理)

img

1、解析mybatis-config.xml

首先,Mybatis在初始化SqlSessionFactoryBean的时候,找到mapper路径去解析里面所有的XML文件

2、创建sqlsource

Mybatis会把每个SQL标签封装成SqlSource对象。然后根据SQL语句的不同,又分为动态SQL和静态SQL。其中,静态SQL包含一段String类型的sql语句;而动态SQL则是由一个个SqlNode组成

3、创建mappedStatement

XML文件中的每一个SQL标签就对应一个MappedStatement对象,这里面有两个属性很重要,id–>全限定类名+方法名组成的ID、sqlSource–>当前SQL标签对应的SqlSource对象。创建完MappedStatement对象,将它缓存到Configuration#mappedStatements中。Configuration就包含了所有的SQL信息。到目前为止,XML就解析完成。通过全限定类名+方法名找到MappedStatement对象,然后解析里面的SQL内容,执行即可。

4、dao接口代理

就是通过JDK动态代理,返回了一个Dao接口的代理对象,这个代理对象的处理器是MapperProxy对
象。所有,我们通过@Autowired注入Dao接口的时候,注入的就是这个代理对象,我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法。

5、执行

它就是通过statement全限定类型+方法名拿到MappedStatement 对象,然后通过执行器Executor去执行具体SQL并返回

5.hibernate和 mybatis的区别?

相同点:
1)都属于ORM框架

2)都是对jdbc的包装

3)都属于持久层的框架

不同点:

  1. hibernate是面向对象的,mybatis是面向sql的;

2 )hibernate全自动的orm,mybatis是半自动的orm;

3)hibernate查询映射实体对象必须全字段查询,mybatis 可以不用;

  1. hibernate级联操作,mybatis 则没有;

  2. hibernate编写hql查询数据库大大降低了对象和数据库的耦合性,mybatis 提供动态sql,需要手写sql,与数据库之间的耦合度取决于程序员所写的sql的方法,所以hibernate的移植性要远大于mybatis。

  3. hibernate有方言夸数据库,mybatis依赖于具体的数据库。7) hibernate拥有完整的日志系统,mybatis 则相对比较欠缺。

2.4 SpringBoot

1.自动装配原理(实现过程)

注解名称作用
@Component基础注解(元注解),可被嵌套
@Configuration声明定义Bean,嵌套了@Component
@Import导入类(如AutoConfigurationImportSelector.class)
@ImportResource导入文件(“classpath:/com/acme/database-config.xml”)
@AutoConfigureAfter条件注解控制相对顺序(在条件注入之后注入),Order实现绝对顺序控制
@ConditionalOnClass当类存在时装配
@ConditionalOnMissingClass当类不存在时装配
@EnableAutoConfiguration使用该注解实现自动装配
@SpringBootConfiguration包含@Configuration,启动类的标注
@PropertySourceproperties文件加载
@ConditionalOnSystemPropertyproperties文件中存在满足条件的属性时装配
@EnableConfigurationProperties是带有@ConfigurationProperties注解的类生效

@SpringBootAplication
SpringBoot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就会运行这个类的main方法来启动SpringBoot项目。
@SpringBootApplication是一个组合注解。

@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 是其中最重要的三个注解

@SpringBootConfiguration 其实就是@Configuration,熟悉spring的都知道,@Configuration 是一个类级注释,指示对象是一个bean定义的源。@Configuration 类通过 @bean 注解的公共方法声明bean。减少xml配置。

@ComponentScan告诉Spring 哪个packages 的用注解标识的类 会被spring自动扫描并且装入bean容器。

@EnableAutoConfiguration让Spring Boot根据类路径的jar包依赖为当前项目进行自动化配置。
例如,添加了spring-boot-starter-web依赖,会自动添加Tomcat和Spring MVC的依赖,那么Spring Boot会对Tomcat和Spring MVC进行自动配置。

2.SpringBoot多环境配置

resources文件夹下创建三个以properties为后缀的文件

application-dev.properties:开发环境

application-test.properties:测试环境

application-prod.properties:生产环境

需要在application.properties文件中通过spring.profiles.active属性来设置,其值对应{profile}值。

spring.profiles.active=dev就会加载application-dev.properties配置文件内容

3.打包方式

war包
将项目打成war包,放入tomcat 的webapps目录下面,启动tomcat,即可访问。

jar包
jar包方式启动,也就是使用spring boot内置的tomcat运行。服务器上面只要你配置了jdk1.8及以上,就ok。

2.5 RabbitMQ

1.消息模式 P2P Worker Pub/Sub(exchange 4种)

四种交换机:direct/topic/headers/fanout,默认交换机是direct,其中Publish/Subscribe,Routing,Topics三种模式可以统一归为Exchange模式,只是创建时交换机的类型不一样,分别是fanout、direct、topic。

Hello-World 简单队列 P2P

一个生产者,一个默认的交换机(direct),一个队列,一个消费者

Work 工作队列

一个生产者,一个默认的交换机(direct),一个队列,多个消费者,默认采用公平分发(例如10条消息,每个消费者接收5条)

Publish/Subscribe 发布订阅模式

一个生产者,一个交换机(fanout),多个队列,多个消费者

(1)一个生产者,多个消费者

(2)每一个消费者都有自己的一个队列

(3)生产者没有直接发消息到队列中,而是发送到交换机

(4)每个消费者的队列都在交换机上绑定

(5)消息通过交换机到达每个消费者的队列

Routing 路由模式

一个生产者,一个交换机(directExchange),多个队列,多个消费者,但是通过指定路由key绑定到队列上,完成指定生产者消费者的通信。

生产者发送消息到交换机并指定一个路由key,消费者队列绑定到交换机时要制定路由key(key匹配就能接受消息,key不匹配就不能接受消息)

Topic 主题模式

一个生产者,一个交换机(TopicExchange),多个队列,多个消费者

又称通配符模式(可以理解为模糊匹配,路由模式相当于精确匹配),此模式实在路由key模式的基础上,使用了通配符来管理消费者接收消息。

*号代表一个单词

#号代表0个或多个单词

2.rabbitMQ防止消息丢失

有三个场景下是会发生消息丢失的:

存储在队列中,如果队列没有对消息持久化,RabbitMQ服务器宕机重启会丢失数据。
生产者发送消息到RabbitMQ服务器过程中,RabbitMQ服务器如果宕机停止服务,消息会丢失。
消费者从RabbitMQ服务器获取队列中存储的数据消费,但是消费者程序出错或者宕机而没有正确消费,导致数据丢失。
针对以上三种场景,RabbitMQ提供了三种解决的方式,分别是消息持久化,confirm机制,ACK事务机制。

消息持久化:需要设置Exchange为持久化和Queue持久化,这样当消息发送到RabbitMQ服务器时,消息就会持久化。

四种交换机都是AbstractExchange抽象类的子类,所以根据java的特性,创建子类的实例会先调用父类的构造器,Exchange为持久化:

img

从上面的注释可以看到durable参数表示是否持久化。默认是持久化(true)。

Queue持久化: img

也是通过durable参数设置是否持久化,默认是true。

3.RabbitMQ消息确认机制

RabbitMQ的事务机制是同步操作,会极大的降低RabbitMQ的性能,所以推出了confirm机制

confirm机制

publisher-confirms:设置为true时。当消息投递到Exchange后,会回调confirm()方法进行通知生产者
publisher-returns:设置为true时。当消息匹配到Queue并且失败时,会通过回调returnedMessage()方法返回消息
spring.rabbitmq.template.mandatory: 设置为true时。指定消息在没有被队列接收时会通过回调returnedMessage()方法退回。

//开启confirm

连接对象调用channel.confirmSelect()方法

//确定批量操作是否成功
channel.waitForConfirmsOrDie();// 当你发送的全部消息,有一个失败的时候,就直接全部失败 抛出异常

//开启异步回调 就是return机制
channel.addConfirmListener()

Return机制:

采用Return机制来监听消息是否从exchange送到了指定的queue中

开启Return机制,在发送消息时,指定mandatory参数为true

channel.basicPublish("",“HelloWorld”**,true,**null,msg.getBytes());

4.ack

spring-boot-data-amqp 是自动ACK机制,就意味着 MQ 会在消息发送完毕后,自动帮我们去ACK,然后删除队列中的消息,这样会存在一些问题:如果消费者处理消息需要较长时间,或者在消费消息的时候出现异常,都会出现问题,手动Ack可以避免消息重复消费。

//手动Ack,确定消费消息
//deliveryTag:该消息的index
//multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

2.6 ElasticSearch

1.倒排索引

正排索引:正排索引也称为"前向索引"。它是创建倒排索引的基础,通过文档到关键字(doc->word)的映射,具有以下字段。

(1)LocalId字段(表中简称"Lid"):表示一个文档的局部编号。

(2)WordId字段:表示文档分词后的编号,也可称为"索引词编号"。

(3)NHits字段:表示某个索引词在文档中出现的次数。

(4)HitList变长字段:表示某个索引词在文档中出现的位置,即相对于正文的偏移量。

倒排索引:单词-文档矩阵是表达两者之间所具有的一种包含关系的概念模型,图1展示了其含义。图3-1的每列代表一个文档,每行代表一个单词,打对勾的位置代表包含关系。
把正向索引的值存入一个词典

2.分词

分词(Analysis):将文本切分为一系列单词的过程,比如 "美国留给伊拉克的是个烂摊子吗?"经过分词后的后果为:美国、伊拉克、烂摊子。
分词器(Analyzer):elasticsearch中执行的分词的主体,官方把分词器分成三个层次:

Character Filters:针对文档的原始文本进行处理,例如将印度语的阿拉伯数字"0 12345678 9"转换成拉丁语的阿拉伯数字"0123456789",或者去除HTML中的特殊标记符号,Character Filters可以有零或多个,安装顺序应用;

Tokenizer:核心,将文档的原始文本按照一定规则切分为单词,Tokenizer只能有一个;

Token Filter:对经过Tokenizer处理过后的单词进行二次加工,比如转换为小写,Token Filter也可以有多个,按顺序依次调用.

三者的调用顺序:Character Filters--->Tokenizer--->Token Filter
Character filters (字符过滤器)

字符过滤器以字符流的形式接收原始文本,并可以通过添加、删除或更改字符来转换该流。

举例来说,一个字符过滤器可以用来把阿拉伯数字(٠‎١٢٣٤٥٦٧٨‎٩)‎转成成Arabic-Latin的等价物(0123456789)。

一个分析器可能有0个或多个字符过滤器,它们按顺序应用。

(PS:类似Servlet中的过滤器,或者拦截器,想象一下有一个过滤器链)

Tokenizer (分词器)

一个分词器接收一个字符流,并将其拆分成单个token (通常是单个单词),并输出一个token流。例如,一个whitespace分词器当它看到空白的时候就会将文本拆分成token。它会将文本“Quick brown fox!”转换为[Quick, brown, fox!]

(PS:Tokenizer 负责将文本拆分成单个token ,这里token就指的就是一个一个的单词。就是一段文本被分割成好几部分,相当于Java中的字符串的 split )

分词器还负责记录每个term的顺序或位置,以及该term所表示的原单词的开始和结束字符偏移量。(PS:文本被分词后的输出是一个term数组)

一个分析器必须只能有一个分词器

Token filters (token过滤器)

token过滤器接收token流,并且可能会添加、删除或更改tokens。
analyzer(分析器)是一个包,这个包由三部分组成,分别是:character filters (字符过滤器)、tokenizer(分词器)、token filters(token过滤器)
一个analyzer可以有0个或多个character filters
一个analyzer有且只能有一个tokenizer
一个analyzer可以有0个或多个token filters
character filter 是做字符转换的,它接收的是文本字符流,输出也是字符流
tokenizer 是做分词的,它接收字符流,输出token流(文本拆分后变成一个一个单词,这些单词叫token)
token filter 是做token过滤的,它接收token流,输出也是token流
由此可见,整个analyzer要做的事情就是将文本拆分成单个单词,文本 ---->  字符  ---->  token

3.项目使用 数据同步

Ajax在网页中最大的一个长处是它能够訪问server上的信息而不须要又一次载入网页,这意味着要检索或是更新信息的某一个小部分的时候,仅仅须要从server端传送那一部分须要的信息而不须要又一次下载整个网页。Ajax能够通过两种方式訪问server,即  


     同步:脚本会停留并等待server发送回复然后再继续;

     异步:脚本同意页面继续其进程并处理可能的回复。

    同步处理用户的请求有一点像又一次载入页面可是仅仅须要下载要求的信息而不是整个页面。因此这一方法会比不使用Ajax要快一些由于信息的下载量要小,所以检索的速度就快了。可是用户可能不习惯在与网页互动的时候进行等待,因此除非你要求的信息是小到能够迅速下载完的,否则用户是不会耐心去等待的。
  异步处理避免了server检索时候的延时问题,由于用户能够继续在页面进行操作,而要求的信息也能够在更新页面的同一时候得到处理。特别是较大的请求,使用异步处理,用户则不会特别意识到延时所带来的麻烦,由于他们的注意力仍然放在对页面的操作上。而对于那些瞬时的响应,你的訪客甚至根本不会意识到server发出了这种请求。
  
  
  一个页面需要在不同的PC端访问,在某一PC端对网页内容发生改变时,其他PC端页面数据实时更新显示.

实现:

采用webSocket+AOP通知的方式实现

思路:

当页面数据修改时,会通过后端保存方法存进数据库,这样我们就要一个入口,当数据保存方法被调用执行完后(AOP后置通知),触发webSocket消息机制,向前端发送更新提示,前端调用更新方法进行页面更新.

实现过程:

在网上有很多关于webSocket的实现和总结,这里我就不一一叙述,大家有兴趣可以在网上查阅,但是大致实现方式我总结有三种;

第一种:原生实现方式

第二种:spring管理的方式

第三种:springBoot管理的方式

我采用的是第三种,大家可以根据自己项目的结构和框架来选择使用哪种方式,但是原理都是一样的,只是代码的写法可能略有不同而已.

三、微服务

1.服务的理解

微服务架构的核心目标是把复杂问题简单化, 通过服务划分,把一个完整的系统拆分成多个高内聚、低耦合的小的子系统。使每个子系统可以独立的运行、升级和测试。然后再通过一些集成手段将这些子系统组合在一起,对外提供完整功能的过程。所以我们对微服务设计的过程就是对系统如何做拆分和集成的过程。

2.微服务解决方案(Spring Cloud)

SpringCloud是分布式微服务架构下的一站式解决方案,是各个微服务架构落地技术的集合体,俗称微服务全家桶。

与SpringBoot 关系?
SpringBoot 专注于快速开发单个个体服务。

SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体服务整合并管理起来,为各个微服务之间提供配置管理,服务发现,断路器,路由,微代理,分布式会话等等服务。

SpringBoot可以离开SpringCloud独立使用开发项目,但是SpringCloud离不开SpringBoot.
SpringBoot专注快速,方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。

3.各大组件(5大组件)

Nacos (注册中心)

Eureka 停止更新 Nacos 阿里巴巴 推荐 Zookeeper 很大数据必备

1.注册中心 实现服务的注册和发现

2.配置中心 实现微服务中动态配置的管理

Ribbon/Feign(负载均衡)

核心作用:1.负载均衡 实现服务集群的策略调用 2.实现服务的远程调用

微服务中实现服务调用的方式:

1.Openfeign 声明式客户端调用 @EnableFeignClients @FeignClient

2.Ribbon 负载均衡客户端调用 1.负载均衡算法 2.服务调用 编码式

3.LoadRunner

Gateway(网关)

后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端。

Config(配置中心)

配置集中管理、统一标准

配置与应用分离

实时更新

高可用

//使用注解@value来注入
@Value("${变量名}")

Sentinel (熔断器)

1.Hystrix (豪猪) 2.Sentinel(流量哨兵) 推荐

1.Sentinel的作用(1.流量控制 2.熔断降级 3.自适应保护)

2.流量控制的策略(单机:QPS或者线程数 最大阈值,集群:QPS或线程数,单机均摊、总体阈值)

3.熔断降级策略( RT(Response Time)响应时间,异常响应的比例,异常响 应的数量,还需要设置熔断的间隔 时间秒)

Sentinel+Nacos实现资源流控、降级、热点、授权

熔断、降级、限流:

熔断强调的是服务之间的调用能实现自我恢复的状态;

限流是从系统的流量入口考虑,从进入的流量上进行限制,达到保护系统的作用;

降级,是从系统内部的平级服务或者业务的维度考虑,流量大了,可以干掉一些,保护其他正常使用;

熔断是降级方式的一种;

降级又是限流的一种方式;

三者都是为了通过一 定的方式去保护流量过大时,保护系统的手段。

Sleuth+zipkin (链路跟踪)

4.组件的实现原理(1-2个)

Gateway(网关)

后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端。

Sentinel (熔断器)

1.Hystrix (豪猪) 2.Sentinel(流量哨兵) 推荐

1.Sentinel的作用(1.流量控制 2.熔断降级 3.自适应保护)

2.流量控制的策略(单机:QPS或者线程数 最大阈值,集群:QPS或线程数,单机均摊、总体阈值)

3.熔断降级策略( RT(Response Time)响应时间,异常响应的比例,异常响 应的数量,还需要设置熔断的间隔 时间秒)

Sentinel+Nacos实现资源流控、降级、热点、授权

熔断、降级、限流:

熔断强调的是服务之间的调用能实现自我恢复的状态;

限流是从系统的流量入口考虑,从进入的流量上进行限制,达到保护系统的作用;

降级,是从系统内部的平级服务或者业务的维度考虑,流量大了,可以干掉一些,保护其他正常使用;

熔断是降级方式的一种;

降级又是限流的一种方式;

三者都是为了通过一定的方式去保护流量过大时,保护系统的手段。

Sleuth+zipkin (链路跟踪)

Nginx与Ribbon的区别

服务器端负载均衡 Nginx

Nginx 基于C语言,快速,性能高5w/s。

Redis 5w/s,RibbatMQ 1.2w/s ApacheActiveMQ 0.6w/s 业务系统,kafka 20w~50w/s大数据,Zuul2.0 200w/s

负载均衡、反向代理,代理后端服务器。隐藏真实地址,防火墙,不能外网直接访问,安全性较高。属于服务器端负载均衡。既请求由 nginx 服务器端进行转发。

客户端负载均衡 Ribbon
Ribbon 是从 eureka 注册中心服务器端上获取服务注册信息列表,缓存到本地,然后在本地实现轮询负载均衡策略。

既在客户端实现负载均衡。

应用场景的区别:

Nginx 适合于服务器端实现负载均衡 比如 Tomcat ,Ribbon 适合与在微服务中 RPC 远程调用实现本地服务负载均衡,比如 Dubbo、SpringCloud 中都是采用本地负载均衡

5.分布式锁

在集群的环境下,对jvm进行枷锁,这就是分布式锁。

分布式锁的具备条件:

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; 
2、高可用的获取锁与释放锁; 
3、高性能的获取锁与释放锁; 
4、具备可重入特性; 
5、具备锁失效机制,防止死锁; 
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

三种分布式锁的实现:

1.基于数据库实现排他锁

基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

2.基于redis实现

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

3.基于zookeeper实现

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

6.分布式事务

就是一次大操作由不同小操作组成,这些小操作分布在不同服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。

CAP理论是:分布式系统在设计时只能在一致性(Consistency)、可用性(Availability)、分区容忍性(Partition Tolerance)中满足两种。

一致性(Consistency):服务A、B、C三个结点都存储了用户数据, 三个结点的数据需要保持同一时刻数据一致性。

可用性(Availability):服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。

分区容忍性(Partition Tolerance):分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。 分布式系统不可避免的出现了多个系统通过网络协同工作的场景,结点之间难免会出现网络中断、网延延迟等现象,这种现象一旦出现就导致数据被分散在不同的结点上,这就是网络分区

7.遇到的问题

8.Dubbo相关(协议、注册中心、超时、服务容错)

9.SpringCloud+Dubbo

四、数据库

4.1 Mysql

1.存储引擎(InnoDB+Myisma)

MySIAM类型不支持事务处理等高级处理,而InnoDB类型支持。MyISAM类型的表强调的是表的性能,其执行速度比InnoDB类型要快,但是不提供事务支持,而InnoDB提供事务支持以及外键等高级数据库功能。

MyISAM具有检查和修复表格的多大数工具,MyISAM表格可以被压缩,而且它们支持全文搜索,它们不是事务安全的,而且也不支持外键。如果事务回滚就造成不完全回滚,不具有原子性。如果执行大量的select,MyISAM是更好的选择。

InnoDB:这种类型是事务安全的,底层结构b+树,它与BDB类型具有相同的特性,他们还支持外键,InnoDB表格的速度很快,具有比BDB 还丰富的特行,因此如果需要一个事务安全的存储引擎,就可以使用它,如果你的数据执行大量的INSERT或UPDATE,处于性能方面的考虑,应该使用InnoDB表 , 对于支持事物的InnoDB类型;

在开启事务的情况下,查询使用for update,如果使用了索引(主键)并且索引生效的情况下,锁的是查到的行,否则是表锁。

2.SQL执行过程

第一步:应用程序将sql发送给数据库服务器执行

在数据库执行sql时,应用程序会连接到相应的数据库服务器,将sql发送给服务器处理

第二步:服务器解析sql语句

1:SQL计划缓存

1):服务器在接收到查询请求后,并不会马上去数据库查询,而是在数据库中的计划缓存中找是否有相对应的执行计划,如果存在,就直接调用已经编译好的执行计划,节省了执行计划的编译时间。

2):如果所查询的行已经存在于数据缓冲存储区中,就不用查询物理文件了,而是从缓存中取数据,这样从内存中取数据就会比从硬盘上读取数据快很多,提高了查询效率.数据缓冲存储区会在后面提到。

3)如果在SQL计划缓存中没有对应的执行计划,服务器首先会对用户请求的SQL语句进行语法效验,如果有语法错误,服务器会结束查询操作,并用返回相应的错误信息给调用它的应用程序。

4)语法符合后,就开始验证它的语义是否正确,例如,表名,列名,存储过程等等数据库对象是否真正存在,如果发现有不存在的,就会报错给应用程序,同时结束查询。

5)获得对象的解析锁,我们在查询一个表时,首先服务器会对这个对象加锁,这是为了保证数据的统一性,如果不加锁,此时有数据插入,但因为没有加锁的原因,查询已经将这条记录读入,而有的插入会因为事务的失败会回滚,就会形成脏读的现象

6)校验数据库用户权限

对数据库用户权限的验证,SQL语句语法,语义都正确,此时并不一定能够得到查询结果,如果数据库用户没有相应的访问权限

7)语法,语义,权限都验证后,服务器并不会马上给你返回结果,而是会针对你的SQL进行优化,选择不同的查询算法以最高效的形式返回给应用程序

8)当确定好执行计划后,就会把这个执行计划保存到SQL计划缓存中

第三步:执行sql

3.慢查询(获取需要优化的SQL语句)

1,slow_query_log

这个参数设置为ON,可以捕获执行时间超过一定数值的SQL语句。

2,long_query_time

当SQL语句执行时间超过此数值时,就会被记录到日志中,建议设置为1或者更短。

3,slow_query_log_file

记录日志的文件名。

4.SQL优化

explain查看sql语句执行效率
explain select * from uchome_space limit 10 union select * from uchome_space limit 10,10
SET STATISTICS IO ON; 开启性能查询
SET STATISTICS IO OFF;关闭性能查询

5.索引(使用、生效、底层原理(BTree B-Tree B+Tree)

面试题

问:索引有哪些数据结构?
答:哈希表、完全平衡二叉树、B树、B+树
问:为何Mysql选择了B+树?
答:哈希表、字段值所对应的数组下标是哈希算法随机算出来的,所以可能出现哈希冲突,哈希表的特点就是可以快速的精确查询,但是不支持范围查询;
二叉树、二叉树是有序的,所以是支持范围查询的。如果数据多了,树高会很高,查询的成本就会随着树高的增加而增加。 
B树、B树的表示要比完全平衡二叉树要“矮”,原因在于B树中的一个节点可以存储多个元素。B树其实就已经是一个不错的数据结构,用来做索引效果还是不错的。
问:那为啥没用B树,而用了B+树?
答:B+树的表示要比B树要“胖”,原因在于B+树中的非叶子节点会冗余一份在叶子节点中,并且叶子节点之间用指针相连。
我们看一下上面的数据结构,最开始的Hash不支持范围查询,二叉树树高很高,只有B树跟B+有的一比。
而B+树是B树的升级版,只是把非叶子节点冗余一下,这么做的好处是为了提高范围查找的效率。提高了的原因也无非是会有指针指向下一个节点的叶子节点。
问:那Hash表在哪些场景比较适合?
答:等值查询的场景,就只有KV(Key,Value)的情况,例如Redis、Memcached等这些NoSQL的中间件。
问:B+树中一个节点到底多大合适?
答:B+树中一个节点为一页或页的倍数最为合适。
因为如果一个节点的大小小于1页,那么读取这个节点的时候其实也会读出1页,造成资源的浪费。
如果一个节点的大小大于1页,比如1.2页,那么读取这个节点的时候会读出2页,也会造成资源的浪费。
所以为了不造成浪费,所以最后把一个节点的大小控制在1页、2页、3页、4页等倍数页大小最为合适。

Mysql的基本存储结构是页(记录都存在页里边):
在这里插入图片描述
在这里插入图片描述

  • 各个数据页可以组成一个双向链表
  • 而每个数据页中的记录又可以组成一个单向链表
  • 每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
  • 以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。

索引存储在内存中,为服务器存储引擎为了快速找到记录的一种数据结构。
索引分类:
单值索引:一个表可以有多个单值索引,例如user表中的:name字段
唯一索引:不能重复的字段,例如:id
复合索引(联合索引):多个列组成的索引,类似二级目录,(name,age),(a,b,c,d)

复合索引中有个最左匹配原则
如果是复合索引,那么key也由多个列组成,同时,索引只能用于查找key是否存在(相等),遇到范围查询 (>、<、between、like左匹配)等就不能进一步匹配了,后续退化为线性查找。因此,索引中列的排列顺序决定了可命中索引的列数。
例子:
如有索引 (a,b,c,d),查询条件 a=1 and b=2 and c>3 and d=4,则会在每个节点依次命中a、b、c,无法命中d。(c已经是范围查询了,d肯定是排不了序了)

b数的结构图如下:
三层b树可以存储上百万条数据在这里插入图片描述
b+树的结构类似b数,全部数据都存放在叶子节点中,b+树的任意数据查询复杂度:n(b+树的高度)
b+树的表示要比B树要“胖”,原因在于b+树中的非叶子节点会冗余一份在叶子节点中,并且叶子节点之间用指针相连
b+树是b树的升级版,只是把非叶子节点冗余一下,这么做的好处是为了提高范围查找的效率。
提高了的原因也无非是会有指针指向下一个节点的叶子节点。
小结:Mysql选用b+树这种数据结构作为索引,可以提高查询索引时的磁盘IO效率,并且可以提高范围查询的效率,并且b+树里的元素也是有序的。

在这里插入图片描述

B+树相对于B树有一些自己的优势,可以归结为下面几点。

  • 单一节点存储的元素更多,使得查询的IO次数更少,所以也就使得它更适合做为数据库MySQL的底层数据结构了。
  • 所有的查询都要查找到叶子节点,查询性能是稳定的,而B树,每个节点都可以查找到数据,所以不稳定。
  • 所有的叶子节点形成了一个有序链表,更加便于查找。

6.事务(ACID 隔离级别 脏读 、虚读、不可重复读)

事务的特性(ACID):

  1. 原子性(Atomicity):原子性是指一个事务中的操作,要么全部成功,要么全部失败,如果失败,就回滚到事务开始前的状态。
  2. 一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。 只读概念会与原子性搞混,不太理解什么意思。乍一看觉得好像一致性就是对原子性进行了另外一种描述,好多人举例说A转给B200 ,A的账号结果减200,B的账户加200,这样保证逻辑运算称为一致性。 其实对于系统来说它不知道什么是逻辑,它只明白命令,而我理解的一致性就是,在事务之前A有1000,那它结束之后只有800,另外那200去哪了,要有个去向,不能凭空消失,B的账户在事务结束后加了200,那这200从来哪来的,也需要有个来源,不能凭空生成的。
  3. 隔离性(Isolation):隔离性是当多个用户 并发的 访问数据库时,如果操作同一张表,数据库则为每一个用户都开启一个事务,且事务之间互不干扰,也就是说事务之间的并发是隔离的。再举个例子,现有两个并发的事务T1和T2,T1要么在T2开始前执行,要么在T2结束后执行,如果T1先执行,那T2就在T1结束后在执行。
  4. 持久性(Durability):持久性就是指如果事务一旦被提交,数据库中数据的改变就是永久性的,即使断电或者宕机的情况下,也不会丢失提交的事务操作。

1.脏读: 脏读是指一个事务在处理数据的过程中,读取到另一个未提交事务的数据。

2.不可重复读:不可重复读是指对于数据库中的某个数据,一个事务范围内的多次查询却返回了不同的结果,这是由于在查询过程中,数据被另外一个事务修改并提交了。

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

四种隔离级别解决了上述问题
1.读未提交(Read uncommitted):这种事务隔离级别下,select语句不加锁。此时,可能读取到不一致的数据,即“读脏 ”。这是并发最高,一致性最差的隔离级别。
2.读已提交(Read committed): 可避免 脏读 的发生。在互联网大数据量,高并发量的场景下,几乎 不会使用 这两种隔离级别。
3.可重复读(Repeatable read): MySql默认隔离级别。可避免 脏读不可重复读 的发生。
4.串行化(Serializable ):可避免 脏读、不可重复读、幻读 的发生。

7.数据库的传播行为

传播行为分为两种:分为支持事物的传播和不支持事物的传播

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

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

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

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

5、PROPAGATION_NOT_SUPPORTED:(不支持事务)以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

6、PROPAGATION_NEVER:(不支持事务)以非事务方式执行,如果当前存在事务,则抛出异常。

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

8.Mysql函数和自定义函数

9.Mysql锁

for update 与 lock in share mode 属于行级锁和页级锁

4.2 Oracle

1.序列

2.存储过程

3.自定义函数

4.分页

4.3 Redis

1.Redis为什么快

1.redis是基于内存的,内存的读写速度非常快;

2.redis是单线程的,省去了很多上下文切换线程的时间;

3.redis使用多路复用技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。
** 1)不需要各种锁的性能消耗**

Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除

一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。

总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

** 2)单线程多进程集群方案**

单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。

所以单线程、多进程的集群不失为一个时髦的解决方案。

3)CPU消耗

采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。

:但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?

:可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。

Redis单线程的优劣势
单进程单线程优势:

  • 代码更清晰,处理逻辑更简单
  • 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
  • 不存在多进程或者多线程导致的切换而消耗CPU

单进程单线程弊端:

  • 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;
    IO多路复用技术
  • redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

2.Redis常用的数据类型(8种)

string:最基本的数据类型,二进制安全的字符串,最大512M。
list:按照添加顺序保持顺序的字符串列表。
set:无序的字符串集合,不存在重复的元素。
sorted set:已排序的字符串集合。
hash:key-value对的一种集合。
bitmap:(位图)更细化的一种操作,以bit为单位。512MB
hyperloglog:基于概率的数据结构。
Geo:地理位置信息储存起来, 并对这些信息进行操作

3.Redis 穿透、雪崩、倾斜等

雪崩:热点数据同时间失效,大量的用户请求涌入,直接就导致本来能扛5000qps的被6000给打死了

穿透:按照通俗来讲,redis是保护sql的一种方式,是用户和sql的中间人,当用户直接去访问了sql就是穿透的情况。如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。

击穿:至于缓存击穿嘛,这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。

解决方案 :

  • 缓存击穿:设置热点数据永远不过期。或者加上互斥锁就能搞定了 。

  • 处理缓存雪崩:在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效,我相信,Redis这点流量还是顶得住的。
    或者设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。

  • 缓存穿透:我会在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。
    这里我想提的一点就是,我们在开发程序的时候都要有一颗“不信任”的心,就是不要相信任何调用方,比如你提供了API接口出去,你有这几个参数,那我觉得作为被调用方,任何可能的参数情况都应该被考虑到,做校验,因为你不相信调用你的人,你不知道他会传什么参数给你。
    从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、位置错误、稍后重试这样的值具体取啥问产品,或者看具体的场景,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。
    这样可以防止攻击用户反复用同一个id暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的,那网关层Nginx本渣我也记得有配置项,可以让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑。
    第二种:布隆过滤器(Bloom Filter)这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。

4.Redis集群方案

5.Redis失效策略

一、定期删除
redis会把设置了过期时间的key放在单独的字典中,定时遍历来删除到期的key。

1).每100ms从过期字典中 随机挑选20个,把其中过期的key删除;
2).如果过期的key占比超过1/4,重复步骤1

为了保证不会循环过度,导致卡顿,扫描时间上限默认不超过25ms。根据以上原理,系统中应避免大量的key同时过期,给要过期的key设置一个随机范围。

二、惰性删除
过期的key并不一定会马上删除,还会占用着内存。 当你真正查询这个key时,redis会检查一下,这个设置了过期时间的key是否过期了? 如果过期了就会删除,返回空。这就是惰性删除。

6.Redis淘汰策略

内存淘汰机制
当redis内存超出物理内存限制时,会和磁盘产生swap,这种情况性能极差,一般是不允许的。通过设置 maxmemory 限制最大使用内存。超出限制时,根据redis提供的几种内存淘汰机制让用户自己决定如何腾出新空间以提供正常的读写服务。

(1)noeviction: 拒绝写操作, 读、删除可以正常使用。如果写入会报异常,默认策略,不建议使用;
(2)allkeys-lru: 移除最近最少使用的key,最常用的策略;
(3)allkeys-random:随机删除某个key,不建议使用;
(4)volatile-lru:在设置了过期时间的key中,移除最近最少使用的key,不建议使用;
(5)volatile-random:在设置了过期时间的key中,随机删除某个key,不建议使用;
(6)volatile-ttl: 在设置了过期时间的key中,把最早要过期的key优先删除。

7.RESP协议

8.Redis的String类型的优化

9.Redis的事务

10.Redis实现分布式锁

11.Redis的持久化策略

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

1.RDB(Redis DataBase)
将内存中的数据以快照的方式写入磁盘中,在redis.conf文件中,我们可以找到如下配置:

save 900 1
save 300 10
save 60 10000

配置含义:
900秒内,如果超过1个key被修改,则发起快照保存
300秒内,如果超过10个key被修改,则发起快照保存
60秒内,如果1万个key被修改,则发起快照保存

RDB方式存储的数据会在dump.rdb文件中(在哪个目录启动redis服务端,该文件就会在对应目录下生成),该文件不能查看,如需备份,对Redis操作完成之后,只需拷贝该文件即可(Redis服务端启动时会自动加载该文件)
在这里插入图片描述
在这里插入图片描述

2.AOF(Append Of File)
AOF默认是不开启的,需要手动开启,同样是在redis.conf文件中开启:
配置文件中的appendonly修改为yes,开启AOF持久化。开启后,启动redis服务端,发现多了一个appendonly.aof文件。之后任何的操作都会保存在appendonly.aof文件中,可以进行查看,Redis启动时会将appendonly.aof文件中的内容执行一遍。
在这里插入图片描述
在这里插入图片描述

如果AOF和RDB同时开启,系统会默认读取AOF的数据。

RDB优点与缺点
优点:

  1. 如果要进行大规模数据的恢复,RDB方式要比AOF方式恢复速度要快。
  2. RDB是一个非常紧凑(compact)的文件,它保存了某个时间点的数据集,非常适合用作备份,同时也非常适合用作灾难性恢复,它只有一个文件,内容紧凑,通过备份原文件到本机外的其他主机上,一旦本机发生宕机,就能将备份文件复制到redis安装目录下,通过启用服务就能完成数据的恢复。

缺点:

  1. RDB这种持久化方式不太适应对数据完整性要求严格的情况,因为,尽管我们可以用过修改快照实现持久化的频率,但是要持久化的数据是一段时间内的整个数据集的状态,如果在还没有触发快照时,本机就宕机了,那么对数据库所做的写操作就随之而消失了并没有持久化本地dump.rdb文件中。

AOF优点与缺点
优点:

  1. AOF有着多种持久化策略:
    appendfsync always:每修改同步,每一次发生数据变更都会持久化到磁盘上,性能较差,但数据完整性较好。
    appendfsync everysec: 每秒同步,每秒内记录操作,异步操作,如果一秒内宕机,有数据丢失。
    appendfsync no:不同步。
  2. AOF文件是一个只进行追加操作的日志文件,对文件写入不需要进行seek,即使在追加的过程中,写入了不完整的命令(例如:磁盘已满),可以使用redis-check-aof工具可以修复这种问题。
  3. Redis可以在AOF文件变得过大时,会自动地在后台对AOF进行重写:重写后的新的AOF文件包含了恢复当前数据集所需的最小命令集合。整个重写操作是绝对安全的,因为Redis在创建AOF文件的过程中,会继续将命令追加到现有的AOF文件中,即使在重写的过程中发生宕机,现有的AOF文件也不会丢失。一旦新AOF文件创建完毕,Redis就会从旧的AOF文件切换到新的AOF文件,并对新的AOF文件进行追加操作。

缺点:

  1. 对于相同的数据集来说,AOF文件要比RDB文件大。
  2. 根据所使用的持久化策略来说,AOF的速度要慢于RDB。一般情况下,每秒同步策略效果较好。不使用同步策略的情况下,AOF与RDB速度一样快。
  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值