SpringBoot启动流程

本地整理了流程图,无法上传
可看Springboot启动流程及原理

IOC(反转控制)与AOP(面向切面编程)

image.png

类加载过程

StringBuilder和StringBuffer的区别

String类中使用final关键字修饰字符数组来保存数组,private fianl char value,所以String对象是不可变的。
StringBuilder与 StringBuffer都继承⾃AbstractStringBuilder 类,在该类中用的也是char[] value保存的,但没有final关键字修饰,所以这两个对象都是可变的。

线程安全

  • String对象不可变,可理解为常量,因此线程安全。
  • StringBuffer对方法/对调用的方法加了Synchronized同步锁,因此线程安全。
  • StringBuilder没有加锁,所以非线程安全。

    性能

  • 对String类型进行改变,都会生成一个新的String对象,然后将指针指向新的String对象。旧的string就等着被回收,所以你每次操作string的时候就会出现很多垃圾对象,效率不高。

  • StringBuffer每次都会对 StringBuffer对象本身进⾏操作,⽽不是⽣成新的对象并改变对象引⽤。
  • StringBuilder比StringBuffer多了个toStringCache字段,用来在toString方法中进行缓存,再重复调用toString()时能提升效率,因此,在不会出现线程安全问题的情况下,StringBuilder比StringBuffer性能好。

    对于三者使⽤的总结

  • 操作少量的数据: 适⽤ String

  • 单线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuilder
  • 多线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuffer

    静态变量、静态方法、静态类

    静态变量

    类级别的变量声明为static,它是属于类的,不是属于类创建的对象或实例,因为它能被类的所有实例公用,因此非线程安全。同时能与fianl一起用,作为所有对象公用的资源或常量。

    静态方法

    属于类不属于实例,它只能访问类的静态变量,或调用类的静态方法。一般作为工具方法,被其他类使用,而不需要创建类的实例。

    静态类

    对嵌套类使用static关键字,不能用于最外层的类。

    静态类与内部类的区别

    内部类

  • 拥有普通类的所有特性,也拥有类成员变量的特性

  • 内部类可以访问其外部类的成员变量,属性,方法及其他内部类

    静态类

  • 只有内部类才能声明为static

  • 只有静态类才能拥有静态成员
  • 静态类只能访问其外部类的静态成员
  • 外部类的静态方法要访问内部类,只能访问静态类

    基本数据类型与引用数据类型

    基本数据类型

    byte/short/int/long/float/double/char/boolen

    引用数据类型

    类/接口/数组等,通过new关键字创建,除了基本数据类型其他都是引用类型

    ==和equals

    ==

  • 判断两个对象的地址是不是相等,基本数据类型的==是比较值,引用数据类型==是内存地址

    equals

  • 判断对象是否相等

  • 类没有覆盖equals时,通过equals比较该类的两个对象时,等价于通过==比较两个对象
  • 类覆盖equals时,通过覆盖的equals比较两个对象的内容是否相等

    注意

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

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

    hashCode与equals

    hashCode

    获取哈希码(散列码),返回一个int整数,用于确定对象在哈希表中的索引位置。

    为什么需要hashCode

    主要目的是为了减少equals次数,相应的就提交了执行速度。
    计算对象的hashCode来判断对象加入位置,如果位置没有对象,则会假设对象没有重复出现,如果有相同hashCode,才会调用equals方法,来检查hashCode相等的对象是否真的相同,如果相同,则不会让其加入成功

    为什么重写equals必须重写hashCode方法

    假如只重写equals而不重写hashcode,那么hashcode方法就是Object默认的hashcode方法,由于默认的hashcode方法是根据对象的内存地址经哈希算法得来的,显然会出现equals相等的对象hashCode不相等的情况,然而原则是两个对象相等,其hashCode一定相等,因此违反了该原则,所以必须重写hashCode方法

    进程、线程、程序

    线程

    与进程相似,是比进程更小的执行单位。一个进程可以产生多个线程。同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或在各个线程之间切换工作时,负担比进程小得多

    程序

    含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,程序是静态的代码。

    进程

    程序的一次执行过程,是系统运行程序的基本单位,是动态的。⼀个进程就是⼀个执⾏中的程序,它在 计算机中⼀个指令接着⼀个指令地执⾏着,同时,每个进程还占有某些系统资源如 CPU 时间, 内存空间,⽂件,输⼊输出设备的使⽤权等等。换句话说,当程序在执⾏时,将会被操作系统载 ⼊内存中。
    线程是进程划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。从另⼀⻆度来说,进程属于操作系统的范畴,主要是同⼀段时间内,可以同时执⾏⼀个以上的程序,⽽线程则是在同⼀程序内⼏乎同时执⾏⼀个以上的程序段。

    线程的状态

    初始/运行(就绪和运行笼统称为运行中)/阻塞/等待(需要等待其他线程做出一些特定动作,如通知或中断)/超时等待(在指定时间自行返回)/终止

    final关键字

    主要用于变量、方法、类三个地方

  • 变量:final修饰基本数据类型,一旦初始化后不能更改;final修饰引用数据类型,初始化后不能再让其指向另一个对象

  • 类:final修饰类,表明这个类不能被继承,该类中的所有成员方法都会被隐式的指定为final方法
  • 方法:final修饰方法,把方法锁定,防止任何继承类修改它的含义,不能被重写。类中所有的private方法都隐式的指定为final

    Java异常处理

    image.png
    所有异常的祖先是java.lang包中的Throwable类,其包含重要子类Exception(异常)和Error(错误)。Exception能被程序本身处理,即被try-catch,Error是无法处理的(只能尽量避免)。
    Exception可分为受检查异常(必须处理)和不受检查异常(可以不处理),除了RuntimeException及其子类如NullPointExecrption 、 NumberFormatException (字符串转换为数字)、 ArrayIndexOutOfBoundsException (数组越界)、 ClassCastException (类型转换错误)、 ArithmeticException (算术错误)等,其他的Exception及其子类都属于受检查异常,如IO 相关的异常、 ClassNotFoundException 、 SQLException等

    try-catch-finally

  • try 块: ⽤于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟 ⼀个 finally 块。

  • catch 块: ⽤于处理 try 捕获到的异常
  • finally 块: ⽆论是否捕获或处理异常, finally 块⾥的语句都会被执⾏。当在 try 块或 catch 块中遇到 return 语句时, finally 语句块将在⽅法返回之前被执⾏。当 try 语句和 finally 语句中都有 return 语句时,在⽅法返回之前,finally 语句的内容将被执⾏,并且 finally语句的返回值将会覆盖原始的返回值

在以下 3 种特殊情况下, finally 块不会被执⾏:

  • 在 try 或 finally 块中⽤了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执⾏
  • 程序所在的线程死亡。
  • 关闭 CPU。

    序列化与反序列化

    序列化

    将对象的状态信息转化为字节序列的过程

    主要用途

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象)

  • 在网络上传送对象的字节序列。(网络传输对象)

    注意

  • static变量不能被序列化,因为静态变量是属于类的

  • 被transient关键字修饰的变量不能被序列化,因为其主要是控制变量是否能够被序列化,如果没有被序列化的成员变量反序列化后,会被设置成初始值,比如String -> null

    为什么需要序列化

  • 我们都知道在java中对象是保持在“堆”中的,堆是一个内存空间,不能长期保持,程序关闭对象就会被回收。但是有时候,我们是需要保存对象里面的信息的,这个时候就需要对对象进行持久化,对象序列化其实就是持久化。

  • 现在微服务很流行,各个服务之间调用对象就需要把对象序列化用于在网络上传输。
  • Java 对象本质上是 class 字节码,很多机器都不能根据这个字节码识别出该 Java 对象。但是序列化是序列成二进制流,二进制在计算机世界是通用的。

    反序列化

    把字节序列恢复为对象的过程

    获取键盘输入的方式

    Scanner

    Scanner in = new Scanner(System.in);
    String s = in.nextLine();

    BufferedReader

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

    IO流

  • 流向:输入流和输出流

  • 操作单元:字节流和字符流(字节流直接操作文件,字符流先放在缓存,再从缓存写入文件)
  • 角色:节点流和处理流
  • InputStream/Reader: 所有输⼊流的基类,前者是字节输⼊流,后者是字符输⼊流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

    有了字节流为什么还需要字符流?

    信息的最小存储单元是字节,但实际是存在字符的输入输出的。字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是⾮常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流比较好,如果涉及到字符的话使⽤字符流比较好。

    BIO/NIO/AIO

    同步与异步:关注的是消息通信机制

    同步:发出一个调用时,在没有得到结果之前,该调用就不返回,一旦调用返回,就得到返回值了,即调用者主动等待这个调用的结果。
    异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果。当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
    实质:访问数据的方式,同步需要当前线程读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,当前进程并不主动读写数据,由操作系统内核完成数据的读写。

    阻塞与非阻塞:关注程序在等待调用结果时的状态

    阻塞:调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
    非阻塞:在不能立刻得到结果之前,该调用不会阻塞当前线程。

    BIO

    同步阻塞IO模式,针对Sender而言,请求发送出去以后,一直等到Receiver有结果了才返回,这是同步。在Sender获取结果的期间一直被block住了,也就是在此期间Sender不能处理其它事情,这是阻塞。
    image.png

    NIO

    同步非阻塞IO模式,针对Sender而言,请求发送出去以后,立刻返回,然后再不停的发送请求,直到Receiver处理好结果后,最后一次发请求给Receiver才获得response。Sender一直在主动轮询,每一个请求都是同步的,整个过程也是同步的。在Sender等待Receiver的response期间一直是可以处理其它事情的(比如:可以发送请求询问结果),这是非阻塞。
    image.png

    AIO

    异步非阻塞IO模式,针对Sender而言,请求发送出去以后,立刻返回,然后再等待Receiver的callback,最后再次请求获取response,这整个过程是异步。在Sender等待Receiver的callback期间一直是可以处理其它事情的,这是非阻塞。
    image.png

    比较

    BIO:适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
    NIO:适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
    AIO:适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

    深拷贝/浅拷贝

    浅拷贝

    对基本数据类型进⾏值传递,对引⽤数据类型进⾏引⽤传递般的拷⻉,此为浅拷⻉。

    例1

    一个对象不包含别的对象的引用,使用浅拷贝,基本数据类型进行值传递,因此修改了user2的值并不会对user1产生影响。
    image.pngimage.png
    image.png

    例2

    一个对象包含了别的对象的引用,使用浅拷贝,并不会把对象中的其他对象进行拷贝,而是直接引用,因此修改了user2中的person对象,user1中也发生了改变。
    image.pngimage.png
    image.png

    深拷贝

    对基本数据类型进⾏值传递,对引⽤数据类型,创建⼀个新的对象,并复制其内容,此为深拷⻉。相比于浅拷贝的例2,深拷贝会将对象中的其他对象的引用创建一个新对象并指向它,因此修改user2时并不会改变user1。

    ArrayList

  • ArrayList底层是基于数组实现的,是一个动态数组,自动扩容。

  • ArrayList不是线程安全的,只能用在单线程环境下。
  • 实现了Serializable接口,因此它支持序列化,能够通过序列化传输;
  • 实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问;
  • 实现了Cloneable接口,能被克隆。

    RandomAccess 接口

    ArrayList 实现了 RandomAccess 接⼝, ⽽ LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!
    ArrayList 底层是数组,⽽ LinkedList 底层是链表。数组天然⽀持随机访问,时间复杂度为 O(1),所以称为快速随机访问。
    链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不⽀持快速随机访问。
    ArrayList 实现了 RandomAccess 接⼝,就表明了他具有快速随机访问功能。 RandomAccess 接⼝只是标识,并不是说ArrayList实现 RandomAccess 接⼝才具有快速随机访问功能的!

    自动扩容机制

ArrayList与LinkedList

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,不保证线程安全;
  • 底层数据结构: Arraylist底层使⽤的是Object数组;LinkedList底层使⽤的是双向链表数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下⾯有介绍到!)
  • 插⼊和删除是否受元素位置的影响
    • ① ArrayList 采⽤数组存储,所以插⼊和删除元素的 时间复杂度受元素位置的影响。 ⽐如:执⾏ add(E e) ⽅法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插⼊和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的操作。
    • ② LinkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置 i 插⼊和删除元素的话( (add(int index, E element) )时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。
  • 是否⽀持快速随机访问: LinkedList 不⽀持⾼效的随机元素访问,⽽ ArrayList ⽀持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。
  • 内存空间占⽤: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空 间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间 (因为要存放直接后继和直接前驱以及数据)。

    ArrayList与Vector

  • ArrayList 是List的主要实现类,底层使⽤ Object[ ] 存储,适⽤于频繁的查找⼯作,线程不安全 ;

  • Vector是List的古⽼实现类,底层使⽤ Object[ ] 存储,线程安全的

    HashMap

    底层原理

    数组+链表的结构,数组默认长度是16,负载因子是0.75,因此阈值是12,超过阈值会进行扩容,链表的默认阈值是8,长度大于阈值时会先判断数组长度是否大于64,若大于将链表转换为红黑树的结构,否则进行数组扩容。

    扩容机制

    HashMap与HashTable

  • 线程是否安全: HashMap 是⾮线程安全的, HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap 吧!);

  • 效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。另外, HashTable 基本被淘汰,不要在代码中使⽤它;
  • 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable不允许有null键和null值,否则会抛出NullPointerException。
  • 初始容量⼤⼩和每次扩充容量⼤⼩的不同:
    • 创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为16。之后每次扩充,容量变为原来的2倍。
    • 创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩ ( HashMap 中的 tableSizeFor() ⽅法保证,下⾯给出了源代码)。也就是说 HashMap总是使⽤ 2 的幂作为哈希表的⼤⼩,后⾯会介绍到为什么是 2 的幂次⽅。
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了᫾⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么 会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时 间。Hashtable 没有这样的机制。

    ConcurrentHashMap 和 Hashtable

    ConcurrentHashMap

    底层原理
    Node数组+链表/红黑二叉树实现
    加锁机制
    并发控制使用synchronized和CAS来操作,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率得到提高。

    CAS

    比较再交换,其操作包含三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置的值与预期原值不匹配,那么处理器不做任何操作。

    HashTable

    底层原理
    数组+链表
    加锁机制
    使用synchronized来保证线程安全,效率非常低下。

    volatile关键字

    用于防止JVM指令重排,保证变量的可见性

  • volatile关键字是线程同步的轻量级实现,所以 volatile性能肯定⽐synchronized关键字要好。但是volatile关键字只能⽤于变量⽽synchronized关键字可以修饰⽅法以及代码块。

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