ThreadLocal全面解析,一篇带你入门

=====================

大厂面试题: 
1.Java中的引用类型有哪几种?
2.每种引用类型的特点是什么?
3.每种引用类型的应用场景是什么?
4.ThreadLocal你了解吗
5.ThreadLocal应用在什么地方?  Spring事务方面应用到了
6.ThreadLocal会产生内存泄漏你了解吗?

1.java中引用类型及特点


  • 强 引用: 最普通的引用 Object o = new Object()
  • 软 引用: 垃圾回收器, 内存不够的时候回收 (缓存)
  • 弱 引用: 垃圾回收器看见就会回收 (防止内存泄漏)
  • 虚 引用: 垃圾回收器看见二话不说就回收,跟没有一样 (管理堆外内存) DirectByteBuffer -> 应用到NIO Netty

finalize(): 当对象被回收时, finalize()方法会被调用, 但是不推荐使用去回收一些资源,因为不知道他什么时候会被调用, 有时候不一定会调用

public class C {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }
}

1.1 强引用

正常引用,但没有人指向的时候就会被回收.

import java.io.IOException;
/**
 * 强引用
 */
public class R1_NormalReference {
    public static void main(String[] args) throws IOException {
        //正常引用
        C c = new C();
        c = null;//没人指向
        System.gc();//DisableExplicitGC

        //阻塞一下,方便看结果
        System.in.read();
    }
}

1.2 软引用

垃圾回收器, 内存不够的时候回收 (缓存)

import java.io.IOException;
import java.lang.ref.SoftReference;

/**
 * 软引用
 */
public class R2_SoftReference {
    public static void main(String[] args) {
        SoftReference<byte[]> soft = new SoftReference<>(new byte[1024 * 1024 * 10]);//10M
        System.out.println(soft.get());
        //gc回收
        System.gc();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(soft.get());

        //再分配一个数组,好heap(堆)放不下, 这个时候系统会回收一次, 如果不够,会把软引用回收
        byte[] bytes = new byte[1024 * 1024 * 15];
        System.out.println(soft.get());

    }
}

结果:
[B@1540e19d
[B@1540e19d
null

前提设置 -Xmx30M 堆内存最大30M 用于测试

idea里这样设置
给堆内存分配空间

1.3 弱引用

遇到GC就会被回收

import java.lang.ref.WeakReference;
/**
 * 弱引用
 */
public class R3_WeakReference {
    public static void main(String[] args) {
        WeakReference<C> weak = new WeakReference<>(new C());
        System.out.println(weak.get());
        //gc回收
        System.gc();
        //遇到GC就会被回收
        System.out.println(weak.get());

    }
}

结果:
com.cz.reference.C@3c679bde
null
finalize

1.4 虚引用

不管三七二十一 遇到直接回收

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;
/**
 * 虚引用
 */
public class R4_PhantomReference {
    private static final List<Object> LIST = new LinkedList<>();
    private static final ReferenceQueue QUEUE = new ReferenceQueue();

    public static void main(String[] args) {

        PhantomReference<C> phantomReference = new PhantomReference<>(new C(),QUEUE);

        new Thread(() -> {
            while (true){
                LIST.add(new byte[1024*1024]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                System.out.println(phantomReference.get());
            }
        }).start();

        new Thread(() -> {
            while (true){
                Reference<? extends C> poll = QUEUE.poll();
                if (poll != null){
                    System.out.println("-----虚引用对象被JVm回收了--------" + poll);
                    return;
                }
            }
        }).start();
    }
}
结果:
null
null
finalize
null
null

总结: 强软弱虚 1

  • 强 正常的引用
  • 软 内存不够, 进行清除
    • 大对象的内存
    • 常用对象的缓存
  • 弱 遇到GC就会被回收
    • 缓存, 没有容器引用指向的时候就需要清除缓存
    • ThreadLocal
    • WeakReferenceMap
  • 虚 看见就回收, 且看不到值
    • 管理堆外内存

2.ThreadLocal


从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变是。这种变畺在多线程环境下访问(通 过get和set方法访问)时能保证各个线程的变星=相对独立于其他线程内的变垦。ThreadLocal实例通常来说都是 private static类型的,用于关联线程和线程上下文。

我们可以得知ThreadLocal的作用是:提供线程内的局部变星,不同的线程之间不会相互干扰,这种变星在 线程的生命周朗内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度.

2.1.特点:

2.2 ThreadLocal 和Synchronized的区别

2.3 ThreadLocal的内部结构

现在让我们看一下ThreadLocal的内部原理, 探究它能实现线程数据隔离的原理

JDK 早期设计:

在这里插入图片描述

每个ThreadLocal都创建一个Map, 然后用Thread(线程) 作为Map的key, 要存储的局部变量作为Map的value, 这样就能达到各个线程的局部变量隔离的效果, 这是最简单的设计方法. 早期设计

JDK8 优化设计(现在的设计)

JDK8中ThreadLocal的设计是 : 每个Thread维护一个ThreadLocalMap, 这个Map的keyThreadLocal实例本身,value才是真正要存储的值Object
在这里插入图片描述

具体过程如下:

1. 每个THreadLocal线程内部都有一个Map(ThreadLocalMap),这个map由一系列entry组成

2. Map里面的entry存储的ThreadLocal对象(key)和线程变量副本(Value)也就是存储的值

3. Thread内部的Map是由ThreadLocal维护的, 由ThreadLocal负责向map获取和设置线程变量值

4. 对于不同的线程, 每次获取value(也就是副本值),别的线程并不能获取当前线程的副本值, 形成了副本的隔离,互不干扰.

对比一下 :
在这里插入图片描述

如今设计的好处:

  1. 每个Map存储的Entry数量变少
  2. 当Thread销毁的时候, THreadLocalMap也会随之销毁, 减少内存的使用.(之前以Thread为key会导致ThreadLocalMap的生命周期很长)

2.4 THreadLocalMap源码分析

2.4.1 基本结构

ThreadLocalMap是ThreadLocal的静态内部类, 没有实现Map接口, 用独立的方式实现了Map的功能, 其内部的Entry也是独立实现.

在这里插入图片描述
在这里插入图片描述
其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。

1.ThreadLocal成员变量

		/**
         * The initial capacity -- MUST be a power of two.
         * 初始化容量,必须是2的整数次幂
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存放数据的table, 同样数组长度必须是2的整数次幂
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * 数组里entrys的个数,可以判断table是否超过阈值 (存储的格式)
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * 阈值 进行扩容的阈值,表使用大于他的时候,进行扩容
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

2.ThreadLocalMap中用于存储数据的entry

		/**
         * 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.
         翻译:
         * Entry继承WeakReference, 并且用ThreadLocal作为key
         * 如果key为null(entry.get() == null)意味着key不在被引用,因此这时候entry也可以从tab
         *中清除(被垃圾回收器回收) 
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

(ThreadLocal) key是弱引用, 其目的就是讲ThreadLocal对象的生命周期和和线程的生命周期解绑. 减少内存使用
从中我们可以发现这个Map的key是ThreadLocal类的实例对象,value为用户的值,并不是网上大多数的例子key是线程的名字或者标识。

3.ThreadLocal的set和get方法代码:

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
	            map.set(this, value); //这里的this指代调用者,是当前类Threadlocal的一个实例,下面会列出map.set()方法的源码
        else
	            createMap(t, value);
}
public T get() {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
			@SuppressWarnings("unchecked")
			                T result = (T)e.value;
			return result;
		}
	}
	return setInitialValue();
}
        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;//ThreadLocalMap中维护了一个数组table, private Entry[] table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);//每次会根据key的hash值计算出对应entry的下标,获取出对应的entry后,再向entry设置键和值,下面的代码就是进行具体的设置逻辑

            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();
        }

到这里,我们就可以理解ThreadLocal究竟是如何工作的了

  • ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
  • 由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的。
    下面给出一个例子:
public class MyDemo {
    private String content;

    private String getContent() {
        return content;
    }

    private void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("-----------------------");
             		System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}


ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

4.如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?

查看源码,可以看到:

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}

对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。

但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。

2.5 强弱引用和内存泄漏

1.内存泄漏相关概念

  • 内存溢出: Memory overflow 没有足够的内存提供申请者使用.
  • 内存泄漏: Memory Leak 程序中已经动态分配的堆内存由于某种原因, 程序未释放或者无法释放, 造成系统内部的浪费, 导致程序运行速度减缓甚至系统崩溃等严重结果. 内存泄漏的堆积终将导致内存溢出

2.强弱引用见第一大点. (如上)

3.如果key是强引用

假设ThreadLocalMap中的key使用了强引用, 那么会出现内存泄漏吗?
在这里插入图片描述

  1. 假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了

  2. 但是因为threadLocalMap的Entry强引用了threadLocal, 造成ThreadLocal无法被回收

  3. 在没有手动删除Entry以及CurrentThread依然运行的前提下, 始终有强引用链threadRef → currentThread → entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏

    也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的

4.如果key是弱引用

假设ThreadLocalMap中的key使用了弱引用, 那么会出现内存泄漏吗?

在这里插入图片描述

  1. 假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了

  2. 由于threadLocalMap只持有ThreadLocal的弱引用, 没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收, 此时Entry中的key = null

  3. 在没有手动删除Entry以及CurrentThread依然运行的前提下, 也存在有强引用链threadRef → currentThread → value, value就不会被回收, 而这块value永远不会被访问到了, 导致value内存泄漏

    也就是说: ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

5.内存泄漏的真实原因

在这里插入图片描述
出现内存泄漏的真实原因出改以上两种情况,2
比较以上两种情况,我们就会发现:
内存泄漏的发生跟 ThreadLocalIMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?

细心的同学会发现,在以上两种内存泄漏的情况中.都有两个前提:
1 .没有手动侧除这个 Entry
2 · CurrentThread 依然运行
第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法翻除对应的 Entry ,就能避免内存泄漏。
第二点稍微复杂一点,由于ThreodLocalMapThreod 的一个属性,被当前线程所引甲丁所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。

综上, ThreadLocal 内存泄漏的根源是:

由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏.

6 为什么使用弱引用

为什么使用弱引用,根据刚才的分析,我们知道了:

无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

​ 要避免内存泄漏有两种方式:
​ 1 .使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
​ 2 .使用完 ThreadLocal ,当前 Thread 也随之运行结束

​ 相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的.所以就会导致一直存在着引用

​ 也就是说,只要记得在使用完ThreadLocal 及时的调用 remove ,无论 key 是强引用还是弱引用都不会有问题.

那么为什么 key 要用弱引用呢

​ 事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null (也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么是会将 value 置为 null 的.

​ 这就意味着使用完 ThreadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏.
附上源码,大家应该很容易看懂这块了吧,我列出的是getEntry的源码:

3.补充哈哈

3.1 threadlocal用在什么场景

看到这个评论简直了哈哈,贴图上来大家乐呵乐呵:

场景一:代替参数的显式传递
当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。

但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。

这个场景其实使用的比较少,一方面显式传参比较容易理解,另一方面我们可以将多个参数封装为对象去传递。
场景二:全局存储用户信息
在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。

在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)

对于笔者而言,这个场景使用的比较多,当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类(AuthNHolder)存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空, 中间的过程无需在关注如何获取用户信息,只需要使用工具类的get方法即可。

public class AuthNHolder {
	private static final ThreadLocal<Map<String,String>> loginThreadLocal = new ThreadLocal<Map<String,String>>();

	public static void map(Map<String,String> map){
		loginThreadLocal.set(map);
	}
	public static String userId(){
    		return get("userId");
	}
	public static String get(String key){
    		Map<String,String> map = getMap();
    		return map.get(key);
    }
	public static void clear(){
       loginThreadLocal.remove();
	}
	
}

场景三:解决线程安全问题
在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?

在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题

ThreadLocal在设计之初就是为解决并发问题而提供一种方案,每个线程维护一份自己的数据,达到线程隔离的效果。
慎用的场景
1.线程池中线程调用使用ThreadLocal 由于线程池中对线程管理都是採用线程复用的方法。在线程池中线程非常难结束甚至于永远不会结束。这将意味着线程持续的时间将不可预測,甚至与JVM的生命周期一致

2.异步程序中,ThreadLocal的參数传递是不靠谱的, 由于线程将请求发送后。就不再等待远程返回结果继续向下运行了,真正的返回结果得到后,处理的线程可能是其他的线程。Java8中的并发流也要考虑这种情况

3.使用完ThreadLocal ,最好手动调用 remove() 方法,防止出现内存溢出,因为中使用的key为ThreadLocal的弱引用, 如果ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,但是如果value是强引用,不会被清理, 这样一来就会出现 key 为 null 的 value。


  1. 马士兵老师 讲详解强软弱虚 ThreadLocal与内存泄漏 ↩︎

  2. java基础教程由浅入深全面解析threadlocal ↩︎

本文的参考

艾欧尼亚,昂扬不灭,为了更美好的明天而战(#^.^#)
原文地址:https://www.cnblogs.com/lovelywcc/p/14022520.html