ArrayList线程不安全。占用内存情况
一:故事背景
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可能开销不是很大,但是这是完全无意义的开销。只有看到这种小点,培养我们的编码习惯和边界,才能避免出更大的错误。