前言

上周我侥幸通过美团一面(点击查看一面过程),岗位是java后端开发工程师。美团面试官给我进行了二面。面试过程中他问了ThreadLocal原理(上次问线程池,这次问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解决。

get()与set()

set()是调用ThreadLocalMap的set()实现的

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}
//getMap方法
ThreadLocalMap getMap(Thread t) {//thred中维护了一个ThreadLocalMapreturn t.threadLocals;}//createMap
void createMap(Thread t, T firstValue) {//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocalst.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap

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进行的。

set() 方法

        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是相互独立的。

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

remove() 方法

/*** 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()方法,清除数据。

Demo程序

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 assignedprivate static final AtomicInteger nextId = new AtomicInteger(0);// Thread local variable containing each thread's IDprivate static final ThreadLocal <Integer> threadId =new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue() {return nextId.getAndIncrement();}};// Returns the current thread's unique ID, assigning it if necessarypublic 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() {@Overridepublic void run() {incrementSameThreadId();}}).start();new Thread(new Runnable() {@Overridepublic void run() {incrementSameThreadId();}}).start();}
}

总结

咱们玩归玩,闹归闹,别拿面试开玩笑。

ThreadLocal的原理在面试中几乎被问烂了。Thread的私有数据是存储在ThreadLocalMap,通过ThreadLoacl进行管理。要了解ThreadLocal的原理,最好多阅读几遍源码,尤其是ThreadLocalMap的源码部分。大家面试前要把知识点记牢。

文章持续更新,可以微信搜索「 云璈公子 」阅读,回复【资料】【面试】【简历】有我准备的一线大厂面试资料和简历模板,同时我的GitHub https://github.com/1170300826/JavaInterview 有互联网一线大厂面试指南。

美团面试,问了ThreadLocal原理,这个回答让我通过了相关推荐

  1. 面试问到 Handler,这样回答让面试官大哥心服口服

    主要说一下Handler的作用,使用,源码分析,工作原理. Handler的作用: 当我们需要在子线程处理耗时的操作(例如访问网络,数据库的操作),而当耗时的操作完成后,需要更新UI,这就需要使用Ha ...

  2. 美团面试,被拷打了一小时....

    刚从美团走出来,被拷打了一小时-越想越觉得可惜,回想面试经过,好好总结了几个点,发现面试没过的主要原因是在几个关键的问题没有给到面试官想要的答案.从而失去了这次宝贵的机会. 根据你的工作经历,说说你对 ...

  3. 程序员离职原因的最佳回答_程序员面试被问离职原因,如实回答不适应996,面试官答复尴尬了...

    跳槽面试是一个技术活,特别是面试环节,需要在简短的时间里回答大量问题.如果稍不小心,可能就会导致哭笑不得的结局. 最近,有个程序员就在网上吐槽说,自己原单位加班很严重,长期996(即早上9嗲上班,晚上 ...

  4. cms是什么意思啊_美团面试官问我:ZGC 的 Z 是什么意思?

    本文的阅读有一定的门槛,请先了解 GC 的基本知识. 现代垃圾收集器的演进大部分都是往减少停顿方向发展. 像 CMS 就是分离出一些阶段使得应用线程可以和垃圾回收线程并发,当然还有利用回收线程的并行来 ...

  5. 面试被问到CAS原理,触及知识盲区,脸都绿了!

    文章目录 前提知识掌握 什么是CAS? CAS原理 CAS带来的问题 CAS原理总结,面试问到了到底该怎么说? 尾言 前提知识掌握 想要了解 CAS 底层原理,那么我们先来了解一下 java.uit. ...

  6. 面试问为什么跳槽,该怎么回答?

    在面试过程中,面试官往往会问到候选人的跳槽经历,并询问原因.对于跳槽频繁的应聘者来说,这可能是一个尤其敏感的问题.面试问为什么跳槽,该怎么回答?小编整理了如下的内容供大家做参考. 首先,应聘者在面试时 ...

  7. 面试被问业余爱好该怎么回答?

    面试的时候,一系列富有挑战性的问题之后,面试官通常还会附加一个看似很随性的问题"平常有什么业余爱好啊?" 如果你真的以为他要和你聊天,就真的天真了-- 面试官究竟要问什么? 首先, ...

  8. 来之不易的美团面试,结果居然挂了...(附面试答案)

    转载自  来之不易的美团面试,结果居然挂了...(附面试答案) 一面 自我介绍 答:自我介绍是面试中唯一的自己主动介绍自己的环节,一定要好好把握好,你数据结构学的号可以手撕一个红黑树你就说我数据结构掌 ...

  9. 计算机面试的时候写过的代码,程序员悲催瞬间:来之不易的美团面试,我尽然挂了(还原真实场景)...

    一面 1.自我介绍 答:自我介绍是面试中唯一的自己主动介绍自己的环节,一定要好好把握好,你数据结构学的号可以手撕一个红黑树你就说我数据结构掌握地很好,反正就是要把自己的优势凸显出来,比如自己对于jav ...

最新文章

  1. 软件项目验收的准备工作
  2. JavaME:Google静态地图API
  3. WinCE系统字体的设置
  4. date js 半年_js Date 日期使用上的一个坑
  5. ubuntu分辨率设置
  6. 利用TreeView实现C#工具箱效果
  7. Visual Studio Ultimate 2012 激活密钥
  8. Reflex WMS入门系列七:收货(Receipt)
  9. 实用工具系列 - FileZilla安装下载与使用
  10. Python中常见的__init__.py是什么意思?详解Python import的方式和原理
  11. excel 冻结多列窗口
  12. Android webview 清除历史访问记录
  13. javascript tree
  14. 7-4 BCD解密 (10分)
  15. android+主流品牌手机,9月Android手机品牌及机型排行报告发布
  16. 计算汉字笔画数的脚本
  17. Android:技术在线面试还是屡次撞板,过来人告诉你不及时
  18. 【单片机】心形流水灯の制作指南(保姆级)
  19. SAP HANA数据库备份失败解决办法
  20. 【一、bootstrap框架前端注册登录页面】

热门文章

  1. MySQL错误:ERROR 1064 (42000)
  2. python输入两点坐标求距离_大一Python课的习题,输入两点,建立起直线方程y=kx+b。输入第3点,求点到直线的距离。这怎...
  3. labview调用halcon匹配详解
  4. 本地idea项目上传到svn
  5. 7-8 九连环问题(c++解决)
  6. 【图论】Johnson算法(多源最短路优化)
  7. 云原生 | Kubernetes - Webhook 模式
  8. 浅谈JVM之运行时数据区
  9. WinCE6/PXA310/TCPMP播放视频xscale硬件加速修改
  10. JavaWeb之Maven学习