最后
最后,强调几点:
- 1. 一定要谨慎对待写在简历上的东西,一定要对简历上的东西非常熟悉。因为一般情况下,面试官都是会根据你的简历来问的; 能有一个上得了台面的项目也非常重要,这很可能是面试官会大量发问的地方,所以在面试之前好好回顾一下自己所做的项目;
- 2. 和面试官聊基础知识比如设计模式的使用、多线程的使用等等,可以结合具体的项目场景或者是自己在平时是如何使用的;
- 3. 注意自己开源的Github项目,面试官可能会挖你的Github项目提问;
我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目。
面试答案
| — | — | — |
| 内存屏障 | memory barriers | 是一组处理器指令,用于实现对内存操作的顺序限制 |
| 缓冲行 | cache line | CPU高速缓存中可以分配的最小单位。处理器填写缓存行时会加载整个缓存行,现代CPU 需要执行几百次CPU指令 |
| 原子操作 | atomic operations | 不可中断的一个或者一系列操作 |
| 缓存行填充 | cache line hit | 当处理器识别到从内存中读取操作数是缓存的,处理器读取整个缓存行到适当的缓存(L1 \L2 \L3) |
| 缓存命中 | cache hit | 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是内存 |
| 写命中 | write hit | 当处理器将操作数协会到一个内存缓存的区域时,他会首先检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器会将这个操作数写回到缓存,而不是写回到内存,这个操作就是写命中 |
| 写缺失 | write misses the cache | 一个有效的缓存行被写入到不存在的内存区域 |
原理:被volatile修饰的变量,通过jvm最终生成的汇编指令会多出一行汇编代码,这行代码是Lock前缀的。
// java 代码
// instance 被 volatile关键字修饰
instance = new Singleton();
// 通过工具获取的JIT编译器生成汇编指令如下
0x01a3deld: movb $0…: lock add1 $x0,(%esp);
Lock 前缀的指令在多核处理器下会引发两件事情
-
将处理器缓存行的数据写回到系统内存
-
这个写回内存的操作会使在其他CPU缓存了该内存地址的数据无效
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存中的数据读到内存缓存(L1 L2或其他)后在进行操作,但操作完不知道何时写会内存。如果对申明了volatile的变量进行操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧值,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线传播的数据来检查自己的缓存是否过期了,当处理器发现自己缓存的数据对应的内存地址被修改,就会将当前处理器的缓存行设置为无效,当处理器对这个数据进行修改时,就会重新从内存中读取数据到缓存中。
=====================================================================================
在多线程并发编程中synchronized一直是元老角色,很多人称呼它为重量级。但是随着Java SE 1.6对synchronized进行了各种优化之后,有些情况synchronized它不在那么重了。接下来阐述的知识点是关于偏向锁、轻量级锁,以及锁的存储结构和升级过程。
synchronized在Java中三种表现形式
-
对于普通同步方法,锁的是当前实例对象
-
对于静态同步方法,锁的是类的class对象
-
对于同步方法块,锁的是synchronized括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时,必须释放锁。name锁到底存在哪里呢?锁里面存的又是什么信息呢?
在JVM规范中可以看到synchronized在JVM中的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,两者实现的细节不一样。代码块使用monitorenter和monitorexit指令实现,而方法同步使用的是另外一种情况,这个在JVM规范中并没有讲解。但是,方法的同步也可使用这两个指令来实现。
-
monitorenter指令是在编译后插入到同步代码块的开始位置
-
monitorexit指令插入到方法结束的位置和异常处
-
JVM要保证每个monitorenter必须有与之对应的monitorexit配对
-
任何对象都有一个monitor与之关联
-
当一个monitor被持有后,它将处于锁定状态
-
线程执行monitorenter指令时,将会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁
synchronize用的锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数据类型,则虚拟机用2个字宽存储对象头 。在32位虚拟机中1字宽等于4字节,即32bit。
Java对象头的长度
| 长度 | 内容 | 说明 |
| — | — | — |
| 32/64bit | Mark Word | 存储对象的hashCod或锁信息 |
| 32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
| 32/64bit | Array length | 数组的长度(如果当前对象是数组) |
Java对象头的Mark Word默认存储的是对象的hashCode、分代年龄和锁标志位。32位JVM的Mark Word的默认存储结构如表
Java对象头存储结构
| 锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
| — | — | — | — | — |
| 无锁状态 | 对象的hashCode | 对象的分代年龄 | 0 | 01 |
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,内容较为复杂,图先不讲了,有兴趣的可以查看相关书籍信息
Java SE 1.6 为了减少获得锁和释放锁带来性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级。注意:锁的升级是不可逆的,意味着偏向锁升级为轻量级锁之后是不能降级为偏向锁的。
2.1.1 偏向锁
HotSpot的作者研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取的,为了让线程获得锁的代价更低,引入了偏向锁。 注意:这个是设计偏向锁的原因和解决思路
2.1.1.1 偏向锁的获取
当一个线程访问同步代码块并获取锁时,会在对线头和栈帧中的锁记录中存储锁偏向的线程ID,以后该线程在进入和退出同步代码块的时候,不需要进行CAS操作来加锁和解锁,只需要简单的测试一下对线头Mark Word里是否存储了当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要在测试一下Mark Word中偏向锁标识是否被设置成立1(表示当前是偏向锁):如果没有设置则使用CAS竞争锁;如果这事了,则尝试使用CAS将当前对象头的偏向锁指向当前线程。
2.1.1.2 偏向锁的撤销(非常妙这里)
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁是,持有偏向锁的线程才会释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁标记活着标记该对象不适合作为偏向锁,最后唤醒暂停的线程
2.1.1.3 关闭偏向锁
Java 1.6 1.7偏向锁是默认开启的,但是它在应用程序启动几秒钟后才会激活,我们可以修改JVM参数来关闭延迟,或者确定应用程序里所有的锁通常情况下都是出于竞争状态,可以直接关闭偏向锁
关闭偏向锁延迟
-XX:BiasedLockingStartupDelay=0
关闭偏向锁 程序默认进入轻量级锁状态
-XX:-UseBiasedLocking=false
2.1.2 轻量级锁
2.1.2.1 轻量级锁加锁
线程在执行同步代码块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试自旋获得锁。
2.1.2.2 轻量级锁解锁
轻量级锁解锁时,使用的是CAS操作将Displaced Mark Word替换回对象头,如果成功则表示没有竞争;如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。 注意:由于自旋过程消耗CPU,为了避免无用的自旋,一当升级为重量级锁,那么就不会再恢复到轻量级锁状态。当前锁出于重量级锁状态时,其他线程尝试获取锁,都会被阻塞,当持有锁的线程释放锁后会唤醒这些线程,重新进行锁的争夺。
2.1.3 锁的优缺点对比
锁的优缺点对比
| 锁 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 偏向锁 | 加锁和解锁过程不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差异 | 如果线程存在锁竞争,会带来额外的锁撤销开销 | 适用于只有一个线程访问同步代码块 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步代码块执行速度非常快 |
| 重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行速度较长 |
在Java中可以通过锁和循环CAS的方式实现原子操作
3.1 使用循环CAS实现原子操作
JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现。自旋CAS实现的基本思路就是循环进行CAS操作知道成功为止,示例代码实现一个安全的计数器和非安全的计数器。
package com.liziba;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
-
@auther LiZiBa
-
@date 2021/2/28 17:39
-
@description: 计数器实现
**/
public class Counter {
// 安全计数器统计数
private AtomicInteger atomicInteger = new AtomicInteger(0);
// 非安全计数器统计数
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List threads = new ArrayList<>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// 非安全计数器
cas.count();
// 安全计数器
cas.safeCount();
总结
至此,文章终于到了尾声。总结一下,我们谈论了简历制作过程中需要注意的以下三个部分,并分别给出了一些建议:
- 技术能力:先写岗位所需能力,再写加分能力,不要写无关能力;
- 项目经历:只写明星项目,描述遵循 STAR 法则;
- 简历印象:简历遵循三大原则:清晰,简短,必要,要有的放矢,不要海投;
以及最后为大家准备的福利时间:简历模板+Java面试题+热门技术系列教程视频
时间:简历模板+Java面试题+热门技术系列教程视频
[外链图片转存中…(img-EH24hphJ-1715080653807)]
[外链图片转存中…(img-Ceu2mn9M-1715080653808)]
[外链图片转存中…(img-cFWVYxnm-1715080653808)]