美团Java后端面试题,巧妙的回答ThreadLocal原理!

太过爱你忘了你带给我的痛 2022-12-07 15:06 267阅读 0赞

想要去好点的公司,想要去前景好的公司都对技术要求挺高的,面试时技术问也会相应的难些,就拿美团来说,它好像比较喜欢线程安全机制问题,之前就有小伙伴被问倒了!所以今天就详细讲一讲ThreadLocal原理。

ThreadLocal
ThreadLocal是线程的内部存储类,可以在指定线程内存储数据。只有指定线程可以得到存储数据。

  1. /** * 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。

  1. /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
  2. ThreadLocal.ThreadLocalMap threadLocals = null;

每个新线程都会实例化为一个ThreadLocalMap并且赋值给成员变量ThreadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

应用场景
当某些数据是以线程为作用域并且不同线程有不同数据副本时,考虑ThreadLocal。

无状态,副本变量独立后不影响业务逻辑的高并发场景。

如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决。
get()与set()
set()是调用ThreadLocalMap的set()实现的:

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value);
  8. }
  9. //getMap方法
  10. ThreadLocalMap getMap(Thread t) {
  11. //thred中维护了一个ThreadLocalMap
  12. return t.threadLocals;
  13. }
  14. //createMap
  15. void createMap(Thread t, T firstValue) {
  16. //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
  17. t.threadLocals = new ThreadLocalMap(this, firstValue);
  18. }

ThreadLocalMap:
ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标是value存储的对应位置。

[图片上传中…(image-cd716a-1587459684812-0)]

ThreadLocalMaps是延迟构造的,因此只有在至少要放置一个条目时才创建。

  1. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  2. table = new Entry[INITIAL_CAPACITY];
  3. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  4. table[i] = new Entry(firstKey, firstValue);
  5. size = 1;
  6. setThreshold(INITIAL_CAPACITY);
  7. }

ThreadLocalMap初始化时创建了默认长度是16的Entry数组。通过hashCode与length位运算确定索引值i。

每个Thread都有一个ThreadLocalMap类型。相当于每个线程Thread都有一个Entry型的数组table。而一切读取过程都是通过操作这个数组table进行的。

set() 方法

  1. private void set(ThreadLocal<?> key, Object value) {
  2. // We don't use a fast path as with get() because it is at
  3. // least as common to use set() to create new entries as
  4. // it is to replace existing ones, in which case, a fast
  5. // path would fail more often than not.
  6. Entry[] tab = table;
  7. int len = tab.length;
  8. //通过&运算计算索引
  9. int i = key.threadLocalHashCode & (len-1);
  10. for (Entry e = tab[i];
  11. e != null;
  12. e = tab[i = nextIndex(i, len)]) {
  13. ThreadLocal<?> k = e.get();
  14. //如果存在key则覆盖
  15. if (k == key) {
  16. e.value = value;
  17. return;
  18. }
  19. if (k == null) {
  20. replaceStaleEntry(key, value, i);
  21. return;
  22. }
  23. }
  24. //新建结点插入
  25. tab[i] = new Entry(key, value);
  26. int sz = ++size;
  27. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  28. rehash();
  29. }

将threadLocalHashCode与长度进行位运算得到索引。

threadLocalHashCode的代码如下:

  1. private final int threadLocalHashCode = nextHashCode();
  2. /** * The next hash code to be given out. Updated atomically. Starts at * zero. */
  3. private static AtomicInteger nextHashCode =
  4. new AtomicInteger();
  5. /** * 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. */
  6. private static final int HASH_INCREMENT = 0x61c88647;
  7. /** * Returns the next hash code. */
  8. private static int nextHashCode() {
  9. return nextHashCode.getAndAdd(HASH_INCREMENT);
  10. }

由于是static变量,threadLocalHashCode在每次加载threadLocal类时会重新初始化,同时会自增一次,增加HASH_INCREMENT(斐波那契散列乘数,通过该数散列出来的结果会比较均匀)。

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。

而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。

对于一个ThreadLocal来讲,他的索引值i是确定的。对于不同线程,同一个threadlocal对应的是不同table的同一下标,即是table[i],不同线程之间的table是相互独立的。

get() 方法
计算索引,直接取出:

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue();
  13. }

remove() 方法

  1. /** * Remove the entry for key. */
  2. private void remove(ThreadLocal<?> key) {
  3. Entry[] tab = table;
  4. int len = tab.length;
  5. int i = key.threadLocalHashCode & (len-1);
  6. for (Entry e = tab[i];
  7. e != null;
  8. e = tab[i = nextIndex(i, len)]) {
  9. if (e.get() == key) {
  10. e.clear();
  11. expungeStaleEntry(i);
  12. return;
  13. }
  14. }
  15. }

线程隔离特性:
线程隔离特性,只有在线程内才能获取到对应的值,线程外不能访问。

(1)Synchronized是通过线程等待,牺牲时间来解决访问冲突。
(2)ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突。

内存泄露问题:
存在内存泄露问题,每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

Demo程序:

  1. import java.util.concurrent.atomic.AtomicInteger;
  2. /** * <h3>Exper1</h3> * <p>ThreadLocalId</p> * * @author : cxc * @date : 2020-04-01 23:48 **/
  3. public class ThreadLocalId {
  4. // Atomic integer containing the next thread ID to be assigned
  5. private static final AtomicInteger nextId = new AtomicInteger(0);
  6. // Thread local variable containing each thread's ID
  7. private static final ThreadLocal <Integer> threadId =
  8. new ThreadLocal<Integer>()
  9. {
  10. @Override
  11. protected Integer initialValue() {
  12. return nextId.getAndIncrement();
  13. }
  14. };
  15. // Returns the current thread's unique ID, assigning it if necessary
  16. public static int get() {
  17. return threadId.get();
  18. }
  19. public static void remove() {
  20. threadId.remove();
  21. }
  22. }
  23. /** * <h3>Exper1</h3> * <p></p> * * @author : cxc * @date : 2020-04-02 00:07 **/
  24. public class ThreadLocalMain {
  25. private static void incrementSameThreadId(){
  26. try{
  27. for(int i=0;i<5;i++){
  28. System.out.println(Thread.currentThread()
  29. +"_"+i+",threadId:"+
  30. ThreadLocalId.get());
  31. }
  32. }finally {
  33. ThreadLocalId.remove();
  34. }
  35. }
  36. public static void main(String[] args) {
  37. incrementSameThreadId();
  38. new Thread(new Runnable() {
  39. @Override
  40. public void run() {
  41. incrementSameThreadId();
  42. }
  43. }).start();
  44. new Thread(new Runnable() {
  45. @Override
  46. public void run() {
  47. incrementSameThreadId();
  48. }
  49. }).start();
  50. }
  51. }

总结:

咱们玩归玩,闹归闹,别拿面试开玩笑。ThreadLocal的原理在面试中几乎被问烂了。Thread的私有数据是存储在ThreadLocalMap,通过ThreadLoacl进行管理。要了解ThreadLocal的原理,最好多阅读几遍源码,尤其是ThreadLocalMap的源码部分。大家面试前要把知识点记牢。要是需要更多的Java后端的面试资料或者线程方面学习资料的可以点击这里,暗号:cszq,也可以关注+私信我,免费提供!
在这里插入图片描述
在这里插入图片描述
最后祝大家工作都能顺顺利利哦!

发表评论

表情:
评论列表 (有 0 条评论,267人围观)

还没有评论,来说两句吧...

相关阅读

    相关 java端面试题总结

    > 牛客网java后端面试题学习,不断学习更新,希望有大佬来指点提携一波,望共勉,一起进步~~ 1. 请你说说进程和线程的区别 > 进程和线程的主要差别在于他们是不同的

    相关 2022Java端面试题整理

    因为这两年互联网整体环境不好,所在公司也在大批量裁员,故花很多时间在看面试题。看了好久,决定自己整理百家面试题,如果能帮助正在找工作的程序员们,那就更好了。会一直更新,直到找到