【实战篇】ThreadLocal及其相关知识(一)

前情提要

本文需要有一定JVM内存知识

本文需要有一定Thread知识

本文需要有一定ThreadPool知识

文章分发

www.yuque.com/docs/share/… 《ThreadLocal及其扩展类》

文章主体开始

本文主要描述ThreadLocal及其存在的问题

基础知识-Java的引用

强引用:在虚拟机GC的时候,可达性分析算法下仍然存活的不会被垃圾回收器回收的。当内存空间不足的时候,就会抛出OOM异常。一般认为就是虚拟机栈一直指向某块堆内存,就是一个强引用。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocal

// ThreadLocal

public T get() {
    // 1,当前线程的 Thread
    Thread t = Thread.currentThread();
    // 2,通过当前线程拿到 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 3,通过 ThreadLocalMap 和 this(ThreadLoca)
        // 拿到 静态内部类 Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}


ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

static class ThreadLocalMap {
    // 3.1,静态内部类是继承 WeakReference 软引用 的
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}
复制代码

Thread

// Thread

public static native Thread currentThread();

ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码

上面的注解解析

注解1解析

Thread t = Thread.currentThread();

通过 Thread 类的静态 native 方法,获取到当前线程对象

注解2解析

ThreadLocalMap map = getMap(t);

通过Thread的当前线程拿到 ThreadLocalMap,getMap是ThreadLocal的方法,但是指向的是Thread的一个属性

ThreadLocal.ThreadLocalMap threadLocals = null;

如果第一次进来,会返回null,就会去设置默认值。

注解3解析

ThreadLocalMap.Entry e = map.getEntry(this); 

static class Entry extends WeakReference<ThreadLocal<?>>

通过 ThreadLocalMap 和 this(ThreadLoca) ,拿到 静态内部类 Entry

静态内部类 Entry 是继承 WeakReference 软引用 

小结

ThreadLocal其实就是Thread的一个属性,里面存放了一些该线程里面的数据

使用注意事项

每次使用完必须要remove,下面是原因的解析

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author xuegao
 * @version 1.0
 * @date 2021/12/11 17:59
 */
public class ThreadLocalV2Service {

    static class LocalVariable {
        // 总共有5M
        private byte[] local = new byte[1024 * 1024 * 5];
    }

    static final ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    static ThreadLocal<LocalVariable> THREAD_LOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; ++i) {
            POOL_EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    LocalVariable localVariable = new LocalVariable();
                    ThreadLocalV2Service.THREAD_LOCAL.set(localVariable);
                    System.out.println("thread name end:" + Thread.currentThread().getName() +
                            ", value:" + ThreadLocalV2Service.THREAD_LOCAL.get());
                    ThreadLocalV2Service.THREAD_LOCAL.remove();
                }
            });
            Thread.sleep(1000);
        }
        // 是否让key失效,都不影响。只要持有的线程存在,都无法回收。
        // ThreadLocalOutOfMemoryTest.localVariable = null;
        System.out.println("pool execute over");
    }
    // 作者:城南码农
    // 链接:https://juejin.cn/post/6982121384533032991
    // 来源:稀土掘金
    // 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
}
复制代码

下面四图是idea的profiler的使用,把代码运行起来就可以进来这里了,或者使用jdk自带的jvisualvm.exe

如果有什么问题,请使用vx联系我吧,在下方

下面两张图是注释后和注释前的区别,指 THREAD_LOCAL.remove()

由上面第一张图可知,如果在使用完threadlocal后进行了remove删除,那么GC后堆内存就基本上被垃圾回收了

第二张图是使用完成后没有进行remove的,我们有6个线程,每个线程持有5M内存,所有大约有30M内存在堆中无法被回收

原因是因为我们定义的是线程池,线程池并不会因为gc,而将线程池里面的6个线程直接销毁,还是会一直持有,所以形成的是,6个thread是栈帧,堆内存是thread里面的threadlocal属性,每个都有5M大小,栈帧一直存在,堆内存会被一直视为有人使用,而永远无法被gc回收掉,造成了内存泄漏。(虽然我们往线程池里面execute了很多任务,但是线程池的maximumPoolSize只有6个,后续的都会覆盖之前的)

实际项目中,多线程技术的广泛使用,如果代码中有threadlocal没有remove,就会造成大量的堆内存被浪费

使用缺陷

tip:父子线程,在web工程中指的是,一个完整的请求是一个线程,但是在这个请求里面,使用或者开启了另外的线程

只能在单个线程下传递数据,无法在父子线程等情况传递数据

以下是代码事例

/**
 * @author xuegao
 * @version 1.0
 * @date 2021/12/11 14:58
 */
public class MyThreadLocalTest1 {
    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    public static void main(String[] args) {
        THREAD_LOCAL.set("luanqi");
        System.out.println("main线程 = " + THREAD_LOCAL.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("新的线程 = " + THREAD_LOCAL.get());
            }
        }).start();
    }
}
// main线程 = luanqi
// 新的线程 = null


// 思路有来自

// 作者:我有一只喵喵
// 链接:https://juejin.cn/post/7022529092519985160
// 来源:稀土掘金
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码

实际中的使用场景

1,单个线程中,数据的透传

其他

vx公众号:Java雪糕

www.yuque.com/xuegao-btut…

Guess you like

Origin juejin.im/post/7040787203349282853