0
点赞
收藏
分享

微信扫一扫

面试问题集锦

月孛星君 2022-04-05 阅读 52
javaspring

1、List和Set的区别

        List:有序,按对象进⼊的顺序保存对象,可重复,允许多个Null元素对象,可以使⽤Iterator取出 所有元素,在逐⼀遍历,还可以使⽤get(int index)获取指定下标的元素。

        Set:⽆序,不可重复,最多允许有⼀个Null元素对象,取元素时只能⽤Iterator接⼝取得所有元 素,在逐⼀遍历各个元素。

2、ArrayList和LinkedList区别

        ArrayList:底层是基于数组实现,更适合随机查找

        LinkedList:基于链表实现,更适合删除和添加

        ArrayList和LinkedList都实现了List接⼝,LinkedList还额外实现了Deque接⼝,所以 LinkedList还可以当做队列来使⽤。

3、HashMap和HashTable有什么区别?其底层实现是什么?

区别 :

        1. HashMap⽅法没有synchronized修饰,线程⾮安全,HashTable线程安全;

        2. HashMap允许key和value为null,⽽HashTable不允许

        hashMap底层实现:数组+链表实现,jdk8开始链表⾼度到8、数组⻓度超过64,链表转变为红⿊树,元素以内部 类Node节点存在

put步骤:

        1. 计算key的hash值,⼆次hash然后对数组⻓度取模,对应到数组下标

        2. 如果没有产⽣hash冲突(下标位置没有元素),则直接创建Node存⼊数组

         3. 如果产⽣hash冲突,先进⾏equal⽐较,相同则取代该元素,不同,则判断链表⾼度插⼊链表,链 表⾼度达到8,并且数组⻓度到64则转变为红⿊树,⻓度低于6则将红⿊树转回链表

        4. key为null,存在下标0的位置

4、谈谈ConcurrentHashMap的扩容机制

1.7版本

        1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的

        2. 每个Segment相对于⼀个⼩型的HashMap

        3. 每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似

        4. 先⽣成新的数组,然后转移元素到新数组中

        5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本

        1. 1.8版本的ConcurrentHashMap不再基于Segment实现

        2. 当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容

        3. 如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然 后判断是否超过阈值,超过了则进⾏扩容

        4. ConcurrentHashMap是⽀持多个线程同时扩容的

        5. 扩容之前也先⽣成⼀个新的数组

        6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或 多组的元素转移⼯作

5、Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?

        1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和 查询整体效率

        2. 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要 判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法

        3. 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬ 的就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈 希算法,节省CPU资源

6、说⼀下HashMap的Put⽅法

        1. 根据Key通过哈希算法与与运算得出数组下标

        2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中 是Node对象)并放⼊该位置

        3. 如果数组下标位置元素不为空,则要分情况讨论

                a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry 对象,并使⽤头插法添加到当前位置的链表中

                b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node         

                        ⅰ. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个 过程中会判断红⿊树中是否存在当前key,如果存在则更新value

                        ⅱ. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插 法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会 判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链 表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成 红⿊树

                        ⅲ. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要 就扩容,如果不需要就结束PUT⽅法

7、说一下深拷⻉和浅拷⻉区别

        深拷⻉和浅拷⻉就是指对象的拷⻉,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实 例对象的引⽤。

        1. 浅拷⻉是指,只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所 指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象

        2. 深拷⻉是指,既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制, 深拷⻉出来的对象,内部的属性指向的不是同⼀个对象

8、HashMap的扩容机制原理

1.7版本

        1. 先⽣成新数组

        2. 遍历⽼数组中的每个位置上的链表上的每个元素

        3. 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标

        4. 将元素添加到新数组中去

        5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本

        1. 先⽣成新数组         

        2. 遍历⽼数组中的每个位置上的链表或红⿊树

        3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去

        4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置                 a. 统计每个下标位置的元素个数

                b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应 位置

                c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的 对应位置

          5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

9、讲讲CopyOnWriteArrayList

        1. ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素 时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏

        2. 写操作会加锁,防⽌出现并发写⼊丢失数据的问题

        3. 写操作结束之后会把原数组指向新数组

        4. CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应 ⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所 以不适合实时性要求很⾼的场景

10、说说类加载器双亲委派模型

        JVM中存在三个默认的类加载器:

        1. BootstrapClassLoader

        2. ExtClassLoader

        3. AppClassLoader

        AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是 BootstrapClassLoader。

        JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会 先使⽤ExtClassLoader的loadClass⽅法来加载类,同样ExtClassLoader的loadClass⽅法中会先使⽤ BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果 BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到, 那么则会由AppClassLoader来加载这个类。

         所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进⾏加载,如果没加载到才由⾃⼰ 进⾏加载。

11、GC如何判断对象可以被回收

引⽤计数法:每个对象有⼀个引⽤计数属性,新增⼀个引⽤时计数加1,引⽤释放时计数减1,计数 为0时可以回收。

可达性分析法:从 GC Roots 开始向下搜索,搜索所⾛过的路径称为引⽤链。当⼀个对象到 GC Roots 没有任何引⽤链相连时,则证明此对象是不可⽤的,那么虚拟机就判断是可回收对象。

引⽤计数法,可能会出现A 引⽤了 B,B ⼜引⽤了 A,这时候就算他们都不再使⽤了,但因为相互引 ⽤ 计数器=1 永远⽆法被回收。

GC Roots的对象有:

         虚拟机栈(栈帧中的本地变量表)中引⽤的对象

        ⽅法区中类静态属性引⽤的对象

        ⽅法区中常量引⽤的对象

        本地⽅法栈中JNI(即⼀般说的Native⽅法)引⽤的对象

可达性算法中的不可达对象并不是⽴即死亡的,对象拥有⼀次⾃我拯救的机会。对象被系统宣告死亡⾄ 少要经历两次标记过程:第⼀次是经过可达性分析发现没有与GC Roots相连接的引⽤链,第⼆次是在由 虚拟机⾃动建⽴的Finalizer队列中判断是否需要执⾏finalize()⽅法。

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize⽅法,若未覆盖,则直接将其回 收。否则,若对象未执⾏过finalize⽅法,将其放⼊F-Queue队列,由⼀低优先级线程执⾏该队列中对 象的finalize⽅法。执⾏finalize⽅法完毕后,GC会再次判断该对象是否可达,若不可达,则进⾏回收, 否则,对象“复活”

每个对象只能触发⼀次finalize()⽅法, 由于finalize()⽅法运⾏代价⾼昂,不确定性⼤,⽆法保证各个对象的调⽤顺序,不推荐使⽤,建议 遗忘它。

12、JVM中哪些是线程共享区

堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的

13、如何排查JVM问题

jmap jstat jstack jvisualvm jsisualvm  dump文件 

需要分析、推理、实践、总结、再分析,最终定位到具体的问题

14、⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?

        1. ⽤户创建⼀个对象,JVM⾸先需要到⽅法区去找对象的类型信息。然后再创建对象。

        2. JVM要实例化⼀个对象,⾸先要在堆当中先创建⼀个对象。-> 半初始化状态

        3. 对象⾸先会分配在堆内存中新⽣代的Eden。然后经过⼀次Minor GC,对象如果存活,就会进⼊S 区。在后续的每次GC中,如果对象⼀直存活,就会在S区来回拷⻉,每移动⼀次,年龄加1。-> 多 ⼤年龄才会移⼊⽼年代? 年龄最⼤15, 超过⼀定年龄后,对象转⼊⽼年代。

         4. 当⽅法执⾏结束后,栈中的指针会先移除掉。

        5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。

15、JVM有哪些垃圾回收算法?

        1. MarkSweep 标记清除算法:这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶 段:直接将垃圾内存回收。这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内 存碎⽚。

        2. Copying 拷⻉算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了拷⻉算法。拷⻉算法将内存 分为⼤⼩相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另 ⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空 间。⽽且,他的效率跟存货对象的个数有关。

        3. MarkCompack 标记压缩算法:为了解决拷⻉算法的缺陷,就提出了标记压缩算法。这种算法在标 记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象 往⼀端移动,然后将端边界以外的所有内存直接清除。

16、垃圾回收分为哪些阶段

G1GC:

        第⼀:初始标记 标记出GCRoot直接引⽤的对象。STW

        第⼆:标记Region,通过RSet标记出上⼀个阶段标记的Region引⽤到的Old区Region。

        第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,⽽只需要遍 历第⼆步标记出来的Region。

        第四:重新标记: 跟CMS中的重新标记过程是差不多的。

        第五:垃圾清理:与CMS不同的是,G1可以采⽤拷⻉算法,直接将整个Region中的对象拷⻉到另 ⼀个Region。⽽这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。

17、线程的⽣命周期?线程有⼏种状态

线程通常有五种状态,创建,就绪,运⾏、阻塞和死亡状态:

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

        2. 就绪状态(Runnable):线程对象创建后,其他线程调⽤了该对象的start⽅法。该状态的线程位于 可运⾏线程池中,变得可运⾏,等待获取CPU的使⽤权。

        3. 运⾏状态(Running):就绪状态的线程获取了CPU,执⾏程序代码。

        4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使⽤权,暂时停⽌运⾏。直到线程 进⼊就绪状态,才有机会转到运⾏状态。

        5. 死亡状态(Dead):线程执⾏完了或者因异常退出了run⽅法,该线程结束⽣命周期。

阻塞的情况⼜分为三种:

        1. 等待阻塞:运⾏的线程执⾏wait⽅法,该线程会释放占⽤的所有资源,JVM会把该线程放⼊“等待 池”中。进⼊这个状态后,是不能⾃动唤醒的,必须依靠其他线程调⽤notify或notifyAll⽅法才能被 唤醒,wait是object类的⽅法

        2. 同步阻塞:运⾏的线程在获取对象的同步锁时,若该同步锁被别的线程占⽤,则JVM会把该线程放 ⼊“锁池”中。

        3. 其他阻塞:运⾏的线程执⾏sleep或join⽅法,或者发出了I/O请求时,JVM会把该线程置为阻塞状 态。当sleep状态超时、join等待线程终⽌或者超时、或者I/O处理完毕时,线程重新转⼊就绪状 态。sleep是Thread类的⽅法

18、ThreadLocal的底层原理

        1. ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意⽅法中获取缓存的数据

        2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的 值

        3. 如果在线程池中使⽤ThreadLocal可能会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要把 设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强 引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿ 动调⽤ThreadLocal的remove⽅法,⼿动清除Entry对象

        4. ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅ 法之间进⾏传递,线程之间不共享同⼀个连接)

19、并发的特性

原⼦性:原⼦性是指在⼀个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执⾏完成,要 不都不执⾏。关键字:synchronized

可⻅性:当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看得到修改的值。关键字:volatile、synchronized、final

有序性:虚拟机在进⾏代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不⼀定会按 照我们写的代码的顺序来执⾏,有可能将他们重排序。实际上,对于有些代码进⾏重排序之后,虽然对 变量的值没有造成影响,但有可能会出现线程安全问题。关键字:volatile、synchronized

volatile本身就包含了禁⽌指令重排序的语义,⽽synchronized关键字是由“⼀个变量在同⼀时刻只允许 ⼀条线程对其进⾏lock操作”这条规则明确的。

synchronized关键字同时满⾜以上三种特性,但是volatile关键字不满⾜原⼦性。

在某些情况下,volatile的同步机制的性能确实要优于锁(使⽤synchronized关键字或 java.util.concurrent包⾥⾯的锁),因为volatile的总开销要⽐锁低。

我们判断使⽤volatile还是加锁的唯⼀依据就是volatile的语义能否满⾜使⽤的场景。

20、讲讲volatile关键字

被volatile修饰的共享变量对所有线程总是可⻅的,也就是当⼀个线程修改了⼀个被volatile修饰共 享变量的值,新值总是可以被其他线程⽴即得知。volatile禁⽌指令重排序优化。

21、为什么⽤线程池?解释下线程池参数?

        1、降低资源消耗;提⾼线程利⽤率,降低创建和销毁线程的消耗。

        2、提⾼响应速度;任务来了,直接有线程可⽤可执⾏,⽽不是先创建线程,再执⾏。

        3、提⾼线程的可管理性;线程是稀缺资源,使⽤线程池可以统⼀分配调优监控。

corePoolSize:代表核⼼线程数,也就是正常情况下创建⼯作的线程数,这些线程创建后并不 会消除,⽽是⼀种常驻线程

maxinumPoolSize:代表的是最⼤线程数,它与核⼼线程数相对应,表示最⼤允许被创建的线程 数,⽐如当前任务较多,将核⼼线程数都⽤完了,还⽆法满⾜需求时,此时就会创建新的线程,但 是线程池内线程总数不会超过最⼤线程数

keepAliveTime 、 unit:表示超出核⼼线程数之外的线程的空闲存活时间,也就是核⼼线程 不会消除,但是超出核⼼线程数的部分线程如果空闲⼀定的时间则会被消除,我们可以通过 setKeepAliveTime 来设置空闲时间

workQueue:⽤来存放待执⾏的任务,假设我们现在核⼼线程都已被使⽤,还有任务进来则全部 放⼊队列,直到整个队列被放满但任务还再持续进⼊则会开始创建新的线程

ThreadFactory:实际上是⼀个线程⼯⼚,⽤来⽣产线程执⾏任务。我们可以选择使⽤默认的创 建⼯⼚,产⽣的线程都在同⼀个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选 择⾃定义线程⼯⼚,⼀般我们会根据业务来制定不同的线程⼯⼚

Handler:任务拒绝策略,有两种情况,第⼀种是当我们调⽤ shutdown 等⽅法关闭线程池 后,这时候即使线程池内部还有没执⾏完的任务正在执⾏,但是由于线程池已经关闭,我们再继续 想线程池提交任务就会遭到拒绝。另⼀种情况就是当达到最⼤线程数,线程池已经没有能⼒继续处 理新提交的任务时,这是也就拒绝

22、线程池的底层⼯作原理

线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:

1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建 新的线程来处理被添加的任务。

2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊ 缓冲队列。

3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数 量⼩于maximumPoolSize,建新的线程来处理被添加的任务。

4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等 于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被 终⽌。这样,线程池可以动态的调整池中的线程数

23、线程池空闲线程过期机制

keepAliveTime WorkQueue

24、公平锁和⾮公平锁区别

公平锁会先检查队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队。

⾮公平锁不会去检查是否有线程在排队,⽽是直接竞争锁。

当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。

25、Sychronized的偏向锁、轻量级锁、重量级锁

1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了

2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个 线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程

3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞

4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标 记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运 ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

26、Sychronized和ReentrantLock的区别

1. sychronized是⼀个关键字,ReentrantLock是⼀个类

2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁

3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁

4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁

5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态

6. sychronized底层有⼀个锁升级的过程

27、谈谈你对AQS的理解,AQS如何实现可重⼊锁?

1. AQS是⼀个JAVA线程同步的框架。是JDK中很多锁⼯具的核⼼实现框架。

2. 在AQS中,维护了⼀个信号量state和⼀个线程组成的双向链表队列。其中,这个线程队列,就是⽤ 来给线程排队的,⽽state就像是⼀个红绿灯,⽤来控制线程排队或者放⾏的。 在不同的场景下,有 Sychronized的偏向锁、轻量级锁、重量级锁 Sychronized和ReentrantLock的区别 谈谈你对AQS的理解,AQS如何实现可重⼊锁? 40 不⽤的意义。

3. 在可重⼊锁这个场景下,state就⽤来表示加锁的次数。0标识⽆锁,每加⼀次锁,state就加1。释 放锁state就减1。

28、synchronized是可重入锁吗?为什么?

是可重入锁。

可重入锁:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

在 java 内部,同一线程在调用自己类中其他 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行,所以synchronized是可重入锁。

重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

29、java的乐观锁和悲观锁

        Atomic开头类都是乐观锁。synchronized和lock是悲观锁。

        乐观锁:CAS算法、版本号。

30、谈谈Spring⽀持的⼏种bean的作⽤域。

singleton:默认,每个容器中只有⼀个bean的实例,单例的模式由BeanFactory⾃身来维护。该对 象的⽣命周期是与Spring IOC容器⼀致的(但在第⼀次被注⼊时才会创建)。

prototype:为每⼀个bean请求提供⼀个实例。在每次注⼊时都会创建⼀个新的对象

request:bean被定义为在每个HTTP请求中创建⼀个单例对象,也就是说在单个请求中都会复⽤这 ⼀个单例对象。

session:与request范围类似,确保每个session中有⼀个bean的实例,在session过期后,bean 会随之失效。

application:bean被定义为在ServletContext的⽣命周期中复⽤⼀个单例对象。

websocket:bean被定义为在websocket的⽣命周期中复⽤⼀个单例对象。

global-session:全局作⽤域,global-session和Portlet应⽤相关。当你的应⽤部署在Portlet容器 中⼯作时,它包含很多portlet。如果你想要声明让所有的portlet共⽤全局的存储变量的话,那么这 全局变量需要存储在global-session中。全局作⽤域与Servlet中的session作⽤域效果相同。

31、说下Spring事务的实现⽅式和原理以及隔离级别

在使⽤Spring框架时,可以有两种使⽤事务的⽅式,⼀种是编程式的,⼀种是申明式的, @Transactional注解就是申明式的。

⾸先,事务这个概念是数据库层⾯的,Spring只是基于数据库中的事务进⾏了扩展,以及提供了⼀些能 让程序员更加⽅便操作事务的⽅式。

⽐如我们可以通过在某个⽅法上增加@Transactional注解,就可以开启事务,这个⽅法中所有的sql都 会在⼀个事务中执⾏,统⼀成功或失败。

在⼀个⽅法上加了@Transactional注解后,Spring会基于这个类⽣成⼀个代理对象,会将这个代理对象 作为bean,当在使⽤这个代理对象的⽅法时,如果这个⽅法上存在@Transactional注解,那么代理逻辑 会先把事务的⾃动提交设置为false,然后再去执⾏原本的业务逻辑⽅法,如果执⾏业务逻辑⽅法没有出 现异常,那么代理逻辑中就会将事务进⾏提交,如果执⾏业务逻辑⽅法出现了异常,那么则会将事务进 ⾏回滚。

当然,针对哪些异常回滚事务是可以配置的,可以利⽤@Transactional注解中的rollbackFor属性进⾏配 置,默认情况下会对RuntimeException和Error进⾏回滚。

spring事务隔离级别就是数据库的隔离级别:外加⼀个默认级别

read uncommitted(未提交读)

read committed(提交读、不可重复读)

repeatable read(可重复读)

serializable(可串⾏化)

32、数据库的配置隔离级别是Read Commited,⽽Spring配置的隔离级别是Repeatable Read,请问这 时隔离级别是以哪⼀个为准?

以Spring配置的为准,如果spring设置的隔离级别数据库不⽀持,效果取决于数据库

33、Spring事务传播机制

多个事务⽅法相互调⽤时,事务如何在这些⽅法间传播,⽅法A是⼀个事务的⽅法,⽅法A执⾏过程中调 ⽤了⽅法B,那么⽅法B有⽆事务以及⽅法B对事务的要求不同都会对⽅法A的事务具体执⾏造成影响, 同时⽅法A的事务对⽅法B的事务执⾏也有影响,这种影响具体是什么就由两个⽅法所定义的事务传播类 型所决定。

1. REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则⾃⼰新建⼀个事务,如果当前存 在事务,则加⼊这个事务

2. SUPPORTS:当前存在事务,则加⼊当前事务,如果当前没有事务,就以⾮事务⽅法执⾏

3. MANDATORY:当前存在事务,则加⼊当前事务,如果当前事务不存在,则抛出异常。

4. REQUIRES_NEW:创建⼀个新事务,如果存在当前事务,则挂起该事务。

5. NOT_SUPPORTED:以⾮事务⽅式执⾏,如果当前存在事务,则挂起当前事务

6. NEVER:不使⽤事务,如果当前事务存在,则抛出异常

7. NESTED:如果当前事务存在,则在嵌套事务中执⾏,否则和REQUIRED的操作⼀样(开启⼀个事 务)

34、Spring事务什么时候会失效?

spring事务的原理是AOP,进⾏了切⾯增强,那么失效的根本原因是这个AOP不起作⽤了!常⻅情况有 如下⼏种:

1、发⽣⾃调⽤,类⾥⾯使⽤this调⽤本类的⽅法(this通常省略),此时这个this对象不是代理类,⽽ 是UserService对象本身! 解决⽅法很简单,让那个this变成UserService的代理类即可!

2、⽅法不是public的:@Transactional 只能⽤于 public 的⽅法上,否则事务不会失效,如果要⽤在⾮ public ⽅法上,可以开启 AspectJ 代理模式。

3、数据库不⽀持事务

4、没有被spring管理

5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

35、什么是bean的⾃动装配,有哪些⽅式?

开启⾃动装配,只需要在xml配置⽂件中定义“autowire”属性。

autowire属性有五种装配的⽅式:

no – 缺省情况下,⾃动配置是通过“ref”属性⼿动设定 。

        ⼿动装配:以value或ref的⽅式明确指定属性值都是⼿动装配。

        需要通过‘ref’属性来连接bean。

byName-根据bean的属性名称进⾏⾃动装配。

byType-根据bean的类型进⾏⾃动装配。

constructor-类似byType,不过是应⽤于构造器的参数。如果⼀个bean与构造器参数的类型形 同,则进⾏⾃动装配,否则导致异常。

autodetect-如果有默认的构造器,则通过constructor⽅式进⾏⾃动装配,否则使⽤byType⽅式进 ⾏⾃动装配。

@Autowired⾃动装配bean,可以在字段、setter⽅法、构造函数上使⽤。

36、Spring中的Bean创建的⽣命周期有哪些步骤

Spring中⼀个Bean的创建⼤概分为以下⼏个步骤:

1. 推断构造⽅法

2. 实例化

3. 填充属性,也就是依赖注⼊

4. 处理Aware回调

5. 初始化前,处理@PostConstruct注解

6. 初始化,处理InitializingBean接⼝

7. 初始化后,进⾏AOP

37、ApplicationContext和BeanFactory有什么区别

BeanFactory是Spring中⾮常核⼼的组件,表示Bean⼯⼚,可以⽣成Bean,维护Bean,⽽ ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也 是⼀个Bean⼯⼚,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如 EnvironmentCapable、MessageSource、ApplicationEventPublisher等接⼝,从⽽ ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的。

38、Spring中的事务是如何实现的

1. Spring事务底层是基于数据库事务和AOP机制的

2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean

3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解

4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接

5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮ 常重要的⼀步

6. 然后执⾏当前⽅法,⽅法中会执⾏sql

7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务

8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务

9. Spring事务的隔离级别对应的就是数据库的隔离级别

10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的

11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为 需要新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql

39、Spring容器启动流程是怎样的

1. 在创建Spring容器,也就是启动Spring时:

2. ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中

3. 然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去进 ⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建

4. 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推断 构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始化 后这⼀步骤中

5. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件

6. Spring启动结束

在源码中会更复杂,⽐如源码中会提供⼀些模板⽅法,让⼦类来实现,⽐如源码中还涉及到⼀些 BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过 BenaFactoryPostProcessor来实现的,依赖注⼊就是通过BeanPostProcessor来实现的。在Spring启动过程中还会去处理@Import等注解

40、Spring⽤到了哪些设计模式

1.工厂模式

        BeanFactory、FactoryBean

2.适配器模式

        AdvisorAdapter,对Advisor进行了适配

3.访问者模式

        PropertyAccessor接口、属性访问器,用来访问和设置某个对象的某个属性

4.装饰器模式

        BeanWrapper

5.代理模式

        AOP

6.观察者模式

        事件监听器

7.策略模式

        InstantiationStrategy,根据不同情况进行实例化

8.模板模式

        JdbcTemplate

9.委派模式

        BeanDefinitionParserDelegate

10.责任链模式

        BeanPostProcessor

41、Spring Boot ⾃动配置原理?

@Import + @Configuration + Spring spi

⾃动配置类由各个starter提供,使⽤@Configuration + @Bean定义配置类,放到METAINF/spring.factories下

使⽤Spring spi扫描META-INF/spring.factories下的配置类

使⽤@Import导⼊⾃动配置类

42、如何理解 Spring Boot 中的 Starter

使⽤spring + springmvc使⽤,如果需要引⼊mybatis等框架,需要到xml中定义mybatis需要的bean。

starter就是定义⼀个starter的jar包,写⼀个@Configuration配置类、将这些bean定义在⾥⾯,然后在 starter包的META-INF/spring.factories中写⼊该配置类,springboot会按照约定来加载该配置类。

开发⼈员只需要将相应的starter包依赖进应⽤,进⾏相应的属性配置(使⽤默认配置时,不需要配 置),就可以直接进⾏代码开发,使⽤对应的功能了,⽐如mybatis-spring-boot--starter,springboot-starter-redis。

43、Spring Boot、Spring MVC 和 Spring 有什么区别

spring是⼀个IOC容器,⽤来管理Bean,使⽤依赖注⼊实现控制反转,可以很⽅便的整合各种框架,提 供AOP机制弥补OOP的代码重复问题、更⽅便将不同类不同⽅法中的共同处理抽取成切⾯、⾃动注⼊给 ⽅法执⾏,⽐如⽇志、异常等

springmvc是spring对web框架的⼀个解决⽅案,提供了⼀个总的前端控制器Servlet,⽤来接收请求, 然后定义了⼀套路由策略(url到handle的映射)及适配执⾏handle,将handle结果使⽤视图解析技术⽣ 成视图展现给前端

springboot是spring提供的⼀个快速开发⼯具包,让程序员能更⽅便、更快速的开发spring+springmvc 应⽤,简化了配置(约定了默认配置),整合了⼀系列的解决⽅案(starter机制)、redis、 mongodb、es,可以开箱即⽤

44、Spring Boot中常⽤注解及其底层实现

1. @SpringBootApplication注解:这个注解标识了⼀个SpringBoot⼯程,它实际上是另外三个注解的 组合,这三个注解是:

        a. @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个 配置类

        b. @EnableAutoConfiguration:向Spring容器中导⼊了⼀个Selector,⽤来加载ClassPath下 SpringFactories中所定义的⾃动配置类,将这些⾃动加载为配置Bean

        c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫 描的路径是启动类所在的当前⽬录

2. @Bean注解:⽤来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注 解的⽅法进⾏解析,将⽅法的名字做为beanName,并通过执⾏⽅法得到bean对象

3. @Controller、@Service、@ResponseBody、@Autowired都可以说

45、Spring Boot是如何启动Tomcat的

1. ⾸先,SpringBoot在启动时会先创建⼀个Spring容器

2. 在创建Spring容器过程中,会利⽤@ConditionalOnClass技术来判断当前classpath中是否存在 Tomcat依赖,如果存在则会⽣成⼀个启动Tomcat的Bean

3. Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端⼝等,然后 启动Tomcat

46、Spring Boot中配置⽂件的加载顺序是怎样的?

优先级从⾼到低,⾼优先级的配置覆盖低优先级的配置,所有配置会形成互补配置。

1. 命令⾏参数。所有的配置都可以在命令⾏上进⾏指定;

2. Java系统属性(System.getProperties());

3. 操作系统环境变量 ;

4. jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置⽂件

5. jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置⽂件 再来加 载不带profile

6. jar包外部的application.properties或application.yml(不带spring.profile)配置⽂件

7. jar包内部的application.properties或application.yml(不带spring.profile)配置⽂件

8. @Configuration注解类上的@PropertySource

47、MyBatis #{}和${}的区别是什么?

#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。

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

Mybatis 在处理 {}替换成变量的值,调⽤ Statement 来赋值;#{} 的变量替换是在DBMS 中、变量替换后,

#{} 对应的变量⾃动加上单引号,{} 的变量替换是在 DBMS 外、变量替换后,{} 对应的变量不会加上单引号

使⽤#{}可以有效的防⽌ SQL 注⼊, 提⾼系统安全性。

48、简述 Mybatis 的插件运⾏原理,如何编写⼀个插件。

Mybatis 只⽀持针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种 接⼝的插件, Mybatis 使⽤ JDK 的动态代理, 为需要拦截的接⼝⽣成代理对象以实现接⼝⽅法拦截功 能, 每当执⾏这 4 种接⼝对象的⽅法时,就会进⼊拦截⽅法,具体就是 InvocationHandler 的 invoke() ⽅法, 拦截那些你指定需要拦截的⽅法。

编写插件: 实现 Mybatis 的 Interceptor 接⼝并复写 intercept()⽅法, 然后在给插件编写注解, 指定 要拦截哪⼀个接⼝的哪些⽅法即可, 在配置⽂件中配置编写的插件。

@Intercepts({@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args
= {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args =
{ Statement.class })})
@Component

invocation.proceed()执⾏具体的业务逻辑

49、索引的基本原理

索引⽤来快速地寻找那些具有特定值的记录。如果没有索引,⼀般来说执⾏查询时遍历整张表。

索引的原理:就是把⽆序的数据变成有序的查询

1. 把创建了索引的列的内容进⾏排序

2. 对排序结果⽣成倒排表

3. 在倒排表内容上拼上数据地址链

4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从⽽拿到具体数据

50、Mysql聚簇和⾮聚簇索引的区别

都是B+树的数据结构

         聚簇索引:将数据存储与索引放到了⼀块、并且是按照⼀定的顺序组织的,找到索引也就找到了数 据,数据的物理存放顺序与索引顺序是⼀致的,即:只要索引是相邻的,那么对应的数据⼀定也是 相邻地存放在磁盘上的

        ⾮聚簇索引:叶⼦节点不存储数据、存储的是数据⾏地址,也就是说根据索引查找到数据⾏的位置 再取磁盘查找数据,这个就有点类似⼀本树的⽬录,⽐如我们要找第三章第⼀节,那我们先在这个 ⽬录⾥⾯找,找到对应的⻚码后再去对应的⻚码看⽂章。

优势:

1、查询通过聚簇索引可以直接获取数据,相⽐⾮聚簇索引需要第⼆次查询(⾮覆盖索引的情况 下)效率要⾼

2、聚簇索引对于范围查询的效率很⾼,因为其数据是按照⼤⼩排列的

3、聚簇索引适合⽤在排序的场合,⾮聚簇索引不适合

劣势:

1、维护索引很昂贵,特别是插⼊新⾏或者主键被更新导⾄要分⻚(page split)的时候。建议在⼤量插⼊ 新⾏后,选在负载较低的时间段,通过OPTIMIZE TABLE优化表,因为必须被移动的⾏数据可能造成碎 ⽚。使⽤独享表空间可以弱化碎⽚

2、表因为使⽤UUId(随机ID)作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有⽐全表扫⾯ 更慢,所以建议使⽤int的auto_increment作为主键

3、如果主键⽐较⼤的话,那辅助索引将会变的更⼤,因为辅助索引的叶⼦存储的是主键值;过⻓的主 键值,会导致⾮叶⼦节点占⽤占⽤更多的物理空间

InnoDB中⼀定有主键,主键⼀定是聚簇索引,不⼿动设置、则会使⽤unique索引,没有unique索引, 则会使⽤数据库内部的⼀个⾏的隐藏id来当作主键索引。在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要⼆次查找,⾮聚簇索引都是辅助索引,像复合索引、前缀索引、唯⼀索引, 辅助索引叶⼦节点存储的不再是⾏的物理位置,⽽是主键值

MyISM使⽤的是⾮聚簇索引,没有聚簇索引,⾮聚簇索引的两棵B+树看上去没什么不同,节点的结构完 全⼀致只是存储的内容不同⽽已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。 表数据存储在独⽴的地⽅,这两颗B+树的叶⼦节点都使⽤⼀个地址指向真正的表数据,对于表数据来 说,这两个键没有任何差别。由于索引树是独⽴的,通过辅助键检索⽆需访问主键的索引树。

如果涉及到⼤数据量的排序、全表扫描、count之类的操作的话,还是MyISAM占优势些,因为索引所占 空间⼩,这些操作是需要在内存中完成的。

51、索引设计的原则?

查询更快、占⽤空间更⼩

1. 适合索引的列是出现在where⼦句中的列,或者连接⼦句中指定的列

2. 基数较⼩的表,索引效果较差,没有必要在此列建⽴索引

3. 使⽤短索引,如果对⻓字符串列进⾏索引,应该指定⼀个前缀⻓度,这样能够节省⼤量索引空间, 如果搜索词超过索引前缀⻓度,则使⽤索引排除不匹配的⾏,然后检查其余⾏是否可能匹配。

4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进 ⾏更新甚⾄重构,索引列越多,这个时间就会越⻓。所以只保持需要的索引有利于查询即可。

5. 定义有外键的数据列⼀定要建⽴索引。

6. 更新频繁字段不适合创建索引

7. 若是不能有效区分数据的列不适合做索引列(如性别,男⼥未知,最多也就三种,区分度实在太低)

8. 尽量的扩展索引,不要新建索引。⽐如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修 改原来的索引即可。

9. 对于那些查询中很少涉及的列,重复值⽐较多的列不要建⽴索引。

10. 对于定义为text、image和bit的数据类型的列不要建⽴索引。

52、InnoDB存储引擎的锁的算法

Record lock:单个⾏记录上的锁

Gap lock:间隙锁,锁定⼀个范围,不包括记录本身

Next-key lock:record+gap 锁定⼀个范围,包含记录本身

相关知识点:

1. innodb对于⾏的查询使⽤next-key lock

2. Next-locking keying为了解决Phantom Problem幻读问题

3. 当查询的索引含有唯⼀属性时,将next-key lock降级为record key

4. Gap锁设计的⽬的是为了阻⽌多个事务将记录插⼊到同⼀范围内,⽽这会导致幻读问题的产⽣

5. 有两种⽅式显式关闭gap锁:(除了外键约束和唯⼀性检查外,其余情况仅使⽤record lock) A. 将 事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1

举报

相关推荐

0 条评论