目录

resume-2

21.List和Set的区别

这种问题面试官一般想考察的都是你对这两种数据结构的了解,以及使用时候的选择依据,可以从数据结构和一些使用案例入手分别做个介绍,可深可浅,就看自己了解的程度了。

  • List,列表,元素可重复。常用的实现ArrayList和LinkedList,前者是数组方式来实现,后者是通过链表来实现,在使用选择的时候,一般考虑的是基本数据结构的特性,比如,数组读取效率较高,链表插入时效率较高。

  • Set,集合,特点就是存储的元素不可重复。常用是实现,是HashSet和TreeSet,分开来谈:

    • HashSet,底层实现是HashMap,存储时,把值存在key,而value统一存储一个object对象。排重的时候,是先通过对象的hashcode来判断,如果不相等,直接存储。如果相等,会再对key做equals判断,如果依然相等,不存储,如果不相等,则存入,我们知道,HashMap是数组+链表的基本结构,同样的,在HashSet中,也是通过同样的策略,存储在相同的数组位置下的链表中。
    • TreeSet,存入自定义对象时,对象需要实现Comparable接口,重写排序规则。使用场景一般是需要保证存储数据的有序和唯一性。底层数据结构是自平衡的二叉排序树(红黑树) 延伸:
  • 在说到HashMap时,可能会直接引到HashMap相关话题,我发现这个问题面试官非常喜欢问,可能是因为HashMap可聊的较多,也能很好的考验下应聘者对底层实现细节、源码阅读、刨根问底的态度。

  • 涉及到二叉树了,小端就遇到过一次问红黑树特性的,因为之前准备过,胸有成竹的啪啪啪正要一一道来呢,结果刚说到第二个特性,面试官就问:红黑树和普通的平衡二叉树有什么区别?当时一脸懵逼样…回来后赶紧补足,核心区别就是:红黑树也是二叉查找树的一种,二叉树需要通过自旋、或其他方式(比如红黑树还能通过变色)来保证平衡(否则就成了链表结构了,没有时间复杂度上的优势了),红黑树限定的条件相对来说比较宽松,也就是说在平衡的过程中,消耗相对较小。

  • 红黑树,在每个节点增加一个存储位表示节点的颜色(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树可以确保没有一条路径会比其他路径长出两倍,因此,红黑树是一种弱平衡二叉树。相对要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除较多的情况下,我们就用红黑树。

  • 由于HashSet无序,为了实现有序的目的,又不想用其他数据结构,可以用LinkedHashSet。简要说明,同HashSet和HashMap关系一样,也是使用了一个LinkedHashMap,LinkedHashMap和普通的HashMap的区别就是,在原有数据结构之上,采用双向链表的形式将所有entry(注意,是前面讲过的数组+链表中的各个链表里的元素,做了连接)连起来,顺序就是entry的插入顺序,这样可以保证元素的迭代顺序和插入顺序相同(有序性),如下图:

    http://img.cana.space/picStore/20210415092659.png

22.HashMap

参考链接

HashMap是典型的空间换时间的一种技术手段

扰动函数

为什么采取高低16位异或,因为原始hashcode按位与hash表数组长度-1,比较的有效位数很少,原始hashcode的高位没有用上,使用高低位异或可以降低扰动,充分将元素打散到散列表上。

put过程

经过高低位异或按位与之后,得到slot下标,根据slot下标可以计算出slot的四种情况,

  • slot==null

  • slot!=null 但是 node还没有链化

    判断key是否相等,如果相等则替换,否则尾插法插入即可

  • slot!=null node已经链化

    同第二种,多了一个遍历操作找到要处理的node

  • slot!=null node树化

    是否达到树化阈值,达到的调用树化方法

扩容为什么是左移2,不是乘以2

编译之后在指令层面,乘法最终会转换为加法,乘以2在处理器层面也会优化成左移,但是没必要让处理器做这个操作。

扩容后的迁移

resize创建数组->tranfer(rehash);挨个节点处理,同put一样分4种情况

使用 e.hash & oldCap 得到的结果,高一位为 0,当结果为 0 时表示元素在扩容时位置不会发生任何变化;这时候得到的结果,高一位为 1,当结果为 1 时,表示元素在扩容时位置发生了变化,新的下标位置等于原下标位置 + 原数组长度

1.7死循环 or cpu100%

归根结底,原因就是1.7链表新节点采用的是头插法,这样在线程一扩容迁移元素时,会将元素顺序改变,导致两个线程中出现元素的相互指向而形成循环链表,1.8采用了尾插法,从根源上杜绝了这种情况的发生

  1. 头插法会使链表发生反转,多线程环境下会产生环;

A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环,

https://www.jianshu.com/p/1e9cf0ac07f4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

多线程不安全

  • 插入时可能存在覆盖情况
  • 扩容时经典cpu100%

HashMap时一般使用什么类型的元素作为Key?

Immutable类,天生线程安全,可以做很好的优化比如缓存hash值,避免重复计算等

如果让你实现一个自定义的class作为HashMap的key该如何实现?

这个问题其实隐藏着几个知识点,覆写hashCode以及equals方法应该遵循的原则,在jdk文档以及《effective java》中都有明确的描述。当然这也在考察应聘者是如何自实现一个Immutable类。如果面试者这个问题也能回答的很好,基本上可以获得一点面试官的好感了。

设计hash算法

hashCode()不要求唯一但是要尽可能的均匀分布,而且算法效率要尽可能的快。取模、MurMurHash

23.强软弱虚

  • 强引用:死不回收

  • 软引用:内存不足即回收,作为高速缓存使用

  • 弱引用:gc时发现即回收,作为可有可无的缓存使用,例如可以使用WeakHashMap来存储图片信息

  • 虚引用:唯一目的是跟踪对象回收,一般配合强引用一起使用,如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的。

    虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

    由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录(比如nio中的堆外内存)。

    示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    
    package com.eh.ftd.jvm;
      
    import java.lang.ref.PhantomReference;
    import java.lang.ref.ReferenceQueue;
      
    public class Some {
      
        static ReferenceQueue<Some> referenceQueue = new ReferenceQueue<>();
      
      
        public static void main(String[] args) {
            // 开启追踪线程
            Thread checkRefQueue = new CheckRefQueue();
            checkRefQueue.setDaemon(true); // 设置成守护线程
            checkRefQueue.start();
      
            // 创建对象
            Some some = new Some();
            PhantomReference<Some> ps = new PhantomReference<>(some, referenceQueue);
      
            // 回收some对象
            some = null;
            gc();
        }
      
        /**
         * 追踪对象回收
         */
        static class CheckRefQueue extends Thread {
            @Override
            public void run() {
                while (true) {
                    try {
                        PhantomReference<Some> ps = (PhantomReference<Some>) referenceQueue.remove();
                        if (ps != null) {
                            System.out.println("追踪垃圾回收过程:Some实例被gc了");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
      
                }
            }
        }
      
        private static void gc() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.gc();
        }
    }
    

    管理堆外内存,Netty中的zero copy,nio就是用虚引用实现的

    虚引用只保存指针,但不允许访问内存值,指针供JVM以C++方式释放内存,比如NIO零拷贝也用到了虚引用

    https://gitee.com/lienhui68/picStore/raw/master/null/20200728192144.png

    当关联堆外内存的对象需要被回收时,关联的堆外内存也得被回收,否则会发生内存泄露

    当虚引用所指向的对象被回收时,对象会入队,gc线程监测到队列里有待回收对象就会以c++方式释放内存。虚引用是由JVM创建

24.为什么并发能提高效率

在程序中对文件的操作(linux下都是文件),也就是IO的耗时是最多的,你可以充分利用,IO读写时的时间同时处理别的任务,来达到充分利用CPU的目的!

25.synchronized

  • 概念,同步/可见性/有序性(即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;)
  • 使用方式
  • 底层原理,monitorenter/monitorexit,方法级别是标志位,ACC_SYNCHRONIZED
  • 锁升级,无锁,偏向锁(单线程),轻量级锁(又来一个线程,cas)/ 自适应性自旋锁(耗时,自旋次数),重量级锁(自旋次数多,升级)

两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

https://www.cnblogs.com/aspirant/p/11470858.html

http://img.cana.space/picStore/20210415173108.png

重量级锁 互斥量 是指ObjectMonitor对象,这个对象维护了waitSet和entrylist这些数据结构,用来管理正在竞争中的一些线程(会将等待中的线程都包装成ObjectWaiter)

对象头的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象本身的哈希码,随着锁级别的不同,对象头里会存储不同的内容。偏向锁存储的是当前占用此对象的线程ID;而轻量级则存储指向线程栈中锁记录的指针。从这里我们可以看到,“锁”这个东西,可能是个锁记录+对象头里的引用指针(判断线程是否拥有锁时将线程的锁记录地址和对象头里的指针地址比较),也可能是对象头里的线程ID(判断线程是否拥有锁时将线程的ID和对象头里存储的线程ID比较)。

锁升级: 1、当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS操作,在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。 2、有竞争出现时,当有另外的线程试图锁定某个已经被偏斜锁锁定的对象,jvm就会撤销revoke偏斜锁,并切换到轻量级锁。轻量级锁依赖CAS操作Mark Word来试图获取锁,如果成功,就使用轻量级锁,否则继续升级未重量级锁 PS:锁降级也是存在的,当JVM进入SafePoint安全点的时候,会检查是否有闲置的Monitor,然后试图进行降级。

安全点就是指,当线程运行到这类位置时,堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等。

HotSpot中,安全点位置主要在:

方法返回之前 调用某个方法之后 抛出异常的位置 循环的末尾

26.让所有线程等待某个事件的发生后执行

  • 读写锁

    主线程获取写锁,其他子线程获取读锁,事件发生后主线程释放写锁

  • CountDownLatch

    初始值设为1,所有子线程await,事件发生后主线程countDown将计数减为0

  • Semaphore

    信号量设置个数是N,主线程调用acquire(N),然后其他线程调用acquire(1),事件发生后主线程释放n个信号量

27.cas的aba问题

参考

加版本号,aba变成1a2b3a

28.synchronized和lock的区别与选择

Synchronized

  • 优点:实现简单,语义清晰,便于JVM堆栈跟踪;加锁解锁过程由JVM自动控制,提供了多种优化方案。
  • 缺点:不能进行高级功能(定时,轮询和可中断等)。

Lock

  • 优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁  
  • 缺点:需手动释放锁unlock,不适合JVM进行堆栈跟踪。

选择:

在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用Synchronized

https://blog.csdn.net/significantfrank/article/details/80399179

29.ConcurrentHashMap

1.7 使用分段锁 解决,增加了segment数组,每一段segment相当于一个hashmap,提升并发数,1.8 不再使用entry数组而是使用node数组,node插入时 使用cas配合自旋完成插入。

分段锁的原理,锁力度减小的思考

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。 锁力度减小的思考:1.8 使用的Node节点,在插入节点时 使用自旋cas进行插入。

30.AQS

同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。

31.死锁

互斥/请求和保持/不可剥夺/循环等待

银行家算法,安全序列

32.如何保证多线程环境下i++正确

  1. cas,AtomicInteger-> getAndIncrement
  2. lock
  3. synchronized

33.线程池

阻塞队列

why线程池

减少系统开销线程复用/控制并发/统一管理线程

核心参数

1
2
3
4
5
6
7
8
// 七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

处理流程

https://gitee.com/lienhui68/picStore/raw/master/null/20200808022756.png

为什么能够复用

执行execute方法时,会将新创建的线程包装成一个Worker对象,它本身也实现了Runnable接口,所以Worker也是个线程任务。前面有个addWorker方法调用t.start触发worker任务被jvm调用,runWorker里会有一个循环不断地getTask只要不为空就会一直处理,返回null则结束,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Worker.getTask方法源码
private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        // 1.allowCoreThreadTimeOut变量默认是false,核心线程即使空闲也不会被销毁
        // 如果为true,核心线程在keepAliveTime内仍空闲则会被销毁。 
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        // 2.如果运行线程数超过了最大线程数,但是缓存队列已经空了,这时递减worker数量。 
     // 如果有设置允许线程超时或者线程数量超过了核心线程数量,
        // 并且线程在规定时间内均未poll到任务且队列为空则递减worker数量
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 3.如果timed为true(想想哪些情况下timed为true),则会调用workQueue的poll方法获取任务.
            // 超时时间是keepAliveTime。如果超过keepAliveTime时长,
            // poll返回了null,上边提到的while循序就会退出,线程也就执行完了。
            // 如果timed为false(allowCoreThreadTimeOut为falsefalse
            // 且wc > corePoolSize为false),则会调用workQueue的take方法阻塞在当前。
            // 队列中有任务加入时,线程被唤醒,take方法返回任务,并执行。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

核心线程的会一直卡在workQueue.take方法,被阻塞并挂起,不会占用CPU资源,直到拿到Runnable 然后返回(当然如果allowCoreThreadTimeOut设置为true,那么核心线程就会去调用poll方法,因为poll可能会返回null,所以这时候核心线程满足超时条件也会被销毁)。

非核心线程会workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,如果超时还没有拿到,下一次循环判断compareAndDecrementWorkerCount就会返回null,Worker对象的run()方法循环体的判断为null,任务结束,然后线程被系统回收 。

如何使用

分成cpu密集型和io密集型两种情况

1.线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

2.因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务

线程的最佳数目 = ( ( 线程等待时间+线程CPU时间 ) /线程CPU时间 ) * CPU数目*CPU期望利用率

参数设置

https://www.imooc.com/article/5887

34.ThreadLocal

做什么

线程隔离,为每个线程提供可变数据的副本

工作机制

Thread对象包含ThreadLocalMap变量,key是ThreadLocal自身

why虚引用

如果key是强引用,那么就有两个强引用指向堆区的ThreadLocal对象,使得这个节点一直处于可达状态无法被回收,如果将key设置为虚引用,当ThreadLocal对象没有强引用可达,就会被回收,活不过下次gc,key为空为后续ThreadLocalMap的垃圾清理工作提供了便利。

环形数组,线性探测法,在ThreadLocalMap中,调用 set()、get()、remove()方法的时候,会清理掉key为null的记录。

内存泄露

在ThreadLocalMap中,调用 set()、get()、remove()方法的时候,会清理掉key为null的记录。在ThreadLocal设置为null之后,ThreadLocalMap中存在key为null的值,那么就可能发生内存泄漏,只有手动调用remove()方法来避免。

ThreadLocal与线程池结合使用需要注意的地方

线程池中的线程在任务执行完成后会被复用,所以在线程执行完成时,要对 ThreadLocal 进行清理(清除掉与本线程相关联的 value 对象)。不然,被复用的线程去执行新的任务时会使用被上一个线程操作过的 value 对象,从而产生不符合预期的结果。

使用场景

1.Spring里的事务

同一个线程执行的方法里拿到的connection得保证是一样的

2.日志traceId

当前端发送请求到服务A时,服务A会生成一个类似UUID的traceId字符串,将此字符串放入当前线程的ThreadLocal中,在调用服务B的时候,将traceId写入到请求的Header中,服务B在接收请求时会先判断请求的Header中是否有traceId,如果存在则写入自己线程的ThreadLocal中。

35.LockSupport和wait/notify区别

总结一下,LockSupport比Object的wait/notify有两大优势

①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。 ②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。

36.Condition

Condition 类似对象监视器, 提供了await/signal/signalall, 比简单的对象锁提供了更多的功能特性,比如condition支持线程在等待队列中响应中断而对象锁不支持。 每个Condition对象都包含着一个队列,该队列是Condition对象实现等待/通知功能的关键。

37.Fork/Join

Fork/Join框架提供了工具通过利用所有可用的处理器,来加速任务的并行处理,其思想为分而治之. Fork/Join框架首先进行Fork(分),递归的将任务分解成更小的、独立的子任务,直到它们足够简单,能被异步执行. 之后,开始进行Join(结果的合并),所有子任务的执行结果将会递归的进行合并,对于没有返回值的任务,程序将会等待子任务执行结束. 为了提供有效的并行执行方法,Fork/Join框架使用了一个叫做ForkJoinPool的线程池,用于管理类型为ForkJoinWorkerThread的工作线程.

38.IOC和DI

ioc是目的,di是手段。ioc是指让生成类的方式由传统方式(new)反过来,既程序员不调用new,需要类的时候由框架注入(di),是同一件不同层面的解读。

39.BeanFactory 和 FactoryBean

BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似

40.BeanFactory 和 ApplicationContext

BeanFactory:

是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能;

ApplicationContext:

应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能;

  1. 国际化(MessageSource)

  2. 访问资源,如URL和文件(ResourceLoader)

  3. 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层

  4. 消息发送、响应机制(ApplicationEventPublisher)

  5. AOP(拦截器)

41.Spring Bean的生命周期及如何管理

构造函数,set,aware,processor,销毁

http://img.cana.space/picStore/20210416101118.png

【1】实例化 Bean:对于 BeanFactory 容器,当客户向容器请求一个尚未初始化的 bean时,或初始化 bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用 createBean进行实例化。对于 ApplicationContext容器,当容器启动结束后,便实例化所有的单实例 bean。容器通过获取 BeanDefinition对象中的信息进行实例化。并且这一步仅仅是简单的实例化,并未进行依赖注入。实例化对象被包装在 BeanWrapper 对象中,BeanWrapper 提供了设置对象属性的接口,从而避免了使用反射机制设置属性。通过工厂方法或者执行构造器解析执行即可:创建的对象是个空对象。

【2】设置对象属性(依赖注入):实例化后的对象被封装在 BeanWrapper对象中,并且此时对象仍然是一个原生的状态,并没有进行依赖注入。紧接着获取所有的属性信息通过 populateBean(beanName,mbd,bw,pvs),Spring 根据 BeanDefinition 中的信息进行依赖注入。并且通过 BeanWrapper提供的设置属性的接口完成依赖注入。赋值之前获取所有的 InstantiationAwareBeanPostProcessor 后置处理器的 postProcessAfterInstantiation() 第二次获取InstantiationAwareBeanPostProcessor 后置处理器;执行 postProcessPropertyValues()最后为应用 Bean属性赋值:为属性利用 setter 方法进行赋值 applyPropertyValues(beanName,mbd,bw,pvs)。

【3】bean 初始化:initializeBean(beanName,bean,mbd)。 1)执行xxxAware 接口的方法,调用实现了BeanNameAware、BeanClassLoaderAware、BeanFactoryAware接口的方法。 2)执行后置处理器之前的方法:applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName)所有后置处理器的 BeanPostProcessor.postProcessBeforeInitialization() 3)执行初始化方法: InitializingBean 与 init-methodinvoke 当 BeanPostProcessor的前置处理完成后就会进入本阶段。先判断是否实现了 InitializingBean接口的实现;执行接口规定的初始化。其次自定义初始化方法。 InitializingBean 接口只有一个函数:afterPropertiesSet()这一阶段也可以在 bean正式构造完成前增加我们自定义的逻辑,但它与前置处理不同,由于该函数并不会把当前 bean对象传进来,因此在这一步没办法处理对象本身,只能增加一些额外的逻辑。若要使用它,我们需要让 bean实现该接口,并把要增加的逻辑写在该函数中。然后 Spring会在前置处理完成后检测当前 bean是否实现了该接口,并执行 afterPropertiesSet函数。当然,Spring 为了降低对客户代码的侵入性,给 bean的配置提供了 init-method属性,该属性指定了在这一阶段需要执行的函数名。Spring 便会在初始化阶段执行我们设置的函数。init-method 本质上仍然使用了InitializingBean接口。 4)applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);执行初始化之后的后置处理器的方法。BeanPostProcessor.postProcessAfterInitialization(result, beanName);

【4】Bean的销毁:DisposableBean 和 destroy-method:和 init-method 一样,通过给 destroy-method 指定函数,就可以在bean 销毁前执行指定的逻辑。

Bean 的管理就是通过 IOC 容器中的 BeanDefinition 信息进行管理的。

42.Spring Bean的加载过程

https://www.jianshu.com/p/9ea61d204559

解析(拿到RootBeanDefinition,由自身的genericBeanDefinition结合父类信息组装而成)->加载

3.1. 转化 BeanName 3.2. 合并 RootBeanDefinition 3.3. 处理循环依赖 3.4. 创建实例 3.5. 注入属性 3.6. 初始化 3.7. 类型转换

http://img.cana.space/picStore/20210416103403.png

43.解决循环依赖

总结:通过三级缓存技术将对象实例化和初始化分离

具体步骤:a->b->a

  1. 创建a对象
    1. 实例化a,从singletonFactories移除,放入earlySingletonObjects
    2. 由于a依赖b,所以需要创建b对象
  2. 创建b对象
    1. 实例化b 从singletonFactories移除,放入earlySingletonObjects
    2. 注入b的依赖 由于b依赖a 所以在容器内找a,从缓存中拿到a
    3. b创建成功,从earlySingletonObjects移除放入singletonObjects
  3. 由于a依赖b, b创建成功随之a也初始化成功
  4. 此时,a和b互相持有的引用指向的实例都是初始化好的,并且都保存在一级缓存中。

44.如何实现ioc和aop

ioc:

利用反射,根据id,classtype,hashmap,先利用反射转换成类,再利用反射setFiledValue将hashmap中属性和值注入进去。

aop:

创建拦截器(通知)链 创建可执行对象 执行可执行对象 获取拦截器链其实就是将Advisor链中的所有advice取出来,构造MethodInterceptor对象。如果Advisor.getAdvice()返回的是MethodInterceptor对象,直接加入链中,如果Advisor.getAdvice()返回的Advice对象,通过适配器模型构造MethodInterceptor对象,加入链中。拦截器链创建好后,构造ReflectiveMethodInvocation对象。 最后就是执行ReflectiveMethodInvocation对象。

45.Spring事务管理机制

  • 如何管理的: Spring事务管理主要包括3个接口,Spring的事务主要是由他们三个共同完成的。 1)PlatformTransactionManager:事务管理器–主要用于平台相关事务的管理 主要有三个方法:commit 事务提交; rollback 事务回滚; getTransaction 获取事务状态。 2)TransactionDefinition:事务定义信息–用来定义事务相关的属性,给事务管理器PlatformTransactionManager使用 这个接口有下面四个主要方法: getIsolationLevel:获取隔离级别; getPropagationBehavior:获取传播行为; getTimeout:获取超时时间; isReadOnly:是否只读(保存、更新、删除时属性变为false–可读写,查询时为true–只读) 事务管理器能够根据这个返回值进行优化,这些事务的配置信息,都可以通过配置文件进行配置。 3)TransactionStatus:事务具体运行状态–事务管理过程中,每个时间点事务的状态信息。 例如它的几个方法: hasSavepoint():返回这个事务内部是否包含一个保存点, isCompleted():返回该事务是否已完成,也就是说,是否已经提交或回滚 isNewTransaction():判断当前事务是否是一个新事务
  • Spring的事务机制包括声明式事务和编程式事务。 编程式事务管理:Spring推荐使用TransactionTemplate,实际开发中使用声明式事务较多。 声明式事务管理:将我们从复杂的事务处理中解脱出来,获取连接,关闭连接、事务提交、回滚、异常处理等这些操作都不用我们处理了,Spring都会帮我们处理。 声明式事务管理使用了AOP面向切面编程实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。

46.Spring中用到了哪些设计模式

  • 代理模式 Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
  • 模板方法 jdbcTemplate
  • 观察者模式 创建容器、bean 各种方法。
  • 适配器模式 适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。 我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter 。

47.SpringMVC工作流程

http://img.cana.space/picStore/20210416104945.png

48.Spring AOP

https://www.jianshu.com/p/5015f212e4e4

AOP是对OOP(面向对象编程)的补充和完善,当一个集合有大量的公共行为和属性时,我们可以通过封装,继承,多态等来表明集合内对象的层次结构。我们最常说的,实现一个父类,然后子类继承它,父类/子类也可以很好的说明,OOP使得我们可以很好的定义一个集合内元素的纵向关系,减少代码的冗余,提高复用性,易扩展性。然而对于某些横向的关系,OOP并不能很好的满足,就会出现大量的重复代码,各个模块的复用性也会降低。

举个例子: 有三个人爷爷,爸爸和儿子,自然就是儿子继承爸爸,爸爸继承爷爷,这三者一生都会经历出生,入学,工作,结婚等等一系列人生阶段。现在当我们需要记录这三者的每个人生阶段发生的时间节点,只能在每个阶段发生时刻记录一下,这就是最简单的日志功能。必然会导致代码的重复性,并且以“入学”为例,“入学”应该只需要知道入学本身的步骤,比如:体检,面试,交学费。而不是: 记录开始时间,体检,面试,交学费,记录结束时间。日志功能对于各个函数都应该是非透明的,函数本身只需要实现核心关注点即业务逻辑本身,这些散落在各个方法核心功能上,却又与核心业务逻辑无关的功能,便是横切关注点。

简而言之,我们把那些与业务逻辑无关的,却被各个业务模块大量调用的逻辑给封装起来,进而便于减少系统的重复代码量,并且能够降低模块间的耦合度,并有利于未来的扩展和维护,降低了维护成本。最重要的我认为,各个函数本身只关注了核心业务逻辑。

49.如何保证Spring中Controller在并发环境下的安全

无状态/ThreadLocal/设置成prototype

50.netty

是什么

Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发 可维护的 高性能的 面向协议的 服务器和客户端。

特点

高并发: Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。

传输快: Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。

封装好: Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口。

零拷贝

  1. CompositeByteBuf,合并多个ByteBuf
  2. 堆外直接内存

组件

  • Channel,ServerSocket、Socket
  • EventLoop,Selector,Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。
  • ChannelFuture,Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
  • ChannelHandler 和 ChannelPipeline
  • EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),上面我们已经说了 EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。

线程模型

reactor模型,1对1,1对多,多对多,结合Selector和Socket工作机制

51.tcp粘包/拆包原因

数据从发送方到接收方需要经过操作系统的缓冲区,而造成粘包和拆包的主要原因就在这个缓冲区上。粘包可以理解为缓冲区数据堆积,导致多个请求数据粘在一起,而拆包可以理解为发送的数据大于缓冲区,进行拆分处理。

https://gitee.com/lienhui68/picStore/raw/master/null/20200904081652.png

拆包

  • 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。

粘包

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

说白了就是TCP是个"流"协议,没有界限的一串数据,发送和接受的数据会暂存在TCP缓冲区,所以应用层需要一套将TCP消息拆包的机制。