2022 Java知识点总结

  • Java Code
    • 类加载
      • 类加载机制
      • 双亲委派机制
      • 类的初始化
    • 反射
      • 反射的实现方式和原理
      • 获取反射中的 Class 对象
      • 获取构造函数
      • 获取属性
      • 获取方法
      • 获取Class对象对应类的修饰符、所在包、类名等基本信息
      • 反射获取注解
    • String
      • String, Stringbuilder, Stringbuffer区别
    • 集合
      • HashMap原理(底层数据结构)
      • HashMap 存数据过程
      • CurrentHashMap原理
      • CurrentHashMap存数据的过程
    • JVM
      • 如何选择合适的收集算法?
      • 新生代怎么转化成老年代?
      • JVM 中确定垃圾的几种算法?
    • 常用的垃圾回收算法?
      • 介绍一下垃圾回收器?
      • 什么是双亲委派模型?为什么要使用双亲委派?
    • Java 中的锁
      • 什么是 CAS?
      • 介绍一下锁的升级?
      • synchronized 和 volatile 的区别
    • 多线程
      • 线程的实现方式
      • runnable和callable的区别?
      • 线程中wait()和sleep()方法有什么区别?
      • **启动线程方法start()和run()有什么区别?**
      • 线程池是如何工作的?
      • 线程池的几个参数介绍,常用队列以及线程池中拒绝策略?
      • 创建线程池的几种方式
      • 什么是死锁,如何避免死锁
  • Stream 流
    • Stream注意点?
    • Stream并行流?并行流的原理
    • 创建Stream的方式
    • Stream的常用方法?
  • Spring
    • Spring 包含的模块有哪些?
    • Spring,Spring MVC,Spring Boot 之间什么关系?
    • 描述一下spring mvc的工作原理,前端请求到后端到返回数据,中间都经历了什么?
    • Springboot启动流程
    • Spring 中 Bean的作用域
    • 将一个类声明为 Bean 的注解有哪些?
    • Spring中注解注入常用的注解有哪些?区别是什么?
    • Spring中bean的生命周期
    • Spring 的循环依赖?怎么解决?
    • spring没有解决的循环依赖我们该如何处理
    • Spring中bean是什么时候被实例化的?
    • Spring bean怎么做到自动装配的?
    • Spring事务失效的原因?
    • Spring 中事务的传播机制
    • IoC
      • 什么是 IoC
      • IoC 解决了什么问题?
      • IoC 和 DI 别再傻傻分不清楚?
    • AOP
      • 什么是 AOP?
      • AOP 解决了什么问题?
      • AOP 为什么叫面向切面编程?
      • AOP 的一些使用场景?
  • Redis
    • Redis 的几种数据类型
    • Redis 持久化
      • RDB 持久化
        • RDB 持久化触发方式?
          • 手动触发
          • 自动触发
        • redis.conf中配置RDB
        • RDB 的优缺点?
      • AOF 持久化
        • 如何实现 AOF
        • redis.conf中配置AOF
    • Redis 是如何从持久化中恢复数据?
    • Redis 事务详解
    • 为什么单线程的 Redis 能那么快?
    • Redis高可用的方案?
    • 主从复制原理以及优缺点
    • 哨兵模式原理以及优缺点
    • 集群模式原理、分片算法、优缺点?
    • 缓存穿透、缓存雪崩?如何避免此类问题
    • 缓存的淘汰策略以及Redis中的淘汰策略
  • MySQL数据库
    • 事务的四大特性
    • 事务的隔离级别
    • 索引失效原因

Java Code

类加载

类加载机制

类的加载过程:Java虚拟机把描述类的数据把.class字节码文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Class对象。类的生命周期包括:加载,验证,准备,解析,初始化,使用,卸载。按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

双亲委派机制

保证类加载流程的安全性,才使用的双亲委派?
当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。 只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。自定义Class Loader ->Appilication Class loader -> Extension Classloader ->Bootstrap ClassLoader
违例:JDBC–Context Classloader
tomcat 为每个 App 创建一个 Loader,里面保存着此 WebApp 的 ClassLoader。需要加载 WebApp 下的类时,就取出 ClassLoader 来使用

类的初始化

时机:1.new对象 2.反射调用对象 3.调用类的静态方法4.读写类的静态变量5.初始化某类是他的直接父类还未初始化则会初始化其父类6.启动时会初始化main()方法所在启动类。 准备阶段,虚拟机会将类变量(static 修饰的变量)分配内存并设置零值。
在类初始化阶段,执行类构造器 () 方法。 类初始化方法有如下特点:
编译器会在将 .java 文件编译成 .class 文件时,收集所有类初始化代码和 static {} 域的代码,收集在一起成为 () 方法;
子类初始化时会首先调用父类的 () 方法;
JVM 会保证 () 方法的线程安全,保证同一时间只有一个线程执行。

反射

反射的实现方式和原理

反射是一种运行时获取和修改对象数据的能力。
原理:Method类的invoke方法,获取一个MethodAccessor对象ma【此时会check是否已有现成的ma,有就返回,没有就用ReflectionFactory 对象的 newMethodAccessor 方法生成一个,此处会返回一个代理了NativeMethodAccessorImpl 对象(delegate熟属性)的DelegatingMethodAccessorImpl对象。】,用ma.invoke()来返回反射的对象,此时便调用了NativeMethodAccessorImpl 的 invoke 方法,而在 NativeMethodAccessorImpl 的 invoke 方法里,其会判断调用次数是否超过阀值(numInvocations)。如果超过该阀值,那么就会生成另一个MethodAccessor 对象,并将原来 DelegatingMethodAccessorImpl 对象中的 delegate 属性指向最新的 MethodAccessor 对象。 实际的 MethodAccessor 实现有两个版本,一个是 Native 版本,一个是 Java 版本。

获取反射中的 Class 对象

  • 通过类的 全路径字符串 获取Class (例如:Class clz = Class.forName(“类的全限定名”);)
  • 使用 类.class (例如:String.class)
  • 通过 对象.getClass() 方法 (例如:Class clz = string.getClass();)

获取构造函数

  • Connstructor getConstructor(Class<?>... parameterTypes) 返回此Class对象对应类的指定public构造器
  • Constructor<?>[] getConstructors() 返回此Class对象对应类的所有public构造器
  • Constructor getDeclaredConstructor(Class<?>... parameterTypes) 返回此Class对象对应类的指定构造器,与构造器的访问权限无关
  • Constructor<?>[] getDeclaredConstructors() 返回此Class对象对应类的所有构造器,与构造器的访问权限无关

获取属性

  • Field getField(String name) 返回此Class对象对应类的指定public Field
  • Field[] getFields() 返回此Class对象对应类的所有public Field
  • Field getDeclaredField(String name) 返回此Class对象对应类的指定Field,与Field的访问权限无关
  • Field[] getDeclaredFields() 返回此Class对象对应类的全部Field,与Field的访问权限无关

获取方法

  • Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回此Class对象对应类的指定方法,与方法的访问权限无关
  • Method[] getDeclaredMethods() 返回此Class对象对应类的全部方法,与方法的访问权限无关

获取Class对象对应类的修饰符、所在包、类名等基本信息

  • int getModifiers(): 返回此类或接口的所有修饰符,修饰符由public、protected、private、final、static、abstract等对应的常量组成。返回的整数应使用Modifier工具类的方法来解码,才可以获取真实的修饰符。
  • Package getPackage(): 获取此类的包
  • String getName(): 以字符串形式返回此Class对象所表示的类的名称
  • String getSimpleName(): 以字符串形式返回此Class对象所表示的类的简称

反射获取注解

  • 获取类/属性/方法的全部注解对象: Annotation[] annotations01 = Class/Field/Method.getAnnotations();
  • 根据类型获取类/属性/方法的注解对象: 注解类型 对象名 = (注解类型) c.getAnnotation(注解类型.class);

String

String, Stringbuilder, Stringbuffer区别

  1. 可变与不可变。String类中使用字符数组保存字符串,因为有“final”修饰符,所以string对象是不可变的。对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.

String类利用了final修饰的char类型数组存储字符,源码如下:

private final char value[];

StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,这两种对象都是可变的。源码如下:

char[] value;
  1. 是否多线程安全。
    String中的对象是不可变的,也就可以理解为常量,显然线程安全。
    StringBuilder是非线程安全的。
    StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
  2. 性能
    每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

集合

HashMap原理(底层数据结构)

在JDK1.7 和JDK1.8 中有所差别:在JDK1.7 中,由“数组+链表”组成。数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

  • 当链表超过 8 且数据总量超过 64 才会转红黑树。
  • 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

HashMap 存数据过程

  1. 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
  2. 如果数组是空的,则调用 resize 进行初始化;
  3. 如果没有哈希冲突直接放在对应的数组下标里;
  4. 如果冲突了,且 key 已经存在,就覆盖掉 value;
  5. 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
  6. 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。

CurrentHashMap原理

JDK1.7中的ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即ConcurrentHashMap 把哈希桶切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
其中,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色;HashEntry 用于存储键值对数据。
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。

再来看下JDK1.8
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加低粒度的锁。
将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。

CurrentHashMap存数据的过程

先来看JDK1.7
首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁。
获取到锁后:
将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
释放 Segment 的锁。

再来看JDK1.8
大致可以分为以下步骤:

  1. 根据 key 计算出 hash值。
  2. 判断是否需要进行初始化。
  3. 定位到 Node,拿到首节点 f,判断首节点 f:
    如果为 null ,则通过cas的方式尝试添加。
    如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容。
    如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入。
  4. 当在链表长度达到8的时候,数组扩容或者将链表转换为红黑树。

JVM

如何选择合适的收集算法?

  • 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。

新生代怎么转化成老年代?

  • Eden区满时,进行Minor GC:当Eden和一个Survivor区中依然存活的对象无法放入到Survivor中,则通过分配担保机制提前转移到老年代中
  • 对象体积太大, 新生代无法容纳:-XX:PretenureSizeThreshold参数意思是如果对象的大小超过这个值的时候,对象直接在old区分配内存,默认值是0,即不管多大都是先在eden中分配内存。此参数只对Serial及ParNew两款收集器有效。
  • Long-lived objects will enter the old age(长期存活的对象将进入老年代):虚拟机对每个对象定义了一个对象年龄(Age)计数器。当年龄增加到一定的临界值时,就会晋升到老年代中,该临界值由参数:-XX:MaxTenuringThreshold来设置。如果对象在Eden出生并在第一次发生MinorGC时仍然存活,并且能够被Survivor中所容纳的话,则该对象会被移动到Survivor中,并且设Age=1;以后每经历一次Minor GC,该对象还存活的话Age=Age+1。
  • 动态对象年龄判定:虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

JVM 中确定垃圾的几种算法?

  1. 引用计数法:给对象中添加⼀个引用计数法,每当有⼀个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的
  2. 可达性分析算法:VM默认使用可达性分析算法。这个算法的基本思想就是通过⼀系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当⼀个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

常用的垃圾回收算法?

  1. 标记-清除算法 (Mark-Sweep):算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
  2. 复制算法 (Coping):它可以将内存分为大小相同的两块,每次使用其中的⼀块。当这⼀块的内存使用完后,就将还存活的对象复制到另⼀块去,然后再把使用的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进行回收。
  3. 标记-整理算法 (Mark-Compact):据老年代的特点特出的⼀种标记算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。
  4. 分代收集算法 (Generational Collection):当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。⼀般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

介绍一下垃圾回收器?

  1. Serial收集器 (Serial Collector):Serial 是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不仅只会使用一个 CPU 或一条线程去完成垃圾收集工作,而且在进行垃圾收集的同时,必须暂停其他所有的工作线程( “Stop The World”),直到垃圾收集结束。
  2. ParNew 收集器(ParNew Collector):ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进⾏垃圾收集外,其余行为(控制、参数、收集算法、回收策略等等)和Serial收集器完全⼀样。它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
  3. Parallel Scavenge收集器(Parallel Scavenge):Parallel Scavenge 收集器类似于ParNew 收集器。Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器。它重点关注的是程序达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
  4. Serial Old 收集器(Serial Old Collector):Serial Old 是 Serial 垃圾收集器的老年代版本,它同样是个单线程的收集器,使用标记-整理算法。这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的老年代垃圾收集器。
  5. Parallel Old 收集器(Parallel Old Collector ):Parallel Old 收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。在 JDK1.6 之前,新生代使用 Parallel Scavenge 收集器只能搭配老年代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和老年代 Parallel Old 收集器的搭配策略。
  6. CMS 收集器(Concurrent Mark Sweep Collector):CMS收集器是一种老年代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,从而为交互比较高的程序提高用户体验。和其他老年代使用标记-整理算法不同,它使用多线程的标记-清除算法
  7. G1 收集器(Garbage First Collector):G1通过把Java堆分成大小相等的多个独立区域,回收时计算出每个区域回收所获得的空间以及所需时间的经验值,根据记录两个值来判断哪个区域最具有回收价值,所以叫Garbage First(垃圾优先)。

什么是双亲委派模型?为什么要使用双亲委派?

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。

Java 中的锁

什么是 CAS?

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
CAS 算法涉及到三个操作:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 要写入的新值

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作

介绍一下锁的升级?

锁状态存在四种分别是: 无状态, 偏向锁, 轻量级锁和重量级锁。 同时锁有一个特性只会升级不会降级。

  1. 首先对象处于无锁状态,接着thread-1访问对象的时候,通过cas操作去获取偏向锁并将锁的偏向位更改为1;(对象会记录下偏向线程thread-1的 id),此时线程1转为偏向锁
  2. 当另外一个thread-2到达的时候会比较自身线程id和对象头中的id是否一致,发现不一致就会去检测对象头中的线程thread-1是否存活, 如果thread-1还是存活的就升级为轻量级锁
  3. 如果thread-2获取失败则说明存在竞争关系,这时将偏向锁升级为轻量级锁;升级为轻量级锁之后会在thread-2线程的栈帧中开辟一块锁记录空间叫做displaced Mark Word,并将锁对象的markword拷贝到线程本身的displaced Mark Word空间中,然后通过cas的方式去设置锁对像中线程id指针,并将锁的标志设置为00;
  4. 当其中一个线程的自旋次数超过阈值(默认是10)的时候为了防止cpu空转,会将自旋锁升级为重量级锁,将对象监视器的指针存储在对象头之中。

synchronized 和 volatile 的区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
  4. synchronized保证了原子性、可见性和有序性;而volatile只保证了可见性和有序性,没有保证原子性。
  5. volatile标记的变量(禁止指令重排序)不会被编译器优化;synchronized标记的变量可以被编译器优化

多线程

线程的实现方式

  • 1.继承Thread类
  • 2.实现Runnable的接口
  • 3.使用Executorservice、callable、Future实现返回结果的多线程

runnable和callable的区别?

相同点:都是接口,都可以编写多线程程序,都采用Thread.Start()启动线程。
不同点:Runnable接口run方法无返回值,Callable接口Call方法有返回值,是个泛型,和Future、FutureTask配合来获取异步执行的结果。Runnable接口run方法只能抛出运行时的异常,并且无法捕获处理;Callable接口的call方法允许抛出异常,可以获取异常信息。Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用就不会阻塞。"

线程中wait()和sleep()方法有什么区别?

sleep()是Thread线程类的静态方法,wait()是object类的方法。
sleep()不释放锁;wait()释放锁。
wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。Sleep()方法执行完成后,线程会自动苏醒。或者可以调用wait(long timeOut)超时后线程或自动苏醒

启动线程方法start()和run()有什么区别?

只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码。

线程池是如何工作的?

当一个任务被提交线程池之后,如果此时的线程数小于核心线程数那么会新起一个线程来执行当前的任务,如果线程数大于核心线程数那么会将任务塞入阻塞队列中等待被执行。
如果阻塞队列满了并且此时的线程数小于最大线程数会创建新的线程来执行当前任务,如果阻塞队列满了并且线程数大于最大线程数就会执行拒绝策略。

线程池的几个参数介绍,常用队列以及线程池中拒绝策略?

  • corePoolSize 线程池核心线程大小
  • maximumPoolSize 线程池最大线程数量
  • keepAliveTime 空闲线程存活时间
  • unit 空闲线程存活时间单位
  • workQueue 工作队列
    • ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序,即先进先出原则。
    • LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。
    • SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略
    • PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级通过参数Comparator实现
  • threadFactory 线程工厂
  • handler 拒绝策略
    • CallerRunsPolicy:该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务
    • AbortPolicy:该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
    • DiscardPolicy:该策略下,直接丢弃任务,什么都不做。
    • DiscardOldestPolicy:该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

创建线程池的几种方式

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

什么是死锁,如何避免死锁

死锁是指2个或2个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而照成的一种堵塞现象。 死锁就是2个线程要相互等待对方释放对象锁。

产生原因,如何避免死锁?

  • 1:互斥条件。 针对资源来说,资源在任意的时刻都应该由一个线程占用。
  • 2:请求保持条件。 一个进程因请求资源而阻塞时,对已经获得的资源保持不放(破坏:一次申请所有资源)
  • 3:不剥夺条件。 进程已获得的资源,在未使用完之前,不能进行强行剥夺。
  • 4:循环等待条件。很多进程之间形成一种首尾相接的循环等待。

只要其中的一个条件不成立的话, 就不会产生死锁。
互斥条件是无法破坏的。
比如:将系统中的所有资源标识设置标志位、排序,规定所有的线程申请资源必须按照一定的顺序来进行操作, 去避免循环等待。
破坏不剥夺条件: 占用部分资源的线程如果在去申请其他资源的时候,如果申请不到,可以主动师傅它只有的资源 。

Stream 流

Stream注意点?

  1. Stream自己不会存储元素。
  2. Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream
  3. Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
  4. 一旦调用了终结方法,流将不能再被使用,一旦调用非终结方法,原来旧的流不能再被使用了,只能用新的流进行操作。终结方法:如果返回值不为Stream流,如foreach,count方法;非终结方法:如果返回值为stream流,如filter,limit,skip,map,cancat等方法。

Stream并行流?并行流的原理

并行流就是把一个内容分成多个数据块,并用不同的线程分成多个数据块,并用不同的线程分别处理每个数据块的流。
采用了Fock/Join框架来进行对数据的并行处理,简单说就是将一个大任务进行拆分(Fock)成若干个小任务,再将一个个小任务运算的记过进行Join汇总。

创建Stream的方式

  1. 通过Collection系列集合提供的stream()方法或者paralleStream()方法来创建Stream
  2. 通过Arrays中的静态方法stream()获取数组流
  3. 通过Stream类的静态方法of()获取数组流
  4. 通过使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流

Stream的常用方法?

过滤:
filter:按条件过滤集合中的数据
distinct: distinct操作类似于我们在写SQL语句时,添加的DISTINCT关键字,用于去重处理,distinct基于Object.equals(Object)实现
limit:类似于SQL语句中的LIMIT关键字,不过相对功能较弱,limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度。
sorted:该操作用于对流中元素进行排序,sorted要求待比较的元素必须实现Comparable接口,如果没有实现也不要紧,我们可以将比较器作为参数传递给sorted(Comparator<? super T> comparator)。
skip:skip操作与limit操作相反,如同其字面意思一样,是跳过前n个元素
映射:
map
flatMap
查找:
allMatch: 用于检测是否全部都满足指定的参数行为,如果全部满足则返回true
anyMatch: 则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true
noneMathch: 用于检测是否不存在满足指定行为的元素,如果不存在则返回true
findFirst: 用于返回满足条件的第一个元素
findAny: 相对于findFirst的区别在于,findAny不一定返回第一个,而是返回任意一个

stream 的分组操作:
1、Collectors.groupingBy来操作集合
Map<String, List> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool));
2、多级分组:
Map<String, Map<String, List>> groups2 = students.stream().collect(
Collectors.groupingBy(Student::getSchool, // 一级分组,按学校
Collectors.groupingBy(Student::getMajor))); // 二级分组,按专业
3、Collector.counting,分组后统计个数:

Spring

Spring 包含的模块有哪些?

Core Container

Spring 框架的核心模块,也可以说是基础模块,主要提供 IoC 依赖注入功能的支持。Spring 其他所有的功能基本都需要依赖于该模块,我们从上面那张 Spring 各个模块的依赖关系图就可以看出来。

  • spring-core :Spring 框架基本的核心工具类。

    spring-beans :提供对 bean 的创建、配置和管理等功能的支持。

    spring-context :提供对国际化、事件传播、资源加载等功能的支持。

    spring-expression :提供对表达式语言(Spring Expression Language) SpEL 的支持,只依赖于 core 模块,不依赖于其他模块,可以单独使用。

AOP

  • spring-aspects :该模块为与 AspectJ 的集成提供支持。

  • spring-aop :提供了面向切面的编程实现。

  • spring-instrument :提供了为 JVM 添加代理(agent)的功能。 具体来讲,它为 Tomcat 提供了一个织入代理,能够为 Tomcat 传递类文 件,就像这些文件是被类加载器加载的一样。没有理解也没关系,这个模块的使用场景非常有限

Data Access/Integration

  • spring-jdbc :提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。
  • spring-tx :提供对事务的支持。
  • spring-orm : 提供对 Hibernate、JPA 、iBatis 等 ORM 框架的支持。
  • spring-oxm :提供一个抽象层支撑 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。
  • spring-jms : 消息服务。自 Spring Framework 4.1 以后,它还提供了对 spring-messaging 模块的继承。

Spring Web

  • spring-web :对 Web 功能的实现提供一些最基础的支持。
  • spring-webmvc : 提供对 Spring MVC 的实现。
  • spring-websocket : 提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
  • spring-webflux :提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。
    Messaging
  • spring-messaging 是从 Spring4.0 开始新加入的一个模块,主要职责是为 Spring 框架集成一些基础的报文传送应用

Spring Test
Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。
Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。

Spring,Spring MVC,Spring Boot 之间什么关系?

Spring 包含了多个功能模块(上面刚刚提到过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。

Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。

使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!

Spring 旨在简化 J2EE 企业应用程序开发。SpringBoot 旨在简化 Spring 开发(减少配置文件,开箱即用!)

Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!

描述一下spring mvc的工作原理,前端请求到后端到返回数据,中间都经历了什么?

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

Springboot启动流程

SpringBoot应用程序的启动流程主要包括初始化SpringApplication和运行SpringApplication两个过程。1.初始化SpringApplication:配置基本的环境变量、资源、构造器和监听器,为运行SpringApplciation实例对象作准备;2.SpringApplication.run():SpringApplicationRunListeners 引用启动监控模块、ConfigrableEnvironment配置环境模块和监听及ConfigrableApplicationContext配置应用上下文。当完成刷新应用的上下文和调用SpringApplicationRunListener#contextPrepared方法后表示SpringBoot应用程序已经启动完成。

Spring 中 Bean的作用域

  1. singleton : bean在每个Spring ioc 容器中只有一个实例
  2. prototype :一个bean的定义可以有多个实例
  3. request :每次http请求都会创建一个bean
  4. session :在一个HTTP Session中,一个bean定义对应一个实例
  5. application/global-session :在一个全局的HTTP Session中,一个bean定义对应一个实例

将一个类声明为 Bean 的注解有哪些?

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。

Spring中注解注入常用的注解有哪些?区别是什么?

常用的注解有@Autowired、@Qualifier、@Resource和@Primary

  • @Autowired:默认是按照 byType 进行装配注入,默认情况下,它要求依赖对象必须存在,如果允许 null 值,可以设置它 required 为false,可用于构造函数、成员变量、setterr方法;
  • @Qualifier:按照 byName 来装配注入的,需要结合@Autowired使用
  • @Resource:默认是按照 byName 来装配注入的,有两个比较重要且日常开发常用的属性:name(名称)、type(类型),如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定nametype属性(不建议这么做)则注入方式为byType+byName,只有当找不到与名称匹配的bean才会按照类型来注入,可用于成员变量、setterr方法;
  • @Primary:当同一个接口有不同的实现类,在声明bean的时候,可以使用@Primary将其中一个可选的bean设置为首选;不可作用在类属性上,可作用于方法、类上

@Qualifier@Primary 注释都存在,那么 @Qualifier 注释将具有优先权。因为 @Primary 是定义了默认值,而 @Qualifier 则非常具体。

Spring中bean的生命周期

  1. Spring中的bean的生命周期主要包含四个阶段:实例化Bean --> Bean属性填充 --> 初始化Bean -->销毁Bean.

  2. 首先是实例化Bean,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚末初始化的依赖时,容器就会调用doCreateBean()方法进行实例化,实际上就是通过反射的方式创建出一个bean对象.

  3. Bean实例创建出来后,接着就是给这个Bean对象进行属性填充,也就是注入这个Bean依赖的其它bean对象

  4. 属性填充完成后,进行初始化Bean操作,初始化阶段又可以分为几个步骤

    • 执行Aware接口的方法
      Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们 拿到Spring容器的些资源。如实现BeanNameAware接口可以获取到BeanName,实现 BeanFactoryAware接口可以获取到工厂对象BeanFactory等

    • 执行BeanPostProcessor的前置处理方法postProcessBeforelnitialization(),对Bean进行一些自定义的前置处理

    • 判断Bean是否实现了InitializingBean接口,如果实现了,将会执行lnitializingBean的afeterPropertiesSet()初始化方法;

    • 执行用户自定义的初始化方法,如init-method等;

    • 执行BeanPostProcessor的后置处理方法postProcessAfterinitialization();

  5. 初始化完成后,Bean就成功创建了,之后就可以使用这个Bean, 当Bean不再需要时,会进行销毁操作

Spring 的循环依赖?怎么解决?

  1. 首先 A 完成初始化第一步并将自己提前曝光出来(通过 ObjectFactory 将自己提前曝光),在 初始化的时候,发现自己依赖对象 B,此时就会去尝试 get(B),这个时候发现 B 还没有被创建 出来;
  2. 然后 B 就走创建流程,在 B 初始化的时候,同样发现自己依赖 C,C 也没有被创建出来;
  3. 这个时候 C 又开始初始化进程,但是在初始化的过程中发现自己依赖 A,于 是尝试 get(A)。这 个时候由于 A 已经添加至缓存中(一般都是添加至三 级缓存 singletonFactories),通过 ObjectFactory 提前曝光,所以可以 通过 ObjectFactory#getObject() 方法来拿到 A 对象。C 拿 到 A 对象后 顺利完成初始化,然后将自己添加到一级缓存中;
  4. 回到 B,B 也可以拿到 C 对象,完成初始化,A 可以顺利拿到 B 完成初始 化。到这里整个链路 就已经完成了初始化过程了。

spring没有解决的循环依赖我们该如何处理

  • 生成代理对象产生的循环依赖
    1、使用@Lazy注解,延迟加载
    2、使用@DependsOn注解,指定加载先后关系
    3、修改文件名称,改变循环依赖类的加载顺序
  • 使用@DependsOn产生的循环依赖
    要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖
  • 多例循环依赖
    可以通过把bean改成单例的解决
  • 构造器循环依赖
    可以通过使用@Lazy注解解决

Spring中bean是什么时候被实例化的?

Spring在实例化bean的时候,需要区分以下两种情形:

  1. 第一:如果使用BeanFactory作为Spring Bean的工厂类,则所有的bean都是在第一次使用该Bean的时候实例化;
  2. 第二:如果使用ApplicationContext作为Spring Bean的工厂类,则分为以下几种情况
    • 如果bean的scope是singleton的,并且lazy-init为false,则ApplicationContext启动的时候就实例化该Bean,并且将实例化的Bean放在一个map结构的缓存中,下次再使用该Bean的时候,直接从这个缓存中取;
    • 如果bean的scope是singleton的,并且lazy-init为true,则该Bean的实例化是在第一次使用该Bean的时候进行实例化;
    • 如果bean的scope是prototype的,则该Bean的实例化是在第一次使用该Bean的时候进行实例化;

Spring bean怎么做到自动装配的?

@SpringBootApplication是核心注解可以看做是@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan的三种注解的集合。
@SpringBootConfiguration的底层是Configuration注解,允许在上下文中注册额外的 bean 或导入其他配置类,@ComponentScan扫描被@Component(@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类,可以自定义不扫描某些bean。
@EnableAutoConfiguration是实现自动装配的核心注解。
@EnableAutoConfiguration是实现自动装配的核心注解,通过AutoConfigurationImportSelector 中的getAutoConfigurationEntry方法实现将符合条件的类加载到ioc容器中,步骤如下:第一判断自动装配开关是否打开,第二:获取获取EnableAutoConfiguration注解中的 exclude 和 excludeName,第三获取需要自动装配的所有配置类,读取META-INF/spring.factories.

Spring事务失效的原因?

在项目中我们通常用 @Transactiona 注解来配置事务的行为但是在以下情况下事务会失效

  1. 该方法注释的方法不是public方法.
  2. 方法中,异常没有抛出,捕获异常的话事务也是不生效的
  3. 发生了内部的调用,就是一个类中的方法中,调用了该类自己的其他方法,而没有经过Spring的代理类,这样事务也是失效的。默认只有在外部调用事务才会生效。(解决办法之一就是在类中注入自己,用注入的对象再调用另外当自己的方法,但是不太优雅)
  4. 数据库引擎不支持事务(比如:MySQL的表采用了MyISM的引擎)
  5. 类没有交由Spring进行管理,这样即使加了@Transactional 注解,事务也是不生效的
  6. 数据源没有配置事务管理器

Spring 中事务的传播机制

事务的传播,是指一个方法调用另一个方法并将事务传递给它。事务的转播机制主要针对被调用者而言,控制它是否被传播或者被怎样传播。Spring 事务的传播机制有七种:

传播行为 描述
PROPAGATION_REQUIRED required:默认的Spring事物传播级别,若当前存在事务,则加入该事务,若不存在事务,则新建一个事务
PROPAGATION_REQUIRE_NEW require_new:若当前没有事务,则新建一个事务。若当前存在事务,则新建 一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交
PROPAGATION_NESTED nested:如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务, 则新建一个事务,类似于REQUIRE_NEW
PROPAGATION_SUPPORTS supports:支持当前事务,若当前不存在事务,以非事务的方式执行
PROPAGATION_NOT_SUPPORTED not_supported:以非事务的方式执行,若当前存在事务,则把当前事务挂起
PROPAGATION_MANDATORY mandatory:强制事务执行,若当前不存在事务,则抛出异常
PROPAGATION_NEVER never:以非事务的方式执行,如果当前存在事务,则抛出异常

IoC

什么是 IoC

IoC (Inversion of control )控制反转/反转控制。它是一种思想不是一个技术实现。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

  • **谁控制谁,控制什么:**传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
  • **为何是反转,哪些方面反转了:**有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

用图例说明一下,传统程序设计如图2-1,都是主动去创建相关对象然后再组合起来:

用图例说明一下,传统程序设计如图2-1,都是主动去创建相关对象然后再组合起来:

IoC 解决了什么问题?

IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?

  1. 对象之间的耦合度或者说依赖程度降低;
  2. 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。

IoC 和 DI 别再傻傻分不清楚?

控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想,DI是实现方式

DI—Dependency Injection,即“依赖注入”组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。**依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。**通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

我们来深入分析一下:

  • 谁依赖于谁

当然是应用程序依赖于IoC容器;

  • 为什么需要依赖

应用程序需要IoC容器来提供对象需要的外部资源;

  • 谁注入谁

很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;

  • 注入了什么

就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

  • IoC和DI有什么关系呢

其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。通俗来说就是IoC是设计思想,DI是实现方式

AOP

什么是 AOP?

AOP:Aspect oriented programming 面向切面编程,AOP 是 OOP(面向对象编程)的一种延续。

下面我们先看一个 OOP 的例子。1

例如:现有三个类,HorsePigDog,这三个类中都有 eat 和 run 两个方法。

通过 OOP 思想中的继承,我们可以提取出一个 Animal 的父类,然后将 eat 和 run 方法放入父类中,HorsePigDog通过继承Animal类即可自动获得 eat()run() 方法。这样将会少些很多重复的代码。

OOP 编程思想可以解决大部分的代码重复问题。但是有一些问题是处理不了的。比如在父类 Animal 中的多个方法的相同位置出现了重复的代码,OOP 就解决不了。

/*** 动物父类*/
public class Animal {/** 身高 */private String height;/** 体重 */private double weight;public void eat() {// 性能监控代码long start = System.currentTimeMillis();// 业务逻辑代码System.out.println("I can eat...");// 性能监控代码System.out.println("执行时长:" + (System.currentTimeMillis() - start)/1000f + "s");}public void run() {// 性能监控代码long start = System.currentTimeMillis();// 业务逻辑代码System.out.println("I can run...");// 性能监控代码System.out.println("执行时长:" + (System.currentTimeMillis() - start)/1000f + "s");}
}

这部分重复的代码,一般统称为 横切逻辑代码

横切逻辑代码存在的问题:

  • 代码重复问题
  • 横切逻辑代码和业务代码混杂在一起,代码臃肿,不变维护

AOP 就是用来解决这些问题的
AOP 另辟蹊径,提出横向抽取机制,将横切逻辑代码和业务逻辑代码分离

代码拆分比较容易,难的是如何在不改变原有业务逻辑的情况下,悄无声息的将横向逻辑代码应用到原有的业务逻辑中,达到和原来一样的效果。

AOP 解决了什么问题?

通过上面的分析可以发现,AOP 主要用来解决:在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。

AOP 为什么叫面向切面编程?

:指的是横切逻辑,原有业务逻辑代码不动,只能操作横切逻辑代码,所以面向横切逻辑

:横切逻辑代码往往要影响的是很多个方法,每个方法如同一个点,多个点构成一个面。这里有一个面的概念

AOP 的一些使用场景?

  1. 记录日志(调用方法后记录日志)
  2. 监控性能(统计方法运行时间)
  3. 权限控制(调用方法前校验是否有权限)
  4. 事务管理(调用方法前开启事务,调用方法后提交关闭事务 )
  5. 缓存优化(第一次调用查询数据库,将查询结果放入内存对象, 第二次调用,直接从内存对象返回,不需要查询数据库 )

Redis

Redis 的几种数据类型

首先对redis来说,所有的key(键)都是字符串,主要包括常见的5种数据类型,分别是:String、List、Set、Zset、Hash

结构类型 结构存储的值 结构的读写能力 实战场景
String字符串 可以是字符串、整数或浮点数 对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作; 1、缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
2、计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
3、session:常见方案spring session + redis实现session共享,# List列表
List列表 一个链表,链表上的每个节点都包含一个字符串 对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或删除元素; 1、微博TimeLine: 有人发布微博,用lpush加入时间轴,展示新的列表信息。
2、消息队列
Set集合 包含字符串的无序集合 字符串的集合,包含基础的方法有看是否存在添加、获取、删除;还包含计算交集、并集、差集等 1、标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人
2、点赞,或点踩,收藏等,可以放到set中实现
Hash散列 包含键值对的无序散列表 包含方法有添加、获取、删除单个元素 缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等
Zset有序集合 和散列一样,用于存储键值对 字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定;包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素 排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行

Redis 持久化

为什么需要持久化

Redis 是个基于内存的数据库。那服务一旦宕机,内存中的数据将全部丢失。通常的解决方案是从后端数据库恢复这些数据,但后端数据库有性能瓶颈,如果是大数据量的恢复,1、会对数据库带来巨大的压力,2、数据库的性能不如 Redis。导致程序响应慢。所以对 Redis 来说,实现数据的持久化,避免从后端数据库中恢复数据,是至关重要的

RDB 持久化

RDB 就是 Redis DataBase 的缩写,中文名为快照/内存快照,RDB持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值。

RDB 持久化触发方式?

手动触发

save 命令:阻塞当前 Redis 服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用

bgsave 命令:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子 进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短

bgsave 具体流程如下:

  • redis客户端执行bgsave命令或者自动触发bgsave命令;
  • 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
  • 如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作;
  • 子进程先将数据写入到临时的rdb文件中,待快照数据写入完成后再原子替换旧的rdb文件;
  • 同时发送信号给主进程,通知主进程rdb持久化完成,主进程更新相关的统计信息(info Persitence下的rdb_*相关选项)。
自动触发

有以下几种情况会自动触发:

  1. redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;

  2. 主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;

  3. 执行debug reload命令重新加载redis时也会触发bgsave操作;

  4. 默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作

redis.conf中配置RDB

# 默认的设置为:
save 900 1  # 如果900秒内有1条Key信息发生变化,则进行快照;
save 300 10 # 如果300秒内有10条Key信息发生变化,则进行快照;
save 60 10000 # 如果60秒内有10000条Key信息发生变化,则进行快照。读者可以按照这个规则,根据自己的实际请求压力进行设置调整。

其他相关配置:

# 文件名称
dbfilename dump.rdb# 文件保存路径
dir /home/work/app/redis/data/# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes# 是否压缩
rdbcompression yes# 导入时是否检查
rdbchecksum yes

RDB 的优缺点?

  • 优点

    • RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;
    • Redis加载RDB文件恢复数据要远远快于AOF方式;
  • 缺点
    • RDB方式实时性不够,无法做到秒级的持久化
    • 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;
    • RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;
    • 版本兼容RDB文件问题;

针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决

AOF 持久化

Redis是“写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。PS: 大多数的数据库采用的是写前日志(WAL),例如MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性。而AOF日志采用写后日志,即先写内存,后写日志

为什么采用写后日志

Redis要求高性能,采用写日志有两方面好处:

  • 避免额外的检查开销:Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
  • 不会阻塞当前的写操作

但这种方式存在潜在风险:

  • 如果命令执行完成,写日志之前宕机了,会丢失数据。
  • 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。

如何实现 AOF

AOF日志记录Redis的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。

  • 命令追加 当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
  • 文件写入和同步 关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:

redis.conf中配置AOF

默认情况下,Redis是没有开启AOF的,可以通过配置redis.conf文件来开启AOF持久化,关于AOF的配置如下:

# appendonly参数开启AOF持久化
appendonly no# AOF持久化的文件名,默认是appendonly.aof
appendfilename "appendonly.aof"# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的
dir ./# 同步策略
# appendfsync always
appendfsync everysec
# appendfsync no# aof重写期间是否同步
no-appendfsync-on-rewrite no# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb# 加载aof出错如何处理
aof-load-truncated yes# 文件重写策略
aof-rewrite-incremental-fsync yes

Redis 是如何从持久化中恢复数据?

其实想要从这些文件中恢复数据,只需要重新启动Redis即可。执行流程:

  1. redis重启时判断是否开启aof,如果开启了aof,那么就优先加载aof文件;
  2. 如果aof存在,那么就去加载aof文件,加载成功的话redis重启成功,如果aof文件加载失败,那么会打印日志表示启动失败,此时可以去修复aof文件后重新启动;
  3. 若aof文件不存在,那么redis就会转而去加载rdb文件,如果rdb文件不存在,redis直接启动成功;
  4. 如果rdb文件存在就会去加载rdb文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么redis重启成功,且使用rdb文件恢复数据;

Redis 事务详解

为什么单线程的 Redis 能那么快?

Redis的瓶颈主要在IO而不是CPU,所以为了省开发量,在6.0版本前是单线程模型;其次,Redis 是单线程主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。(但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的)。

Redis 采用了多路复用机制使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。

Redis高可用的方案?

  1. 主从复制
  2. 哨兵模式
  3. 集群

主从复制原理以及优缺点

主从复制原理:
主节点(master)负责读写,从节点(slave)负责读。这个系统的运行依靠三个主要的机制:
a. 当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串的命令流来保持对 slave 的更新,以便于将自身数据集的改变复制给 slave ,包括客户端的写入、key 的过期或被逐出等等。
b. 当 master 和 slave 之间的连接断开之后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流。
c. 当无法进行部分重同步时, slave 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave 。

主从复制优缺点:
优点:
a. 高可靠性,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行。另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题
b. 读写分离策略,从节点可以扩展主库节点的读能力,能有效应对大并发量的读操作。
弊端:
a. 故障恢复复杂,主节点挂了之后,需要手动将一个从节点晋升为主节点,同事需要通知业务方变更配置,并且需要让其他从节点去复制新的主节点,整个过程需要人为干预,比较繁琐。
b. 主库的写能力受到单机的限制,可以考虑分片
c. 主库的存储能力受到单机的限制,可以考虑Pika
d. 丛节点复制数据有弊端。

哨兵模式原理以及优缺点

哨兵模式:
原理:Redis Sentinel是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel集群和Redis数据集群。
其中Redis Sentinel集群是由若干Sentinel节点组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。
Redis Sentinel的节点数量要满足2n+1(n>=1)的奇数个

哨兵模式优缺点:
优点:
a. redis sentinel 集群部署简单
b. 能够解决主从模式下的高可用切换问题
c. 很方便实现Redis数据节点的线性扩展,轻松突破Redis自身单线程瓶颈,可极大满足Redis大容量或高性能的业务需求
d. 可以实现一套Sentinel监控一组Redis数据节点或多组数据节点

缺点:
a. 资源浪费,Redis数据节点中slave节点作为备份节点不提供服务;
b. Redis Sentinel主要是针对Redis数据节点中的主节点的高可用切换,对Redis的数据节点做失败判定分为主观下线和客观下线两种,对于Redis的从节点有对节点做主观下线操作,并不执行故障转移。
c. 不能解决读写分离问题,实现起来相对复杂。

集群模式原理、分片算法、优缺点?

集群的原理:
Redis Cluster是社区版推出的Redis分布式集群解决方案,主要解决Redis分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster能起到很好的负载均衡的目的。
Redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。

集群模式下多个master节点,是怎么解决数据分片问题的? 希望数据平均分配的话,用的什么算法?
1、 哈希取模算法:
当有n个节点得时候,此时这多个节点都是正常得,这些节点都是固定有序得。
当存储一个数据,会对这个数据得key获取哈希值然后进行取模操作,取模得到得值肯定不会大于节点数量得。
通过得到的值去操作对应的节点。
如果此时,有一个节点挂机了,等于之前节点的顺序改变了。除了失去了这个节点上面得数据外,
若此时对key进行哈希取模,就会发现得到的值很有可能找不到对应的节点去拿了,
这就可能丢掉得不只是一个节点得数据。
2、 一致性哈希算法
一致性哈希的原理: 把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织。
因为是环形空间,0 和 2^32-1 是重叠的。 假设我们有四台机器要哈希环来实现映射(分布数据),
我们先根据机器的名称或者 IP 计算哈希值,然后分布到哈希环中(红色圆圈)。

集群的优缺点
优点:
a. 无中心架构;
b. 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布;
c. 可扩展性:可线性扩展到1000多个节点,节点可动态添加或删除;
d. 高可用性:部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升;
e. 降低运维成本,提高系统的扩展性和可用性

缺点:
a. 数据通过异步复制,不保证数据的强一致性。
b. 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。
c. 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
d. 不支持多数据库空间,单机下的redis可以支持到16个数据库,集群模式下只能使用1个数据库空间,即db0。

缓存穿透、缓存雪崩?如何避免此类问题

缓存穿透,缓存雪崩问题,如何避免?
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层
缓存雪崩就是缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求

缓存穿透解决方案:

  1. 最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。
  2. 缓存无效额的key: 缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key
  3. 布隆过滤器: 把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

缓存雪崩解决方案:

  1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  2. 限流,避免同时处理大量的请求。
  3. 设置不同的失效时间比如随机设置缓存的失效时间。
  4. 对于一些热点,不宜改动的数据直接设置缓存永不失效。

缓存的淘汰策略以及Redis中的淘汰策略

缓存淘汰策略
LRU(Least Recently Used):淘汰最近最少使用的key。在缓存写满的时候,会根据所有数据的访问记录,淘汰掉未来被访问几率最低的数据
LFU(Least Frequently Used):优先淘汰最不常用的、使用最少的key,LFU的侧重点是缓存的使用频率,系统有大量热点缓存数据可能更适合
FIFO(First In First Out): 先进先出。即先缓存进来的数据会优先被淘汰。
RANDOM: 随机。随机淘汰,适用于缓存数据被访问的概率差不多时

Redis的淘汰策略? 主要分三类:

  • 不淘汰

    • noeviction:禁止淘汰数据,写入操作报错。这是 Redis 默认的内存淘汰策略
  • 对设置了过期时间的数据中进行淘汰
    • volatile-random:从设置了过期时间的 key 中,随机选出数据进行淘汰;
    • volatile-ttl:从设置了过期时间的 key 中,选出即将过期的数据(按照过期时间的先后,选出最先过期的数据)进行淘汰;
    • volatile-lru:从设置了过期时间的 key 中使用 LRU 算法,选出最近使用最少的数据进行淘汰;
    • volatile-lfu:从设置了过期时间的 key 中使用 LFU 算法,选出使用频率最低的数据进行淘汰;
  • 全部数据进行淘汰
    • allkeys-lru:从所有 key 中使用 LRU 算法,选出最近使用最少的数据进行淘汰;
    • allkeys-lfu:从所有 key 中使用 LFU 算法,选出使用频率最低的数据,进行淘汰;
    • allkeys-random:从所有的 key 中,随机选出数据进行淘汰;

MySQL数据库

事务的四大特性

  1. 原子性:事务包含的所有数据库操作要么全部成功,要不全部失败回滚
  2. 一致性:一个事务执行之前和执行之后都必须处于一致性状态
  3. 隔离性:一个事务未提交的业务结果是否对于其它事务可见(常见的事务隔离级别如下所示)
  4. 持久性:一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作

事务的隔离级别

  • Read uncommitted(读未提交):如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务B读取到了事务A未提交的数据。解决了更新丢失,但还是可能会出现脏读

  • Read committed(读提交):如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。 解决了更新丢失和脏读问题

  • Repeatable read(可重复读取):可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过“共享读镜”和“排他写锁”实现。解决了更新丢失、脏读、不可重复读、但是还会出现幻读

  • Serializable(可序化):提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过“行级锁”是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读。 解决了更新丢失、脏读、不可重复读、幻读(虚读)

分别解决的问题

隔离级别 脏读 不可重复读 幻读
Read uncommitted(读未提交)
Read committed(读提交) X
Repeatable read(可重复读) X X
Serializable(可序化) X X X

索引失效原因

  • 隐式的类型转换,索引失效
  • 查询条件包含or,可能导致索引失效
  • like通配符可能导致索引失效
  • 查询条件不满足联合索引的最左匹配原则
  • 在索引列上使用mysql的内置函数
  • 对索引进行列运算(如,+、-、*、/),索引不生效
  • 索引字段上使用(!= 或者 < >),索引可能失效
  • 索引字段上使用is null, is not null,索引可能失效
  • 左右连接,关联的字段编码格式不一样
  • 优化器选错了索引

2022 Java 知识点总结相关推荐

  1. Java知识点总结(JavaIO-合并流类)

    Java知识点总结(JavaIO- 合并流类 ) @(Java知识点总结)[Java, JavaIO] [toc] 合并流的主要功能是将两文件的内容合并成一个文件 public class Demo1 ...

  2. 给Java新手的一些建议——Java知识点归纳(Java基础部分)

    写这篇文章的目的是想总结一下自己这么多年来使用java的一些心得体会,主要是和一些java基础知识点相关的,所以也希望能分享给刚刚入门的Java程序员和打算入Java开发这个行当的准新手们,希望可以给 ...

  3. Java知识点总结(JavaIO- System类对IO的支持与Scanner类 )

    Java知识点总结(JavaIO- System类对IO的支持与Scanner类 ) @(Java知识点总结)[Java, JavaIO] [toc] System类 public class Dem ...

  4. Java知识点总结(JDBC-封装JDBC)

    Java知识点总结(JDBC-封装JDBC) @(Java知识点总结)[Java, JDBC] 封装JDBC src目录下新建一个db.properties文件,用于封装数据库连接信息 把获取数据库连 ...

  5. Java知识点总结(Java容器-EnumSet)

    Java知识点总结(Java容器-EnumSet) @(Java知识点总结)[Java, Java容器, JavaCollection, JavaSet] EnumSet EnumSet是一个专为枚举 ...

  6. java webservice接口开发_给Java新手的一些建议----Java知识点归纳(J2EE and Web 部分)

    J2EE(Java2 Enterprise Edition) 刚出现时一般会用于开发企业内部的应用系统,特别是web应用,所以渐渐,有些人就会把J2EE和web模式画上了等号.但是其实 J2EE 里面 ...

  7. Java知识点总结(注解-内置注解)

    Java知识点总结(注解-内置注解) @(Java知识点总结)[Java, 注解] @Override 定义在java.lang.Override 中,此注释只适用于修饰方法,表示一个方法声明打算重写 ...

  8. Java知识点总结(Java容器-ArrayList)

    Java知识点总结(Java容器-ArrayList) @(Java知识点总结)[Java, Java容器, JavaCollection, JavaList] ArrayList 底层实现是数组,访 ...

  9. Java知识点总结(反射-获取类的信息)

    Java知识点总结(反射-获取类的信息) @(Java知识点总结)[Java, 反射] 应用反射的API,获取类的信息(类的名字.属性.方法.构造器等) import java.lang.reflec ...

最新文章

  1. 数据结构第一次作业——抽象数据类型
  2. mongodb 按配置文件mongodb.conf启动
  3. Linux 系统启动流程及其介绍
  4. tddebug怎么读取asm文件_如何利用 ASM 实现既有方法的增强?
  5. 使用SwingWorker的Java Swing中的多线程
  6. ‘utf-8‘ codec can‘t decode byte 0xb8 in position 0: invalid start byte
  7. 《中国人工智能学会通讯》——12.44 分类型数据的定义
  8. python电脑怎么运行_如何运行python文件
  9. 机器学习实战(一)——员工离职预测
  10. 提高工作效率必备的生产力工具
  11. ClientDisconnectionReason(客户端断开原因)_羊豆豆_新浪博客
  12. 第十章:如何制定项目目标?
  13. 编写SQL语句,检索Customers表中所有的列,再编写另外的SELECT语句,仅检索顾客的ID
  14. 大厂对ChatGPT的开发利用和评估案例收录
  15. 命名实体识别的难点与现状
  16. 【JAVA学习】1、零基础入门Java 基础语法:概念、规则、格式
  17. Python中int、str、bytes相互转化,还有2进制、16进制表示,你想要的都在这里了
  18. 当不小心更改了matlab工具箱的内置函数怎么办?以及matlab指定工具箱卸载
  19. 职业生涯成长阶段分几个阶段_开发人员职业生涯的每个阶段的最佳选择
  20. 小技巧:Excel顽固的名称、引用冲突的解决

热门文章

  1. R6-4 sdust-Java-可实现多种排序的Book类
  2. js实现用户输入年月份,判断是否为闰年,该月份有多少天
  3. 说一个头疼的问题:后端瞎返回数据导致APP崩溃,你会怎么办?
  4. IE常见问题解决方案大全
  5. 失眠睡不着觉怎么办?这些助眠好物帮助你走出失眠
  6. centos7 操作记录
  7. 联想 扬天M4000q-11-Hackintosh-Opencore 黑苹果efi引导文件
  8. 理解Aho-Corasick自动机算法
  9. mysql 数据库1
  10. CSR867x — 实现SPP数据收发