ThreadLocal的使用+源码分析

ThreadLocal介绍

ThreadLocal,意为线程的局部变量,是在线程本地存了一个变量的副本。只有当前线程可以访问。它的目的是为了在线程之间共享某个变量,而不会发生冲突。就好比有100个人需要填一张表,按照同步机制的做法就是保证这支笔同一时间只有一个人使用,而ThreadLocal则是采用另一种方式:每个人都发了一支笔。相比同步机制,这是一种“空间换时间”的做法。

在同步机制中,使用了synchronized和volatile等关键字实现了这一个目的,但实际上这样的实现是比较耗时的,相比ThreadLocal,这样做属于“时间换空间”。

ThreadLocal使用&使用场景

ThreadLocal的使用场景:
每条线程都需要存取一个同名变量,但每条线程中该变量的值不完全相同。

使用ThreadLocal使用主要有以下方法:

  • public void set(T value); :为当前线程设置一个value值,即只有当前线程可以访问的值
  • public T get();:获取set的值
  • public void remove();:删除线程当前threadlocal的值,无法恢复

这里假设要使用一个SimpleDateFormat对象来实现字符串类型日期的格式化,但是由于SimpleDateFormat.parse()方法并不是线程安全的,所以需要保证同一时间只有一个线程使用SimpleDateFormat,可以使用synchronized,也可以使用ThreadLocal。这里演示ThreadLocal的方式。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by makersy on 2019
 */

public class ThreadLocalDomo {

//    static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();

    public static class ParseDate implements Runnable {

        int i = 0;

        public ParseDate(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            try {
                if (tl.get() == null) {
                    tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                }
                Date t = tl.get().parse("2019-01-01 19:10:" + i % 60);
                System.out.println(i + ":" + t);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; ++i) {
            es.submit(new ParseDate(i));
        }
        es.shutdown();
    }
}


这里首先判断ThreadLocal有没有被设置,没有就进行set,然后使用Thread的本地变量来进行日期的格式化。

源码分析

内部结构

每个线程内部都定义了一个ThreadLocalMap,初始为null。ThreadLocalMap是ThreadLocal的静态内部类,内部定义了一个类似map的键值对结构,称为Entry。

对ThreadLocal的弱引用其对应的对象以键值对的形式存为Entry,然后存放在Entry数组里,注意这个Entry数组是定义在ThreadLocalMap类中。

之所以要定义成Entry数组,是因为一个线程可能会设置多个ThreadLocal,因此用数组存放。

Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

此处Entry的是使用了WeakReference,也就是对ThreadLocal的弱引用,注意它并不是ThreadLocal本身,只是一个引用。为什么要继承WeakReference,这个后面详细说明。

线程、ThreadLocal、ThreadLocalMap、Entry之间是个什么关系,我花了好久才理解,这里我画了一张图来表示:
在这里插入图片描述

ThreadLocal总体结构:
在这里插入图片描述

ThreadLocal的set、get、remove方法

set

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);  //当前线程的ThreadLocalMap 
    if (map != null)  //存在ThreadLocalMap
        map.set(this, value);  //在entry数组中添加或者覆盖设置的值
    else
        createMap(t, value);  //不存在map,新建并赋值
}

get

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {  //如果当前线程的ThreadLocalMap存在
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {  //如果map中存在调用此方法的ThreadLocal对象
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;  //正常返回
        }
    }
    return setInitialValue();  //上述任意一个条件不满足的话,就去设置Entry,或者创建ThreadLocalMap
}

remove


/**
* Removes the current thread's value for this thread-local
* variable.  If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim.  This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);  //计算key应在数组中的位置

	//线性探测再散列,直到找到相同的key值(覆盖),或对应位置key为null(清理并设置)
    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();
}

注释里解释流程比较方便。

为什么key要使用弱引用

使用弱引用是有一个GC的考虑。首先解释下什么是弱引用:

  • 当对某个对象的引用只有弱引用时,一旦JVM进行GC时就会收回此对象。

为什么不使用强引用呢?需要结合上面的引用结构图来说明。

在这里插入图片描述
当线程不再使用某个ThreadLocal(假设叫tla),即不持有对tla的强引用的时候,GC就会自动回收tla,因为此时tla剩余的引用是Entry的key持有的弱引用。此时,如果key是强引用就不会回收,造成了内存浪费。

但是由于value值是强引用,万一value值也不需要了,但是JVM又没有回收,就会造成内存泄漏。这里JDK的做法是:当系统进行ThreadLocalMap清理时,会自动将key值为null的 “垃圾数据” 回收。

当对ThreadLocal对象使用set、get、remove方法时,都会进行清理。

因此,移除ThreadLocal,不建议采用 tla = null这样的做法,正确的做法是tla.remove(),这样可以最大程度保证不会发生内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值