培训网站,天津网站建设电焊机,做网站设计和推广,软文广告平台原文地址#xff1a;cmsblogs.com/?p2442 ThreadLocal介绍 ThreadLocal提供了一种解决多线程环境下成员变量的问题#xff0c;但是它并不是解决多线程共享变量的问题。那么ThreadLocal到底是什么呢#xff1f; API是这样介绍的#xff1a;This class provides thread-loca… 原文地址cmsblogs.com/?p2442 ThreadLocal介绍 ThreadLocal提供了一种解决多线程环境下成员变量的问题但是它并不是解决多线程共享变量的问题。那么ThreadLocal到底是什么呢 API是这样介绍的This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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). 该类提供了线程局部thread-local变量。这些变量不同于普通对应物因为访问某个变量通过其get或set方法的每个线程都有自己的局部变量它独立于变量的初始化副本。ThreadLocal实例通常是类中的private static字段它们希望将状态与某一个线程例如用户ID或事务ID相关联。 ThreadLocal与线程同步机制不同线程同步机制是多个线程共享同一个变量而ThreadLocal为了每一个线程创建一个单独的变量副本故而每个线程都可以独立地改变自己所拥有的变量副本而不会影响其他线程所对应的副本。 ThreadLocal使用示例代码如下 public class SeqCount {private static ThreadLocalInteger seqCount new ThreadLocalInteger() {// 实现initialValue()public 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 thread1 new SeqThread(seqCount);SeqThread thread2 new SeqThread(seqCount);SeqThread thread3 new SeqThread(seqCount);SeqThread thread4 new SeqThread(seqCount);thread1.start();thread2.start();thread3.start();thread4.start();}private static class SeqThread extends Thread {private SeqCount seqCount;SeqThread(SeqCount seqCount) {this.seqCount seqCount;}public void run() {for (int i 0; i 3; i) {System.out.println(Thread.currentThread().getName() seqCount : seqCount.nextSeq());}}}
}
复制代码ThreadLocal实现原理 ThreadLocal的实现是这样的每个Thread维护一个ThreadLocalMap映射表这个映射表的key是 ThreadLocal实例本身value是真正需要存储的Object。 也就是说ThreadLocal本身并不存储值它只是作为一个key来让线程从ThreadLocalMap获取 value。值得注意的是图中的虚线表示ThreadLocalMap是使用ThreadLocal的弱引用作为Key的弱引用的对象在GC时会被回收。 ThreadLocal源码分析 ThreadLocalMap ThreadLocalMap的构造函数如下 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其内部利用Entry来实现key-value的存储如下 static class Entry extends WeakReferenceThreadLocal? {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal? k, Object v) {super(k);value v;}
}
复制代码可以看出Entry的key就是ThreadLocal而value就是值。同时Entry也继承WeakReference所以说Entry所对应keyThreadLocal实例的引用为一个弱引用。 接下来看看ThreadLocalMap最核心的方法set(ThreadLocal key, Object value)、getEntry()方法。 1、set(ThreadLocal? key, Object value) 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实例不存在则创建tab[i] new Entry(key, value);int sz size;// cleanSomeSlots 清楚陈旧的Entrykey null// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值则进行 rehashif (!cleanSomeSlots(i, sz) sz threshold)rehash();}复制代码set()操作除了存储元素外还有一个很重要的作用就是replaceStaleEntry()和cleanSomeSlots()这两个方法可以清除掉key null 的实例防止内存泄漏。在set()方法中还有一个变量很重要threadLocalHashCode定义如下 private final int threadLocalHashCode nextHashCode();
复制代码从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值定义为final表示ThreadLocal一旦创建其散列值就已经确定了生成过程则是调用nextHashCode() private static AtomicInteger nextHashCode new AtomicInteger();private static final int HASH_INCREMENT 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
复制代码nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量。 2、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;elsereturn getEntryAfterMiss(key, i, e);
}
复制代码采用了开放定址法所以当前key的散列值和元素在数组的索引并不是完全对应的首先取一个探测数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;// 当key null时调用了expungeStaleEntry()方法该方法用于处理key null// 有利于GC回收能够有效地避免内存泄漏。if (k null)expungeStaleEntry(i);elsei nextIndex(i, len);e tab[i];}return null;}
复制代码ThreadLocal核心方法 set(T value)设置当前线程的线程局部变量的值 public void set(T value) {Thread t Thread.currentThread();ThreadLocalMap map getMap(t);if (map ! null)map.set(this, value);elsecreateMap(t, value);
}void createMap(Thread t, T firstValue) {t.threadLocals new ThreadLocalMap(this, firstValue);
}
复制代码获取当前线程所对应的ThreadLocalMap如果不为空则调用ThreadLocalMap的set()方法key就是当前ThreadLocal如果不存在则调用createMap()方法创建。 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;}}// 如果ThreadLocalMap不存在返回初始值。return setInitialValue();
}
复制代码首先通过当前线程获取所对应的成员变量ThreadLocalMap然后通过ThreadLocalMap获取当前ThreadLocal的Entry最后通过所获取的Entry获取目标值result。 initialValue()返回该线程局部变量的初始值 protected T initialValue() {return null;
}
复制代码这个方法将在一个线程第一次使用get方法访问变量时被调用除非线程先前调用了set方法在这种情况下线程不会调用initialValue方法。通常情况下每个线程最多调用一次此方法但在后续调用remove和get时可能会再次调用此方法。 默认实现返回null如果程序员希望线程局部变量具有非null的初始值则必须对ThreadLocal进行子类化并重写此方法。 remove()将当前线程局部变量的值删除 public void remove() {ThreadLocalMap m getMap(Thread.currentThread());if (m ! null)m.remove(this);
}
复制代码该方法的目的是减少内存的占用。当然我们不需要显示调用该方法因为一个线程结束后它所对应的局部变量就会被垃圾回收。 ThreadLocal为什么会内存泄漏 ThreadLocalMap使用ThreadLocal的弱引用作为key如果一个ThreadLocal没有外部强引用来引用它那么系统GC的时候这个ThreadLocal势必会被回收ThreadLocalMap中就会出现key为null的Entry就没有办法访问这些key为null的Entry的value。如果当前线程再迟迟不结束的话这些key为null的Entry的value就会一直存在一条强引用链Thread Ref - Thread - ThreaLocalMap - Entry - value永远无法回收造成内存泄漏。 其实ThreadLocalMap的设计中已经考虑到这种情况也加上了一些防护措施在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。 但是这些被动的预防措施并不能保证不会内存泄漏 使用static的ThreadLocal延长了ThreadLocal的生命周期可能导致的内存泄漏。 分配使用了ThreadLocal又不再调用get(),set(),remove()方法那么就会导致内存泄漏。 ThreadLocal内存泄漏的根源是由于ThreadLocalMap的生命周期跟Thread一样长如果没有手动删除对应key就会导致内存泄漏而不是因为弱引用。 理解了ThreadLocal内存泄漏的前因后果那么怎么避免内存泄漏呢 每次使用完ThreadLocal都调用它的remove()方法清除数据。在使用线程池的情况下没有及时清理ThreadLocal不仅是内存泄漏的问题更严重的是可能导致业务逻辑出现问题。所以使用ThreadLocal就跟加锁完要解锁一样用完就清理。 参考资料 【死磕Java并发】—–深入分析ThreadLocal 深入分析 ThreadLocal 内存泄漏问题 如果读完觉得有收获的话欢迎点赞、关注、加公众号【牛觅技术】查阅更多精彩历史