过于简单的就不说了,比如“equals()
和==
”有什么区别,相信大家都会。
Java与JVM知识
JVM的内存结构
- 程序计数器(Program Counter Register):是线程私有的一块内存,记录当前线程执行的字节码指令位置。
- Java虚拟机栈(Java Virtual Machine Stacks):也是线程私有的,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法的调用都会在Java虚拟机栈中生成一个栈帧,用于存储方法的局部变量等信息。
- 本地方法栈(Native Method Stack):也是线程私有的,为Java虚拟机调用Native方法服务。
- Java堆(Java Heap):是所有线程共享的一块内存区域,用于存储对象实例。Java堆还可以分为新生代和老年代,分别用于存储不同生命周期的对象实例。
- 方法区(Method Area):也是所有线程共享的,用于存储类信息、常量池、静态变量、即时编译器编译后的代码等数据。
对于线程私有的内存结构(程序计数器、Java虚拟机栈、本地方法栈),不同线程之间互不干扰,是各自独立的。而对于所有线程共享的内存结构(Java堆、方法区),需要进行线程同步,保证线程安全。
JVM的内存一般分为五个区域。
区域 | 线程拥有 | 作用 | 大小 |
---|---|---|---|
程序计数器 | 线程私有 | 记录下一条指令的地址 | 很小 |
虚拟机栈 | 线程私有 | 存放方法的调用 | 一般 |
本地方法栈 | 线程私有 | C/C++写的native方法 | 一般 |
虚拟机堆 | 共享 | 创建的对象等数据 | 大 |
方法区 | 共享 | 常量、静态变量等 | 一般 |
注意:
- 程序计数器很小,不会溢出内存(Out Of Memory)。
- 虚拟机堆是垃圾回收的重点照顾区域。
- 诸如int等8种基本变量不在堆里,而是在栈里;而常量、静态变量也不在堆里,在方法区。
JVM虚拟机堆的内存结构
JVM虚拟机堆主要包括老生代、新生代、元空间三部分。其中新生代又包括了 Eden、S0、S1三个区域。
(编者注:Eden 是英文里“伊甸园”的含义,大家可以理解为产房。Survivor 0、Survivor 1 两个区域意思是“幸存者0”“幸存者1”,简称为 S0、S1)
堆的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
一个对象诞生于 Eden 区。当 Eden 区人满为患时,JVM 使用 minor gc,把 Eden 区已经死掉的对象清理掉。然后把 S0 中的对象、Eden 区幸存的对象,移动到 S1 区。(当然,也可以是从 S1 移动到 S0 区)这个时候所有的对象“年龄”都 +1。当年龄达到一定程度时,就会被移动到老生区。
死亡对象判断方法
引用计数法
如果别的地方有对这个对象的引用,那么引用计数 +1。如果没有别的地方引用了,就判断这个对象死亡。
这个算法不是很好,因为如果两个对象相互引用,而不被其它的地方引用,那么他们并不会被判断死亡。
可达性分析法
把对象之间的引用关系看做一张图(或一棵树),从垃圾回收根节点(GC Roots)出发,能够达到的对象,就是还存活的。无法达到的对象,就是已经死亡的。
Java的垃圾回收是怎样的机制
Java中的垃圾回收是自动化的,它会在程序运行时自行管理内存的分配和回收。Java的垃圾回收机制会定期扫描堆内存中的对象,标记出无法回收的对象并清除掉不再被引用的对象以释放内存。Java采用了可达性分析算法来判断对象是否可以回收。如果一个对象再也无法通过任何引用达到,那么它就可以被回收。此外,Java还提供了finalize()
方法让开发人员在对象被回收之前做一些清理工作。
因为垃圾回收机制和内存管理是由虚拟机完成的,所以Java程序员无需自己手动释放内存,这是Java的自动化垃圾回收机制带来的好处。
Java垃圾回收常用的算法包括标记-清除、标记-整理、复制算法和分代算法。
-
标记-清除算法会标记不需要回收的对象,然后清除掉需要回收的对象;
-
标记-整理算法会对所有存活的对象进行整理以减少内存碎片;
-
标记-复制算法会将存活的对象复制到新的空间中,然后清除旧空间;
-
而分代算法则是将对象按照生命周期的长短分为不同的代,在不同的代中采用不同的回收算法来提升垃圾回收效率。
Java的异常/错误及其分类
Java中的异常主要分为三类:受检异常、运行时异常和错误。
- 受检异常(Checked Exception):这类异常在编译期就需要被检查到并进行处理,否则编译器会报错。如IOException和SQLException等。这类异常在处理时需要使用try-catch语句或者throws声明抛出。
- 运行时异常(Unchecked Exception):这类异常在程序运行时才会抛出,如空指针异常(NullPointerException)和数组下标越界异常(ArrayIndexOutOfBoundsException)等。这种异常通常是由于程序逻辑错误或者意外情况导致的。程序员通常不需要对此类异常进行特别处理。
- 错误(Error):这类异常通常是由于系统错误或者资源耗尽等严重问题导致的,如OutOfMemoryError等。与运行时异常类似,错误也不需要特别处理,通常会导致程序崩溃,而需要排查和修复错误产生的根本原因。
所以,处理异常需要根据异常类型和情况选择合适的方式进行处理,通常我们会对受检异常进行捕获和处理,对于运行时异常和错误则不需要特别处理。
Java的反射
反射是Java程序运行时获取程序结构(如类、接口、变量、方法、注解等)信息的一种能力。它允许程序在运行时获取一个类的完整构造,并进行操作,比如实例化对象、获取属性、调用方法等。通过反射,程序可以动态地创建对象、读取和修改对象的属性值和调用对象的方法,同时也可以操作类的元数据,例如泛型信息、注解等。
用途:
- 动态地创建对象:通过反射可以在运行时根据类名动态地创建对象。
- 访问私有变量和方法:反射可以访问和修改类中的私有变量和方法,这在某些情况下是十分必要的。
- 动态代理:通过反射可以动态地生成代理类,从而实现AOP编程。
- 编写通用代码:反射可以使代码更加通用,从而减少代码量。
使用JDBC时,为什么要反射
动态加载JDBC驱动程序的类,从而使得JDBC驱动程序能够被使用。因为JDBC驱动程序不是由Java语言编写的,它们是通过本地代码实现的,因此需要使用本地库。而在Java中,通过反射机制可以动态获取并加载本地库,使得JDBC驱动程序能够被正确加载和使用。
Java类加载的过程
Java类加载的过程可分为三个阶段:
- 加载:通过类加载器将class文件加载到JVM中,并生成对应的Class对象。
- 链接:
2.1. 验证:确保加载的类符合Java语言规范和JVM规范,避免安全问题。
2.2. 准备:为类变量分配内存空间,并设置默认值。
2.3. 解析:将符号引用转换为直接引用,如将变量或方法的名字转换为内存地址。 - 初始化:为类变量赋初始值,并执行静态代码块。
如果有父类,则先进行父类加载。如果这些阶段中存在错误,便会抛出ClassNotFoundException
、NoClassDefFoundError
等异常。
(编者注:其它书籍、文档、博客可能会使用别的说法,例如把2.链接中的三个步骤拆开来。)
Java中和class有关的Exception
-
ClassNotFoundException
,表示在运行时找不到指定的类,通常是因为类路径或者类名称不正确。检查型。 -
NoClassDefFoundError
,表示无法找到类定义,通常是因为代码试图使用某个类,但找不到该类的定义。非检查型,而且是Error
。 -
NoSuchMethodError
,表示代码试图调用不存在的方法,通常是因为当前版本的类库与代码编译时使用的类库不同。 -
IllegalAccessException
,表示试图访问私有成员或受保护的成员,但没有足够的权限。 -
InstantiationException
,表示试图创建抽象类或接口的实例,或试图创建没有无参构造函数的类的实例。
Class.forName()、ClassLoader.loadClass()有什么区别
-
Class.forName()
在加载类的过程中,会对类进行初始化。 -
ClassLoader.loadClass()
不会自动初始化类,需要显式调用Class.newInstance()
或Class.getDeclaredConstructor()
创建实例对象来启动初始化。
Java并发编程知识
进程与线程的概念是什么?有什么区别?
进程是程序在执行过程中分配和管理资源的基本单位。包含了代码、数据以及执行的上下文等相关信息。操作系统会为每一个进程分配一定的资源,例如内存、CPU时间片等。
线程是进程中的一个执行单元,一个进程可以包含多个线程。线程共享进程的资源,包括代码、数据和上下文等,因此在同一个进程中的多个线程之间可以直接共享数据。同时,线程也有自己的栈空间和寄存器数据。
进程和线程的主要区别在于:
- 进程之间相互隔离,不能直接访问和修改对方的内存空间;而线程之间可以共享同一个进程的资源;
- 每个进程有独立的内存空间,但是进程内的多个线程共享同一个内存空间;
- 创建和销毁线程的开销比创建和销毁进程的开销小,因此多用于并发环境下的处理。
(编者注:可能在其它的博客、文档、书籍里,有说得更清楚的。)
为什么要多线程编程?什么场景下使用多线程编程?
多线程编程可以提高程序的执行效率和响应速度。当程序需要处理多个任务时,使用多线程可以让不同的任务并行执行,避免单线程阻塞导致程序响应缓慢。
多线程编程适用于以下场景:
- 当程序需要执行一些耗时的操作时,使用多线程可以将这些操作放到另一个线程中执行,让主线程可以继续响应用户的操作,提高程序的响应速度。
- 当程序需要执行多个任务时,使用多线程可以同时执行这些任务,提高了程序的并发能力。
- 当需要使用某些第三方库或API时,这些库可能是阻塞式的,也就是说在执行过程中会阻塞当前线程,影响程序的响应速度。使用多线程可以将这些操作放到另一个线程中执行,保持主线程的响应速度。
有哪些并发问题?
-
竞态条件:多个线程(或进程)同时访问共享数据,导致数据的状态不确定性和不一致性。
-
死锁和活锁:多个线程持有互斥锁,但是它们彼此需要对方释放锁才能继续执行,导致线程无法前进。
-
资源不足:多个线程竞争资源,但是资源数限制,导致某些进程无法获取所需资源。
-
数据同步问题:多线程访问同一数据源可能会导致数据不一致,需要使用同步技术来保证数据的一致性。
-
上下文切换开销:多个线程在并发执行时,会频繁地发生上下文切换,导致系统开销增加。
进程间通信、线程间通信的方法有哪些?
进程间通信的方式包括管道、命名管道、共享内存、消息队列、信号量和套接字等。其中,管道和命名管道用于单向通信,共享内存和消息队列用于数据共享,信号量用于进程间进行同步和互斥,套接字则可以实现不同机器间的通信。(笔者注:这套说辞仅适用于Java,应该不是随便两个进程都可以这样的。)
线程间通信的方式包括锁、条件变量、信号量、读写锁和原子变量等。其中,锁用于实现线程之间的互斥,条件变量用于线程之间的通信,信号量也可以用于线程间同步和互斥。读写锁则用于读取和修改共享数据的场景,原子变量则可以保证某些操作的原子性。
线程安全是什么含义?
"线程安全"是指在多线程应用程序中,当多个线程同时访问共享资源时,不会发生不确定的、意外的结果。
线程安全要求:原子性、可见性、有序性。
-
原子性(atomicity)指的是一个操作是不可被中断的整体,在对多个数据进行操作的时候,要么所有数据同时发生变化,要么所有数据都不发生变化。
-
可见性(visibility)指的是一个线程修改的变量能否被其他线程及时感知到。在多线程编程中,如果不保证可见性,可能会出现一个线程修改了共享变量的值,但是其他线程仍然读取的是旧值的情况。
-
有序性(ordering)指的是一个线程中的操作与其他线程中的操作发生的先后顺序可能会存在差异,需要通过特定的机制来保证一定的顺序,以及保证在一个线程中的操作顺序。
引申:用synchronized
上锁保证三个特性。用volatile
保证可见性、有序性,不保证原子性。
守护线程是什么?
守护线程(daemon thread),是服务其他的线程。当所有的非守护线程结束运行时,守护线程会随着虚拟机的关闭而结束运行。
守护线程主要用于在后台执行支持性任务,比如Java垃圾回收线程就是一个典型的守护线程。
start()方法和 run()方法有什么区别?
调用 start()
方法会创建一个新的线程并在新的线程中执行 run()
方法。
而调用 run()
方法则只是在当前线程中直接执行 run()
方法。如果直接调用 run()
方法,那么就不会创建新的线程,而是在当前线程中直接执行,这可能会阻塞主线程。
Java有哪些常见的线程安全的容器?
Java中的线程安全容器主要有以下几种:
- ConcurrentHashMap:适用于高并发环境的哈希表,支持高效的并发读写操作。
- CopyOnWriteArrayList:一个线程安全的ArrayList,它采用了一种写时复制的思想,在写操作时,会进行数据的复制,因此读操作不会阻塞写操作。
- ConcurrentLinkedQueue:基于链表实现的线程安全队列,适用于高并发的生产者消费者模型。
- BlockingQueue:Java中提供的阻塞队列接口,提供了
put
、take
等阻塞方法,能够很好地支持生产者消费者模式。
还有一些其他的线程安全容器,如ThreadLocal等,不过它们的作用和上述容器不太相同。
ConcurrentHashMap是怎么实现线程安全的?
ConcurrentHashMap实现了分段锁,将整个Map的存储空间分成了若干个小段,每一小段都是一个独立的锁。不同的线程可以同时访问这些小段上的不同数据,从而提高了并发性能。
分段锁的另一个好处是,当一个线程访问其中一个小段时,其他线程可以并行地访问其他小段,不会被阻塞。这种机制在高并发的场景下非常有效,可以极大提升ConcurrentHashMap的性能。
CopyOnWriteArrayList是怎么实现线程安全的?
CopyOnWriteArrayList是通过在修改元素的时候不直接修改原始数组,而是先将原始数组复制一份,并在副本上进行修改的方式实现线程安全的。当有线程要修改CopyOnWriteArrayList时,先复制一份当前的元素数组,在副本上进行修改操作,并最终将修改后的副本替换掉原始的元素数组,这样就避免了多线程并发修改同一个数组时可能会出现的问题。
在读取元素时,CopyOnWriteArrayList会直接从原始的数组中读取数据,因为在多线程并发访问的情况下,读取数据是线程安全的。
因此,CopyOnWriteArrayList适用于读多写少的场景,因为每次写入数据都需要复制一份原始数组,这会占用一定的内存空间,加重GC负担,并且会造成数据不一致的问题。
Java线程池的核心参数
- corePoolSize:线程池中保持的最小线程数量。
- maximumPoolSize:线程池中允许的最大线程数量。
- keepAliveTime:当线程池中线程数量超过corePoolSize时,多余的空闲线程的存活时间。
- unit:keepAliveTime的时间单位。
- workQueue:用来保存等待执行的任务的阻塞队列。
- threadFactory:创建新线程的工厂。
- handler:当线程池中的工作队列已满且无法再添加新线程时,线程池将用handler来拒绝新提交的任务。
corePoolSize和maximumPoolSize的区别
corePoolSize表示核心线程池大小,也就是线程池中保持活动状态的线程数,即使这些线程处于空闲状态,也不会被回收。
maximumPoolSize则是线程池最大的线程数,如果队列满了,且当前线程数小于最大线程数,那就会创建新的线程来处理任务。
关于线程池的demo程序
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 定义一个线程池,其中最多有5个线程并行执行
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 向线程池提交10个任务
for (int i = 0; i < 10; i++) {
executorService.execute(new Task(i + 1));
}
// 关闭线程池
executorService.shutdown();
}
/**
* 自定义的任务类,实现Runnable接口
*/
static class Task implements Runnable {
private int taskNum;
public Task(int taskNum) {
this.taskNum = taskNum;
}
@Override
public void run() {
// 执行任务,打印任务编号和当前线程名称
System.out.println("Task " + taskNum + " is running. Thread name: " + Thread.currentThread().getName());
// 睡眠一段时间,模拟任务的执行时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 任务执行完毕,打印结束信息
System.out.println("Task " + taskNum + " is finished. Thread name: " + Thread.currentThread().getName());
}
}
}
死锁的4个条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未完成使用之前,不能被强行剥夺,只能由进程自己释放。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
悲观锁和乐观锁的含义
(概念)悲观锁默认其他线程可能会修改它,因此在访问前会先对资源进行加锁,以防止其他线程的干扰。(应用场景)悲观锁一般采用互斥锁、读写锁等机制来实现。悲观锁适用于写操作比较频繁的场景,如数据库的更新操作。
(概念)乐观锁默认认为对共享资源的修改是少数,因此不用加锁。而是在更新数据时比较数据版本号等信息,如果版本号匹配,就执行更新操作,否则认为数据冲突,需要回滚或者重试。乐观锁一般采用版本号、时间戳等机制来实现。(应用场景)乐观锁适用于读操作比较频繁而写操作比较少的场景,如多用户访问同一笔数据的场景。
乐观锁的CAS算法
CAS(Compare And Swap)算法通过比较内存中的值是否与期望值相同,如果相同,则将新值写入该内存地址;如果不同,则表明有其他线程已经修改过内存中的值,此时需要重新读取内存中的值并再次比较和尝试修改,直到修改成功。这种算法避免了多线程同时写入数据时出现数据混乱的问题,提高了程序的并发性能。
算法
编者按:算法这里就不多说了,就聊聊排序算法吧,被问到的概率比较大。
快速排序算法
快速排序的思路是选取一个基准元素,将数组中小于该元素的放在左边,大于该元素的放在右边,然后递归地对左边和右边重复这个过程,直到排序完成。
以下是Java实现的快速排序函数:
public static void quickSort(int[] arr, int left, int right) {
if (left >= right) { // 如果左右指针相遇或交错,则返回
return;
}
int pivot = partition(arr, left, right); // 选取基准元素
quickSort(arr, left, pivot - 1); // 对左半部分进行快速排序
quickSort(arr, pivot + 1, right); // 对右半部分进行快速排序
}
public static int partition(int[] arr, int left, int right) {
int pivot = arr[right]; // 基准元素选择最右边的一个
int i = left - 1; // 定义i为小于基准元素的区间的末尾
for (int j = left; j < right; j++) {
if (arr[j] < pivot) { // 如果当前元素比基准元素小
i++; // i扩大区间
swap(arr, i, j); // 交换i和j所指的元素
}
}
swap(arr, i+1, right); // 交换i+1和基准元素
return i + 1;
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
快速排序最好、平均和最坏情况下的时间复杂度分别为、
和
。
Java自带的排序算法
Java自带的排序,例如Arrays.sort()
,并不是快速排序算法。而是Tim算法。
Arrays.sort()
使用了一种名为TimSort
的排序算法。它是一种基于归并排序和插入排序的混合排序算法,在处理小数组时使用插入排序,在处理大数组时使用归并排序,并尝试通过充分利用已排序的子序列来提高性能。
常见的排序算法
排序算法 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 是否稳定 |
---|---|---|---|---|
冒泡排序 | 稳定 | |||
选择排序 | 不稳定 | |||
插入排序 | 稳定 | |||
快速排序 | 不稳定 | |||
归并排序 | 稳定 | |||
堆排序 | 不稳定 |
MySQL数据库
MySQL的引擎
- InnoDB是MySQL默认的引擎,支持事务处理和行级锁,适合数据更新较频繁的应用。只支持BTREE索引,不支持HASH索引。
- MyISAM不支持事务处理,但对读操作性能好,适合于数据量大但更新不频繁的应用。
- Memory引擎将表存储在内存中,读写速度非常快,但数据不是持久的,重启后数据将会丢失。
BTREE索引和HASH索引对比
BTREE索引使用B-tree数据结构进行存储,它可以对索引列进行排序,支持范围查找和顺序遍历,适合处理范围查询,比如大于、小于、比较等。但是在进行等值查询时,BTREE索引的查询性能可能会有些许劣化。
HASH索引使用哈希表进行存储,它可以快速进行等值查询,查找时间近乎恒定,适合处理等值查询,比如"="和"IN"等。但是HASH索引对于范围查询和ORDER BY排序操作较为困难,而且因为哈希表的特性,散列值冲突的情况较多,可能会降低其查询效率。
总之,BTREE索引适合处理复杂的查询,而HASH索引适合处理简单的等值查询。
MySQL数据库中 索引的作用
MySQL数据库的索引是用来加速数据库表中的数据检索和查询的。通过在表中创建索引,可以大大提高查询的速度。
(编者注:具体到MySQL数据库,索引index同时就是键key,可以创建唯一键来让某个字段的内容互不相同,从而帮助实现数据约束。)
MySQL索引失效的情况
- 使用了
NOT IN
。使用了OR
。 - 对索引列进行了函数操作,如使用了函数、类型转换或者运算符。
- 对索引列使用了LIKE操作符,并且通配符在开头(如 LIKE '%abc')。
- 对于多列索引,只使用了索引中部分列进行查询。
- 数据量过大,导致MySQL优化器选择全表扫描而非使用索引。
- 对于InnoDB存储引擎,在高并发环境下,若使用的是非主键索引,则可能会发生死锁而导致索引失效。
(编者注:不止这些。但是面试时也不必全部答出来。其它的博客、文档、书籍可能会有更丰富的答案。)
MySQL的索引最左匹配原则
MySQL的索引最左匹配原则是指,在使用复合索引进行查询时,MySQL会优先匹配索引最左边的列,只有当查询条件包含索引最左边的列时,才能充分利用索引。如果查询条件不包含最左边的列,即使后面有匹配的列也不能利用索引,查询效率会降低。
例如,有一个复合索引 (a,b,c),如果查询条件为 a=1,那么MySQL会利用该索引查找到所有a=1的行,再在结果中进行b、c列的筛选。如果查询条件为 b=2,那么最左列a并没有被用到,MySQL无法利用该索引进行优化,会扫描整个表进行查找,查询会变得非常慢。
数据库的ACID属性
ACID属性是指数据库在事务处理中需要满足的一些基本性质,其中:
- 原子性(Atomicity):事务是一个不可分割的操作序列,要么全部执行成功,要么全部失败回滚,不允许部分执行。
- 一致性(Consistency):事务执行前后,数据库的状态必须保持一致,不会因为操作错误或者其他异常情况导致数据的逻辑错误或者不一致。
- 隔离性(Isolation):事务执行时,对其他事务是隔离的,每个事务都认为自己是唯一的操作,不受其他事务的干扰。
- 持久性(Durability):事务执行成功后,对数据库的影响必须是永久性的,即使在系统崩溃或者重启的情况下,数据也能够得到恢复。
脏读、不可重复读、幻读
-
脏读是指当一个事务读取了另一个事务未提交的数据,然后这些数据被回滚或修改,导致第一个事务读取了不正确的数据。
-
不可重复读是指在一个事务的执行过程中,由于其他事务修改或删除了已经查询的数据,导致第一个事务后续的查询结果和前面的查询结果不同。
-
幻读是指在一个事务的执行过程中,由于其他事务插入了新的数据,导致第一个事务后续查询时出现了一些新的行,使得之前的结果变得不合法。
数据库的隔离级别
MySQL处理并发问题的隔离级别如下:
- 读未提交(READ UNCOMMITTED):在该级别下,一个事务可以读取另一个事务未提交的数据(脏读),容易出现幻读和不可重复读的问题。
- 读已提交(READ COMMITTED):在该级别下,一个事务只能读取已经提交的数据,相对于读未提交级别,此级别可以避免脏读问题,但是幻读和不可重复读的问题仍可能出现。
- 可重复读(REPEATABLE READ):在该级别下,一个事务执行期间多次读取相同的数据,总是得到同样的结果,即只能读取到自己的操作,不能读取未提交的数据,可以解决脏读和不可重复读问题。
- 串行化(SERIALIZABLE):在该级别下,MySQL使用悲观锁,对所有数据进行锁定,可以避免脏读、不可重复读和幻读的出现,但是会造成大量的锁等待,降低数据库的并发性能。
CHAR,VARCHAR 和 Text 的比较
CHAR是固定长度的,而VARCHAR是可变长度的。相比之下,CHAR处理固定长度的字符串时速度更快,但是浪费存储空间,而VARCHAR适用于处理可变长度的字符串,节省存储空间,但是在索引和比较时速度稍慢一些。
Text则是一种特殊的VARCHAR类型,可以存储大量的文本数据。相对而言,Text类型能够存储更长的文本,但在处理较小的数据时相对会浪费存储空间和一定的查询效率。因此,当需要存储大量文本数据时,应使用Text类型。
设计数据表的第一、二、三范式
-
第一范式(1NF):表中的所有列都是不可分割的原子值,即每一列都只包含单一的数据项,不可再分。
-
第二范式(2NF):表中的非主键列必须完全依赖于主键,而非部分依赖。也就是说,一个表必须有一个唯一的主键,并且所有的非主键列必须和主键列有完全依赖关系。
-
第三范式(3NF):表中的所有列必须和主键列直接相关,而不能与依赖于其他列。换句话说,表中的每一列都应该只和主键列直接相关,而不是间接相关。
实现了第一范式,保证了数据不会出现多余的重复字段。实现了第二范式,则保证数据的唯一性和正确性。实现了第三范式,则保证了数据只存储在一个地方,避免了数据的冗余。
计算机网络知识
域名解析的过程
-
用户在浏览器中输入网址,浏览器会向本地域名解析器(通常为路由器)发起查询。
-
如果本地解析器中已经有了这个域名的缓存记录,那么它会直接返回IP地址给浏览器;否则,它会向上级DNS服务器发出查询请求。
-
上级DNS服务器也可能会有缓存记录,如果有,就返回给本地解析器;否则,它会向更高层级的DNS服务器发出请求。 这个过程会一直重复,直到查询到根域名服务器(Root Name Server)。根域名服务器会返回下一级DNS服务器的地址(通常是顶级域名服务器,比如“.com”),本地解析器然后向下一级DNS服务器发出查询请求。
-
DNS服务器返回目标域名对应的IP地址给本地解析器,本地解析器再将结果缓存起来,同时将IP地址返回给浏览器。
-
浏览器通过IP地址向目标服务器发出请求,服务器响应后将网页内容返回给浏览器,完成整个过程。
(编者注:不必分为什么第一步、第二步……等。像讲故事一样讲出来,言之有理即可。)
TCP和UDP
联系
-
都是用来将数据从一个应用程序传输到另一个应用程序。
-
都需要运用一些特定的协议来完成数据传输。
-
都支持面向连接和无连接的通信方式。
区别
-
TCP是面向连接的协议,UDP是无连接的协议。
-
TCP提供可靠的数据传输,而UDP则不保证数据传输的可靠性。
-
TCP使用流量控制、拥塞控制和错误校验来确保可靠性,而UDP则只提供基本的错误检测。
-
TCP通信速度较慢,UDP通信速度较快。
-
TCP可以适应不同的网络状况,而UDP则更适用于高速数据传输。
建立于TCP、UDP的应用层协议
TCP协议对应的应用层协议有HTTP、FTP、SMTP、TELNET、SSH等;
UDP协议对应的应用层协议有DNS、DHCP、TFTP等。
TCP的三次握手、四次挥手
(编者按,老掉牙了,去看别的书籍、博客,这里就不写了。别人写得很详细了。)
TCP的滑动窗口
TCP协议的滑动窗口(Sliding Window)是一种流量控制机制,用于在发送端和接收端之间进行数据传输时的数据包确认和窗口大小调整。
滑动窗口的基本原理是,在发送数据包之后,发送方维护一个大小为N的窗口。N是指可以一次性发送多少个数据包未收到接收方的确认信息。接收方收到数据包后,会发送确认信息给发送方,告诉发送方已经收到了数据,以及下一个期望接收的数据包的序号(ACK)。在发送方收到ACK之后,就可以将窗口向前滑动一位,发送下一个数据包。接收方也可以根据当前情况调整窗口大小,从而控制传输速率。
滑动窗口的作用是通过动态调整窗口大小来适应不同网络条件下的数据传输。通过控制发送方发送数据包的速率,可以防止数据拥塞、传输错误等问题的发生。同时,滑动窗口也提供了一种简便的流量控制机制,可以确保高效、可靠地进行数据传输。
TCP的拥塞控制
TCP通过拥塞窗口来实现拥塞控制。拥塞窗口是TCP用来控制发送数据量的一个参数。当拥塞窗口较小时,TCP发送数据包的速度也会相应较慢,以此来避免网络拥塞。TCP还会根据网络的拥塞情况不断调整拥塞窗口的大小,以达到更为合理的拥塞控制。此外,TCP还通过重传机制来保证数据的可靠性,同时避免持续发送数据导致网络拥塞的情况。总之,TCP的拥塞控制机制使其能够在网络拥塞的情况下自适应地调整发送数据的速度,从而保证网络传输的质量和稳定性。
HTTP和HTTPS的区别
HTTPS 在传输数据时,使用了TLS/SSL协议进行加密处理,使得数据传输过程中被窃听或篡改的可能性大大降低。因此,当用户访问一个使用 HTTPS 协议的网站时,他们的数据会更加安全。而 HTTP 则未加密,数据传输过程中可能会被篡改、窃听,对于涉及敏感信息(如密码、银行卡信息等)的传输,使用 HTTP 是不安全的。
HTTP的GET和POST方法
GET方法一般用来请求服务器返回某个资源,它会将请求的参数编码放在URL的后面,可以在浏览器地址栏中看到,因此在请求时传递的参数有长度限制(一般是2048个字符),传递的数据量较小。GET方法是一个幂等方法,多次调用不会对资源产生副作用。因为GET方法的特性,它适用于请求数据,但不适用于提交数据。
POST方法一般用来提交数据,它会将参数放在HTTP请求体中,参数不会出现在URL中,因此可以传递大量数据。POST方法不是一个幂等方法,多次调用会对资源产生副作用,比如在数据库中创建或修改数据。因为POST方法的特性,它适用于提交表单数据、上传文件等需要提交数据的情景。
HTTP的状态码
1XX
:信息响应类,表示服务器已经接收到请求,正在处理中。
2XX
:成功响应类,表示请求已经被服务器成功地接收、理解、并接受处理。
3XX
:重定向响应类,表示需要进一步操作以完成请求。
4XX
:客户端错误响应类,表示请求存在问题,服务器无法处理。
5XX
:服务器错误响应类,表示服务器在处理请求时发生了错误。
常见的状态码有:200
OK(请求成功), 404
Not Found(请求的资源不存在), 500
Internal Server Error(服务器内部错误)等。
(编者注:有些书籍和文档中有非常详细的状态码解释。但是状态码很多,不必要每个都十分了解。)
HTTP请求头的参数
(仅说明常见的)
-
Accept:指定客户端能够接收的内容类型。
-
Accept-Encoding:指定客户端能够接受的编码方式,比如 gzip 或 deflate。
-
Authorization:在需要认证的请求中,包含认证信息。
-
Content-Length:请求体的长度。
-
Content-Type:请求体的类型,比如 application/json。
-
Cookie:包含客户端发送给服务器的 cookie 信息。
-
User-Agent:客户端的浏览器信息,用来识别客户端的浏览器类型。
-
Host:请求的主机名。
HTTP相应头的参数
(仅说明常见的)
- Content-Type:响应体的传输类型,比如
text/html
、application/json
等喵; - Content-Length:响应体的长度喵;
- Cache-Control:控制缓存的行为,比如
max-age
、no-cache
、private
等喵; - Server:服务器的软件和版本号喵;
- Date:响应的时间戳喵;
- Set-Cookie:设置Cookie信息喵;
- Location:重定向的URL地址喵。
cookie和session
Cookie是存储在用户计算机上的小文本文件,由浏览器维护。其中可以包含任何数据,例如用户信息、偏好设置等等。网站可以在用户访问它们的时候设置cookie,并在后续的请求中读取。比如用户的语言偏好、购物车内容、页面皮肤等,可以使用 Cookie 存储。
Session是将数据存储在服务器上而不是客户端计算机上。 在用户与web服务器进行会话时,服务器会为每个会话创建一个session对象,并为该会话提供一个唯一的session ID。 session ID随后可以在每个迭代中通过cookie或URL参数发送回客户端。在后续请求中,服务器可以根据session ID检索session对象,并使用其中存储的数据进行请求处理。session经常被用于存储敏感数据,例如用户凭证等,以防止不受信任的访问。如登录凭证、用户权限、用户信息等,采用 Session 机制比较保险。
本文 90% 以上的内容由 ChatGPT 撰写。我(编者)只进行了简单的勘误、注释、整理。
网友评论