除了自身以外数组的乘积
要点:从左到右,然后从右到左,两个数组累乘
java
class Solution {
public int[] productExceptSelf(int[] nums) {
//左边累计乘法累计乘法
int n = nums.length;
int[] left = new int[n];
int[] right = new int[n];
left[0] = nums[0];
for(int i = 1; i < n; i++){
left[i] = left[i-1]*nums[i];
}
right[n-1] = nums[n-1];
for(int i = n-2; i >= 0; i--){
right[i] = right[i+1] * nums[i];
}
int[] ans = new int[n];
for(int i = 0; i < n; i++){
if(i == 0){
ans[i] = right[1];
}else if(i == n-1){
ans[i] = left[n-2];
}else{
ans[i] = left[i-1] * right[i+1];
}
}
return ans;
}
}
Z 字形变换
要点:flag = -1 or 1
java
class Solution {
public String convert(String s, int numRows) {
if(numRows < 2) return s;
List<StringBuilder> rows = new ArrayList<StringBuilder>();
for(int i = 0; i < numRows; i++){
rows.add(new StringBuilder());
}
int flag = -1;
int i = 0;
for(char c : s.toCharArray()){
rows.get(i).append(c);
if(i == 0 || i == numRows -1){
flag = - flag;
}
i += flag;
}
StringBuilder ans = new StringBuilder();
for(StringBuilder row : rows){
ans.append(row);
}
return ans.toString();
}
}
有效的数独
要点:三个数组应对三个boolean【列/行/条件】【数字】类型的判断
java
class Solution {
public boolean isValidSudoku(char[][] board) {
//三个boolean表示三个条件
boolean[][] hang = new boolean[9][9];
boolean[][] lie = new boolean[9][9];
boolean[][][] kuan = new boolean[3][3][9];
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j++){
if(board[i][j] == '.'){
continue;
}
int shu = board[i][j] - '1';
if( hang[i][shu] || lie[j][shu] || kuan[i/3][j/3][shu]){
return false;
}
hang[i][shu] = lie[j][shu] = kuan[i/3][j/3][shu] = true;
}
}
return true;
}
}
随机知识
JMM-Java 内存模型与 happens-before 面试终极总结
时间:26.6.14
第一部分:核心知识点(必须背诵)
一、JMM 是什么?
- Java 内存模型(Java Memory Model) :是一个抽象规范,定义了多线程程序中共享变量的可见性、有序性、原子性的保证规则。
- 核心目标:屏蔽不同硬件/操作系统的内存访问差异,让 Java 并发程序在各种平台上行为一致。
- 抽象结构:主内存(共享变量)+ 工作内存(线程私有缓存,包含共享变量的副本)。线程间通信必须经过主内存。
二、happens-before 规则(共 7 条,默写前 5 条即可)
- 程序次序规则:同一线程内,书写在前面的操作 happens-before 后面的操作。
- 管程锁定规则:对锁的 unlock 操作 happens-before 后续对同一个锁的 lock 操作。
- volatile 变量规则:对 volatile 变量的写 happens-before 后面对该变量的读。
- 线程启动规则:线程的 start() 方法 happens-before 该线程内的任何动作。
- 线程终止规则:线程内的所有动作 happens-before 其他线程通过 join() 或 isAlive() 检测到该线程终止。
- 中断规则:对线程 interrupt() 的调用 happens-before 被中断线程检测到中断。
- 对象终结规则:对象构造函数执行结束 happens-before 其 finalize() 方法开始。
传递性:若 A happens-before B,B happens-before C,则 A happens-before C。
三、内存屏障(底层实现)
| 屏障类型 | 作用 | 插入场景 |
|---|---|---|
| LoadLoad | 禁止读读重排序,屏障前的读先于屏障后的读完成 | volatile 读之后 |
| LoadStore | 禁止读写重排序,屏障前的读先于屏障后的写完成 | volatile 读之后 |
| StoreStore | 禁止写写重排序,屏障前的写先于屏障后的写完成 | volatile 写之前 |
| StoreLoad | 禁止写读重排序,屏障前的写先于屏障后的读,且会刷新写缓冲区到主内存 | volatile 写之后(最重屏障) |
volatile 写 :插入 StoreStore(禁止普通写与 volatile 写重排序)+ StoreLoad(刷新所有写结果到主内存,并使其他 CPU 缓存行失效)
volatile 读:插入 LoadLoad + LoadStore(禁止后续普通读写重排序到 volatile 读之前,并强制从主内存读取)
四、volatile 与 synchronized 对比
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | 保证(写立即可见) | 保证(解锁时刷新,加锁时重新读) |
| 原子性 | 不保证复合操作(如 i++) | 保证(代码块内互斥) |
| 有序性 | 禁止重排序(通过屏障) | 禁止重排序(通过锁边界) |
| 适用场景 | 单个变量的读写,状态标志,DCL 中的 instance | 复合操作,多变量一致更新 |
五、DCL(双重检查锁)为什么要用 volatile?
java
class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 非原子:1.分配内存 2.初始化 3.赋值引用
}
}
}
return instance;
}
}
- 没有 volatile 时,步骤 2 和 3 可能重排序(先赋值引用,后初始化),其他线程会看到非 null 但未初始化的对象。
volatile禁止了这种重排序(通过 StoreStore + StoreLoad 屏障),保证初始化完成后再赋值。
六、final 字段的特殊语义
- 在构造函数内对 final 字段的写入,与随后将该对象引用赋值给其他引用之间,存在 happens-before 关系。
- 只要对象正确构造(无 this 逃逸),所有线程都能看到 final 字段的正确值,无需额外同步。
七、常见重排序示例与修复
无同步问题代码:
java
int a = 0; boolean flag = false; // 普通变量
// T1: a = 1; flag = true;
// T2: if (flag) System.out.println(a);
可能输出 0 或 无限循环。修复:将 flag 声明为 volatile(利用传递性保证 a 的可见性)。
第二部分:面试问答库(标准答案)
Q1:什么是 JMM?它的核心目标是什么?
A :JMM 是 Java 内存模型的缩写,是一个抽象规范,定义了一套 happens-before 规则。核心目标是屏蔽不同硬件和操作系统的内存访问差异 ,保证多线程程序的原子性、可见性、有序性,让 Java 程序在各种平台上表现一致。
Q2:请说出 happens-before 规则中任意五条。
A:(如前述 1-5 条)
Q3:volatile 是如何保证可见性的?底层用了什么屏障?
A:
- 写 volatile:插入 StoreStore 屏障(禁止普通写与 volatile 写重排序),再插入 StoreLoad 屏障(强制将写缓冲区所有数据刷新到主内存,并使其他 CPU 的缓存行失效)。
- 读 volatile :插入 LoadLoad 和 LoadStore 屏障(禁止后续普通读写重排序到读之前,并强制从主内存读取,而不是本地缓存)。
从而建立 volatile 写 happens-before volatile 读 的规则。
Q4:synchronized 是如何保证可见性的?
A :当线程释放锁(退出 synchronized 块)时,会将工作内存中的修改刷新到主内存(类似 StoreLoad 屏障效果);当线程获取锁(进入 synchronized 块)时,会从主内存重新读取变量(类似 LoadLoad + LoadStore 屏障效果)。因此,锁的 unlock happens-before 后续 lock,保证了临界区内所有变量的可见性。
Q5:什么是重排序?happens-before 如何禁止有害重排序?
A :重排序是编译器、CPU 为了提高性能而调整指令执行顺序的行为。happens-before 规则通过内存屏障禁止可能破坏可见性/有序性的重排序,例如:
- volatile 写之前的普通写不能重排序到 volatile 写之后(StoreStore)。
- volatile 读之后的普通读不能重排序到 volatile 读之前(LoadLoad)。
- synchronized 块内的代码不能重排序到块外(通过锁边界屏障)。
Q6:DCL 中为什么要用 volatile?不用会出现什么问题?
A :因为 instance = new Singleton() 不是原子操作,分为三步:分配内存、初始化对象、将引用赋值给变量。没有 volatile 时,步骤 2 和 3 可能重排序(先赋值引用,后初始化)。另一线程看到 instance != null,但拿到的是未初始化的对象,使用时会出错。volatile 禁止这种重排序,保证对象初始化完成后再赋值。
Q7:以下代码是否有数据竞争?如何修复?
java
int a = 0; boolean flag = false;
// T1: a = 1; flag = true;
// T2: if (flag) System.out.println(a);
A :存在数据竞争。因为 flag 是普通变量,没有 happens-before 关系,T2 可能看到 flag = true 但 a = 0(可见性问题或重排序)。修复 :将 flag 声明为 volatile,此时 a=1 happens-before flag=true(程序次序),flag=true happens-before flag 读(volatile 规则),flag 读 happens-before a 读(程序次序),传递得 a=1 happens-before a 读,因此输出 1。
Q8:final 字段在 JMM 中有何特殊规则?
A:只要对象正确构造(构造函数中没有将 this 引用泄露给其他线程),那么其他线程无需同步就能看到 final 字段在构造函数中设置的最终值。这是因为 JMM 提供了特殊的 happens-before 保证:构造函数内对 final 字段的写 happens-before 将该对象引用赋值给其他线程可见的变量。
Q9:as-if-serial 是什么意思?和 happens-before 什么关系?
A :as-if-serial 是 JMM 对单线程程序的承诺:无论编译器或 CPU 如何重排序,执行结果不能被改变,就好像程序是顺序执行的一样。happens-before 中的程序次序规则保证了 as-if-serial 语义;在多线程中,happens-before 扩展了该概念,规定了跨线程的可见性。
Q10:StoreStore 屏障如何禁止重排序?它作用范围是所有写操作吗?
A :StoreStore 屏障是一条抽象指令,告诉 CPU/编译器:屏障前的所有写操作(包括普通变量和 volatile 变量)必须在屏障后的写操作之前完成,并且对其他处理器可见。它会强制清空写缓冲区中屏障前的写操作,或者禁止重排序跨越该屏障。作用范围是屏障前的所有写操作,不限于 volatile。
附录:典型代码题目汇总及解析(面试必考)
题目1:NoVisibility -- 普通变量可见性问题
java
public class NoVisibility {
private static boolean ready = false;
private static int number = 0;
private static class ReaderThread extends Thread {
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String\[\] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
问 :可能输出什么?为什么?如何修复?
答:
- 可能输出 42(运气好)
- 可能无限循环(ready 的更新对读线程不可见)
- 可能输出 0(重排序导致 ready = true 先于 number = 42 刷新到主内存)
原因 :没有 happens-before 关系,允许重排序和本地缓存。
修复 :将ready声明为volatile。
题目2:volatile 写-读传递性(经典题目)
java
volatile int a = 0;
volatile int b = 0;
// 线程1
a = 1; // volatile写
b = 2; // volatile写
// 线程2(在线程1之后执行)
int rb = b; // volatile读
int ra = a; // volatile读
问 :线程2能看到 ra=1, rb=2 吗?如果交换线程2中读的顺序呢?
答:
- 一定能看到。因为
a=1happens-beforeb=2(程序次序),b=2happens-beforerb=b(volatile写-读),rb=bhappens-beforera=a(程序次序),传递得a=1happens-beforera=a。 - 即使线程2先读
a再读b,由于b=2尚未被读,但a=1已经因b=2的 volatile 写而刷新到主内存,且读a发生在读b之前?这里注意:如果线程2先读a,此时b=2还没有被读,但a=1因为b=2的 StoreLoad 屏障已经被刷到主内存,所以a读能看见 1。严格分析:a=1happens-beforeb=2,而b=2happens-before 线程2的b读,但b读还没发生,所以a读没有直接的 happens-before 关系?实际上,如果线程2先读a,没有 volatile 读b发生,则a读与a=1之间只有b=2的写-读链条,但b读未执行,因此不能保证a读到 1。但题目隐含线程2的两个读都发生在b=2之后(即先读b再读a或同时)。如果先读a再读b,且a读在线程1的b=2之前(物理时间),则可能读到 0。所以通常题目会保证b读先发生。
结论 :只要线程2的任意一个 volatile 读发生在b=2之后,那么该读及其之后的所有普通读都能看到a=1。
题目3:普通变量 + volatile flag 传递性修复
java
int a = 0;
volatile boolean flag = false;
// 线程1
a = 1;
flag = true;
// 线程2
if (flag) {
System.out.println(a);
}
问 :线程2一定能看到 a=1 吗?为什么?
答 :
一定能。a=1 happens-before flag=true(程序次序),flag=true happens-before 线程2的 flag 读(volatile规则),flag 读 happens-before a 读(程序次序),传递得 a=1 happens-before a 读,因此看到 1。
追问 :为什么 a 不用 volatile?
答 :因为 flag 的 volatile 写会通过 StoreStore 屏障将 a=1 刷新到主内存,volatile 读后的 LoadLoad 屏障会强制 a 从主内存读取。
题目4:DCL 与 volatile(双重检查锁)
java
class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
问 :这段代码有什么问题?如何修复?
答 :
问题在于 instance = new Singleton() 不是原子操作,可能发生重排序:先分配内存并赋值引用(instance 非 null),然后才初始化对象。另一个线程进入 if (instance == null) 判断为 false,返回未初始化的对象,使用时出错。
修复 :将 instance 声明为 volatile,禁止重排序。
题目5:StoreStore 屏障代码示例(写写重排序)
java
int x = 0;
volatile int v = 0;
// 线程1
x = 42; // 普通写
v = 1; // volatile写
问 :如果没有 StoreStore 屏障,可能发生什么?StoreStore 如何阻止?
答 :
可能发生重排序:先执行 v=1,再执行 x=42。这样另一个线程看到 v=1 时,x 可能还是 0。
StoreStore 屏障插入在 x=42 和 v=1 之间,强制要求 x=42 必须完成(写到主内存或写缓冲区刷出)之后才能执行 v=1,从而禁止重排序。
知识点总结:
屏障不是针对关键词修饰的变量,是在这个变量之前的所有都要刷新写和读
碎碎念:后续会更新每天学习的八股和算法 题,开始准备秋招的第35天。努力连续更新100天!以后每天就按,秋招项目【java +agent】,科研,必做项目,算法,八股,锻炼身体来总结。
总结:要掌控好自己的时间
1.算法要系统过一遍【晚上】1h
2.秋招项目,【java 】开始实际看业务,1/6;无
【agent 】还在学,整理cc,无,
3.科研要跑一下,
4.检测项目也得总结文档,
6.背八股,【30min】
7.锻炼身体,【30min]
反思:睡到中午,然后下午加班两小时,回来一直玩手机,晚上学了两小时。