How does ThreadLocal ensure that objects are only accessed by the current thread?
Let’s dive into the internal implementation of ThreadLocal.
The ones we need to focus on are, of course, ThreadLocal’s set() and get() methods.
set
Let’s start with the set() method:
/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map ! = null) map.set(this, value); else createMap(t, value); }Copy the code
When we set, we first get the current thread object, then use getMap() to get the thread’s ThreadLocalMap, and store the value into ThreadLocalMap.
A ThreadLocalMap can be thought of as a Map (it’s not, but you can think of it simply as a HashMap), but as a member defined inside a Thread.
Note that the following definitions are taken from the Thread class
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
Copy the code
The data that is set to ThreadLocal is also written to the Map of threadLocals.
Where key is the current ThreadLocal object and value is the desired value.
ThreadLocals itself stores all the “local variables” of the current thread, which is a collection of ThreadLocal variables.
Here is also a benefit for developers who want to improve:Java Advanced Notes, full PDF file click here for free.
get
When the get() method is used, it is natural to pull the data out of the Map.
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ 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(); }Copy the code
The get() method retrieves the current thread’s ThreadLocalMap object and then retrieves the actual data inside by making itself the key.
Thread.exit()
Given the internal implementation of ThreadLocal, the question naturally arises:
These variables are maintained inside the Thread class (ThreadLocalMap is defined in this class), which means that references to the object will persist as long as the Thread does not exit.
When a Thread exits, the Thread class does some cleaning, including cleaning up ThreadLocalMap.
/** * This method is called by the system to give a Thread * a chance to clean up before it actually exits. */ 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; }Copy the code
Therefore, using a thread pool means that the current thread does not necessarily exit (for example, with a fixed size thread pool, threads always exist).
If so, setting some large objects into a ThreadLocal (which is actually stored in a ThreadLocalMap held by the thread) can expose the system to the possibility of memory leaks.
What I mean by this is that you set an object into a Threadlocal, but don’t clean it up, and after you use it a few times, the object is no longer useful, but it can’t be reclaimed.
At this point, if you want to reclaim the object in time, it is best to remove the variable using threadlocal.remove (), as we would normally close the database connection.
If you do not need the object, you should tell the virtual machine to reclaim it to prevent memory leaks.
tl = null
Another interesting case is that the JDK might also allow you to release ThreadLocal just like a normal variable.
For example, we sometimes write code like obj = NULL to speed up garbage collection.
If you do this, the object obj points to will be more easily discovered by the garbage collector, speeding up collection.
Similarly, if we set the variables of a ThreadLocal to NULL manually, such as tl = NULL, then all local variables of the thread corresponding to the ThreadLocal can be reclaimed.
What’s the secret?
Let’s start with a simple example.
package com.shockang.study.java.concurrent.thread_local; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadLocalDemo_Gc { static volatile ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>() { protected void finalize() throws Throwable { System.out.println(this.toString() + " is gc"); }}; static volatile CountDownLatch cd = new CountDownLatch(10000); public static class ParseDate implements Runnable { int i = 0; public ParseDate(int i) { this.i = i; } public void run() { try { if (tl.get() == null) { tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") { protected void finalize() throws Throwable { System.out.println(this.toString() + " is gc"); }}); System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat"); } Date t = tl.get().parse("2015-03-29 19:29:" + i % 60); } catch (ParseException e) { e.printStackTrace(); } finally { cd.countDown(); } } } public static void main(String[] args) throws InterruptedException { ExecutorService es = Executors.newFixedThreadPool(10); for (int i = 0; i < 10000; i++) { es.execute(new ParseDate(i)); } cd.await(); System.out.println("mission complete!!" ); tl = null; System.gc(); System.out.println("first GC complete!!" ); Tl = new ThreadLocal<SimpleDateFormat>(); cd = new CountDownLatch(10000); for (int i = 0; i < 10000; i++) { es.execute(new ParseDate(i)); } cd.await(); Thread.sleep(1000); System.gc(); System.out.println("second GC complete!!" ); }}Copy the code
The example above is to track the garbage collection of ThreadLocal objects and the internal SimpleDateFormat objects.
To do this, we override the Finalize () method.
This way, we can see where objects are when they are recycled.
In the main function, there are two task commits, each with 10,000 tasks.
After the first task commit, we set the TL to NULL and do a GC.
Next, we do a second task commit, and when it’s done, we do another GC.
One of the most likely outputs from the above code is shown below.
19:create SimpleDateFormat 15:create SimpleDateFormat 17:create SimpleDateFormat 18:create SimpleDateFormat 20:create SimpleDateFormat 14:create SimpleDateFormat 11:create SimpleDateFormat 12:create SimpleDateFormat 13:create SimpleDateFormat 16:create SimpleDateFormat mission complete!! first GC complete!! com.shockang.study.java.concurrent.thread_local.ThreadLocalDemo_Gc$1@5041865d is gc 11:create SimpleDateFormat 14:create SimpleDateFormat 20:create SimpleDateFormat 12:create SimpleDateFormat 16:create SimpleDateFormat 13:create SimpleDateFormat 18:create SimpleDateFormat 15:create SimpleDateFormat 17:create SimpleDateFormat 19:create SimpleDateFormat second GC complete!!Copy the code
Notice what these outputs mean.
First, each of the 10 threads in the thread pool creates an instance of the SimpleDateFormat object.
After the first GC, you can see that the ThreadLocal object is reclaimed (anonymous classes are used here, so the name of the class is a bit strange, and this class is the T object created at the beginning).
Commit the second task, this time also create 10 SimpleDateFormat objects, and then do the second GC.
After the second GC, all 10 subclass instances of SimpleDateFormat created the first time are reclaimed.
Although we did not manually remove these objects, it is still possible for the system to reclaim them.
ThreadLocal.ThreadLocalMap
To understand the recycling mechanism, we need to further understand the ThreadLocal ThreadLocalMap implementation.
As we said earlier, ThreadLocalMap is something like a HashMap.
To be more precise, it is more similar to WeakHashMap.
The implementation of ThreadLocalMap uses weak references.
A weak reference is a much weaker reference than a strong reference.
If a weak reference is found during garbage collection, the Java virtual machine immediately collects it.
A ThreadLocalMap consists of a series of entries, each of which is a WeakReference< ThreadLocal>.
/** * 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; }}Copy the code
The parameter k is the Map key and v is the Map value, where K is also a ThreadLocal instance and is used as a weak reference.
Super (k) is the constructor that calls WeakReference
So while ThreadLocal is used as the Map’s key, it doesn’t actually hold a reference to ThreadLocal.
When a ThreadLocal’s external strong reference is reclaimed, the key in the ThreadLocalMap becomes null.
When the system does a ThreadLocalMap cleanup (adding new variables to the table, for example, does an automatic cleanup, although the JDK may or may not do a thorough scan, but it clearly worked in this case), the garbage data is recycled.
ThreadLocal’s collection mechanism
The collection mechanism of ThreadLocal is shown in figure 1.