ThreadLocal usage and full source code analysis

Posted by derksj on Fri, 14 Jan 2022 00:26:23 +0100

1. ThreadLocal introduction

1.1 official introduction

From the description in the official Java document: the ThreadLocal class is used to provide local variables inside the thread. When this variable is accessed in a multithreaded environment (through get and set methods), it can ensure that the variables of each thread are relatively independent of those in other threads. ThreadLocal instances are usually of type private static and are used to associate threads and thread contexts.

We can know that ThreadLocal is used to provide local variables in the thread, and different threads will not interfere with each other. This variable plays a role in the life cycle of the thread, reducing the complexity of passing some public variables between multiple functions or components in the same thread.

Summary:

Thread Concurrency: in the scenario of multithreading concurrency
Pass data: we can pass public variables in the same thread and different components through ThreadLocal
Thread isolation: the variables of each thread are independent and will not affect each other

1.2 basic use

1.2.1 common methods

Before using ThreadLocal, let's know some common methods of ThreadLocal

Method declaration description
ThreadLocal() creates a ThreadLocal object
Public void set (t value) sets the local variable bound by the current thread
public T get() gets the local variable bound by the current thread
public void remove() removes the local variable bound by the current thread
1.2.2 use cases
Let's take a look at the following case and feel the characteristics of ThreadLocal thread isolation:

public class MyDemo {
    private String content;

    private String getContent() {
        return content;
    }

    private void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "Data");
                    System.out.println("-----------------------");
             		System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("thread " + i);
            thread.start();
        }
    }
}

Print results:

From the results, it can be seen that there is no isolation of data between threads due to exceptions when multiple threads access the same variable. Let's take a look at an example of using ThreadLocal to solve this problem.

public class MyDemo1 {

    private static ThreadLocal<String> tl = new ThreadLocal<>();

    private String content;

    private String getContent() {
        return tl.get();
    }

    private void setContent(String content) {
         tl.set(content);
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "Data");
                    System.out.println("-----------------------");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("thread " + i);
            thread.start();
        }
    }
}

Print results:

From the results, this well solves the problem of data isolation between multithreads, which is very convenient.

1.3 ThreadLocal class and synchronized keyword

1.3.1 synchronized synchronization mode

Some friends here may think that in the above example, we can realize this function by locking. Let's first look at the effect achieved with synchronized code blocks:

public class Demo02 {
    
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Demo02 demo02 = new Demo02();
        
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(){
                @Override
                public void run() {
                    synchronized (Demo02.class){
                        demo02.setContent(Thread.currentThread().getName() + "Data");
                        System.out.println("-------------------------------------");
                        String content = demo02.getContent();
                        System.out.println(Thread.currentThread().getName() + "--->" + content);
                    }
                }
            };
            t.setName("thread " + i);
            t.start();
        }
    }
}

Print results:

It can be found from the results that locking can indeed solve this problem, but here we emphasize the problem of thread data isolation, not the problem of multi-threaded data sharing. It is inappropriate to use the synchronized keyword in this case.

1.3.2 difference between ThreadLocal and synchronized

Although both ThreadLocal mode and synchronized keyword are used to deal with the problem of multi-threaded concurrent access to variables, they have different perspectives and ideas.

synchronized
The principle synchronization mechanism adopts the method of "time for space", which only provides a variable for different threads to queue up for access
ThreadLocal
The method of "space for time" is adopted to provide a copy of variables for each thread, so as to realize simultaneous access without interference
Focus on the synchronization of accessing resources between multiple threads, and isolate the data between each thread in multiple threads
Summary:
In the case just now, although both ThreadLocal and synchronized can solve the problem, ThreadLocal is more appropriate because it can make the program have higher concurrency.

2. Application scenario_ Transaction case

Through the above introduction, we have basically understood the characteristics of ThreadLocal. But what scene is it used in? Next, let's look at a case: transaction operations.

2.1 transfer cases

2.1.1 scenario construction

Here, we first build a simple transfer scenario: there is a data table account, in which there are two users Jack and Rose. User Jack transfers money to user Rose.

The implementation of the case mainly uses mysql database, JDBC and C3P0 framework. The following is the detailed code:

(1) project structure

(2) data preparation

(3) dao layer code: AccountDao

public class AccountDao {
public void out(String outUser, int money) throws SQLException {
    String sql = "update account set money = money - ? where name = ?";

    Connection conn = JdbcUtils.getConnection();
    PreparedStatement pstm = conn.prepareStatement(sql);
    pstm.setInt(1,money);
    pstm.setString(2,outUser);
    pstm.executeUpdate();

    JdbcUtils.release(pstm,conn);
}

public void in(String inUser, int money) throws SQLException {
    String sql = "update account set money = money + ? where name = ?";

    Connection conn = JdbcUtils.getConnection();
    PreparedStatement pstm = conn.prepareStatement(sql);
    pstm.setInt(1,money);
    pstm.setString(2,inUser);
    pstm.executeUpdate();

    JdbcUtils.release(pstm,conn);
}
}

(4) service layer code: AccountService

public class AccountService {

    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        try {
            // Transfer out
            ad.out(outUser, money);
            // to change into
            ad.in(inUser, money);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

(5) Tool class: JdbcUtils

public class JdbcUtils { 
public static void commitAndClose(Connection conn) {
    try {
        if(conn != null){
            //Commit transaction
            conn.commit();
            //Release connection
            conn.close();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

public static void rollbackAndClose(Connection conn) {
    try {
        if(conn != null){
            //Rollback transaction
            conn.rollback();
            //Release connection
            conn.close();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}
} 

2.1.2 import transaction

The transfer in the case involves two DML operations: one transfer out and one transfer in. These operations need to be atomic and inseparable. Otherwise, data modification exceptions may occur.

public class AccountService {
    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        try {
            // Transfer out
            ad.out(outUser, money);
            // Exceptions during simulated transfer
            int i = 1/0;
            // to change into
            ad.in(inUser, money);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

Therefore, it is necessary to operate transactions to ensure that the transfer out and transfer in operations are atomic, either successful or failed at the same time.

(1) api for transaction operation in JDBC

Method function of Connection interface
void setAutoCommit(false) disable automatic transaction commit (change to manual)
void commit(); Commit transaction
void rollback(); Rollback transaction
(2) Precautions for starting a transaction:

In order to ensure that all operations are in one transaction, the connection used in the case must be the same: the connection of the service layer to open the transaction must be consistent with the connection of the dao layer to access the database

When threads are concurrent, each thread can only operate its own connection

2.2 general solutions
2.2.1 realization of conventional scheme
Based on the premise given above, the solutions you usually think of are:

Pass parameter: pass the connection object from the service layer to the dao layer
Lock
The following is the code implementation modification:

(1) accountservice class

public class AccountService {
    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        //In the case of thread concurrency, in order to ensure that each thread uses its own connection, it is locked
        synchronized (AccountService.class) {

            Connection conn = null;
            try {
                conn = JdbcUtils.getConnection();
                //Open transaction
                conn.setAutoCommit(false);
                // Transfer out
                ad.out(conn, outUser, money);
                // Exceptions during simulated transfer
//            int i = 1/0;
                // to change into
                ad.in(conn, inUser, money);
                //Transaction commit
                JdbcUtils.commitAndClose(conn);
            } catch (Exception e) {
                e.printStackTrace();
                //Transaction rollback
                JdbcUtils.rollbackAndClose(conn);
                return false;
            }
            return true;
        }
    }
}

(2) AccountDao class (note here: the connection cannot be released in the dao layer, but in the service layer, otherwise the service layer cannot be used if it is released in the dao layer)

public class AccountDao {
    public void out(Connection conn, String outUser, int money) throws SQLException{
        String sql = "update account set money = money - ? where name = ?";
        //Comment: the code that obtains the connection from the connection pool and uses the connection passed from the service
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();
        //The connection cannot be released here. It needs to be used in the service layer
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }

    public void in(Connection conn, String inUser, int money) throws SQLException {
        String sql = "update account set money = money + ? where name = ?";
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }
}

2.2.2 disadvantages of conventional scheme

We can see that the above methods have indeed solved the problem as required, but careful observation will find the disadvantages of this implementation:

The connection is directly transferred from the service layer to the dao layer, resulting in the improvement of code coupling

Locking will cause threads to lose concurrency and reduce program performance

2.3 ThreadLocal solution

2.3.1 implementation of ThreadLocal scheme

For such a scenario that requires data transfer and thread isolation in the project, we might as well use ThreadLocal to solve it:

(1) modification of tool class: add ThreadLocal

public class JdbcUtils {
    //ThreadLocal object: bind the connection to the current thread
    private static final ThreadLocal<Connection> tl = new ThreadLocal();

    // c3p0 database connection pool object properties
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();

    // Get connection
    public static Connection getConnection() throws SQLException {
        //Fetch the connection object bound by the current thread
        Connection conn = tl.get();
        if (conn == null) {
            //If not, remove it from the connection pool
            conn = ds.getConnection();
            //Then bind the connection object to the current thread
            tl.set(conn);
        }
        return conn;
    }

    //Release resources
    public static void release(AutoCloseable... ios) {
        for (AutoCloseable io : ios) {
            if (io != null) {
                try {
                    io.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void commitAndClose() {
        try {
            Connection conn = getConnection();
            //Commit transaction
            conn.commit();
            //Unbind
            tl.remove();
            //Release connection
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose() {
        try {
            Connection conn = getConnection();
            //Rollback transaction
            conn.rollback();
            //Unbind
            tl.remove();
            //Release connection
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

(2) modification of AccountService class: there is no need to pass the connection object

public class AccountService {
    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();

        try {
            Connection conn = JdbcUtils.getConnection();
            //Open transaction
            conn.setAutoCommit(false);
            // Transfer out: there is no need to pass parameters here!
            ad.out(outUser, money);
            // Exceptions during simulated transfer
//            int i = 1 / 0;
            // to change into
            ad.in(inUser, money);
            //Transaction commit
            JdbcUtils.commitAndClose();
        } catch (Exception e) {
            e.printStackTrace();
            //Transaction rollback
           JdbcUtils.rollbackAndClose();
            return false;
        }
        return true;
    }
}

(3) modification of AccountDao class: use as usual

2.3.2 benefits of ThreadLocal scheme

From the above cases, we can see that ThreadLocal scheme has two outstanding advantages in some specific scenarios:

Transfer data: save the data bound by each thread and obtain it directly where needed to avoid the code coupling problem caused by direct parameter transfer

Thread isolation: the data between threads are isolated from each other but have concurrency to avoid performance loss caused by synchronization

3. Internal structure of ThreadLocal

Through the above learning, we have a certain understanding of the role of ThreadLocal. Now let's take a look at the internal structure of ThreadLocal and explore the principle of thread data isolation.

3.1 common misunderstandings

If we don't look at the source code, we may guess that ThreadLocal is designed like this: each ThreadLocal creates a Map, then uses the thread as the key of the Map and the local variable to be stored as the value of the Map, so as to achieve the effect of separating the local variables of each thread. This is the simplest design method. The ThreadLocal in the earliest JDK was designed in this way, but it is not now.

3.2 current design

However, the JDK optimizes the design scheme. In JDK8, the design of ThreadLocal is: each Thread maintains a ThreadLocalMap, the key of this Map is the ThreadLocal instance itself, and the value is the real value Object to be stored.

The specific process is as follows:

There is a Map (ThreadLocalMap) inside each Thread

The Map stores the ThreadLocal object (key) and the variable copy (value) of the thread

The map inside the Thread is maintained by ThreadLocal, which is responsible for obtaining and setting the variable value of the Thread from the map.

For different threads, each time the replica value is obtained, other threads cannot obtain the replica value of the current thread, forming the isolation of replicas without interference with each other.

3.3 benefits of this design

This design is just opposite to the design we said at the beginning. This design has the following two advantages:

After this design, the number of entries stored in each Map will be reduced. Because the previous storage quantity was determined by the number of threads, now it is determined by the number of ThreadLocal. In practical application, the number of ThreadLocal is often less than the number of threads.
After the Thread is destroyed, the corresponding ThreadLocalMap will also be destroyed, which can reduce the use of memory.

4. Source code of the core method of ThreadLocal

Based on the internal structure of ThreadLocal, we continue to analyze its core method source code and have a deeper understanding of its operation principle.

In addition to the construction methods, ThreadLocal has the following four exposed methods:

Method declaration description
protected T initialValue() returns the initial value of the local variable of the current thread
Public void set (t value) sets the local variable bound by the current thread
public T get() gets the local variable bound by the current thread
public void remove() removes the local variable bound by the current thread
The following is a detailed source code analysis of the four methods (in order to ensure clear thinking, the ThreadLocalMap part will not be expanded temporarily, and the next knowledge point will be explained in detail)

4.1 set method

(1) source code and corresponding Chinese Notes

 /**
     * Set the value of ThreadLocal corresponding to the current thread
     *
     * @param value The value of ThreadLocal corresponding to the current thread to be saved
     */
    public void set(T value) {
        // Gets the current thread object
        Thread t = Thread.currentThread();
        // Gets the ThreadLocalMap object maintained in this thread object
        ThreadLocalMap map = getMap(t);
        // Determine whether the map exists
        if (map != null)
            // Map. Is called if it exists Set set this entity entry
            map.set(this, value);
        else
            // 1) The ThreadLocalMap object does not exist for the current Thread
            // 2) Then createMap is called to initialize the ThreadLocalMap object
            // 3) And store t (the current thread) and value (the value corresponding to t) as the first entry in ThreadLocalMap
            createMap(t, value);
    }

 /**
     * Get the ThreadLocalMap maintained corresponding to the current Thread 
     * 
     * @param  t the current thread Current thread
     * @return the map Corresponding maintained ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/**
     *Create the maintained ThreadLocalMap corresponding to the current Thread 
     *
     * @param t Current thread
     * @param firstValue The value of the first entry stored in the map
     */
	void createMap(Thread t, T firstValue) {
        //This here is threadLocal that calls this method
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

(2) code execution process

A. first obtain the current thread and obtain a Map according to the current thread

B. if the obtained Map is not empty, set the parameter to the Map (the reference of the current ThreadLocal is used as the key)

C. if the Map is empty, create a Map for the thread and set the initial value

4.2 get method

(1) source code and corresponding Chinese Notes

 /**
     * Returns the value of ThreadLocal saved in the current thread
     * If the current thread does not have this ThreadLocal variable,
     * Then it initializes the value by calling the {@ link #initialValue} method
     *
     * @return Returns the value of this ThreadLocal corresponding to the current thread
     */
    public T get() {
        // Gets the current thread object
        Thread t = Thread.currentThread();
        // Gets the ThreadLocalMap object maintained in this thread object
        ThreadLocalMap map = getMap(t);
        // If this map exists
        if (map != null) {
            // With the current ThreadLocal as the key, call getEntry to obtain the corresponding storage entity e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // Judge e as empty 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // Get the value corresponding to the storage entity e
                // That is, the value of this ThreadLocal corresponding to the current thread we want
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	Initialization: there are two situations in which the current code is executed
        	The first case: the map does not exist, which means that this thread does not maintain a ThreadLocalMap object
        	The second case: the map exists, but there is no entry associated with the current ThreadLocal
         */
        return setInitialValue();
    }

    /**
     * initialization
     *
     * @return the initial value Initialized value
     */
    private T setInitialValue() {
        // Call initialValue to get the initialized value
        // This method can be overridden by subclasses. If it is not overridden, it returns null by default
        T value = initialValue();
        // Gets the current thread object
        Thread t = Thread.currentThread();
        // Gets the ThreadLocalMap object maintained in this thread object
        ThreadLocalMap map = getMap(t);
        // Determine whether the map exists
        if (map != null)
            // Map. Is called if it exists Set set this entity entry
            map.set(this, value);
        else
            // 1) The ThreadLocalMap object does not exist for the current Thread
            // 2) Then createMap is called to initialize the ThreadLocalMap object
            // 3) And store t (the current thread) and value (the value corresponding to t) as the first entry in ThreadLocalMap
            createMap(t, value);
        // Returns the set value value
        return value;
    }

(2) code execution process

A. first obtain the current thread and obtain a Map according to the current thread

B. if the obtained Map is not empty, use the reference of ThreadLocal in the Map as the key to obtain the corresponding Entry e in the Map, otherwise go to D

C. if e is not null, return e.value, otherwise go to D

D. if the Map is empty or e is empty, obtain the initial value value through the initialValue function, and then use the reference and value of ThreadLocal as the firstKey and firstValue to create a new Map

Summary: first get the ThreadLocalMap variable of the current thread. If it exists, return the value. If it does not exist, create and return the initial value.

4.3 remove method

(1) source code and corresponding Chinese Notes

 /**
     * Delete the entity entry corresponding to ThreadLocal saved in the current thread
     */
     public void remove() {
        // Gets the ThreadLocalMap object maintained in the current thread object
         ThreadLocalMap m = getMap(Thread.currentThread());
        // If this map exists
         if (m != null)
            // Map. Is called if it exists remove
            // Delete the corresponding entity entry with the current ThreadLocal as the key
             m.remove(this);
     }

(2) code execution process

A. first obtain the current thread and obtain a Map according to the current thread

B. if the obtained Map is not empty, remove the entry corresponding to the current ThreadLocal object

4.4 initialValue method

/**
  * Returns the initial value of ThreadLocal corresponding to the current thread
  
  * The first call to this method occurs when the thread accesses the ThreadLocal value of this thread through the get method
  * Unless the thread calls the set method first, in this case, initialValue will not be called by the thread.
  * Typically, this method is called at most once per thread.
  *
  * <p>This method simply returns null {@code null};
  * If the programmer wants the ThreadLocal thread local variable to have an initial value other than null,
  * This method must be overridden by subclassing {@ code ThreadLocal}
  * Usually, it can be implemented by anonymous inner classes
  *
  * @return The initial value of the current ThreadLocal
  */
protected T initialValue() {
    return null;
}

This method returns the initial value of the thread local variable.

(1) This method is a deferred call method. From the above code, we know that it is executed only when the get method is called before the set method is called, and it is only executed once.

(2) The default implementation of this method returns a null directly.

(3) If you want an initial value other than null, you can override this method. (Note: this method is a protected method, which is obviously designed to be covered by subclasses)

5. ThreadLocalMap source code analysis

When analyzing the ThreadLocal method, we know that the operation of ThreadLocal is actually expanded around ThreadLocalMap. The source code of ThreadLocalMap is relatively complex. We discuss it from the following three aspects.

5.1 basic structure

ThreadLocalMap is an internal class of ThreadLocal. It does not implement the Map interface. It implements the function of Map in an independent way, and its internal Entry is also implemented independently.

(1) Member variable

  /**
     * Initial capacity - must be an integral power of 2
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The definitions of table and Entry classes for storing data are analyzed below
     * Similarly, the array length must be an integral power of 2.
     */
    private Entry[] table;

    /**
     * The number of entries in the array can be used to judge whether the current usage of table exceeds the threshold.
     */
    private int size = 0;

    /**
     * The threshold for capacity expansion. Capacity expansion is performed when the table usage is greater than it.
     */
    private int threshold; // Default to 0

Similar to HashMap, initial_ Capability represents the initial capacity of the Map; Table is an array of Entry type, which is used to store data; Size represents the number of stores in the table; Threshold represents the corresponding size threshold when capacity expansion is required.

(2) Storage structure - Entry

/*
 * Entry Inherit the WeakReference and use ThreadLocal as the key
 * If the key is null(entry.get() == null), it means that the key is no longer referenced,
 * Therefore, the entry can also be cleared from the table at this time.
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

In ThreadLocalMap, Entry is also used to save K-V structure data. However, the key in the Entry can only be a ThreadLocal object, which is limited in the construction method.

In addition, Entry inherits WeakReference, that is, key (ThreadLocal) is a weak reference. Its purpose is to unbind the life cycle of ThreadLocal object and thread life cycle.

5.2 weak references and memory leaks

Some programmers will find a memory leak when using ThreadLocal. They guess that the memory leak is related to the weak referenced key used in the Entry. This understanding is actually wrong.

Let's first review several noun concepts involved in this problem, and then analyze the problem.

(1) Memory leak related concepts

Memory overflow: memory overflow. There is not enough memory for the applicant.
Memory leak: memory leak refers to the heap memory that has been dynamically allocated in the program. For some reason, the program does not release or cannot release, resulting in a waste of system memory, slowing down the running speed of the program and even system crash. The accumulation of memory leaks will eventually lead to memory overflow.
(2) Weak reference related concepts

There are four types of references in Java: strong, soft, weak and virtual. At present, this problem mainly involves strong reference and weak reference:

Strong Reference is the most common common object Reference. As long as there is a strong Reference pointing to an object, it can indicate that the object is still "alive", and the garbage collector will not recycle this object.

Weak reference: once the garbage collector finds an object with only weak reference, it will reclaim its memory whether the current memory space is sufficient or not.

(3) If the key uses strong references

Assuming that the key in ThreadLocalMap uses a strong reference, will there be a memory leak?

At this time, the memory diagram of ThreadLocal (solid line indicates strong reference) is as follows:

Suppose threadLocal Ref is recycled after ThreadLocal is used in business code.

However, because the Entry of threadLocalMap strongly references threadLocal, threadLocal cannot be recycled.

On the premise that the Entry is not manually deleted and the CurrentThread is still running, there is always a strong reference chain threadref - > CurrentThread - > threadlocalmap - > Entry, and the Entry will not be recycled (the Entry includes ThreadLocal instance and value), resulting in Entry memory leakage.

In other words, the key in ThreadLocalMap uses strong reference, which can not completely avoid memory leakage.

(5) If the key uses a weak reference

If the key in ThreadLocalMap uses a weak reference, will there be a memory leak?

At this time, the memory diagram of ThreadLocal (solid line indicates strong reference and dotted line indicates weak reference) is as follows:

Similarly, suppose threadLocal Ref is recycled after ThreadLocal is used in business code.

Because ThreadLocalMap only holds weak references to ThreadLocal and no strong references point to ThreadLocal instances, ThreadLocal can be successfully recycled by gc. At this time, the key in the Entry is null.

However, on the premise that the Entry is not deleted manually and the CurrentThread is still running, there is also a strong reference chain threadref - > CurrentThread - > threadlocalmap - > Entry - > value. Value will not be recycled, and this value will never be accessed, resulting in value memory leakage.

That is, the key in ThreadLocalMap uses a weak reference, which may also lead to memory leakage.

(6) True cause of memory leak

Comparing the above two cases, we will find that the occurrence of memory leakage has nothing to do with whether the key in ThreadLocalMap uses weak reference. So what is the real cause of memory leakage?

Careful students will find that in the above two memory leaks, there are two preconditions:

This Entry was not manually deleted
CurrentThread is still running
The first point is well understood. As long as the remove method is called to delete the corresponding Entry after using ThreadLocal, memory leakage can be avoided.

The second point is a little more complicated. Because ThreadLocalMap is a property of Thread and is referenced by the current Thread, its life cycle is as long as Thread. After using ThreadLocal, if the execution of the current Thread ends, the ThreadLocalMap will naturally be recycled by gc to avoid memory leakage at the root.

To sum up, the root cause of ThreadLocal memory leak is: since the life cycle of ThreadLocalMap is as long as Thread, memory leak will occur if the corresponding key is not manually deleted.

(7) Why use weak references

According to the analysis just now, we know that no matter what type of reference the key in ThreadLocalMap uses, memory leakage cannot be completely avoided, which has nothing to do with the use of weak references.

There are two ways to avoid memory leaks:

After using ThreadLocal, call its remove method to delete the corresponding Entry

After using ThreadLocal, the current Thread will end

Compared with the first method, the second method is obviously more difficult to control. Especially when using thread pool, thread termination will not be destroyed.

In other words, as long as you remember to call remove in time after using ThreadLocal, there will be no problem whether the key is a strong reference or a weak reference. So why do keys use weak references?

In fact, in the set/getEntry method in ThreadLocalMap, it will judge that the key is null (that is, ThreadLocal is null). If it is null, it will set the value to null.

This means that even if you forget to call the remove method after using ThreadLocal and the CurrentThread is still running, the weak reference can provide more protection than the strong reference: the ThreadLocal of the weak reference will be recycled, and the corresponding value will be cleared the next time ThreadLocalMap calls any of the methods set, get and remove, so as to avoid memory leakage.

5.3 hash conflict resolution

The resolution of hash conflict is an important content in Map. Let's take the resolution of hash conflict as a clue to study the core source code of ThreadLocalMap.

(1) Start with the set() method of ThreadLocal

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            //The set method of ThreadLocalMap was called
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        	//The construction method of ThreadLocalMap was called
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

This method, which we just analyzed, is used to set the local variables bound by the current thread:

A. first obtain the current thread and obtain a Map according to the current thread

B. if the obtained Map is not empty, set the parameter to the Map (the reference of the current ThreadLocal is used as the key)

(the set method of ThreadLocalMap is called here)

C. if the Map is empty, create a Map for the thread and set the initial value

(the construction method of ThreadLocalMap is called here)

This code involves two methods of ThreadLocalMap in two places. We will then analyze these two methods.

(2) Construction method threadlocalmap (ThreadLocal <? > firstkey, object firstvalue)

/*
  * firstKey : This ThreadLocal instance (this)
  * firstValue :  Thread local variables to save
  */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //Initialize table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //Calculation index (key code)
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //Set value
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        //Set threshold
        setThreshold(INITIAL_CAPACITY);
    }

The constructor first creates an Entry array with a length of 16, then calculates the index corresponding to the firstKey, stores it in table, and sets size and threshold.

Key analysis: int i = firstkey threadLocalHashCode & (INITIAL_CAPACITY - 1).

a. About firstkey threadLocalHashCode:

 	private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
//AtomicInteger is an Integer class that provides atomic operations. It operates addition and subtraction in a thread safe manner. It is suitable for high concurrency
    private static AtomicInteger nextHashCode =  new AtomicInteger();
     //Special hash value
    private static final int HASH_INCREMENT = 0x61c88647;

Here, an AtomicInteger type is defined, which gets the current value and adds hash each time_ INCREMENT´╝îHASH_INCREMENT = 0x61c88647. This value is related to the Fibonacci sequence (golden section number). Its main purpose is to make the hash code evenly distributed in the n-power array of 2, that is, the Entry[] table. This can avoid hash conflict as much as possible.

b. About & (initial_capability - 1)

Hashcode & (size - 1) algorithm is used to calculate hash, which is equivalent to a more efficient implementation of modular operation hashcode% size. Because of this algorithm, we require that size must be an integral power of 2, which can also reduce the number of hash conflicts on the premise that the index does not cross the boundary.

(3) set method in ThreadLocalMap

private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //Calculate index (key code, just analyzed)
        int i = key.threadLocalHashCode & (len-1);
        /**
         * Find elements using linear probing (key codes)
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            //The key corresponding to ThreadLocal exists and directly overwrites the previous value
            if (k == key) {
                e.value = value;
                return;
            }
            // The key is null, but the value is not null, indicating that the previous ThreadLocal object has been recycled,
           // The Entry in the current array is a stale element
            if (k == null) {
                //Replace old elements with new elements. This method performs many garbage cleaning actions to prevent memory leakage
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
    	//If the key corresponding to ThreadLocal does not exist and no old element is found, a new Entry is created at the position of the empty element.
            tab[i] = new Entry(key, value);
            int sz = ++size;
            /**
             * cleanSomeSlots Used to clear elements with e.get()==null,
             * The object associated with this data key has been recycled, so the Entry(table[index]) can be set to null.
             * If no entry is cleared and the current usage reaches the load factor definition (2 / 3 of the length), proceed to 				 *  rehash (perform a full table scan cleanup)
             */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

 /**
     * Gets the next index of the ring array
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

Code execution process:

A. First, calculate the index I according to the key, and then find the Entry at position i,

B. If the Entry already exists and the key is equal to the passed in key, then directly assign a new value to the Entry,

C. If the Entry exists but the key is null, call replacestateentry to replace the Entry with an empty key,

D. Continue loop detection until a null place is encountered. At this time, if you have not return ed during the loop, create an Entry at the null position, insert it, and increase the size by 1.

Finally, we call cleanSomeSlots to clean up key's Entry for null, and finally return to clean up Entry. Then we'll decide if sz is > thresgold = rehash, and then we will call the rehash function to perform a sweep scan of the whole table.

Key analysis: ThreadLocalMap uses linear detection to solve hash conflicts.

This method detects the next address at a time until there is an empty address. If the empty address cannot be found in the whole space, overflow will be generated.

For example, suppose the length of the current table is 16, that is, if the hash value of the calculated key is 14, if there is already a value on table[14], and its key is inconsistent with the current key, then a hash conflict occurs. At this time, 14 plus 1 will get 15, and table[15] will be used for judgment. At this time, if the conflict still returns to 0, take table[0], and so on, Until it can be inserted.

According to the above description, the Entry[] table can be regarded as a ring array.

reference material: https://www.bilibili.com/video/BV1N741127FH

Topics: Java Multithreading Concurrent Programming