【Java面试谈一谈你对ThreadLocal的理解

2023年 8月 13日 56.3k 0

@

文章导航


在多线程情况下,对于一个共享变量或者资源对象进行读或者写操作时,就必须考虑线程安全问题。而ThreadLocal采用的是完全相反的方式来解决线程安全问题。他实现了对资源对象的线程隔离,让每个线程各自使用各自的资源对象,避免争用引发的线程安全问题。ThreadLocal同时实现了线程内的资源共享。 例如方法1对ThreadLocal中的变量进行了设置,那么方法2中只要是同一个线程,那么他也能访问到线程1在ThreadLocal中设置的变量。

ThreadLocal原理

package com.example.scheduledlovetoobject.threadPoolTest;

import lombok.SneakyThrows;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * @author: Zhangjinbiao
 * @Date: 2022/12/7 10:33
 * @Connection: qq460219753 wx15377920718
 * Description:
 * Version: 1.0.0
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        test1();
        test2();
    }

    public static void test1() {
        for (int i = 0; i  {
                System.out.println(Utils.getConnection());
            }, "t" + i).start();
        }
    }

    public static void test2() {
        for (int i = 0; i  {
                System.out.println(Thread.currentThread().getName() + Utils.getConnection());
                System.out.println(Thread.currentThread().getName() + Utils.getConnection());
                System.out.println(Thread.currentThread().getName() + Utils.getConnection());
            }, "t" + i).start();
        }
    }

    static class Utils {
        public static final ThreadLocal tl = new ThreadLocal();

        public static Connection getConnection() {
            Connection conn = tl.get();
            if (conn == null) {
                conn = innerGetConnection();
                tl.set(conn);
            }
            return conn;
        }

        private static Connection innerGetConnection() {
            try {
                return DriverManager.getConnection("jdbc:mysql://localhost:3306/yoshino?useSSL=false", "root", "root");
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

在这里插入图片描述

在这里插入图片描述

大致设计

上面的代码很容易就看懂了,问题在于,ThreadLocal是怎么做到不同的线程存放不同的对象,而同一个线程能取出同样的对象的呢?
其实当你看到它使用的是set和get方法的时候,你就应该大致能猜到,他的底层应该有一个Map结构来存储线程以及这个线程中的资源,当然,这是JDK早期的设计了,下面是早期的设计
在这里插入图片描述
在JDK8中,ThreadLocal的结构已经改变了。
在这里插入图片描述
在这里插入图片描述
可以发现,我们并不是在ThreadLocal中存储我们的资源,而是在每一个线程中存储,由于每一个线程对象都有一个ThreadLocalMap的局部变量,因此每一个线程之间的ThreadLocalMap都是互相隔离的,所以同一个线程能访问到同一个ThreadLocalMap对象中的数据。我们使用ThreadLocal的set方法的时候其实是向Thread中的ThreadLocalMap设置值。那么下次我们调用ThreadLocal的get方法的时候,他其实会先获取当前线程对象,然后使用这个线程对象去访问ThreadLocalMap中的数据。

只有在你第一次使用这个Map集合的时候,他才会创建,也就是它使用的是懒加载。

因此ThreadLocal的原理是, 每一个线程对象中都有一个ThreadLocalMap类型的成员变量,用来存储资源对象。这个Map的key是ThreadLocal对象,value才是真正要存储的资源。
具体的过程是这样的:
( 1 ) 每个Thread线程内部都有一个Map (ThreadLocalMap)
( 2 ) Map里面存储ThreadLocal对象( key )和线程的变量副本 ( value )
( 3 ) Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
( 4 )对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

那么现在的设计的好处在哪里呢?

  • 每个Map存储的Entry变少,在早期的设计中key为Thread对象,而我们知道Thread对象的个数是很多的,而现在的设计key为ThreadLocal,我们一般设定ThreadLocal为static,所以能保证ThreadLocalMap存储的键值对更少。
  • 现在在Thread销毁之后,ThreadLocalMap也会自动销毁,减少对内存的时候。而早期的设计,即使线程消失了,ThreadLocalMap依旧存在,还是要维护它。

方法介绍

  • 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
  • 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
  • 调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值

set方法
执行流程:
首先获取当前线程,并根据当前线程获取一个Map
如果获取的Map不为空,则将次数设置到Map中
如果Map为空,则给该线程创建Map,并设置初始值

    public void set(T value) {
    //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取此线程对象中的Map对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //已经创建那么直接放入此entry
            map.set(this, value);
        } else {
        //不存在则创建 并将当前线程t和value值作为第一个entry存放到Map中
            createMap(t, value);
        }
    }
	//返回当前线程对应的Map
	 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    } 
    //这里的this是调用createMap的ThreadLocal对象
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

get方法
执行流程:
A.首先获取当前线程,根据当前线程获取一个Map
B.如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D
C.如果e不为null,则返回e.value,否则转到D
D.Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //以当前ThreadLocal为key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //初始化,有两种情况会执行这个代码
        //map不存在,表示此线程没有维护ThreadLocalMap对象
        //map存在,但是没有查询到与当前ThreadLocal关联的entry
        return setInitialValue();
    }
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
 	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 T setInitialValue() {
	 //调用initialValue获取初始化的值
	 //子类可以重写,不重写返回null
        T value = initialValue();
        //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取当前线程对象维护的Map
        ThreadLocalMap map = getMap(t);
        if (map != null) { //map存在设置实体entry
            map.set(this, value);
        } else {
        //1:当前线程不存在ThreadLocalMap对象
        //2:则调用这个方法创建Map
        //并且将t和value设置进去
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal) this);
        }
        return value;
    }

remove方法
执行流程:
首先获取当前线程,并且根据该线程获取一个Map
如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

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

initialValue方法
( 1 )这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
( 2 )这个方法缺省实现直接返回一个null。
( 3 )如果想要一个除null之外的初始值,可以重写此方法。(备注∶该方法是一个protected的方法显然是为了让子类覆盖而设计的)

返回当前线程对应的ThreadLocal的初始值
此方法的第一次调用发生在,当线程通过get方法访问此钱程的
ThreadLocal值时除非线程先调用了set方法,在这种情况下,
initialvalue 才不会被这个线程调用。通常情况下,每个线程最多调用一次这个方法。
这个方法仅仅简单的返回nu1l {@code nu11};
如果程序员想ThreadLoca1线程局部变量有一个除nu11以外的初始值,
必须通过子类继承{@code ThreadLocaT]的方式去重写此方法
通常,可以通过匿名内部类的方式实现

  protected T initialValue() {
        return null;
    }

底层理解

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

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。|
另外,Entry继承WeakReference,也就是key ( ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

 static class ThreadLocalMap {
        static class Entry extends WeakReference k = e.get();
				//如果已经有这个key则直接覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
				//key为null,但是值补位null,说明ThreadLocal对象已经被回收了
				//当前数组中的Entry是一个陈旧的stale元素
                if (k == null) {
                //用新元素替换旧的,这个方法做了很多垃圾回收操作,防止内存泄漏
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			//key不存在且没有旧元素,直接在空元素位置创建一个新的Entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //cleanSomeSlots用于清除e.get()==null的元素
            //这种数据key关联的对象已经被回收,所以此时可以把对应的位置设置为null
            //如果没有清除任何entry,表示当前使用量达到了负载因子,2/3,
            //那么就rehash(会执行一次全表的扫描操作)
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
    	//线性探测    
	 private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

代码执行流程:
A.首先还是根据key计算出索引 i ,然后查找 i 位置上的Entry ,
B.若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
C.若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
D.不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是否>=threshold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。

重点分析:
ThreadLocalMap使用线性探测法来解决哈希冲突的。
该方法一次探测下一地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
按照上面的描述,可以把Entry[] table看成一个环形数组。

get/set/remove方法的一些细节

对于get方法,如果get的时候发现key是null,也就是此时ThreadLocal已经被回收了,那么此时就会把对应的value也设定为空来释放空间,但是会把key再次设定为当前ThreadLocal。

对于set方法,如果发现set的时候得到的索引处的key为null,那么说明已经被回收掉了,那么此时会把数据放入进去,然后使用一种启发式扫描,他会扫描邻近的key是否为null,如果为null就进行对value的清理。相比全表扫描效率更高。启发次数与元素个数,是否发现null key有关。

对于get和set方法,他们只有在没有引用key(null key)的时候才会触发垃圾回收。 但是我们使用ThreadLocal一般都是静态的,所以这个ThreadLocal一般不会被回收,也就是他是一个强引用,所以一般不会出现null key的情况。

因此一般我们都使用remove方法,在某一个key不使用的时候,手动使用remove方法来设定其为null。

总结

每个线程都可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程.
避免了线程访问实例变量发生安全问题. 同时我们也能得出下面的结论:
(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;
(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;
(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;
(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value
值;
(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的
ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);
(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;
(7)线程死亡时,线程局部变量会自动回收内存;
(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的
ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:
ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;
(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中
Entry的回收;

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换
时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,
因此可以同时访问而互不影响。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论