One Trick Per Day

简介: 本文详解Java开发中六大易被忽视的陷阱:Map初始化容量设置不当、线程池滥用导致OOM、Arrays.asList的不可变性、遍历Map性能优化、SimpleDateFormat线程安全问题及并发更新加锁策略。通过源码分析与实例演示,揭示问题根源并提供Guava等最佳实践方案,助力提升系统稳定性与性能。

1.初始化Map大小并非用多少指定多少
初始化Map并非用多少初始化Size是多少,建议使用Guava,避免扩容引起的动荡()
说明
如:Map<String, String> map = new HashMap<>(1); 在具体使用时,并非size=1,而是最近的2的幂等,如1实际是2,3实际是4,9实际是16
使用方法
依赖gvaua:Map<String, String> map = Maps.newHashMapWithExpectedSize(7);

<dependency>  <groupId>com.google.guava</groupId>  <artifactId>guava</artifactId>  <version>17.0</version></dependency>

手动声明:Map<String, String> map = new HashMap<>(实际存储个数 / 0.75 + 1);
2.线程池初始化严禁使用Executors
使用线程池时候,我们可能会使用下面四个场景,这在alibaba代码规范中都是明令禁止的

// 创建一个单线程化的Executor[因为数量固定,可能会堆积大量请求,导致OOM]private static ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();// 创建一个固定数目线程的线程池[因为数量固定,可能会堆积大量请求,导致OOM]private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);// 创建一个可执行命令的单线程Executor[可能会创建大量的线程,导致OOM]private static ExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();// 创建一个可缓存的线程池(60S存活时间)[可能会创建大量的线程,导致OOM]private static ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

我们先来一个简单的例子,模拟一下使用 Executors 导致 OOM 的情况。

public class ExecutorsDemo {    private static ExecutorService executor = Executors.newFixedThreadPool(15);    public static void main(String[] args) {        for (int i = 0; i < Integer.MAX_VALUE; i++) {            executor.execute(new SubThread());        }    }}class SubThread implements Runnable {    @Override    public void run() {        try {            Thread.sleep(10000);        } catch (InterruptedException e) {            //do nothing        }    }}

通过指定 JVM 参数:-Xmx8m -Xms8m 运行以上代码,会抛出 OOM:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded  at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371) at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)

以上代码指出,ExecutorsDemo.java 的第 16 行,就是代码中的 executor.execute(new SubThread());。

通过上面的例子,我们知道了 Executors 创建的线程池存在 OOM 的风险,那么到底是什么原因导致的呢?我们需要深入 Executors 的源码来分析一下。其实,在上面的报错信息中,我们是可以看出蛛丝马迹的,在以上的代码中其实已经说了,真正的导致 OOM 的其实是 LinkedBlockingQueue.offer 方法。

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371) at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)


如果读者翻看代码的话,也可以发现,其实底层确实是通过 LinkedBlockingQueue 实现的:

public static ExecutorService newFixedThreadPool(int nThreads) {    return new ThreadPoolExecutor(nThreads, nThreads,                                  0L, TimeUnit.MILLISECONDS,                                  new LinkedBlockingQueue<Runnable>());}

如果读者对 Java 中的阻塞队列有所了解的话,看到这里或许就能够明白原因了。Java 中 的 BlockingQueue 主 要 有 两 种 实 现, 分 别 是 ArrayBlockingQueue 和 LinkedBlockingQueue。ArrayBlockingQueue 是一个用数组实现的有界阻塞队列,必须设置容量。LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE。这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置 LinkedBlockingQueue 的容量的话,其默认容量将会是 Integer.MAX_VALUE。 而 newFixedThreadPool 中创建 LinkedBlockingQueue 时,并未指定容量。此时,LinkedBlockingQueue 就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。上面提到的问题主要体现在 newFixedThreadPool 和 newSingleThreadExecutor 两个工厂方法上,并不是说newCachedThreadPool 和 newScheduledThreadPool 这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致 OOM

private static ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS,                                                                 new ArrayBlockingQueue(10));

正确使用:

private static ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS,                                                                 new ArrayBlockingQueue(10));

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。但是部分alibaba作者更推荐使用guava创建对应的线程池,示例如下:

public class ExecutorsDemo {    private static ThreadFactory namedThreadFactory = new            ThreadFactoryBuilder()            .setNameFormat("demo-pool-%d").build();    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,            0L, TimeUnit.MILLISECONDS,            new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new            ThreadPoolExecutor.                    AbortPolicy());    public static void main(String[] args) {        for (int i = 0; i < Integer.MAX_VALUE; i++) {            pool.execute(new SubThread());        }    }}

通过上述方式创建线程时,不仅可以避免 OOM 的问题,还可以自定义线程名称,更加方便的出错的时候溯源。

3.Arrays.asList之后不要调用修改操作

String[] str = new String[] { "you", "wu" };List list = Arrays.asList(str);

因为asList返回的实际是一个Arrays内部类,并没有实现集合的修改方法(add/remove/clear)// 当操作修改方法时,会报UnsupportedOperationException。
第一种情况:list.add("yangguanbao"); 运行时异常。
第二种情况:str[0] = "gujin"; 那么 list.get(0)也会随之修改。[涉及栈堆指针操作,修改数组的数据,导致同样引用该数据的list值被改变]


4.使用 entrySet 遍历 Map 类集合 KV
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。
如果是 JDK8,使用 Map.foreach 方法。
正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。
5.SimpleDateFormat不要定义为static
SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:

private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {

@Override

protected DateFormat initialValue() {

return new SimpleDateFormat("yyyy-MM-dd");

}

};

说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
6.并发修改同一记录时需要加锁
要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次

相关文章
|
5月前
|
文字识别 程序员 API
Python 合并 PDF 文件(批量处理方法)
周一邮件堆成山?PDF合并总被收费、限页、传服务器?用Python+PyPDF2,5行代码批量合并PDF,免费、离线、无水印!支持自定义顺序、选页、加书签、处理加密/扫描件,还能打包成双击即用的exe——告别工具焦虑,三秒搞定。
572 4
|
7月前
|
数据采集 自然语言处理 数据可视化
微调完怎么判断好不好?大模型效果评估入门指南(附代码)
本文详解大模型微调后如何科学评估效果,涵盖文本分类、生成与语言建模三类任务的核心指标(如F1、BLEU、ROUGE、PPL),结合Python代码实操演示,并强调需结合业务场景、微调前后对比及稳定性验证,避免“指标虚高”。附实用工具推荐,助力新手高效完成评估闭环。
微调完怎么判断好不好?大模型效果评估入门指南(附代码)
|
8月前
|
缓存 弹性计算 分布式计算
阿里云服务器租用价格参考:活动价500元以内经济型、通用算力型云服务器
阿里云服务器活动价格在500元以内的云服务器有哪些?2026年,阿里云再次下调了云服务器的收费标准,为用户提供了更加经济实惠的选择,目前500元预算即可以购买38元、99元、199元等特价云服务器,也可以购买4核8G等配置更高的云服务器。本文将为您详细介绍在500元预算内,能够租用到的阿里云服务器实例及其配置,以供大家参考和选择。
465 2
|
10月前
|
缓存
基于Squid代理实现SSRF攻击物理防御
本文介绍一种通过Squid代理集群实现物理隔绝的SSRF防御方案。利用Private Link与NLB构建隔离网络,将用户URL请求转发至独立Squid集群处理,有效阻断内网探测与攻击,提升系统安全性。
359 4
|
数据采集 人工智能 数据处理
如何画关系代数的连接图?关系代数中的连接操作:从理论到实践
三桥君认为,关系代数中的连接是一个重要而且容易混乱的知识点,三桥君通过查阅很多资料总结了与连接有关的知识点,并发现了他们之间的关系。本文产品专家三桥君通过理论知识先了解连接相关的重要名词意思,然后通过画图来理解画连接的思路以及他们之间的关系
406 0
|
8月前
|
搜索推荐 Java 关系型数据库
基于springboot的热门文创内容推荐分享系统
本研究聚焦文创产业线上平台内容推荐不足问题,结合MySQL、Java、SpringBoot与B/S架构,构建个性化推荐系统。旨在提升用户获取文创内容的效率与体验,促进产业传播效能与商业转化,推动技术在实际场景中的应用发展。
|
调度 开发者 Python
python超详细的日期操作【建议收藏备用】
python超详细的日期操作【建议收藏备用】
523 0
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
487 5
单片机按键的使用
单片机按键的使用
499 0