最近碰到一个使用ThreadLocal时因为未调用remove()而险些引起内存溢出的问题,所以看了下ThreadLocal的源码,结合线程池原理做一个简单的分析,确认是否最终会导致内存溢出。


既然是因为没调用remove()方法而险些导致内存溢出,那首先看下remove()方法中做了什么。

1. public void
2. ThreadLocalMap m = getMap(Thread.currentThread());
3. if (m != null)
4. this);
5. }


从remove()的实现来看就是一个map.remove()的调用。既然不调用map.remove()可能会引起内存溢出的话,就需要看看ThreadLocalMap的实现了。

1. /**
2. * ThreadLocalMap is a customized hash map suitable only for
3. * maintaining thread local values. No operations are exported
4. * outside of the ThreadLocal class. The class is package private to
5. * allow declaration of fields in class Thread. To help deal with
6. * very large and long-lived usages, the hash table entries use
7. * WeakReferences for keys. However, since reference queues are not
8. * used, stale entries are guaranteed to be removed only when
9. * the table starts running out of space.
10. */
11. static class
12.
13. /**
14. * The entries in this hash map extend WeakReference, using
15. * its main ref field as the key (which is always a
16. * ThreadLocal object). Note that null keys (i.e. entry.get()
17. * == null) mean that the key is no longer referenced, so the
18. * entry can be expunged from table. Such entries are referred to
19. * as "stale entries" in the code that follows.
20. */
21. static class Entry extends
22. /** The value associated with this ThreadLocal. */
23. Object value;
24.
25. Entry(ThreadLocal k, Object v) {
26. super(k);
27. value = v;
28. }
29. }
30.
31. /**
32. * The initial capacity -- MUST be a power of two.
33. */
34. private static final int INITIAL_CAPACITY = 16;
35.
36. /**
37. * The table, resized as necessary.
38. * table.length MUST always be a power of two.
39. */
40. private
41.
42. /**
43. * The number of entries in the table.
44. */
45. private int size = 0;
46.
47. /**
48. * The next size value at which to resize.
49. */
50. private int threshold; // Default to 0
51.
52. /**
53. * Set the resize threshold to maintain at worst a 2/3 load factor.
54. */
55. private void setThreshold(int
56. 2 / 3;
57. }
58.
59. /**
60. * Increment i modulo len.
61. */
62. private static int nextIndex(int i, int
63. return ((i + 1 < len) ? i + 1 : 0);
64. }
65.
66. /**
67. * Decrement i modulo len.
68. */
69. private static int prevIndex(int i, int
70. return ((i - 1 >= 0) ? i - 1 : len - 1);
71. }
72.
73. /**
74. * Construct a new map initially containing (firstKey, firstValue).
75. * ThreadLocalMaps are constructed lazily, so we only create
76. * one when we have at least one entry to put in it.
77. */
78. ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
79. new
80. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
81. new
82. 1;
83. setThreshold(INITIAL_CAPACITY);
84. }
85.
86. /**
87. * Construct a new map including all Inheritable ThreadLocals
88. * from given parent map. Called only by createInheritedMap.
89. *
90. * @param parentMap the map associated with parent thread.
91. */
92. private
93. Entry[] parentTable = parentMap.table;
94. int
95. setThreshold(len);
96. new
97.
98. for (int j = 0; j < len; j++) {
99. Entry e = parentTable[j];
100. if (e != null) {
101. ThreadLocal key = e.get();
102. if (key != null) {
103. Object value = key.childValue(e.value);
104. new
105. int h = key.threadLocalHashCode & (len - 1);
106. while (table[h] != null)
107. h = nextIndex(h, len);
108. table[h] = c;
109. size++;
110. }
111. }
112. }
113. }
114.
115. /**
116. * Get the entry associated with key. This method
117. * itself handles only the fast path: a direct hit of existing
118. * key. It otherwise relays to getEntryAfterMiss. This is
119. * designed to maximize performance for direct hits, in part
120. * by making this method readily inlinable.
121. *
122. * @param key the thread local object
123. * @return the entry associated with key, or null if no such
124. */
125. private
126. int i = key.threadLocalHashCode & (table.length - 1);
127. Entry e = table[i];
128. if (e != null
129. return
130. else
131. return
132. }
133.
134. /**
135. * Version of getEntry method for use when key is not found in
136. * its direct hash slot.
137. *
138. * @param key the thread local object
139. * @param i the table index for key's hash code
140. * @param e the entry at table[i]
141. * @return the entry associated with key, or null if no such
142. */
143. private Entry getEntryAfterMiss(ThreadLocal key, int
144. Entry[] tab = table;
145. int
146.
147. while (e != null) {
148. ThreadLocal k = e.get();
149. if
150. return
151. if (k == null)
152. expungeStaleEntry(i);
153. else
154. i = nextIndex(i, len);
155. e = tab[i];
156. }
157. return null;
158. }
159.
160. /**
161. * Set the value associated with key.
162. *
163. * @param key the thread local object
164. * @param value the value to be set
165. */
166. private void
167.
168. // We don't use a fast path as with get() because it is at
169. // least as common to use set() to create new entries as
170. // it is to replace existing ones, in which case, a fast
171. // path would fail more often than not.
172.
173. Entry[] tab = table;
174. int
175. int i = key.threadLocalHashCode & (len-1);
176.
177. for
178. e != null;
179. e = tab[i = nextIndex(i, len)]) {
180. ThreadLocal k = e.get();
181.
182. if
183. e.value = value;
184. return;
185. }
186.
187. if (k == null) {
188. replaceStaleEntry(key, value, i);
189. return;
190. }
191. }
192.
193. new
194. int
195. if
196. rehash();
197. }
198.
199. /**
200. * Remove the entry for key.
201. */
202. private void
203. Entry[] tab = table;
204. int
205. int i = key.threadLocalHashCode & (len-1);
206. for
207. e != null;
208. e = tab[i = nextIndex(i, len)]) {
209. if
210. e.clear();
211. expungeStaleEntry(i);
212. return;
213. }
214. }
215. }
216.
217. /**
218. * Replace a stale entry encountered during a set operation
219. * with an entry for the specified key. The value passed in
220. * the value parameter is stored in the entry, whether or not
221. * an entry already exists for the specified key.
222. *
223. * As a side effect, this method expunges all stale entries in the
224. * "run" containing the stale entry. (A run is a sequence of entries
225. * between two null slots.)
226. *
227. * @param key the key
228. * @param value the value to be associated with key
229. * @param staleSlot index of the first stale entry encountered while
230. * searching for key.
231. */
232. private void
233. int
234. Entry[] tab = table;
235. int
236. Entry e;
237.
238. // Back up to check for prior stale entry in current run.
239. // We clean out whole runs at a time to avoid continual
240. // incremental rehashing due to garbage collector freeing
241. // up refs in bunches (i.e., whenever the collector runs).
242. int
243. for (int
244. (e = tab[i]) != null;
245. i = prevIndex(i, len))
246. if (e.get() == null)
247. slotToExpunge = i;
248.
249. // Find either the key or trailing null slot of run, whichever
250. // occurs first
251. for (int
252. (e = tab[i]) != null;
253. i = nextIndex(i, len)) {
254. ThreadLocal k = e.get();
255.
256. // If we find key, then we need to swap it
257. // with the stale entry to maintain hash table order.
258. // The newly stale slot, or any other stale slot
259. // encountered above it, can then be sent to expungeStaleEntry
260. // to remove or rehash all of the other entries in run.
261. if
262. e.value = value;
263.
264. tab[i] = tab[staleSlot];
265. tab[staleSlot] = e;
266.
267. // Start expunge at preceding stale entry if it exists
268. if
269. slotToExpunge = i;
270. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
271. return;
272. }
273.
274. // If we didn't find stale entry on backward scan, the
275. // first stale entry seen while scanning for key is the
276. // first still present in the run.
277. if (k == null
278. slotToExpunge = i;
279. }
280.
281. // If key not found, put new entry in stale slot
282. null;
283. new
284.
285. // If there are any other stale entries in run, expunge them
286. if
287. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
288. }
289.
290. /**
291. * Expunge a stale entry by rehashing any possibly colliding entries
292. * lying between staleSlot and the next null slot. This also expunges
293. * any other stale entries encountered before the trailing null. See
294. * Knuth, Section 6.4
295. *
296. * @param staleSlot index of slot known to have null key
297. * @return the index of the next null slot after staleSlot
298. * (all between staleSlot and this slot will have been checked
299. * for expunging).
300. */
301. private int expungeStaleEntry(int
302. Entry[] tab = table;
303. int
304.
305. // expunge entry at staleSlot
306. null;
307. null;
308. size--;
309.
310. // Rehash until we encounter null
311. Entry e;
312. int
313. for
314. (e = tab[i]) != null;
315. i = nextIndex(i, len)) {
316. ThreadLocal k = e.get();
317. if (k == null) {
318. null;
319. null;
320. size--;
321. else
322. int h = k.threadLocalHashCode & (len - 1);
323. if
324. null;
325.
326. // Unlike Knuth 6.4 Algorithm R, we must scan until
327. // null because multiple entries could have been stale.
328. while (tab[h] != null)
329. h = nextIndex(h, len);
330. tab[h] = e;
331. }
332. }
333. }
334. return
335. }
336.
337. /**
338. * Heuristically scan some cells looking for stale entries.
339. * This is invoked when either a new element is added, or
340. * another stale one has been expunged. It performs a
341. * logarithmic number of scans, as a balance between no
342. * scanning (fast but retains garbage) and a number of scans
343. * proportional to number of elements, that would find all
344. * garbage but would cause some insertions to take O(n) time.
345. *
346. * @param i a position known NOT to hold a stale entry. The
347. * scan starts at the element after i.
348. *
349. * @param n scan control: <tt>log2(n)</tt> cells are scanned,
350. * unless a stale entry is found, in which case
351. * <tt>log2(table.length)-1</tt> additional cells are scanned.
352. * When called from insertions, this parameter is the number
353. * of elements, but when from replaceStaleEntry, it is the
354. * table length. (Note: all this could be changed to be either
355. * more or less aggressive by weighting n instead of just
356. * using straight log n. But this version is simple, fast, and
357. * seems to work well.)
358. *
359. * @return true if any stale entries have been removed.
360. */
361. private boolean cleanSomeSlots(int i, int
362. boolean removed = false;
363. Entry[] tab = table;
364. int
365. do
366. i = nextIndex(i, len);
367. Entry e = tab[i];
368. if (e != null && e.get() == null) {
369. n = len;
370. true;
371. i = expungeStaleEntry(i);
372. }
373. while ( (n >>>= 1) != 0);
374. return
375. }
376.
377. /**
378. * Re-pack and/or re-size the table. First scan the entire
379. * table removing stale entries. If this doesn't sufficiently
380. * shrink the size of the table, double the table size.
381. */
382. private void
383. expungeStaleEntries();
384.
385. // Use lower threshold for doubling to avoid hysteresis
386. if (size >= threshold - threshold / 4)
387. resize();
388. }
389.
390. /**
391. * Double the capacity of the table.
392. */
393. private void
394. Entry[] oldTab = table;
395. int
396. int newLen = oldLen * 2;
397. new
398. int count = 0;
399.
400. for (int j = 0; j < oldLen; ++j) {
401. Entry e = oldTab[j];
402. if (e != null) {
403. ThreadLocal k = e.get();
404. if (k == null) {
405. null; // Help the GC
406. else
407. int h = k.threadLocalHashCode & (newLen - 1);
408. while (newTab[h] != null)
409. h = nextIndex(h, newLen);
410. newTab[h] = e;
411. count++;
412. }
413. }
414. }
415.
416. setThreshold(newLen);
417. size = count;
418. table = newTab;
419. }
420.
421. /**
422. * Expunge all stale entries in the table.
423. */
424. private void
425. Entry[] tab = table;
426. int
427. for (int j = 0; j < len; j++) {
428. Entry e = tab[j];
429. if (e != null && e.get() == null)
430. expungeStaleEntry(j);
431. }
432. }
433. }


首先从声明上来看,ThreadLocalMap并不是一个java.util.Map接口的实现,但是从Entry的实现和整个ThreadLocalMap的实现来看却实现了一个Map的功能,并且从具体的方法的实现上来看,整个ThreadLocalMap实现了一个HashMap的功能,对比HashMap的实现就能看出。



但是,值得注意的是ThreadLocalMap并没有put(K key, V value)方法,而是set(ThreadLocal key, Object value),从这里可以看出,ThreadLocalMap并不是想象那样以Thread为key,而是以ThreadLocal为key。


了解了ThreadLocalMap的实现,也知道ThreadLocal.remove()其实就是ThreadLocalMap.remove(),那么再看看ThreadLocal的set(T value)方法,看看value是如何存储的。

1. public void
2. Thread t = Thread.currentThread();
3. ThreadLocalMap map = getMap(t);
4. if (map != null)
5. this, value);
6. else
7. createMap(t, value);
8. }
9.
10. ThreadLocalMap getMap(Thread t) {
11. return
12. }
13.
14. void
15. new ThreadLocalMap(this, firstValue);
16. }

可以看到,set(T value)方法为每个Thread对象都创建了一个ThreadLocalMap,并且将value放入ThreadLocalMap中,ThreadLocalMap作为Thread对象的成员变量保存。那么可以用下图来表示ThreadLocal在存储value时的关系。





所以当ThreadLocal作为单例时,每个Thread对应的ThreadLocalMap中只会有一个键值对。那么如果不调用remove()会怎么样呢?



假设一种场景,使用线程池,线程池中有200个线程,并且这些线程都不会释放,ThreadLocal做单例使用。那么最多也就会产生200个ThreadLocalMap,而每个ThreadLocalMap中只有一个键值对,那最多也就是200个键值对存在。



但是线程池并不是固定一个线程数不改变,下面贴一段tomcat的线程池配置

1. <Connector executor="tomcatThreadPool"
2. "8080" protocol="HTTP/1.1"
3. "60000"
4. "30000"
5. "5"
6. "75"
7. "150"
8. "8443" URIEncoding="UTF-8" acceptCount="1000" disableUploadTimeout="true"/>


可以看到线程池其实有线程最小值和最大值的,并且有超时时间,所以当线程空闲时间超时后,线程会被销毁。那么当线程销毁时,线程所持有的ThreadLocalMap也会失去引用,并且由于ThreadLocalMap中的Entry是WeakReference,所以当YGC时,被销毁的Thread所对应的value也会被回收掉,所以即使不调用remove()方法,也不会引起内存溢出。