上周我侥幸通过美团一面( 点击查看一面过程 ),岗位是java后端开发工程师。美团面试官给我进行了二面。面试过程中他问了ThreadLocal原理(上次问线程池,这次问ThreadLocal,美团爸爸这么喜欢线程安全机制么),今天详细讲一讲 ThreadLocal原理 。
ThreadLocal是线程的内部存储类,可以在指定线程内存储数据。只有指定线程可以得到存储数据。
/** * This class provides thread-local variables. These variables differ from * their normal counterparts in that each thread that accesses one (via its * {@code get} or {@code set} method) has its own, independently initialized * copy of the variable. {@code ThreadLocal} instances are typically private * static fields in classes that wish to associate state with a thread (e.g., * a user ID or Transaction ID). */ 复制代码
每个线程都有一个ThreadLocalMap的实例对象,并且通过ThreadLocal管理ThreadLocalMap。
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; 复制代码
每个新线程都会实例化为一个ThreadLocalMap并且赋值给成员变量ThreadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
当某些数据是以线程为作用域并且不同线程有不同数据副本时,考虑ThreadLocal。
无状态,副本变量独立后不影响业务逻辑的高并发场景。
如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决。
set()是调用ThreadLocalMap的set()实现的
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } //getMap方法 ThreadLocalMap getMap(Thread t) { //thred中维护了一个ThreadLocalMap return t.threadLocals; } //createMap void createMap(Thread t, T firstValue) { //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals t.threadLocals = new ThreadLocalMap(this, firstValue); } 复制代码
ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标是value存储的对应位置。
ThreadLocalMaps是延迟构造的,因此只有在至少要放置一个条目时才创建。
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); } 复制代码
ThreadLocalMap初始化时创建了默认长度是16的Entry数组。通过hashCode与length位运算确定索引值i。
每个Thread都有一个ThreadLocalMap类型。相当于每个线程Thread都有一个Entry型的数组table。而一切读取过程都是通过操作这个数组table进行的。
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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)]) { ThreadLocal<?> k = e.get(); //如果存在key则覆盖 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(); } 复制代码
将threadLocalHashCode与长度进行 位运算 得到索引。
threadLocalHashCode的代码如下:
private final int threadLocalHashCode = nextHashCode(); /** * The next hash code to be given out. Updated atomically. Starts at * zero. */ private static AtomicInteger nextHashCode = new AtomicInteger(); /** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */ private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } 复制代码
由于是static变量,threadLocalHashCode在每次加载threadLocal类时会重新初始化,同时会自增一次,增加HASH_INCREMENT( 斐波那契散列乘数 ,通过该数散列出来的结果会比较均匀)。
static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。
对于一个ThreadLocal来讲,他的索引值i是确定的。对于不同线程,同一个threadlocal对应的是不同table的同一下标,即是table[i],不同线程之间的table是相互独立的。
计算索引,直接取出
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(); } 复制代码
/** * Remove the entry for key. */ 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; } } } 复制代码
线程隔离特性,只有在线程内才能获取到对应的值,线程外不能访问。
Synchronized是通过线程等待,牺牲时间来解决访问冲突
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突
存在内存泄露问题, 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
import java.util.concurrent.atomic.AtomicInteger; /** * <h3>Exper1</h3> * <p>ThreadLocalId</p> * * @author : cxc * @date : 2020-04-01 23:48 **/ public class ThreadLocalId { // Atomic integer containing the next thread ID to be assigned private static final AtomicInteger nextId = new AtomicInteger(0); // Thread local variable containing each thread's ID private static final ThreadLocal <Integer> threadId = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return nextId.getAndIncrement(); } }; // Returns the current thread's unique ID, assigning it if necessary public static int get() { return threadId.get(); } public static void remove() { threadId.remove(); } } /** * <h3>Exper1</h3> * <p></p> * * @author : cxc * @date : 2020-04-02 00:07 **/ public class ThreadLocalMain { private static void incrementSameThreadId(){ try{ for(int i=0;i<5;i++){ System.out.println(Thread.currentThread() +"_"+i+",threadId:"+ ThreadLocalId.get()); } }finally { ThreadLocalId.remove(); } } public static void main(String[] args) { incrementSameThreadId(); new Thread(new Runnable() { @Override public void run() { incrementSameThreadId(); } }).start(); new Thread(new Runnable() { @Override public void run() { incrementSameThreadId(); } }).start(); } } 复制代码
咱们玩归玩,闹归闹,别拿面试开玩笑。
ThreadLocal的原理在面试中几乎被问烂了。 Thread的私有数据是存储在ThreadLocalMap,通过ThreadLoacl进行管理。 要了解ThreadLocal的原理,最好多阅读几遍源码,尤其是 ThreadLocalMap的源码部分 。大家面试前要把知识点记牢。
文章持续更新,可以微信搜索「 云璈公子 」阅读,回复【资料】【面试】【简历】有我准备的一线大厂面试资料和简历模板,同时我的GitHub github.com/1170300826/… 有互联网一线大厂面试指南。