Java面经之ThreadLocal底层原理

共 9717字,需浏览 20分钟

 ·

2021-03-26 21:49

点击蓝字关注我们,获取更多面经







ThreadLocal底层原理







1 概述

ThreadLocal是java.lang包中的一个类,用来实现变量的线程封闭性,即只有当前线程可以操作该变量,通过把一个变量存在当前线程的一个Map容器中来实现。当然,这样解释很抽象,对于一些人来说难以理解,这里我先介绍一个《java并发编程实战》中描述的jdbc应用场景,来让你知道ThreadLocal到底有什么用。


2 ThreadLocal的意义

我们知道,当在普通方法中创建一个变量类,若没有特别在方法区域外留有该类的引用,当方法结束后在其它地方不能够再使用这个类。当我们在其它地方还要用到方法中创建的对象时,我们通常会用一个全局变量指向这个对象,这样在整个项目中都能再访问这个对象了。但想想,在多线程情况下,每个线程访问该方法都会创建一个全局对象,在高并发下那我们岂不是要创建成千上万个全局变量来存?若该对象还带有每个线程特有的参数,那就要保证每个线程在之后能调用自己的创建的对象,一般情况下是很难进行管理的。而ThreadLocal就做到了既能让对象在其它地方被创建线程访问,也省去了自己管理全局对象的麻烦。


一般来讲,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法(save,get等)时都要传递一个Connection对象。由于Jdbc连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的,比如线程一刚获取全局的Connection,准备进行数据库操作,但线程二却执行了Connection.close()。


这种情况下我们可能会取消Connection这个全局变量,在每次要进行数据库相关操作时直接new一个Connection对象进行连接,而这又会导致一个线程执行多次数据库操作时要new多个connection对象,加大系统的负担。这时就会想能不能有这样一种方法,既不让Connection成为全局变量来保证线程安全,又可以实现全局Connection带来的“一次连接”式的便利?ThreadLocal就是做这件事的。


3 简要应用

下面第一行代码new了一个静态的ThreadLocal<Connection>。以后只要让每个线程在进行jdbc操作前都执行第二行代码,就会把一个创建的Connection对象存放到当前线程的map容器中(ThreadLocalMap,见下文介绍),后面该线程要进行数据库操作时只要执行第三行代码,就能拿到这个引用进行连接,不用每次都new一个新的。


private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>(); connectionHolder.set(DriverManager.getConnection(DB_URL)); connectionHolder.get();


你可能会问只是一句简单的connectionHolder.get()代码,而且ThreadLocal对象只有一个,那怎么能准确的拿到当前线程中的Connection呢?答案就是:这个connection是存在当前线程(一个Thread)中的,不是ThreadLocal中。表面上调用的ThreadLocal.get(),实际上是在当前线程对象的Map容器中进行设置和查找。不过当前线程的map容器中可能会存多个ThreadLocal的值,所以Map中的key就是ThreadLoca对象,值就是connection,来进行区分。还不懂的话见下面的代码解析,很简单。


4 源代码分析

ThreadLocalMap是ThreadLocal类中的静态内部类,可以看成一个map容器。Thread类中有一个全局变量threadLocals 就是ThreadLocalMap类型,它才是ThreadLocal存储数据的真正容器。


public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained     * by the ThreadLocal class. */    ThreadLocal.ThreadLocalMap threadLocals = null;}


4.1 ThreadLocalMap底层结构

在看ThreadLocal的底层代码之前,我们先来看看ThreadLocalMap的操作,这样有利于接下来更好地理解。

static class ThreadLocalMap {    private static final int INITIAL_CAPACITY = 16;     private int size = 0;     private int threshold;     private Entry[] table;    //Thread的map容器     // 构造方法,在第一次调用ThreadLocal.set()时会new一个ThreadLocalMap    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {        table = new Entry[INITIAL_CAPACITY];        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);        table[i] = new Entry(firstKey, firstValue);        size = 1;        setThreshold(INITIAL_CAPACITY);    }     static class Entry extends WeakReference<ThreadLocal<?>> {            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }     }}


可能会好奇,table中的Entry只有一个value,并没有像Hashmap中那样是键值对啊,为什么叫它map容器呢?其实Entry是有key的,key就是构造Entry时传入的ThreadLocal参数,不过指向ThreadLocal的成员变量是从Reference类中继承的。从Entry的构造方法中可看到传入了一个ThreadLocal对象,它就是key,我们追踪super(k),最后发现在Entry的父类Reference中,传给了成员变量referent。至于这个referent是怎么用到容器的操作上,下面马上会看到。

public abstract class Reference<T> {    private T referent;           // key。注入了传入的ThreadLocal,被Entry继承      //ThreadLocal最终传给了参数referent    Reference(T referent, ReferenceQueue<? super T> queue) {        this.referent = referent;        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;    }     // 获取“key”    public T get() {        return this.referent;    }}


4.2 ThreadLocalMap的set方法

set方法有点像HashMap的put方法,HashMap中也是用一个Node[] table来存储数据,Node里面包含了键值对,而这里Entry也是<referent,value>键值对。


首先根据根据key的hashcode计算出插入的Entry在table中的位置,然后从计算出的位置向前循环遍历,直到找到的Entry为null或key为null(被GC了),就把Entry插入到该位置。在遍历的过程中会判断遍历元素与插入元素的是否是相同的Entry,规则是比较Entry的key即referent。可以看到下面通过Entry.get()来获取key,而get()方法继承自Reference父类中,就是返回referent属性。

 

   private void set(ThreadLocal<?> key, Object value) {            Entry[] tab = table;            int len = tab.length;             //根据ThreadLocal的hash值计算应该从table中哪个位置开始(跟HashMap一样)            int i = key.threadLocalHashCode & (len-1);             //遍历table中的每一个Entry            for (Entry e = tab[i];                 e != null;                 e = tab[i = nextIndex(i, len)]) {                ThreadLocal<?> k = e.get();                 //如果找到相同的key,覆盖并返回                if (k == key) {                    e.value = value;                    return;                }                 //如果发现了空key为空,即ThreadLocal对象被GC了,就把数据放到该位置                if (k == null) {                    replaceStaleEntry(key, value, i);                    return;                }            }             //当返现e==null时,执行下面代码,new再检测是否扩容            tab[i] = new Entry(key, value);            int sz = ++size;            if (!cleanSomeSlots(i, sz) && sz >= threshold)                rehash();        }


4.3 ThreadLocalMap的getEntry方法

首先根据key找到table中的一个位置,如果该位置上的元素不为空且key等于传入的key,则直接返回该位置上的Entry;若位置上的元素为null或key为null,则返回null;否则向前循环遍历table直到发现key相等的Entry返回,或遍历到null直接返回null。

 

      private Entry getEntry(ThreadLocal<?> key) {            int i = key.threadLocalHashCode & (table.length - 1);            Entry e = table[i];            if (e != null && e.get() == key)                return e;            else                return getEntryAfterMiss(key, i, e);        }         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {            Entry[] tab = table;            int len = tab.length;             while (e != null) {                ThreadLocal<?> k = e.get();                if (k == key)                    return e;                if (k == null)                    expungeStaleEntry(i);                else                    i = nextIndex(i, len);                e = tab[i];            }            return null;        }


4.4 ThreadLocal的set方法

先拿到当前线程的ThreadLocalMap容器,若线程的容器为null(一次都没有使用过)则初始化,创建一个新的ThreadLocalMap同时将key和value传入,插入到指定位置,ThreadLocalMap的构造方法参见4.1中的代码。若不为空则直接调用该容器的set方法插入键值对。两种方法都是把自己当key传入。

    public void set(T value) {        Thread t = Thread.currentThread();    //获取当前线程对象        ThreadLocalMap map = getMap(t);       //拿到t的map容器        if (map != null)                    map.set(this, value);            //map不空,根据key(调用set方法的ThreadLocal对象)设置值        else            createMap(t, value);             //map为空则创建一个map(第一次插入,有点怪怪的)    }     // 从当前线程中获取ThreadLocalMap     ThreadLocalMap getMap(Thread t) {        return t.threadLocals;    }     // 初始化当前线程的ThreadLocalMap容器    void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);    }


4.5 ThreadLocal的get方法

很简单,直接调用的ThreadLocalMap的get方法,把自己作为key传入getEntry方法中。若容器为null,则调用setInitialValue方法。

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


4.6ThreadLocal的setInitialValue方法

会返回一个初始值,由initialValue返回。可以看到initialValue方法只是返回了一个null,如果我们不重写它的话。同时再加一个判断,若容器为null,则跟set方法一样进行初始化,不过这里的value不是传入的,而是initialValue,默认为null。

private T setInitialValue() {        T value = initialValue();        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);        return value;    } protected T initialValue() {        return null;    }


5 案例理解

下面创建了5个线程,在每个线程执行期间调用了两个ThreadLocal对象的set方法,然后打印get方法的返回值。显然同一个ThreadLocal在不同线程中调用get和set方法是能区分线程的。

public class Test1 {     public static void main(String[] args) {        for (int i=0;i<5;++i){            new MyThread().start();        }    }} class MyThread extends Thread{     private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();    private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<String>();     @Override    public void run(){        threadLocal1.set(getName()+":调用了threadLocal1的set方法");        threadLocal2.set(getName()+":调用了threadLocal2的set方法");        System.out.println(threadLocal1.get());        System.out.println(threadLocal2.get());    }} /*---------------打印结果-----------------Thread-0:调用了threadLocal1的set方法Thread-0:调用了threadLocal2的set方法Thread-2:调用了threadLocal1的set方法Thread-2:调用了threadLocal2的set方法Thread-3:调用了threadLocal1的set方法Thread-4:调用了threadLocal1的set方法Thread-3:调用了threadLocal2的set方法Thread-4:调用了threadLocal2的set方法Thread-1:调用了threadLocal1的set方法Thread-1:调用了threadLocal2的set方法*/


5 注意:脏读、内存泄漏

5.1 脏读

当使用线程池的时候,由于工作线程是循环利用的,上一个任务线程通过ThreadLocal在工作线程中存入了数据,下一个任务线程被该工作线程执行时,依然能够读到上一个任务线程存入的数据,也就是读到了脏数据。


解决办法就是在一个线程执行结束后将数据通过ThreadLocal.remove()删除掉。


5.2 内存泄漏

由于ThreadLocalMap是以弱引用的方式引用着ThreadLocal,换句话说,就是ThreadLocal是被ThreadLocalMap以弱引用的方式关联着,因此如果ThreadLocal没有被ThreadLocalMap以外的对象引用(如手动令ThreadLocal = null或下面红色字体),则在下一次GC的时候,ThreadLocal实例就会被回收,那么此时ThreadLocalMap里的一组KV的K就是null了,因此在没有额外操作的情况下,此处的V便不会被外部访问到,而且只要Thread实例一直存在,Thread实例就强引用着ThreadLocalMap,因此ThreadLocalMap就不会被回收,那么这里K为null的V就一直占用着内存。


综上,发生内存泄露的条件是


ThreadLocal实例没有被外部强引用,比如我们假设在提交到线程池的task中实例化的ThreadLocal对象,当task结束时,ThreadLocal的强引用也就结束了

ThreadLocal实例被回收,但是在ThreadLocalMap中的V没有被任何清理机制有效清理

当前Thread实例一直存在,则会一直强引用着ThreadLocalMap,也就是说ThreadLocalMap也不会被GC

也就是说,如果Thread实例还在,但是ThreadLocal实例却不在了,则ThreadLocal实例作为key所关联的value无法被外部访问,却还被强引用着,因此出现了内存泄露。


解决办法就是创建ThreadLocal时将ThreadLocal变量设置成全局static类型,当需要存储线程私有的数据时,通过全局的ThreadLocal变量来存,不要在普通方法体中定义局部的ThreadLocal变量来存数据;不要手动将ThreadLocal引用指向null。









更多面经





360-测试开发面经(一)


百度-测试开发面经(一)


字节跳动-测试开发面经(一)


    扫描二维码

   获取更多面经

  扶摇就业  


浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报