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 [email protected] #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 [email protected] #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 
* [email protected] #get read} by the current thread, its value will be 
* reinitialized by invoking its [email protected] #initialValue} method, 
* unless its value is [email protected] #set set} by the current thread 
* in the interim.  This may result in multiple invocations of the 
* [email protected] 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(),这样可以最大程度保证不会发生内存泄漏。

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/19353.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论