你真的了解ThreadLocal吗?
发布于 2021-10-12 10:48
在Java中如何解决线程安全问题?
1.使用synchronized或者是Lock
2.使用ThreadLocal
如果是使用互斥锁即synchronized或Lock,锁的实现方案是通过线程排队,上个线程结束后且释放锁下个线程才能进入,从而解决线程安全的问题;但是这样做效率肯定会有所下降,以时间换空间的方法在大部分项目中都不可取。
当我们使用ThreadLocal时特别要注意内存溢出(Out Of Memory,简称 OOM)
什么是内存溢出?
是指无用的对象一直占据着内存且没有被释放,并且一直得不到释放,这种无用对象占据内存空间的现象就称为内存溢出。
代码演示部分
package com.enu;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author:zjy
* @description:
* @create:2021-05-26 23:16
**/
public class test {
//创建一个初始化数组大小 10m
static class MyTask{
private byte[] bytes = new byte[10 * 1024 * 1024];
}
//自定义一个ThreadLocal
private static ThreadLocal<MyTask> threadLocal = new ThreadLocal<>();
//创建主测试方法
public static void main(String[] args) throws InterruptedException {
//创建线程池
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(
5,
5,
5,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100));
//循环创建10次
for (int i = 0; i < 10; i++) {
executeTask(threadPoolExecutor);
Thread.sleep(1000);
}
}
private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
threadPoolExecutor.execute(() -> {
MyTask myTask = new MyTask();
System.out.println("创建对象");
//将对象设置进ThreadLocal
threadLocal.set(myTask);
});
}
}
这部分代码最终的打印结果:
如果我们在idea中设置堆的内存大小(建议根据电脑内存大小决定,尽量设置小一些),然后再执行上部分的代码,那么程序会报错(内存溢出)。
原因及分析
当我们使用set方法时:
1. 获取到当前的线程
2. 根据当前线程获取到变量
3. 如果变量不为null则把该变量以k v 的形式存储到map中去
这样我们不难看出每个线程执行时会通过ThreadLocal把变量存储到一个叫ThreadLocalMap的集合中去。
贴示出ThreadLocalMap的源码:
static class ThreadLocalMap{
// 实际存储数据的数组
private Entry[] table;
// 存储变量的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
}
从上述源码我们可以看出:ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值。
因为线程池的生命周期很长,所以线程池存储的数据(value)则会一直存在,从而造成了内存处于紧张或溢出的场景。
可以通过如下办法避免这样的场景出现,代码如下:
package com.enu;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author:zjy
* @description:
* @create:2021-05-26 23:16
**/
public class test {
//创建一个初始化数组大小 10m
static class MyTask{
private byte[] bytes = new byte[10 * 1024 * 1024];
}
//自定义一个ThreadLocal
private static ThreadLocal<MyTask> threadLocal = new ThreadLocal<>();
//创建主测试方法
public static void main(String[] args) throws InterruptedException {
//创建线程池
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(
5,
5,
5,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100));
//循环创建10次
for (int i = 0; i < 10; i++) {
executeTask(threadPoolExecutor);
Thread.sleep(1000);
}
}
private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
threadPoolExecutor.execute(() -> {
MyTask myTask = new MyTask();
System.out.println("创建一个对象");
try {
//将对象设置进ThreadLocal
threadLocal.set(myTask);
//其他业务代码
}finally {
//移除掉该线程在内存中所产生的数据
threadLocal.remove();
}
});
}
}
通过ThreadLocal中的remove()方法来解决该问题,这么牛皮的方法,我们先通过源码来解析一波再说。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
通过该代码段可以看出最后会在Thread中移除ThreadLocalMap,也就是说会把该集合中所存储的数据都丢弃掉从而释放内存空间,就不会在使用ThreadLocal的时候造成OOM。
本章完!下次再见
文章持续更新中,感兴趣的看官们可以微信搜一搜【爱搞事的程序猿】,欢迎你一起来讨论!
本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。
相关素材