你真的了解ThreadLocal吗?

发布于 2021-10-12 10:48

前言
一般在项目中,有时候会有大量使用线程的情况,如果是读操作的话不会涉及到线程安全的问题,但如果是写操作那就会考虑到线程本身的安全问题了。

所谓的线程不安全是指多个线程同时去操作同一个全局变量,如果执行的结果和我们所预期想的不一样,那么我们就称之为线程安全问题。

在Java中如何解决线程安全问题?1.使用synchronized或者是Lock2.使用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中设置堆的内存大小(建议根据电脑内存大小决定,尽量设置小一些),然后再执行上部分的代码,那么程序会报错(内存溢出)。

原因及分析

为什么我们使用ThreadLocal会报内存溢出的错误?

接下来我们一起进入源码来分析一波。

当我们使用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 我们将第一时间删除。

相关素材