博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
面试再问ThreadLocal,别说你不会
阅读量:4102 次
发布时间:2019-05-25

本文共 5551 字,大约阅读时间需要 18 分钟。

 

ThreadLocal是什么

以前面试的时候问到ThreadLocal总是一脸懵逼,只知道有这个哥们,不了解他是用来做什么的,更不清楚他的原理了。表面上看他是和多线程,线程同步有关的一个工具类,但其实他与线程同步机制无关。线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每个线程创建一个单独的变量副本,每个线程都可以改变自己的变量副本而不影响其它线程所对应的副本。

官方API上是这样介绍的:该类提供了线程局部(thread-local)变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

ThreadLocal的API

ThreadLocal定义了四个方法:

  • get():返回此线程局部变量当前副本中的值

  • set(T value):将线程局部变量当前副本中的值设置为指定值

  • initialValue():返回此线程局部变量当前副本中的初始值

  • remove():移除此线程局部变量当前副本中的值

ThreadLocal还有一个特别重要的静态内部类ThreadLocalMap,该类才是实现线程隔离机制的关键。get()、set()、remove()都是基于该内部类进行操作,ThreadLocalMap用键值对方式存储每个线程变量的副本,key为当前的ThreadLocal对象,value为对应线程的变量副本。

试想,每个线程都有自己的ThreadLocal对象,也就是都有自己的ThreadLocalMap,对自己的ThreadLocalMap操作,当然是互不影响的了,这就不存在线程安全问题了,所以ThreadLocal是以空间来交换安全性的解决思路。

使用实例

假设每个线程都需要一个计数值记录自己做某件事做了多少次,各线程运行时都需要改变自己的计数值而且相互不影响,那么ThreadLocal就是很好的选择,这里ThreadLocal里保存的当前线程的局部变量的副本就是这个计数值。

public class SeqCount { private static ThreadLocal
seqCount = new ThreadLocal
() { @Override protected Integer initialValue() { return 0; } }; public int nextSeq() { seqCount.set(seqCount.get() +1); return seqCount.get(); } public static void main(String [] args) { SeqCount seqCount = new SeqCount(); SeqThread seqThread1 = new SeqThread(seqCount); SeqThread seqThread2 = new SeqThread(seqCount); SeqThread seqThread3 = new SeqThread(seqCount); SeqThread seqThread4 = new SeqThread(seqCount); seqThread1.start(); seqThread2.start(); seqThread3.start(); seqThread4.start(); } public static class SeqThread extends Thread { private SeqCount seqCount; public SeqThread(SeqCount seqCount) { this.seqCount = seqCount; } @Override public void run() { for (int i=0; i<3; i++) { System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq()); } } } }

运行结果:

 

解决SimpleDateFormat的线程安全

我们知道SimpleDateFormat在多线程下是存在线程安全问题的,那么将SimpleDateFormat作为每个线程的局部变量的副本就是每个线程都拥有自己的SimpleDateFormat,就不存在线程安全问题了。

public class SimpleDateFormatDemo { private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; private static ThreadLocal
threadLocal = new ThreadLocal<>(); /** * 获取线程的变量副本,如果不覆盖initialValue方法,第一次get将返回null,故需要创建一个DateFormat,放入threadLocal中 * @return */ public DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if (df == null) { df = new SimpleDateFormat(DATE_FORMAT); threadLocal.set(df); } return df; } public static void main(String [] args) { SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo(); MyRunnable myRunnable1 = new MyRunnable(formatDemo); MyRunnable myRunnable2 = new MyRunnable(formatDemo); MyRunnable myRunnable3 = new MyRunnable(formatDemo); Thread thread1= new Thread(myRunnable1); Thread thread2= new Thread(myRunnable2); Thread thread3= new Thread(myRunnable3); thread1.start(); thread2.start(); thread3.start(); } public static class MyRunnable implements Runnable { private SimpleDateFormatDemo dateFormatDemo; public MyRunnable(SimpleDateFormatDemo dateFormatDemo) { this.dateFormatDemo = dateFormatDemo; } @Override public void run() { System.out.println(Thread.currentThread().getName()+" 当前时间:"+dateFormatDemo.getDateFormat().format(new Date())); } }}

运行结果:

 

源码分析

ThreadLocalMap

ThreadLocalMap内部是利用Entry来进行key-value的存储的。

static class Entry extends WeakReference
> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal
k, Object v) { super(k); value = v; } }

上面源码中key就是ThreadLocal,value就是值,Entry继承WeakReference,所以Entry对应key的引用(ThreadLocal实例)是一个弱引用。

 

set(ThreadLocal key, Object value)

/** * Set the value associated with key. * * @param key the thread local object * @param value the value to be set */ private void set(ThreadLocal
key, Object value) { Entry[] tab = table; int len = tab.length; //根据ThreadLocal的散列值,查找对应元素在数组中的位置 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; } // key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了 if (k == null) { replaceStaleEntry(key, value, i); return; } } //ThreadLocal对应的key实例不存在,new一个 tab[i] = new Entry(key, value); int sz = ++size; //清楚陈旧的Entry(key == null的) // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

这个set操作和集合Map解决散列冲突的方法不同,集合Map采用的是链地址法,这里采用的是开放定址法(线性探测)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key ==null的实例,防止内存泄漏。

 

getEntry()

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

由于采用了开放定址法,当前keu的散列值和元素在数组中的索引并不是一一对应的,首先取一个猜测数(key的散列值),如果所对应的key是我们要找的元素,那么直接返回,否则调用getEntryAfterMiss

private Entry getEntryAfterMiss(ThreadLocal
key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal
k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }

这里一直在探测寻找下一个元素,知道找的元素的key是我们要找的。这里当key==null时,调用expungeStaleEntry有利于GC的回收,用于防止内存泄漏。

ThreadLocal为什么会内存泄漏

ThreadLocalMap的key为ThreadLocal实例,他是一个弱引用,我们知道弱引用有利于GC的回收,当key == null时,GC就会回收这部分空间,但value不一定能被回收,因为他和Current Thread之间还存在一个强引用的关系。由于这个强引用的关系,会导致value无法回收,如果线程对象不消除这个强引用的关系,就可能会出现OOM。有些时候,我们调用ThreadLocalMap的remove()方法进行显式处理。

 

总结

ThreadLocal不是用来解决共享变量的问题,也不是协调线程同步,他是为了方便各线程管理自己的状态而引用的一个机制。

每个ThreadLocal内部都有一个ThreadLocalMap,他保存的key是ThreadLocal的实例,他的值是当前线程的局部变量的副本的值。

 

转载地址:http://suusi.baihongyu.com/

你可能感兴趣的文章
就在昨天,全球 42 亿 IPv4 地址宣告耗尽!
查看>>
200页!分享珍藏很久的Python学习知识手册(附链接)
查看>>
4 岁小女孩给 Linux 内核贡献提交
查看>>
推荐几个私藏很久的技术公众号给大家
查看>>
王垠受邀面试阿里 P9,被 P10 面跪后网上怒发文,惨打 325 的 P10 赵海平回应了!...
查看>>
Python 趣味打怪:147 段简单代码助你从入门到大师
查看>>
卧槽!小姐姐用动画图解 Git 命令,这也太秀了吧?!
查看>>
厉害了!Python 编辑器界的神器 Jupyter ,推出官方可视化 Debug 工具!
查看>>
卧槽!Java 虚拟机竟然还有这些性能调优技巧...
查看>>
听说玩这些游戏能提升编程能力?
查看>>
7 年工作经验,面试官竟然还让我写算法题???
查看>>
被 Zoom 逼疯的歪果仁,造出了视频会议机器人,同事已笑疯丨开源
查看>>
上古语言从入门到精通:COBOL 教程登上 GitHub 热榜
查看>>
再见,Eclipse...
查看>>
如果你还不了解 RTC,那我强烈建议你看看这个!
查看>>
沙雕程序员在无聊的时候,都搞出了哪些好玩的小玩意...
查看>>
漫话:为什么你下载小电影的时候进度总是卡在 99% 就不动了?
查看>>
我去!原来大神都是这样玩转「多线程与高并发」的...
查看>>
当你无聊时,可以玩玩 GitHub 上这个开源项目...
查看>>
B 站爆红的数学视频,竟是用这个 Python 开源项目做的!
查看>>