Bootstrap

Threadlocal学习

一、threadLocal简介

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前的线程。该变量对其他线程而言是隔离的,也就是说该变量是独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

二、threadlocal的底层实现原理

每一个线程的Thread对象中都会有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocalHashCode为键,以本地线程变量为值的K-V健值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每个ThreadLocal对象都包含着独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量。

源码:

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

这个是threadLocal在堆栈的分布:

三、threadLocal的使用

使用代码:

public class ThreadLocalDemo {

    public static void main(String[] args) {
        //起两个线程,模拟两个用户发起请求到web服务,
        // 比如两个用户都在请求首页接口
        //这时web服务端会有两个线程对应处理两个请求
        for (int i = 0; i < 2; i++) {
            int fn = i;
            new Thread(() -> {
                ServiceA.a("accountId" + fn);
                ServiceB.b();
                ServiceC.c();
            }, "线程_" + i).start();
        }
    }
    /**
     * 一般使用一个全局静态类管理ThreadLocal对象
     */
    static class ResourceClass {

        public final static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        public final static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

        public static void setThreadLocal1(String value) {
            ResourceClass.threadLocal1.set(value);
        }
        public static String getThreadLocal1() {
            return ResourceClass.threadLocal1.get();
        }

        public static void setThreadLocal2(String value) {
            ResourceClass.threadLocal2.set(value);
        }
        public static String getThreadLocal2() {
            return ResourceClass.threadLocal2.get();
        }
    }

    /**
     * 业务类A
     */
    static class ServiceA {
        /**
         * 模拟业务,获取到accountId,并保存到threadLocal1变量中
         * 同时保存线程名字到threadLocal2中
         */
        public static void a(String accountId) {
            String name = Thread.currentThread().getName();
            ResourceClass.setThreadLocal1(accountId);
            ResourceClass.setThreadLocal2(name);
            System.out.println(name + "在A类中保存accountId: " + accountId + " 到ThreadLocal\n");
        }
    }
    /**
     * 业务类B
     */
    static class ServiceB {
        /**
         * 模拟执行一些业务,要用到accountId
         */
        public static void b() {
            String accountId = ResourceClass.getThreadLocal1();
            String name = ResourceClass.getThreadLocal2();
            System.out.println(name + "在B类利用accountId:" + accountId + " 继续执行一些B类业务\n");
        }
    }
    /**
     * 业务类C
     */
    static class ServiceC {
        /**
         * 模拟执行一些业务,要用到accountId
         */
        public static void c() {
            String accountId = ResourceClass.getThreadLocal1();
            String name = ResourceClass.getThreadLocal2();
            System.out.println(name + "在C类利用accountId:" + accountId + " 继续执行一些C类业务\n");
        }
    }
}

四、threadLocal的使用场景

ThreadLocal有两个实用的场景:

  • 1、每个实例需要自己单独的实例
  • 2、实例需要在多个方法中共享,但不希望被多个线程共享

具体使用:

  • 存储用户Session
  • 数据库连接,处理数据库事务
  • 数据跨层传递
  • Spring使用ThreadLocal解决线程安全问题

五、ThreadLocal导致的内存泄漏

分析ThreadLocal导致的内存泄露前,我们首先要掌握内存泄漏,强引用与弱引用以及GC垃圾回收机制。

内存泄漏

内存泄漏就是为程序申请完内存后,无法释放已申请的空间,一次内存泄漏不会出现问题,但是积少成多导致内存用尽,这事就大了。

直白的说就是不再使用的对象或者变量占用的空间不能被垃圾回收就是内存泄漏

强引用与弱引用

强引用:使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收,当内存空间不足,Java虚拟机宁愿发生OOM,也不会回收具有强引用的对象。

如果想要解除这种引用,可以显式地将引用赋值为null。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

GC回收机制--如何判断对象已死

java如何找到需要回收的对象。

  • 引用计数法:没个对象有一个引用计数属性,新增一个引用计数+1,引用释放时计数-1,计数为0时可以回收。
  • 可达性分析:GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。

引用计数法存在循环引用的问题(所以JVM没用引用计数法)。

ThreadLocal的内存泄漏的原因

首先来看ThreadLocal的原理图:

这里主要看虚线的引用是一个弱引用。看代码

static class Entry extends WeakReference<ThreadLocal<?>>

这里的WeakReference表示就是一个弱引用。

所以ThreadLocalMap中key引用ThreadLocal是一个弱引用,垃圾回收会在发生GC时将key=null,但这个时候value还强引用着线程变量,这个现象导致我们不能通过ThreadLocal(key)来清除ThreadLocalMap中Entry的value,只有在线程退出的时候才能清除掉这个引用链,造成了内存泄漏。

那么这里因为弱引用导致了内存泄漏,我们为什么要使用弱引用呢?

如果Entry对于ThreadLocal是强引用,需要回收ThreadLocal的时候,由于ThreadLocal的引用是强引用,那么ThreadLocal就不能被回收,所以不回被回收。

真正原因是ThreadLocal强引用着ThreadLocalMap,导致ThreadLocalMap生命周期与Thread一样,如果没有手动去回收对应的Entry就会导致内存泄漏。

怎么解决这个内存泄漏呢

每次使用完ThreadLocal都调用它的remove方法清除数据。

将Thread变量定义为private static,这样可以保证一直存在ThreadLocal的弱引用,保证任何时候可以通过ThreadLocal访问到Entry的value值,进而清除掉。

;