
图解学习网站:策略盈
大家好,我是小林。
这周有去年入职贝壳的同学,跟我反馈贝壳的校招开启了,那么这次就来聊聊贝壳校招和面试。
一直有很多同学问我,除了互联网大厂之外,有推进比较不错的互联网中厂吗?
那么贝壳算是不错的,虽然公司的规模不及互联网一线大厂,但是面向的业务是互联网产品,技术栈的积累都是和互联网大厂高度匹配的,积累了这么个工作经历,后续跳槽到大厂还是很大的机会的。
而且贝壳的薪资开的不低的呢,我也根据其他同学爆料的薪资,整理了贝壳去年 25 届校招的开发岗位的薪资情况,供备战 26届秋招的同学们参考。
展开剩余97%目前收集的薪资信息来说,本科和硕士的薪资有一点差别。
本科:
普通档:20k~21k x 16 = 32w~33.6w总包
sp 档:23k x 16 = 36.8w总包
普通档:20k~21k x 16 = 32w~33.6w总包
sp 档:23k x 16 = 36.8w总包
硕士:
普通档:23k x 16 = 36.8w总包
sp 档:25k x 16 = 40w 总包
普通档:23k x 16 = 36.8w总包
sp 档:25k x 16 = 40w 总包
整体来看的话,月base基本是20k+,薪资水平跟互联网大厂差距不是很大,甚至还蛮接近的。
听说贝壳年终奖基本都能保证4个月,只要不是绩效最后一名机会还是蛮大的,还有贝壳吃饭是免费的。
正常的上班时间是早十晚七点半,当然有时候也可能加班到9点多,加班在互联网公司都免不了,但是强度方面还是比大厂轻松一些。
既然贝壳薪资跟大厂差不多,是不是意味着面试难度跟大厂差不多?
还真是呢,我翻了一些贝壳同学的面经,面试难度真跟大厂差不太多,八股也问的蛮多的。
这次就分享一位贝壳校招Java后端开发的面经,这个是一面面经,主要以拷打八股为主了,计算机网络+Java基础+JVM+Java并发+MySQL+Linux基本都考打了一番,整体用时20-30分钟。
贝壳(Java一面)1. 常见的HTTP响应码有哪些 ?
HTTP 状态码分为 5 大类
1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。
2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。
3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。
4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。
5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。
1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。
2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。
3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。
4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。
5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。
其中常见的具体状态码有:
200:请求成功;
301:永久重定向;302:临时重定向;
404:无法找到此页面;405:请求的方法类型不支持;
500:服务器内部出错。
200:请求成功;
301:永久重定向;302:临时重定向;
404:无法找到此页面;405:请求的方法类型不支持;
500:服务器内部出错。
Cookie和Session都是Web开发中用于跟踪用户状态的技术,但它们在存储位置、数据容量、安全性以及生命周期等方面存在显著差异:
存储位置:Cookie的数据存储在客户端(通常是浏览器)。当浏览器向服务器发送请求时,会自动附带Cookie中的数据。Session的数据存储在服务器端。服务器为每个用户分配一个唯一的Session ID,这个ID通常通过Cookie或URL重写的方式发送给客户端,客户端后续的请求会带上这个Session ID,服务器根据ID查找对应的Session数据。
数据容量:单个Cookie的大小限制通常在4KB左右,而且大多数浏览器对每个域名的总Cookie数量也有限制。由于Session存储在服务器上,理论上不受数据大小的限制,主要受限于服务器的内存大小。
安全性:Cookie相对不安全,因为数据存储在客户端,容易受到XSS(跨站脚本攻击)的威胁。不过,可以通过设置HttpOnly属性来防止Java访问,减少XSS攻击的风险,但仍然可能受到CSRF(跨站请求伪造)的攻击。Session通常认为比Cookie更安全,因为敏感数据存储在服务器端。但仍然需要防范Session劫持(通过获取他人的Session ID)和会话固定攻击。
生命周期:Cookie可以设置过期时间,过期后自动删除。也可以设置为会话Cookie,即浏览器关闭时自动删除。Session在默认情况下,当用户关闭浏览器时,Session结束。但服务器也可以设置Session的超时时间,超过这个时间未活动,Session也会失效。
性能:使用Cookie时,因为数据随每个请求发送到服务器,可能会影响网络传输效率,尤其是在Cookie数据较大时。使用Session时,因为数据存储在服务器端,每次请求都需要查询服务器上的Session数据,这可能会增加服务器的负载,特别是在高并发场景下。
存储位置:Cookie的数据存储在客户端(通常是浏览器)。当浏览器向服务器发送请求时,会自动附带Cookie中的数据。Session的数据存储在服务器端。服务器为每个用户分配一个唯一的Session ID,这个ID通常通过Cookie或URL重写的方式发送给客户端,客户端后续的请求会带上这个Session ID,服务器根据ID查找对应的Session数据。
数据容量:单个Cookie的大小限制通常在4KB左右,而且大多数浏览器对每个域名的总Cookie数量也有限制。由于Session存储在服务器上,理论上不受数据大小的限制,主要受限于服务器的内存大小。
安全性:Cookie相对不安全,因为数据存储在客户端,容易受到XSS(跨站脚本攻击)的威胁。不过,可以通过设置HttpOnly属性来防止Java访问,减少XSS攻击的风险,但仍然可能受到CSRF(跨站请求伪造)的攻击。Session通常认为比Cookie更安全,因为敏感数据存储在服务器端。但仍然需要防范Session劫持(通过获取他人的Session ID)和会话固定攻击。
生命周期:Cookie可以设置过期时间,过期后自动删除。也可以设置为会话Cookie,即浏览器关闭时自动删除。Session在默认情况下,当用户关闭浏览器时,Session结束。但服务器也可以设置Session的超时时间,超过这个时间未活动,Session也会失效。
性能:使用Cookie时,因为数据随每个请求发送到服务器,可能会影响网络传输效率,尤其是在Cookie数据较大时。使用Session时,因为数据存储在服务器端,每次请求都需要查询服务器上的Session数据,这可能会增加服务器的负载,特别是在高并发场景下。
简单来说:
运行时异常是程序自身逻辑漏洞导致的意外错误,编译器不强制处理,重点在于通过规范编码避免(如判空、边界检查);
非运行时异常是程序运行中可能遇到的可预期外部问题,编译器强制要求处理,确保程序在异常场景下有合理的应对逻辑。
运行时异常是程序自身逻辑漏洞导致的意外错误,编译器不强制处理,重点在于通过规范编码避免(如判空、边界检查);
非运行时异常是程序运行中可能遇到的可预期外部问题,编译器强制要求处理,确保程序在异常场景下有合理的应对逻辑。
二者的核心区别体现在编译检查要求和使用场景上,具体差异如下:
特性运行时异常(RuntimeException)非运行时异常(Checked Exception) 继承关系继承自 RuntimeException(间接继承 Exception)直接继承 Exception(不包含 RuntimeException分支) 编译检查编译时不强制要求捕获或声明抛出(可处理也可不处理)编译时强制要求捕获(try-catch)或声明抛出(throws) 典型代表NullPointerException、IndexOutOfBoundsException、ClassCastException等IOException、SQLException、ClassNotFoundException等 产生原因通常由程序逻辑错误导致(如空指针访问、数组越界)通常由外部环境因素导致(如文件不存在、网络连接失败) 处理原则应通过代码逻辑优化避免(而非捕获),属于 “编程错误”必须显式处理(捕获或声明),属于 “可预见的外部异常”
4. 抽象类和接口的区别 是什么?
两者的特点:
抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8 以后可以有默认方法和静态方法)。适用于定义类的能力或功能。
抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8 以后可以有默认方法和静态方法)。适用于定义类的能力或功能。
两者的区别:
实现方式:实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
方法方式:接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
访问修饰符:接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态常量)。
实现方式:实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
方法方式:接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
访问修饰符:接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态常量)。
区别:
String 是 Java 中基础且重要的类,被声明为 final class,是不可变字符串。因为它的不可变性,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类。它提供了 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列。在很多情况下我们的字符串拼接操作不需要线程安全,所以 StringBuilder 登场了。
StringBuilder 是 JDK1.5 发布的,它和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。
String 是 Java 中基础且重要的类,被声明为 final class,是不可变字符串。因为它的不可变性,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类。它提供了 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列。在很多情况下我们的字符串拼接操作不需要线程安全,所以 StringBuilder 登场了。
StringBuilder 是 JDK1.5 发布的,它和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。
线程安全:
StringBuffer:线程安全策略盈
StringBuilder:线程不安全
StringBuffer:线程安全
StringBuilder:线程不安全
速度:
一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。
一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。
使用场景:
操作少量的数据使用 String。
单线程操作大量数据使用 StringBuilder。
多线程操作大量数据使用 StringBuffer。
操作少量的数据使用 String。
单线程操作大量数据使用 StringBuilder。
多线程操作大量数据使用 StringBuffer。
6. 什么时候用String?什么时候用StringBulider ?
String 适用于字符串内容无需修改或仅进行少量简单操作的场景,因其不可变性可利用常量池节省内存且适合作为集合键。
而 StringBuilder 则适用于需要频繁修改字符串(如循环拼接、动态生成)的场景,其可变性避免了创建大量中间对象,能显著提升效率,多线程环境下则应使用线程安全的 StringBuffer。
7. JVM的内存结构是什么样的 ?
根据 JDK 8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
JVM的内存结构主要分为以下几个部分:
程序计数器:可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的 Java 方法的 JVM 指令地址。如果线程执行的是 Native 方法,计数器值为 null。是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域,生命周期与线程相同。
Java 虚拟机栈:每个线程都有自己独立的 Java 虚拟机栈,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。可能会抛出 StackOverflowError 和 OutOfMemoryError 异常。
本地方法栈:与 Java 虚拟机栈类似,主要为虚拟机使用到的 Native 方法服务,在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。本地方法执行时也会创建栈帧,同样可能出现 StackOverflowError 和 OutOfMemoryError 两种错误。
Java 堆:是 JVM 中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,用于存放对象实例。从内存回收角度,堆被划分为新生代和老年代,新生代又分为 Eden 区和两个 Survivor 区(From Survivor 和 To Survivor)。如果在堆中没有内存完成实例分配,并且堆也无法扩展时会抛出 OutOfMemoryError 异常。
方法区(元空间):在 JDK 1.8 及以后的版本中,方法区被元空间取代,使用本地内存。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然方法区被描述为堆的逻辑部分,但有 “非堆” 的别名。方法区可以选择不实现垃圾收集,内存不足时会抛出 OutOfMemoryError 异常。
运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,具有动态性,运行时也可将新的常量放入池中。当无法申请到足够内存时,会抛出 OutOfMemoryError 异常。
直接内存:不属于 JVM 运行时数据区的一部分,通过 NIO 类引入,是一种堆外内存,可以显著提高 I/O 性能。直接内存的使用受到本机总内存的限制,若分配不当,可能导致 OutOfMemoryError 异常。
程序计数器:可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的 Java 方法的 JVM 指令地址。如果线程执行的是 Native 方法,计数器值为 null。是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域,生命周期与线程相同。
Java 虚拟机栈:每个线程都有自己独立的 Java 虚拟机栈,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。可能会抛出 StackOverflowError 和 OutOfMemoryError 异常。
本地方法栈:与 Java 虚拟机栈类似,主要为虚拟机使用到的 Native 方法服务,在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。本地方法执行时也会创建栈帧,同样可能出现 StackOverflowError 和 OutOfMemoryError 两种错误。
Java 堆:是 JVM 中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,用于存放对象实例。从内存回收角度,堆被划分为新生代和老年代,新生代又分为 Eden 区和两个 Survivor 区(From Survivor 和 To Survivor)。如果在堆中没有内存完成实例分配,并且堆也无法扩展时会抛出 OutOfMemoryError 异常。
方法区(元空间):在 JDK 1.8 及以后的版本中,方法区被元空间取代,使用本地内存。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然方法区被描述为堆的逻辑部分,但有 “非堆” 的别名。方法区可以选择不实现垃圾收集,内存不足时会抛出 OutOfMemoryError 异常。
运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,具有动态性,运行时也可将新的常量放入池中。当无法申请到足够内存时,会抛出 OutOfMemoryError 异常。
直接内存:不属于 JVM 运行时数据区的一部分,通过 NIO 类引入,是一种堆外内存,可以显著提高 I/O 性能。直接内存的使用受到本机总内存的限制,若分配不当,可能导致 OutOfMemoryError 异常。
实现多线程主要有三种核心方式:
继承Thread类,重写其run方法定义线程执行逻辑,通过调用start方法启动线程(而非直接调用run)。代码如下:
继承Thread类,重写其run方法定义线程执行逻辑,通过调用start方法启动线程(而非直接调用run)。代码如下:
classMyThreadextendsThread{
@Override
publicvoidrun{
for(inti = 0; i < 5; i++) {
System.out.println("Thread "+ Thread.currentThread.getId + ": "+ i);
try{
Thread.sleep(100); // 休眠100毫秒
} catch(InterruptedException e) {
e.printStackTrace;
}
}
}
}
publicclassThreadExample{
publicstaticvoidmain(String[] args){
MyThread thread1 = newMyThread;
MyThread thread2 = newMyThread;
thread1.start; // 启动线程
thread2.start;
}
}
实现Runnable接口,重写run方法,然后将实现类实例作为参数传入Thread类的构造方法,再通过Thread对象的start启动线程。这种方式更灵活,可避免单继承限制,适合多线程共享资源的场景。代码如下:
实现Runnable接口,重写run方法,然后将实现类实例作为参数传入Thread类的构造方法,再通过Thread对象的start启动线程。这种方式更灵活,可避免单继承限制,适合多线程共享资源的场景。代码如下:
classMyRunnableimplementsRunnable{
@Override
publicvoidrun{
for(inti = 0; i < 5; i++) {
System.out.println("Runnable "+ Thread.currentThread.getId + ": "+ i);
try{
Thread.sleep(100);
} catch(InterruptedException e) {
e.printStackTrace;
}
}
}
}
publicclassRunnableExample{
publicstaticvoidmain(String[] args){
Thread thread1 = newThread(newMyRunnable);
Thread thread2 = newThread(newMyRunnable);
thread1.start;
thread2.start;
}
}
实现Callable接口(JDK 1.5+),重写call方法(该方法可返回结果并抛出异常),结合Future或FutureTask使用,能获取线程执行的返回值,适用于需要异步获取结果的场景。代码如下:
实现Callable接口(JDK 1.5+),重写call方法(该方法可返回结果并抛出异常),结合Future或FutureTask使用,能获取线程执行的返回值,适用于需要异步获取结果的场景。代码如下:
importjava.util.concurrent.ExecutionException;
importjava.util.concurrent.FutureTask;
// 实现Callable接口
classMyCallableimplementsCallable<Integer> {
@Override
publicInteger callthrowsException {
intsum = 0;
for(inti = 0; i <= 5; i++) {
sum += i;
System.out.println("Callable "+ Thread.currentThread.getId + ": "+ i);
Thread.sleep(100);
}
returnsum; // 返回计算结果
}
}
publicclassCallableExample{
publicstaticvoidmain(String[] args)throwsExecutionException, InterruptedException {
FutureTask<Integer> futureTask1 = newFutureTask<>(newMyCallable);
FutureTask<Integer> futureTask2 = newFutureTask<>(newMyCallable);
Thread thread1 = newThread(futureTask1);
Thread thread2 = newThread(futureTask2);
thread1.start;
thread2.start;
// 获取线程执行结果
System.out.println("Result 1: "+ futureTask1.get);
System.out.println("Result 2: "+ futureTask2.get);
}
}
9. 线程池的常用参数有哪些?
线程池七大核心参数如下所示:
corePoolSize:线程池核心线程数量,如果设置为5,线程池初始化后默认保持5个线程待命。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
maximumPoolSize:线程池允许的最大线程数。当任务队列已满,且当前线程数小于 maximumPoolSize时,线程池会创建新的线程来处理任务,直至线程数达到 maximumPoolSize。
keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
unit:就是keepAliveTime时间的单位。
workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
threadFactory:用于创建线程的工厂。通过自定义线程工厂,你可以为线程设置名称、优先级等属性。
handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。
corePoolSize:线程池核心线程数量,如果设置为5,线程池初始化后默认保持5个线程待命。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
maximumPoolSize:线程池允许的最大线程数。当任务队列已满,且当前线程数小于 maximumPoolSize时,线程池会创建新的线程来处理任务,直至线程数达到 maximumPoolSize。
keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
unit:就是keepAliveTime时间的单位。
workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
threadFactory:用于创建线程的工厂。通过自定义线程工厂,你可以为线程设置名称、优先级等属性。
handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。
synchronized是 Java 中用于实现线程同步的关键字,其核心原理是通过 获取对象锁来保证临界区代码的原子性、可见性和有序性,从而避免多线程并发访问共享资源时出现的数据不一致问题。
publicvoidsyncMethod{
synchronized(this) {
// 临界区代码
}
}
其底层实现依赖 JVM 内置锁(监视器锁,Monitor)和 对象头中的标记字段,具体原理可分为以下几个层面:
对象监视器(Monitor):Java 中每个对象都关联一个 监视器(Monitor),它是一种同步工具,包含一个 互斥锁和等待队列(用于线程等待 / 唤醒)。当线程执行 synchronized修饰的代码时,必须先 获取对象的 Monitor 锁,执行完后再 释放锁,同一时刻,只有一个线程能持有 Monitor 锁(互斥性),其他线程尝试获取锁时会被阻塞,进入等待队列,直到锁被释放。
对象头与 Mark Word:锁的状态信息存储在对象的 对象头(Object Header)中,其中 Mark Word字段是核心,用于记录对象的锁状态(无锁、偏向锁、轻量级锁、重量级锁)。JVM 通过 Mark Word 的值判断锁状态,并动态升级锁(锁膨胀)以优化性能:
无锁状态:Mark Word 存储对象哈希码等信息。
偏向锁:适用于单线程重复获取锁的场景,只在第一次获取时 CAS 操作设置线程 ID,之后直接判断线程 ID 即可,几乎无开销。
轻量级锁:多线程交替执行时,通过 CAS 尝试获取锁,失败则自旋(忙等),避免阻塞线程(适合短时间持有锁的场景)。
重量级锁:多线程竞争激烈时,升级为依赖操作系统互斥量(Mutex)的重量级锁,未获取锁的线程会被阻塞并进入内核态,虽然开销大,但适合长时间持有锁的场景。
字节码层面:javac编译 synchronized代码时,会在同步块前后插入 monitorenter和 monitorexit指令:
monitorenter:尝试获取对象的 Monitor 锁,若成功则锁计数器加 1,失败则阻塞。
monitorexit:释放锁,锁计数器减 1,当计数器为 0 时,其他线程可竞争锁(确保异常时也能释放锁,会生成两个 monitorexit,一个正常退出,一个异常退出)。
对象监视器(Monitor):Java 中每个对象都关联一个 监视器(Monitor),它是一种同步工具,包含一个 互斥锁和等待队列(用于线程等待 / 唤醒)。当线程执行 synchronized修饰的代码时,必须先 获取对象的 Monitor 锁,执行完后再 释放锁,同一时刻,只有一个线程能持有 Monitor 锁(互斥性),其他线程尝试获取锁时会被阻塞,进入等待队列,直到锁被释放。
对象头与 Mark Word:锁的状态信息存储在对象的 对象头(Object Header)中,其中 Mark Word字段是核心,用于记录对象的锁状态(无锁、偏向锁、轻量级锁、重量级锁)。JVM 通过 Mark Word 的值判断锁状态,并动态升级锁(锁膨胀)以优化性能:
无锁状态:Mark Word 存储对象哈希码等信息。
偏向锁:适用于单线程重复获取锁的场景,只在第一次获取时 CAS 操作设置线程 ID,之后直接判断线程 ID 即可,几乎无开销。
轻量级锁:多线程交替执行时,通过 CAS 尝试获取锁,失败则自旋(忙等),避免阻塞线程(适合短时间持有锁的场景)。
重量级锁:多线程竞争激烈时,升级为依赖操作系统互斥量(Mutex)的重量级锁,未获取锁的线程会被阻塞并进入内核态,虽然开销大,但适合长时间持有锁的场景。
无锁状态:Mark Word 存储对象哈希码等信息。
偏向锁:适用于单线程重复获取锁的场景,只在第一次获取时 CAS 操作设置线程 ID,之后直接判断线程 ID 即可,几乎无开销。
轻量级锁:多线程交替执行时,通过 CAS 尝试获取锁,失败则自旋(忙等),避免阻塞线程(适合短时间持有锁的场景)。
重量级锁:多线程竞争激烈时,升级为依赖操作系统互斥量(Mutex)的重量级锁,未获取锁的线程会被阻塞并进入内核态,虽然开销大,但适合长时间持有锁的场景。
字节码层面:javac编译 synchronized代码时,会在同步块前后插入 monitorenter和 monitorexit指令:
monitorenter:尝试获取对象的 Monitor 锁,若成功则锁计数器加 1,失败则阻塞。
monitorexit:释放锁,锁计数器减 1,当计数器为 0 时,其他线程可竞争锁(确保异常时也能释放锁,会生成两个 monitorexit,一个正常退出,一个异常退出)。
monitorenter:尝试获取对象的 Monitor 锁,若成功则锁计数器加 1,失败则阻塞。
monitorexit:释放锁,锁计数器减 1,当计数器为 0 时,其他线程可竞争锁(确保异常时也能释放锁,会生成两个 monitorexit,一个正常退出,一个异常退出)。
synchronized的核心原理是 基于对象 Monitor 实现的互斥锁机制,通过对象头的 Mark Word 记录锁状态,并动态升级锁(偏向锁→轻量级锁→重量级锁)以平衡性能与安全性。其底层通过 monitorenter/monitorexit指令控制锁的获取与释放,最终保证多线程并发访问共享资源时的同步性。JDK 1.6 后对 synchronized进行了大量优化(如锁膨胀、偏向锁、适应性自旋),性能已接近 ReentrantLock。
11. 数据库底层数据结构是什么?为什么用B+树 ?
MySQL InnoDB 存储引擎索引默认的数据结构是 B + 树。
要理解 B + 树的优势,需要先明确索引的核心需求:减少磁盘 I/O 次数(数据库数据通常存储在磁盘,磁盘 I/O 速度远慢于内存,查询效率的关键是降低磁盘读写次数)。B + 树通过自身结构设计,完美适配这一需求,具体原因可从以下几点展开:
多阶平衡结构,大幅降低树的高度(减少 I/O 次数):普通二叉树(如二叉搜索树)的高度与数据量呈O(log₂n)增长(例如 100 万条数据,高度约 20),但 B + 树是多阶平衡树,通常阶数很高(如 InnoDB 中,每个节点对应磁盘页,16KB 的页可存储约 1000 个索引项,即阶数约 1000)。此时,树的高度会急剧降低:例如 1000 万条数据,B + 树高度仅需 3(1000³ = 10 亿,足以覆盖千万级数据)。这意味着查询任意一条数据,仅需 3 次磁盘 I/O(读取根节点→中间节点→叶子节点),而二叉树需要 20 次以上,效率差距巨大。
非叶子节点仅存索引,叶子节点存完整数据,空间利用率更高:B + 树的非叶子节点只存储索引键(不存数据),因此每个节点能容纳更多索引项,进一步提升阶数、降低树高;而 B 树(注意:B 树≠B + 树)的非叶子节点既存索引又存数据,每个节点容纳的索引项更少,树高更高,I/O 次数更多。B + 树的叶子节点存储完整数据(或数据的物理地址),且所有叶子节点通过 “双向链表” 连接,既能支持高效的 “随机查询”(通过索引定位到叶子节点),也能支持高效的 “范围查询”(无需回溯树结构,直接遍历叶子节点的链表即可,如查询 “id>100 且 id<200” 的数据)。
平衡特性,保证查询效率稳定:B + 树是严格平衡的树(所有叶子节点在同一层),无论查询哪个数据,都需要遍历从根节点到叶子节点的路径,磁盘 I/O 次数固定(即查询时间稳定);而二叉搜索树可能因数据插入顺序变成 “斜树”(类似链表),查询效率退化到 O (n)(最坏情况需遍历所有节点),稳定性远不如 B + 树。
多阶平衡结构,大幅降低树的高度(减少 I/O 次数):普通二叉树(如二叉搜索树)的高度与数据量呈O(log₂n)增长(例如 100 万条数据,高度约 20),但 B + 树是多阶平衡树,通常阶数很高(如 InnoDB 中,每个节点对应磁盘页,16KB 的页可存储约 1000 个索引项,即阶数约 1000)。此时,树的高度会急剧降低:例如 1000 万条数据,B + 树高度仅需 3(1000³ = 10 亿,足以覆盖千万级数据)。这意味着查询任意一条数据,仅需 3 次磁盘 I/O(读取根节点→中间节点→叶子节点),而二叉树需要 20 次以上,效率差距巨大。
非叶子节点仅存索引,叶子节点存完整数据,空间利用率更高:B + 树的非叶子节点只存储索引键(不存数据),因此每个节点能容纳更多索引项,进一步提升阶数、降低树高;而 B 树(注意:B 树≠B + 树)的非叶子节点既存索引又存数据,每个节点容纳的索引项更少,树高更高,I/O 次数更多。B + 树的叶子节点存储完整数据(或数据的物理地址),且所有叶子节点通过 “双向链表” 连接,既能支持高效的 “随机查询”(通过索引定位到叶子节点),也能支持高效的 “范围查询”(无需回溯树结构,直接遍历叶子节点的链表即可,如查询 “id>100 且 id<200” 的数据)。
平衡特性,保证查询效率稳定:B + 树是严格平衡的树(所有叶子节点在同一层),无论查询哪个数据,都需要遍历从根节点到叶子节点的路径,磁盘 I/O 次数固定(即查询时间稳定);而二叉搜索树可能因数据插入顺序变成 “斜树”(类似链表),查询效率退化到 O (n)(最坏情况需遍历所有节点),稳定性远不如 B + 树。
文件相关(mv mkdir cd ls)
进程相关( ps top netstate )
权限相关(chmod chown useradd groupadd)
网络相关(netstat ip addr)
测试相关(测试网络连通性:ping 测试端口连通性:telnet)策略盈
文件相关(mv mkdir cd ls)
进程相关( ps top netstate )
权限相关(chmod chown useradd groupadd)
网络相关(netstat ip addr)
测试相关(测试网络连通性:ping 测试端口连通性:telnet)
发布于:广东省领航优配提示:文章来自网络,不代表本站观点。