模拟面试

基础

  1. Object的方法有哪些

  2. 类加载顺序

    • 首先加载父类的静态字段或者静态语句块

    • 子类的静态字段或静态语句块

    • 父类普通变量以及语句块

    • 父类构造方法被加载

    • 子类变量或者语句块被加载

    • 子类构造方法被加载

  3. Java的泛型了解吗?

  4. 如果有两个类,一个animal,一个dog,dog继承于animal,现在有一个animallist,它的泛型是animal,然后有个doglist,泛型是dog,doglist能不能赋值给animallist?不能

  5. 泛型里面 List和List 泛型里面的List和List<?>什么关系 讲一下用的集合结构 针对你说的map,想要它线程安全,怎么做 对于一个list遍历时,修改元素,会怎么样 聊天框发一个题,关于String的声明,然后在方法中修改,最后输出 来讲讲clone(),浅拷贝和深拷贝 深拷贝怎么实现,Clonable()是什么接口,里面有哪些方法 除了深拷贝,你还知道哪些深拷贝的方法 来说说hashCode()和equals(),他俩作用,关系 说说==和equals,object1 == object2 和object1.equals(object2)一定一样吗 说说为什么需要重写equals(),什么情况下需要重写equals 正常情况下,Java的基本数据类型和包装数据类型都已经重写的Object类的[equals方法和hashCode方法 需要重写equals方法的场景:主要依据业务场景,例如:当我们对Coder这个对象进行判断的时候,我们不需要Coder的所有的属性都必须相同,则可以重写Coder的equals方法,和hashCode()方法,达到只判断Coder的name相同即为对象相同,而不需要考虑其他的age等属性是否相同----达到效果:对象的name相同(不考虑age属性的异同),equals就相等 为什么一定需要重写hashcode方法 如果没有重写hashCode方法,会使用Object的hashCode方法,是依据对象的内存地址进行hash算法取得的值 不重写hashCode方法,如果我们进行equals判断两个对象是否相等的时候,明明两个对象所有的属性都相同(equals无法相等)但是内存地址不同,进而两个对象的hashCode也不同,在使用HashMap,HashSet等需要依赖hash值的数据结构时,就会出现相同的对象因hash值不同,储存到了不同的哈希槽中,产生了错误的结果 反射原理 动态获取程序信息以及动态调用对象的功能 优点 可扩展性:使用来自外部的用户自定义类 为很多框架开箱即用的功能提供便利 缺点 性能开销:动态类型的解析,所以 JVM 无法对这些代码进行优化 内部暴露:可以通过Method.Accessible 绕过Java 语言的访问权限,在私有的方法所在类之外的地方调用该方法(安全隐患) 动态代理的实现方式 使用动态代理的好处:方便地为某些行为添加一些共同的逻辑(Spring AOP)或者是将执行权完全交由代理控制(RPC) 常见使用场景:AOP、RPC、JDBC的连接等 JDK proxy:通过拦截器加反射的方式实现,实现接口的方式使用 CGLIB:基于 ASM 实现,继承类的方式使用 Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib,只需要在 Spring 配置中添加 <aop:aspectj-autoproxy proxy-target-class=“true”/> 即可 ArrayList和LinkedList的区别,分别在什么场景下使用比较好 Synchronized修饰成员方法和静态方法的区别 Java访问修饰符的作用范围 static和 abstract可以同时修饰一个方法吗 abstract与static不能同时使用,static关键字修饰的成员是属于类的,而abstract一般是用来修饰普通方法目的是为了让子类继承后重写该方法,而static修饰的方法是不存在继承重写的 abstract和final可以同时作为一个类的修饰符 Abstract表示抽象类,抽象类本身不可实例化,必须有子类去继承,且子类中实现抽象父类中所有的抽象方法,子类才可实例化。 final修饰的类,不可继承 final的作用对象 hashCode和 eqauls的关系, hashcode相等的话 equals能否相等, equals相等 hashcode能否相等? float f = 3.14;能通过编译吗? finally语句先于 return 和 throw语句执行,try不能单独使用,必须有catch或者finally String 字符串直接拼接等于常量操作,带变量的拼接 等同于new 不会初始化子类的几种 调用的是父类的static方法或者字段 调用的是父类的final方法或者字段 通过数组来引用 只要是被子类重写的方法,不使用super调用都是调用子类方法 i++,++i 需要注意 i是会变化的 导包只可以导到当前层,不可以再导入包里面的包中的类 要导入java/awt/event下面的所有类:只能是import java.awt.event.* 由大小写字母、数字、下划线、和美元符号组成,但不能以数字开头。标识符不能使用java中的关键字 ascii码对应值:0:48 A:65 a:97,数字连起来是486597 -> 486 597 -> 486 (486 + 111) ,因此 '2'-->50 二维数组定义,一维长度必须定义,二维可以后续定义 Arraylist默认数组大小是10,扩容后的大小是扩容前的1.5倍,最大值小于Integer 的最大值减8,如果新创建的集合有带初始值,默认就是传入的大小,也就不会扩容 构造器之间调用用this,当前类调用自己的构造器Car(int x)--->this(x); 以java8为准,switch支持10种类型 基本类型:byte char short int 对于包装类 :Byte,Short,Character,Integer String enum 不支持:long、double、float try catch 当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块。只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了return或throw等语句,finally块会终止方法,系统将不会跳回去执行try块、catch块里的任何代码 finally代码块在return中间执行。return的值会被放入临时空间,然后执行finally代码块,如果finally中有return,会刷新临时空间的值finally没有return则方法结束返回临时空间值 long → float 无须强制转换(这个最选项容易出错 add(++a)后,得到的是128,但因为byte型 范围是 -128-127 ,超出了范围 ,把128转换成二进制,然后把第一位当为符号位,所以是**-128** 内部类 静态内部类:不可以访问外部非静态变量 成员内部类:可以访问外部所有资源,但本身内部不可有静态属性 局部内部类:不可以被访问修饰符和static修饰,只能访问final变量和形参 匿名内部类:没有构造器,没有静态资源,无法被访问修饰符和static修饰,创建一定跟在new后面 ArrayList和LinkedList的区别?arrayList如何扩容?扩容的时候插入元素怎么办? ArrayList使用场景,LinkedList使用场景?它们的优点 ArrayList 遍历时删除(添加)常见陷阱及其解决 普通for循环:删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用 增强for循环:删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出 解决:删除完毕马上使用break跳出,则不会触发报错 iterator遍历(迭代器遍历):这种方式可以正常的循环及删除。但要注意的是,使用iterator的remove方法,如果用list的remove方法同样会报上面提到的ConcurrentModificationException错误 jdk1.7和1.8 hashmap的区别 最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构 jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容 插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插 扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容 HashMap实现原理 1.8之前是数组+链表的结构;1.8之后是数组+链表+红黑树的结构 为何随机增删、查询效率都很高的原因是? 增删是在链表上完成的,而查询只需扫描部分,则效率高 HashMap的负载因子是什么? 与扩容机制有关;即若当前容器的容量,达到设定最大值,就需要要执行扩容操作。 举个例子:当前的容器容量是16,负载因子是0.75;16*0.75=12,也就是说,当容量达到了12的时就会执行扩容操作。 作用很简单,相当于是一个扩容机制的阈值。 当超过了这个阈值,就会触发扩容机制 为什么设置0.75? 0.5:一半空间就扩容,空间利用率不高,1:装满才扩容,影响put的时间 泊松分布和指数分布(数学推导而得0.75) 时间和空间的权衡:负载因子是0.75的时,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率 HashMap的扩容机制—resize() 什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶 int转long,long转int public、 protected、 default、 private区别? 接口方法不写修饰符和实现类不写修饰符有区别吗?接口中能使用什么修饰符?接口中可以使用 static修饰符吗? synchronized修饰实例方法和静态方法的区别? 封装类和基本类型的区别、关系 ConcurrentHashMap 原理, put 与putIfAbsent的区别? put在放入数据时,如果放入数据的key已经存在与Map中,最后放入的数据会覆盖之前存在的数据, putIfAbsent在放入数据时,如果存在重复的key,那么putIfAbsent不会放入值 有了解JUC其他类吗? 自旋锁、轻量级锁、偏向锁、锁消除、锁粗化 自旋锁:互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。 自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定 **偏向锁:**偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。 当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态 锁消除:锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除 锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。 接口和抽象类的区别 得分点 接口与抽象类的方法,接口与抽象类的常量与变量,单继承多实现 接口更关注的是功能,也可以理解为某个操作;而抽象类更关注的是所属关系,也就是A is a B. 1、两者都不能实例化 区别:1、抽象类定义的关键字是abstract class,接口定义的关键字是interface; 2、属性上,抽象类可以有静态变量、常量和成员变量,接口只能有常量; 3、抽象方法可以有普通方法,而接口jdk1.8之前只能有抽像方法(1.8之后,增加了静态方法和默认方法); 4、抽象方法可以有构造方法,接口不可以有构造方法 5、抽象类单继承,接口可以多实现 6、抽象方法在业务编程上更像一个模板,有自己的功能,同时也可以有优化补充的多种形式 而接口更像是一种规范和要求,实现就要按照要求来进行 接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。 从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类,让它们都实现新增的方法。 接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。从 Java 9 开始,允许将方法定义为 private,这样就能定义某些复用的代码又不会把方法暴露出去。 接口的字段默认都是 static 和 final 的 对象的创建 类加载检查:当遇到new指令之后,首先会去到静态常量池中看看能否找到这个指令所对应的符号引用,然后会检查符号引用所对应的是否被加载——连接——初始化,如果有的话就进行第二步,如果没有就要先进行类的加载 分配内存:当类加载检查通过后,对象的大小在类加载完成之后就可以确定,所以首先会为准备新创建的对象根据对象大小分配内存,这块内存就是在堆中划分。那么如何进行内存的分配呢,一般情况下会有两种情况:“指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定 指针碰撞 假设Java堆的内存是绝对规整的,所有用过的内存都放一边,空闲的内存放在另一边,中间放着一个指针 作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间那边挪动一段与对象大小相等的距离 空闲列表 如果Java堆中的内存并不是规整的,已使用的内存和空间的内存是相互交错的,虚拟机必须维护一个空闲 列表,记录上哪些内存块是可用的,在分配时候从列表中找到一块足够大的空间划分给对象使用 初始化零值虚拟机需要将分配到的内存空间中的数据类型都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式 执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来 对象的访问方式 句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据类型数据各自的具体地址信息 直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址 话说:这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销 对象的内存布局 对象头:对象头包括两部分:Mark Word 和 类型指针(数组还需额外空间存储数组长度) Mark word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 Klass word:类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例 数组长度:如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度 对象体:是用于保存对象属性的主体部分,占用内存空间取决于对象的属性数量和类型; 对齐字节:是为了减少堆内存的碎片空间(不一定准确) 锁的升级: 【1】初期锁对象刚创建时,还没有任何线程来竞争,对象的 Mark Word是下图的第一种情形,这偏向锁标识位是0锁状态01,说明该对象处于无锁状态(无线程竞争它)。 【2】当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时 Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图第二种情形。 【3】当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的 Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。 【4】如果竞争的这个锁对象的线程超过两个线程,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象 Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程 虚方法表 通过实例引用调用实例方法的时候,先通过对象实例数据找到对应的方法区中的对象类型数据中找,找不到的话再去父类类型信息中找。 如果继承的层次比较深,要调用的方法位于比较上层的父类,则调用的效率是比较低的,因为每次调用都要经过很多次查找。这时候大多系统会采用一种称为虚方法表的方法来优化调用的效率。 所谓虚方法表,就是在类加载的时候,为每个类创建一个表,这个表包括该类的对象所有动态绑定的方法及其地址,包括父类的方法,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。当通过对象动态绑定方法的时候,只需要查找这个表就可以了,而不需要挨个查找每个父类 CAS ABA问题解决 解决方案:JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。如果当前引用 == 预期引用,并且当前标志等于预期标志,则以原子方式将该引用和该标志的值设置为给定的更新值 String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串(首次遇到的字符串实例),则返回代表池中这个字符串的String对象的引用 可重入锁:同一个线程,获取对象锁之后内部方法需再次获取锁资源(同一个对象),可以直接获取到锁资源,不会因为之前已经获取过还没释放而阻塞 可重入锁的一个优点就是可一定程度的避免死锁 AQS:**构建锁和同步器的基础框架 是JUC的基石** 请求共享资源,如果资源空闲,将资源设置为锁定状态,并设置当前线程为工作线程,如果共享资源被占用,那么就需求有一套阻塞等待唤醒机制来保证锁的分配 AQS通过内置的FIFO双向队列来完成资源的排队工作,它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及ReentrantLock.park()等方式,维护state变量的状态,是并发达到同步的控制效果 什么是循环依赖(BeanCurrentlyInCreationException) 多个bean之间相互依赖,形成了一个闭环;比如:A依赖于B、B依赖于C、C依赖于A 通常来说,如果问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中,属性互相引用的场景 默认的单例(singleton)的场景是支持循环依赖的,不报错 原型(Prototype)的场景是不支持循环依赖的,会报错 Spring内部通过3级缓存来解决循环依赖(中间态的概念) 中间态:已经实例化但还没有初始化(属性填充)的状态(半成品) 三个Map: 第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象 第二级缓存:earlySingletonObjects,存放早期暴露出来的bean对象,bean的生命周期未结束(属性还未填充完) 第三级缓存:singletonFactories,存放可以生成Bean的工厂,加入A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Bean 默认的单例(singleton)的场景是支持循环依赖的,不报错 原型(Prototype)的场景是不支持循环依赖的,会报错 实例化/初始化 实例化:内存中申请一块内存空间(租好房子,自己的家具还没有搬进去) 初始化属性填充:完成属性的各种赋值(装修、家电家具进场) 只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,如果是非单例的bean,每次从容器中获取都是一个全新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中 构造器循环依赖式无法解决的,你想让构造器注入支持循环依赖,是不存在的 总结:假设A、B循环引用,实例化A的时候就将其放入三级缓存中(提前暴露),接着填充属性的时候,发现依赖了B,同样的流程也是实例化后放入三级缓存,接着去填充属性的时候发现B自己依赖A,这时候从缓存中查找到早期暴露的A,没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充和初始化,这时候B完成后,就去完成剩下的A的步骤,如果没有AOP代理,就进行AOP处理获取代理后的对象A,注入B,走剩下的流程 A/B对象在三级缓存中的迁移说明 1、A创建的过程中需要B,于是A将自己放到三级缓存里面,去实例化B 2、B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A,然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A 3、B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面 jv 运行时数据区 程序计数器(Program Counter Register),也叫PC寄存器,是线程私有的,是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令 用以完成分支、循环、跳转、异常处理、线程恢复等基础功能 方法区 存储已被虚拟机加载的类相关信息,静态变量、常量等 在不同的虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace) :栈管运行,堆管存储 属于线程私有的,存储局部变量表、操作数栈、常量池引用等 :所有的对象都是在堆上分配内存的,是gc的主要区域 学过虚拟机是吧,说说cms和g1 什么么时候会stw,如果让你设计,怎么让stw的时间最小 stop the world:等待所有用户线程进入安全点后并阻塞,做一些全局性操作的行为 什么时候会发生(什么时候会触发进入安全点) GC Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation) Biased lock revocation 取消偏向锁 Various debug operation (e.g. thread dump or deadlock check) dump 线程 引起长时间STW原因 GC RevokeBias 撤销偏向锁操作也会消耗很长时间。在高并发系统中,建议禁用偏向锁(增加 -XX:-UseBiasedLocking) Java GC机制 常见的垃圾收集器 年轻代:serial、parNew 老年代:serialOld、ParallelOld、CMS G1:整堆收集器 JMM内存模型 所有变量存在主内存,每个线程有自己的工作内存 线程对变量的操作必须在工作内存进行,而不能直接对主内存进行操作。并且每个线程不能访问其他线程的工作内存 类加载器 启动类加载器(Bootstrap ClassLoader) 扩展类加载器(Extension ClassLoader) 应用程序类加载器(Application ClassLoader) 双亲委派 一个类加载器首先将类加载请求委派到父类加载器,只有当父类加载器无法完成时才尝试自己加载 好处:最终都是委托给顶层的启动类加载器进行加载,可以使得基础类得到统一 可以破坏双亲委派吗?为什么要破坏双亲委派? 可以 因为在某些情况下父类加载器需要委托子类加载器去加载class文件 常见案例:JDBC ,DriverManager;服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派 JDK监控和故障处理工具 * cpu占用过高,如何定位具体线程,具体哪一行代码? 第一步找到cpu占用最高的进程id(定位进程PID) 定位到是那个java程序 jps -l | grep 进程pid # 这行命令的作用是通过jps -l打印进程pid对应的java程序是哪一个,至于grep只是为了过滤 定位线程的tid ps -mp 进程pid -o THREAD,tid,time 根据tid得到16进制的线程id printf "%x\n" 上面得到的线程tid 最后通过jstack定位到具体的代码 jstack可以打印堆栈信息,因为堆栈信息内容比较多,所以我们需要知道对应的线程id才可以快速的找到问题所在 jstack 进程id | grep 16进制的tid -A60 JVM 内存分配 对象优先在 eden 区分配,如果eden空间不足,会触发minorGC 进入老年代的三种方式 大对象直接进入老年代 长期存活的对象将进入老年代 动态对象年龄判定:如果在Survivor空间相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄 可达性分析算法 从GCRoots开始向下搜索,没走过一条路径称之为引用链,如果一个对象到GCRoots,没有任何引用链相连,那么这个对象不可达,不可达的对象可以被回收 sj模式 有用过什么设计模式,具体说一个用到的例子 单例有那两种方式,细节,你写过什么样的,双重检测锁方式有什么好处,为什么要有两次检测 了解哪些设计模式 来共享屏幕,手写单例吧(我还写错了) 单例模式 确保一个类只有一个实例,并提供该实例的全局访问点 懒汉式(线程不安全) public class Singleton {    private static Singleton uniqueInstance;    private Singleton() {   }    public static Singleton getUniqueInstance() {        if (uniqueInstance == null) {            uniqueInstance = new Singleton();       }        return uniqueInstance;   } } 双重校验(线程安全) public class Singleton {    private volatile static Singleton uniqueInstance;    private Singleton() {   }    public static Singleton getUniqueInstance() {        if (uniqueInstance == null) {            synchronized (Singleton.class) {                if (uniqueInstance == null) {                    uniqueInstance = new Singleton();               }           }       }        return uniqueInstance;   } } 简单工厂模式 在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类 定义一个下单接口 public interface IVender {    /**     * 供应商下单方法     */    void order(); } 然后分别实现A、B供应商的下单方法。 public class VendorA implements IVender {    @Override    public void order() {        // 业务逻辑处理        System.out.println("A供应商下单成功,下单时间" + new Date());   } } public class VendorB implements IVender {    @Override    public void order() {        // 业务逻辑处理        System.out.println("B供应商下单成功,下单时间:" + new Date());   } } 接着定义一个工厂类,根据传入的不同参数请求,分别创建不同的供应商实例并返回,若碰到无效的参数,则抛出异常。 public class VendorFactory { ​    public static IVender createVendor(String type) {        switch (type) {            case "A":                return new VendorA();            case "B":                return new VendorB();            default:                throw new RuntimeException("供应商不存在");       }   } } 观察者模式 当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象 适配器模式 适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁,它结合了两个独立接口的功能。 这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能 策略模式 在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改 多xc ArrayList和LinkedList的区别(我提了一下并发不安全,成功转移到提问并发 上面两个有什么并发安全的实现 synchronized原理 synchronized 不管是读还是写,如果前面有锁,只能是等待 lock中有读写锁,可以做到读读并发,读写互斥,写写互斥,但是synchronized做不到 反编译结果:Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因 monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者; 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1; 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权; monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁; Java中的原子类是怎么实现的知道吗? CAS和Volatile ABA问题解决 AtomicStampedReference:在修改值的同时附加一个时间戳,只有值和时间戳都相同才进行修改 加版本标识 volatile关键字有什么特点,原理是什么? Java中有什么无锁操作的方式? 线程池的核心参数是什么? corePoolSize:核心线程数 maximumPoolSize:最大线程数 keepAliveTime:空闲线程的存活时间 unit:keepAliveTime的单位 workQueue:等待队列 threadFactory:线程工厂 rejectedExecutionHandler:拒绝策略 有哪几种拒绝策略? AbortPolicy:跑出rejectExecutionException异常 DiscardPolicy:不处理新任务直接丢弃 DiscardOldestPolicy:丢弃最早未处理的任务 线程池运行流程 线程池创建,准备好corePoolSize数量的线程,准备接受任务 新任务进来,用core准备好的空闲线程处理任务 核心线程数满了,将再进来的任务放入阻塞队列中,空闲的core会自动去阻塞队列获取任务来执行 阻塞队列满了,会直接创建新的线程来执行任务,最大能到maximumPoolSize的数量 max数量的线程都执行完成,max-core数量的空闲线程,会在keepAliveTime指定的时间后销毁,直到保持core数量个线程 如果线程数量开到max数量,还有新的任务进入,就会采用rejectedExecutionHandler指定的策略进行处理 所有的线程创建都是由指定的factory创建的 常用线程池的四种创建方式 newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程 ExecutorService executorService = Executors.newCachedThreadPool(); newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待 ExecutorService executorService = Executors.newFixedThreadPool(3); newScheduledThreadPool:创建一个周期性的线程池,支持定时及周期性执行任务 ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3); newSingleThreadExecutor:创建一个单线程的线程池,可保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行 ExecutorService executorService = Executors.newSingleThreadExecutor(); 如果线程池核心线程数设置为5,最大线程数设置为10,阻塞队列是一个无界队列,一直提交线程,线程数最大能达到多少? 如果线程池核心线程数设置为5,最大线程数设置为10,阻塞队列大小也设置的是10,线程池刚启动的时候有多少个线程? 看过Java线程池的源码吗?如果想监控线程池的资源,通过看它的源码,它有没有提供什么方式来实现这个功能? Java线程有哪几种状态,以及状态之间如何转换的? 线程中断有没有了解过? ThreadLocal的基本原理是什么? ThreadLocal 是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的,这样就可以避免资源竞争带来的多线程的问题 每当你创建一个 ThreadLocal 变量,那么访问这个变量的每个线程都会在当前线程存一份这个变量的本地副本,只有自身线程能够访问,和其他线程是不共享的,这样可以避免线程资源共享变量冲突的问题 ThreadLocal 内存泄露问题 内存泄露 是由于 ThreadLocalMapkey 为弱引用导致的,弱引用对象,在没有被外部引用时,当发生GC 是 key 被回收为 null, 但是 value 还存在强引用,可能会存在 内存泄露问题 解决: key : 创建为弱引用对象 调用 set(), get(), remove() 都会对 key = null 进行清除 value 操作 建议:在使用ThreadLocal的时候要养成及时 remove() 的习惯 ThreadLocalMap的哈希冲突 jdk 中大多数的类都是采用了链地址法(链表)来解决hash 冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢? 开放地址法:这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入 如果在线程池中的task中出现了没有捕获的异常会对当前线程有什么影响?对线程池有什么影响? 没有影响 源码分析: ThreadPoolExecutor----程序会捕获包括Error在内的所有异常,并且在程序最后,将出现过的异常和当前任务传递给afterExecute方法 而ThreadPoolExecutor中的afterExecute方法是没有任何实现的如果我们的任务抛出了异常,我们也无法立刻感知到。 即使感知到了,也无法查看异常信息 如果出现上面这种异常应该对线程进行什么处理呢,线程池应该怎么处理呢? 自定义线程池,继承ThreadPoolExecutor并复写其afterExecute(Runnable r, Throwable t)方法 实现Thread.UncaughtExceptionHandler接口,实现void uncaughtException(Thread t, Throwable e);方法,并将该handler传递给线程池的ThreadFactory 如果提交任务的时候使用的方法是submit,那么该方法将返回一个Future对象,所有的异常以及处理结果都可以通过future对象获取。 采用Future模式,将返回结果以及异常放到Future中,在Future中处理过多线程的代码吗?知道哪些jdk查看占用的命令 jstack:生产JVM当前时刻线程的快照(Threaddump,当前进程中所有线程的信息),目的:帮助定位程序问题出现的原因:如长时间停顿、CPU占用率过高等 使用jps、ps -ef | grep java查看当前java进程的pid,严重情况下可以使用top命令查看当前系统cpu/内存使用率最高的进程pid 使用top -Hp pid 命令查看进程里面占用最多的资源的线程 使用命令printf "%x\n" 3440 把线程pid转换成16进制数,得到:d70 使用jstack 3429 | grep -20 d70命令查询该线程阻塞的地方 问linux命令,查看占用线程 线程执行sleep(100),变成什么状态 线程进入就绪状态,如果很长时间都没有获得CPU的执行权,有可能导致睡了大于100ms Thread.sleep(0),是否有用? Thread.sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,重新计算优先级。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。这也是我们在大循环里面经常会写一句Thread.sleep(0),因为这样就给了其他线程比如Paint线程获得CPU控制权的权利,这样界面就不会假死在哪里 Java多线程sleep和wait的区别 sleep方法是Thread线程类的方法,而wait是Object类的方法 sleep可以在任何地方使用,而wait只能在同步方法和同步块中使用 sleep、wait调用后都会暂停当前线程并让出CPU的执行时间,但不同的是 sleep不会释放当前持有对象的锁资源,到时间后会继续执行 wait会释放所有的锁并需要notify/notifyAll后重新获取到对象资源后才能继续执行 wait(1000) vs sleep(1000) Thread.Sleep(1000) 意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。 wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码 如果锁被其他线程占用,则等待其他线程释放锁 注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify 多线程情况下,如何让某个线程等到其他线程全都执行完了再执行 多线程分别统计然后进行计算应该如何实现? Java线程池参数 HashMap的工作原理 通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度 HashMap构造函数的capacity指数组长度还是元素个数 table数组的大小是由capacity这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30(243行) loadFactor是装载因子,主要目的使用来确认table数组是否需要动态扩展,默认是0.75 扩容时,调用resize()方法,将table数组长度变为原来的两倍 如果数据很大,扩展将会带来性能的损失,在性能要求较高的地方,这种损失可能是致命的 HashMap扩容 红黑树 DK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列 JDK1.8 之后,当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()方法--树形化。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间,否则,就是只是执行 resize() 方法对数组扩容 根据哈希表中元素个数确定是扩容还是树形化 如果是树形化ßß 遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系 然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容 hashmap和concurrenthashmap的区别是什么 HashMap是线程不安全的,当出现多线程操作时,会出现安全隐患;而ConcurrentHashMap是线程安全的。 HashMap不支持并发操作,没有同步方法,ConcurrentHashMap支持并发操作,通过继承 ReentrantLock(JDK1.7重入锁)/CAS和synchronized(JDK1.8内置锁)来进行加锁(分段锁),每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。 ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁 ConCurrentHashMap和HashTable 两者都是线程安全的 hashTable:利用synchroized关键字实现的线程安全 conCurrentHashMap:利用分段锁(Segment:实现了ReentrantLock)实现 ConcurrentHashMap的put方法:不同于hashMap和HashTable,put和get需要两次hash到达指定的entry Segment实现了ReentrantLock,当执行put操作时,会进行第一次hash定位Segment的位置,然后进行第二次hash,找到对应的HashEntry的位置 ConcurrentHashMap的size方法如何保证线程安全 BlockingQueue如何保证线程安全 Java中有哪些多线程机制 synchronized机制,实现的是公平锁还是非公平锁,为什么公平锁要比非公平锁慢? 多线程 对变量+1(原子类) ThreadLocal(详细讲怎么使用的) .Atomic原子类的原理, CAS有哪些问题? 高并发情况下计数应该使用哪个类 分布式锁,超时了应该怎么办? 如何优化锁的粒度,如何续锁? 高并发情况下如何优化? 分段加锁:把数据分成很多个段,每个段是一个单独的锁,所以多个线程过来并发修改数据的时候,可以并发的修改不同段的数据 一旦对某个数据做了分段处理之后,有一个坑大家一定要注意:就是如果某个下单请求,咔嚓加锁,然后发现这个分段库存里的库存不足了,此时咋办? 这时你得自动释放锁,然后立马换下一个分段库存,再次尝试加锁后尝试处理。这个过程一定要实现 布隆过滤器 red redis为什么使用单线程 Redis 的性能瓶颈不在 CPU ,主要在内存和网络; 单线程编程容易并且更容易维护;多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能 采用非阻塞的io多路复用机制,同时监听多个socket,并根据不同的事件选择事件处理器 redis哨兵模式和集群模式分别是用来解决什么问题的? 哨兵模式 主观下线、客观下线(quorum值) 客观下线后 1、哨兵选出leader来主持主从切换(raft算法) 1.1、leader候选者(确认客观下线的哨兵节点) 1.2、投票(候选者可以投给自己) 任意候选者满足两个条件:获得的投票数大于半数值、大于等于quorum值 2、主从故障转移 2.1、选出新的主节点(过滤不健康的从节点、优先级、主从复制程度offser、runId) 2.2、从节点指向新的主节点(进行数据同步) 2.3、通知客户端主节点已变更 2.4、监控就得主节点,恢复后降为从节点,进行数据同步 集群模式当获取数据时,可能会回复asyn、moved,了不了解这些回复分别代表什么?(不了解。。。) 使用集群模式的话会有什么限制吗?(不同节点上的数据无法使用事务) 如果想使用事务操作的话,希望key能分布在一个节点上,有没有什么办法可以让key分布在同一个节点上?(redis提供了这样的功能hashtag) redis用到了哪些数据结构? zset的底层数据结构有没有了解过? 跳表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。在大部分情况下,跳跃表的效率可以和平衡树相媲美,而且实现比平衡树更加简单 跳表本质上是个有序的链表,其通过在节点上随机的添加辅助连接使得查找的时间复杂度从O(N)变为平均O(logN),最坏O(N) 通过辅助节点来加快搜索过程:在顶层的链表进行扫描,直到 遇到一个含有较小关键字且指向一个含有较大关键字节点的节点,或者到达这一层的最后一个节点,然后下降到下一层辅助节点继续查找,直到确认目标值不存在或者找到了目标值所在节点(当前节点的数据比目标值小,且下一个节点的值(78)比目标值大,且此时已位于最低一层辅助节点,所以判定目标值不存在于该跳跃表中。查找结束) Q:查找过程中为何没有用到数据节点自身的next指针呢? A: 因为第一层的辅助节点等价于数据节点自身的next。其实在跳跃表中,数据节点只有数据,辅助节点中只有next指针 Q: 当数据节点变多时,三层辅助节点好像不太够用? A: 当跳跃表的数据节点变多时,我们可以加入更多层的辅助节点来保证足够快的查找速度 为什么zset要在数据比较小的时候使用压缩列表呢,压缩列表是一个什么样的数据结构,能带来什么好处呢? 压缩列表的最大特点,就是它被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,不仅可以利用 CPU 缓存,而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销。 但是,压缩列表的缺陷也是有的: 不能保存过多的元素,否则查询效率就会降低; 新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题 压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值; 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值; quicklist 其实 quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表 quicklist 解决办法,通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能 redis的rehash有了解过吗,是怎么进行rehash的? Redis 定义一个 dict 结构体,这个结构体里定义了两个哈希表 在正常服务请求阶段,插入的数据,都会写入到「哈希表 1」,此时的「哈希表 2 」 并没有被分配空间 随着数据逐步增多,触发了 rehash 操作,这个过程分为三步: 给哈希表 2 分配空间,一般会比哈希表 1 大 2 倍; 将哈希表 1 的数据迁移到哈希表 2 中; 迁移完成后,哈希表 1 的空间会被释放,并把哈希表 2 设置为哈希表 1,然后新创建一个空白的哈希表,为下次 rehash 做准备 渐进式rehash 将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移 渐进式 rehash 步骤如下: 给哈希表 2分配空间; 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将哈希表 1 中索引位置上的所有 key-value 迁移到哈希表 2 上; 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把哈希表 1 的所有 key-value 迁移到哈希表 2,从而完成 rehash 操作 触发 rehash 操作的条件,主要有两个:(负载因子=哈希表已保存节点数量/哈希表大小) 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作 redis的rehash 和 hashMap的resize()区别? redis的rehash,是将ht[0]的数据搬到ht[1] (ht[1]是ht[2]两倍),扩容过程中新加入的元素直接往ht[1]里面添加 hashmap的resize,创建一个新的数组,将旧的搬到新的数组中 我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。 确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算 在进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上。在前面提到,HashMap 使用 hash%capacity 来确定桶下标。HashMap capacity 为 2 的 n 次方这一特点能够极大降低重新计算桶下标操作的复杂度 redis分布式锁,怎么保证可重入性? 设计思路:假设锁的key为“lock”,hashKey是当前线程的id:“threadId”,锁自动释放时间假设为20 获取锁(lua脚本 判断并加锁) 判断lock是否存在 EXISTS lock 不存在,则自己获取锁,记录重入层数为1. 存在,说明有人获取锁了,继续判断是不是自己的锁,即判断当前线程id作为hashKey是否存在:HEXISTS lock threadId 不存在,说明锁已经有了,且不是自己获取的,锁获取失败. 存在,说明是自己获取的锁,重入次数+1: HINCRBY lock threadId 1 ,最后更新锁自动释放时间, EXPIRE lock 20 释放锁: 判断当前线程id作为hashKey是否存在: HEXISTS lock threadId 不存在,说明锁已失效 存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 , 获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock 因此,存储在锁中的信息就必须包含:key、线程标识、重入次数。不能再使用简单的 key-value 结构, 这里推荐使用 hash 结构。而且要让所有指令都在同一个线程中操作,那么使用 lua 脚本 redis作用,怎么用的,缓存一致性是怎么保证的 .你用了主从复制是吧,如果主库 数据还没有同步到从库,此时 redis里面的是从库的脏 数据,怎么办? redis主从数据不一致 无法避免,常见的几种优化方案 业务可以接受,系统不优化 强制读主,高可用主库,用缓存提高读性能 在cache里记录哪些记录发生过写请求,来路由读主还是读从 保证缓存和数据数据一致性的问题(双写一致性) 如果你查询的 数据,缓存中没有,你是怎么处理的,应该怎么优化 项目中Redis的数据怎么更新到数据库中去的,有没有定期清空Redis之类的 Redis如何根据前缀查询某个key(类似模糊查询) 缓存穿透、缓存击穿、缓存雪崩 Redis的常用数据结构? 如果需要统计某一天的用户的访问次数应该用Redis哪个数据结构?如何找到排名前十的用户? 缓存一致性 redis集群模式切片 redis事务 不了解 redis脑裂,数据丢失怎么办 当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端 在 Redis 的配置文件中有两个参数我们可以设置: min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据 min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据 原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了 等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失 Redlock 算法加锁三个过程: 第一步是,客户端获取当前时间(t1)。 第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作: 加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。 第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败 可以看到,加锁成功要同时满足两个条件(简述:如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功) 加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了 my mysql有什么引擎 实务的四大特性,原理(我说知道是mvcc,但不了解细节,他就问了我怎么了解到mvcc的,是看博客,还是看书,然后又插入了下面一个问题) 什么是索引 表中一列或多列的排序结果 优点:加速表与表之间的联系,提高查询速度,大大减少了服务器需要扫描的行数,如果语句中存在排序和分组可以有效减小排序分组消耗的时间 缺点:占用物理空间,每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护 聚簇索引和非聚簇索引能介绍一下吗? PrepareStatement预编译的过程是在哪里? 为什么使用statement会有SQL注入的风险呢? 如果有一个SQL语句,我想进行SQL注入攻击,应该怎么样攻击,怎么样设计SQL语句? 执行一条 SQL 查询语句,期间发生了什么? 连接器:建立连接,管理连接、校验用户身份; 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块; 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型; 执行 SQL:执行 SQL 共有三个阶段: 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划; 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端; .sql中模糊查询的时候需要注意什么 联合索引需要注意什么 数据库主键,自增主键和uuid哪个更好? 索引的优缺点 读写分离的优缺点 MyBatis的两级缓存 MySQL,从三张表中查询平均分数大于85的学生 left join 左外连接:以左表为主,左表全展示,不在右边的补null,right join 右外连接:以右表为主,右表全展示,不在左边的补null InnoDB以外的存储引擎,他们和InnoDB的区别 MylSAM 区别: InnoDB 行级锁,MyISAM只支持表锁 InnoDB支持事务,MyISAM不支持事务 InnoDB支持外键 InnoDB 支持数据库崩溃后的安全恢复 并发事务会引发什么问题? 脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象 修改丢失:多个事务修改同一个字段,以最后一次为准,其他修改都会丢失 不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象 幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象 那为了避免这些问题,SQL 提出了四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,从左往右隔离级别顺序递增,隔离级别越高,意味着性能越差,InnoDB 引擎的默认隔离级别是可重复读 MySQL如何保证可重复读以及防止幻读? 事务隔离级别: 读未提交(*read uncommitted*),指一个事务还没提交时,它做的变更就能被其他事务看到; 读提交(*read committed*),指一个事务提交之后,它做的变更才能被其他事务看到; 可重复读(*repeatable read*),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别串行化(*serializable* );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行; MVCC保证隔离性:通过版本号,避免同一数据在不同事务间的竞争 InnoDB在每行数据都增加三个隐藏字段,一个唯一行号,一个记录创建的版本号,一个记录回滚的版本号,在多版本并发控制中,为了保证数据操作在多线程过程中的事务隔离机制,降低锁竞争的压力,保证较高的并发量;在每开启一个事务时,会生成一个事务的版本号,被操作的数据会生成一条新的、临时的数据行,但是在提交前对其他事务是不可见的,对于数据的更新(包括增删改)操作成功,会将这个版本号更新到数据的行中,事务提交成功,将新的版本号更新到此数据行中,这样保证了每个事务操作的数据,都是互不影响的,也不存在锁的问题 MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作 通过 next-key lock(记录锁+间隙锁)方式解决了幻读 为什么MySQL innodb 选择B+树做为索引的数据结构 MySQL 的数据是持久化的,意味着数据(索引+记录)是保存到磁盘上的,因为这样即使设备断电了,数据也不会丢失。 由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到某行数据,然后读入到内存,也就是说查询过程中会发生多次磁盘 I/O,而磁盘 I/O 次数越多,所消耗的时间也就越大。 所以,我们希望索引的数据结构能在尽可能少的磁盘的 I/O 操作中完成查询工作,因为磁盘 I/O 操作越少,所消耗的时间也就越小。 另外,MySQL 是支持范围查找的,所以索引的数据结构不仅要能高效地查询某一个记录,而且也要能高效地执行范围查找。 所以,要设计一个适合 MySQL 索引的数据结构,至少满足以下要求: 能在尽可能少的磁盘的 I/O 操作中完成查询工作; 要能高效地查询某一个记录,也要能高效地执行范围查找; 索引数据结构演变 1、二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n) 2、为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高,而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。(红黑树也是2个节点,树高,且红黑树维护成本高) 3、B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有: B+ Tree 是基于 B Tree 和叶子节点顺序访问指针实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能 叶子节点才会存放实际数据(索引+记录),非叶子节点只会存放索引; B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。 B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化; B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树 事务四大特性+隔离级别 原子性、一致性、隔离性、持久性 隔离级别:读未提交、读已提交、可重复读、串行化 索引创建的规则 * 性别字段可不可以建立索引 索引的使用条件 适合使用索引 字段有唯一性限制的,比如商品编码; 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。 经常用于 GROUP BYORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的 不适合 WHERE 条件,GROUP BYORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。 表数据太少的时候,不需要创建索引; 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。 索引失效的情况 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效; 当我们在查询条件中对索引列使用函数,就会导致索引失效。 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效 主键索引(聚簇索引)为什么比辅助索引(二级索引)快 主键索引和非主键索引的区别是:非主键索引的叶子节点存放的是主键的值,而主键索引的叶子节点存放的是整行数据,其中非主键索引也被称为二级索引,而主键索引也被称为聚簇索引 什么是覆盖索引? 覆盖索引是指,索引上的信息足够满足查询请求,不需要再回到主键上去取数据。 MySQL数据结构的演变 全部遍历 二叉树 红黑树-- 自动平衡树 m阶b树 b+树 mysql分页,如果查10000到10010的数据怎么写,有什么问题? MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写,利用子查询先快速定位需要获取的 id 段,然后再关联查询,就是对分页进行 SQL 改写的具体实现; 简单分页:主键索引 复杂分页:复合索引 explain分析 Type: ALL(全表), index(索引), range(范围), ref(常量), eq_ref, const, system, NULL(从左到右,性能从差到好) possible_keys:可能使用到的索引 key:实际使用的索引 rows:扫描的行数 extra:该列包含MySQL解决查询的详细信息(尽量不要出现) 主从复制 bin log 只会在事务提交后一次性记录执行过的事务中的sql语句以及其反向sql(作为回滚用),保存的是逻辑日志->执行的sql语句 slave会从master读取bin log来进行数据同步 MySQL复制过程分成三步: master将改变记录到二进制日志(bin log),这些记录过程叫做二进制日志事件,bin log events slave将master的binary log events拷贝到它的中继日志(relay log) slave重做中继日志中的事件,将改变应用到自己的数据库中,MySQL复制是异步的 MySQL主从复制延时 主库上可以多客户端并发的写入,当主库的写压力较高时,由于从库的SQL线程是单线程的,导致从库处理速度可能会跟不上主库的处理速度,从而造成了主从延迟 并行复制:指的就是从库开启多个SQL线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行 MySQL主从复制数据丢失问题 MySQL默认是异步复制,主库在执行完客户端提交的事务后会立即将结果返给给客户端,并不关心从库是否已经接收并处理。那么可能主库挂掉了,主库提交的日志并没有及时发送给从库,导致数据丢失 全同步复制能避免上面的问题,即当主库执行完一个事务,需要等待所有从库执行完该事务才能返回给客户端。显然,全同步复制会严重影响复制的性能半同步复制是介于异步复制和全同步复制之间,主库在提交后执行事务提交的线程将一直等待,直到至少有一个半同步从库确认已接收到所有事件,从库仅在将事件写入中继日志(relay log)并刷新到磁盘后,才对接收到事务的事件进行确认,主库收到确认后才会对客户端进行响应。半同步复制保证了事务成功提交后,至少有两份日志记录,一份在主库的binlog上,另一份在至少一个从库的中继日志relay log上,这样就进一步保证了数据的完整性 count(*) 和 count(1) 有什么区别?哪个性能最好? 执行效果上: count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。 执行效率上: 列名为主键,count(列名)会比count(1)快 列名不为主键,count(1)会比count(列名)快 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*) 如果有主键,则 select count(主键)的执行效率是最优的 如果表只有一个字段,则 select count(*)最优 如何优化count(*) 一、近似值 可以使用 show table status 或者 explain 命令来表进行估算 二、额外表保存记数值 MySQL是如何保证原子性 利用Innodb的undo log; undo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息 例如: (1)当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据 (2)当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作 (3)当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操 undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子 MySQL是如何保证持久性的 是利用了redo log;Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失;redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。innodb通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。也就是说提交了两个日志文件 sproot Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架 说说@Autowired和@Resource注解的区别 @Autowied是Spring提供的注解、@Resource是JDK提供的注解 @Autowied是只能按类型注入,@Resource默认按名称注入,也支持按类型注入 @Autowired按类型装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它required属性为false,如果我们想使用按名称装配,可以结合@Qualifier注解一起使用 IOC IoC:控制反转 对象交给spring容器进行管理,方便解耦 之前我们创建对象时用new,现在直接从spring容器中取,维护对象之间的依赖关系,降低对象之间的耦合度。 实现方式为DI,依赖注入,有三种注入方式:构造器、setter、接口注入 AOP AOP是面向切面编程,它是一种编程思想,它是一种通过预编译方式和运行期间动态代理的方式实现不修改源代码的情况下给程序动态添加功能的一种技术,可以降低代码的耦合度,便于管理,提高代码的可重用性。 AOP的实现方式有两种: JDK动态代理,可以在运行时创建接口的代理实例 CGLIB动态代理:可以在运行期间创建子类的动态实例 AOP的应用场景有:事务,日志管理等 spring事务管理 spring支持编程式事务管理和声明式事务管理两种方式: 编程式事务管理使用TransactionTemplate 声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。 声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别 Bean的作用域 作用域分为singleton、prototype、request、session、globalsession singleton表示在spring容器中仅存在一个实例,以单例的方式存在 prototype:每次调用都会返回一个新的实例 request:每次HTTP请求都会创建一个新的bean session:同一个HTTP session共享一个bean,不同的HTTP请求使用不同的session global session:同一个全局session共享一个bean bean生命周期 创建,初始化,调用,销毁 实例化(必须的)构造函数构造对象 初始化 就绪 销毁 Spring Boot的理解,以及它和Spring的区别? Spring Boot是Spring组件一站式的解决方案,简化了Spring繁琐的配置,简化开发约定大于配置 SpringBoot的优点: 可以快速构建项目 可以对主流开发框架的无配置集成 项目可以独立运行,无需外部依赖Serlvet容器 提供运行时的应用监控 简化开发 核心功能: 自动配置:针对很多Spring应用程序常用的应用功能,SpringBoot能自动提供相关配置。 * 起步依赖:SpringBoot通过起步依赖为项目的依赖管理提供帮助,起步依赖其实就是特殊的Maven依赖和Gradle依赖,利用了传递依赖解析,把常用库聚合在一起,组成了几个为特定功能而定制的依赖 端点监控:SpringBoot可以对正在运行的项目提供监控 说说Spring Boot的启动流程 springboot启动类,调用run方法 run方法执行流程 获取监听器参数配置 打印Banner信息 创建并初始化容器 监听器发送通知 说说Spring Boot的起步依赖 starter配置,约定大于配置,spring boot将日常企业应用研发中的各种场景都抽取出来,做成一个个的starter(启动器),starter中整合了该场景下各种可能用带的依赖,用户只需要在Maven中引入starter依赖,spring boot就能自动扫描到要加载的信息并启动响应的默认配置 说说Spring Boot的自动装配 SpringBoot通过@EnableAutoConfiguration开启自动配置功能 加载Spring.factories中注册的AutoConfiguration类,当某个AutoConfiguration类满足@Conditional注解的条件时,实例化该AutoConfiguration中定义的Bean,并注入到Spring容器中,完成依赖框架的自动配置 说说Spring Boot常用的注解 @SpringbootApplication注解,它是springboot的核心注解,用于开启自动配置 @EnableautoConfiguration注解:它用来开启自动注解,配置各种组件 @ComponentScan,用于扫描指定的包和组件 @SpringBootConfiguration:声明当前类springboot应用的配置类,项目中只能有一个一般无须我们添加。 还有其他的import注解,conditional,uncondition注解等等,它们都是注解里面包含的注解 说说BeanFactory和FactoryBean的区别 BeanFactory是最基础的IOC容器,给Spring 的容器定义一套规范,给IOC容器提供了一套完整的规范; FactoryBean只是SpringIOC容器创建Bean的一种形式; 在MyBatis中$和#有什么区别 使用$设置参数时,MyBatis会创建普通的SQL语句,执行SQL时,直接将参数拼接在SQL中,可能会产生SQL注入攻击,但在某些场景中,比如需要动态指定SQL语句中的列名时,就只能使用$占位符了 使用#设置参数时,MyBatis会创建预编译的SQL语句,预编译的SQL语句执行效率高,并且可以防止SQL注入攻击,在实际开发中,大部分情况下使用#占位符 介绍一下MyBatis的缓存机制 一级缓存默认开启,作用域session,二级缓存作用域namespace MyBatis的缓存机制,一级缓存也称为本地缓存,它默认启用且不能关闭 一级缓存存在于SqlSession的生命周期中,即它是SqlSession级别的缓存,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中,如果同一个SqlSession中执行的方法和参数完全一致,则会将缓存的对象返回 二级缓存则为SqlSessionFactory,mybaits的全局配置setting有一个参数cacheEnabled,这个参数是二级缓存的全局开关,默认值是true,初始状态为启用状态,映射语句文件中的所有SELECT 语句将会被缓存 映射语句文件中的所有时INSERT 、UPDATE 、DELETE 语句会刷新缓存 缓存会使用Least Recently U sed ( LRU ,最近最少使用的)算法来收回 prototype在什么场景下使用 prototype:对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题 singleton:对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争 Stateless无状态用单例Singleton模式,Stateful有状态就用原型Prototype模式 singleton是线程安全的吗,为什么 不是 在 @Controller/@Service 等容器中,默认情况下,scope值是单例-singleton的,也是线程不安全的 尽量不要在@Controller/@Service 等容器中定义静态变量,不论是单例(singleton)还是多实例(prototype)他都是线程不安全的 默认注入的Bean对象,在不设置scope的时候他也是线程不安全的。 一定要定义变量的话,ThreadLocal来封装,这个是线程安全的 用到了springboot,问一下bean的作用域有哪些 bean的后处理器是怎么让spring知道并使用的 哪些类spring会帮我们管理呢? 配置了@Service就一定会放入bean中吗?有没有什么条件? 如果第三方jar包中有一个@Service,会被扫描进来吗? 第三方jar包要想被扫描进来的话是要将它的类路径加载进spring是吗? 第三方jar包如何保证spring会将它加载进spring中? Spring中为什么要将bean交给Spring来管理? 介绍一下Spring MVC的执行流程? 用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 DispatcherServlet(前端控制器); 由 DispatcherServlet 请求一个或多个 HandlerMapping(处理器映射器),并返回一个执行链(HandlerExecutionChain)。 DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器); HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller); Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC的底层对象,包括 Model 数据模型和 View 视图信息); DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析; ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet; DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图); 视图负责将结果显示到浏览器(客户端) 如果我自己弄一套Controller、Service、Dao,不依赖spring容器,能不能work,这个过程应该怎么写知道吗?不知道。。。 Spring的bean通常会new多少个对象,比如userService是每一个请求来了就new一个userService还是全局共享一个userService? 就拿这个userService来说,假设里面有个一个getUserById的方法,那应该把这个userService的scope设置成单例的还是session的?singleton,因为一般来说service和dao都是无状态的,只是通过service和dao来操作数据库,使用session或者prototype的话也可以,只是会浪费资源。 spring的权限管理步骤是什么? spring的拦截器是什么设计模式? 用了spring是吧,说说@Autowired和@Resources的区别,Spring你项目中是怎么保证线程安全的呢 你说了byType,如果注解注入的时候多个bean都实现了当前接口,Spring容器是怎么处理的 又问那你知道使用xml配置文件时,这种情况是怎么解决的吗 Spring事务管理 Spring如何实现IoC?使用的是Java的哪个机制? Spring的自动注入方式? Spring如何在完成属性注入之后输出一条日志? AOP的通知类型?发生异常后afterreturning是否还会调用? 不会 进入AfterThrowing IOC、AOP springBoot 启动流程,关闭流程 spring Aop @before @after @around @afterReturn @afterThrow 的执行顺序 正常执行:@Before(前置通知)---> @After(后置通知)---> @AfterReturning(正常返回) 异常执行:@Before(前置通知)---> @After(后置通知)---> @AfterThrowing(方法异常) 网络 https的原理,为什么要有https https协议需要到CA申请证书,一般免费证书较少,因而需要一定费用 http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl/tls加密传输协议 http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443 http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全 tcp三握手四挥手 TCP的粘包 拆包 正常:两个数据包逐一分开发送 粘包:两个包一同发送, 拆包:Server接收到不完整的或多出一部分的数据包 粘包/拆包的原因 发生TCP粘包或拆包有很多原因,现列出常见的几点: 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包 粘包、拆包解决办法 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开 常见的http状态码,301,302分别是什么,500是什么 http每次建立连接都要断开吗,http是那一层的协议 tcp和udp区别,适用场景 .你说tcp是可靠的,为什么是可靠的,那么让你设计基于udp的可靠协议怎么设计 http2.0相比于1.0的优化 其他 除了项目上写的序列化框架还了解其他序列化框架吗? 解释一下什么是粘包,什么是拆包 有没有了解过netty里自己解决粘包拆包的? HTTP如何解决粘包问题的?(header字段?) Prometheus String POD_CPU_TOP5 = "topk(5, sum(irate(container_cpu_usage_seconds_total{container!='',container!='POD',namespace!='default',namespace!='kube-public',namespace!='kube-system',namespace!='kube-node-lease',namespace!='ingress-nginx',namespace!='monitoring'}[5m])) by (namespace) / sum(kube_pod_container_resource_requests_cpu_cores) by (namespace)) * 100";   String POD_MEM_TOP5 = "topk(5, sum(container_memory_working_set_bytes{container!='POD',container!='',namespace!='default',namespace!='kube-public',namespace!='kube-system',namespace!='kube-node-lease',namespace!='ingress-nginx',namespace!='monitoring'}) by (namespace) / sum(kube_pod_container_resource_requests_memory_bytes{container!='',namespace!='default',namespace!='kube-public',namespace!='kube-system',namespace!='kube-node-lease',namespace!='ingress-nginx',namespace!='monitoring'}) by (namespace)) * 100";   String NODE_CPU_TOP5 = "topk(5, 100 - (avg(irate(node_cpu_seconds_total{mode='idle'}[5m])) by (instance)) * 100)";   String NODE_MEM_TOP5 = "topk(5 , (sum(node_memory_MemTotal_bytes) by (instance) - sum(node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes + node_memory_Slab_bytes) by (instance)) / (sum(node_memory_MemTotal_bytes) by (instance))) * 100";    -- fabric 内存使用率 "("+"sum(container_memory_usage_bytes{namespace='kube-system',container='fabric'}) by (node) - "+ "sum(container_memory_cache{namespace='kube-system',container='fabric'}) by (node) +" +"sum(container_memory_usage_bytes{namespace='kube-system',container='ryu-controller'}) by (node) - " +"sum(container_memory_cache{namespace='kube-system',container='ryu-controller'}) by (node)"+ ")"+"/" +"("+"sum(kube_pod_container_resource_requests_memory_bytes{namespace='kube-system',container='fabric'}) by (node) +"+"sum(kube_pod_container_resource_requests_memory_bytes{namespace='kube-system',container='ryu-controller'}) by (node)" +")"); HTTP中的content-length和chunk(听着好像是这么读的)解决粘包知不知道? 服务端将服务注册进去,客户端请求服务,这个过程在项目中大概是什么样子的,描述一下? 如何解决Kafka消息积压? 实时/消费任务挂掉导致的消息积压:在积压数据不多和影响较小的情况下,除了重启消费任务、排查问题还需解决消息积压问题 1、任务重新启动后直接消费最新的消息,对于"滞后"的历史数据采用离线程序进行"补漏" 2、建新的topic并配置更多数量的分区(大量消息积压),将积压消息的topic消费者逻辑改为直接把消息打入新的topic,将消费逻辑写在新的topic的消费者中 Kafka分区数设置的不合理或消费者"消费能力"不足的优化 Kafka分区数是Kafka并行度调优的最小单元,如果Kafka分区数设置的太少,会影响Kafka Consumer消费的吞吐量。 如果数据量很大,Kafka消费能力不足,则可以考虑增加Topic的Partition的个数,同时提升消费者组的消费者数量 Kafka消息key设置的优化 使用Kafka Producer消息时,可以为消息指定key,但是要求key要均匀,否则会出现Kafka分区间数据不均衡。 所以根据业务,合理修改Producer处的key设置规则,解决数据倾斜问题 如何把电脑硬盘的1T数据存到内存的8g里 还问了排序算法,口述归并思路,快排在什么情况下最快 冒泡排序    //冒泡排序    //双重循环    //比较前后相邻的两个个数据,如果前面数据大于后面的数据,就将这两个数据交换位置    public int[] bubbleSort(int[] arr){        for(int i=0;i<arr.length;i++){            for(int j=1;j<arr.length-i;j++){                if(arr[j-1]>arr[j]){                    int temp=arr[j];                    arr[j]=arr[j-1];                    arr[j-1]=temp;               }           }       }        return arr;   } 快速排序 //快速排序    //平均O(nlogn)在最坏状况下则需要O(n2)次比较,但这种状况并不常见    //事实上,快速排序通常明显比其他O(nlogn)算法更快    //快排步骤:    //1、从数列中挑出一个元素,称为 "基准"(pivot)    //2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)    // 在这个分区退出之后,该基准就处于数列的中间位置position    //3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序 ​    public int[] quickSort(int[] arr, int left, int right){        if(left<right){            //划分子数组的位置            int mid = position (arr, left, right);            quickSort(arr,left,mid-1);            quickSort(arr, mid + 1, right);       }        return arr;   }    public int position (int[] arr,int left,int right){        int pivot=left;        int index=pivot+1;        for (int i = index; i <= right; i++) {            // 将比中心元素小的元素和指针指向的元素交换位置            // 如果第一个元素比中心元素小,这里就是自己和自己交换位置,指针和索引都向下一位移动            // 如果元素比中心元素大,索引向下移动,            // 指针指向这个较大的元素,直到找到比中心元素小的元素,并交换位置,指针向下移动            if(arr[i]<arr[pivot]){                swap(arr,i,index);                index++;           }       }        //将中间元素和指针指向的元素交换位置        swap(arr,pivot,index-1);        return index - 1;   }    public void swap(int[] arr,int i,int j){        int temp=arr[i];        arr[i]=arr[j];        arr[j] = temp;   } 归并思路: 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列; 设定两个指针,最初位置分别为两个已经排序序列的起始位置; 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置; 重复步骤 3 直到某一指针达到序列尾; 将另一序列剩下的所有元素直接复制到合并序列尾。 堆排序 LRU import java.util.*; ​ ​ public class Solution {     //构建node节点     //初始化头尾节点,容量和map     //get:不存在 -1;存在 移动到队头 返回值     //set:不存在 添加map, addNode ;存在 移动到队头 替换新值     // moveToHead: removeNode,addToHead     class Node{          int key;          int value;          Node prev;          Node next;          Node(){};          Node(int key,int value){               this.key=key;               this.value=value;               prev=null;               next=null;         }     }     Map<Integer,Node> map=new HashMap<>();     int size=0;     int capacity=0;     Node head;     Node tail;    public Solution(int capacity) {         // write code here         this.capacity=capacity;         head=new Node();         tail=new Node();         head.next=tail;         tail.next=head;   } ​    public int get(int key) {         // write code here         Node node=map.get(key);         if(node==null) return -1;         moveToHead(node);         return node.value;   } ​    public void set(int key, int value) {         // write code here         Node node=map.get(key);         if(node==null){              Node newNode=new Node(key,value);              map.put(key,newNode);              addToHead(newNode);              size++;              if(size>capacity){                   Node tail=getLastNode();                   removeNode(tail);                   map.remove(tail.key);                   size--;             }         }else{              moveToHead(node);              node.value=value;         }   } ​    public void addToHead(Node node){         node.prev=head;         node.next=head.next;         head.next.prev=node;         head.next=node;   }    public void removeNode(Node node){         node.prev.next=node.next;         node.next.prev=node.prev;   }    public void moveToHead(Node node){         removeNode(node);         addToHead(node);   } ​    public Node  getLastNode(){         Node node=tail.prev;         return node;   } } ​ /** * Your Solution object will be instantiated and called as such: * Solution solution = new Solution(capacity); * int output = solution.get(key); * solution.set(key,value); */ 会不会Kafka(不会,只用过RabbitMQ),MQ如何避免消息丢失 知道哪些限流算法?sentinel的限流算法 算法:判断链表是否有环,如何求出环的长度 算法:两数之和,有重复数字的情况 布隆过滤器 布隆过滤器专门用来检测集合中是否存在特定的元素 设计思想:布隆过滤器由一个长度为m比特的位数组(bit array)与k个哈希函数(hash function)组成的数据结构。位数组初始化均为0,所有的哈希函数都可以分别把输入数据尽量均匀地散列。 当要向布隆过滤器中插入一个元素时,该元素经过k个哈希函数计算产生k个哈希值,以哈希值作为位数组中的下标,将所有k个对应的比特值由0置为1。 当要查询一个元素时,同样将其经过哈希函数计算产生哈希值,然后检查对应的k个比特值:如果有任意一个比特为0,表明该元素一定不在集合中;如果所有比特均为1,表明该集合有可能性在集合中。为什么不是一定在集合中呢?因为不同的元素计算的哈希值有可能一样,会出现哈希碰撞,导致一个不存在的元素有可能对应的比特位为1,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的 总结一下:布隆过滤器认为不在的,一定不会在集合中;布隆过滤器认为在的,可能在也可能不在集合中。 布隆过滤器优缺点 优点: 节省空间:不需要存储数据本身,只需要存储数据对应hash比特位 时间复杂度低:插入和查找的时间复杂度都为O(k),k为哈希函数的个数 缺点: 存在假阳性:布隆过滤器判断存在,可能出现元素不在集合中;判断准确率取决于哈希函数的个数 不能删除元素:如果一个元素被删除,但是却不能从布隆过滤器中删除,这也是造成假阳性的原因了 布隆过滤器适用场景 爬虫系统url去重 垃圾邮件过滤 黑名单 多租户系统概述 froend分离 做前后端分离的时候,你项目里前端登录状态是怎么保持的? 有没有其他更好的方式?(jwt) Token即为令牌,是服务器生成的一串字符串,作为客户端向服务器进行请求的“通行证”。在客户端进行初次登陆后由服务器返回,之后的每次请求只需要携带token进行请求即可,而无需携带密码等敏感信息 为什么用Token token可以减少敏感信息在网络间的传递 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用 JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展 Token 三部分 Header payload 载荷 签名 双token机制 就是在登陆操作之后由服务端返回两个token:accessToken和refreshToken,在之后的验证登录态的操作中使用这两个token进行验证,其中accessToken的过期时间相当短,refreshToken的过期时间相对于accessToken而言相当长,且会不断的刷新,每次刷新后的refreshToken都是不同的 场景:在使用app或者在网页上进行操作时,token突然就过期了,然后只好被迫停止现在正在进行的操作跳转到登录页进行重新登录操作,对于频繁使用或正在进行某个操作的用户,这是一种非常不好的体验 好处: accessToken的存在,保证了登录态的正常验证,因其过期时间的短暂也保证了帐号的安全性 refreshToekn的存在,保证了用户(即使是非活跃用户)无需在短时间内进行反复的登陆操作来保证登录态的有效性,同时也保证了活跃用户的登录态可以一直存续而不需要进行重新登录,其反复刷新也防止某些不怀好意的人获取refreshToken后对用户帐号进行动手动脚的操作 双token检验流程: 首先进行正常的登录操作,在后台服务器验证账号密码成功之后返回2个token:accessToken和refreshToken 在进行服务器请求的时候,先将Token发送验证,如果accessToken有效,则正常返回请求结果; 如果accessToken无效,则验证refreshToken。此时如果refreshToken有效则返回请求结果和新的accessToken和新的refreshToken。如果refreshToken无效,则提示用户进行重新登陆操作 为什么使用cookie会导致csrf攻击? 跨站点伪造攻击 cookie是不跨域的,为什么B网站会拿到A网站的cookie呢? 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B 浏览器会依据加载的域名附带上对应域名cookie 就是如果用户在A网站登录且生成了授权的cookies,然后访问B网站,B站故意构造请求A站的转账请求, 浏览器会附带上A站此登录用户的授权cookie信息,这样就构成crsf攻击,并没有跨域。用户的钱就这样被转走了 为什么使用jwt能避免csrf攻击呢? CSRF的原理是危险网站B盗取登陆成功的业务网站A的Cookie,用户点击危险网站B的带有请求A网站URL的的按钮,关键在于网站A服务端鉴权是使用的Cookie,token存在Cookie里只是暂存,最终的唯一去向是添加到请求request的Header的Authorization 中,服务端要做的是从Header中取出这串token进行验证 jwt的认证过程是什么? 谈谈JWT 在浏览器中输入url地址 ->> 显示主页的过程 总体来说分为以下几个过程: DNS解析 TCP连接 发送HTTP请求 服务器处理请求并返回HTTP报文 浏览器解析渲染页面 连接结束