在多线程编程中,如何保证线程间的数据隔离和线程安全是一个重要问题。Java提供的ThreadLocal
类通过让每个线程拥有独立的数据副本,巧妙地解决了这个问题。本文将深入分析ThreadLocal
的工作机制,并探讨如何防止内存泄漏。
目录
- ThreadLocal基本使用
- set方法分析
- get方法分析
- remove方法分析
- 设计ThreadLocal需要考虑的问题
- 线程间数据隔离
- 防止内存泄漏
- 总结
1. ThreadLocal基本使用
ThreadLocal
通过为每个线程提供独立的变量副本,确保多线程环境下变量的线程安全性。以下是一个简单的使用示例:
package threadlocaltest;public class Main {private static final ThreadLocal<String> userLocal = new ThreadLocal<>();private static final ThreadLocal<String> domainLocal = new ThreadLocal<>();public String getCurrentUser() {return userLocal.get();}public void setCurrentUser(String str) {userLocal.set(str);}public String getDomain() {return domainLocal.get();}public void setDomain(String str) {domainLocal.set(str);}public static void main(String[] args) {// 获取数据System.out.println(userLocal.get());System.out.println(domainLocal.get());}
}
2. set方法分析
ThreadLocal
的set
方法将值存储在当前线程的ThreadLocalMap
中:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}
}
关键步骤
- 获取当前线程。
- 获取线程的
ThreadLocalMap
对象。 - 如果
ThreadLocalMap
不为空,则调用map.set
方法存储值。 - 如果
ThreadLocalMap
为空,则初始化一个新的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
的set
方法:
private void set(ThreadLocal<?> key, Object value) {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();}
}
3. get方法分析
ThreadLocal
的get
方法从当前线程的ThreadLocalMap
中获取值:
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;}}return setInitialValue();
}
关键步骤
- 获取当前线程。
- 获取线程的
ThreadLocalMap
对象。 - 调用
map.getEntry
方法获取Entry。 - 如果Entry不为空,则返回Entry的值。
- 如果
ThreadLocalMap
为空,则初始化一个默认值。
ThreadLocalMap
的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);}
}
ThreadLocalMap
的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;
}
4. remove方法分析
ThreadLocal
的remove
方法用于从当前线程的ThreadLocalMap
中移除值:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this);}
}
ThreadLocalMap
的remove
方法:
private void remove(ThreadLocal<?> key) {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)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}
}
5. 设计ThreadLocal需要考虑的问题
5.1 线程间数据隔离
为了实现线程间数据隔离,Java将ThreadLocal
集成到了每个线程中:
ThreadLocal.ThreadLocalMap threadLocals = null;
5.2 防止内存泄漏
内存泄漏的发生时机
当内存空间被占满且无法释放时,会导致内存泄漏。
防止内存泄漏的措施
为了防止内存泄漏,可以采取以下措施将无用的数据及时回收掉。
标识无用数据
在ThreadLocal
中,使用弱引用来标识无用数据。当弱引用的key被GC回收后,key会变成null,从而标识这条数据无用了。
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
何时回收无用数据
可以在每次put或get时检查并回收无用数据,同时提供remove方法,用户可以在适当的时候手动调用以加速回收。
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;tab[staleSlot].value = null;tab[staleSlot] = null;size--;Entry e;int i;for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;while (tab[h] != null) {h = nextIndex(h, len);}tab[h] = e;}}}return i;
}
该回收方法在get
、set
、remove
、数组扩容时都会被使用到。ThreadLocalMap
的key使用弱引用本身就是优化内存泄漏的一种手段,在ThreadLocal
不是全局静态使用方式时,弱引用会生效将key回收,表示这是一条被废弃的数据,但是value还被entry持有,entry被entry数组持有,所以在get
或set
时都会触发expungeStaleEntry
的回收机制,但是为了减少无用数据在内存里的停留时间,可以调用remove
方法加速回收。
总结
在ThreadLocal
中,通过使用弱引用和expungeStaleEntry
方法,可以有效防止内存泄漏。此外,提供remove
方法允许用户手动清理无用数据,从而减少内存占用。这种设计思想在保证线程安全的同时,也对内存管理进行了优化。通过本文的详细分析,希望读者对ThreadLocal
的工作机制和内存管理有更深入的理解。