leecodecode【面试150】【2026.6.14打卡-java版本】

除了自身以外数组的乘积

要点:从左到右,然后从右到左,两个数组累乘

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 条即可)

  1. 程序次序规则:同一线程内,书写在前面的操作 happens-before 后面的操作。
  2. 管程锁定规则:对锁的 unlock 操作 happens-before 后续对同一个锁的 lock 操作。
  3. volatile 变量规则:对 volatile 变量的写 happens-before 后面对该变量的读。
  4. 线程启动规则:线程的 start() 方法 happens-before 该线程内的任何动作。
  5. 线程终止规则:线程内的所有动作 happens-before 其他线程通过 join() 或 isAlive() 检测到该线程终止。
  6. 中断规则:对线程 interrupt() 的调用 happens-before 被中断线程检测到中断。
  7. 对象终结规则:对象构造函数执行结束 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=1 happens-before b=2(程序次序),b=2 happens-before rb=b(volatile写-读),rb=b happens-before ra=a(程序次序),传递得 a=1 happens-before ra=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=1 happens-before b=2,而 b=2 happens-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=42v=1 之间,强制要求 x=42 必须完成(写到主内存或写缓冲区刷出)之后才能执行 v=1,从而禁止重排序。

知识点总结:

屏障不是针对关键词修饰的变量,是在这个变量之前的所有都要刷新写和读

碎碎念:后续会更新每天学习的八股和算法 题,开始准备秋招的第35天。努力连续更新100天!以后每天就按,秋招项目【java +agent】,科研,必做项目,算法,八股,锻炼身体来总结。

总结:要掌控好自己的时间

1.算法要系统过一遍【晚上】1h

2.秋招项目,【java 】开始实际看业务,1/6;无

【agent 】还在学,整理cc,无,

3.科研要跑一下,

4.检测项目也得总结文档,

6.背八股,【30min】

7.锻炼身体,【30min]

反思:睡到中午,然后下午加班两小时,回来一直玩手机,晚上学了两小时。

相关推荐
yaoxin5211238 小时前
434. Java 日期时间 API - Period 基于日期的时间段
java·开发语言·python
noipp8 小时前
推荐题目:洛谷 P10907 [蓝桥杯 2024 国 B] 蚂蚁开会
c语言·c++·算法·编程·洛谷
kyriewen8 小时前
Git Commit 前自动修复代码风格?配置 Husky + lint-staged,从此 CR 只聊逻辑
前端·git·面试
何极光8 小时前
IDEA集成Maven
java·maven·intellij-idea
程序员二叉9 小时前
【JUC】ThreadLocal底层原理|内存泄漏|弱引用|跨线程传递方案
java·开发语言·面试·职场和发展·juc
程序员二叉9 小时前
【JUC】线程池全套深度详解|参数|流程|拒绝策略|调优|异常处理
java·开发语言·jvm·算法·面试·juc
老马识途2.09 小时前
在AI的帮助下理解spring的启动过程
java·前端·spring
青山木9 小时前
Hot 100 --- 轮转数组
java·数据结构·算法
徐小夕9 小时前
Loop Engineering 深度解析与实战指南(全网最全)
前端·算法·github