ArrayList线程不安全。占用内存情况
- 故事背景
- 方案&思路
- 解决线程不安全的问题
- 方案一:在这两个方法之前添加 synchronized 关键字。
- 方案二:使用ThreadLocal变量。
- 解决重复创建对象问题。
- 总结&升华
故事背景
存入redis的值,可能会出现错误的情况。如果出现错误,接口将会报错
排查之发现是。对ArrayList的使用问题。问题主要有两个:
使用了线程不安全的ArrayList作为公共变量
每次给ArrayList重新赋值的时候都创建了一个新的变量,导致内存飙升问题
方案&思路
我们用一个例子来复现一下这个问题
测试的类
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);
}
}
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();
}
}
}
执行结果
结果中可能会报错 java.util.ConcurrentModificationException 这个错是由于使用list的时候对list进行修改导致的
在结果里我们可以看到,我们声明的 testList 在一个线程操作的时候,另外的线程对它进行了修改。如果他是线程安全的话打印的值只有两种可能
updateTestList[a01+, a02+, a03+, a04+]
updateTestList2[b01+, b02+, b03+, b04+]
解决线程不安全的问题
方案一:在这两个方法之前添加 synchronized 关键字。
如何解决我们上述的这个问题呢?简单的方法就是给我们定义的updateTestList进行上锁。
看一下结果:
我们可以发现,添加上synchronized 关键字之后,执行没有问题了。通过加锁,保证线程安全。
方案二:使用ThreadLocal变量。
这个我之前的博客有写过,大家可以观看。
在线人员逻辑反例–ThreadLocal、继承、索引失效、
使用ThreadLocal声明变量,他会为每个线程都创建一个变量。注意内存消耗
使用方法
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());
}
}
对应结果
解决重复创建对象问题。
我们重点来看一下这部分代码
每次调用updateTestList方法的时候,都会重新创建一个新的对象。然后之前的对象由于失去引用,等待
GC回收。并发上来之后,就会导致内存上升。
内存变化图:
每调用一次方法,都会去创建新的对对象,并且将testList的引用指向新的地址。
解决方法:
解决方法很简单,我们业务的需要时将这个List清空,我们只需要调用 removeALL方法就够了,这样的话,在堆里始终是同一块内存地址。不会持续开辟内存空间
总结&升华
- 编写代码的时候,要清楚它的道理,可能会出现的大坑,提前跳过去。掉进去之后要保证以后再也不会掉进去。
- 考虑效率问题,避免不必要的内存开销。