| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> java面试 -> 正文阅读 |
|
[Java知识库]java面试 |
一、设计模式 1.工厂模式1.1简单工厂模式1.2工厂方法模式1.3抽象工厂模式2.单例模式1.1 饿汉式单例1.2 懒汉式单例1.3 双重检查锁单例模式1.4 静态内部类单例模式1.5 反射破坏单例1.6 序列化破坏单例1.7 注册式单例3.原型模式4.代理模式4.1 CGLib 和 JDK 动态代理对比1.JDK 动态代理是实现了被代理对象的接口,CGLib 是继承了被代理对象。 2.JDK 和 CGLib 都是在运行期生成字节码,JDK 是直接写 Class 字节码,CGLib 使用 ASM框架写 Class 字节码,Cglib 代理实现更复杂,生成代理类比 JDK 效率低。 3.JDK 调用代理方法,是通过反射机制调用,CGLib 是通过 FastClass 机制直接调用方法,CGLib 执行效率更高。 4.2 静态代理和动态的本质区别1.静态代理只能通过手动完成代理操作,如果被代理类增加新的方法,代理类需要同步新增,违背开闭原则。 2.动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则。 3.若动态代理要对目标类的增强逻辑扩展,结合策略模式,只需要新增策略类便可完成,无需修改代理类的代码。 4.3 代理模式的优缺点使用代理模式具有以下几个优点: 1、代理模式能将代理对象与真实被调用的目标对象分离。 2、一定程度上降低了系统的耦合度,扩展性好。 3、可以起到保护目标对象的作用。 4、可以对目标对象的功能增强。 当然,代理模式也是有缺点的: 1、代理模式会造成系统设计中类的数量增加。 2、在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。 3、增加了系统的复杂度。 4.4 Spring 中的代理选择原则1、当 Bean 有实现接口时,Spring 就会用 JDK 的动态代理 2、当 Bean 没有实现接口时,Spring 选择 CGLib。 3、Spring 可以通过配置强制使用 CGLib,只需在 Spring 的配置文件中加入如下代码: 5.委托模式6.策略模式7.适配器模式8.模板模式二、Java集合1.HashMap1.1HashMap中put方法的流程 ?final V putVal(int hash, K key, V value, boolean onlyIfAbsent, ? ? ? ? ? ? ? ? ? ? boolean evict) { ? ? ?Node<K,V>[] tab; Node<K,V> p; int n, i; ? ? ?// 步骤①:tab为空则创建 ? ? ?// table未初始化或者长度为0,进行扩容 ? ? ?if ((tab = table) == null || (n = tab.length) == 0) ? ? ? ? ?n = (tab = resize()).length; ? ? ?// 步骤②:计算index,并对null做处理 ? ? ? ?// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) ? ? ?if ((p = tab[i = (n - 1) & hash]) == null) ? ? ? ? ?tab[i] = newNode(hash, key, value, null); ? ? ?// 桶中已经存在元素 ? ? ?else { ? ? ? ? ?Node<K,V> e; K k; ? ? ? ? ?// 步骤③:节点key存在,直接覆盖value ? ? ? ? ?// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 ? ? ? ? ?if (p.hash == hash && ? ? ? ? ? ? ((k = p.key) == key || (key != null && key.equals(k)))) ? ? ? ? ? ? ? ? ?// 将第一个元素赋值给e,用e来记录 ? ? ? ? ? ? ? ? ?e = p; ? ? ? ? ?// 步骤④:判断该链为红黑树 ? ? ? ? ?// hash值不相等,即key不相等;为红黑树结点 ? ? ? ? ?else if (p instanceof TreeNode) ? ? ? ? ? ? ?// 放入树中 ? ? ? ? ? ? ?e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); ? ? ? ? ?// 步骤⑤:该链为链表 ? ? ? ? ?// 为链表结点 ? ? ? ? ?else { ? ? ? ? ? ? ?// 在链表最末插入结点 ? ? ? ? ? ? ?for (int binCount = 0; ; ++binCount) { ? ? ? ? ? ? ? ? ?// 到达链表的尾部 ? ? ? ? ? ? ? ? ?if ((e = p.next) == null) { ? ? ? ? ? ? ? ? ? ? ?// 在尾部插入新结点 ? ? ? ? ? ? ? ? ? ? ?p.next = newNode(hash, key, value, null); ? ? ? ? ? ? ? ? ? ? ?// 结点数量达到阈值,转化为红黑树 ? ? ? ? ? ? ? ? ? ? ?if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st ? ? ? ? ? ? ? ? ? ? ? ? ?treeifyBin(tab, hash); ? ? ? ? ? ? ? ? ? ? ?// 跳出循环 ? ? ? ? ? ? ? ? ? ? ?break; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ?// 判断链表中结点的key值与插入的元素的key值是否相等 ? ? ? ? ? ? ? ? ?if (e.hash == hash && ? ? ? ? ? ? ? ? ? ? ((k = e.key) == key || (key != null && key.equals(k)))) ? ? ? ? ? ? ? ? ? ? ?// 相等,跳出循环 ? ? ? ? ? ? ? ? ? ? ?break; ? ? ? ? ? ? ? ? ?// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 ? ? ? ? ? ? ? ? ?p = e; ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? ?// 表示在桶中找到key值、hash值与插入元素相等的结点 ? ? ? ? ?if (e != null) { ? ? ? ? ? ? ?// 记录e的value ? ? ? ? ? ? ?V oldValue = e.value; ? ? ? ? ? ? ?// onlyIfAbsent为false或者旧值为null ? ? ? ? ? ? ?if (!onlyIfAbsent || oldValue == null) ? ? ? ? ? ? ? ? ?//用新值替换旧值 ? ? ? ? ? ? ? ? ?e.value = value; ? ? ? ? ? ? ?// 访问后回调 ? ? ? ? ? ? ?afterNodeAccess(e); ? ? ? ? ? ? ?// 返回旧值 ? ? ? ? ? ? ?return oldValue; ? ? ? ? } ? ? } ? ? ?// 结构性修改 ? ? ?++modCount; ? ? ?// 步骤⑥:超过最大容量 就扩容 ? ? ?// 实际大小大于阈值则扩容 ? ? ?if (++size > threshold) ? ? ? ? ?resize(); ? ? ?// 插入后回调 ? ? ?afterNodeInsertion(evict); ? ? ?return null; ?} 1.调用put方法实际上是调用putVal方法,如果Node<K,V>数组为空时,先进行调用resize()方法扩容。 2.根据h=key.hashCode()) ^ (h >>> 16)得到key的hash值,然后通过index=(n-1)&h计算得出索引位置。 3.根据索引找到key存放的位置,有三种情况。 ①.该位置没有值,用该数据新生成一个节点保存新数据,返回null。 ②.如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作。 ③.如果该位置是一个链表,分为两种情况。1.如果有这个节点则更新覆盖值,如果没有这个节点则在链表的尾部插入这个节点并保存数据,返回老数据。 4.如果++size > threshold则进行扩容操作。 1.2HashMap的扩容resize()方法 ?final Node<K,V>[] resize() { ? ? ?Node<K,V>[] oldTab = table;//oldTab指向hash桶数组 ? ? ?int oldCap = (oldTab == null) ? 0 : oldTab.length; ? ? ?int oldThr = threshold; ? ? ?int newCap, newThr = 0; ? ? ?if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空 ? ? ? ? ?if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值 ? ? ? ? ? ? ?threshold = Integer.MAX_VALUE; ? ? ? ? ? ? ?return oldTab;//返回 ? ? ? ? }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16 ? ? ? ? ?else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && ? ? ? ? ? ? ? ? ? oldCap >= DEFAULT_INITIAL_CAPACITY) ? ? ? ? ? ? ?newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold ? ? } ? ? ?else if (oldThr > 0) // initial capacity was placed in threshold ? ? ? ? ?newCap = oldThr; ? ? ?else { ? ? ? ? ? ? ? // zero initial threshold signifies using defaults ? ? ? ? ?newCap = DEFAULT_INITIAL_CAPACITY; ? ? ? ? ?newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); ? ? } ? ? ?if (newThr == 0) { ? ? ? ? ?float ft = (float)newCap * loadFactor; ? ? ? ? ?newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? ? ? ? ? ? ? ? ? ? (int)ft : Integer.MAX_VALUE); ? ? } ? ? ?threshold = newThr; ? ? ?@SuppressWarnings({"rawtypes","unchecked"}) ? ? ? ? ?Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组 ? ? ?table = newTab;//将新数组的值复制给旧的hash桶数组 ? ? ?if (oldTab != null) {//进行扩容操作,复制Node对象值到新的hash桶数组 ? ? ? ? ?for (int j = 0; j < oldCap; ++j) { ? ? ? ? ? ? ?Node<K,V> e; ? ? ? ? ? ? ?if ((e = oldTab[j]) != null) {//如果旧的hash桶数组在j结点处不为空,复制给e ? ? ? ? ? ? ? ? ?oldTab[j] = null;//将旧的hash桶数组在j结点处设置为空,方便gc ? ? ? ? ? ? ? ? ?if (e.next == null)//如果e后面没有Node结点 ? ? ? ? ? ? ? ? ? ? ?newTab[e.hash & (newCap - 1)] = e;//直接对e的hash值对新的数组长度求模获得存储位置 ? ? ? ? ? ? ? ? ?else if (e instanceof TreeNode)//如果e是红黑树的类型,那么添加到红黑树中 ? ? ? ? ? ? ? ? ? ? ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); ? ? ? ? ? ? ? ? ?else { // preserve order ? ? ? ? ? ? ? ? ? ? ?Node<K,V> loHead = null, loTail = null; ? ? ? ? ? ? ? ? ? ? ?Node<K,V> hiHead = null, hiTail = null; ? ? ? ? ? ? ? ? ? ? ?Node<K,V> next; ? ? ? ? ? ? ? ? ? ? ?do { ? ? ? ? ? ? ? ? ? ? ? ? ?next = e.next;//将Node结点的next赋值给next ? ? ? ? ? ? ? ? ? ? ? ? ?if ((e.hash & oldCap) == 0) {//如果结点e的hash值与原hash桶数组的长度作与运算为0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?if (loTail == null)//如果loTail为null ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?loHead = e;//将e结点赋值给loHead ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?else ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?loTail.next = e;//否则将e赋值给loTail.next ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?loTail = e;//然后将e复制给loTail ? ? ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? ? ? ?else {//如果结点e的hash值与原hash桶数组的长度作与运算不为0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?if (hiTail == null)//如果hiTail为null ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?hiHead = e;//将e赋值给hiHead ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?else ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?hiTail.next = e;//如果hiTail不为空,将e复制给hiTail.next ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?hiTail = e;//将e复制个hiTail ? ? ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? } while ((e = next) != null);//直到e为空 ? ? ? ? ? ? ? ? ? ? ?if (loTail != null) {//如果loTail不为空 ? ? ? ? ? ? ? ? ? ? ? ? ?loTail.next = null;//将loTail.next设置为空 ? ? ? ? ? ? ? ? ? ? ? ? ?newTab[j] = loHead;//将loHead赋值给新的hash桶数组[j]处 ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? ?if (hiTail != null) {//如果hiTail不为空 ? ? ? ? ? ? ? ? ? ? ? ? ?hiTail.next = null;//将hiTail.next赋值为空 ? ? ? ? ? ? ? ? ? ? ? ? ?newTab[j + oldCap] = hiHead;//将hiHead赋值给新的hash桶数组[j+旧hash桶数组长度] ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } ? ? } ? ? ?return newTab; ?} ?? ?? 1.2.1 resize()总结 1.调用put()方法,如果Node<K,V>数组为空则需调用resize()方法。 2.resize()方法中,如果oldCap>0,则newCap设置为oldCap的2倍,newCap扩容后仍然小于最大容量并且oldCap大于默认值16,则阈值设置为之前的2倍。如果oldCap为空则newCap设置为默认的16,阈值为12。 3.如果Node<K,V>数组不为空,循环Node<K,V>数组。有三种情况: 3.1 若遍历的当前节点e.next == null,则通过e.hash & (newCap - 1)获得索引,将节点放入该索引。 3.2 如果当前节点是红黑树的类型,那么添加到红黑树中。 3.3 如为链表,则循环链表,若(e.hash & oldCap) == 0则将节点放在低位,否则放在高位。高位索引为之前的索引+旧hash桶数组长度。 1.3 HashMap初始容量为什么是2的n次幂及扩容为什么是2倍 1.因为HashMap通过(n-1)& hash 算法计算添加元素的位置时,假设n=16,则n-1的二进制位0000 1111有更多的1, 可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表结构,影响查询效率。 2.ArrayList2.1ArrayList的扩容机制 2.1.1 先来看看add方法 ?// 将指定的元素追加到此列表的末尾。 ?? ?public boolean add(E e) { ? ? //添加元素之前,先调用ensureCapacityInternal方法 ? ?ensureCapacityInternal(size + 1); ?// Increments modCount!! ?//这里看到ArrayList添加元素的实质就相当于为数组赋值 ? ? elementData[size++] = e; ? ? return true; ?} 2.1.2 ensureCapacityInternal() 方法 可以看到 add 方法 首先调用了ensureCapacityInternal(size + 1) ?//得到最小扩容量 ? ? ?private void ensureCapacityInternal(int minCapacity) { ? ? ? ? ?if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { ? ? ? ? ? ? ? ?// 获取默认的容量和传入参数的较大值 ? ? ? ? ? ? ?minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); ? ? ? ? } ?? ? ? ?ensureExplicitCapacity(minCapacity); ?} ?? 2.1.3 ensureExplicitCapacity() 方法 ?//判断是否需要扩容 ? ? ?private void ensureExplicitCapacity(int minCapacity) { ? ? ? ? ?modCount++; ? ? ?// overflow-conscious code ? ? ? if (minCapacity - elementData.length > 0) ? ? ? ? ?//调用grow方法进行扩容,调用此方法代表已经开始扩容了 ? ? ? ? ?grow(minCapacity); ?} 2.1.4 grow() 方法 ?/** ? * ArrayList扩容的核心方法。 ? */ ?private void grow(int minCapacity) { ? ? ?// oldCapacity为旧容量,newCapacity为新容量 ? ? ?int oldCapacity = elementData.length; ? ? ?//将oldCapacity 右移一位,其效果相当于oldCapacity /2, ? ? ?//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, ? ? ?int newCapacity = oldCapacity + (oldCapacity >> 1); ? ? ?//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, ? ? ?if (newCapacity - minCapacity < 0) ? ? ? ? ?newCapacity = minCapacity; ? ? // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, ? ? //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 ? ? ?if (newCapacity - MAX_ARRAY_SIZE > 0) ? ? ? ? ?newCapacity = hugeCapacity(minCapacity); ? ? ?// minCapacity is usually close to size, so this is a win: ? ? ?elementData = Arrays.copyOf(elementData, newCapacity); ?} 2.1.5 总结 1.调用add方法添加元素之前,首先会调用ensureCapacityInternal() 方法,如果数组为空,则最小扩容量为默认的容量和传入参数的较大值,接着调用ensureExplicitCapacity(minCapacity)方法。 2.在ensureExplicitCapacity(minCapacity) 方法中,如果最小扩容量minCapacity大于数组长度则调用grow()方法。 3.在grow()方法中定义oldCapacity和newCapacity两个参数,newCapacity=oldCapacity + (oldCapacity >> 1)新容量为旧容量的1.5倍。如果新容量还是小于最小扩容量,则把最小需要容量当作数组的新容量。如果minCapacity大于最大容量,则新容量则为 4.最后通过Arrays.copyOf(elementData, newCapacity)方法给原有数组扩容。 Arrays.copyOf() 内部实际调用了 System.arraycopy() 方法。 三、多线程3.1 线程和进程1.进程是程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即一个进程的创建、运行到消亡。 2.一个进程中可以产生多个线程。 3.线程与进程相似,但线程是比进程更小的执行单位。多个线程可以共享进程的堆和方法区,但每个线程有自己的程序计算器、虚拟机栈和本地方法栈。 3.2 并发和并行1.并发:同一时间段,多个任务都在执行。 2.并行:单位时间内吗,多个任务同时执行。 3.3 什么是上下?切换1.-当前任务执行完时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以加载这个任务的状态。任务从保存到再次加载的过程就是一次上下文切换。 3.4 什么是线程死锁?1.线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,所以这两个线程会进入相互等待,从而死锁。 3.5 如何避免死锁3.6 双重校验锁实现单例模式?/** ? * @author chenw ? * date 2021/4/16 9:36 ? */ ?public class Singleton { ? ? ?private static volatile Singleton singleton = null; ?? ? ? ?private Singleton() { ?? ? ? } ?? ? ? ?public static Singleton getSingleton() { ? ? ? ? ?// 第一次判断 ? ? ? ? ?if (singleton == null) { ? ? ? ? ? ? ?synchronized (Singleton.class) { ? ? ? ? ? ? ? ? ?// 第二次判断 ? ? ? ? ? ? ? ? ?if (singleton == null) { ? ? ? ? ? ? ? ? ? ? ?singleton = new Singleton(); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? ?return singleton; ? ? } ?? ? ? ?public static void main(String[] args) { ? ? ? ? ?for (int i = 0; i < 100; i++) { ? ? ? ? ? ? ?new Thread(() -> { ? ? ? ? ? ? ? ? ?System.out.println(Thread.currentThread().getName() + ":" + Singleton.getSingleton().hashCode()); ? ? ? ? ? ? }).start(); ? ? ? ? } ?? ? ? } ?} 为什么是双重校验锁实现单例模式呢? 第一次校验:也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。 第二次校验:也就是第二个if(singletonnull),这个校验是防止二次创建实例,假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singletonnull,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。 还有,这里的private static volatile Singleton singleton = null;中的volatile也必不可少,volatile关键字可以防止jvm指令重排优化, 因为 singleton = new Singleton() 这句话可以分为三步: 1.为 singleton 分配内存空间; 2.初始化 singleton; 3.将 singleton 指向分配的内存空间。 是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。 使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。 3.7 ReentrantLock原理3.8 ThreadLocal原理四、MySQL4.1 ?条SQL语句在MySQL中如何执?的4.2 ?条SQL语句执?得很慢的原因有哪些?4.3 后端程序员必备:书写?质量SQL的30条建议五、Redis5.1 为啥 redis 单线程模型也能效率这么高?1.纯内存操作 2.核心是基于非阻塞的IO多路复用机制。 3.单线程反而避免了多线程的上下文切换的问题。 5.2 redis 的线程模型1.redis 内部使用文件事件处理器 文件事件处理器的结构包含 4 个部分:
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。 5.3 redis 和 memcached 的区别1.Redis支持更丰富的数据类型,如list,set,zset,hash。而memcached支持简单的数据类型string。 2.Redis支持数据的持久化。 3.Redis是单线程的IO多路复用模型,而memcached是多线程的非阻塞IO复用的网络模型。 5.4 Redis的缓存雪崩、缓存穿透、缓存击穿1.缓存雪崩:当缓存在同一时间内大面积失效,刚好这个时候 Redis 请求的并发量又很大,就会导致所有的请求落到数据库,导致数据库压力过重。 缓存雪崩的解决方案: 1)加互斥锁或者使用队列,针对同一个 key 只允许一个线程到数据库查询。 2)缓存定时预先更新,避免同时失效。 3)通过加随机数,使 key 在不同的时间过期。 4)缓存永不过期。 2.缓存穿透:数据在数据库和 Redis 里面都不存在,可能是一次条件错误的查询。因为数据库值不存在,所以肯定不会写入 Redis,那么下一次查询相同的 key 的时候,肯定还是会再到数据库查一次。 缓存穿透解决方案: 1)使用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。 2)缓存空数据,如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 3.缓存击穿:针对一个key缓存失效,此时大量的并发请求,会导致从数据库请求,导致数据库压力过重。与缓存雪崩的区别是,缓存雪崩针对的是多个key,缓存击穿是一个key。 缓存击穿的解决方案: 1.使用互斥锁(mutex key) 六、spring6.1 列举?些重要的Spring模块?1.Spring Core: 基础,Spring所以的功能都依赖于该类库,主要提供IOC依赖注入功能。 2.Spring Aspects:该模块为与AspectJ的集成提供支持。 3.Spring AOP:提供了面试切面编程的实现。 4.Spring JDBC:Java数据库连接。 5.Spring Web:为创建web应用程序提供支持。 6.Spring Test:提供了测试的支持。 7.Spring JMS:Java消息服务。 8.Spring ORM:用于支支持Hibernate等ORM工具。 6.2 Spring IOCIOC控制反转:所谓控制反转,就是把原先我们代码里面需要实现的对象创建、依赖的代码,反转给容器来帮忙实现。 IOC的流程:例如是XML定义Bean的,XML配置被转化成Document,Document最终被解析成 BeanDefinition,将 BeanDefinition注册到 IOC 容器中,实际上这个IOC容器是一个HashMap。 6.3 Spring DIDI(Dependency Injection)依赖注入:就是指对象是被动接受依赖类而不是自己主动去找,换句话说就 是指对象不是从容器中查找它依赖的类,而是在容器实例化对象的时候主动将它依赖的类注入给它。 6.4 Spring AOPSpring AOP:面向切面编程,可以通过动态代理在不修改源代码的情况下给程序动态统一添加功能的一种技术。 Spring AOP原理是:如果代理对象实现了某个接口,则使用JDK动态代理,否则使用CGLib 代理。最后调用invoke()方法 ,获取方法上的拦截链,若拦截链为空,则直接反射调用方法,如不为空,则执行拦截链方法和反射调用方法。 6.5 Spring 中的 bean 的作?域有哪些?1.singleton : 唯? bean 实例,Spring 中的 bean 默认都是单例的。 2.prototype : 每次请求都会创建?个新的 bean 实例。 3.request : 每?次HTTP请求都会产??个新的bean,该bean仅在当前HTTP request内有 效。 4.session : 每?次HTTP请求都会产??个新的 bean,该bean仅在当前 HTTP session 内有 效。 5.global-session: 全局session作?域,仅仅在基于portlet的web应?中才有意义,Spring5已 经没有了。Portlet是能够?成语义代码(例如:HTML)?段的?型Java Web插件。它们基于 portlet容器,可以像servlet?样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不 同的会话 6.6 @Component 和 @Bean 的区别是什么?1.作?对象不同: @Component 注解作?于类,? @Bean 注解作?于?法。 2.@Bean注解比@Component注解的自定义性更强 ,例如当引用第三方类库装配到Spring容器,就只能使用@Bean注解了。 3.@Component 通常是通过类路径扫描来?动装配到Spring容器中 ,而@Bean通常和@Configuration 讲返回对象装配到容器中。 6.7 将?个类声明为Spring的 bean 的注解有哪些?
6.8 Spring 中的 bean ?命周期?6.9 Spring MVC?作原理Spring MVC工作流程(重要) 1.浏览器发送request请求到DispatcherServlet。 2.DispatcherServlet根据HandlerMapping,拿到对应的Handler处理器(controller)。 3.DispatcherServlet会调用HandlerAdapter 适配器去执行Handler来处理对应的业务逻辑。 4.处理完业务后,返回ModelAndView对象给DispatcherServlet,Model 是返回的数据对 象, View 是个逻辑上的View 。 5.ViewResolver视图解析器会根据逻辑View查找实际的View。 6.DispaterServlet 把返回的 Model 传给 View(视图渲染)。 7.最后把view返回给浏览器。 6.11 Spring中使用的设计模式
七、MyBatis7.1 {}和${}的区别
7.2 resultType 和 resultMap 的区别7.3 通常?个 Xml 映射?件,都会写?个 Dao 接? 与之对应,请问,这个 Dao 接?的?作原理是什么?Dao 接??的?法,参数不同时,?法能重载吗?答:Dao 接?,就是?们常说的 Mapper 接?,接?的全限名,就是映射?件中的 namespace 的值,接?的?法名,就是映射?件中 MappedStatement 的 id 值,接??法内的参数,就是传递给 sql 的参数。 Mapper 接?是没有实现类的,当调?接??法时,接?全限名+?法名拼接字符串作为 key 值,可唯?定位?个 MappedStatement ,举 例: com.mybatis3.mappers.StudentDao.findStudentById ,可以唯?找到namespace 为 com.mybatis3.mappers.StudentDao 下? id = findStudentById 的 MappedStatement 。在 Mybatis 中,每?个 ?<select> 、 <insert> 、 <update> 、 <delete> 标签,都会被解析为? 个MappedStatement 对象。 Dao 接??的?法,是不能重载的,因为是全限名+?法名的保存和寻找策略。 Dao 接?的?作原理是 JDK 动态代理,Mybatis 运?时会使? JDK 动态代理为 Dao 接??成代理 proxy 对象,实际上是调用invoke()方法,在invoke()方法通过调用sqlSession.selectOne()方法执行sql。 7.4 Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复; 原因就是namespace+id是作为Map<String, MapperStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 7.5 MyBatis 插件怎么编写和使用?原理是什么?使用:继承 Interceptor 接口,加上注解,在 mybatis-config.xml 中配置 原理:动态代理,责任链模式,使用 Plugin 创建代理对象 在被拦截对象的方法调用的时候,先走到 Plugin 的 invoke()方法,再走到 Interceptor 实现类的 intercept()方法,最后通过 Invocation.proceed()方法调用被 拦截对象的原方法。 7.6 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 延迟加载的基本原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 7.7 MyBatis 一级缓存与二级缓存的区别
7.8 Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?<resultMap>、<parameterMap>、<sql>、<include>、<selectKey> ,加上动态sql的9个标签,其中 <sql> 为sql片段标签,通过 <include> 标签引入sql片段, <selectKey> 为不支持自增的主键生成策略标签。 八、RabbitMQ8.1 为什么要使用MQ?
8.2 为什么要使用RabbitMQ?
8.3 使用RabbitMQ的场景
8.4 如何避免消息重复投递或重复消费?
8.5 消息基于什么传输?
8.6 如何确保消息不丢失?消息持久化,当然前提是队列必须持久化 RabbitMQ确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit会将消息提交到日志文件后才发送响应。 一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前RabbitMQ重启,那么Rabbit会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列。 8.7 常用的交换机有哪些?1.Direct Exchange
2.Fanout Exchange
3.Topic Exchange
九、SpringCloud9.1 Ribbon综上所述 , Ribbon 的 负载均衡主要是通过 LoadBalancerClient 来实现的,而 LoadBalancer-Client 具体交给了 ILoadBalancer 来处理, ILoadBalancer 通过配置 !Rule、 !Ping 等,向 EurekaClient 获取注册列表的信息 ,默认每 10 秒向 EurekaClient 发送一次“ ping ”, 进而检查是否需要更新 服务的注册列表信息 。最后 ,在得到服务注册列表信息后, ILoadBalancer 根据!Rule 的策略进 行负载均衡。 而 RestTemplate 加上@LoadBalance 注解后,在远程调度时能够负载均衡, 主要是维护了 一个被@LoadBalance 注解的 RestTemplate 列表,并给该列表中 的 RestTemplate 对象添加了 拦 截器。在拦截器的方法中 ,将远程调度方法交给了 Ribbon 的 负载均衡器 LoadBalancerClient 去处理 ,从而达到了负载均衡的目的 。 9.2 Feign总的来说,Feign 的源码实现过程如下。 ( 1) 首先通过@EnableFeignClients 注解开启 FeignClient 的功能 。只有这个注解存在,才 会在程序启动时开启对@FeignClient 注解的包扫描 。 (2)根据Feign 的规则实现接口,井在接口上面加上@FeignClient注解 。 (3 )程序启动后,会进行包扫描,扫描所有的@ FeignClient的注解 的类 ,并将这些信息 注入 IoC 容器中。 (4 )当接口的方法被调用时,通过JDK 的代理来生成具体的 RequestTemplate 模板对象。 (5 )根据 RequestTemplate 再生成 Http 请求的 Request 对象 。 ( 6 ) Request 对象交给 Client 去处理,其中Client 的网络请求框架可以是 HttpURLConnection、HttpClient 和 OkHttp。 (7)最后Client被封装到LoadBalanceClient类,这个类结合类 Ribbon做到了负载均衡 。 十、JVM10.1 JVM类加载机制
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种: 1.创建类的实例,也就是new的方式 2.访问某个类或接口的静态变量,或者对该静态变量赋值 3.调用类的静态方法 4.反射(如Class.forName(“com.shengsiyuan.Test”)) 5.初始化某个类的子类,则其父类也会被初始化 6.Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 | -2025/1/31 11:05:22- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |