202510|技术日志
1009
HashMap null key
HashMap 允许 null key 的存在
因为早期HashTable不支持,HashMap作为HashTable的后续替代方案,在设计结构上引入了对null key的支持
HashMap在计算null key的散列函数时默认会将null key永远设置在0号桶,也正因此HashMap只允许一个null key
虽然map.put(null, null)是一个合法操作,但是不推荐。因为会导致get(null)仍然返回null,而一般情况我们使用 map ,如果返回null就下意识的认为key不存在,在上面描述的这种情况下,key存在,只是key和value都为null;
因此判断key是否存在的最佳实践是:应当使用map.containsKey()
单核CPU支持多线程吗
单核CPU是支持的,在程序执行时底层通过操作系统时间片轮转的方式,把CPU的时间片分给不同的线程执行。
由于现代CPU的处理速度一般都比较快,虽然受限于单个核心,同一个时间只能执行一个线程,但是可以快速在多个线程之间进行切换,给我们一种多个任务在同时执行的”多线程”的感觉
1010
操作系统线程调度方案
线程调度是 操作系统 管理 程序线程执行顺序 以及 系统整体资源 的一个重要机制。
主要可以分为两种调度方案:
- 非抢占式调度:任务必须主动让出CPU执行权,否则会一直占用CPU。也就是线程执行完毕后,主动通知系统切换到另一个线程
- 抢占式调度:操作系统调度器来统一控制CPU的执行权,调度器可以强制中断CPU对于当前任务的处理,将CPU分配给更高优先级的任务
对比
| 抢占式 | 非抢占式(协同式调度) | |
|---|---|---|
| 切换机制 | 调度器强制中断 | 任务主动让出 |
| 资源开销 | 存在线程上下文切换开销 | 不存在线程上下文切换 |
| 死锁饥饿风险 | 低,调度器可介入干预 | 高,可能一直占用CPU资源 |
| 适用场景 | 对响应时间敏感的实时性场景,如现代操作系统;多线程编程场景 | 资源有限、协程或事件驱动框架 |
常见的抢占式调度机制
优先级调度:高优先级线程优先执行,Windows的线程调度使用的就是这个机制
时间片轮转:OS决定每个线程分配到固定时间片轮流执行
短任务优先:短任务可以抢占长任务的CPU执行权
单核多线程最佳实践场景
单核多线程适合IO密集型场景,而多核更适用于CPU密集型计算场景。
因为IO密集型的任务,CPU利用率低,更多时间是在等待IO设备,瓶颈在IO。采用单核多线程可以让单核的这个CPU在多个IO密集的任务之间轮转执行,最大化利用CPU;
而CPU密集型的任务,使用多核CPU运行能最大化利用CPU同时获得最大的执行效率。
1013
并发和并行的区别
并发Concurrency,对应单核CPU,多个任务同一时间只有一个任务在跑,源于操作系统的线程调度策略快速切换制造 “同时” 的错觉
并行Parallelism,对应多核CPU,多个任务实际真的在多个物理处理单元上执行
为什么ConcurrentHashMap不支持null key
关联[HashMap null key注意事项](#HashMap null key)
ConcurrentHashMap不仅不支持null key,甚至还不支持null value
二义性问题
当我们map.get(null)发现等于null的时候,就会出现一个问题:
- 是Map中不包含这个null key
- 还是Map中包含这个key,只是value是null
在HashMap中,设计之初就是为了单线程考虑的,我们可以进行这样的判断
1 | |
但是在ConcurrentHashMap中,由于是多线程环境,并且map.get()以及map.containsKey()这两个操作是两个独立的原子操作,因此中间可能存在 “线程切换导致的状态变化”,导致判断结果不一致。
1 | |
1015
对象逃逸&标量替换
本内容属于Java内存模型优化、JVM内存管理相关知识点
对象逃逸
对象逃逸是指是对象的作用域超出了某个方法/线程
我们分三种情况进行讨论
- 未逃逸
1 | |
- 方法逃逸
对象被作为方法返回值返回,或被传递给其他方法(但未被其他线程访问),导致其作用范围超出当前方法
1 | |
上述两种方式本质上都是方法内创建的user对象作用域超出了本身的方法
- 线程逃逸
在这种情况下,对象一般会被存储到类的静态变量、实例变量中,或被传递给其他线程(如作为线程的 Runnable 参数),导致其他线程可以访问该对象
1 | |
未逃逸对象栈上分配
在编译期间,JVM引入了一个机制:如果某个对象未发生逃逸,则会将这个对象分配到栈上
原因是因为栈相比于堆,更适合存储短期存在的对象,减轻GC压力
| 内存区域 | 特点 | 使用场景 |
|---|---|---|
| 堆 | 线程共享,对象需要通过GC回收 | 长期存在的对象 |
| 栈 | 线程私有,方法内产生创建,方法结束后自动销毁回收 | 短期存在的对象 |
未逃逸的对象生命周期和方法一致,方法调用时创建,方法执行结束后就应当被回收。这种特性刚好匹配栈的 “自动回收” 机制 —— 无需 GC 介入,方法执行完栈帧弹出,对象内存直接释放,效率远高于堆分配。
分配前提:未逃逸+标量替换
当然在栈上的对象并不是完整的对象实例,JVM会通过标量替换优化
1 | |
由于对象未逃逸,只会在方法内部进行使用,因此在JVM的标量替换优化作用下,栈上并不会存储完整的User对象,而是直接在栈上分配 name(引用类型)和 age(int 类型)两个标量,方法结束后随栈帧销毁。
1016
包装类缓存
我们都知道包装类是在基础数据类型的基础上套了一层对象。为了减少对象的创建,JVM引入了对包装类的缓存机制:
在一定范围内,当我们通过自动装箱或使用**valueOf()方法创建包装类对象时,JVM 不会每次都创建新的对象,而是会从一个预先创建好的内部缓存数组(或称为对象池/常量池)**中直接返回已有对象的引用。
| 包装类 | 是否支持缓存 | 默认缓存范围 | 备注 |
|---|---|---|---|
| Byte | ✅ | -128 到 127 |
缓存所有可能的 Byte 值 |
| Short | ✅ | -128 到 127 |
范围较小,仅缓存常用范围 |
| Integer | ✅ | -128 到 127 |
最常用,可通过 JVM 参数 (-XX:AutoBoxCacheMax) 调整上限 |
| Long | ✅ | -128 到 127 |
范围较小,仅缓存常用范围 |
| Character | ✅ | \u0000 到 \u007F |
对应十进制 0 到 127,缓存ASCII字符集 |
| Boolean | ✅ | true 和 false |
缓存所有可能的 Boolean 值(只有两个) |
| Float/Double | ❌ | 无 | 不缓存,因为浮点数的特性(数量过多、精度问题)不适合缓存 |
💡 重点关注:Integer 缓存:默认缓存上限 127 可以通过 JVM 启动参数 -XX:AutoBoxCacheMax=<size> 来调整(但下限 -128 是固定的,不能调整)
知道了这一个,下面我们看经典面试题:
1 | |
值 100 在 Integer 的缓存范围 [-128, 127] 内。a 和 b 都是通过自动装箱(即调用 Integer.valueOf(100))创建。因此,a 和 b 引用的是同一个缓存对象。
而 200 超出了默认的缓存上限,因此 c 和 d 是两个不同的对象。
需要注意,并不是只要在缓存范围内命中就会返回缓存,只有自动装箱或是调用
valueOf()的时候才会采用缓存。如果直接new包装类对象,永远都不会命中缓存。
1 | |
最佳实践
在比较两个包装类对象的时候,始终选择用equals()来进行比较。避免使用 == 运算符,因为它比较的是对象的引用地址,容易因包装类的缓存机制的存在而产生意想不到的结果(如上面的 a == b 和 c == d)。
但是如果比较的是一个基本数据类和包装类的时候,就要用 == 来比较了。因为此时会自动拆箱,通过 == 来进行基础数据类型的比较。
拼接字符串时,加号还是StringBuilder
| 场景 | 推荐方案 | 理由 | 核心面试考点 |
|---|---|---|---|
| 少量(2-3 次) 且在 同一行 拼接 | 使用 + 运算符 |
编译器会自动优化成 StringBuilder(语法糖),代码简洁。 |
编译器的自动优化 |
| 循环内 或 大量(4 次及以上) 拼接 | 使用 StringBuilder |
避免在循环中重复创建大量临时的 String 和 StringBuilder 对象,性能开销小。 |
String 的不可变性、对象创建开销 |
| 多线程环境 下拼接 | 使用 StringBuffer |
StringBuffer 是 线程安全 的,但性能比 StringBuilder 稍差。 |
线程安全性、锁竞争 |
需要注意的是在循环中拼接,一定不能用加号,因为加号会触发编译器的语法糖,编译器会将其转为new StringBuilder().append().toString()假设循环 N 次,就会创建 N 个 StringBuilder 对象和 N 个新的 String 对象,导致大量的对象创建和内存开销,性能是 O(N2) 级别的。
如果有循环拼接的场景,且是单线程,采用 StringBuilder,可变字符串,直接修改 value数组
1021
整理Java内存模型笔记