针对简谈ThreadLocal和简谈中国式现代化的基本特征这两个问题,本篇文章进行了详细的解答,同时本文还将给你拓展Android详解ThreadLocal及InheritableThreadLoca
针对简谈ThreadLocal和简谈中国式现代化的基本特征这两个问题,本篇文章进行了详细的解答,同时本文还将给你拓展Android 详解ThreadLocal及InheritableThreadLocal、FastThreadLocal 是什么鬼?吊打 ThreadLocal 的存在!!、Java 并发编程 ③ - ThreadLocal 和 InheritableThreadLocal 详解、Java多线程之深入解析ThreadLocal和ThreadLocalMap等相关知识,希望可以帮助到你。
本文目录一览:- 简谈ThreadLocal(简谈中国式现代化的基本特征)
- Android 详解ThreadLocal及InheritableThreadLocal
- FastThreadLocal 是什么鬼?吊打 ThreadLocal 的存在!!
- Java 并发编程 ③ - ThreadLocal 和 InheritableThreadLocal 详解
- Java多线程之深入解析ThreadLocal和ThreadLocalMap
简谈ThreadLocal(简谈中国式现代化的基本特征)
一、ThreadLocal 线程私有的。
为什么说ThreadLocal是线程私有的? 上源码 ThreadLocal.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);
}
从源码得知。我们在往 ThreadLocal 中存数据时。 它首先 获取到当前线程。并从当前线程中拿到 ThreadLocalMap 。再往里存放数据。
接下来我们再来看ThreadLocalMap 中 key ,value 究竟又存放的是什么。显然 ThreadLocalMap 中 key ->ThreadLocal ;value ->我们set 进去的 value。
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();
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();
}
由源码,我们很自然就能得出结论。 既然调用 ThreadLocal .set() 方法时,每次都是获取当前线程,再完成数据存储动作的。那么,它自然 具备了线程隔离性。由某一个线程所持有。所以ThreadLocal是线程私有的。
接下来,我们用代码来证明
/**
* 首先 我先初始化一个 threadLocal 。、
* 再主线程main 中 新启一个线程 thread,并在该线程中完成 set操作。
* 由此 当前应用存在两个线程 一个是 main ,一个是 thread
* 由于threadLocal 是线程私有的。所以 主线程 main 获取的值为 null
*/
public class Test {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
Thread thread = new Thread(()->{
System.out.println(String.format("往 线程%s 中 存入 1 ",Thread.currentThread().getName()));
threadLocal.set(1);
try{
Thread.sleep(1000);
System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
}catch(Exception ex){
ex.printStackTrace();
}
});
thread.start();
try{
// 让 主线程睡一会儿。确保 cpu 已执行 thread 中的 run()方法
Thread.sleep(500);
}catch(Exception ex){
ex.printStackTrace();
}
System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
}
}
验证完毕。让我们继续来看下源码。
1.1 首先,我们来看一下 Thread 类。java源码中包含一个 ThreadLocalMap 的成员变量 threadLocals
/*java源码中包含一个 ThreadLocalMap 的成员变量 threadLocals */
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
从上图我们可以看出ThreadLocalMap 是 ThreadLocal 中的 静态内部类。
1.2 接着我们来看下 ThreadLocal 中的源码。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
看过源码后我们发现 ThreadLocalMap 中 也包含一个 静态内部类 Entry 继承自 WeakReference<ThreadLocal<?>> 同时拥有 一个ThreadLocal 的引用。和 value。
那么问题来了。此处为什么要用 弱引用?
首先。我们再来复习一下,弱引用的特点。 当一个对象没有被强引用存在时。 弱引用 将被jvm忽视。直接gc掉。想一下,如果不是 弱引用。而是强引用,会有什么问题。假使 Entry 未继承弱引用,则 entry 对 ThreadLocal 强引用。 则 当 ThreadLocal 被复制为null时 意味着,ThreadLocal 已经没有用了。 但 entry 对 ThreadLocal 的 强引用。导致 ThreadLocal 没有办法被回收。 会造成内存泄漏。 所以 这里被继承自 弱引用。
验证来了。上代码,看看 ThreadLocal 被置为空后。 是否被gc了。
/**
* 首先 我创建了两个 ThreadLocal 对象 分别是 threadLocal,threadLocal_2
* 分别往 主线程 main 中 存放数字 1,2
* 然后将 threadLocal 置为空。 然后再 手动 gc 。
* 分别再gc 前 和 gc 后 查看 当前 线程中的 ThreadLocalMap。
* 由于获取 ThreadLocalMap 访问级别为default 所以我这里将用 debug的方式进行查看。
*/
public class Test1 {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal_2 = new ThreadLocal<>();
threadLocal.set(1);
threadLocal_2.set(2);
System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
threadLocal_2.get();
threadLocal = null;
System.gc();
threadLocal_2.get();
}
}
未gc前。debug 看到的 情况。
gc 后。 debug 看到的情况。
终上所诉。验证了 弱引用的实际效果。 和 为什么不用强引用 的原因。
这里,我们发现了。 弱引用后, ThreadLocalMap 中对应的 entry 的 referent (指向 threadLocal的引用)确实被gc了。
继续看源码。 referent 是哪里来的。 entry 继承自 WeakReference<ThreadLocal<?>> 。而 WeakReference 又继承自 抽象类 Reference<T>。
private T referent; /* Treated specially by GC */
volatile ReferenceQueue<? super T> queue;
/* When active: NULL
* pending: this
* Enqueued: next reference in queue (or this if last)
* Inactive: this
*/
@SuppressWarnings("rawtypes")
volatile Reference next;
好,了解到这里。 我们不难发现。 虽然被gc了。但是 还是存在一个问题,ThreadLocalMap -》entry -》key -》 referent (threadLocal)已然为null了。但是却仍然存在ThreadLocalMap中,占用的部分的内存。 应用不可能通过某个引用再次拿到 entry 中的value了。 那不就是内存泄漏了吗?
好问题! 这里编写人员,也想到了该问题。所以,他们的处理逻辑是:再调用 set,remove 方法时,调用方法 expungeStaleEntry 将 键为null 的对象remove掉。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 从staleSlot后一个index开始向后遍历,直到遇到为null的entry
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果entry的key为null,则清除掉该entry
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null) // 对该entry,重新进行hash并解决冲突
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值
return i;
}
到这里,这样处理,就万事大吉了吗? 远远没有这么简单。 新问题又来了。 假使 set完后,就再也没有调用 set 和 remove 方法。那 不还是内存泄漏了吗?
所以再使用 ThreadLocal 时,要养成一个好习惯,ThreadLocal 再没有用时,就将 ThreadLocal 置为null。以免出现内存泄漏。
最后,我们来分析一个问题。ThreadLocal 是线程私有的。如果是多线程中(多线程中的线程是可复用的)使用了 ThreadLocal 会有什么问题?
答案也很简单。如果没有及时清理 ThreadLocal 除内存泄露外,还可能引发数据问题。 话不多说, 上代码。
/**
* 先后创建6个任务,前3个线程写数据,后3个线程读取数据。
*
* 发现threadLocal 里的 值被取出了。
*
* 假使有个业务场景是 往当前线程 存放 用户名(采用ThreadLocal来存储) 。
* 先进行非空判断,再进行 存储。 结果,悲剧了。 上个线程的 ThreadLocal 并没有清理。导致 不为空。
* 结果上线文中存储的用户名就乱套了。可能就张冠李戴了
*/
public class Test2 {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
for(int i=0;i<3;i++){
fixedThreadPool.execute(()->{
try {
threadLocal.set("ckr");
} catch (Exception e) {
e.printStackTrace();
}
});
}
try{
Thread.sleep(2000);
}catch(Exception ex){
}
for(int i=0;i<3;i++){
fixedThreadPool.execute(()->{
try {
System.out.println(threadLocal.get());
} catch (Exception e) {
e.printStackTrace();
}
});
}
try{
Thread.sleep(2000);
}catch(Exception ex){
}
fixedThreadPool.shutdown();
}
}
以上。关于ThreadLocal的一些问题,我们都了解了,再次,特别强调,ThreadLocal 有风险。需要谨慎使用。
关于 java 中的四种引用强,软,弱,虚。 请查看上篇博客: https://my.oschina.net/u/4141142/blog/4517998
Android 详解ThreadLocal及InheritableThreadLocal
Android 详解ThreadLocal及InheritableThreadLocal
概要:
因为在android中经常用到handler来处理异步任务,通常用于接收消息,来操作UIThread,其中提到涉及到的looper对象就是保存在Threadlocal中的,因此研究下Threadlocal的源码。
分析都是基于android sdk 23 源码进行的,ThreadLocal在android和jdk中的实现可能并不一致。
在最初使用Threadlocal的时候,很容易会产生的误解就是threadlocal就是一个线程。
首先来看下Threadlocal的简单例子:
一个简单的Person类:
public class Person { public String name; public int age; public Person(String name,int age) { this.name = name; this.age = age; } }
一个简单的activity:
public class MainActivity extends Activity { //ThreadLocal初始化 private ThreadLocal<Person> mThreadLocal = new ThreadLocal<Person>(); private Person mPerson = new Person("王大侠",100); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //将mPerson对象设置进去 mThreadLocal.set(mPerson); Log.d("主线程"," 名字:" + mThreadLocal.get().name + " 年龄:" + mThreadLocal.get().age); } }
运行看看是否能得到mperson对象:
04-19 13:14:31.053 2801-2801/com.example.franky.myapplication d/主线程: 名字:王大侠 年龄:100
果然得到了!说明当前线程确实已经存储了mPerson对象的引用。
那么我们开启个子线程看看适合还能获取到mPerson对象呢:
public class MainActivity extends Activity { //ThreadLocal初始化 private ThreadLocal<Person> mThreadLocal = new ThreadLocal<Person>(); private Person mPerson = new Person("王大侠",100); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //将mPerson对象设置进去 mThreadLocal.set(mPerson); new Thread(new Runnable() { @Override public void run() { Log.d("子线程"," 名字:" + mThreadLocal.get().name + " 年龄:" + mThreadLocal.get().age); } }).start(); } }
运行看看结果:
`java.lang.NullPointerException: Attempt to read from field ' java.lang.String com.example.franky.myapplication.Person.name' on a null object reference`
哈哈,报错信息很明显,空指针异常,这清楚的表明子线程是获取不到mperson对象的,但可能到这里一些朋友可能有些晕了,明明我通过set()方法将mperson设置给threadlocal对象了啊,为啥在这里get()不到呢?
这里我们开始追踪threadlocal的源码看看有没有线索来解释这个疑问。
首先我们可以看看threadlocal的构造方法:
/** * Creates a new thread-local variable. */ public ThreadLocal() {}
构造方法平淡无奇,那么我们瞅瞅threadlocal的类说明吧,看看有没有发现:
implements a thread-local storage,that is,a variable for which each thread * has its own value. all threads share the same {@code threadlocal} object,* but each sees a different value when accessing it,and changes made by one * thread do not affect the other threads. the implementation supports * {@code null} values.
个人英文其实不是很好,大致的意思是每个线程都能在自己的线程保持一个对象,如果在一个线程改变对象的属性不会影响其他线程。但我们不要误读,如果某个对象是共享变量,那么在某个线程中改变它时,其他线程访问的时候其实该对象也被改变了,所以并不是说ThreadLocal是线程安全的,我们只要理解ThreadLocal是能在当前线程保存一个对象的,这样我们不用到处传递这个对象。
那ThreadLocal是线程吗?其实看看threadlocal有没有继承thread类就知道了:
public class ThreadLocal<T> { }
答案是没有~~,这说明threadlocal并不是线程。
明白了这点,那我们继续往下看看ThreadLocal是如何将对象保存起来的,瞅瞅set()方法:
public void set(T value) { Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values == null) { values = initializeValues(currentThread); } values.put(this,value); }
首先通过Thread currentthread = thread.currentthread();获取到当前线程
然后currentthread作为方法参数传递给了vlaues方法:
Values values(Thread current) { return current.localValues; }
这里我们看到return的是thread类的一个成员变量,我们瞅瞅Thread类中的这个变量:
ThreadLocal.Values localValues;
这里我们看到localvalues成员变量的类型就是ThreadLocal.Values
这个类其实是ThreadLocal的内部类。
然后这里判断得到的values对象是不是null,也就是说Thread类中的成员变量localvalues是不是null,由于我们是初次设置,所以这个对象肯定是null,那继续走
values initializevalues(thread current) { return current.localvalues = new values();}
很明显直接给localvalues变量new了一个value对象。那我们看看values对象里有啥:
首先看看构造:
Values() { initializeTable(INITIAL_SIZE); this.size = 0; this.tombstones = 0; }
看起来是初始化了一些成员变量的值,INITIAL_SIZE的值为16,
看看initializeTable(INITIAL_SIZE)这个方法是做啥的:
private void initializeTable(int capacity) { this.table = new Object[capacity * 2]; this.mask = table.length - 1; this.clean = 0; this.maximumload = capacity * 2 / 3; // 2/3 }
初始化了长度为32的table数组,mask为31,clean为0,maximumload为10。
又是一堆成员变量,那只好看看变量的说明是做啥的:
这个table很简单就是个object[]类型,意味着可以存放任何对象,变量说明:
/** * Map entries. Contains alternating keys (ThreadLocal) and values. * The length is always a power of 2. */ private Object[] table;
啊!原来这里就是存放保存的对象的。
其他的变量再看看:
/** Used to turn hashes into indices. */ private int mask; /** Number of live entries. */ private int size; /** Number of tombstones. */ private int tombstones; /** Maximum number of live entries and tombstones. */ private int maximumload; /** Points to the next cell to clean up. */ private int clean;
这样看来mask是用来计算数组下标的,size其实是存活的保存的对象数量,tombstones是过时的对象数量,maximumload是最大的保存数量,clean是指向的下一个要清理的位置。大概明白了这些我们再继续看:
values.put(this,value);
继续追踪:
/** * Sets entry for given ThreadLocal to given value,creating an * entry if necessary. */ void put(ThreadLocal<?> key,Object value) { cleanUp(); // Keep track of first tombstone. That's where we want to go back // and add an entry if necessary. int firstTombstone = -1; for (int index = key.hash & mask;; index = next(index)) { Object k = table[index]; if (k == key.reference) { // Replace existing entry. table[index + 1] = value; return; } if (k == null) { if (firstTombstone == -1) { // Fill in null slot. table[index] = key.reference; table[index + 1] = value; size++; return; } // Go back and replace first tombstone. table[firstTombstone] = key.reference; table[firstTombstone + 1] = value; tombstones--; size++; return; } // Remember first tombstone. if (firstTombstone == -1 && k == TOMBSTONE) { firstTombstone = index; } } }
该方法直接将this对象和要保存的对象传递了进来,
第一行的cleanUp()其实是用来对table执行清理的,比如清理一些过时的对象,检查是否对象的数量是否超过设置值,或者扩容等,这里不再细说,有兴趣大家可以研究下。
然后利用key.hash&mask计算下标,这里key.hash的初始化值:
private static AtomicInteger hashCounter = new AtomicInteger(0); private final int hash = hashCounter.getAndAdd(0x61c88647 * 2);
然后注释说为了确保计算的下标指向的是key值而不是value,当然为啥用上述数值进行计算就能保证获取的key值,貌似是和这个0x61c88647数值有关,再深入的大家可以留言。
然后最重要的就是
if (firstTombstone == -1) { // Fill in null slot. table[index] = key.reference; table[index + 1] = value; size++; return; }
这里将自身的引用,而且是弱引用,放在了table[index]上,将value放在它的下一个位置,保证key和value是排列在一起的,这样其实我们知道了key其实是threadlocal的引用,值是value,它们一同被放置在table数组内。
所以现在的情况是这样,Thread类中的成员变量localValues是ThreadLocal.Values类型,所以说白了就是当前线程持有了ThreadLocal.Values这样的数据结构,我们设置的value全部都存储在里面了,当然如果我们在一个线程中new了很多ThreadLocal对象,其实指向都是Thread类中的成员变量localValues,而且如果new了很多ThreadLocal对象,其实都是放在table中的不同位置的。
那接下来看看get()方法:
public T get() { // Optimized for the fast path. Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values != null) { Object[] table = values.table; int index = hash & values.mask; if (this.reference == table[index]) { return (T) table[index + 1]; } } else { values = initializeValues(currentThread); } return (T) values.getAfterMiss(this); }
代码比较简单了,首先还是获取当前线程,然后获取当前线程的Values对象,也就是Thread类中的成员变量localValues,然后拿到Values对象的table数组,计算下标,获取保存的对象,当然如果没有获取到return (T) values.getAfterMiss(this),就是返回null了,其实看方法Object getAfterMiss(ThreadLocal<?> key)中的这个代码:
Object value = key.initialValue(); protected T initialValue() { return null; }
就很清楚了,当然我们可以复写这个方法来实现自定义返回,大家有兴趣可以试试。
到此我们再回过头来看看开始的疑问,为啥mThreadLocal在子线程获取不到mPerson对象呢?原因就在于子线程获取自身线程中的localValues变量中并未保存mPerson,真正保存的是主线程,所以我们是获取不到的。
看完了ThreadLocal我们再看看它的一个子类InheritableThreadLocal,该类和ThreadLocal最大的不同就是它可以在子线程获取到保存的对象,而ThreadLocal只能在同一个线程,我们看看简单的例子:
public class MainActivity extends Activity { private InheritableThreadLocal<Person> mInheritableThreadLocal = new InheritableThreadLocal<Person>(); private Person mPerson = new Person("王大侠",100); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //将mPerson设置到当前线程 mInheritableThreadLocal.set(mPerson); Log.d("主线程"+Thread.currentThread().getName()," 名字:" + mInheritableThreadLocal.get().name + " 年龄:" + mInheritableThreadLocal.get().age); new Thread(new Runnable() { @Override public void run() { Log.d("子线程"+Thread.currentThread().getName()," 名字:" + mInheritableThreadLocal.get().name + " 年龄:" + mInheritableThreadLocal.get().age); } }).start(); }}
运行看看输出:
04-21 13:09:11.046 19457-19457/com.example.franky.myapplication D/主线程main: 名字:王大侠 年龄:100 04-21 13:09:11.083 19457-21729/com.example.franky.myapplication D/子线程Thread-184: 名字:王大侠 年龄:100
很明显在子线程也获取到了mPerson对象,那它是如何实现的呢?
看下源码:
public class InheritableThreadLocal<T> extends ThreadLocal<T> { /** * Creates a new inheritable thread-local variable. */ public InheritableThreadLocal() { } /** * Computes the initial value of this thread-local variable for the child * thread given the parent thread's value. Called from the parent thread when * creating a child thread. The default implementation returns the parent * thread's value. * * @param parentValue the value of the variable in the parent thread. * @return the initial value of the variable for the child thread. */ protected T childValue(T parentValue) { return parentValue; } @Override Values values(Thread current) { return current.inheritableValues; } @Override Values initializeValues(Thread current) { return current.inheritableValues = new Values(); } }
很明显InheritableThreadLocal重写了两个方法:
Values values(Thread current)方法返回了Thread类中的成员变量inheritableValues。
Values initializeValues(Thread current)也是new的对象也是指向inheritableValues。
而ThreadLocal中都是指向的localValues这个变量。
也就是说当我们调用set(T value)方法时,根据前面的分析,其实初始化的是这个inheritableValues,那么既然子线程能够获取到保存的对象,那我们看看这个变量在Thread类中哪里有调用,搜索下就看到:
private void create(ThreadGroup group,Runnable runnable,String threadName,long stackSize) { ... // Transfer over InheritableThreadLocals. if (currentThread.inheritableValues != null) { inheritableValues = new ThreadLocal.Values(currentThread.inheritableValues); } // add ourselves to our ThreadGroup of choice this.group.addThread(this); }
在Thread类中的create方法中可以看到,该方法在Thread构造方法中被调用,如果currentThread.inheritableValues不为空,就会将它传递给Values的有参构造:
/** * Used for InheritableThreadLocals. */ Values(Values fromParent) { this.table = fromParent.table.clone(); this.mask = fromParent.mask; this.size = fromParent.size; this.tombstones = fromParent.tombstones; this.maximumload = fromParent.maximumload; this.clean = fromParent.clean; inheritValues(fromParent); }
这里可以看到将inheritableValues的值完全复制过来了,所以我们在子线程一样可以获取到保存的变量,我们的分析就到此为止吧。
自己总结的肯定有很多纰漏,还请大家多多指正。
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!
FastThreadLocal 是什么鬼?吊打 ThreadLocal 的存在!!
ThreadLocal 大家都知道是线程本地变量,今天栈长再介绍一个神器:FastThreadLocal,从字面上看就是:Fast + ThreadLocal,一个快的 ThreadLocal?这到底是什么鬼呢?
一、FastThreadLocal 简介
FastThreadLocal 并不是 JDK 自带的,而是在 Netty 中造的一个轮子,Netty 为什么要重复造轮子呢?
来看下它源码中的注释定义:
/**
* A special variant of {@link ThreadLocal} that yields higher access performance when accessed from a
* {@link FastThreadLocalThread}.
* <p>
* Internally, a {@link FastThreadLocal} uses a constant index in an array, instead of using hash code and hash table,
* to look for a variable. Although seemingly very subtle, it yields slight performance advantage over using a hash
* table, and it is useful when accessed frequently.
* </p><p>
* To take advantage of this thread-local variable, your thread must be a {@link FastThreadLocalThread} or its subtype.
* By default, all threads created by {@link DefaultThreadFactory} are {@link FastThreadLocalThread} due to this reason.
* </p><p>
* Note that the fast path is only possible on threads that extend {@link FastThreadLocalThread}, because it requires
* a special field to store the necessary state. An access by any other kind of thread falls back to a regular
* {@link ThreadLocal}.
* </p>
*
* @param <V> the type of the thread-local variable
* @see ThreadLocal
*/
public class FastThreadLocal<V> {
...
}
FastThreadLocal 是一个特殊的 ThreadLocal 变体,当从线程类 FastThreadLocalThread 中访问 FastThreadLocalm时可以获得更高的访问性能。如果你还不知道什么是 ThreadLocal,可以关注公众号Java技术栈阅读我之前分享的文章。
二、FastThreadLocal 为什么快?
在 FastThreadLocal 内部,使用了索引常量代替了 Hash Code 和哈希表,源代码如下:
private final int index;
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
FastThreadLocal 内部维护了一个索引常量 index,该常量在每次创建 FastThreadLocal 中都会自动+1,从而保证了下标的不重复性。
这要做虽然会产生大量的 index,但避免了在 ThreadLocal 中计算索引下标位置以及处理 hash 冲突带来的损耗,所以在操作数组时使用固定下标要比使用计算哈希下标有一定的性能优势,特别是在频繁使用时会非常显著,用空间换时间,这就是高性能 Netty 的巧妙之处。
要利用 FastThreadLocal 带来的性能优势,就必须结合使用 FastThreadLocalThread 线程类或其子类,因为 FastThreadLocalThread 线程类会存储必要的状态,如果使用了非 FastThreadLocalThread 线程类则会回到常规 ThreadLocal。
Netty 提供了继承类和实现接口的线程类:
- FastThreadLocalRunnable
- FastThreadLocalThread
Netty 也提供了 DefaultThreadFactory
工厂类,所有由 DefaultThreadFactory
工厂类创建的线程默认就是 FastThreadLocalThread 类型,来看下它的创建过程:
先创建 FastThreadLocalRunnable,再创建 FastThreadLocalThread,基友搭配,干活不累,一定要配合使用才“快”。
三、FastThreadLocal 实战
要使用 FastThreadLocal 就需要导入 Netty 的依赖了:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.52.Final</version>
</dependency>
写一个测试小示例:
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.FastThreadLocal;
public class FastThreadLocalTest {
public static final int MAX = 100000;
public static void main(String[] args) {
new Thread(() -> threadLocal()).start();
new Thread(() -> fastThreadLocal()).start();
}
private static void fastThreadLocal() {
long start = System.currentTimeMillis();
DefaultThreadFactory defaultThreadFactory = new DefaultThreadFactory(FastThreadLocalTest.class);
FastThreadLocal<String>[] fastThreadLocal = new FastThreadLocal[MAX];
for (int i = 0; i < MAX; i++) {
fastThreadLocal[i] = new FastThreadLocal<>();
}
Thread thread = defaultThreadFactory.newThread(() -> {
for (int i = 0; i < MAX; i++) {
fastThreadLocal[i].set("java: " + i);
}
System.out.println("fastThreadLocal set: " + (System.currentTimeMillis() - start));
for (int i = 0; i < MAX; i++) {
for (int j = 0; j < MAX; j++) {
fastThreadLocal[i].get();
}
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("fastThreadLocal total: " + (System.currentTimeMillis() - start));
}
private static void threadLocal() {
long start = System.currentTimeMillis();
ThreadLocal<String>[] threadLocals = new ThreadLocal[MAX];
for (int i = 0; i < MAX; i++) {
threadLocals[i] = new ThreadLocal<>();
}
Thread thread = new Thread(() -> {
for (int i = 0; i < MAX; i++) {
threadLocals[i].set("java: " + i);
}
System.out.println("threadLocal set: " + (System.currentTimeMillis() - start));
for (int i = 0; i < MAX; i++) {
for (int j = 0; j < MAX; j++) {
threadLocals[i].get();
}
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadLocal total: " + (System.currentTimeMillis() - start));
}
}
结果输出:
可以看出,在大量读写面前,写操作的效率差不多,但读操作 FastThreadLocal 比 ThreadLocal 快的不是一个数量级,简直是秒杀 ThreadLocal 的存在。
当我把 MAX 值调整到 1000 时,结果输出:
读写操作不多时,ThreadLocal 明显更胜一筹!
上面的示例是单线程测试多个 ThreadLocal,即数组形式,另外,我也测试了多线程单个 ThreadLocal,这时候 FastThreadLocal 效率就明显要落后于 ThreadLocal。。
最后需要说明的是,在使用完 FastThreadLocal 之后不用 remove 了,因为在 FastThreadLocalRunnable 中已经加了移除逻辑,在线程运行完时会移除全部绑定在当前线程上的所有变量。
所以,使用 FastThreadLocal 导致内存溢出的概率会不会要低于 ThreadLocal?
不一定,因为 FastThreadLocal 会产生大量的 index 常量,所谓的空间换时间,所以感觉 FastThreadLocal 内存溢出的概率更大,但好在每次使用完都会自动 remove。
四、总结
Netty 中的 FastThreadLocal 在大量频繁读写操作时效率要高于 ThreadLocal,但要注意结合 Netty 自带的线程类使用,这可能就是 Netty 为什么高性能的奥妙之一吧!
如果没有大量频繁读写操作的场景,JDK 自带的 ThreadLocal 足矣,并且性能还要优于 FastThreadLocal。
好了,今天的分享就到这里了,觉得有用,转发分享一下哦。
最后,Java 系列教程还会继续更新,关注Java技术栈公众号第一时间推送,还可以在公众号菜单中获取历史 Java 教程,都是干货。
版权申明:本文系公众号 "Java技术栈" 原创,原创实属不易,转载、引用本文内容请注明出处,禁止抄袭、洗稿,请自重,尊重他人劳动成果和知识产权。
近期热文推荐:
1.Java 15 正式发布, 14 个新特性,刷新你的认知!!
2.终于靠开源项目弄到 IntelliJ IDEA 激活码了,真香!
3.我用 Java 8 写了一段逻辑,同事直呼看不懂,你试试看。。
4.吊打 Tomcat ,Undertow 性能很炸!!
5.《Java开发手册(嵩山版)》最新发布,速速下载!
觉得不错,别忘了随手点赞+转发哦!
Java 并发编程 ③ - ThreadLocal 和 InheritableThreadLocal 详解
原文地址:Java 并发编程 ③ - ThreadLocal 和 InheritableThreadLocal 详解
前言
往期文章:
- Java 并发编程基础 ① - 线程
- Java 并发编程 ② - 线程生命周期与状态流转
继上一篇结尾讲的,这一篇文章主要是讲ThreadLocal 和 InheritableThreadLocal。主要内容有:
- ThreadLocal 使用 和 实现原理
-
ThreadLocal 副作用
- 脏数据
- 内存泄漏的分析
- InheritableThreadLocal 使用 和 实现原理
一、ThreadLocal
ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,即变量在线程间隔离而在方法或类间共享的场景。 确切的来说,ThreadLocal 并不是专门为了解决多线程共享变量产生的并发问题而出来的,而是给提供了一个新的思路,曲线救国。
通过实例代码来简单演示下ThreadLocal的使用。
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " set 1");
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 不会收到线程2的影响,因为ThreadLocal 线程本地存储
System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
threadLocal.remove();
});
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " set 2");
threadLocal.set(2);
threadLocal.remove();
});
ThreadPoolUtil.tryReleasePool(service);
}
}
可以看到,线程1不会受到线程2的影响,因为ThreadLocal 创建的是线程私有的变量。
二、ThreadLocal 实现原理 ⭐
2.1 理清 ThreadLocal 中几个关键类之间的关系
我们先看下ThreadLocal 与 Thread 的类图,了解他们的主要方法和相互之间的关系。
图中几个类我们标注一下:
- Thread
- ThreadLocal
- ThreadLocalMap
- ThreadLocalMap.Entry
接下去,我们首先先开始了解这几个类的相互关系:
-
Thread 类中有一个 threadLocals 成员变量(实际上还有一个inheritableThreadLocals,后面讲),它的类型是ThreadLocal 的内部静态类ThreadLocalMap
public class Thread implements Runnable { // ...... 省略 /* ThreadLocal values pertaining to this thread. This map is maintained
ThreadLocal.ThreadLocalMap threadLocals = null;
```
-
ThreadLocalMap 是一个定制化的Hashmap,为什么是个HashMap?很好理解,每个线程可以关联多个ThreadLocal变量。
/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread. To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when
*/
static class ThreadLocalMap {
// ...
}
```
-
ThreadLocalMap 初始化时会创建一个大小为16的Entry 数组,Entry 对象也是用来保存 key- value 键值对(这个Key固定是ThreadLocal 类型)。值得注意的是,这个Entry 继承了
WeakReference
(这个设计是为了防止内存泄漏,后面会讲)static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
2.2 ThreadLocal的set、get及remove方法的源码
a. void set(T value)
public void set(T value) {
// ① 获取当前线程
Thread t = Thread.currentThread();
// ② 去查找对应线程的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// ③ 第一次调用就创建当前线程的对应的ThreadLocalMap
// 并且会将值保存进去,key是当前的threadLocal,value就是传进来的值
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
b. T get()
public T get() {
// ① 获取当前线程
Thread t = Thread.currentThread();
// ② 去查找对应线程的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
if (map != null) {
// ③ 不为null,返回当前threadLocal 对应的value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// ④ 当前线程的threadLocalMap为空,初始化
return setInitialValue();
}
private T setInitialValue() {
// ⑤ 初始化的值为null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化当前线程的threadLocalMap
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
c. void remove()
如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例对应的本地变量。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
从源码中可以看出来,自始至终,这些本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量,那个线程私有的threadLocalMap 里面。
ThreadLocal就是一个工具壳和一个key,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。
讲到这里,实现原理就算讲完了,实际上ThreadLocal 的源码算是非常简单易懂。关于ThreadLocal 真正的重点和难点,是我们后面的内容。
三、ThreadLocal 副作用
ThreadLocal 是为了线程能够安全的共享/传递某个变量设计的,但是有一定的副作用。
ThreadLocal 的主要问题是会产生脏数据和内存泄露。
先说一个结论,这两个问题通常是在线程池的线程中使用 ThreadLocal 引发的,因为线程池有线程复用和内存常驻两个特点。
3.1 脏数据
脏数据应该是大家比较好理解的,所以这里呢,先拿出来讲。线程复用会产生脏数据。由于线程池会重用 Thread 对象 ,那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用。如果在实现的线程 run() 方法体中不显式地调用 remove() 清理与线程相关的 ThreadLocal 信息,那么倘若下一个线程不调用 set() 设置初始值,就可能 get() 到重用的线程信息,包括 ThreadLocal 所关联的线程对象的 value 值。
为了便于理解,这里提供一个demo:
public class ThreadLocalDirtyDataDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
for (int i = 0; i < 2; i++) {
MyThread thread = new MyThread();
pool.execute(thread);
}
ThreadPoolUtil.tryReleasePool(pool);
}
private static class MyThread extends Thread {
private static boolean flag = true;
@Override
public void run() {
if (flag) {
// 第一个线程set之后,并没有进行remove
// 而第二个线程由于某种原因(这里是flag=false) 没有进行set操作
String sessionInfo = this.getName();
threadLocal.set(sessionInfo);
flag = false;
}
System.out.println(this.getName() + " 线程 是 " + threadLocal.get());
// 线程使用完threadLocal,要及时remove,这里是为了演示错误情况
}
}
}
执行结果:
Thread-0 线程 是 Thread-0
Thread-1 线程 是 Thread-0
3.2 内存泄露 ⭐
在讲这个之前,有必要看一张图,从栈与堆的角度看看ThreadLocal 使用过程当中几个类的引用关系。
看到红色的虚线箭头没?这个就是理解ThreadLocal的一个重点和难点。
我们再看一遍Entry的源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap 的每个 Entry 都是一个对键的弱引用 - WeakReference<ThreadLocal<?>>
,这一点从super(k)
可看出。另外,每个 Entry都包含了一个对 值 的强引用。
在前面的叙述中,我有提到Entry extends WeakReference<ThreadLocal<?>>
是为了防止内存泄露。实际上,这里说的防止内存泄露是针对ThreadLocal 对象的。
怎么说呢?继续往下看。
如果你有学习过Java 中的引用的话,这个WeakReference
应该不会陌生,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收<font color=''#ff6600'' >只被弱引用关联</font>的对象。
更详细的相关内容可以阅读笔者的这篇文章 【万字精美图文带你掌握JVM垃圾回收#Java 中的引用】 )
通过这种设计,即使线程正在执行中, 只要 ThreadLocal 对象引用被置成 null,Entry 的 Key 就会自动在下一次 YGC 时被垃圾回收(因为只剩下ThreadLocalMap 对其的弱引用,没有强引用了)。
如果这里Entry 的key 值是对 ThreadLocal 对象的强引用的话,那么即使ThreadLocal的对象引用被声明成null 时,这些 ThreadLocal 不能被回收,因为还有来自 ThreadLocalMap 的强引用,这样子就会造成内存泄漏。
这类key被回收( key == null
)的Entry 在 ThreadLocalMap 源码中被称为 stale entry (翻译过来就是 “过时的条目”),会在下一次执行 ThreadLocalMap 的 getEntry 和 set 方法中,将 这些 stale entry 的value 置为 null,使得原来value 指向的变量可以被垃圾回收。
“会在下一次执行 ThreadLocalMap 的 getEntry 和 set 方法中,将 这些 stale entry 的value 置为 null,使得 原来value 指向的变量可以被垃圾回收”这一部分描述,可以查阅 ThreadLocalMap#expungeStaleEntry()
方法源码及调用这个方法的地方。
这样子来看,ThreadLocalMap 是通过这种设计,解决了 ThreadLocal 对象可能会存在的内存泄漏的问题,并且对应的value 也会因为上述的 stale entry 机制被垃圾回收。
但是我们为什么还会说使用ThreadLocal 可能存在内存泄露问题呢,在这里呢,指的是还存在那个Value(图中的紫色块)实例无法被回收的情况。
请注意哦,上述机制的前提是ThreadLocal 的引用被置为null,才会触发弱引用机制,继而回收Entry 的 Value对象实例。我们来看下ThreadLocal 源码中的注释
instances are typically private static fields in classesThreadLocal 对象通常作为私有静态变量使用
-- 如果说一个 ThreadLocal 是非静态的,属于某个线程实例类,那就失去了线程内共享的本质属性。
作为静态变量使用的话, 那么其生命周期至少不会随着线程结束而结束。也就是说,绝大多数的静态threadLocal对象都不会被置为null。这样子的话,通过 stale entry 这种机制来清除Value 对象实例这条路是走不通的。必须要手动remove() 才能保证。
这里还是用上面的例子来做示例。
public class ThreadLocalDirtyDataDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
for (int i = 0; i < 2; i++) {
MyThread thread = new MyThread();
pool.execute(thread);
}
ThreadPoolUtil.tryReleasePool(pool);
}
private static class MyThread extends Thread {
private static boolean flag = true;
@Override
public void run() {
if (flag) {
// 第一个线程set之后,并没有进行remove
// 而第二个线程由于某种原因(这里是flag=false) 没有进行set操作
String sessionInfo = this.getName();
threadLocal.set(sessionInfo);
flag = false;
}
System.out.println(this.getName() + " 线程 是 " + threadLocal.get());
// 线程使用完threadLocal,要及时remove,这里是为了演示错误情况
}
}
}
在这个例子当中,如果不进行 remove() 操作, 那么这个线程执行完成后,通过 ThreadLocal 对象持有的 String 对象是不会被释放的。
为什么说只有线程复用的时候,会出现这个问题呢?当然啦,因为这些本地变量都是存储在线程的内部变量中的,当线程销毁时,threadLocalMap的对象引用会被置为null,value实例对象 随着线程的销毁,在内存中成为了不可达对象,然后被垃圾回收。
// Thread#exit()
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
总结
总结一下
- WeakReference 的引入,是为了将ThreadLocal 对象与ThreadLocalMap 设计成一种弱引用的关系,来避免ThreadLocal 实例对象不能被回收而存在的内存泄露问题,当threadLocal 对象被回收时,会有清理 stale entry 机制,回收其对应的Value实例对象。
- 我们常说的内存泄露问题,针对的是threadLocal对应的Value对象实例。在线程对象被重用且threadLocal为静态变量时,如果没有手动remove(),就可能会造成内存泄露的情况。
- 上述两种内存泄露的情况只有在线程复用的情况下才会出现,因为在线程销毁时threadLocalMap的对象引用会被置为null。
- 解决副作用的方法很简单,就是每次用完ThreadLocal,都要及时调用 remove() 方法去清理。
四、InheritableThreadLocal
在一些场景中,子线程需要可以获取父线程的本地变量,比如用一个统一的ID来追踪记录调用链路。但是ThreadLocal 是不支持继承性的,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到对应的对象的。
为了解决这个问题,InheritableThreadLocal 也就应运而生。
4.1 使用
public class InheritableThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
// 主线程
threadLocal.set("hello world");
// 启动子线程
Thread thread = new Thread(() -> {
// 子线程输出父线程的threadLocal 变量值
System.out.println("子线程: " + threadLocal.get());
});
thread.start();
System.out.println("main: " +threadLocal.get());
}
}
输出:
main: hello world
子线程: hello world
4.2 原理
要了解原理,我们先来看一下 InheritableThreadLocal 的源码。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// ①
protected T childValue(T parentValue) {
return parentValue;
}
// ②
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
// ③
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
public class Thread implements Runnable {
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
可以看到,InheritableThreadLocal 继承了ThreadLocal,并且重写了三个方法,看来实现的门道就在这三个方法里面。
先看代码③,InheritableThreadLocal 重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals 变量的实例而不再是threadLocals。由代码②可知,当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals。
可以这么说,在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals。
代码②③都讲了,再来看看代码①,以及如何让子线程可以访问父线程的本地变量。
这要从创建Thread的代码说起,打开Thread类的默认构造函数,代码如下。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ... 省略无关部分
// 获取父线程 - 当前线程
Thread parent = currentThread();
// ... 省略无关部分
// 如果父线程的inheritThreadLocals不为null 且 inheritThreadLocals=true
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 设置子线程中的inheritableThreadLocals变量
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// ... 省略无关部分
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
再来看看里面是如何执行createInheritedMap 的。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 这里调用了重写的代码① childValue
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
在该构造函数内部把父线程的inheritableThreadLocals成员变量的值复制到新的ThreadLocalMap 对象中。
## 小结
本章讲了ThreadLocal 和 InheritableThreadLocal 的相关知识点。
ThreadLocal 实现线程内部变量共享,InheritableThreadLocal 实现了父线程与子线程的变量继承。但是还有一种场景,InheritableThreadLocal 无法解决,也就是在使用线程池等会池化复用线程的执行组件情况下,异步执行执行任务,需要传递上下文的情况。
针对上述情况,阿里开源了一个TTL
库,即Transmittable ThreadLocal来解决这个问题,有兴趣的朋友们可以去看看。
之后有时间的话我会单独写一篇文章介绍一下。
如果本文有帮助到你,希望能点个赞,这是对我的最大动力????。
参考
- 《Java 并发编程之美》
- 《码出高效》
Java多线程之深入解析ThreadLocal和ThreadLocalMap
ThreadLocal概述
ThreadLocal是线程变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
它具有3个特性:
- 线程并发:在多线程并发场景下使用。
- 传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量。
- 线程隔离:每个线程变量都是独立的,不会相互影响。
在不使用ThreadLocal的情况下,变量不隔离,得到的结果具有随机性。
public class Demo {
private String variable;
public String getVariable() {
return variable;
}
public void setVariable(String variable) {
this.variable = variable;
}
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo.setVariable(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
}).start();
}
}
}
输出结果:


Thread-2 Thread-2
Thread-4 Thread-4
Thread-1 Thread-2
Thread-0 Thread-2
Thread-3 Thread-3
在不使用ThreadLocal的情况下,变量隔离,每个线程有自己专属的本地变量variable,线程绑定了自己的variable,只对自己绑定的变量进行读写操作。
public class Demo {
private ThreadLocal<String> variable = new ThreadLocal<>();
public String getVariable() {
return variable.get();
}
public void setVariable(String variable) {
this.variable.set(variable);
}
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo.setVariable(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
}).start();
}
}
}
输出结果:


Thread-0 Thread-0
Thread-1 Thread-1
Thread-2 Thread-2
Thread-3 Thread-3
Thread-4 Thread-4
synchronized和ThreadLocal的比较
上述需求,通过synchronized加锁同样也能实现。但是加锁对性能和并发性有一定的影响,线程访问变量只能排队等候依次操作。TreadLocal不加锁,多个线程可以并发对变量进行操作。
public class Demo {
private String variable;
public String getVariable() {
return variable;
}
public void setVariable(String variable) {
this.variable = variable;
}
public static void main(String[] args) {
Demo demo = new Demo1();
for (int i = 0; i < 5; i++) {
new Thread(()->{
synchronized (Demo.class){
demo.setVariable(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
}
}).start();
}
}
}
ThreadLocal和synchronized都是用于处理多线程并发访问资源的问题。ThreadLocal是以空间换时间的思路,每个线程都拥有一份变量的拷贝,从而实现变量隔离,互相不干扰。关注的重点是线程之间数据的相互隔离关系。synchronized是以时间换空间的思路,只提供一个变量,线程只能通过排队访问。关注的是线程之间访问资源的同步性。ThreadLocal可以带来更好的并发性,在多线程、高并发的环境中更为合适一些。
ThreadLocal使用场景
转账事务的例子
JDBC对于事务原子性的控制可以通过setAutoCommit(false)设置为事务手动提交,成功后commit,失败后rollback。在多线程的场景下,在service层开启事务时用的connection和在dao层访问数据库的connection应该要保持一致,所以并发时,线程只能隔离操作自已的connection。
解决方案1:service层的connection对象作为参数传递给dao层使用,事务操作放在同步代码块中。
存在问题:传参提高了代码的耦合程度,加锁降低了程序的性能。
解决方案2:当需要获取connection对象的时候,通过ThreadLocal对象的get方法直接获取当前线程绑定的连接对象使用,如果连接对象是空的,则去连接池获取连接,并通过ThreadLocal对象的set方法绑定到当前线程。使用完之后调用ThreadLocal对象的remove方法解绑连接对象。
ThreadLocal的优势:
- 可以方便地传递数据:保存每个线程绑定的数据,需要的时候可以直接获取,避免了传参带来的耦合。
- 可以保持线程间隔离:数据的隔离在并发的情况下也能保持一致性,避免了同步的性能损失。
ThreadLocal的原理
每个ThreadLocal维护一个ThreadLocalMap,Map的Key是ThreadLocal实例本身,value是要存储的值。
每个线程内部都有一个ThreadLocalMap,Map里面存放的是ThreadLocal对象和线程的变量副本。Thread内部的Map通过ThreadLocal对象来维护,向map获取和设置变量副本的值。不同的线程,每次获取变量值时,只能获取自己对象的副本的值。实现了线程之间的数据隔离。
JDK1.8的设计相比于之前的设计(通过ThreadMap维护了多个线程和线程变量的对应关系,key是Thread对象,value是线程变量)的好处在于,每个Map存储的Entry数量变少了,线程越多键值对越多。现在的键值对的数量是由ThreadLocal的数量决定的,一般情况下ThreadLocal的数量少于线程的数量,而且并不是每个线程都需要创建ThreadLocal变量。当Thread销毁时,ThreadLocal也会随之销毁,减少了内存的使用,之前的方案中线程销毁后,ThreadLocalMap仍然存在。
ThreadLocal源码解析
set方法
首先获取线程,然后获取线程的Map。如果Map不为空则将当前ThreadLocal的引用作为key设置到Map中。如果Map为空,则创建一个Map并设置初始值。
get方法
首先获取当前线程,然后获取Map。如果Map不为空,则Map根据ThreadLocal的引用来获取Entry,如果Entry不为空,则获取到value值,返回。如果Map为空或者Entry为空,则初始化并获取初始值value,然后用ThreadLocal引用和value作为key和value创建一个新的Map。
remove方法
删除当前线程中保存的ThreadLocal对应的实体entry。
initialValue方法
该方法的第一次调用发生在当线程通过get方法访问线程的ThreadLocal值时。除非线程先调用了set方法,在这种情况下,initialValue才不会被这个线程调用。每个线程最多调用依次这个方法。
该方法只返回一个null,如果想要线程变量有初始值需要通过子类继承ThreadLocal的方式去重写此方法,通常可以通过匿名内部类的方式实现。这个方法是protected修饰的,是为了让子类覆盖而设计的。
ThreadLocalMap源码分析
ThreadLocalMap是ThreadLocal的静态内部类,没有实现Map接口,独立实现了Map的功能,内部的Entry也是独立实现的。
与HashMap类似,初始容量默认是16,初始容量必须是2的整数幂。通过Entry类的数据table存放数据。size是存放的数量,threshold是扩容阈值。
Entry继承自WeakReference,key是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
弱引用和内存泄漏
内存溢出:没有足够的内存供申请者提供
内存泄漏:程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等验证后沟。内存泄漏的堆积会导致内存溢出。
弱引用:垃圾回收器一旦发现了弱引用的对象,不管内存是否足够,都会回收它的内存。
内存泄漏的根源是ThreadLocalMap和Thread的生命周期是一样长的。
如果在ThreadLocalMap的key使用强引用还是无法完全避免内存泄漏,ThreadLocal使用完后,ThreadLocal Reference被回收,但是Map的Entry强引用了ThreadLocal,ThreadLocal就无法被回收,因为强引用链的存在,Entry无法被回收,最后会内存泄漏。
在实际情况中,ThreadLocalMap中使用的key为ThreadLocal的弱引用,value是强引用。如果ThreadLocal没有被外部强引用的话,在垃圾回收的时候,key会被清理,value不会。这样ThreadLocalMap就出现了为null的Entry。如果不做任何措施,value永远不会被GC回收,就会产生内存泄漏。
ThreadLocalMap中考虑到这个情况,在set、get、remove操作后,会清理掉key为null的记录(将value也置为null)。使用完ThreadLocal后最后手动调用remove方法(删除Entry)。
也就是说,使用完ThreadLocal后,线程仍然运行,如果忘记调用remove方法,弱引用比强引用可以多一层保障,弱引用的ThreadLocal会被回收,对应的value会在下一次ThreadLocalMap调用get、set、remove方法的时候被清除,从而避免了内存泄漏。
Hash冲突的解决
ThreadLocalMap的构造方法
构造函数创建一个长队为16的Entry数组,然后计算firstKey的索引,存储到table中,设置size和threshold。
firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1)用来计算索引,nextHashCode是Atomicinteger类型的,Atomicinteger类是提供原子操作的Integer类,通过线程安全的方式来加减,适合高并发使用。
每次在当前值上加上一个HASH_INCREMENT值,这个值和斐波拉契数列有关,主要目的是为了让哈希码可以均匀的分布在2的n次方的数组里,从而尽量的避免冲突。
当size为2的幂次的时候,hashCode & (size - 1)相当于取模运算hashCode % size,位运算比取模更高效一些。为了使用这种取模运算, 所有size必须是2的幂次。这样一来,在保证索引不越界的情况下,减少冲突的次数。
ThreadLocalMap的set方法
ThreadLocalMao使用了线性探测法来解决冲突。线性探测法探测下一个地址,找到空的地址则插入,若整个空间都没有空余地址,则产生溢出。例如:长度为8的数组中,当前key的hash值是6,6的位置已经被占用了,则hash值加一,寻找7的位置,7的位置也被占用了,回到0的位置。直到可以插入为止,可以将这个数组看成一个环形数组。
我们今天的关于简谈ThreadLocal和简谈中国式现代化的基本特征的分享就到这里,谢谢您的阅读,如果想了解更多关于Android 详解ThreadLocal及InheritableThreadLocal、FastThreadLocal 是什么鬼?吊打 ThreadLocal 的存在!!、Java 并发编程 ③ - ThreadLocal 和 InheritableThreadLocal 详解、Java多线程之深入解析ThreadLocal和ThreadLocalMap的相关信息,可以在本站进行搜索。
本文标签: