面试八股
JAVA
Day1√
String、StringBuffer、StringBuilder的区别?**
这三者的区别主要集中在不可变性、线程安全和性能方面。
- String是不可变且线程安全的,因为String类型的字符串一旦创建就不可修改,因此,String类型的字符串是线程安全的,可以由多个线程同时调用,但是一旦对它有修改操作则会创建新的字符串对象,这会影响性能;
- StringBuffer是可变且线程安全的,它在修改时是在同一对象上进行的,不会创建新的对象;因为StringBuffer的方法是同步的,所以它在使用时是线程安全的。由此可见StringBuffer适用于多线程环境,在单线程环境中使用会产生额外的性能开销;
- StringBuilder是可变但线程不安全的,它的方法没有同步,因此线程不安全,适用于单线程环境的字符串操作。
总之,String适用于不经常修改字符串的情况,StringBuffer和StringBuilder适用于经常修改的情况,并且要根据是否需要线程安全以及性能考量来选择。
接口和抽象类的区别?
相同点:都不能直接被实例化,需要继承或实现后才能使用
不同点:
从定义上说:接⼝是⼀种抽象类型,它定义了⼀组方法(方法签名)但没有实现任何⽅法的具体代码。接口中的方法默认是抽象的,且接口中只能包含常量(static final变量)和抽象方法。抽象类是⼀个类,可以包含抽象方法和具体方法。抽象类中的抽象方法是没有实现的方法,而具体方法则包含实现代码。
定义的关键字不同:定义的关键字不同(abstract/Interface),子类继承或实现的关键字也不同(extends/implements)。
类型扩展不同:抽象类是单继承,而接口是多实现。
构造器:接口没有构造器,抽象类可以有构造器,当子类实例化时,会调用父类的构造器。
访问修饰符:接口中的方法默认是public abstract,变量默认是public static final;抽象类中的抽象方法默认是protected,并且不能被private修饰。
设计目的:
接口用于定义规范,强调“行为”或“能力”。
抽象类用于代码复用,提供通用的实现或基础功能,并且可以包含方法的具体实现。
Java常见的异常类有哪些?**
Java的异常类主要分为两大类:Error 和 Exception。Error表示程序无法处理的异常,通常是由JVM或底层系统引起的,如内存溢出或系统错误,而Exception则表示程序可以处理的异常,通常由程序员编写的代码引起的,并且可以通过异常处理机制捕获和处理。
Exception又分为两种主要类型:
- Checked Exception(受检异常):是指在编译时必须进行处理的异常。如果有一个方法抛出这类异常,则要么在方法类使用try-catch块捕获,要么在方法签名中使用throws关键字声明该异常。常见的Checked Exception包括IOException、SQLException、ClassNotFoundException、InterruptException等。
- Unchecked Exception(非受检异常,也称运行时异常):是指在编译时不需要进行处理的异常,他们通常是由程序的逻辑错误引起的。例如NullPointerException、ArrayIndexOutOfBoundsException等。
Day2√
说一说Java面向对象三大特性?***
- 封装:指隐藏对象的内部细节,封装将对象内部状态(字段、属性)隐藏起来,并通过定义公共的方法(接口)来操作对象,外部代码只需要知道如何使用这些方法而无需了解内部实现,提高了代码的安全性和可维护性。
- 继承:是一种通过已有的类(父类)创建新类(子类)的方式。子类可以继承父类的属性和方法,并且可以通过重写父类的方法来改变或扩展其行为,提高了代码的可重用性和可扩展性。
- 多态:是指相同的操作或方法可以在不同的对象上产生不同的行为,通过方法的重载和重写来实现,提高了代码的灵活性。
说一说你对Java多态的理解?**
多态有两种形式:编译时多态和运行时多态。
- 编译时多态也称为静态多态,是通过方法的重载实现。编译器在编译时根据方法的签名(方法名和参数列表)来选择调用合适的方法。
- 运行时多态也称为动态多态,是通过方法的重写实现。在运行时,通过对象的实际类型来确定调用的方法。
重写和重载的区别?**
重写和重载都是实现多态的方式,其中,重写是运行时多态,重载是编译时多态。
- 重写发生在子类和父类之间,重写要求子类重写的方法与父类被重写的方法具有相同的参数列表(即参数类型、数量、顺序相同)和相同的返回类型。
- 重载是指在一个类中定义多个同名方法,这些方法的参数列表不同,返回类型可以相同也可以不相同。
Day3√
static和final的作用?**
- static用于声明静态成员,即静态变量和方法。其中静态变量属于类而不属于实例对象,所有实例对象都共享相同的静态变量。静态方法也一样,属于类,可以通过类名直接调用而不需要创建类的实例。
- final用于修饰不可改变的变量、方法和类。对于变量,一旦赋值后就不可修改;对于方法,表示不可被子类重写;对于类,表示该类不能被继承。
java 中 == 和 equals() 的区别?**
- “==”运算符是比较两个对象的引用(内存地址),即检查它们是否指向相同的内存地址。
- **equals()**通常用于比较两个对象的内容。默认情况下equals()也是比较对象的引用,但是很多类都重写了这个方法以比较对象的内容。
为什么重写equals()时也要重写hashcode()方法?***
因为原始的equals()是比较两个对象的引用,但是我们经常用它比较两个对象是否一样,所以通常需要重写equals()。hashcode()方法返回的是对象的哈希码值,用于确定对象在哈希表中的存储位置。在集合类中,如果两个对象被认为是相等的,那么它们的哈希码应该相同。但是哈希码相等的两个对象不一定相等。遵守以下规定:
- 两个对象是相等的,即equals()返回为true,那么它们的哈希码也一定相同;
- 两个对象的哈希码相同,但这两个对象不一定相等。
Day4√
Java的集合类有哪些,哪些是线程安全的,哪些是线程不安全的?***
Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection又派生出Set、List、Queue这三个子接口。所有Java集合类都是Set、List、Queue、Map这四个接口的实现类。其中,
List接口:有序集合,允许重复元素。常见实现类有ArrayList、LinkedList、Vector等;
Set接口:不允许重复元素的集合。常见实现类有HashSet、LinkedHashSet、TreeSet等;
Queue接口:表示队列的数据结构。常见的实现类有LinckedList、PriorityQueue等;
Map接口:表示键值对的集合。常见实现类有HashMap、LinkedHashMap、TreeMap、Hashtable等。
线程安全的集合包括Vector、Hashtable、ConcurrentHashMap、Collections.synchronizedList、Collections.synchronizedSet 、Collections.synchronizedMap。
ArrayList 和 Array 有什么区别?***
- 大小和自动扩容
- Array数组在创建时必须指定大小,且大小是固定的。一旦数组被创建,其大小不能更改;
- ArrayList是动态数组实现的,它的大小可以动态增长或缩小。在不断添加元素时,ArrayList 会自动进行扩容。
- 支持泛型
- Array数组可以存储任何类型的元素,但不支持泛型;
- ArrayList支持泛型,也可以指定存储的元素类型。
- 存储对象
- Array数组可以直接存储基本类型数据,也可以存储对象;
- ArrayList中只能存储对象。对于基本类型数据,需要使用其对应的包装类。
- 集合功能
- Array数组是一个简单的数据结构,不提供额外的方法来对元素进行增删查改操作;
- ArrayList是集合框架的一部分,提供了丰富的方法,如添加、删除、查找等。
ArrayList 和 LinkedList 的区别是什么?***
- 内部数据结构
- ArrayList基于动态数组实现;
- LinkedList基于双向链表实现。
- 遍历性能
- ArrayList支持快速的随机访问和遍历,因为可以直接通过索引访问元素;
- LinkedList随机访问性能较差,因为必须从链表的头部或尾部开始遍历寻找目标索引。
- 插入和删除
- ArrayList在末尾进行插入和删除操作是高效的,但在中间或开头插入和删除需要移动元素,性能较差。
- LinkedList插入和删除元素的性能相对较好,特别是在链表中间或头尾插入和删除元素时。
- 使用场景
- ArrayList适用于需要频繁随机访问元素,而对插入和删除操作要求不那么严格的场景。
- LinkedList适用于需要频繁插入和删除操作,而对随机访问的需求较少的场景。
ArrayList的扩容机制?***
ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。(不是原数组,而是新数组然后给予数组对象地址)。
默认情况下,新的容量会是原容量的1.5倍。新容量=旧容量右移一位(相当于除于2)在加上旧容量
ArrayList 的底层是用动态数组来实现的。我们初始化一个ArrayList 集合并且没有指定容量时,其实它是个空数组,只有当我们添加第一个元素时,会初始化容量为10。当添加第11个元素时,便开始可以扩容了,新数组大小是旧数组的1.5倍,然后将旧数组的元素copy到新数组中,最后将ArrayList内部指向旧数组的引用更新为指向新数组的引用。
Day5√
Java中的HashMap了解吗?HashMap 的底层实现是什么?***
在Java中,HashMap是Map接口的实现类,其数据是以键值对的形式存储,Key只允许有一个null值,value可以有多个null值;是线程不安全的。
在JDK1.8之前HashMap底层是由数组+链表的形式构成的;JDK1.8之后,底层是由数组+链表/红黑树构成的。
解决 Hash 冲突的方法有哪些?HashMap 是如何解决 hash 冲突的?***
解决hash冲突一般有开放定址法、链地址法、再哈希法、建立公共溢出区,其中HashMap使用链地址法解决哈希冲突。具体如下:
- 开放定址法:遇到hash冲突时,去寻找一个新的且空闲的哈希地址。寻找新的位置有以下几种方法:
线性探测:当发生冲突时,线性地检查下一个槽位直到找到空闲地位置。(问题:会出现元素堆积现象)
二次探测:与线性探测法不同的是每次增加的步长为平方数,以减少聚集现象。
双重哈希:第一个哈希函数计算原始哈希值,使用第二个哈希函数来计算增量步长。发生冲突时,使用第 一个哈希函数计算得到的值加上第二个哈希函数得到的步长来计算下一个探测位置。
随机探测:使用随机的方式选择下一个探测位置。
二次探测的计算公式:(h1(key)+i⋅i) mod M
双重哈希的计算公式:(h1(key)+i⋅h2(key)) mod M,其中M 是哈希表的大小,i 是冲突次数。
- 再哈希法:构造多个不同的哈希函数,当发生冲突时就是用第二个、第三个。。。直到不发生冲突为止。
- 链地址法:将所有哈希地址相同的元素都连接在一条链表中。
- 建立公共溢出区:将哈希表分为基本表和溢出表,将发生哈希冲突的元素都放在溢出表中。
HashMap 的 put 方法流程?***
首先使用键所在类的hashCode()方法计算键的哈希码,然后对该哈希码进行扰动处理以减少哈希冲突的可能性。然后使用哈希码和当前哈希表的长度计算键值应该放入的位置。如果该位置为空,那么就创建一个新的键值对节点将元素放在此处。如果不为空,就需要遍历该位置上的链表,检查要插入的键是否已经存在。如果要插入的键存在,就修改键对应的值;如果要插入的键不存在,就在链表的末尾添加新的键值对节点。
HashMap的扩容机制?***
HashMap的扩容机制在jdk1.8的前后有些许不同。
** jdk1.8 之前要满足以下两个条件:**
- 存放新值的时候当前已有的元素个数必须大于等于一个阈值,该阈值等于当前table的容量 * 负载因子,初始化时table的长度为16,负载因子为0.75,那么阈值就是12。
- 存放新值的时候发生哈希碰撞。
特点:先判断扩容,再添加(扩容使用头插法)
jdk1.8之后满足以下条件之一:
- 当前存储的数量大于等于阈值。
- 当某个链表长度 >= 8,但是数组的长度 < 64时。
特点:先添加,再判断扩容(扩容使用尾插法)
扩容之后对table的调整:
扩容会创建一个新的数组,长度为原来的两倍。然后重新计算元素的位置并插入。
链表与红黑树相互转换的条件:
链表转红黑树:某个链表长度 > 8,且HashMap的数组长度 >= 64。
红黑树转链表:1. 扩容resize()时,红黑树拆分成的树的节点数 <= 6 个,则退化成链表。2. 删除元素remove()时,如果红黑树根节点root为空,或者root的左/右子树为空,root.left.left为空,都会发生红黑树退化成链表。
Day6√
HashMap 为什么是线程不安全的?如何实现线程安全?***
主要原因是它的操作不是原子的,在多线程环境下,可能出现数据不一致的情况。
实现线程安全的HashMap,有以下几种方式:
- 使用Hashtable代替Hashmap。
- 使用Map map = Collections.synchronizedMap(new HashMap());
- 使用ConcurrentHashmap。
在jdk1.8之前使用的是分段锁,它将哈希表分成多个段,每个段都有自己的锁。
在jdk1.8之后,ConcurrentHashMap通过CAS+synchronized实现线程安全,相比分段锁,锁的粒度更小。
- 使用ReentrantLock锁来保证线程安全。
concurrentHashMap 如何保证线程安全?**
在jdk1.8之前使用的是分段锁,它将哈希表分成多个段(Segment数组),每个段都由一个ReetrantLock保护,默认Segment数组长度为16,这意味着最多可以支持16个线程并发写。
在jdk1.8之后,ConcurrentHashMap通过Node + CAS + synchronized实现线程安全,synchronized 只锁定当前链表或红黑树的首节点,相比分段锁,锁的粒度更小。
HashMap和ConcurrentHashMap的区别?**
- HashMap不是线程安全的,而ConcurrentHashMap是线程安全的;
- HashMap适用于单线程环境,不需要考虑同步操作;ConcurrentHashMap适用于多线程环境。
Day7√
HashSet 和 HashMap 的区别?**
- 实现接口:HashSet实现Set接口,HashMap实现Map接口;
- 存储类型:HashSet存储无序不重复的元素,HashMap存储无序不重复的键值对;
- 实现方式:HashSet底层使用HashMap实现,HashSet中的每个元素之际上都是HashMap的一个键,每个键映射的值为一个Object类型的静态对象,名为
PRESENT
。
HashMap 和 HashTable 的区别?**
- 线程安全:HashMap线程不安全。Hashtable是线程安全的,因为它在每个方法上添加 synchronized 关键字保证每个操作都是同步的。
- 是否支持 null:HashMap允许一个键和任意数量的值为为null。而Hashtable不允许键或值为null。
- 性能:HashMap因为没有同步开销,在单线程环境中性能更好。尽管Hashtable是同步的,但由于实现的效率低下,它在多线程环境下性能也可能比HashMap底。
- 继承关系:HashMap继承自AbstractMap类,而Hashtable继承自Dictionary类,但它们都实现了Map接口。
- 初始容量和扩容大小:HashMap初始容量为16,每次扩容都为原来的2倍;Hashtable初始容量为11,每次扩容都为原来的2倍+1。
- 迭代器:HashMap使用Iterator迭代器,Hashtable使用Enumerator迭代器,后者在迭代时如果Hashtable被修改,会抛出ConcurrentModificationException异常。
浅拷贝和深拷贝的区别?**
- 浅拷贝是指只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。换句话说,浅拷贝只是创建一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是将引用复制到新对象中,两个对象指向的是同一个引用对象。
- 深拷贝是指在复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。换句话说,深拷贝会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对象。
Day8√
Java创建线程的方式?***
大家都说Java有三种创建线程的方式!并发编程中的惊天骗局! (qq.com)
- 继承自Thread类:创建一个新的类,该类继承Thread类,然后重写run()方法定义要执行的任务,最后调用start()方法启动线程。
- 实现Runnable接口:创建一个新的类,该类实现Runnable接口,然后重写run()方法,最后创建Thread对象时传递Runnable实例,并调用start()方法。
- 实现Callable接口:创建一个新的类,该类实现 Callable 接口,实现 call()方法以定义线程要执行的任务,使用 ExecutorService 和 Future 来获取任务的结果。
- 使用ExecutorService线程池
- 使用CompletableFuture类
总结:这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()创建。不管是哪种方式,最终还是依赖于new Thread().start()。
线程 start 和 run 的区别?**
- start()方法用于启动一个新线程,并且会在新线程中执行run()方法的代码。
- run()方法只是普通的方法调用,不会启动新线程,而是直接在当前线程中执行 run() 方法的内容。
sleep()和wait()的区别?
- 所属类和调用方式
wait()方法属于Object类,需要在synchronized块或方法中调用。
sleep()方法属于Thread类,可以在任何地方调用。 - 对锁的处理机制
wait()方法会释放锁,让出CPU资源,并且线程进入等待状态,直到被其他线程唤醒。
sleep()方法不会释放锁,线程会一直占用CPU资源,只是暂停执行一段时间。 - 唤醒机制
wait()方法需要被notify()或notifyAll()方法唤醒。
sleep()方法会自动苏醒,不需要其他线程唤醒。 - 用途
wait()方法通常用于线程间通信和协作,例如生产者-消费者模型。
sleep()方法通常用于让线程暂停执行一段时间,例如模拟延迟操作。 - 异常处理
wait()方法和sleep() 方法都可能会抛出InterruptedException异常,需要捕获或声明。 - 总结
wait()方法和sleep()方法都让线程暂停执行,但wait()方法会释放锁,sleep()方法不会释放锁。
wait()方法需要被notify()或notifyAll()方法唤醒,sleep()方法会自动苏醒。
wait()方法通常用于线程间通信和协作,sleep()方法通常用于让线程暂停执行一段时间。
Day9√
线程间的通信方式有些?***
线程间的通信方式主要有两种,分别是共享变量和消息传递。详细如下:
- 共享变量:通过共享变量(volatile修饰的变量),多个线程可以访问和修改同一个对象的状态。这种方法要求保证对共享变量的访问是线程安全的,通常通过同步(synchronized)代码块或方法来实现。
- 消息传递:通过Object类的wait()、notify()、notifyAll()方法实现等待/通知机制。这些方法必须在同步代码块或方法中调用,以确保对共享资源的访问是线程安全的。
- ReentrantLock和Condition:ReenTrantLock锁的实现原理虽然和synchronized不同,但是它和synchronized一样都是通过建立线程间的互斥访问临界区,来保证线程安全,实现线程间的通信。相比于synchronized使用Object类的三个方法来实现线程的阻塞和运行两个状态的切换,ReentrantLock使用Condition阻塞队列的await()、signal()、signalAll()三个方法来实现线程阻塞和运行两个状态的切换,进而实现线程间的通信。
你知道Java中有哪些锁吗?***
阿里二面:Java中锁的分类有哪些?你能说全吗? - 码农Academy - 博客园 (cnblogs.com)
- 基于锁的获取与释放方式可以分为显示锁和隐式锁
- 隐式锁:是通过synchronized关键字实现的一种线程同步机制。当一个线程进入被synchronized修饰的方法或代码块时,它会自动获得对象级别的锁,退出该方法或代码块时则会自动释放这把锁。
- 显示锁:显式锁是由 java.util.concurrent.locks.Lock 接口及其诸多实现类提供的同步机制,相较于通过synchronized关键字实现的隐式锁机制,显式锁提供了更为多样化的锁操作选项,包括但不限于支持线程在等待锁时可被中断、根据先后顺序分配锁资源的公平锁与非*公平锁机制,以及能够设定锁获取等待时间的定时锁功能。常见的显示锁有ReentrantLock、ReentrantReadWriteLock、StampedLock等。*
- 基于对资源的访问权限可以分为独占锁和共享锁
- 独占锁:又称为排他锁或写锁,它确保在任一时刻最多只有一个线程可以获得锁并对受保护的资源进行访问或修改。独占锁可以通过synchronized关键字或ReentrantLock实现。
- 共享锁:也成为读锁,允许多个线程同时读取共享资源,但不允许任何线程修改资源。可通过读写锁(ReadWriteLock)中的读锁实现。
- 基于锁的占有权是否可重入分为可重入锁和非可重入锁
- 可重入锁:又称为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁,从而避免了在递归调用或嵌套同步块中产生的死锁风险。其中synchronized实现的隐式锁和ReentrantLock都是可重入锁。
- 不可重入锁:与可重入锁相反。
- 基于锁的公平性可以分为公平锁和非公平锁
- 公平锁:在多线程环境中,锁的分配遵循“先请求先服务”的原则,即按照线程请求锁的顺序分配锁资源。公平锁可以有效避免某个线程长期得不到锁而导致的饥饿现象。在实现时可以通过向ReentrantLock中传入true参数获得。
- 非公平锁:与公平锁相反,不遵循“先请求先服务”的原则。非公平锁在某些场景下可以提高系统的并发性能,因为它允许刚释放锁的线程或者其他新到达的线程立刻获取锁,而不是强制排队等待,避免了线程切换的开销和等待时间。在实现时可以通过向ReentrantLock中传入false参数获得。
- 基于对共享资源的访问方式可以分为悲观锁和乐观锁
- 悲观锁:悲观锁认为在多线程环境下对共享资源的访问极有可能发生冲突,因此在访问资源之前,先尝试获取锁并锁定资源,直到该线程完成对资源的访问并释放锁,其他线程才能继续访问。
- 乐观锁:乐观锁认为在访问数据时,不会有其他线程来修改该数据。因此,乐观锁在操作数据的时候不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。乐观锁通过CAS算法实现。
因此,悲观锁适合写操作较多且读操作较少的并发场景,乐观锁适用于读多写少的场景或者并发较少的场景。
- 基于锁的升级以及优化可分为偏向锁、轻量级锁和重量级锁
- 偏向锁:偏向锁是指⼀段同步代码⼀直被⼀个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级锁:轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁:重量级锁是指当锁为轻量级锁的时候,另⼀个线程虽然是自旋,但自旋不会⼀直持续下去,当自旋⼀定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
- 自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,该线程不会进入阻塞状态,而是不断循环检查锁是否已经被释放,直到获取到锁为止。
- 分段锁:是一种将数据或资源划分为多个段(segments),并对每个段分配单独锁的锁机制,可以减少锁的粒度从而减少锁冲突。在jdk1.8之前的ConcurrentHashMap就是使用分段锁来保证自身的线程安全。
Day10√
说说你对 synchronized 的理解?***
synchronized关键字是Java中的一种同步锁,主要用于多线程环境下保证线程的安全性。当一个方法或一个代码块被synchronized修饰时,它被称为同步方法或同步代码块。这意味着每次只有一个线程可以进入该方法或代码块,其他线程必须等待,直到当前线程执行完毕并释放锁。synchronized具有原子性、可见性、有序性和可重入性。
synchronized 和 Lock 的区别是什么?**
synchronized和Lock都是Java中用于实现线程同步的机制,都可以保证线程安全。它们的区别主要如下:
- 来源不同:synchronized是Java内置的一个关键字,而Lock是一个接口,它下面有很多实现类,例如ReentrantLock就是它的一个实现类。
- 用法和获取方式不同:
synchronized可以写在需要同步的对象、方法或者是特定代的码块中,是隐式获取锁。主要有两种用法,一种是把synchronized修饰在方法上,一种是把synchronized修饰在代码块上。
Lock控制锁的粒度是通过lock()方法和unlock()方法来实现的,是显示地获取锁,这两个方法之间地代码是线程安全的。
由此可见,Lock比synchronized在使用上相对来说要更加灵活一些。Lock可以自主地去决定什么时候加锁,什么时候释放锁。只需要调用lock()和unlock()这两个方法就可以了。需要注意的是,为了避免死锁,一般我们unlock()方法写在finally块中,而synchronized 只有代码块执行结束或者代码抛出异常时才会释放锁。
- 性能区别:在低并发情况下,synchronized 的性能优于 Lock,因为 Lock 需要显式地获取和释放锁,而 synchronized 是在 JVM 层面实现的;在高并发的情况下,Lock 的性能要远远优于 synchronized,因为 Lock 可以更好地支持高并发和读写分离的场景。
- 使用场景不同:synchronized和Lock在一般情况下没有什么区别,但是在非常复杂的同步应用中,建议使用Lock。因为synchronized只提供了非公平锁的实现,而Lock提供了公平所和非公平锁的机制。
公平锁是指线程竞争锁资源的时候,如果已经有其他线程正在排队或者等待锁释放,那么当前竞争锁的线程是无法去插队的。
非公平锁就是不管是否线程再排队等待锁,它都会去尝试竞争一次锁。
synchronized 和 ReentrantLock 的区别是什么?**
在Java中,synchronized(内置锁)和 ReentrantLock(可重入锁)是两种常用的锁。它们的区别如下:
- 用法和获取方式不同:同上
- 锁的类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
- 使用场景:同上
- 锁的灵活性:ReentrantLock相对于Synchronized提供了更多的功能。例如,可以设置超时时间,以避免线程无限期地等待锁;可以判断锁是否被其他线程持有,从而进行相应的处理;还可以使用Condition类实现线程等待/通知机制,以支持更复杂的并发控制。
Day11√
volatile 关键字的作用有哪些?***
volatile 是 Java 中的一个关键字,用于修饰变量,它的主要作用是保证变量在多线程环境下的可见性和禁止指令重排序,但它不保证原子性。
- 可见性:指当一个线程修改了一个被volatile关键字修饰的变量时,其他线程能立即看到修改的值。
- 禁止指令重排序:指确保对volatile变量的读写操作不会被编译器或处理器随意重新排序,从而确保了程序执行的有序性。
- 不保证原子性:volatile不保证操作的原子性。synchronized可以同时保证可见性和原子性。
- 可见性(Visibility)
- 在多线程环境中,线程通常会将变量的值从主内存复制到自己的工作内存(CPU 缓存)中进行操作。如果一个线程修改了变量的值,但其他线程仍然从自己的工作内存中读取该变量的旧值,就会导致数据不一致的问题。
- 当一个变量被声明为 volatile 时,Java 内存模型确保该变量的所有读操作都会直接从主内存中读取,而写操作会直接写入主内存。因此,volatile 变量对所有线程都是可见的。
- 有序性(Ordering)
- Java 内存模型允许编译器和处理器对指令进行重排序,以优化性能。但这种重排序可能导致线程间的执行顺序不一致,从而引发潜在的并发问题。
- 使用 volatile 修饰的变量可以禁止指令重排序。具体来说,Java 在 volatile 变量的读/写操作时会插入内存屏障(Memory Barrier),从而保证在 volatile 变量的操作前后的代码执行顺序不会被重排序。
volatile 与 synchronized 的对比?**
- 机制与用途
- synchronized:它是Java的一个关键字,用于提供线程间的同步机制。当一个线程进入一个由synchronized修饰的代码块或方法时,它会获取一个监视器锁(monitor lock),这保证了同一时间只有一个线程可以执行这段代码。其主要用途是确保数据的一致性和线程安全性。
- volatile:这是Java的一个关键字,用于修饰变量。volatile的主要作用是确保变量在多线程环境中的可见性和有序性,即当一个线程修改了一个volatile变量的值,其他线程能够立即看到这个修改。此外,它还可以防止指令重排序。但是,volatile并不能保证复合操作的原子性。
- 原子性:
- synchronized:它可以保证被其修饰的代码块的原子性,即这段代码在执行过程中不会被其他线程打断。
- volatile:只能保证单个读写操作的原子性,对于复合操作(如自增、自减等)则无法保证原子性。
- 互斥性:synchronized:提供了互斥性,即同一时间只有一个线程可以执行被其修饰的代码块或方法。volatile:不提供互斥性,只是确保变量的可见性。
- 性能:volatile通常比synchronized更轻量级,因为它不涉及锁的获取和释放。但是,这也意味着它提供的同步级别较低。
JDK8新特性?***
- Lambda表达式:Lambda表达式是一个匿名函数,Java8允许把函数作为参数传递进方法中。
- Stream API:提供了一种处理集合数据流的方式,支持并行操作和链式调用,简化了集合数据的过滤、映射、排序等操作。它可以提高代码的可读性和简洁性。
- 函数式接口:包含一个抽象方法的接口,可以使用
@FunctionalInterface
注解来明确表示它是一个函数式接口,这样就可以用lambda表达式或方法引用来实现。 - 默认方法:在接口中允许定义具有具体实现的方法,从而解决了接口演化时向后兼容的问题。
- Optional类:用于防止空指针异常,提供了更好的空值处理方式,避免了NullPointerException。
- 新的日期和时间API:包括
java.time
包,提供了一系列全新的日期、时间、时区以及持续时间的API,替代了旧的java.util.Date
和Calendar
类。
Day12√
什么是线程池?为什么需要线程池?**
线程池(Thread Pool) 是一种管理和重用线程的机制,用于提高多线程应用程序的性能和效率。线程池在程序启动时创建一定数量的线程,将它们放入池中,并在需要时重复使用这些线程,而不是为每个任务都创建新线程。这有助于减少线程创建和销毁的开销,提高资源利用率,并且可以更好地控制并发线程数量。
- 降低资源消耗:通过重复利用已创建的线程减少线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性:线程池可以限制并发线程的数量,防止系统过载,提高系统稳定性。
常见的几种线程池:
- FixedThreadPool:固定大小的线程池,可控制线程最大并发数,超过核心线程数的任务将会进入队列排队等待执行,通常用于负载较重且稳定的场景。
- CacheThreadPoll: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
- SingleThreadExecutor:单线程的线程池,只有一个工作线程,保证所有任务按照提交顺序执行,常用于执行串行化任务或者需要顺序执行的任务。
- ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。
说一说线程池有哪些常用参数?**
- corePoolSize(核心线程数):线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)。大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。
- maximumPoolSize(线程池最大线程数):线程池允许创建的最大线程数量。当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程并执行。
- keepAliveTime(空闲线程存活时间):当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。可被回收的线程:设置allowCoreThreadTimeout=true的核心线程;大于核心线程数的线程(非核心线程)。
- unit(空闲线程存活时间单位):keepAliveTime的时间单位。
- workQueue(工作队列):存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任务调度时再从队列中取出任务。它仅仅用来存放被execute()方法提交的Runnable任务。工作队列实现了BlockingQueue接口,常见的工作队列有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。
- threadFactory(线程工厂):创建线程的工厂,可以设定线程名、线程编号等。
- handler(拒绝策略):当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。
线程池的拒绝策略?
若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。
- AbortPolicy
第1种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException的RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
- DiscardPolicy
第2种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
- DiscardOldestPolicy
第3种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
- CallerRunsPolicy
第4种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
BIO、NIO、AIO 的区别?**
- BIO:同步阻塞 I/O 模型中,当一个线程执行 I/O 操作时,它会被阻塞直到 I/O 操作完成。这会导致线程无法执行其他任务。
适用场景:适用于连接次数比较少、并发不高的场景,例如传统的 Socket 编程。
- NIO:同步非阻塞 I/O 模型中,一个线程执行一个 I/O 操作时不会等待,而是继续执行其他任务,线程需要通过轮询(polling)或者选择器(Selector)来检查哪些连接已经准备好进行 I/O 操作。核心组件包括通道(Channel)、缓冲区(Buffer)和选择器(Selector)
适用场景:适用于高并发、连接次数比较多的场景。
- Channel(通道):通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
- Buffer(缓冲区):Buffer是一个对象,它包含一些要写入或者要读出的数据。在面向流的I/O中,可以将数据写入或者将数据直接读到Stream对象中。
- Selector(选择器):选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
- AIO:异步非阻塞 I/O 模型是在 NIO 上进一步发展的, 提供了异步操作的能力。在进行 I/O 操作时,可以注册一个回调函数,当IO操作完成时,系统会调用回调函数通知应用程序。
适用场景:适用于处理大量并发连接的场景,并且希望充分利用系统资源。
举例:类比烧开水
- BIO相当于在烧一壶开水,我一直在这一壶水面前等待水烧开,只有水烧开了我才能去做别的事情。
- NIO相当于我烧一排开水,我不断的检查有没有开水烧开,等到所有开水烧开我才能去别的事情。
- AIO相当于我把开水交给另一个人去烧开,然后我去做别的事情了,当水烧开了这个人会就会过来通知我水已经烧开了。
Day13
Java内存区域?**
有哪些垃圾回收算法?***
有哪些垃圾回收器?***
Day14
介绍一下什么是强引用、软引用、弱引用、虚引用?**
Java四大引用详解:强引用、软引用、弱引用、虚引用 - BAT架构技术与大厂面试 - SegmentFault 思否
等级: 强 > 软 > 弱 > 虚
强引用是Java中声明对象的默认引用方式,也是使用最多的一种方式,请记住不管内存紧张也罢,不足也罢,gc永不回收强引用的对象, 即使 jvm 出现(内存溢出错误)OutOfMemoryError,使程序停止,也不会回收对象来提高内存。
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示。
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
类加载机制介绍一下?**
双亲委派机制是什么?**
Day14.5√
Spring和SpringBoot的区别?**
- Spring
Spring 是一个全面的 Java 企业级应用程序开发框架,提供了广泛的功能,包括依赖注入、 AOP (面向切面编程)、事务管理等。
- SpringMVC
Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。主要关注于处理 Web 请求、管理用户会话、控制应用程序流程等。
- SpringBoot
Springboot 是一个微服务框架,延续了 Spring 框架的核心思想IOC和AOP,简化了应用的开发和部署。 Spring Boot 是为了简化 Spring 应用的创建、运行、调试、部署等而出现的,提供约定大于配置的方式,使用它可以做到专注于 Spring 应用的开发,而无需过多关注 XML 的配置。使得构建独立的、自包含的Spring 应用程序变得更加容易。简而言之, Spring 是一个全面的框架,Spring MVC 是 Spring 框架的一部分,专注于Web应用程序开发,而 Spring Boot 是基于Spring的微服务框架,旨在简化和加速 Spring 应用程序的开发。
说一说你对Spring AOP的理解?**
【Spring】面试官:谈一谈你对spring AOP的理解_spring 面试中,谈谈你对spring aop的理解-CSDN博客
AOP称为面向切面编程。AOP是一种编程思想,是对面向对象编程(OOP)的一种补充。传统OOP开发中的代码逻辑是自上而下的,而在开发过程会产生一些横切性的问题,例如事务管理、记录日志、权限控制等。这些横切性的问题和主业务逻辑关系不大,但是会散落到代码的各个部分,难以维护。AOP的编程思想就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。提到了代码的可重用性和可维护性。
AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。静态代理是编译期实现,动态代理是运行期实现,可想而知前者拥有更好的性能。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和Cglib动态代理。如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
这里顺带总结一下 AOP 关键术语(不理解也没关系,可以继续往下看):
- 横切关注点(cross-cutting concerns) :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
- 切面(Aspect):对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能。
- 连接点(JoinPoint):连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等)。
- 通知(Advice):通知就是切面在某个连接点要执行的操作。通知有五种类型,分别是前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程。
- 切点(Pointcut):一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。比如
execution(* com.xyz.service..*(..))
匹配com.xyz.service
包及其子包下的类或接口。- 织入(Weaving):织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种,分别是编译期织入(AspectJ)和运行期织入(AspectJ)。
Spring AOP的通知类型有哪些?**
Spring AOP有五种通知类型,分别是前置通知Before
、返回通知After returning
、环绕通知Around
、异常通知After throwing
、后置通知After
。
- 前置通知
**Before**
:在连接点前面执行但不会影响连接点的执行,除非它引发异常。 - 返回通知
**After returning**
:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。 - 环绕通知
**Around**
:****这是最强大的通知类型。环绕通知可以在方法调用前后完成自定义行为。它可以选择是否继续执行连接点或直接返回自定义的返回值又或者抛出异常将执行结束。 - 异常通知
**After throwing**
:在连接点抛出异常后执行。 - 后置通知
**After(finally)**
:在连接点执行完成后执行,不管连接点是正常执行完成,还是抛出异常,都会执行通知内容。
Spring中AOP失效的场景**
- 非Spring管理的对象:Spring的AOP只能拦截由Spring容器管理的Bean对象。如果您使用了非受Spring管理的对象,则AOP将无法对其进行拦截。
- 同一个Bean内部方法调用:如果一个Bean内部的方法直接调用同一个Bean内部的另一个方法,AOP将无法拦截这个内部方法调用。因为AOP是基于代理的,只有通过代理对象才能触发AOP拦截。
- 静态方法:Spring的AOP只能拦截非静态方法。如果您尝试拦截静态方法,AOP将无法生效。
- final方法:AOP无法拦截final方法。final方法是不可重写的,因此AOP无法生成代理对象来拦截这些方法。直接在对象内部调用方法:如果您直接在对象内部调用方法而不通过代理对象,AOP将无法拦截。因此,建议始终通过代理对象调用方法以确保AOP的生效。
- 异步方法:对于使用Spring的异步特性(如@Async注解)的方法,AOP拦截器可能无法正常工作。这是因为异步方法在运行时会创建新的线程或使用线程池,AOP拦截器无法跟踪到这些新线程中的方法调用。
说一说你对 Spring中IOC的理解?***
IOC称为控制反转。IOC是一种设计思想,是指在开发中将设计好的对象交给容器管理,而不是传统的在对象内部直接控制,这样会大大降低代码的耦合性。有了IOC容器后,我们把创建和查找依赖对象的控制权交给容器,容器初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从IOC容器中取出需要的对象进行依赖注入(DI)。
你会如何实现一个IOC容器?**
- 先准备一个容器对象,map结构;
- 进行配置文件的读取,或注解的解析操作,将需要创建的bean封装为BeanDefinition对象存储在容器中;
- 容器将封装好的 beanDefiniotn 对象,通过反射进行实例化操作;
- 通过依赖注入注入给类中的对应属性值进行设置,完成整个对象的创建,变成一个完成的bean对象,存储在某个map结构中;
- 通过容器对象来获取对象,进行对象的获取和逻辑处理工作;
- 提供销毁操作,当对象不用,或者容器关闭的时候,将无用的对象进行销毁。
依赖注入的三种方式***
Spring基础——深入理解依赖注入的三种方式-百度开发者中心 (baidu.com)
- 构造函数注入
构造函数注入是通过在类的构造函数中声明依赖项来实现的。当Spring容器创建对象时,它会使用构造函数参数来注入依赖项。这种方式被认为是最佳实践,因为它可以确保依赖项在对象创建时就被初始化,并且对象在整个生命周期中始终保持不变。
优点:
- 确保依赖项在对象创建时就被初始化。
- 可以设置依赖项为final,确保它们不会被修改。
- 支持不可变对象。
缺点:
- 如果类有很多依赖项,构造函数可能会变得很长且难以阅读。
1 | public class MyService { |
- Setter方法注入
Setter方法注入是通过在类中提供setter方法来注入依赖项的。Spring容器会调用这些setter方法来设置依赖项的值。这种方式更加灵活,因为可以在对象创建后的任何时候注入依赖项。
优点:
- 可以在对象创建后的任何时候注入依赖项。
- 可以重新配置已存在的对象。
缺点:
- 可能导致对象状态的不确定性,因为依赖项可以在任何时候被更改。
- 不支持不可变对象。
1 | public class MyService { |
- 字段(Field)注入
字段注入是通过在类的字段上使用注解来实现的。Spring容器会自动注入依赖项到这些字段中。这种方式相对简单,但通常不是最佳实践,因为它可能会导致代码难以阅读和维护。
优点:
- 代码简单明了,易于实现。
缺点:
- 不支持不可变对象。
- 可能导致代码难以阅读和维护。
- 依赖项的生命周期可能不明确。
1 | public class MyService { |
@Resource和@Autowired的区别是什么?**
共同点:@Autowired
和@Resource
都是 Spring/Spring Boot 项目中,用来进行依赖注入的注解。在接口仅有单一实现类时,两个注解的修饰效果相同,可以互相替换,不影响使用。
区别:
- 来源不同:@Resource是Java定义的注解,来自于 JSR-250 (Java 250 规范提案);而@Autowired注解是Spring框架提供的。
- 依赖查找的顺序不同:@Resource注解默认按照名称进行匹配查找,如果找不到,则按照类型进行匹配。而@Autowired注解默认是按照类型进行匹配,如果出现多个类型一致的实例对象,则需要指定名称。
- 支持参数不同:@Autowired 只支持设置一个 required 的参数,required属性表示是否必须注入该属性;@Resource 支持包括 name 和 type 等 7 个参数。
- 依赖注入的用法不同:@Autowired 既支持构造方法注入,又支持 Field 注入和 Setter 注入,而 @Resource 只支持 Field 注入和 Setter 注入。
Day15√
Bean的作用域?**
在Spring中,bean作用域用于确定哪种类型的bean实例应该从Spring容器中返回给调用者。目前Spring Bean的作用域主要有六种。
Scope | Description |
---|---|
singleton | (默认的)在Spring IoC容器中,一个bean定义对应只会有唯一的一个bean实例,bean以单例方式存在。 |
prototype | 一个bean定义可以有多个bean实例。每次从容器中调用bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()。 |
request | 一个bean定义对应于单个HTTP请求的生命周期。也就是说,每个HTTP请求都有一个bean实例,且该实例仅在这个HTTP请求的生命周期里有效。该作用域仅适用于WebApplicationContext环境。 |
session | 一个bean定义对应于单个HTTP Session的生命周期。也就是说,每个HTTP Session都有一个bean实例,且该实例仅在这个HTTP Session的生命周期里有效。该作用域仅适用于WebApplicationContext环境。 |
application | 一个bean定义对应于单个ServletContext的生命周期。该作用域仅适用于WebApplicationContext环境。 |
websocket | 一个bean 定义对应于单个websocket 的生命周期。该作用域仅适用于WebApplicationContext环境。 |
Bean的生命周期?**
在 Java 中,Bean 就是一个由 Spring IOC 容器初始化、管理和维护的普通Java对象。通过 Spring 容器,我们可以方便的创建和获取这些对象,并且可以配置它们的行为与属性。它的生命周期如下:
- 创建 Bean 的实例:实例化一个 Bean 对象。Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
- Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如
@Autowired
等注解注入的对象、@Value
注入的值、setter
方法或构造函数注入依赖和值、@Resource
注入的各种资源。 - Bean 初始化:
- 如果 Bean 实现了
BeanNameAware
接口,调用setBeanName()
方法,传入 Bean 的名字。 - 如果 Bean 实现了
BeanClassLoaderAware
接口,调用setBeanClassLoader()
方法,传入ClassLoader
对象的实例。 - 如果 Bean 实现了
BeanFactoryAware
接口,调用setBeanFactory()
方法,传入BeanFactory
对象的实例。 - 与上面的类似,如果实现了其他
*.Aware
接口,就调用相应的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessBeforeInitialization()
方法 - 如果 Bean 实现了
InitializingBean
接口,执行afterPropertiesSet()
方法。 - 如果 Bean 在配置文件中的定义包含
init-method
属性,执行指定的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法。
- 销毁 Bean:销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
- 如果 Bean 实现了
DisposableBean
接口,执行destroy()
方法。 - 如果 Bean 在配置文件中的定义包含
destroy-method
属性,执行指定的 Bean 销毁方法。或者,也可以直接通过@PreDestroy
注解标记 Bean 销毁之前执行的方法。
Spring循环依赖是怎么解决的?**
定义:循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用。Spring2.6以前允许出现循环依赖,但是这是一种种糟糕的设计缺陷,所以之后的Spring版本不建议出现循环依赖。
Spring创建Bean的流程:
- 先去 一级缓存
singletonObjects
中获取,存在就返回; - 如果不存在或者对象正在创建中,于是去 二级缓存
earlySingletonObjects
中获取; - 如果还没有获取到,就去 三级缓存
singletonFactories
中获取,通过执行ObjectFacotry
的getObject()
就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。
Spring的三级缓存包括:
- 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
- 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中
ObjectFactory
产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()
都是会产生新的代理对象的。- 三级缓存(singletonFactories)**:存放
ObjectFactory
,ObjectFactory
的getObject()
方法(最终调用的是getEarlyBeanReference()
方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。
Spring的解决循环依赖方法:
Spring框架使用三级缓存来解决循环依赖的问题,确保了即使循环依赖的情况存在也能正确创建Bean。也就是说,如果发生了循环依赖的话,就去三级缓存singletonFactory
中拿到三级缓存中存储的ObjectFactory
并调用它的getObject()
方法来获取这个循环对象的前期暴露对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址了),并且将这个前期暴露对象放到二级缓存中,这样在循环依赖时,就不会重复初始化了!
缺点: 增加了内存开销(需要维护三级缓存,也就是三个 Map),降低了性能(需要进行多次检查和转换)。并且,还有少部分情况是不支持循环依赖的,比如非单例的 bean 和@Async
注解的 bean 无法支持循环依赖。
只用两级缓存够吗?
在没有 AOP 的情况下,确实可以只使用一级和三级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,二级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。
@Lazy注解解决解决循环依赖
@Lazy
用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。
不建议使用全局懒加载!全局懒加载会让bean第一次使用的时候加载会变慢,并且迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。
首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性;
由于在 A 上标注了 @Lazy 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性;
之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。
Spring中用到了那些设计模式?**
Spring(22) Spring中的9种设计模式_spring有些什么设计模式-CSDN博客
Java之美-从菜鸟到高手演变之设计模式_java 设计模式 菜鸟-CSDN博客
- 工厂设计模式:Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
- 代理设计模式:Spring AOP 就是基于动态代理的实现。为其他对象提供一个代理以控制对这个对象的访问。
- 单例设计模式:Spring 中的 Bean 默认都是单例的。确保某一个类只有一个实例。单例模式:5种实现方式 - 码农StayUp - 博客园
- 模板方法模式:Spring中jdbcTemplate、hibernateTemplate 等以Template结尾的对数据库操作的类,它们就使用到了模板模式。
- 包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并被自动更新。Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式:Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
Day16√
描述一下 SpringMVC 的执行流程?**
- 客户端发送请求:客户端(浏览器)发送请求到前端控制器(DispatcherServlet),请求被Servlet拦截以后,转发给SpringMVC框架
- DispatcherServlet处理请求:SpingMVC中的DispatcherServlet核心控制器会接收到请求,并转发给HandlerMapping
- HandlerMapping负责解析请求,根据请求信息和配置信息(xml配置,注解)找到对应的Controller类,如果有配置拦截器,会按照顺序执行拦截器里面的PreHandler方法。
- 找到匹配的Controller后,会把请求参数传给Controller里面的方法。
- Controller方法执行完后,会返回一个ModeAndView,其中包括视图名称和需要传递给视图的模型数据。
- 视图解析器会根据名字找到视图,然后把视图模型填充到视图里面,再渲染成html内容,返回给客户端
SpringBoot Starter有什么用?如何手写一个Starter?**
[手把手教你手写一个最简单的 Spring Boot Starter - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/353561846#:~:text=了解了 Starte)
Spring Boot Starter的作用是简化和加速项目的配置和依赖管理。
- Spring Boot Starter可以理解为一种预配置的模块,它封装了特定功能的依赖项和配置,开发者只需引入相关的Starter依赖,无需手动配置大量的参数和依赖项。
- Starter还管理了相关功能的依赖项,包括其他Starter和第三方库,确保它们能够良好地协同工作,避免版本冲突和依赖问题。
- Spring Boot Starter的设计使得应用可以通过引入不同的Starter来实现模块化的开发。每个Starter都关注一个特定的功能领域,如数据库访问、消息队列、Web开发等。
- 开发者可以创建自定义的Starter,以便在项目中共享和重用特定功能的配置和依赖项。
SpringBoot常用注解?**
肝了一周总结的SpringBoot常用注解大全,一目了然!平时使用SpringBoot开发项目,少不了要使用到它的注解。 - 掘金 (juejin.cn)
- @SpringBootApplication:是Spring Boot的核心注解,主要目的是开启自动配置。这个注解包含了**@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan这三个注解。**
- @SpringBootConfiguration:该注解继承自@Configuration,二者功能基本一致,标注当前类是配置类。
- @EnableAutoConfiguration:用于启用Spring Boot的自动配置机制,根据项目的依赖和配置自动配置Spring应用程序。
- @ComponentScan:1)告诉Spring哪个package的用注解标识的类会被Spring自动扫描并且装入IoC容器中。2)自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些Bean加载到IoC容器中。
- @Controller:用于标识类作为Spring MVC的Controller。
- @RestController:类似于@Controller,但它是专门用于RESTful web服务的。它包含了@Controller和@ResponseBody。
- @RequestMapping:用于将HTTP请求映射到controller的处理方法。可以用在类级别和方法级别。
- @Autowired:用于自动注入Spring容器中的Bean,可以用在构造方法、字段、Setter方法上。
- @Service:用于标识类作为服务层的Bean。
- @Repository:用于标识类作为数据访问层的Bean,通常用于与数据库交互。
- @Component:通用的组件注解,用于标识任何spring托管的Bean。
- @Configuration:用于定义配置类,类中可能包含一些@Bean注解用于定义Bean。
- @Value:用于从属性文件或配置中读取值,将值注入到成员变量中。
- @Qualifier:与@Autowired一起使用,指定注入时使用的Bean名称。
- @ConfigurationProperties:用于将配置文件中的属性映射到Java Bean。
- @Profile:用于定义不同环境下的配置,可以标识在类或方法上。
- @Async:用于将方法标记为异步执行。
Spring和SpingBoot的区别?**
Spring 框架是一个广泛应用于企业级 Java 开发的开源框架,为开发Java应用程序提供了全面的基础架构支持。Spring Boot 则是在 Spring 框架基础上的一种简化配置、快速开发的框架。
- 环境配置不同:Spring的环境配置相对较为繁琐,需要手动进行配置,例如配置数据源、配置日志、配置Servlet等等。而Spring Boot则是提供了一套自动配置机制,通过约定大于配置的方式,可以减少开发者的环境配置工作量,从而快速构建应用程序。
- 启动方式不同:Spring的启动方式是通过XML配置文件或Java配置类来配置应用程序,然后通过ApplicationContext来启动应用程序。而Spring Boot则是通过内嵌的Tomcat、Jetty、Undertow等容器,可以直接使用java -jar命令启动应用程序。
- 依赖管理不同:在Spring中,需要手动添加各种依赖库,例如Spring MVC、Spring Security等等。而在Spring Boot中,则是通过Spring Boot Starter依赖,可以一次性添加一系列的依赖库,从而简化依赖管理的工作量。
- 默认配置不同:Spring Boot为开发者提供了一系列的默认配置,例如日志、数据源等,大多数情况下可以直接使用默认配置来构建应用程序。而在Spring中,需要手动配置这些内容,增加了开发者的工作量。
- 微服务支持:Spring 虽然可以用于构建微服务,但需要额外的配置和集成工作。Spring Boot 提供了对微服务架构的内置支持,如服务发现、负载均衡、配置管理等,与Spring Cloud紧密集成,更易于构建微服务应用。
总结:Spring Boot就是Spring的完善和扩展,就是为我们便捷开发,方便测试和部署,提高效率而诞生的框架技术。
Day16.5
过滤器和拦截器的区别是什么
面试突击90:过滤器和拦截器有什么区别?-腾讯云开发者社区-腾讯云 (tencent.com)
- 出身不同
过滤器来自于 Servlet,而拦截器来自于 Spring 框架。
- 触发时机不同
请求的顺序是:请求进入容器>进入过滤器>进入Servlet>进入拦截器>执行控制器(Controller)。所以过滤器和拦截器的执行时机也是不同的,过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法。
- 实现不同
过滤器是基于方法的回调实现的,而拦截器是基于动态代理(底层是反射)实现的。
- 支持的项目类型不同
过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;而拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application 或 Swing 程序中。
- 使用的场景不同
- 因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务。
- 而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。
数据库
Day17√
一条SQL查询语句是如何执行的?**
一条SQL查询语句是如何执行的? - 知乎 (zhihu.com)
- 连接器:跟客户端建立连接、获取权限、维持和管理连接;
- 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
- 解析 SQL:通过分析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树;
- 执行 SQL:执行 SQL 共有三个阶段:
- 预处理阶段:检查表或字段是否存在;将
select *
中的*
符号扩展为表上的所有列; - 优化阶段:基于查询成本的考虑,优化器会选择查询成本最小的执行计划;
- 执行阶段:执行器根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端。
- 预处理阶段:检查表或字段是否存在;将
数据库的事务隔离级别有哪些?***
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
- 脏读(Dity Read):指一个事务读取了另一个事务未提交的数据。当一个事务修改了某个数据,但还未提交时,另一个事务读取了这个未提交的数据,如果第一个事务回滚了,那么第二个事务读取到的数据就是无效的。脏读会导致数据的不一致性。
- 幻读(Phantom Read):指一个事务在读取某个范围的数据时,另一个事务插入了新的数据,导致第一个事务再次读取同样的范围时,发现有新的数据出现。幻读主要发生在并发的插入操作中,会导致第一个事务读取到不一致的数据。
- 不可重复读 (Non-repeatable Read):指一个事务在读取某个数据后,再次读取同样的数据时,发现数据已经发生了变化。不可重复读主要发生在并发的更新操作中,会导致事务之间读取到不一致的数据。
- 读未提交(Read Uncommitted)
- 允许一个事务读取另一个事务尚未提交的数据修改。
- 最低的隔离级别,存在脏读、不可重复读和幻读的问题。
- 读已提交(Read Committed)
- 一个事务只能读取已经提交的数据。其他事务的修改在该事务提交之后才可见。
- 解决了脏读问题,但仍可能出现不可重复读和幻读。
- 可重复读(Repeatable Read)
- 事务执行期间,多次读取同一数据会得到相同的结果,即在事务开始和结束之间,其他事务对数据的修改不可见。
- 解决了不可重复读问题,但仍可能出现幻读。
- 序列化(Serializable)
- 最高的隔离级别,确保事务之间的并发执行效果与串行执行的效果相同,即不会出现脏读、不可重复读和幻读。
事务的四大特性有哪些?***
事务的四大特性通常被称为 ACID 特性
- 原子性(Atomicity):确保事务的所有操作要么全部执行成功,要么全部失败回滚,不存在部分成功的情况。
- 一致性(Consistency):事务在执行前后,数据库从一个一致性状态转变到另一个一致性状态。
- 隔离性(Isolation):多个事务并发执行时,每个事务都应该被隔离开来,一个事务的执行不应该影响其他事务的执行。
- 持久性(Durability):一旦事务被提交,它对数据库的改变就是永久性的,即使在系统故障或崩溃后也能够保持。
Day18√
MySQL的执行引擎有哪些?**
主要有InnoDB、MyISAM、Memery等引擎:
**InnoDB**
引擎提供了对事务ACID四大特性的支持,还提供了行级锁和外键约束。是MySQL默认存储引擎,适用于需要事务支持、高并发性、高写入需求的应用。**MyISAM**
引擎使用表级锁,支持全文索引,但不支持事务,也不支持行级锁和外键约束。MySQL最早提供的存储引擎,适用于以读操作为主的应用。**Memery**
就是将数据放在内存中,数据处理速度很快,但是安全性不高(一旦数据库崩溃,数据就会丢失)。常应用于临时表中。Merge
将多个相同的MyISAM表合并为一个虚表,常应用于日志和数据仓库。
MySQL索引有哪些种类?
- 按数据结构分类:
B+ tree索引:所有数据存储在叶子节点,复杂度为O(logn),适合范围查询。
Hash 索引:适合等值查询,检索效率高,一次到位。
Full-text 索引:又称为全文索引,MyISAM 和 InnoDB 中都支持使用全文索引,一般在文本类型 char,text,varchar类型上创建。
- 按物理存储分类:
聚簇索引:数据存储与索引一起存放,叶子节点会存储一整行记录,找到索引也就找到了数据。
非聚簇索引:又称为二级索引,数据存储与索引分开存放,叶子节点不存储数据,存储的是数据主键。
- 按字段特性分类:
主键索引:建立在主键 PRIMARY 字段上的索引,不允许有空值。
唯一索引:建立在 UNIQUE 字段上的索引,但是允许有空值。
普通索引:MySQL中基本索引类型,允许空值和重复值。
前缀索引:是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引。
- 按字段个数分类:
单列索引:建立在单个字段上的索引为单列索引。
联合索引:建立在多个字段上的索引,使用时遵循最左前缀原则。
MySQL为什么使用B+树来作索引?B树与B+树的区别?***
- 单点查询:B 树进行单个索引查询时,最快可以在 O(1) 的时间代价内就査到,而从平均时间代价来看,会比 B+ 树稍快一些。但是 B 树的查询波动会比较大,因为每个节点即存索引又存记录,所以有时候访问到了非叶子节点就可以找到索引,而有时需要访问到叶子节点才能找到索引。B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
- 插入和删除效率:B+ 树有大量的冗余节点(非叶子节点),删除一个数据的时候,可以直接从叶子节点中删除,甚至可以不动非叶子节点,删除非常快。B+树的插入也是一样,有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。B树没有冗余节点,删除节点的时候非常复杂,可能涉及复杂的树的变形。
- 范围查询:B+ 树所有叶子节点之间用链表连接了起来,有利于范围查询,而 B树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。B+ 树的插入和删除效率更高。存在大量范围检索的场景,适合使用 B+树,比如数据库。而对于大量的单个索引查询的场景,可以考虑 B 树,比如nosql的MongoDB。
说一下索引失效的场景?**
索引失效的10种场景,你知道几个呢?(面试必刷!)_索引失效的场景-CSDN博客
- 使用左或者左右模糊匹配:比如
LIKE '%abc'
这样的查询会导致索引失效。 - 在索引列上使用函数或表达式:索引列上参与计算,索引失效。
1 | SELECT * FROM table WHERE UPPER(column) = 'VALUE'; |
- OR 条件:当查询中使用多个 OR 条件时,如果这些条件不涉及同一列(例如在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列),那么索引会失效。数据库可能会选择全表扫描而不是使用多个索引。
- 违背最左匹配原则,索引失效。
在使用联合索引进行查询时,查询条件需要遵循索引中列的顺序,从左到右进行匹配。
- 不等号条件:通常情况下,索引只能用于等值比较。当查询中包含不等号条件(如>, <, between, in)时,索引可能会失效。
- 隐式类型转换:如果查询中的条件涉及到隐式类型转换,例如将字符串与数字比较,索引可能无法被使用。
Day19√
MySQL 的日志文件有哪几种?**
**undo log**
(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和MVCC。**redo log**
(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障后的数据恢复。**binlog**
(归档日志):是 MySQL Server 层生成的一种二进制日志,主要用于数据备份和主从复制。用来记录对 MySQL 数据更新或潜在发生更新的 SQL 语句,并以 “事务”的形式保存在磁盘中。**relay log**
(中继日志):用于主从复制场景下,从服务器通过 I/O 线程拷贝主服务器的**binlog**
后本地生成的日志,然后从服务器 SQL 线程会读取relay log
的内容并应用到从服务器,从而使从服务器和主服务器的数据保持一致。
什么是慢查询?原因是什么?可以怎么优化?**
慢查询是指数据库查询的执行时间超过指定的超时时间时(long_query_time:默认10秒 )。
原因:
- 查询语句比较复杂:查询涉及多个表,包含复杂的连接和子查询,可能导致执行时间较长。
- 查询数据量大:当查询的数据量庞大时,即使查询本身并不复杂,也可能导致较长的执行时间。
- 缺少索引:如果查询的表没有合适的索引,需要遍历整张表才能找到结果,查询速度较慢。
- 数据库设计不合理:数据库表设计庞大,查询时可能需要较多时间。
- 并发冲突:当多个查询同时访问相同的资源时,可能发生并发冲突,导致查询变慢。
- 硬件资源不足:如果MVSQL服务器上同时运行了太多的查询,会导致服务器负载过高,从而导致查询变慢
优化:
- 不使用子查询
- 分组统计可以禁止排序:默认情况下,MySQL对所有GROUP BY col1,col2…的字段进⾏排序。如果查询包括GROUP BY,想要避免排序结果的消耗,则可以指定ORDER BY NULL禁止排序。
- 禁止不必要的ORDER BY排序
- 尽量不要超过三个表join
- **不要使用 select ***
- 排序请尽量使用升序
- 尽量使用数字型字段
- 避免索引失效
可以对数据库表做那些优化?**
- 合理使用数据库分表
对于一些特别大的表,可以考虑将其拆分成多个子表,从而更好地管理数据
- 建立索引
在经常被查询的列上建立索引,提高查询性能。但是也要注意过多的索引影响插入、更新和删除的性能
- **避免使用 **
select
只选择需要的列而不是使用 SELECT
- 选择合适的数据类型:在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可能小。
- 尽量使用TINYINT,SMALLINT,MEDIUM INT替代INT类型,如果是非负则加上UNSIGNED
- VARCHAR的长度只分配真正需要的空间
- 尽量使用整数或者枚举替代字符串类型,因为数值型数据被处理起来的速度要比文本类型要快得多。
- 时间类型尽量使用TIMESTAMP而ETIME
- 单表不要放太多字段
- 尽量少使用NULL,很难查询优化而且占用额外索引空间
- 尽量把字段设置为NOT NULL
- 使用连接(JOIN)来代替子查询(Sub-Queries)
连接(JOIN)之所以更有效率一些,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上需要两个步骤的查询工作。
- 避免全表扫描
当表中数据量巨大时,全表扫描会非常耗时。通过使用合适的查询条件来避免全表扫描,可以显著提高查询效率。
- 使用查询缓存
当相同的查询被频繁执行时,使用查询缓存可以避免重复的数据库扫描。
Day20√
说一说你了解的 MVCC 机制?
MVCC(Multi-Version Concurrency Control,多版本并发控制),用于管理多个事务同时访问和修改数据库的数据,而不会导致数据不一致或冲突。MVCC 的核心思想是每个事务在数据库中看到的数据版本是事务开始时的一个快照,而不是实际的最新版本。这使得多个事务可以并发执行,而不会互相干扰。使用 MVCC 和锁可以实现事务的隔离性。
关系型数据库和非关系型数据库的区别?
关系型数据库(Relational Database)和非关系型数据库(NoSQL Database)是两种主流的数据库系统,它们在数据存储方式、扩展方式以及事务性支持等方面存在区别。以下是具体分析:
- 数据存储方式
- 关系型数据库:关系型数据库使用表格形式存储数据,每个表格由行和列组成,并具有固定的模式(schema),对数据的结构和关系进行严格定义。
- 非关系型数据库:非关系型数据库采用灵活的数据模型,通常使用键值对、文档、列族或图形等形式组织数据,适合存储半结构化和非结构化数据。
- 扩展方式
- 关系型数据库:关系型数据库主要通过纵向扩展,即提升单个服务器的性能来应对需求增长,但最终可能达到扩展的上限。
- 非关系型数据库:非关系型数据库具有良好的横向扩展性,可以通过增加更多服务器节点来分担负载,适应大规模分布式数据存储。
- 事务性支持
- 关系型数据库:关系型数据库支持复杂的事务管理,确保数据的完整性和一致性,通过ACID(原子性、一致性、隔离性、持久性)的事务处理机制。
- 非关系型数据库:非关系型数据库虽然也能实现事务操作,但在事务的稳定性方面不如关系型数据库,其优势主要在于高并发和大数据量处理。
- 一致性
- 关系型数据库:关系型数据库强调数据完整性和一致性,通过主键和外键等约束,保证数据之间的关系和准确性。
- 非关系型数据库:非关系型数据库更加注重可用性和分布式存储,允许一定程度的数据冗余,适用于对数据一致性要求不高的场景。
- 查询语言
- 关系型数据库:关系型数据库使用标准化的SQL语言进行数据查询和管理,便于复杂查询操作。
- 非关系型数据库:非关系型数据库根据数据模型不同,支持多种查询语言如XPath、JavaScript等,更适用于特定场景的数据处理。
- 选择方法
- 关系型数据库:关系型数据库适用于需要结构严谨、数据完整性要求高的应用场景,如金融、医疗等行业。
- 非关系型数据库:非关系型数据库适合大规模数据、高并发访问及数据结构多变的场景,如社交媒体、大数据处理。
针对上述分析,提出以下几点考虑因素:
- 安全性:关系型数据库由于其严格的事务管理和一致性控制,通常提供更高的数据安全性。
- 性能需求:非关系型数据库在处理大规模数据和高并发请求时表现出色,适合需要高性能的应用。
- 成本:非关系型数据库往往有更低的硬件成本,并且很多选项是开源的,降低了部署和维护费用。
- 技术栈:选择与团队技术栈相匹配的数据库类型可以降低学习成本和开发难度。
- 未来可扩展性:预留考虑未来可能增长的数据量和需求变化,选择能够支持长期发展的数据库解决方案。
MySQL 和 Redis 的区别?**
- 存储方式:redis基于键值对,支持多种数据结构;而MySQL是一种关系型数据库,使用表来组织数据。
- 持久化:redis将数据存在内存中,通过持久化机制将数据写入磁盘;MySQL通常将数据存储在磁盘上。
- 复杂查询支持:redis不使用SQL,而是使用自己的命令集,不支持复杂的查询;MySQL使用SQL语言,可以进行复杂的查询操作。
- 应用场景:redis以高性能能低延迟为目标,适用于读多写少的应用场景;MySQL适用于需要复杂查询、事务处理和大规模数据存储的应用场景。
Redis有什么优缺点?为什么用Redis查询会比较快?**
优点:Redis是一个基于内存的数据库,读写速度非常快,通常被用作缓存、消息队列、分布式锁和键值存储数据库。常支持多种数据类型,如字符串、哈希表、列表、集合、有序集合等,Redis还提供了分布式特性,可以将数据分布在多个节点上,以提高可扩展性和可用性。
缺点:1.内存限制,redis将数据存储在内存中,因此它受到物理内存大小限制;2.单线程模型,redis使用单线程处理客户端请求,这限制了他在高并发场景下的吞吐量;3.redis不支持SQL数据库那样的复杂查询操作。
redis查询速度快的原因:
- 基于内存:内存的本身的读写速度很快,这是redis速度快的主要原因;
- 高效的数据结构:redis专门设计了STRING、LIST、HASH等高效数据结构,依赖各种数据结构提升了读写的效率;
- 单线程:单线程操作避免了多线程资源竞争和上下文切换带来的性能损失;
- I/O多路复用:采用I/O多路复用同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。
Day21√
Redis的基本数据类型有那些?有哪些使用场景***
Redis是一个基于内存的数据库,读写速度非常快,通常被用作缓存、消息队列、分布式锁和键值存储数据库。常见的数据类型有String(字符串)、Hash(哈希表)、List(列表)、Set(集合)、Zset(有序集合)。
- 字符串:存储字符串数据,也可以存储整数、浮点数,是最基本的数据类型,常用于缓存对象、常规计数、分布式锁、共享Session信息等;
- 哈希表:存储字段和值的映射,常用于缓存对象、购物车等;
- 列表:存储有序的字符串元素,常用于消息队列(有两个问题:1. ⽣产者需要自行实现全局唯⼀ ID;2. 不能以消费组形式消费数据);
- 集合:存储无序不重复的字符串元素,常用于聚合运算场景(并集、交集、差集),如点赞、共同关注、抽奖活动;
- 有序集合:类似于集合,但是可以根据元素所关联的分数进行排序,常用于排序场景,如排行榜。
随着Redis版本更新,又更新了这些数据类型:
- BitMap:存储位的数据结构,可以处理一些位运算操作,比如签到、登录状态等;
- HyperLogLog:用于基数估算的数据结构,用于统计元素的唯⼀数量,如海量数据基数统计的场景;
- GEO:存储地理位置信息的数据结构;
- Stream:专门为消息队列设计的数据类型。
Redis是单线程的还是多线程的,为什么?***
Redis是单线程的,redis的单线程指的是网络请求模块使用单线程进行处理,其他模块仍用多个线程 原因如下:
- CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存或者网络带宽。
- 单线程容易实现,并且单线程避免了多线程的资源竞争和上下文切换的开销。
day22√
Redis持久化机制有哪些?***
- AOF日志:每次执行一条写操作指令,就把该指令以追加的方式写入到一个文件里;
- RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
- 混合持久化方式:工作在AOF日志重写的过程中,集成了前两种的优点。在 AOF 日志重写过程中,Redis 会先将当前内存中的数据以 RDB 的形式写入到 AOF 文件的开头,然后再将后续的写入命令以 AOF 形式追加到文件中。
AOF优点是服务器宕机时丢失数据少,但是数据恢复不够快;RDB的优点是数据恢复快,但是保存快照的频率不好把握,频率高会影响性能,频率低会丢失的数据较多。
缓存雪崩、击穿、穿透和解决办法?***
- 缓存雪崩:指的是在某个时间点,缓存中大量数据同时失效,导致请求直接访问数据库或其他后端系统,增加了系统的负载。
原因:大量数据的过期时间相近或者Redis服务器故障宕机。
解决方法:对于第一种情况可以均匀设置过期时间、加互斥锁、后台更新缓存(让缓存“永久有效”,并将更新缓存的⼯作交由后台线程定时更新)等策略;对于第二种情况可以采用服务熔断或请求限流机制,还有构建Redis缓存高可用集群等方法。
- 缓存击穿:指的是有大量请求查询一个缓存中不存在但数据库中存在的数据时,这些请求直接访问到数据库,增加数据库的负载。
原因:单个热点数据的缓存失效,短时间内大量的请求直接访问数据库。
解决方法:缓存击穿是缓存雪崩的一个子集,可以采用互斥锁和后台更新缓存等策略。
- 缓存穿透:指的是查询一个缓存和数据库都不存在的数据,这个数据始终无法被缓存,导致每次请求都直接访问数据库,增加数据库的负载。
原因:恶意攻击、业务误操作,缓存和数据库中的数据都被删除了。
解决方法:限制非法请求、对查询的数据,在缓存中设置空值、使用布隆过滤器过滤恶意请求。
布隆过滤器是一种空间效率极高的概率型数据结构,它被用来测试一个元素是否在一个集合中。布隆过滤器可能会给出错误的正例,但永远不会给出错误的反例。也就是说,如果布隆过滤器说某个元素不在集合中,那么这个结论一定是正确的;但如果它说某个元素在集合中,那么这个结论可能是错误的。原理如下:
- 初始化
- 位数组:布隆过滤器的核心是一个很长的二进制向量(位数组),初始时所有位都是0。
- 哈希函数:需要选择多个独立的哈希函数(通常为k个),这些函数会将输入映射到位数组中的不同位置。
- 插入元素
- 当一个元素被添加到布隆过滤器时,使用所有的哈希函数计算该元素对应的位数组的位置。
- 对于每个哈希函数计算出的位置,将位数组中对应位置的比特设为1。
- 查询元素
- 当查询一个元素是否存在时,同样使用所有的哈希函数计算该元素对应的位数组的位置。
- 如果所有计算出的位置上的比特都为1,则认为该元素可能存在于集合中。
- 如果任一位置的比特为0,则确定该元素不在集合中。
如何保证数据库和缓存的一致性?***
- 缓存更新策略
Cache Aside(旁路缓存)
- 原理: 读操作:先从缓存中读取数据,如果没有就再去数据库里面读数据,然后把数据放回缓存中,如果缓存中可以找到数据就直接返回数据;写操作:更新数据的时候先把数据持久化到数据库,然后删除缓存。
- 问题: 假如有两个操作一个更新一个查询,第一个操作先更新数据库,还没来及删除数据库,查询操作可能拿到的就是旧的数据,更新操作马上让缓存失效了,所以后续的查询可以保证数据的一致性;还有的问题就是有一个是读操作没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,也会造成脏数据。
- 可行性: 出现上述问题的概率其实非常低,需要同时达成读缓存时缓存失效并且有并发写的操作。数据库读写要比缓存慢得多,所以读操作在写操作之前进入数据库,并且在写操作之后更新,概率比较低。
Read/Write Through
- 原理: Read/Write Through原理是把更新数据库(Repository) 的操作由缓存代理,应用认为后端是一个单一的存储,而存储自己维护自己的缓存。
- Read Through: 应用查询缓存是否存在,存在则返回;不存在则由缓存组件去数据库加载数据到缓存。
- Write Through(双写法): 先查询要写入的数据在缓存是否存在,存在则先更新缓存然后再更新数据库最后返回;如果要写入的数据在缓存不存在,则直接将数据写入数据库。
Write Behind(Write Back)
- 原理: Write Behind 和 Read/Write Through 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。在更新数据的时候,只更新缓存,不更新数据库,而缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操作非常快,带来的问题是,数据不是强一致性的,而且可能会丢。
- 第二步失效问题: 这种可能性极小,缓存删除只是标记一下无效的软删除,可以看作不耗时间。如果会出问题,一般程序在写数据库那里就没有完成: 故意在写完数据库后,休眠很长时间再来删除缓存。
哨兵的工作原理?***
- 判断节点是否存活:每个哨兵定期向Redis服务器发送PING命令,以检测服务器是否处于活跃状态。若哨兵在连续一定次数未收到服务器的响应,就认为该服务器主观下线。然后哨兵就会从从节点中选择一个作为主节点。
- 选出新主节点:在发现主服务器下线后,哨兵们会协调选举一个新的主服务器。这个过程中,哨兵会考虑每个可用的从服务器,选择个作为新的主服务器,并将其他从服务器配置为复制新的主服务器。
- 具体过程:
- 选择候选从服务器:哨兵会从可用的从服务器中选择一组候选服务器,通常选择复制偏移量 (replicationoffset) 最大的从服务器。
- 计算投票:每个哨兵为每个候选从服务器投票。投票的考量因素包括从服务器的复制偏移量、连接质量、优先级等。
- 达成共识:哨兵们根据投票结果达成共识,选择一个从服务器作为新的主服务器。这通常需要获得多数哨兵的同意。
- 更新配置信息:一旦新的主服务器被选出,哨兵会更新 Redis 集群的配置信息,包括将新的主服务器的地址和端口通知给其他哨兵和客户端。
- 通知客户端:哨兵会向客户端发送通知,告知客户端新的主服务器的位置,以便客户端能够重新连接。
Day22.5
Redis中的大Key和热Key是什么,怎么解决?
一文了解如何发现并解决Redis热key与大key问题-腾讯云开发者社区-腾讯云 (tencent.com)
Redis如何实现分布式锁?
Redis实现分布式锁的7种方案 - why414 - 博客园
想要实现分布式锁,必须要求Redis有互斥的能力。可以使用SETNX命令,其含义是SET IF NOT EXIST,即如果key不存在,才会设置它的值,否则什么也不做。两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。
锁过期释放,业务没执行完怎么办?
Redis 分布式锁过期了,但业务还没有执行完,怎么办! - 知乎 (zhihu.com)
在Redission中,只要线程一加锁成功,就会启动一个watch dog
看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。
Redis过期删除策略有哪些?
- 定时删除:设置key的过期时间,当时间到达时,自动执行key的删除操作。
- 惰性删除:每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
- 定期删除:每隔一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
Redis选择 惰性删除 + 定期删除 这两种策略配合使用。
计算机网络
Day23
TCP/IP模型和OSI模型?**
OSI模型是国际标准化组织ISO制定的一个用于计算机或通信系统互联的标准体系,主要分为七个层级,从上到下依次为:应用层,表示层,会话层,传输层,网络层,数据链路层和物理层。详细如下:
- 应用层:这一层为用户程序提供网络服务。常见的协议有FTP、SMTP、HTTP、DNS。
- 表示层:负责数据的格式转换、加密和解密,确保数据在不同系统之间的正确解释性和呈现。也就是把计算机能够识别的东西转换为人能够识别的东西。
- 会话层:建立、管理和终止应用程序之间的会话连接。
- 传输层:提供端到端的数据传输服务。它使用TCP和UDP来传输管理数据。
- 网络层:负责数据的路由和转发,选择最佳路径将数据从园主机传输到目标主机。使用IP地址来识别主机和网络,并提供逻辑地址寻址。传输单位是数据报。常见协议有ICMP、ARP、IP。
- 数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。 ⽤MAC地址访问介质,传输单位是帧。
- 物理层:负责物理媒介传输的,例如电缆、光纤或无线信号。这一层的数据叫比特。
TCP/IP模型是是一种用于组织和描述计算机网络通信的标准协议,它是互联网最常用的协议栈。主要分为四个层级:
- 应用层:该层与OSI模型的应用层和表示层以及会话层类似,提供直接与用户应用程序交互的接口。常见的协议有电子邮件(SMTP)、网页浏览(HTTP)、文件传输(FTP)等。
- 传输层:该层对应OSI模型的传输层。它负责端到端的数据传输服务,提供可靠的、无连接的数据传输服务。常见的协议有TCP和UDP。TCP提供面向连接且可靠的数据传输,确保数据的准确性和完整性;而UDP是无连接的,适用于不要求可靠性的传输,如实时音频和视频流。
- 网际层:该层对应OSI模型的网络层。主要协议是IP,它负责数据包的路由和转发,选择最佳路径将数据从源主机传输到目标主机。IP协议使用IP地址来识别主机和网络,并进行逻辑地址寻址。
- 网络接口层:该层对应OSI模型的数据链路层和物理层。它负责物理传输媒介的传输,例如以太网、WiFi等,并提供错误检测和纠正的功能。此外该层还包括硬件地址——MAC地址的管理。
从输入URL到页面展示发生了什么?***
- URL 输入:用户在浏览器的地址栏中输入URL,例如”https://www.example.com"
- 域名解析:浏览器通过域名系统(DNS)将域名解析为IP地址,以确定要连接的服务器位置。
- 建立连接:浏览器使用解析得到的 IP 地址,与服务器建立网络连接。这通常涉及使用 TCP 协议进行三次握手。
- 发送请求:浏览器向服务器发送 HTTP 请求,请求服务器的网页内容。请求中包含了要访问的路径、方法(GET、POST等)、头部信息等。
- 服务器处理:服务器接收到请求后,根据请求的内容和路径,处理请求并返回响应。服务器可能从数据库中获取数据,生成动态内容,然后将响应发送回浏览器。
- 接收响应:浏览器接收到服务器的响应,响应包含了 HTTP 状态码、头部信息和页面内容等。
- 解析和渲染:浏览器开始解析响应内容,构建文档对象模型(DOM)和渲染树。它解析HTML、CSS和JavaScript,并确定页面的结构、样式和行为。
- 页面渲染:浏览器使用渲染树和样式信息,将页面内容绘制到屏幕上。这包括布局、绘制和显示页面元素。
- 执行JavaScript:如果页面包含JavaScript,浏览器会执行JavaScript 代码,添加交互和动态行为。
- 加载资源:页面中可能包含外部资源,如图片、样式表、脚本文件等。浏览器会根据需要下载这些资源,以完整地呈现页面。
- 完成页面加载:页面的所有内容和资源加载完成后,浏览器显示完整的页面。
Day24
HTTP请求报文和响应报文是怎样的?**
HTTP请求报文主要由请求行、请求头、请求体构成。下面是一个GET请求报文示例:
1 | GET /example/index.html |
- 请求行:(请求方法 URL 协议版本号)
- 请求方法: GET、POST、PUT、DELETE、PATCH、HEAD、CONNECT、OPTIONS、TRACE
- URL:
<协议>://<主机>:<端口>/<路径>?<参数>
- 协议版本号:HTTP版本号
- 请求头:包含请求的附加信息,由key:value组成,它可以包含很多不同的字段,用于告知服务器有关请求的详细信息。些常见的请求头部字段包括:
- Host:指定服务器的主机名和端口号
- User-Agent:标识客户端的用户代理(浏览器或其他工具)
- Accept:指定客户端可以接受的响应数据类型。
- Content-Type:指定请求主体的数据类型
- Authorization:用于进行身份验证的凭据。
- 空行:空行是请求头部和请求主体之间的空行,用于分隔请求头部和请求主体。
- 请求体:承载多个请求参数的数据,请求主体是可选的,通常在发送POST、PUT等请求时包含请求的实际数据。例如使用POST请求提交表单数据或上传文件时,请求体会包含这些数据。
HTTP响应报文是服务器向客户端返回的数据格式,用于传达服务器对客户端请求的处理结果以及相关数据。一个标准的HTTP响应报文通常包含状态行、响应头、响应体。
1 | 200 OK |
- 状态行:(协议版本号 状态码 状态消息)
- 协议版本号:HTTP版本号
- 状态码:服务器处理结果的三位数字号码
- 状态消息:对状态码的简要描述
- 响应头:响应头部也是以键值对的形式提供的额外信息,类似于请求头部,用于告知客户端有关响应的详细信息。一些常见的响应头部字段包括:
- Content-Type:指定响应主体的MIME类型:
- Content-Length:指定响应主体的长度(字节数)
- Server:指定服务器的信息。
- Location:在重定向时指定新的资源位置。
- Set-Cookie:在响应中设置Cookie。
- 空行:空行是响应头部和响应主体之间的空行,用于分隔响应头部和响应主体。
- 响应体:响应主体包含服务器返回给客户端的实际数据。例如,当请求一个网页时,响应主体将包含HTML内容。响应主体的存在与否取决于请求的性质以及服务器的处理结果。
HTTP请求方式有哪些?**
HTTP1.1规定了九种标准请求方式,包括GET、POST、HEAD、PUT、DELETE、TRACE、PATCH、CONNECT、OPTIONS。其中最常用的就是GET和POST请求。详细如下:
- GET:申请获取资源,不对服务器产生影响
- POST:POST请求通常用于发送数据,例如提交表单数据、上传文件等,会影响服务器,服务器可能动态创建新的资源或更新原有资源。
- HEAD:类似GET,仅要求服务器返回头部信息,不返回实际的资源内容
- PUT:用于更新服务器上的资源或创建新资源。(与POST的区别是:PUT通常指定了资源的存放位置,而POST则没有,POST的数据存放位置由服务器自己决定。)
- DELETE:请求服务器删除指定的资源。
- TRACE:用于测试。要求目标服务器返回原始的HTTP请求内容。
- PATCH:用于对资源进行部分更新。
- CONNECT:用于代理服务器。
- OPTIONS:用于获取服务器支持的HTTP方法列表,以及针对指定资源支持的方法。
GET请求和POST请求的区别?***
- 使用场景:GET请求和POST请求都是HTTP请求方法,GET请求用于从服务器获取数据,POST请求用于向服务器发送数据。
- 传递参数
- GET请求的参数一般写在URL中,所以GET传送的数据量较小,不能大于2KB,且只接受ASCII字符
- POST请求参数一般放在请求体中,所以其请求信息没有长度限制,对于数据类型也没有限制
- 安全和幂等
安全:在HTTP协议中安全指的是请求方法不会破坏服务器上的资源。
幂等:多次执行相同的操作,结果都相同。
- GET为安全幂等的,因为它为只读操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的
- POST因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。
- 缓存机制:GET请求可以被浏览器缓存,POST请求不会被缓存。
- 时间消耗
- GET 产生一个 TCP 数据包,浏览器会把 header 和 data 一并发送出去,服务器响应 200(返回数据)
- POST产生两个TCP 数据包,对于POST,浏览器先发送 Header,服务器响应 100 continue,浏览器再发送data,服务器响应 200 ok(返回数据)
- 编码方式
- GET 请求只能进行 URL编码
application/x-www-form-urlencoded
- POST 支持多种编码方式
application/x-www-form-urlencoded
或multipart/form-data
(为二进制数据使用多种编码)。
Day25
HTTP请求中常见的状态码?**
什么是强缓存和协商缓存?**
强制缓存与协商缓存:概念原理、区别、适用场景和具体示例-CSDN博客
- 强制缓存是浏览器对之前请求过的文件进行缓存,以便下一次访问时重复使用,节省带宽,提高访问速度,降低服务器压力。其工作原理是是通过HTTP响应头中的特定字段来控制的,例如
Expires
和Cache-Control
,它们指示了资源缓存的有效时间。当浏览器在有效时间内再次请求同一资源时,它会直接从本地缓存中获取该资源,而不会向服务器发送请求。
Expires
强缓存:Expires
表示未来资源会过期的时间点,当时间超过了Expires
设置的时间点,浏览器就会重定向服务器请求资源。
因为
Expires
判断强缓存过期机制是获取本地时间戳与之前拿到资源文件中的Expires
字段的时间做比较,来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:如果本地时间不准确则会导致出现错误。
Cache-Control
强缓存:在HTTP1.1中增加了该字段,它提供了更灵活的缓存控制机制。例如,可以通过max-age
参数设置缓存的最大生存时间(以秒为单位),如Cache-Control: max-age=1200
表示缓存有效时间为1200秒。
Cache-Control
有有六个属性,分别是max-age、s-maxage、no-cache、no-store、private、public。
- max-age决定客户端资源被缓存多久。
- s-maxage决定代理服务器缓存的时长。
- no-cache表示是强制进行协商缓存。
- no-store是表示禁止任何缓存策略。
- public表示资源既可以被浏览器缓存也可以被代理服务器缓存。
- private表示资源只能被浏览器缓存,默认为private。
- 协商缓存是浏览器与服务器之间进行通信以确认缓存资源是否仍然有效的过程。
协商缓存主要涉及两组HTTP头字段:ETag
和If-None-Match
,以及Last-Modified
和If-Modified-Since
。它们的工作原理如下:
ETag/If-None-Match:当浏览器第一次请求某个资源时,服务器会返回一个ETag(实体标签),它是一个资源版本的唯一标识符。浏览器在后续请求该资源时,会在请求头中携带If-None-Match字段,其值为先前接收到的ETag。服务器会根据这个值来判断资源是否有更新。如果有更新,服务器会返回新的资源和新的ETag;如果没有更新,服务器会返回304 Not Modified状态码,告诉浏览器可以使用缓存中的资源。
Last-Modified/If-Modified-Since:类似于ETag机制,但Last-Modified记录的是资源最后修改的时间。浏览器在后续请求时,会在请求头中携带If-Modified-Since字段,其值为先前接收到的Last-Modified时间。服务器会检查资源的最后修改时间是否在这个时间之后。如果是,说明资源有更新,服务器会返回新资源和新的Last-Modified时间;如果不是,服务器同样会返回304 Not Modified状态码。
todo…
Day26
HTTP1.0和HTTP1.1的区别?***
- 长连接
- HTTP1.1支持长连接,每一个TCP连接上可以传送多个HTTP请求和响应,默认开启Connection: Keep-Alive。
- HTTP1.0默认为短连接,每次请求都需要建立一个TCP连接。
- 缓存
- HTTP1.0主要使用If-Modified-Since/Expires来做为缓存判断的标准。
- HTTP1.1则引入了更多的缓存控制策略例如Entity tag, If-None-Match等更多可供选择的缓存头来控制缓存策略。
- 管道化
- 基于HTTP1.1的长连接,使得请求管线化成为可能。管线化使得请求能够“并行”传输,但是响应必须按照请求发出的顺序依次返回,性能在一定程度上得到了改善。
- 增加Host字段
- 使得一个服务器能够用来创建多个 Web 站点。
- 状态码
- 新增了24个错误状态响应码。
- 带宽优化
- HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能。
- HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content)。
HTTP2.0与HTTP1.1的区别?***
- 二进制分帧:在应用层(HTTP/2.0)和传输层(TCP or UDP)之间增加一个二进制分帧层,从而突破 HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。
- 多路复用(MultiPlexing),允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息,这个强大的功能则是基于“二进制分帧”的特性。
- 首部压缩,HTTP1.1 不支持 header 数据的压缩,HTTP/2.0 使用 HPACK 算法对 header 的数据进行压缩,这样数据体积小了,在网络上传输就会更快。高效的压缩算法可以很大的压缩 header ,减少发送包的数量从而降低延迟。
- 服务端推送(server push),在 HTTP/2 中,服务器可以对客户端的一个请求发送多个响应,即服务器可以额外的向客户端推送资源,而无需客户端明确的请求。
HTTP3.0有了解过吗?**
Day27
HTTPS和HTTP有哪些区别?***
- HTTP 是明文传输,HTTPS 通过 SSL\TLS 进行了加密。
- HTTP 的端口号是 80,HTTPS 是 443。
- HTTPS 需要到 CA 申请证书。
- HTTP 的连接简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。
HTTPS工作原理?**
- 首先,客户端向服务器端发送请求报文,请求与服务端建立连接。
- 服务端产生一对公私钥,然后将自己的公钥发送给CA机构,CA机构也有一对公私钥,然后CA机构使用自己的私钥将服务端发送过来的公钥进行加密,产生一个CA数字证书。
- 服务端响应客户端的请求,将CA机构生成的数字证书发送给客户端。
- 客户端将服务端发送过来的数字证书进行解析(因为浏览器产商跟CA机构有合作,所以浏览器中已经保存了大部分CA机构的密钥,用于对服务端发送过来的数字证书进行解密),验证这个数字证书是否合法,如果不合法,会发送一个警告。如果合法,取出服务端生成的公钥。
- 客户端取出公钥并生成一个随机码key(其实就是对称加密中的密钥)
- 客户端将加密后的随机码key发送给服务端,作为接下来的对称加密的密钥
- 服务端接收到随机码key后,使用自己的私钥对它进行解密,然后获得到随机码key。
- 服务端使用随机码key对传输的数据进行加密,在传输加密后的内容给客户端
- 客户端使用自己生成的随机码key解密服务端发送过来的数据,之后,客户端和服务端通过对称加密传输数据,随机码Key作为传输的密钥。
Day28
TCP和UDP的区别?***
- TCP与UDP的概念
- TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
- UDP(用户数据报协议)为应用程序提供了一种无需建立连接就可以发送封装的IP数据包的方法。
- 特点
- TCP:面向连接,传输可靠,传输形式为字节流,传输效率慢,所需资源多。
- UDP:无连接、传输不可靠、传输形式为数据报文段,传输效率快,所需资源少。
- 区别
3.1、是否面向连接: TCP 是面向连接的传输,UDP 是无连接的传输。
3.2、是否是可靠传输:TCP是可靠的传输服务,在传递数据之前,会有三次握手来建立连接;在数据传递时,有确认、窗口、重传、拥塞控制机制。 UDP是不可靠传输,数据传递不需要给出任何确认,且不保证数据不丢失及到达顺序。
3.3、是否有状态:TCP 传输是有状态的,它会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等,而 UDP 是无状态的。
3.4、传输形式: TCP 是面向字节流的,UDP 是面向报文的。
3.5、传输效率:由于TCP 传输的时候多了连接、确认重传等机制,所以TCP 的传输效率要比UDP 低。
3.6、首部开销 :TCP 首部开销 (20 ~ 60字节)比UDP 首部开销 (8字节)要大。
3.7、是否提供广播或多播服务: TCP 只支持点对点通信UDP 支持一对一、一对多、多对一、多对多。
- 对应的使用场景 TCP常用于要求通信数据可靠场景(如网页浏览、文件传输、邮件传输、远程登录、数据库操作等)。 UDP常用于要求通信速度高场景(如域名转换、视频直播、实时游戏等)。
TCP连接如何确保可靠性?***
建立连接:通过三次握手建立连接,保证连接实体真实存在。
序列号:保证数据是按序、完整到达。
确认机制:通过 ARQ 协议实现。基本原理是每发完⼀个分组就停止发送,等待对方确认。如果没收到确认,会重发数据包,直到确认后再发下⼀个分组。
合理分片:tcp会按最大传输单元(MTU)合理分片,接收方会缓存未按序到达的数据,重新排序后交给应用层。
校验和:TCP报文头有校验和,用于校验报文是否损坏。
超时重传:如果发送一直收不到应答,可能是发送数据丢失,也可能是应答丢失,发送方再等待一段时间之后都会进行重传。
流量控制:当接收方来不及处理发送方的数据,能通过滑动窗口,提示发送方降低发送的速率,防止包丢失。
拥塞控制:网络层拥堵造成的拥塞,包括慢启动,拥塞避免,快速重传三种机制。
拥塞控制算法主要有以下几种:
慢启动
在连接刚开始时,发送方会逐渐增加发送窗口大小,从而以指数增长的速度增加发送的数据量
拥塞避免
一旦慢启动阶段过去,发送方进入拥塞避免阶段。在这个阶段,发送方逐渐增加发送窗口的大小,但增加速率较慢,避免过快增加导致网络拥塞。
超时重传
如果发送方在超时时间内未收到确认,它会认为数据包丢失,并重传这些数据包。这是拥塞控制的最后手段,用于检测和处理网络中的丢包或拥塞情况。当网络出现拥塞,也就是会发生数据包重传。
快速重传(Fast Retransmit)和快速恢复(Fast Recovery)
当发送方发送的数据包丢失或网络出现拥塞时,接收方会发送重复确认(duplicate ACK)通知发送方有数据包丢失。当发送方收到一定数量的重复确认时,它会立即重传丢失的数据包,而不是等待超时。这样可以减少网络的拥塞程度。
拥塞窗口调整
发送方根据网络的拥塞程度动态调整发送窗口的大小,通过监测网络延迟和丢包情况来确定合适的发送速率,以避免网络拥塞。
UDP怎么实现可靠传输?**
字节一面:如何用 UDP 实现可靠传输? - 知乎 (zhihu.com)
Day29
三次握手的过程,为什么是三次?***
三次握手才可以防止失效的连接请求被服务端接受从而残生错误。(主要原因)
三次握手才可以同步双方的初始序列号。因为两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
三次握手才可以避免资源浪费。
四次挥手的过程,为什么是四次?***
TCP 是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后才会完全关闭了 TCP 连接。 总结:两次握手可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。
TIME-WAIT状态为什么要等待 2MSL 时间后才进入CLOSED状态?
为了保证服务端能收到客户端的确认应答。若客户端发完确认应答后直接进入CLOSED状态,那么如果该应答丢失,服务端等待超时后就会重新发送连接释放请求,但此时客户端已经关闭了,不会作出任何响应,因此服务端永远无法正常关闭。
HTTP的Keep-Alive是什么?TCP 的Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?**
HTTP 的 Keep-Alive,是由应用层(用户态)实现的,称为 HTTP 长连接;
每次请求都要经历这样的过程:建立 TCP ->请求资源 ->响应资源 ->释放连接,这就是HTTP短连接,但是这样每次建立连接都只能请求一次资源,所以HTTP 的 Keep-Alive实现了使用同一个TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,就就是 HTTP 长连接。
TCP 的 Keepalive,是由 TCP 层(内核态)实现的,称为 TCP 保活机制;
通俗地说,就是TCP有一个定时任务做倒计时,超时后会触发任务,内容是发送一个探测报文给对端,用来判断对端是否存活。Day30
DNS查询过程?**
CDN是什么?**
Cookie和Session是什么?有什么区别?**
Cookie和Session都是Web开发中用于跟踪用户状态的技术,但它们在存储位置、数据容量、安全性以及生命周期等方面存在显著差异:
- 存储位置:Cookie的数据存储在客户端(通常是浏览器)。当浏览器向服务器发送请求时,会自动附带Cookie中的数据。Session的数据存储在服务器端。服务器为每个用户分配一个唯一的Session ID,这个ID通常通过Cookie或URL重写的方式发送给客户端,客户端后续的请求会带上这个Session ID,服务器根据ID查找对应的Session数据。
- 数据容量:单个Cookie的大小限制通常在4KB左右,而且大多数浏览器对每个域名的总Cookie数量也有限制。由于Session存储在服务器上,理论上不受数据大小的限制,主要受限于服务器的内存大小。
- 安全性:Cookie相对不安全,因为数据存储在客户端,容易受到XSS(跨站脚本攻击)的威胁。不过,可以通过设置HttpOnly属性来防止JavaScript访问,减少XSS攻击的风险,但仍然可能受到CSRF(跨站请求伪造)的攻击。Session通常认为比Cookie更安全,因为敏感数据存储在服务器端。但仍然需要防范Session劫持(通过获取他人的Session ID)和会话固定攻击。
- 生命周期:Cookie可以设置过期时间,过期后自动删除。也可以设置为会话Cookie,即浏览器关闭时自动删除。Session在默认情况下,当用户关闭浏览器时,Session结束。但服务器也可以设置Session的超时时间,超过这个时间未活动,Session也会失效。
- 性能:使用Cookie时,因为数据随每个请求发送到服务器,可能会影响网络传输效率,尤其是在Cookie数据较大时。使用Session时,因为数据存储在服务器端,每次请求都需要查询服务器上的Session数据,这可能会增加服务器的负载,特别是在高并发场景下。
Day30(dns查询、cdn、cookie/session)
DNS查询过程?**
DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是域名和 IP 地址的映射问题。
- 首先用户在浏览器输入URL地址后,会先查询浏览器缓存是否有该域名对应的IP地址。
- 如果浏览器缓存中没有,会去计算机本地的Host文件中查询是否有对应的缓存。
- 如果Host文件中也没有则会向本地的DNS解析器(通常由你的互联网服务提供商(ISP)提供)发送一个DNS查询请求。
- 如果本地DNS解析器没有缓存该域名的解析记录,它会向根DNS服务器发出查询请求。根DNS服务器并不负责解析域名,但它能告诉本地DNS解析器应该向哪个顶级域(.com/.net/.org)的DNS服务器继续查询。
- 本地DNS解析器接着向指定的顶级域DNS服务器发出查询请求。顶级域DNS服务器也不负责具体的域名解析,但它能告诉本地DNS解析器应该前往哪个权威DNS服务器查询下一步的信息。
- 本地DNS解析器最后向权威DNS服务器发送查询请求。 权威DNS服务器是负责存储特定域名和IP地址映射的服务器。当权威DNS服务器收到查询请求时,它会查找”example.com”域名对应的IP地址,并将结果返回给本地DNS解析器。
- 本地DNS解析器将收到的IP地址返回给浏览器,并且还会将域名解析结果缓存在本地,以便下次访问时更快地响应。
CDN是什么?**
CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。
通俗易懂的解释:
通过IP访问实际资源时,如果CDN上并没有缓存资源,则会到源站请求资源,并缓存到CDN节点上,这样,用户下一次访问时,该CDN节点就会有对应资源的缓存了。
作用:
- 加速用户对该网站的打开速度,提升用户体验。
- 网站不容易宕机,多个服务器达到负载均衡,不会让流量集中涌入其中某一台服务器。
- 解决延迟问题,提高直播等相关内容的时效性。
Cookie 和 Session 是什么?又什么区别?
Cookie 和 Session 都用于管理用户的状态和身份,Cookie 通过在客户端记录信息确定用户
身份, Session 通过在服务器端记录信息确定用户身份。
- Cookie
- Cookie 是存储在用户浏览器中的小型文本文件,用于在用户和服务器之间传递数据。通常,服务器会将一个或多个 Cookie 发送到用户浏览器,然后浏览器将这些 Cookie 存储在本地。
- 服务器在接收到来自客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从而动态生成与该客户端相对应的内容。
- Session
客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session。Session 主要用于维护用户登录状态、存储用户的临时数据和上下文信息等。
区别:
- 存储位置:Cookie 数据存储在用户的浏览器中,而 Session 数据存储在服务器上。
- 数据容量:Cookie 存储容量较小,一般为几 KB。Session 存储容量较大,通常没有固定限制,取决于服务器的配置和资源。
- 安全性:由于 Cookie 存储在用户浏览器中,因此可以被用户读取和篡改。相比之下Session 数据存储在服务器上,更难被用户访问和修改。
- 传输方式:Cookie 在每次 HTTP 请求中都会被自动发送到服务器,而 Session ID 通常通过 Cookie 或 URL 参数传递。
操作系统
Day31
进程和线程的区别?***
进程是系统进行资源调度和分配的的基本单位,实现了操作系统的并发。
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。一个进程由一个或多个线程组成,这些线程共享同一块内存。
区别:
- 资源开销
- 进程:由于每个进程都有独立的内存空间,创建和销毁进程的开销较大。进程间切换需要保存和恢复整个进程的状态,因此上下文切换的开销较高。
- 线程:线程共享相同的内存空间,创建和销毁线程的开销较小。线程间切换只需要保存和恢复少量的线程上下文,因此上下文切换的开销较小。
- 通信与同步
- 进程:由于进程间相互隔离,进程之间的通信需要使用一些特殊机制,如管道、消息队列、共享内存等。
- 线程:由于线程共享相同的内存空间,它们之间可以直接访问共享数据,线程间通信更加方便。
- 安全性
- 进程:由于进程间相互隔离,一个进程的崩溃不会直接影响其他进程的稳定性。
- 线程:由于线程共享相同的内存空间,一个线程的错误可能会影响整个进程的稳定性。
并行和并发有什么区别?**
并行是在同一时刻执行多个任务
并发是在相同的时间段内执行多个任务,任务可能交替执行,通过调度实现。
并行是指在同一时刻执行多个任务,这些任务可以同时进行,每个任务都在不同的处理单元(如多个CPU核心)上执行。在并行系统中,多个处理单元可以同时处理独立的子任务,从而加速整体任务的完成。
并发是指在相同的时间段内执行多个任务,这些任务可能不是同时发生的,而是交替执行,通过时间片轮转或者事件驱动的方式。并发通常与任务之间的交替执行和任务调度有关。
解释一下用户态和核心态?**
用户态 User Mode
和核心态 Kernel Mode
是操作系统中两种不同的执行模式,用于控制进程或程序对计算机硬件资源的访问权限和操作范围。
- 用户态:在用户态下,进程或程序只能访问受限的资源和执行受限的指令集,不能直接访问操作系统的核心部分,也不能直接访问硬件资源,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。
- 核心态:核心态是操作系统的特权级别,允许进程或程序执行特权指令和访问操作系统的核心部分。在核心态下,进程可以直接访问硬件资源,执行系统调用,管理内存、文件系统等操作。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0的状态我们称之为内核态。
什么场景下,会发生内核态和用户态的切换
- 系统调用:当用户程序需要请求操作系统提供的服务时,会通过系统调用进入内核态
- 异常:当程序执行过程中出现错误或异常情况时,CPU会自动切换到内核态,以便操作系统能够处理这些异常。
- 中断:外部设备(如键盘、鼠标、磁盘等)产生的中断信号会使CPU从用户态切换到内核态。操作系统会处理这些中断,执行相应的中断处理程序,然后再将CPU切换回用户态
Day32
进程调度算法你了解多少?**
进程调度算法是操作系统中用来管理和调度进程(也称为任务或作业)执行的方法。这些算法决定了
在多任务环境下,如何为各个进程分配 CPU 时间,以实现公平性、高吞吐量、低延迟等不同的调度
目标。
- 先来先服务调度算法
按照进程到达的先后顺序进行调度,即最早到达的进程先执行,直到完成或阻塞,
- 最短作业优先调度算法
优先选择运行时间最短的进程来运行
- 高响应比优先调度算法
综合考虑等待时间和服务时间的比率,选择具有最高响应比的进程来执行
- 时间片轮转调度算法将CPU 时间划分为时间片(时间量),每个进程在一个时间片内运行,然后切换到下一个进程。
- 最高优先级调度算法
为每个进程分配一个优先级,优先级较高的进程先执行。这可能导致低优先级进程长时间等待可能引发饥饿问题。
- 多级反馈队列调度算法
将进程划分为多个队列,每个队列具有不同的优先级,进程在队列之间移动。具有更高优先级的队列的进程会更早执行,而长时间等待的进程会被提升到更高优先级队列。
- 最短剩余时间优先
每次选择剩余执行时间最短的进程来执行。
- 最大吞吐量调度
旨在最大化单位时间内完成的进程数量
- 最大吞吐量调度
旨在最大化单位时间内完成的进程数量
进程间有哪些通信方式?***
- 管道:是一种半双工的通信方式,数据只能单向流动而且只能在具有父子进程关系的进程间使用。
- 命名管道:也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 信号量:是一个计数器,可以用来控制多个进程对共享资源的访问,常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此主要作为进程间以及同一进程内不同线程之间的同步手段。
- 消息队列:消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号:用于通知接收进程某个事件已经发生,从而迫使进程执行信号处理程序。
- 共享内存:就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的进程通信方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,比如信号量配合使用,来实现进程间的同步和通信。
Socket
套接字:是支持TCP/IP 的网络通信的基本操作单元,主要用于在客户端和服务器之间通过网络进行通信。
解释一下进程同步和互斥,以及如何实现进程同步和互斥?**
进程同步是指多个并发执行的进程之间协调和管理它们的执行顺序,以确保它们按照一定的顺序或时间间隔执行。比如说,你想要和你的队友一起完成一个副本,你们需要相互配合,有时候等待对方的信号或者消息,有时候按照对方的要求执行某些动作,这就是进程同步。
互斥指的是在某一时刻只允许一个进程访问某个共享资源。当一个进程正在使用共享资源时,其他进程不能同时访问该资源。比如说,你想要使用一个祭坛来祈愿,但是这个祭坛一次只能被一个人使用,如果有其他人也想要使用,他们就必须等待你使用完毕后再去使用,这就是进程互斥。
解决进程同步和互斥的问题有很多种方法,其中一种常见的方法是使用信号量和 PV 操作。信号量是一种特殊的变量,它表示系统中某种资源的数量或者状态。PV 操作是一种对信号量进行增加或者减少的操作,它们可以用来控制进程之间的同步或者互斥。
举个例子,假设有一个信号量s表示一个祭坛是否可用,初始值为 1。如果 s 的值为 1,表示祭坛空闲;如果 s 的值为 0,表示祭坛被占用;如果 s 的值为 -1,表示有一个人在等待使用祭坛。那么我们可以用 PV 操作来实现对祭坛的互斥访问:
- 如果你想要使用祭坛,你就执行 P(s)操作,将 s 的值减 1。如果结果为 0或者正数,表示你可以使用祭坛;如果结果为负数,表示有人在使用祭坛,你就必须等待。
- 如果你使用完了祭坛,你就执行 V(s)操作,将 s 的值加 1。如果结果为正数或者 0,表示没有人在等待使用祭坛;如果结果为负数,表示有人在等待使用祭坛,你就需要唤醒他们中的一个。
这样就可以保证每次只有一个人能够使用祭坛,实现了进程互斥。
除此之外,下面的方法也可以解决进程同步和互斥问题:
- 临界区(Critical Section):将可能引发互斥问题的代码段称为临界区。为了实现互斥,每个进程在进入临界区前必须获取一个锁,退出临界区后释放该锁。这确保同一时间只有一个进程可以进入临界区。
- 互斥锁(Mutex):互斥锁是一种同步机制,用于实现互斥。每个共享资源都关联一个互斥锁进程在访问该资源前需要先获取互斥锁,使用完后释放锁。只有获得锁的进程才能访问共享资源。
- 条件变量(Condition Variable):条件变量用于在进程之间传递信息,以便它们在特定条件下等待或唤醒。通常与互斥锁一起使用,以确保等待和唤醒的操作在正确的时机执行。
Day33
什么是死锁,如何预防死锁?***
死锁是指两个或多个进程在争夺系统资源时,由于互相等待对方释放资源而无法继续执行的状态。
死锁只有同时满足以下四个条件才会发生:
- 互斥条件:一个进程占用了某个资源时,其他进程无法同时占用该资源
- 请求保持条件:一个线程因为请求资源而阻塞的时候,不会释放自己的资源
- 不可剥夺条件:资源不能被强制性地从一个进程中剥夺,只能由持有者自愿释放,
- 环路等待条件:多个进程之间形成一个循环等待资源的链,每个进程都在等待下一个进程所占有的资源。
只需要破坏上面一个条件就可以破坏死锁。
- 破坏请求与保持条件:一次性申请所有的资源,
- 破坏不可剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。让所有进程按照相同的顺序请求资源,释放资源则反序释放。
介绍一下几种典型的锁?**
详见day9
两个基础的锁:
- 互斥锁:互斥锁是一种最常见的锁类型,用于实现互斥访问共享资源。在任何时刻,只有一个线程可以持有互斥锁,其他线程必须等待直到锁被释放。这确保了同一时间只有一个线程能够访问被保护的资源。
- 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询,直到锁被释放。
其他的锁都是基于这两个锁的
- 读写锁:允许多个线程同时读共享资源,只允许一个线程进行写操作。分为读(共享)和写(排他)两种状态。
- 悲观锁:认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁
- 乐观锁:先不管,修改了共享资源再说,如果出现同时修改的情况,再放弃本次操作
什么是虚拟内存?为什么需要虚拟内存?**
虚拟内存在每一个进程创建加载的过程中,会分配一个连续虚拟地址空间,它不是真实存在的,而是通过映射与实际地址空间对应,这样就可以使每个进程看起来都有自己独立的连续地址空间,并允许程序访问比物理内存 RAM 更大的地址空间,每个程序都可以认为它拥有足够的内存来运行。
需要虚拟内存的原因:
- **内存扩展: **虚拟内存使得每个程序都可以使用比实际可用内存更多的内存,从而允许运行更大的程序或处理更多的数据
- 内存隔离:虚拟内存还提供了进程之间的内存隔离。每个进程都有自己的虚拟地址空间,因此一个进程无法直接访问另一个进程的内存,
- 物理内存管理:虚拟内存允许操作系统动态地将数据和程序的部分加载到物理内存中,以满足当前正在运行的进程的需求。当物理内存不足时,操作系统可以将不常用的数据或程序暂时移到硬盘上,从而释放内存,以便其他进程使用。
- 页面交换:当物理内存不足时,操作系统可以将一部分数据从物理内存写入到硬盘的虚拟内存中,这个过程被称为页面交换。当需要时,数据可以再次从虚拟内存中加载到物理内存中。这样可以保证系统可以继续运行,尽管物理内存有限。
- 内存映射文件:虚拟内存还可以用于将文件映射到内存中,这使得文件的读取和写入可以像访问内存一样高效。
Day34
你知道的线程同步的方式有哪些?**
线程同步机制是指在多线程编程中,为了保证线程之间的互不干扰,而采用的一种机制。常见的线程同步机制有以下几种:
- 互斥锁:互斥锁是最常见的线程同步机制。它允许只有一个线程同时访问被保护的临界区(共享资源)。
- 条件变量:条件变量用于线程间通信,允许一个线程等待某个条件满足,而其他线程可以发出信号通知等待线程。通常与互斥锁一起使用。
- 读写锁:读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。
- 信号量:用于控制多个线程对共享资源进行访问的工具。
有哪些页面置换算法?**
页面置换算法详解 - Leophen - 博客园 (cnblogs.com)
操作系统之页面置换算法——FIFO、OPT和LRU_fifo替换算法-CSDN博客
页面置换算法是什么?
进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就需要从内存中调出一页程序或数据,送入磁盘的对换区,其中选择调出页面的算法就称为页面置换算法。
好的页面置换算法应有较低的页面更换频率,也就是说,应将以后不会再访问或者以后较长时间内不会再访问的页面先调出。
Day35
熟悉哪些Linux命令?**
- 文件相关(mv mkdir cd ls)
- 进程相关( ps top netstate )
- 权限相关(chmod chown useradd groupadd)
- 网络相关(netstat ip addr)
- 测试相关(测试网络连通性:ping;测试端口连通性:telnet)
如何查看某个端口有没有被占用?**
netstat
或ss命令
1 | netstat -tuln | grep 端口号 |
lsof
命令
这个命令是查看进程占用哪些文件的
1 | lsof -i:端口号 |
fuser
命令
fuser
命令和lsof
正好相反,是查看某个文件被哪个进程占用的。
1 | fuser 22/tcp -v |
- nmap工具
nmap默认总是会扫描端口,要扫描本机端口,很方便。
1 | nmap localhost |
说一下 select、poll、epoll?了解I/O多路复用吗?**
图解 | 原来这就是 IO 多路复用 - 闪客sun - 博客园 (cnblogs.com)
select,poll,epoll 都是 I/O 多路复用的机制。
I/O多路复用是一种在单个线程中管理多个输入/输出通道的技术。它允许一个线程同时监听多个输入流(例如网络套接字、文件描述符等),并在有数据可读或可写时进行相应的处理,而不需要为每个通道创建一个独立的线程。
常见的I/O多路复用机制包括select、poll和epoll。这些机制通过将多个I/O通道注册到一个事件管理器中,然后通过阻塞方式等待事件的发生。一旦有事件发生(如有数据可读或可写),线程就会被唤醒,然后可以针对具体的事件进行处理。
- 当我们调用 select 函数时会传入一个文件描述符集合(fd_set),内核会根据 IO 状态对 fd_set 的内容进行修改,从而通知执行 select 函数的进程哪一个文件或者 Socket 是可读的。
select的缺点:
- select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
- select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)
- select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
- poll 和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。
- epoll 主要就是针对select的三个缺点进行了改进。改进如下:
- 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。
- 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
- 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。
数据结构与算法
常见的排序算法说一下?也说一下各个时间复杂度?***
- 冒泡排序:通过相邻元素的比较和交换,每次将最大(或最小)的元素逐步“冒泡”到最后(或最前)。时间复杂度:最好情况下O(n),最坏情况下O(n^2),平均情况下O(n^2)。,空间复杂度:O(1)。
- 插入排序:将待排序元素逐个插入到已排序序列的合适位置,形成有序序列。时间复杂度:最好情况下O(n),最坏情况下O(n^2),平均情况下O(n^2),空间复杂度:O(1)。
- 选择排序(Selection Sort):通过不断选择未排序部分的最小(或最大)元素,并将其放置在已排序部分的末尾(或开头)。时间复杂度:最好情况下O(n^2),最坏情况下O(n^2),平均情况下O(n^2),空间复杂度:O(1)。
- 快速排序(Quick Sort):通过选择一个基准元素,将数组划分为两个子数组,使得左子数组的元素都小于(或等于)基准元素,右子数组的元素都大于(或等于)基准元素,然后对子数组进行递归排序。时间复杂度:最好情况下O(nlogn),最坏情况下O(n^2),平均情况下O(nlogn),空间复杂度:最好情况下O(logn),最坏情况下O(n)。
- 归并排序(Merge Sort):将数组不断分割为更小的子数组,然后将子数组进行合并,合并过程中进行排序。时间复杂度:最好情况下O(nlogn),最坏情况下O(nlogn),平均情况下O(nlogn)。空间复杂度:O(n)。
- 堆排序(Heap Sort):通过将待排序元素构建成一个最大堆(或最小堆),然后将堆顶元素与末尾元素交换,再重新调整堆,重复该过程直到排序完成。时间复杂度:最好情况下O(nlogn),最坏情况下O(nlogn),平均情况下O(nlogn)。空间复杂度:O(1)。