阅读视图

发现新文章,点击刷新页面。

面试官最爱追问:多线程到底用来干什么?

原文来自于:zha-ge.cn/java/64

面试官最爱追问:多线程到底用来干什么?

最近面试了几家公司,感觉面试官们都像集体开了个「多线程灵魂拷问」专题会一样,上来总是:**「你说说,多线程到底用来干啥?」**兄弟我一听,内心疯狂翻白眼。这个问题,听着简单,但真要答全,可不是几句“提高效率”这么潦草。今天想着写一篇小故事,把我和多线程“纠缠半生”的心酸史讲讲,给看官们乐一乐,也许还真能派上用场。


多线程——不是拿来装高大上的

先说实话,刚入行那会儿,我对多线程的理解也就「能同时干好几件事」这么朴素。后来踩过的坑多了,才明白这锅饭根本没那么容易吃。

  • 以为多线程就等于速度快?其实火候不对反而慢成🐢。
  • 觉得加了线程就能让服务器起飞?很可能一不小心线程打起来服务器趴下。

说白了,多线程是把双刃剑。哪能乱舞呢?得看场景。


那,多线程究竟该用在哪里?

说正事。大厂面试官其实很关心你遇到实际业务场景能不能灵光一现——

常见的合理用法

  • 处理高并发请求:比如秒杀、抢票。没法让大家顺序排队慢慢买吧。
  • IO密集型任务异步处理:比如给用户发邮件啥的,当然等着有点傻。
  • 定时任务&批量任务并发执行:1000个订单要算积分,同步来明天都下班了……

我有次给电商项目做促销,后台要生成一堆优惠券,顺手扔了个线程池。代码看似简单:

ExecutorService pool = Executors.newFixedThreadPool(10);
for (Coupon coupon : coupons) {
    pool.execute(() -> couponService.generate(coupon));
}
// ...

**说实话,还真快多了。**但谁能想到,第二天运维写信说数据库被打爆,查来查去——都是我这波线程猛男一夜之间把DB压力表干红。


踩坑瞬间

多线程和我最经典的“翻车现场”有如下代表作:

  • 忘了同步:多个用户一起抢红包,结果金额成了负数,老板差点报警……
  • 线程数无限增长:Executor写得太潇洒,居然有同事直接用Executors.newCachedThreadPool(),后来JVM直接OOM。
  • 脏读/死锁:有个方法里 synchronized 写得不明不白,让两个线程互相“拉锯战”,程序不报错,就是不动了。

还有一次,想着把计算任务全塞进多线程。写着写着,心里还在偷笑:“我这不是要秒天秒地吗?”

synchronized (sharedObj) {
    // 关键处理
    doImportantStuff();
}
// ...

结果同事打电话过来说:“你这多线程加个 synchronized 是在表演单线程吗?”我:……突然有点尴尬。


经验启示

多年摸爬滚打,关于「多线程到底用在啥时候」我总结了几点“人话版”口诀:

  • 别搞事的小场面别瞎开线程。单线程能搞定的事,就别装13。
  • 慢的地方优先异步化。比如文件上传、远程接口,IO这种天生慢性子最适合多线程。
  • 共享资源先想清楚会不会出事。哪怕有锁,也不一定万无一失。
  • 线程不是免费午餐。线程开多了服务器是要“交保护费”的,CPU、内存能吃多少心里要有数。
  • 优先用线程池,别直接一顿 new Thread(),后劲你受不了。

最后,面试时如果真被问,甭背八股,结合实际聊聊就好。老板要听你会用,而不是死记原理。

Java 线程池中的 submit 和 execute 有何不同

原文来自于:zha-ge.cn/java/60

Java 线程池中的 submit 和 execute 有何不同

在 Java 线程池中,ExecutorService 提供了两种常用的任务提交方法:submitexecute。虽然它们都可以将任务提交到线程池中执行,但它们在功能和使用场景上存在显著差异。本文将详细探讨这两种方法的区别,帮助开发者更好地选择适合的工具。


submitexecute 的核心区别

1. 返回值

  • execute
    execute 方法用于提交没有返回值的任务。它接受一个 Runnable 类型的参数,并且没有返回值。使用 execute 提交的任务无法获取执行结果,适合那些只需要执行某种操作而不需要关心结果的场景。

    executorService.execute(() -> {
        // 执行某种操作,不返回结果
    });
    
  • submit
    submit 方法则更为灵活,它可以提交两种类型的任务:

    • Runnable:类似于 execute,但返回一个 Future 对象。
    • Callable:可以返回一个结果,执行完成后可以通过 Future 获取结果。
    Future<Integer> future = executorService.submit(() -> {
        // 执行某种操作,返回结果
        return 42;
    });
    
    // 获取执行结果
    Integer result = future.get();
    

2. 异常处理

  • execute
    当使用 execute 提交的任务发生异常时,异常会直接抛出到调用线程,或者由线程池的默认异常处理机制处理(通常会打印到控制台)。

  • submit
    使用 submit 提交的任务如果发生异常,异常会被捕获并封装到 Future 对象中。只有在调用 future.get() 时,异常才会被抛出。

    Future<Integer> future = executorService.submit(() -> {
        throw new RuntimeException("任务执行失败");
    });
    
    try {
        Integer result = future.get();
    } catch (InterruptedException | ExecutionException e) {
        // 处理异常
    }
    

3. 适用场景

  • 使用 execute 的场景

    • 任务不需要返回结果。
    • 不需要捕获任务执行过程中的异常。
    • 场景示例:日志记录、文件写入等操作。
  • 使用 submit 的场景

    • 任务需要返回结果。
    • 需要捕获和处理任务执行中的异常。
    • 场景示例:计算任务、数据库查询等需要结果反馈的操作。

常见误区与注意事项

  1. 不要混淆 submit(Runnable)execute(Runnable)
    虽然 submit(Runnable)execute(Runnable) 都可以提交无返回值的任务,但 submit 会返回一个 Future 对象。如果不需要结果,使用 execute 更为合适。

  2. submit 提交的 Callable 任务必须处理异常
    如果 Callable 任务中抛出异常,必须在调用 future.get() 时捕获并处理,否则会导致程序崩溃。

  3. 线程池关闭时的注意事项
    如果使用 submit 提交了多个任务,确保在关闭线程池之前所有 Future 对象都已正确处理。


总结

  • execute:适用于不需要结果的简单任务,使用方便但功能有限。
  • submit:适用于需要结果或需要处理异常的任务,功能更强大但需要额外处理 Future 对象。

在实际开发中,根据具体需求选择合适的工具,可以有效提升代码的可维护性和健壮性。

shutdown 和 shutdownNow 有啥不一样?一文看懂 Java 线程池关闭方式

原文来自于:zha-ge.cn/java/59

shutdown 和 shutdownNow 有啥不一样?一文看懂 Java 线程池关闭方式

咱们今天聊个特别接地气但偏偏容易迷糊的小选手:Java 线程池的 shutdown 和 shutdownNow。说实话,这俩名字一看就像是亲兄弟,放一个班级里绝对同桌,搞不好连饭都抢对方的。但等真用起来,它们的“性格”完全不一样——突然暴躁、温柔劝退,简直戏精。下面就让我给大家唠唠我亲身见识到的这些“程池兄弟”的故事。

线程池要收工,总不能一拍脑袋就走吧

有一天,产品经理说:有些活儿做完就别干了,省点儿资源,回家睡觉吧。我拍拍胸脯,“线程池有 shutdown,交给我!”当时以为,这不就一个方法名吗?关门熄灯,就结束呗。

然后我顺手加了一句:

executorService.shutdown();
// 后面以为线程池就干干净净歇菜

页面一刷新,诶,怎么新来的任务还会被拒绝?可惜没多想,觉得挺合理的。本以为生活继续,突然有小伙伴说:你要是急着下班,shutdownNow 你试过没,会有啥新花样?

踩坑瞬间

我一拍脑门,试试呗,反正羊毛出在羊身上,bug谁没写过!于是我加上这行让人心跳加速的代码:

List<Runnable> notStarted = executorService.shutdownNow();
// 打印一下看看没被执行的任务都有哪些
System.out.println("没来得及干的任务: " + notStarted);

初衷就一个:马上收工!全部停摆!结果一跑,发现事情不太对——不是所有正在干活的线程都立刻停住了。还有,某些任务卡着卡着突然 InterruptedException 飞出来,把业务流程搅和得一团糟。

最绝的是,队列里没执行的 Runnable 干脆给我吐出来了,一堆“遗留作业”等我补锅。刚加完 shutdownNow,就炸出一屋子 Exception,自测直接红。

于是我回去看源码和官方注释,这才明白:

  • shutdown():温柔派,只是告诉线程池,“兄弟们,不接新活了,干完手头的项目都回家。”
  • shutdownNow():大喇叭,直接喊停!正在干的线程发个 interrupt(但不保证真的秒停),还把队列里没开始的活扔回给你认领。

用 shutdown 还是 shutdownNow,一不留神全家桶

基于我那次“有惊无险”的实际体验,各位兄弟姐妹谨记:

方法 行为简述 线程状态 队列处理
shutdown 拒绝新任务,等老任务做完 继续执行 手尾能收干净
shutdownNow 试图强停、interrupt 在跑的 不一定立刻终止 返回所有未开始任务

就像点外卖选“温和收餐”还是“暴力催单”,各有风险。你要是任务非原子、容易响应中断还好,要是不理 interrupt,shutdownNow 就能让你见识“死机全家桶”。

有一次我调度一个处理大文件的线程,用 shutdownNow,以为它能直接砍掉。结果对方压根不接中断信号,文件没处理完线程根本死不了。经理骂娘,说程序员光会拍脑袋写代码。

经验启示

这玩意儿的教训,我给大家总结几点,免得大家踩我的坑:

  • 优先用 shutdown,让线程池优雅退休,不要一上来就暴力赶人。
  • shutdownNow 真的只适合应急,比如线上事故、关机那种生死存亡时刻。
  • 队列里未执行的任务,用 shutdownNow 可以收集处理,但记得别随便丢,还是得业务善后。
  • 如果你的线程里逻辑根本不 care interrupt,那 shutdownNow 的“猛男操作”形同虚设。
  • 想要线程真的停,实际得让任务响应 Thread.interrupt(),不然它还是坚挺到底。

细心如我,最后贴段伪代码冷静分析:

for (Runnable task : notStarted) {
    // 做善后处理,比如重试、丢弃或者报警提醒
    handleLeftTask(task);
}

生活已经很难了,线程池能不暴力就不暴力,“慢慢收场”永远都比“突然杀青”大家好过。今天就先聊到这儿,各位别深夜 shutdownNow,梦里都得被线程 interrupt 吓醒!

Java ThreadPoolExecutor 动态调整核心线程数:方法与注意事项

原文来自于:zha-ge.cn/java/58

Java ThreadPoolExecutor 动态调整核心线程数:方法与注意事项

不得不承认,这两年我的头发越来越少了,罪魁祸首之一就是给服务器的线程池随意添加特性。比如今天要聊的主角——让 ThreadPoolExecutor 的核心线程数动态调整。刚上手时信心满满,结果在一些细节上翻了几个跟头。接下来,用闲聊的方式,复盘这段“线程池养成记”。


动态调整线程池的初衷

场景很简单,公司后台每天会经历任务激增和低谷期。起初设定了50个核心线程:

ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 200,
                                                    60L, TimeUnit.SECONDS,
                                                    new LinkedBlockingQueue<>());

初期运行良好,但某天监控告警显示CPU波动异常。明明核心线程充足,为何资源利用率不稳?这让我开始怀疑自己的配置是否合理。


动态调整的核心方法

经过一番研究,发现 setCorePoolSize 方法可以动态调整核心线程数。但实际使用中,需要注意以下几点:

  • 正在执行的任务不受影响:核心线程数减少不会终止正在执行的任务。
  • 核心线程超时设置:若 allowCoreThreadTimeOut(false),核心线程即使空闲也不会退出。
  • 线程创建的延迟性:增加核心线程数不会立即生效,需等待新任务到来。
  • 队列的影响:队列任务过多时,调整核心线程数的效果有限。
  • 任务与线程数的平衡:核心线程数不能低于队列中的任务数,否则线程池会尽力维持核心线程数量。

实践中的解决方案

总结出以下调整思路:

  1. 开启核心线程超时executor.allowCoreThreadTimeOut(true);
  2. 逐步调整核心线程数executor.setCorePoolSize(20);
  3. 监控和观察:定期检查队列和线程池状态,避免盲目调整。

配合这些设置,线程池能够更灵活地释放资源,避免过多空闲线程占用资源。


经验总结

以下是几点关键经验:

坑/建议 核心要点
setCorePoolSize 仅修改数值,不会主动终止线程
allowCoreThreadTimeOut 必须开启才能让核心线程自动退出
队列的影响 队列任务过多时,动态调整效果有限
新增核心线程 只有新任务到来时才会创建新线程
平滑调整,避免激进 最好配合监控,逐步调整,避免对系统造成过大波动

结语

动态调整线程池确实很实用,但需要细心维护。别指望Java能自动适应所有需求,尤其是在线上压力不同的情况下。改天再遇上“线程池发疯”,也能顺嘴把这些故事拿出来聊聊,顺便放松下发际线压力。欢迎大家留言交流!

—— 继续搬砖,溜了溜了 🏃♂️

HashMap 扩容为啥总是 2 的倍数?一场来自底层的“强迫症”探险

原文来自于:zha-ge.cn/java/55

HashMap 扩容为啥总是 2 的倍数?一场来自底层的“强迫症”探险

你有没有发现,Java 的 HashMap 用起来挺香,但扩容这事儿真的很神秘。不是 10、不是 16,就是 32、64,总之永远是 2 的倍数,感觉像 HashMap 内部养了个强迫症的架构师。前阵子项目有个同事问我“为啥不搞个19试试”,我直接笑出声:你咋不上天呢?

——话说回来,这种底层“奇怪坚持”,肯定是有故事的嘛,所以忍不住自己扒拉了一番,收获了一箩筐“啊哈”时刻。来吧,让我用程序员唠嗑语气跟你扯扯这个 2 的倍数扩容背后的小九九!


位运算的浪漫

起初我单纯以为:“HashMap 老老实实用 2 的倍数,不就是方便大家算一算容量么。”但后来翻了一圈源码,发现人家玩的可不是加减法,而是位运算

看下面这点彩蛋——(别担心,代码不长)

int index = (n - 1) & hash;

瞅见没?这玩意就是用来定位元素放哪的。 如果 n 是 16(2 的 4 次方),n - 1 就是 15,二进制全是 1, 和 hash 做与运算,不就是变相 hash % n 嘛?关键是—— 这玩意比真正的 % 运算快很多倍,省事又高效, 不用 hashCode 取余,不用管 n 是不是质数,直接掐指算!


诡异的坑爹时刻

那天刚搞懂为啥用 2 的倍数,忍不住想皮一下—— “要不试试把容量设成 9,看看会咋样?”

然后我就掉坑了。 早期 JDK 版本里,你自作聪明塞个 9、15 进去,不会善罢甘休: 首先,元素分布就会特别丑——好几个 hashCode 虽然不一样, 结果 &(按位与)出来还是同一个桶, 明明人多桶多,却扎堆到一块儿。 极端点,所有元素全挤一个链表里…… WTF,红黑树都救不了你!

再扒拉一遍扩容源码,好家伙,tableSizeFor(capacity) 直接帮你凑最近的 2 的幂。 你想 9?对不起,给你 16。你想 33?那就 64。老板不让穷,也不让抠!


踩坑瞬间

敲重点!真·苦主现场——

  • 指定了一个“看起来挺合理”的初始容量,结果后台日志一看,嘿,为啥变成 16 了?
  • 算 hash 桶的时候,直接 %,性能不如人家 &,慢慢悠悠卡你没商量。
  • 想用特殊大小调优,发现怎么设都不生效,HashMap 自带“自动纠正机制”,就是跟你对着干那种……

经验启示

扒拉了这么一遭,得出的教训如下一箩筐:

  • 不要跟源码较劲,HashMap 就爱 2 的幂,任谁来也改不了。
  • 初始化容量随便给,反正 HashMap 会“圆滑”地给你凑个 2 的倍数。
  • 位运算用得妙,比 % 秒杀好几条街:HashMap 的快感,从底层“算桶”开始。
  • 工作里调优容量时,只要量级对了,不用纠结非要贴着需求给数字——源码帮你“切整”。

——写这篇的时候才体会到,所谓“底层优化”,有时真就像设计师的强迫症,但细品还真香!


唠叨到这里,是不是解开你心头“2 的倍数强迫症”的谜团了?下次有人再问,你就劝他放下执念、顺应天命。代码世界嘛,有些“规定动作”,还是挺有它的艺术的,你说呢?

好了,我要去泡杯咖啡,发会儿呆,下次见记得点我名!

Java HashMap 扩容机制详解:触发条件与实现原理

原文来自于:zha-ge.cn/java/54

Java HashMap 扩容机制详解:触发条件与实现原理

有时候,写 Java 写着写着,HashMap 就开始耍小脾气。要么莫名其妙慢下来,要么空间蹭蹭见底,最后一看,原来是扩容背锅。今天就来聊聊我和 HashMap 扩容那些见不得人的内幕,顺便带一点代码小料。


那次 HashMap 把我搞懵了

先交代下场景:那会儿我在做个高并发日志聚合服务,业务量增长飞快。内存明明还挺大,CPU 占用突然就暴涨。下意识觉得啥锁卡死了?JDWP 监控半天,发现某段业务竟然全卡在 HashMap 里!我的第一反应:不会是扩容在搞事吧?

于是就打开源码,边啃边吐槽。


扩容的小九九

HashMap 的扩容,其实是“一拍脑门”规律。

  • 容量(capacity):桶的个数
  • 负载因子(loadFactor):默认 0.75
  • 扩容阈值(threshold):capacity × loadFactor

只要 size 超过 threshold,HashMap 就要大搬家!每次容量翻倍,然后 Rehash,老键全都要洗个三温暖。堪比程序员大重构。

小代码一把梭

来抄下源码核心片段,约 10 行出头:

if (size >= threshold) {
    // 升级容量
    resize();
}

void resize() {
    // 容量变两倍
    Node<K,V>[] newTab = new Node[oldCap << 1];
    // 重哈希老数据
    for (Node<K,V> e : oldTab) {
        // ...把节点丢进新桶
    }
}

有没有发现?扩容一到位,CPU 很快就不淡定。插入不是“补一块砖”,而是“全楼拆迁”。


踩坑瞬间

有些朋友老喜欢这么用:

Map<String, String> map = new HashMap<>();
for (...) {
    map.put(k, v);  // 插满自动扩容!
}
  • 没指定初始容量,默认开 16 个桶。
  • 业务数据一多,每塞满 12 个元素就 realloc 一次。
  • 瞬间三连扩容 + Rehash,内存抖三抖。

最骚的是,如果你用大小刚好 2 的幂,感觉没问题。可有业务激增,过 thousand 条,扩容抖得比我写的周报还频繁!那 CPU 峰值,真让人头皮发麻。


手动避坑指南

后来我乖多了。新建 HashMap,直接:

int expectSize = 5000;
int initCapacity = (int) (expectSize / 0.75f) + 1; // 6670
Map<String, String> map = new HashMap<>(initCapacity);

技巧 combo:

  • 预估最大元素,按 0.75 加一刀。
  • 不要等着扩容惩罚 CPU。
  • 生产居然稳住了,再也没见到爆表。

经验启示

  • HashMap 扩容很 “懒”,等到阈值才挪窝,一挪就全家搬家超耗能。
  • 预估好初始容量,能省老鼻子资源。要伸手就伸大一点!
  • 涉及大批量、高并发插入,一定要提前分配,别让扩容成“定时炸弹”。
  • 插入超多数据时,HashMap 真心不快,考虑别的 Map 实现(比如 ConcurrentHashMap)。

哦对,这次学乖了。以后看到 HashMap,先估一把容量,不做小气鬼,代码和 CPU 都能松口气。反正内存又不是自己掏钱买的(老板:你说啥?)。 OK,今天 HashMap 的“扩容风波”就聊到这,溜了溜了——下次再踩坑记得拉我一把。

其实我不是很想和 Hashtable 说再见:一次跟“古董” HashMap 探险的的碎碎念

原文来自于:zha-ge.cn/java/52

其实我不是很想和 Hashtable 说再见:一次跟“古董” HashMap 探险的的碎碎念

说起来,这周末本来打算加班摸鱼,愣是在代码里遇见了 HashMap 和 Hashtable 这俩“老熟人”。原本想着这点操作小菜一碟,谁知又不小心掉进了一个老旧的坑里。给各位看官唠唠,这场“熟悉又陌生”的 Hash 家族档案案。


大家都说 HashMap 和 Hashtable 像是一对亲兄弟:名字只差了个 “ble”,平时见面也很随意。可真用起来,怎么越来越感觉 Hashtable 像年迈长辈,而 HashMap 则属后浪青年?这俩的代沟,简直开挂。

先列个大表,方便谁以后跟我一样记性差还能偷懒地查查:

特性 HashMap Hashtable
线程安全
键/值允许 null 允许 都不允许
性能 更快 有点慢(加锁嘛)
时代感 年轻气盛 远古老古董

奇葩点滴回忆杀

有次同事用 Hashtable 存数据(真的!还有这种人!),我追问一句:“为啥用 Hashtable 啊老哥?”他说:“听说线程安全!”
我:OK fine,心想本宝宝还不是第一次踩雷……

代码随手敲一段,就这样:

Map<String, String> table = new Hashtable<>();
table.put(null, "哈?"); // 啪——NullPointerException

啊这,连 null 都不让我存? HashMap 倒是没人管你:

Map<String, String> map = new HashMap<>();
map.put(null, "哈喽世界"); // 完美运行

Hashtable:不让存 null,就是这么自信。HashMap:来吧宝贝,想存啥都行~


踩坑瞬间

说点糗事。
刚入行那会儿,项目里有个配置工具,搜一眼是 Hashtable,心想行呗,老规矩。我不假思索往里 put 空 key,直接炸了:

Exception in thread "main" java.lang.NullPointerException

当场社死。后来问师傅,人家只轻描淡写一句:“谁让你用 Hashtable 了?现在都不用它了。” 哦豁,这才发现原来新项目都是 HashMap+ConcurrentHashMap,Hashtable 是“考古现场”象征……

还有个更坑爹:
你敢在多线程里用 HashMap 存取不加锁?
那就等着看“莫名其妙”丢数据,甚至死循环,简直玄学;Hashtable 则自带 synchronized,妥妥当当,但慢到让人抓狂。


经验启示

说真格的,两招送给后来的小伙伴:

  • 拒绝 Hashtable,优雅用 ConcurrentHashMap 或 Collections.synchronizedMap。
    线程安全换新姿势,不走 Hashtable 老路。

  • 需要允许 null 时,不要想太多,选 HashMap,别犹豫。
    好吗?Hashtable 会气死你的。

  • 艰难维护老旧代码时,下手之前看清楚 Map 的实现,别掉进“NullPointerException”的陷阱。


唠唠结尾

其实,大部分 Javaer 都已经和 Hashtable “说再见”了。做为一个“考古爱好者”以及“踩坑能手”,再次总结:

  • HashMap 适合大部分新项目,灵活自由,配合 ConcurrentHashMap 足够应付并发。
  • Hashtable,留着给老代码吧,也许还要等着被彻底淘汰。
  • 最后:有事没事,看一眼表格,少踩一次雷,工作效率upup!

没啥技术含量,全是实战血泪史。有缘人来看了一乐,没准就能少受一次气,多摸一天鱼。码完,收尾,下班溜了~

聊聊我和 ArrayList、LinkedList、Vector 的“一地鸡毛”

原文来自于: https://zha-ge.cn/java/45

聊聊我和 ArrayList、LinkedList、Vector 的“一地鸡毛”

Java 的这仨 List,你可能觉得早都烂熟于心了——但真用到项目里,坑声四起。下面就随我一起,八一八我和这三位“表哥”之间爱恨纠缠的小故事。放心,咱不是刷题,是掏心窝子的聊天~


回忆杀:初识“三兄弟”

第一次学 Java 集合,老师一边敲黑板一边念叨:

  • ArrayList:用得最多,底层数组,查询快,增删慢;
  • LinkedList:链表实现,插入删除爽到飞起,查找就很丧气。
  • Vector:线程安全版 ArrayList,老古董,慎用。

我点点头,心里想:“谁还不能背两句口诀?”

可现实总是啪啪打脸。


真香 or 真坑?项目里的瞬间闪回

做项目那会儿,初生牛犊,脑子里只有 ArrayList。见啥 list 就 new ArrayList,根本不想别的。

某天搞批量插入,发现性能死慢死慢。代码关键部分是这样的:

for (int i = 0; i < data.length; i++) {
    myList.add(0, data[i]);
    // ..........
}

老天爷,头一次感受到什么叫 “查询快,插入慢”。每加一条,后面的元素要挪窝,难怪 CPU 风扇吹得欢实。

切换成 LinkedList,一下子丝滑,仿佛给代码上了润滑油。


踩坑瞬间

踩坑合集来啦,不藏私!

  • 插队惹祸:批量在头部插入数据,ArrayList 性能炸裂,LinkedList 不疼不痒。
  • Vector 的冷宫:被同事一通吐槽,才意识到 Vector 性能差、不推荐,基本没人再用。同步你以为很香?不如用 Collections.synchronizedList 吧。
  • 遍历翻车:用 LinkedList 频繁下标访问,发现慢到怀疑人生。链表没下标的命,硬要它干数组的活儿,吃力还不讨好。

小结——这仨到底咋选?

说真的,写了这么久,踩过各种坑,才慢慢摸出这点门道:

  • ArrayList:日常首选,查询多、插入删除少,用它没毛病。
  • LinkedList:头尾插入、删除频繁的小场合,想要刀头舔血的快感?它最适合。
  • Vector:单线程时代的遗物,除非你在整理遗产,否则请离它远点。
  • 别忘了:线程安全?要么自己加锁,要么 Collections.synchronizedList。

经验启示

  • 千万别机械背“口诀”,要真明白底层实现。
  • 选对工具,就像提对武器,写代码才能游刃有余。
  • 踩过的坑,都变成了“前车之鉴”。你问我怎么理解 List?多敲代码,多出 bug,没有一条是白费的。

晚上写到这里肚子又饿了,得,收个尾巴。

希望你别和我一样,因为 List 选型把自己折腾进 ICU。祝大家永远追风少年,头发茂密,bug 少一点,踩坑也别太疼!

使用 HashMap 提高性能的小技巧

原文来自于:zha-ge.cn/java/44

使用 HashMap 提高性能的小技巧

最近在重构一个老项目,每次看到满屏的HashMap,我总会想起曾经遇到的那些坑。虽然HashMap用起来简单,但真要优化性能,还真有几个小技巧值得分享。

初识 HashMap 的性能问题

刚开始使用HashMap时,我们通常会直接使用默认构造器:

Map<String, Object> map = new HashMap<>();

这种方式虽然简单,但在处理大量数据时可能会遇到性能瓶颈。比如,频繁的resize操作会导致垃圾回收压力增大,进而影响应用性能。

优化 HashMap 的初步尝试

经过一些研究,我发现指定初始容量可以有效减少resize的次数:

Map<String, Object> bigMap = new HashMap<>(2048);

通过预估数据量并设置合理的初始容量,可以显著减少HashMap的扩容次数,从而提升性能。

常见性能问题及解决方案

在使用HashMap时,我们可能会遇到以下问题:

  • 问题1: 初始容量设置不合理,导致频繁扩容。
  • 问题2: 存入大量null键或值,引发NullPointerException
  • 问题3: 错误地认为HashMap是线程安全的,导致多线程环境下数据不一致。
  • 问题4: 自定义对象作为键时,hashCode()equals()方法实现不当,导致哈希冲突。

例如,如果一次性向HashMap中插入大量数据,而没有指定初始容量,可能会导致频繁的resize操作,从而影响性能:

// 不良示例:未指定初始容量,导致频繁resize
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < 100000; i++) {
    map.put("key" + i, "value" + i);
}

提升 HashMap 性能的实用技巧

为了更好地利用HashMap,我们可以参考以下建议:

  • 1. 合理设置初始容量
    根据预计的数据量,合理设置HashMap的初始容量。如果不确定数据量,可以通过以下公式估算:

    new HashMap<>((int)(targetSize / 0.75f) + 1);
    

    其中,0.75是默认的负载因子。

  • 2. 避免使用null键或值
    HashMap允许使用null键或值,但在实际使用中应尽量避免,以防止NullPointerException的发生。

  • 3. 使用不可变对象作为键
    键对象应尽量使用不可变类(如String),并确保hashCode()equals()方法实现正确。

  • 4. 多线程环境下使用ConcurrentHashMap
    如果需要在多线程环境下使用HashMap,建议使用ConcurrentHashMap以保证线程安全。

  • 5. 避免频繁调用remove方法
    remove方法可能会导致HashMap的容量调整,影响性能。如果需要频繁删除元素,可以考虑其他数据结构。

代码示例:合理设置初始容量

以下是一个合理设置HashMap初始容量的示例:

int estimatedSize = 10000;
// 根据负载因子0.75计算初始容量
Map<String, Object> optimizedMap = new HashMap<>((int)(estimatedSize / 0.75f) + 1);

总结

通过合理设置初始容量、避免使用null键或值、使用不可变对象作为键等方法,我们可以有效提升HashMap的性能。记住,HashMap并非万能,选择合适的数据结构才能事半功倍。

最后提醒: 在实际开发中,一定要根据具体场景选择合适的数据结构,并通过性能测试验证优化效果。

Java Set 不会重复?原来它有“记仇”的本事!

原文来自于:zha-ge.cn/java/43

Java Set 不会重复?原来它有“记仇”的本事!

前几天和同事聊天,忽然有人问: “为啥 Java 里的 Set,往里加东西,就是死活不让你重复?它心里到底咋想的?” 我一听,哈,这话题熟,讲起来比吐槽同事还顺!

Set 里的“记仇大法”

你想啊,Set,中文翻译就是“集合”,但它跟“名单”可不一样。名单——随便写谁,重复也没人管。可 Java 的 Set,这可是一份“只能出现一次”的黑名单。你要是往里面塞重复的,Set 就跟个记仇精似的,死活认准你已经来过了。

为啥?其实本质上,Set 不是靠人品,靠的是底层的 hashCodeequals

  • 每次你往 Set 里丢个对象,它悄悄问自己俩问题:
    1. 这东西的 hashCode 跟别人一样吗?
    2. 如果 hashCode 一样,我还能用 equals 抖搂出真实身份吗?

只要发现“老熟人”,它就直接甩锅说,喂,别进来了,等下一个吧。

代码片段来一眼

说到这儿,上点干货,给你看关键步骤:

if (!set.contains(obj)) {
    set.add(obj);
}

嗯,JDK 底层其实比这个还复杂,但道理就是这么个意思。每加一个,都要“打卡登记”——否则,不让进。

踩坑瞬间

这东西好用归好用,我当年刚入职那会儿,却栽了个大跟头。

当时写了个 User 类,里面有 name 和 age,往 HashSet 里丢了一堆“看起来一模一样”的 user:

User user1 = new User("小明", 11);
User user2 = new User("小明", 11);
set.add(user1);
set.add(user2);

我以为二选一,Set 肯定只留一个。结果一打印,两个都在!啊??怎么回事,这不是背叛了“不能重复”的信仰吗!

真相永远藏在 equals/hashCode

后来查了下,这才明白—— 原来自己写的 User 根本没覆写 equals 和 hashCode 啊!人家 Set 判断重复靠的就是这俩,你不写,它就只会比较引用(地址),user1 != user2,当然不当回事。

于是我灰溜溜补上:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return age == user.age && Objects.equals(name, user.name);
}
// hashCode 也得覆盖!

这下,加多少“同款用户”,Set 都只留一个了!

经验启示

别看 Set 冷酷,无情,其实它就是死认“你有没有本事自证身份”。用 Set 装自定义对象,千!万!记!得!

  • equals/hashCode 必须重写
  • List/数组要“包容”,Set 要“守门”
  • 如果用的是 HashSet,hashCode 绝对是主要判断,TreeSet 则是 compareTo

我们程序员写类,有点像给对象办身份证。身份证号一换(equals/hashCode),Set 才认你。 要不然你每次都“离家出走换新衣服”,它根本不认识你是谁!

以后见了 Set,可别只会往里塞,要让“黑名单”起作用,你得懂规则,否则老是掉进这个“重复元素不生效”的坑。

有啥用呢?去重嘛,统计嘛,社交App判好友……反正场景一大片!

行吧,今天就聊到这儿,这 Set 的“记仇大法”你可别再忘咯。哪天朋友问起,你也能一顿唠嗑吹回去嘿。

—— 代码就像生活,想不重复,可不容易 😊

Java 集合框架详解:常见集合类及分类方式

原文来自于:zha-ge.cn/java/40

Java 集合框架详解:常见集合类及分类方式

还记得我刚入职场那会儿,面对业务数据的“动态存取、批量处理”需求,以及“插入、查找两手抓”的要求时,我对集合的理解还停留在List、Map、Set傻傻分不清的状态。今天,让我们一起深入探讨Java集合框架的门道。


集合框架的分类概述

Java集合框架可以分为以下几个主要类别:

  • List接口:有序且允许重复元素。常用的实现类有ArrayListLinkedList,适用于顺序存储和批量处理。
  • Set接口:无序且不允许重复元素。常用的实现类有HashSetTreeSet,适用于需要唯一元素的场景。
  • Map接口:存储键值对,键唯一,值可以重复。常用的实现类有HashMapTreeMapLinkedHashMap,适用于快速查找和映射关系。

以下是各集合类的对比表格:

接口类型 实现类 是否有序 是否允许重复元素 主要用途
List ArrayList 支持 顺序存储、批量处理
Set HashSet 不支持 唯一元素集合
Map HashMap 键不重复 键-值映射查询

常见问题与解决方案

在实际开发中,使用集合类时可能会遇到一些常见问题:

  • 自定义对象在Set中无法去重:这是因为没有重写equals()hashCode()方法。确保在将自定义对象存储到Set或Map中时,正确实现这两个方法。
  • ArrayList在多线程环境下不安全:在多线程场景中,建议使用线程安全的集合类,如Collections.synchronizedList包装的ArrayList,或者直接使用CopyOnWriteArrayList
  • HashMap在插入顺序上不可靠:如果需要保持插入顺序,可以考虑使用LinkedHashMapTreeMap

以下是关键代码示例:

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    // 根据实际业务字段进行比较
    return Objects.equals(field1, ((YourClass) obj).field1) && 
           Objects.equals(field2, ((YourClass) obj).field2);
}

@Override
public int hashCode() {
    return Objects.hash(field1, field2);
}

使用建议

为了更好地利用集合框架,建议遵循以下原则:

  • 选择合适的集合类型
    • 需要有序且允许重复元素时,使用List。
    • 需要唯一元素时,使用Set。
    • 需要键值对映射时,使用Map。
  • 自定义对象的处理
    • 在将自定义对象存储到Set或Map中之前,必须重写equals()hashCode()方法。
  • 线程安全
    • 在单线程环境下,优先使用性能较好的ArrayList、HashSet和HashMap。
    • 在多线程环境下,使用线程安全的集合类,如Collections.synchronizedListCopyOnWriteArrayList等。
  • 性能考量
    • 对于频繁查找的场景,优先使用HashMap。
    • 如果需要保持插入顺序,可以使用LinkedHashMap。
    • 如果需要有序的集合,可以使用TreeSet或TreeMap,但需注意它们的性能开销。
  • 依赖倒置原则
    • 声明集合变量时,使用接口类型(如List、Set、Map),而不是具体的实现类。这样可以提高代码的灵活性和可维护性。

总结

Java集合框架是一个功能强大且灵活的工具,正确理解和使用它可以显著提高开发效率和代码质量。通过选择合适的集合类型、正确处理自定义对象、关注线程安全以及合理考虑性能因素,我们可以充分发挥集合框架的优势,写出高效、可靠且易于维护的代码。

希望这篇文章能帮助你更好地理解和使用Java集合框架。下一次,我们可以深入探讨集合框架的源码实现,揭开更多“黑科技”的面纱!

你遇到过 ConcurrentModificationException 吗?其实很常见

原文来自于:zha-ge.cn/java/39

你遇到过 ConcurrentModificationException 吗?其实很常见

说起来,咱们搞 Java 的,谁还没“交过学费”给 ConcurrentModificationException 呢?这货最喜欢在你信心满满地遍历集合时,冷不丁一闷棍,打得你怀疑人生。

其实刚做开发那几年,我还以为这是老鸟们才会遇到的“高端”问题。后来才发现:哦豁,这就是个大路货,谁都有机会摔一跤。今天就来聊聊我和它不得不说的故事。


一开始,是在做个清理 List 的小功能。需求特别朴实:遍历集合,发现某些元素(比如 null)就顺手给干掉。代码写得贼溜溜,看着就像这样:

for (String item : list) {
    if (item == null) {
        list.remove(item);
    }
}

自测了一下,前几个小时一切安好。我心想:这不就是“边走边拔草”嘛,谁不会?直到有一天,线上日志刷刷掉了几页——java.util.ConcurrentModificationException。别说用户,连我都一脸问号。


踩坑瞬间

  • 异常到底为啥?猜半天。
  • 改用 for 循环,心想“笨办法最靠谱”,结果一顿猛敲还是爆。
  • 看源码,才知道所谓“fail-fast”机制。原来人家老早就记着你动过“歪心思”,立马甩你个异常回去。

要多糗有多糗:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (……) {
        list.remove(……); // 这里炸了
    }
}

没错,iterator 没说让你直接改列表。偷偷加菜的下场,就是被老板放进冷宫。


江湖救急大法

痛定思痛。我兜里仅剩几颗糖,决定再啃啃文档。最终,我找到了解药——用 iterator.remove(),就是点名让你“合法拔草”。改完之后,再也没闹过脾气:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if (item == null) {
        it.remove();
    }
}

就这么加了一行,看似没啥神奇,实则暗藏杀机(不是,是埋藏了官方的保护线)。 当然,你要真想在并发下动集合,那就别说 ArrayList,得用 CopyOnWriteArrayList 之类才安全。不过那都是后话了。


经验启示

踩完坑,才知道自己之前有多天真。给后来人提几条醒:

  • 不要在 for-each 或普通 for 循环里直接增删集合。
  • 想安全删元素,老老实实用 Iteratorremove()
  • 如果是多线程场景,ArrayList 再香也得换种方式(比如并发包的安全列表)。
  • 看到 ConcurrentModificationException,八成你在遍历时干了“不可告人”的事。

咳,写累了。你们谁有比我更刺激的踩坑经历欢迎留言互嘲,毕竟代码路上,谁都在边走边“踩雷”对吧?


收个尾,顺祝所有码农写代码顺畅,小心点老毛病,别被集合“反杀”!

有一天,我和 CopyOnWriteArrayList 杯“线程安全”的咖啡

原文来自于:[zha-ge.cn/java/38]zha-ge.cn/java/38)

有一天,我和 CopyOnWriteArrayList 杯“线程安全”的咖啡

“说出来你可能不信,我第一次遇见 CopyOnWriteArrayList,真有点像突然喝到了一杯奇葩口味咖啡。” 那天项目里一顿多线程操作 ArrayList,直接就炸了——各种 ConcurrentModificationException 扑面而来。眼看需求日期在逼近,我只能抱着百度拼命啃资料,终于,那个神秘的名字——CopyOnWriteArrayList,出现在我的面前。

奇怪的名字,神奇的用法

这货的名字一看就不走寻常路:“写时复制”列表。跟平时 ArrayList 那种你加我加大家抢着改的作风完全不一样。 CopyOnWriteArrayList 的大法简单粗暴:只要有修改操作,比如 add 或 remove,它就直接把底层的数组“复制”一份,改完再换上。 读的时候,全世界读线程用的都是一个老版本的新数组,根本不用加锁。 写的时候,悄咪咪整出个新副本,等你们都读完了再说。

代码长这样:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("程序员");
list.add("出门左转");
list.remove("左转");
// ...

嗯,是不是看着很清爽? 连 iterator 都不用担心:

for (String s : list) {
    System.out.println(s);
    // list.add("插队"); // 不会抛 ConcurrentModificationException
}

想当初用普通 ArrayList,改着改着分分钟爆炸。CopyOnWriteArrayList 则“稳如老狗”。

踩坑瞬间

说起来简单,真用的时候,坑也是多得想抽自己耳光。

  • 那天真有个需求,存个8万条数据要频繁 add/remove。本想 CopyOnWriteArrayList 线程安全,直接莽,结果垃圾回收疯狂爆发,内存飙升。
  • 遍历过程中,想试试能不能边遍历边加元素,嗯,是不会抛异常,结果新加的东西遍历完了也看不到……
  • 查文档的时候脑子一热:既然线程安全,啥场面都能用吧!后来 Leader 只说了三个字:“开除吧!”

经验启示

时间一久,我总结出一套“用 CopyOnWriteArrayList 的锦囊”:

  • 适合读多写少场景 频繁写?直接 gg,别用这玩意儿,损失性能简直肉眼可见。
  • 遍历期间修改,遍历不到最新元素 它的 iterator 永远“活在过去”。要最新数据,遍历得重来一遍。
  • 内存占用高 千万别往里塞上万条、甚至百万级别数据。
  • 线程安全≠银弹 要是真有写多场景,还得老老实实用锁或者选用别的并发集合。

适合这样:

  • “配置黑板”类读多改少
  • 维护小容量缓存
  • 偶尔边迭代边刮胡子改几个元素

收个尾巴

说白了,CopyOnWriteArrayList 就是个谈恋爱怕闹分手的“安全派”: 你要改,我就自个儿带球跑路,剩下的你们慢慢看前任。 用得好,项目稳如老狗;用错场景,队友只会拿你祭天。

所以,下回再遇到多线程需要安全集合的场景,还得盘一盘,别再头铁乱撸 CopyOnWriteArrayList 啦!

——聊着聊着,咖啡快凉了,晚安,我的并发朋友们。

为什么 JDK 1.8 要给 HashMap 加红黑树?

原文来自于:zha-ge.cn/java/35

JDK 1.8 中 HashMap 引入红黑树的深层原因

问题的根源:链表的性能瓶颈

在 JDK 1.8 之前,HashMap 采用"数组 + 链表"的数据结构。当发生哈希冲突时,新元素会被添加到链表的头部或尾部。这种设计在正常情况下工作良好,但存在一个致命缺陷:当大量元素映射到同一个桶时,链表会变得很长,导致查询性能急剧下降

想象一下,如果一个桶中的链表有 1000 个元素,那么在最坏情况下,查找一个元素需要遍历整个链表,时间复杂度退化为 O(n),完全失去了哈希表应有的 O(1) 查询优势。

恶意攻击的威胁

更严重的是,这个特性可能被恶意利用。攻击者可以精心构造大量具有相同哈希值的字符串,强制它们映射到 HashMap 的同一个桶中,形成极长的链表。这种哈希碰撞攻击可以让服务器的 CPU 使用率飙升,造成拒绝服务攻击(DoS)。

红黑树:优雅的解决方案

JDK 1.8 引入红黑树正是为了解决这个问题。新的设计规则如下:

  • 阈值控制:当链表长度超过 8 时,自动转换为红黑树
  • 动态切换:当红黑树节点数量少于 6 时,重新退化为链表
  • 性能保障:红黑树保证了 O(log n) 的查询时间复杂度

这样的设计兼顾了两种数据结构的优势:

  • 链表在元素较少时具有更好的空间效率和简单性
  • 红黑树在元素较多时提供稳定的查询性能

为什么选择红黑树?

你可能会问,为什么不选择 AVL 树或其他平衡二叉树?答案在于权衡:

  1. 相对平衡:红黑树不要求严格平衡,但保证最长路径不超过最短路径的2倍
  2. 插入删除效率:相比 AVL 树,红黑树的插入和删除操作需要的旋转次数更少
  3. 实现复杂度:在性能和实现复杂度之间找到了最佳平衡点

总结

HashMap 引入红黑树是一个典型的工程优化案例。它不仅解决了链表在极端情况下的性能问题,还有效防范了哈希碰撞攻击。这个改进体现了 Java 团队对性能和安全性的持续关注,也展现了在数据结构设计中"没有银弹,只有权衡"的工程哲学。

通过这个优化,HashMap 在保持原有简单易用特性的同时,获得了更加稳定和可靠的性能表现,这正是一个成熟框架应有的品质。

❌