One Trick Per Day

本文涉及的产品
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
简介: 初始化Map应避免直接指定大小,建议用Guava或手动计算容量;禁止使用Executors创建线程池,防止OOM,推荐ThreadPoolExecutor或Guava;Arrays.asList返回不可变集合,禁用修改操作;遍历Map优先使用entrySet或forEach;SimpleDateFormat非线程安全,勿声明为static;并发更新记录时,根据冲突概率选用乐观锁或悲观锁。

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));

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出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 代替 DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

6.并发修改同一记录时需要加锁

要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。

说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次

相关文章
|
10月前
|
Web App开发 Ubuntu Unix
深入了解Ubuntu的命令行界面:使用终端和常用命令
实例3:使用包管理命令安装新的软件包: 更新软件包列表:sudo apt update 安装软件包:sudo apt install package-name
|
6月前
|
存储 缓存 运维
一场FullGC故障排查
本文记录了一次Java应用CPU使用率异常升至104%的排查过程。通过分析发现,问题由Full GC频繁触发导致,根源在于将大体积Excel数据以List&lt;Map&lt;String, String&gt;&gt;形式加载至JVM内存,造成堆内存膨胀且长期驻留,最终引发内存压力和GC风暴。结合JProfiler工具定位到大对象后,提出“治本”(移出JVM内存)与“治标”(精简数据结构)两类解决方案,并总结了从监控识别、工具分析到代码优化的完整排查思路,强调应关注JVM层面而非仅机器指标。
|
6月前
|
运维 Devops 开发工具
生产环境缺陷管理
git-poison基于go-git实现分布式bug追溯,解决多分支开发中bug漏修、漏发等协同难题,自动化卡点发布流程,降低沟通成本,提升发布安全性与效率。
生产环境缺陷管理
|
6月前
|
运维 NoSQL 测试技术
Redis:内存陡增100%深度复盘
本文复盘了一起Redis因缓冲区占满内存导致服务不可用的事故。根本原因为大KEY引发带宽耗尽,触发Redis内存使用率飙升,最终输入/输出缓冲区膨胀挤占全部内存,导致SET/GET超时。文章深入分析了缓冲区机制、内存淘汰策略,并指出即使配置合理,缓冲区失控仍可致服务崩溃。最后提出开发运维规范,强调压测、监控与合理设计的重要性。
Redis:内存陡增100%深度复盘
|
6月前
|
Java 测试技术 Linux
生产环境发布管理
本文介绍大型团队中基于自动化部署平台(CI/CD)的多环境发布流程,涵盖dev、test、pre、prod各环境职责,以及如何通过Jenkins+K8S实现高效部署与回滚,并结合Skywalking等工具进行日志追踪与问题排查。
生产环境发布管理
|
6月前
|
自然语言处理 fastjson Java
FastJson:大面积故障规避案例
本文记录了一次由Kotlin语法误用引发的FastJson反序列化重大故障排查过程。因将 `{}` 错误赋值给Java对象字段,导致FastJson解析时触发 `kotlin_error` 静态标记位异常,进而引发全局反序列化失败。问题根源隐蔽,影响深远,最终通过深入源码定位并修复。分享多语言混编下的踩坑经验,强调框架不可盲信,代码需严谨对待。(239字)
|
6月前
|
SQL 安全 网络协议
常见的网络攻击
本文介绍了常见网络威胁,包括恶意软件、网络钓鱼、中间人攻击、DDoS攻击、SQL注入、零日漏洞及DNS隧道。这些攻击通过漏洞、欺骗或加密流量等方式窃取数据、破坏系统或阻断服务,凸显网络安全防护的重要性。
常见的网络攻击
|
6月前
|
前端开发 程序员
常见注解及使用说明
本文介绍SpringMVC中@RequestMapping注解的作用及原理,讲解如何通过注解将HTTP请求映射到控制器方法,并列举@GetMapping等派生注解,帮助理解前后端接口的对应关系。
|
6月前
|
测试技术 UED
发布模式
蓝绿部署通过两套系统(蓝/绿)并行,实现零停机发布与快速回滚;金丝雀发布逐步替换旧版本,降低风险;A/B测试则用于对比多版本效果,优化用户体验。三者适用场景不同,各有侧重。
发布模式
|
6月前
|
Java Shell Apache
Jmeter快速入门
本文介绍了JMeter的安装与快速入门指南。首先需确保已安装JDK并配置环境变量,随后从官网或本地包下载JMeter,解压后通过双击或命令行启动。教程还演示了如何设置中文界面、创建线程组、添加HTTP取样器及监听器,并查看测试结果,帮助用户快速掌握基本使用方法。
Jmeter快速入门

热门文章

最新文章