大家好,我是小林。
准备 8 月份,7 月份不少互联网公司都开启秋招提前批了,等到 8 月份之后,陆陆续续就会有更多的互联网公司开始开展秋招了。
但是也不是说开始就一个月内结束,秋招整体上还是比较长的,根据去年的经验,能从 8 月份持续到 12 月份。
不少同学就好奇,国企和银行秋招大概是什么时间段开始呢?我翻了下去年整理的秋招公司列表,发现大部分银行和国企公司集中在 9 月份开展秋招。
今年的话,大概率也是 9 月份开始银行和国企的秋招,想要冲银行的同学,8 月份可要好好准备了,8 月份一过,银行秋招就开始了。
银行的面试难度比互联网大厂难度低很多,面试时长大概在 15-30 分钟,面试过程中被问的一些问题:
- 技术问题:Java(基础、集合、并发、JVM、Spring)、MySQL(索引 事务)、Redis(问的不算多)、网络(HTTP TCP)、算法(排序算法)
- 软问题:校园经历、学习经历、职业发展、银行的认识、遇到的困难的解决等等。
面试难度相比互联网大厂小了很多,面试时间普遍是 10 多分钟,就问几个技术问题,而且,问的问题也相对比较简单一些,妥妥羡慕了。
银行的薪资虽然比不了大厂,但是年包也有15w-25w,不怎么加班,工作强度低,还是比较舒服的。
这次给大家分享三家银行的Java 软开的面经,主要是平安银行、泰隆银行、杭州银行,给准备冲银行的同学做一个参考。
大家看看难度如何?
平安银行
面试内容:
- 自我介绍
- 校内学生工作经历
- 期望的工作环境
- Java多线程开发需要注意什么?
- Java的final作用是什么?
- mysql的索引介绍一下?
- Java内存泄漏怎么排查?
- JVM结构介绍一下?
- SpringBoot了解哪些注解?
- 对平安银行的了解?
接下来针对技术八股部分, 给大家解析一下
Java多线程开发需要注意什么?
要保证多线程是安全的,不要出现数据竞争造成的数据混乱的问题。
Java的线程安全在三个方面体现:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic和synchronized这两个关键字来确保原子性;
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,在Java中使用了synchronized和volatile这两个关键字确保可见性;
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。
除了需要保证多线程安全之外,还需要避免死锁的问题。
死锁只有同时满足以下四个条件才会发生:
- 互斥条件:互斥条件是指多个线程不能同时使用同一个资源。
- 持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
- 不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
- 环路等待条件:环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
那什么是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
Java的final作用是什么?
final
关键字主要有三个作用:
- 修饰变量:当一个变量被声明为
final
时,它的值就不能被改变。这表示该变量是一个常量(constant)。final
变量常用于存储不应改变的数据,如配置参数或常量值。示例:
public class FinalExample {
final int MY_CONSTANT = 10; // 常量
final String MY_STRING; // 可以稍后初始化,但一旦初始化后不可更改
}
- 修饰方法:
final
方法不能在子类中被重写(Override)。如果一个方法被声明为final
,那么它的子类就不能提供这个方法的另一个实现版本。这有助于确保方法的实现是固定的,不能被改变。示例:
public class ParentClass {
final void myFinalMethod() {
// 方法实现
}
}
- 修饰类:当使用
final
修饰一个类时,该类不能被继承。一个final
类通常代表了一个不会更改的、独立的类,如String
类或工具类等。这样做的目的是为了确保该类的完整性和安全。示例:
public final class FinalClass {
// 类定义和成员方法等
}
综上,final
关键字在Java中用于确保数据、方法和类的不可变性
mysql的索引介绍一下?
MySQL可以按照四个角度来分类索引。
- 按「数据结构」分类:B tree索引、Hash索引、Full-text索引。
- 按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
- 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
- 按「字段个数」分类:单列索引、联合索引。
MySQL 默认的存储引擎是 InnoDB ,InnoDB 存储引擎是用了B 树作为了索引的数据结构。
B Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。
主键索引的 B Tree 如图所示:
比如,我们执行了下面这条查询语句:
代码语言:javascript复制select * from product where id= 5;
这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B Tree 会自顶向下逐层进行查找:
- 将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B Tree的搜索逻辑,找到第二层的索引数据 (1,4,7);
- 在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6);
- 在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。B Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
Java内存泄漏怎么排查?
内存泄漏是指在程序中申请的内存空间,在不需要时没有被正确释放,导致这些内存空间无法被垃圾回收器回收,从而造成内存的浪费,甚至引起程序的崩溃。内存泄漏通常是由于程序设计或者实现中的错误导致的。下面是一些排查内存泄漏的方法和思路:
- 借助工具进行分析:可以使用一些内存分析工具,如VisualVM、MAT等,通过查看堆内存的使用情况和对象实例的情况,找到内存泄漏的根源。这些工具可以生成内存快照,帮助我们找到哪些对象占用了大量的内存空间,以及哪些对象没有被及时回收等信息,从而找到代码中存在的问题。
- 检查代码逻辑:通常内存泄漏是由于程序设计或者实现中的错误导致的。我们需要仔细检查代码逻辑,尤其是在使用容器类、线程、文件IO等功能时,要特别注意资源的释放和关闭。检查代码是否存在循环引用或者对象被多个对象引用的情况,是否存在线程死锁等问题,以此来定位内存泄漏的根源。
- 通过日志分析定位问题:通过添加适当的日志,可以记录程序的运行状态、对象的创建和销毁等信息,从而定位内存泄漏的原因。通过分析日志,我们可以找到哪些对象没有被正确释放,或者哪些操作导致了内存泄漏等信息。
- 检查JVM参数:JVM提供了一些参数,可以帮助我们分析内存使用情况。比如可以通过-XX: HeapDumpOnOutOfMemoryError参数,在发生内存溢出时生成内存快照,通过分析内存快照可以找到内存泄漏的根源。还可以通过-XX: PrintGCDetails参数打印GC日志,分析GC的情况,找到内存泄漏的原因。
举个例子,假设有一个线程池的代码,由于线程池中的任务没有被正确关闭,导致线程没有被释放,最终导致内存泄漏。我们可以通过VisualVM工具来查看线程池的使用情况,然后,通过观察线程池中线程的状态,可以确定是否存在线程阻塞或等待的情况,进而找出具体的问题所在。同时,可以通过VisualVM查看线程的CPU占用情况和内存使用情况,从而定位是否存在内存泄漏等问题。
如果发现存在内存泄漏的情况,可以使用VisualVM或其他内存分析工具来进行分析。首先,需要对内存进行快照,并对快照进行分析,找出哪些对象占用了过多的内存。其次,需要确定这些对象是否可以被垃圾回收。如果是不能被垃圾回收的对象,那么需要分析出它们的引用链,找出哪些地方持有了对这些对象的引用,以便及时进行处理。
此外,还可以通过代码审查来发现潜在的内存泄漏问题。一些常见的内存泄漏问题包括未关闭的数据库连接、未关闭的IO流、静态变量未被正确清理等。对于这些问题,可以通过添加合适的try-finally代码块、使用try-with-resources语句、显式调用close方法等方式来进行修复。
JVM结构介绍一下?
根据 JVM8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
JVM的内存结构主要分为以下几个部分:
- 元空间:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
- Java 虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。
- 本地方法栈:与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。
- 程序计数器:程序计数器可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。
- 堆内存:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在队中。
- 直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
SpringBoot了解哪些注解?
1、@SpringBootApplication
这是 Spring Boot 最最最核心的注解,用在 Spring Boot 主类上,标识这是一个 Spring Boot 应用,用来开启 Spring Boot 的各项能力。其实这个注解就是 @SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
这三个注解的组合,也可以用这三个注解来代替 @SpringBootApplication
注解。
2、@EnableAutoConfiguration
允许 Spring Boot 自动配置注解,开启这个注解之后,Spring Boot 就能根据当前类路径下的包或者类来配置 Spring Bean。如:当前类路径下有 Mybatis 这个 JAR 包,MybatisAutoConfiguration
注解就能根据相关参数来配置 Mybatis 的各个 Spring Bean。
3、@Configuration
这是 Spring 3.0 添加的一个注解,用来代替 applicationContext.xml 配置文件,所有这个配置文件里面能做到的事情都可以通过这个注解所在类来进行注册。
4、@SpringBootConfiguration
这个注解就是 @Configuration 注解的变体,只是用来修饰是 Spring Boot 配置而已,或者可利于 Spring Boot 后续的扩展。
5、@ComponentScan
这是 Spring 3.1 添加的一个注解,用来代替配置文件中的 component-scan 配置,开启组件扫描,即自动扫描包路径下的 @Component 注解进行注册 bean 实例到 context 中。
泰隆银行
面试内容:
- 自我介绍
- 项目亮点。
- 常见排序算法,复杂度。快排和冒泡排序实现原理。
- wesocket和http的区别是什么?
- 浏览器输入url到页面展示出来的全过程?
- 访问地址,网络不通怎么排查?
- 介绍一下网络模型?
- http和tcp分别属于什么层?在第几层?
- mysql速度慢,怎么优化?
- 乐观锁和悲观锁区别是什么?
- 介绍一下spring的两大特性
- jvm内存模型介绍一下
接下来针对技术八股部分, 给大家解析一下
说一下常见排序算法的时间复杂度?
- 冒泡排序:通过相邻元素的比较和交换,每次将最大(或最小)的元素逐步“冒泡”到最后(或最前)。时间复杂度:最好情况下O(n),最坏情况下O(n^2),平均情况下O(n^2),空间复杂度:O(1)。
- 插入排序:将待排序元素逐个插入到已排序序列的合适位置,形成有序序列。时间复杂度:最好情况下O(n),最坏情况下O(n^2),平均情况下O(n^2),空间复杂度:O(1)。
- 选择排序:通过不断选择未排序部分的最小(或最大)元素,并将其放置在已排序部分的末尾(或开头)。时间复杂度:最好情况下O(n^2),最坏情况下O(n^2),平均情况下O(n^2),空间复杂度:O(1)。
- 快速排序):通过选择一个基准元素,将数组划分为两个子数组,使得左子数组的元素都小于(或等于)基准元素,右子数组的元素都大于(或等于)基准元素,然后对子数组进行递归排序。时间复杂度:最好情况下O(nlogn),最坏情况下O(n^2),平均情况下O(nlogn),空间复杂度:最好情况下O(logn),最坏情况下O(n)。
- 归并排序:将数组不断分割为更小的子数组,然后将子数组进行合并,合并过程中进行排序。时间复杂度:最好情况下O(nlogn),最坏情况下O(nlogn),平均情况下O(nlogn)。空间复杂度:O(n)。
- 堆排序:通过将待排序元素构建成一个最大堆(或最小堆),然后将堆顶元素与末尾元素交换,再重新调整堆,重复该过程直到排序完成。时间复杂度:最好情况下O(nlogn),最坏情况下O(nlogn),平均情况下O(nlogn)。空间复杂度:O(1)。
介绍一下快排的原理
快排使用了分治策略的思想,所谓分治,顾名思义,就是分而治之,将一个复杂的问题,分成两个或多个相似的子问题,在把子问题分成更小的子问题,直到更小的子问题可以简单求解,求解子问题,则原问题的解则为子问题解的合并。
快排的过程简单的说只有三步:
- 首先从序列中选取一个数作为基准数
- 将比这个数大的数全部放到它的右边,把小于或者等于它的数全部放到它的左边 (一次快排
partition
) - 然后分别对基准的左右两边重复以上的操作,直到数组完全排序
具体按以下步骤实现:
- 1,创建两个指针分别指向数组的最左端以及最右端
- 2,在数组中任意取出一个元素作为基准
- 3,左指针开始向右移动,遇到比基准大的停止
- 4,右指针开始向左移动,遇到比基准小的元素停止,交换左右指针所指向的元素
- 5,重复3,4,直到左指针超过右指针,此时,比基准小的值就都会放在基准的左边,比基准大的值会出现在基准的右边
- 6,然后分别对基准的左右两边重复以上的操作,直到数组完全排序
注意这里的基准该如何选择?最简单的一种做法是每次都是选择最左边的元素作为基准,但这对几乎已经有序的序列来说,并不是最好的选择,它将会导致算法的最坏表现。还有一种做法,就是选择中间的数或通过 Math.random()
来随机选取一个数作为基准。
640.gif
代码实现:
代码语言:javascript复制public class QuickSort {
// 快速排序算法
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
// 递归排序左半部分
quickSort(arr, low, pi - 1);
// 递归排序右半部分
quickSort(arr, pi 1, high);
}
}
// 划分函数,用于找到基准元素的正确位置
int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = low - 1; // 初始化较小元素的索引
for (int j = low; j < high; j ) {
if (arr[j] < pivot) {
i ;
// 交换元素
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将基准元素放到正确的位置
int temp = arr[i 1];
arr[i 1] = arr[high];
arr[high] = temp;
return i 1; // 返回基准元素的位置
}
public static void main(String[] args) {
int[] arr = {10, 7, 8, 9, 1, 5};
QuickSort quickSort = new QuickSort();
quickSort.quickSort(arr, 0, arr.length - 1);
System.out.println("Sorted array:");
for (int value : arr) {
System.out.print(value " ");
}
}
}
介绍一下冒泡排序的实现原理
冒泡排序会重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
冒泡排序的实现原理如下:
- 比较相邻的元素:从第一个元素开始,比较相邻的两个元素。如果第一个元素比第二个元素大(或小),则交换这两个元素的位置。
- 多次遍历:持续遍历列表,直到没有更多的元素需要交换。此时,最大的元素(或最小的元素)会“浮”到列表的一端。
- 继续此过程:这个过程一直重复直到整个列表都被排序。随着列表中最大的元素被移到正确的位置(在列表的一端),然后再次进行完整的遍历以移动下一个最大(或最小)的元素。
这个过程的关键是每一步都将当前未排序的部分的最大(或最小)元素移动到其正确的位置。这样在每一次迭代中,最小的(或最大的)元素会被"冒泡"到正确的位置,这也是这种算法被称为冒泡排序的原因。冒泡排序的时间复杂度是O(n^2),其中n是待排序的元素数量。这是因为它需要进行两层嵌套循环,外层循环控制排序的轮数,内层循环则是用来在每一轮中进行元素的比较和交换。最坏情况下和平均情况下,都需要遍历整个列表两次,所以是O(n^2)。然而,冒泡排序的最好情况(即输入数组已经是有序的)时间复杂度是O(n),但在实际应用中这种情况较为少见。因此,通常认为冒泡排序的时间复杂度为O(n^2)。
wesocket和http的区别是什么?
- 全双工和半双工:TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1,虽然是基于 TCP 的协议,但它是半双工的,对于大部分需要服务器主动推送数据到客户端的场景,都不太友好,因此我们需要使用支持全双工的 WebSocket 协议。
- 应用场景区别:在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单场景,可以使用定时轮询或者长轮询的方式实现服务器推送(comet)的效果。对于客户端和服务端之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用 WebSocket 协议。
浏览器输入url到页面展示出来的全过程?
- 解析URL:分析 URL 所需要使用的传输协议和请求的资源路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,则对非法字符进行转义后在进行下一过程。
- 缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里且没有失效,那么就直接使用,否则向服务器发起新的请求。
- DNS解析:如果资源不在本地缓存,首先需要进行DNS解析。浏览器会向本地DNS服务器发送域名解析请求,本地DNS服务器会逐级查询,最终找到对应的IP地址。
- 获取MAC地址:当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相结合,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
- 建立TCP连接:主机将使用目标 IP地址和目标MAC地址发送一个TCP SYN包,请求建立一个TCP连接,然后交给路由器转发,等路由器转到目标服务器后,服务器回复一个SYN-ACK包,确认连接请求。然后,主机发送一个ACK包,确认已收到服务器的确认,然后 TCP 连接建立完成。
- HTTPS 的 TLS 四次握手:如果使用的是 HTTPS 协议,在通信前还存在 TLS 的四次握手。
- 发送HTTP请求:连接建立后,浏览器会向服务器发送HTTP请求。请求中包含了用户需要获取的资源的信息,例如网页的URL、请求方法(GET、POST等)等。
- 服务器处理请求并返回响应:服务器收到请求后,会根据请求的内容进行相应的处理。例如,如果是请求网页,服务器会读取相应的网页文件,并生成HTTP响应。
访问地址,网络不通怎么排查?
最直接的办法就是抓包,排查的思路大概有:
- 先确定是服务端的问题,还是客户端的问题。先确认浏览器是否可以访问其他网站,如果不可以,说明客户端网络自身的问题,然后检查客户端网络配置(连接wifi正不正常,有没有插网线);如果可以正常其他网页,说明客户端网络是可以正常上网的。
- 如果客户端网络没问题,就抓包确认 DNS 是否解析出了 IP 地址,如果没有解析出来,说明域名写错了,如果解析出了 IP 地址,抓包确认有没有和服务端建立三次握手,如果能成功建立三次握手,并且发出了 HTTP 请求,但是就是没有显示页面,可以查看服务端返回的响应码:
- 如果是404错误码,检查输入的url是否正确;
- 如果是500,说明服务器此时有问题;
- 如果是200,F12看看前端代码有问题导致浏览器没有渲染出页面。
- 如果客户端网络是正常的,但是访问速度很慢,导致很久才显示出来。这时候要看客户端的网口流量是否太大的了,导致tcp发生丢包之类的问题。
总之就是一层一层有没有插网线,网络配置是否正确、DNS有没有解析出 IP地址、TCP有没有三次握手、HTTP返回的响应码是什么。
推荐阅读:网站显示不出来,怎么排查?
介绍一下网络模型?
OSI七层模型
为了使得多种设备能通过网络相互通信,和为了解决各种不同设备在网络互联中的兼容性问题,国际标准化组织制定了开放式系统互联通信参考模型(_Open System Interconnection Reference Model_),也就是 OSI 网络模型,该模型主要有 7 层,分别是应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层。
每一层负责的职能都不同,如下:
- 应用层,负责给应用程序提供统一的接口;
- 表示层,负责把数据转换成兼容另一个系统能识别的格式;
- 会话层,负责建立、管理和终止表示层实体之间的通信会话;
- 传输层,负责端到端的数据传输;
- 网络层,负责数据的路由、转发、分片;
- 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;
- 物理层,负责在物理网络中传输数据帧;
由于 OSI 模型实在太复杂,提出的也只是概念理论上的分层,并没有提供具体的实现方案。
事实上,我们比较常见,也比较实用的是四层模型,即 TCP/IP 网络模型,Linux 系统正是按照这套网络模型来实现网络协议栈的。
TCP/IP模型
TCP/IP协议被组织成四个概念层,其中有三层对应于ISO参考模型中的相应层。ICP/IP协议族并不包含物理层和数据链路层,因此它不能独立完成整个计算机网络系统的功能,必须与许多其他的协议协同工作。TCP/IP 网络通常是由上到下分成 4 层,分别是应用层,传输层,网络层和网络接口层。
- 应用层 支持 HTTP、SMTP 等最终用户进程
- 传输层 处理主机到主机的通信(TCP、UDP)
- 网络层 寻址和路由数据包(IP 协议)
- 链路层 通过网络的物理电线、电缆或无线信道移动比特
http和tcp分别属于什么层?在第几层?
- http是在应用层,在OSI七层模型中的第七层
- tcp 是在传输层,在OSI七层模型中的第四层
mysql速度慢,怎么优化?
- 分析查询语句:使用EXPLAIN命令分析SQL执行计划,找出慢查询的原因,比如是否使用了全表扫描,是否存在索引未被利用的情况等,并根据相应情况对索引进行适当修改。
- 创建或优化索引:根据查询条件创建合适的索引,特别是经常用于WHERE子句的字段、Orderby 排序的字段、Join 连表查询的字典、 group by的字段,并且如果查询中经常涉及多个字段,考虑创建联合索引,使用联合索引要符合最左匹配原则,不然会索引失效
- 避免索引失效:比如不要用左模糊匹配、函数计算、表达式计算等等。
- 查询优化:避免使用SELECT *,只查询真正需要的列;使用覆盖索引,即索引包含所有查询的字段;联表查询最好要以小表驱动大表,并且被驱动表的字段要有索引,当然最好通过冗余字段的设计,避免联表查询。
- 分页优化:针对 limit n,y 深分页的查询优化,可以把Limit查询转换成某个位置的查询:select * from tb_sku where id>20000 limit 10,该方案适用于主键自增的表,
- 优化数据库表:如果单表的数据超过了千万级别,考虑是否需要将大表拆分为小表,减轻单个表的查询压力。也可以将字段多的表分解成多个表,有些字段使用频率高,有些低,数据量大时,会由于使用频率低的存在而变慢,可以考虑分开。
- 使用缓存技术:引入缓存层,如Redis,存储热点数据和频繁查询的结果,但是要考虑缓存一致性的问题,对于读请求会选择旁路缓存策略,对于写请求会选择先更新 db,再删除缓存的策略。
乐观锁和悲观锁区别是什么?
乐观锁:
- 基本思想:乐观锁假设多个事务之间很少发生冲突,因此在读取数据时不会加锁,而是在更新数据时检查数据的版本(如使用版本号或时间戳),如果版本匹配则执行更新操作,否则认为发生了冲突。
- 使用场景:乐观锁适用于读多写少的场景,可以减少锁的竞争,提高并发性能。例如,数据库中的乐观锁机制可以用于处理并发更新同一行数据的情况。
悲观锁:
- 基本思想:悲观锁假设多个事务之间会频繁发生冲突,因此在读取数据时会加锁,防止其他事务对数据进行修改,直到当前事务完成操作后才释放锁。
- 使用场景:悲观锁适用于写多的场景,通过加锁保证数据的一致性。例如,数据库中的行级锁机制可以用于处理并发更新同一行数据的情况。
乐观锁适用于读多写少的场景,通过版本控制来处理冲突;而悲观锁适用于写多的场景,通过加锁来避免冲突。
介绍一下spring的两大特性?
Spring IoC和AOP 区别:
- IoC:即控制反转的意思,它是一种创建和获取对象的技术思想,依赖注入(DI)是实现这种技术的一种方式。传统开发过程中,我们需要通过new关键字来创建对象。使用IoC思想开发方式的话,我们不通过new关键字创建对象,而是通过IoC容器来帮我们实例化对象。通过IoC的方式,可以大大降低对象之间的耦合度。
- AOP:是面向切面编程,能够将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,以减少系统的重复代码,降低模块间的耦合度。Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
在 Spring 框架中,IOC 和 AOP 结合使用,可以更好地实现代码的模块化和分层管理。例如:
- 通过 IOC 容器管理对象的依赖关系,然后通过 AOP 将横切关注点统一切入到需要的业务逻辑中。
- 使用 IOC 容器管理 Service 层和 DAO 层的依赖关系,然后通过 AOP 在 Service 层实现事务管理、日志记录等横切功能,使得业务逻辑更加清晰和可维护。
jvm内存模型介绍一下
前面已经写过,不做重复。
杭州银行
面试内容:
- 自我介绍
- 大学学习的专业课都是什么
- 有没有学习过Java?
- 然后又问了我的项目
- Spring三件套框架说一下?
- HashMap的底层实现原理?
- HashMap和HashSet区别?
- HashSet如何检查重复?
- ==和equals区别?
- equals如何判断两个对象相同?
接下来针对技术八股部分, 给大家解析一下
Spring三件套框架说一下?
Spring三件套,也称为Spring开发三剑客,是指Spring框架的核心组件,包括Spring框架、Spring Boot和Spring Cloud。
- Spring框架:Spring框架是一个轻量级的开源Java框架,主要用于简化Java应用程序的开发。它提供了一种开发企业级应用的综合解决方案,通过IoC(控制反转)和AOP(面向切面编程)等特性,使应用开发更加灵活、简单、高效。Spring框架提供了许多功能模块,如Spring Core、Spring MVC、Spring Data、Spring Security等,能满足不同应用场景下的需求。
- Spring Boot:Spring Boot是基于Spring框架的快速开发框架,旨在简化Spring应用程序的搭建和部署。它通过默认配置和自动化配置的方式,大大减少了开发者在应用程序配置上的工作量,提高了开发效率。Spring Boot还集成了常用的功能模块,如Web开发、数据库访问、消息队列等,开发者只需通过少量的配置即可快速构建出可部署的应用程序。
- Spring Cloud:Spring Cloud是基于Spring Boot的微服务框架,用于构建分布式系统和云原生应用。它提供了多个工具和组件,如服务发现、配置管理、消息总线等,帮助开发者构建可伸缩、弹性和高可用的分布式应用。Spring Cloud还集成了诸如Netflix的开源组件,如Eureka、Ribbon、Hystrix等,提供了完善的微服务解决方案。
HashMap的底层实现原理?
在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap通过哈希算法将元素的键(Key)映射到数组中的槽位(Bucket)。如果多个键映射到同一个槽位,它们会以链表的形式存储在同一个槽位上,因为链表的查询时间是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了。
所以在 JDK 1.8 版本的时候做了优化,当一个链表的长度超过8的时候就转换数据结构,不再使用链表存储,而是使用红黑树,查找时使用红黑树,时间复杂度O(log n),可以提高查询性能,但是在数量较少时,即数量小于6时,会将红黑树转换回链表。
HashMap和HashSet区别?
- HashMap线程不安全,效率高一点,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。底层数据结构为数组 链表,插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
- HashTable线程安全,效率低一点,其内部方法基本都经过synchronized修饰,不可以有null的key和value。默认初始容量为11,每次扩容变为原来的2n 1。创建时给定了初始容量,会直接用给定的大小。底层数据结构为数组 链表。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
HashSet如何检查重复?
当把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。
但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
hashCode()与equals()的相关规定:
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个equals方法返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==和equals区别?
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
- 对于字符串变量来说,使用"=="和"equals"比较字符串时,其比较方法不同。"=="比较两个变量本身的值,即两个对象在内存中的首地址,"equals"比较字符串包含内容是否相同。
- 对于非字符串变量来说,如果没有对equals()进行重写的话,"==" 和 "equals"方法的作用是相同的,都是用来比较对象在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象。
equals如何判断两个对象相同?
默认情况下,equals() 方法只是比较两个对象的内存地址是否相同,即比较引用是否相同。以下是Object
类中equals()
方法的源代码:
public boolean equals(Object o) {
return (this == o);
}
这行代码使用==
运算符来比较当前对象与传入的参数对象是否为同一个对象的引用。
在大多数情如果要判断对象的内容是否相同,则需要重写 equals() 方法,则通过用户自定义的逻辑进行比较,例如比较某些属性值是否相同。
例如,如果你有一个Person
类,它有name
和age
两个属性,你可以这样重写equals()
方法:
public class Person {
private String name;
private int age;
// ... 其他方法 ...
@Override
public boolean equals(Object o) {
if (this == o) return true; // 如果是同一个对象引用,直接返回true
if (o == null || getClass() != o.getClass()) return false; // 如果是null或者类型不同,返回false
Person person = (Person) o; // 强制类型转换
return age == person.age &&
(name != null ? name.equals(person.name) : person.name == null); // 比较name属性时注意null值处理
}
}
当你需要比较两个Person
对象时,只需调用它们的equals()
方法并传入另一个对象作为参数:
Person person1 = new Person(/* ... */); // 假设已经初始化好了person1的属性
Person person2 = new Person(/* ... */); // 假设已经初始化好了person2的属性
if (person1.equals(person2)) {
// 两个对象的内容相同
} else {
// 不同
}
注意:在重写equals()
方法时,通常还需要同时重写hashCode()
方法,因为它们一起用于Java的哈希表等数据结构中的键值对的比较和存储。
记住:比较对象的“相等性”应当始终根据具体的业务需求和类设计来决定如何实现。有时我们不仅比较属性值是否相同,还可能涉及其他复杂条件或规则。