28个案例问题分析---15---登陆之后我加入的课程调用接口报错--ArrayList线程不安全。占用内存情况

简介: 28个案例问题分析---15---登陆之后我加入的课程调用接口报错--ArrayList线程不安全。占用内存情况

ArrayList线程不安全。占用内存情况

4.2 重复创建对象造成内存空间浪费

一:故事背景

1.1 问题描述

存入redis的值,可能会出现错误的情况。如果出现错误,接口将会报错

1.2 问题原因

排查之发现是。对ArrayList的使用问题。问题主要有两个:

1.使用了线程不安全的ArrayList作为公共变量

2.每次给ArrayList重新赋值的时候都创建了一个新的变量,导致内存飙升问题

二:问题复现

2.1 ThreadTest 代码

我们用一个例子来复现一下这个问题

测试的类

public class ThreadTest {
    //新建一个list作为成员变量
    List<String> testList ;
    public void updateTestList(){
        testList = new ArrayList<>();
        testList.add("a01+");
        testList.add("a02+");
        testList.add("a03+");
        testList.add("a04+");
        //打印一下看看有什么
        System.out.println("updateTestList"+testList);
    }
    public void updateTestList2(){
        testList = new ArrayList<>();
        testList.add("b01+");
        testList.add("b02+");
        testList.add("b03+");
        testList.add("b04+");
        //看一下list里有什么
        System.out.println("updateTestList2"+testList);
    }
}

上述代码创建了一个List作为成员变量,在下面的方法中,重新new了一个List对象,并且让成员变量的指针指向新创建的List对象。

2.2 main函数 代码

测试代码,通过多线程的方式,模拟并发执行

public class Main {
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        //开一个多线程测试一下
        for (int i = 0; i < 100; i++) {
          Thread thread = new Thread(new Runnable() {
              @Override
              public void run() {
                  threadTest.updateTestList();
                  threadTest.updateTestList2();
              }
          });
        thread.start();
        }
    }
}

2.3 执行结果

2.4 结果分析

结果中可能会报错 java.util.ConcurrentModificationException 这个错是由于使用list的时候对list进行修改导致的

在结果里我们可以看到,我们声明的 testList 在一个线程操作的时候,另外的线程对它进行了修改。

如果他是线程安全的话打印的值只有两种可能

updateTestList[a01+, a02+, a03+, a04+]

updateTestList2[b01+, b02+, b03+, b04+]

所以我们得出结论:

ArrayList 是线程不安全的

三: 问题解决

上文提到我们一共有两个问题,这里先解决线程不安全的问题

3.1 在这两个方法之前添加 synchronized 关键字。

如何解决我们上述的这个问题呢?简单的方法就是给我们定义的updateTestList进行上锁。

看一下结果:


我们可以发现,添加上synchronized 关键字之后,执行没有问题了。通过加锁,保证线程安全。


3.2 使用ThreadLocal变量。

这个我之前的博客有写过,大家可以观看。

在线人员逻辑反例–ThreadLocal、继承、索引失效、

使用ThreadLocal声明变量,他会为每个线程都创建一个变量。注意内存消耗


3.2.1 使用方法

public class ThreadTest2 {
    ThreadLocal<List<String>> testList = ThreadLocal.withInitial(()->new ArrayList<>());
    public  void updateTestList(){
        testList.get().removeAll(testList.get());
        testList.get().add("a01+");
        testList.get().add("a02+");
        testList.get().add("a03+");
        testList.get().add("a04+");
        //打印一下看看有什么
        System.out.println("updateTestList"+testList.get());
    }
    public  void updateTestList2(){
        testList.get().removeAll(testList.get());
        testList.get().add("b01+");
        testList.get().add("b02+");
        testList.get().add("b03+");
        testList.get().add("b04+");
        //看一下list里有什么
        System.out.println("updateTestList2"+testList.get());
    }
}

3.2.2 对应结果

3.3 解决重复创建对象问题。

这里解决第问题,重复创建对象造成的内存空间浪费问题:

3.3.1 问题复现

我们重点来看一下这部分代码每次调用updateTestList方法的时候,都会重新创建一个新的对象。然后之前的对象由于失去引用,等待

GC回收。并发上来之后,就会导致内存上升。

3.3.2 内存变化

内存变化图:

每调用一次方法,都会去创建新的对对象,并且将testList的引用指向新的地址。

3.3.1 解决方法

解决方法很简单,我们业务的需要时将这个List清空,我们只需要调用 removeALL方法就够了,这样的话,在堆里始终是同一块内存地址。不会持续开辟内存空间


四:总结&升华

4.1 ArrayList线程不安全

Java中的ArrayList是线程不安全的,因为它不是同步的(unsynchronized)。具体来说,当多个线程同时对同一个ArrayList实例进行修改操作时,可能会出现不可预期的结果。


例如,当一个线程在执行add()操作添加元素时,另一个线程可能正在执行remove()操作删除元素,这样就会破坏ArrayList内部的数据结构。另外,由于ArrayList是动态数组,在增加或删除元素时需要重新分配内存空间,这也会增加线程不安全的风险。


为了避免这种情况,Java提供了同步的(synchronized)版本Vector,或者使用并发安全的CopyOnWriteArrayList。在多线程环境中,建议使用这些线程安全的集合类。我们这里没有讲解以后有机会将会更新。

4.2 重复创建对象造成内存空间浪费

Java中的ArrayList是线程不安全的,因为它不是同步的(unsynchronized)。具体来说,当多个线程同时对同一个ArrayList实例进行修改操作时,可能会出现不可预期的结果。


例如,当一个线程在执行add()操作添加元素时,另一个线程可能正在执行remove()操作删除元素,这样就会破坏ArrayList内部的数据结构。另外,由于ArrayList是动态数组,在增加或删除元素时需要重新分配内存空间,这也会增加线程不安全的风险。


为了避免这种情况,Java提供了同步的(synchronized)版本Vector,或者使用并发安全的CopyOnWriteArrayList。在多线程环境中,建议使用这些线程安全的集合类。


虽然,我们这里创建List可能开销不是很大,但是这是完全无意义的开销。只有看到这种小点,培养我们的编码习惯和边界,才能避免出更大的错误。


目录
相关文章
|
1月前
|
存储 缓存 监控
Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
本文介绍了Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
108 7
|
2月前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
|
3月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
26 1
|
3月前
|
监控 Java 数据库连接
使用线程池时,如何避免内存泄漏的问题?
使用线程池时,如何避免内存泄漏的问题?
|
3月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
65 2
|
3月前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
|
3月前
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
47 0
|
4月前
|
安全 Java 调度
python3多线程实战(python3经典编程案例)
该文章提供了Python3中多线程的应用实例,展示了如何利用Python的threading模块来创建和管理线程,以实现并发执行任务。
75 0
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
358 1
|
1月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。