java基础解析系列(七)---ThreadLocal原理分析

目录

作用

  • 与同步机制区分开来,同步机制是为了解决在共享情况下并发导致的问题。而ThreadLocal是避免了共享
  • 在多线程情况下,为了避免共享,我们可以采用多线程多实例的方式,也可以使用ThreadLocal来避免共享冲突

什么是ThreadLocal

  • ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定

实验

public class T {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
public void set() {
longLocal.set(Thread.currentThread().getId());
}
public long getLong() {
return longLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final T test = new T();
test.set();
Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println("线程一:"+test.getLong());
};
};
thread1.start();
thread1.join();
System.out.println("main线程:"+test.getLong());
System.out.println("没有发生值的覆盖,两个线程保存的值是不同的");
}
}
  • 输出: 线程一:11 main线程:1 没有发生值的覆盖,两个线程保存的值是不同的
  • 证明ThreadLocal确实为变量在每个线程中都创建了一个副本

Thread的成员

ThreadLocal.ThreadLocalMap threadLocals = null;
  • Thread有一个ThreadLocalMap成员

ThreadLocal的set方法

179    public void set(T value) {
180 Thread t = Thread.currentThread();
181 ThreadLocalMap map = getMap(t);
182 if (map != null)
183 map.set(this, value);
184 else
185 createMap(t, value);
186 }
  • 181行通过当前线程获取ThreadLocalMap
212    ThreadLocalMap getMap(Thread t) {
213 return t.threadLocals;
214 }
  • 185行如果当前线程的成员threadLocals还是空的,创建一个map
224    void createMap(Thread t, T firstValue) {
225 t.threadLocals = new ThreadLocalMap(this, firstValue);
226 }
  • 将当前的ThreadLocal对象和value作为参数
328        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
329 table = new Entry[INITIAL_CAPACITY];
330 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
331 table[i] = new Entry(firstKey, firstValue);
332 size = 1;
333 setThreshold(INITIAL_CAPACITY);
334 }

java基础解析系列(七)---ThreadLocal原理分析-LMLPHP

  • 通过ThreadLocalMap的构造方法可以看到,该方法创建一个Entry数组,然后通过传入的key(当前ThreadLocal对象)计算在数组中的下标,然后将Entry放入数组。
  • Thread的ThreadLocalMap存放的Entry,键是不同的ThreadLoacal对象,也就是说一个线程绑定多个ThreadLocal对象
  • 那么也就是说,ThreadLocal设置值的时候,这个值是存放在当前线程的一个map(这个map存放了多个ThreadLocal对象)里面,因此,不同线程之间即避免了共享

ThreadLocalMap的set方法

416        private void set(ThreadLocal key, Object value) {
417
418 // We don't use a fast path as with get() because it is at
419 // least as common to use set() to create new entries as
420 // it is to replace existing ones, in which case, a fast
421 // path would fail more often than not.
422
423 Entry[] tab = table;
424 int len = tab.length;
425 int i = key.threadLocalHashCode & (len-1);
426
427 for (Entry e = tab[i];
428 e != null;
429 e = tab[i = nextIndex(i, len)]) {
430 ThreadLocal k = e.get();
431
432 if (k == key) {
433 e.value = value;
434 return;
435 }
436
437 if (k == null) {
438 replaceStaleEntry(key, value, i);
439 return;
440 }
441 }
442
443 tab[i] = new Entry(key, value);
444 int sz = ++size;
445 if (!cleanSomeSlots(i, sz) && sz >= threshold)
446 rehash();
447 }
  • 分析这段代码,他解决hash冲突的办法不同与hashmap使用链表来解决冲突问题。通过计算key的hashcode获取数组中的下标后,然后进入427行,432判断要放入的键是否和该下标中原来的键相同,是的话进行值的覆盖。如果为空的,放入该Entry。如果不同的话且不为空,看当前下标+1的位置,同样进入循环。依次执行下去。

ThreadLocal的get方法

142    public T get() {
143 Thread t = Thread.currentThread();
144 ThreadLocalMap map = getMap(t);
145 if (map != null) {
146 ThreadLocalMap.Entry e = map.getEntry(this);
147 if (e != null)
148 return (T)e.value;
149 }
150 return setInitialValue();
151 }
  • 可以发现,执行get方法的时候,也是先获取当前线程,然后获得该线程的一个ThreadLocalMap成员,通过这个map和和当前的ThreadLocal对象作为键,来获取value

内存泄露

271        static class Entry extends WeakReference<ThreadLocal> {
  • 如果ThreadLocal不使用弱引用(这篇文章有介绍),那么当ThradLocal th=null时候,因为ThreadLocalMap仍然有th的强引用,所以并不能回收。而如果key使用弱引用的时候,th为null的时候,下次回收的时候就会将这个key回收
  • 但是有一个问题,虽然这个key可以被回收,但是这个value仍然有强引用,并不能回收。如果当前线程不结束,并且不调用set/get/remove方法(这些方法会对key为null的entry进行释放),这片内存会被一直占用。这就是内存泄露的原因
  • 因此在用完ThreadLocal的时候,记得执行remove方法,避免内存泄露

简单总结

  • 同一个ThreadLocal对象,不同的线程,不同的ThreadLocal对象的值

我觉得分享是一种精神,分享是我的乐趣所在,不是说我觉得我讲得一定是对的,我讲得可能很多是不对的,但是我希望我讲的东西是我人生的体验和思考,是给很多人反思,也许给你一秒钟、半秒钟,哪怕说一句话有点道理,引发自己内心的感触,这就是我最大的价值。(这是我喜欢的一句话,也是我写博客的初衷)

04-24 23:38