【JavaSE】浅谈TreadLocal,TreadLocal的常用方法set()、get()、remove()源码分析

论坛 期权论坛 脚本     
匿名网站用户   2020-12-20 12:40   11   0

1.TreadLocal是什么

先来看一段代码:

public class Test {
    private static String commStr;
    private static ThreadLocal<String> threadStr = new ThreadLocal<String>();
    public static void main(String[] args) {
        commStr = "main";
        threadStr.set("main");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                commStr = "thread";
                threadStr.set("thread");
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(commStr);    //thread
        System.out.println(threadStr.get());   //main
    }
}

在主类中创建了两个静态变量,一个String,另一个TreadLocal类创建的对象,虽然都是静态的,但是输出结果却不是两个thread(正确输出结果在输出语句后面注释),说明普通的静态变量,主线程和各个线程对它产生的改变都可以对其他线程产生影响。而TreadLocal类创建的对象对每个线程来说是独立的,为每个线程创建了一个副本变量。用这种方法可以解决线程之间对某变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个变量进行了修改的。

2.TreadLocal的主要方法

public T get()
public void set(T value)
public void remove()
protected T initialValue()
  • set(T value)方法用于设置副本变量的值
  • get()方法用于获取当前线程副本变量的值
  • initialValue()为当前线程默认的副本变量值。
  • remove()方法移除当前前程的副本变量值。

一、set(T value)方法

先来看一下源码:
在这里插入图片描述

①.getMap(t)方法

要设置副本变量值,就要先获取此线程的ThreadLocalMap对象。
源码如下:

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

可以看到这个方法很简单,只是返回当前线程的threadLocals变量

ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到在源码里面,threadLocals变量的默认值是null。
所以我们可以得知,getMap(t)函数在当前线程第一次使用此方法的时候,返回的是null。

②.createMap()方法

此时我们走到了createMap方法,先看一下源码:

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

可以看到这时给threadLocals进行了赋值,再来看一下ThreadLocalMap(this, firstValue)方法是如何给threadLocals进行赋值的。

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

这是一个有参构造方法,传入的值是当前正在调用set方法的ThreadLocal对象this,还有一个就是要设置的副本变量值。方法内部先创建一个键值对的对象数组,然后传入的当前TreadLocal对象通过hash方法得到需要保存的下标i,然后将这个键值对保存到此下标的数组处,创建成功size赋值为1,并且给出此hash的负载因子。通过这个方法创建一个TreadLocalMap的对象赋值给threadLocals,此时通过threadLocals就可以找到这个键值对数组。

③.map的set方法

如果在第二步已经进行过第一次的createMap方法,此时getMap方法就可以得到非null的map,可以调用map的set方法将新的键值对添加到threadLLocals里面。这里对源码不做赘述。

二、get()方法

源码如下:

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

getMap()方法上面已经说过,如果还没用调用set方法,直接调用get方法,map会得到一个null的值。

①.setInitialValue()方法

当map为空时,此函数会返回setInitialValue()方法的返回值,先看一下源码:

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

这里除了initialValue()方法其他上面都说过了,直接看initialValue()方法是如果得到value副本变量值的。

 protected T initialValue() {
        return null;
    }

恩,它很省事,直接返回一个null。那这样导致setInitialValue()方法给get方法返回的也是一个null。
注意:如果ThreadLocal对象在没有调用set()方法的时候,直接调用get()方法,会得到一个null,很可能产生空指针异常。

②.getEntry(this)方法

如果map不为空,我们自然要取得在当前线程的map(也就是treadLocals)里面所保存的和此TreadLocal对应的键值对,所以传入this,看一下源码:

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

可以看到函数一开始就对此TreadLocal进行hash算法,得到它所对应数组下标i,然后判断此下标处保存的key是否和当前key一样,如果一样说明找到了正确的键值对,直接返回e。如果不一样就调用getEntryAfterMiss(key, i, e)方法,这个方法源码里面是使用线性探测的方法去找对应的key值,如果找不到就返回null。
如果返回了正确的Entry e,就将e的value值作为get的返回值。

三、remove()方法

源码如下:

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

直接调用了ThreadLocalMap的remove方法。

private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

这个方法也是先找到当前ThreadLocal对应的hash下标i,然后通过线性探测的方式去找准确的key值,如果找到了。就用clear()方法将这个key值置为null,方便之后的 expungeStaleEntry(i)方法进行清理,expungeStaleEntry(i)方法不止清理了i位置上的entry,还把i之后的key为null的entry都清理了,并且顺带将一些有哈希冲突的entry给填充回可用的index中。

小总结

到这里ThreadLocal的remove方法也分析完了。remove方法的关键就在于主动断开entry的key的引用链接,这样在后续的expungeStaleEntry方法中,就会将这种key为null的entry给设置为null,方便GC对内存进行回收。

ThreadLocal的set,get和remove方法看下来,除了正常的功能之外,就是多了对key为null的entry的清理工作,方便GC回收这部分占用的内存。expungeStaleEntry就是最核心的清理方法,这也是ThreadLocalMap的一种防范机制,因为ThreadLocalMap的生命周期和线程是一样长的,不采取这种防范机制,是会造成内存泄漏的。如果多定义了几个ThreadLocal对象,并且线程都将占用内存比较大的对象给放到对应的线程中,可能就会造成OOM异常了。

四、 initialValue()方法

这个方法在第二个get()方法那里提到过,主要是当某个TreadLocal还没有进行set方法而直接调用get方法,返回的默认副本变量值,初始是null。可以覆写这个方法,改变初始值。

3.ThreadLocal的应用场景

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:1136255
帖子:227251
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP