[Java] Detailed Analysis of ThreadLocal

Comprehensive analysis of ThreadLocal

Pre-knowledge

  • Have a certain javase and javaweb foundation
  • Familiar with the synchronized keyword
  • Familiar with HashMap
  • Familiar with JDBC technology

learning target

  • Understand the introduction of ThreadLocal
  • Master the application scenarios of ThreadLocal
  • Understand the internal structure of ThreadLocal
  • Understand the core method source code of ThreadLocal
  • Understand the source code of ThreadLocalMap

1. Introduction to ThreadLocal

1.1 Official introduction

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code 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).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal&lt;Integer&gt; threadId =
 *         new ThreadLocal&lt;Integer&gt;() {
 *             &#64;Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code ThreadLocal}
 * instance is accessible; after a thread goes away, all of its copies of
 * thread-local instances are subject to garbage collection (unless other
 * references to these copies exist).
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
    
    
    ...

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

我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
总结:
1. 线程并发: 在多线程并发的场景下
2. 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3. 线程隔离: 每个线程的变量都是独立的,不会互相影响

1.2 Basic use

1.2.1 Common methods

​ Before using it, let's get to know some common methods of ThreadLocal

method declaration describe
ThreadLocal() Create a ThreadLocal object
public void set( T value) Set a local variable bound to the current thread
public T get() Get the local variables bound by the current thread
public void remove() Remove local variables bound by the current thread

1.2.2 Use cases

我们来看下面这个案例	, 感受一下ThreadLocal 线程隔离的特点: 
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() + "的数据");
                    System.out.println("-----------------------");
             		System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

print result:

1574149020726

​ From the results, it can be seen that the exception occurs when multiple threads access the same variable, and the data between threads is not isolated. Let's look at an example of using ThreadLocal to solve this problem.

public class MyDemo {
    
    

    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() + "的数据");
                    System.out.println("-----------------------");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

print result:

image-20230813162503280

Judging from the results, this is a good solution to the problem of data isolation between multiple threads, which is very convenient.

1.3 ThreadLocal class and synchronized keyword

1.3.1 synchronized synchronization method

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

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() + "的数据");
                        System.out.println("-------------------------------------");
                        String content = demo02.getContent();
                        System.out.println(Thread.currentThread().getName() + "--->" + content);
                    }
                }
            };
            t.setName("线程" + i);
            t.start();
        }
    }
}

print result:

1578321788844

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

1.3.2 The difference between ThreadLocal and synchronized

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

synchronized ThreadLocal
principle The synchronization mechanism adopts the method of "exchanging time for space", and only provides a copy of variables to allow different threads to queue up for access ThreadLocal adopts the method of "exchanging space for time", providing each thread with a copy of variables, so as to achieve simultaneous access without interfering with each other
focus Synchronization of access to resources between multiple threads In multithreading, the data between each thread is isolated from each other
总结: 在刚刚的案例中,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。

2. Application Scenario_Business Case

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

2.1 Transfer case

2.1.1 Scene construction

​ Here we first build a simple transfer scenario: There is a data table account, which contains two users Jack and Rose, and 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

1574045241145

(2) Data preparation

-- 使用数据库
use test;
-- 创建一张账户表
create table account(
	id int primary key auto_increment,
	name varchar(20),
	money double
);
-- 初始化数据
insert into account values(null, 'Jack', 1000);
insert into account values(null, 'Rose', 0);

​ (3) C3P0 configuration files and tools

<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
 <!--  连接参数 -->
 <property name="driverClass">com.mysql.jdbc.Driver</property>
 <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
 <property name="user">root</property>
 <property name="password">1234</property>
 
 <!-- 连接池参数 -->
 <property name="initialPoolSize">5</property>
 <property name="maxPoolSize">10</property>
 <property name="checkoutTimeout">3000</property>
</default-config>

</c3p0-config>

​ (4) Tool class: JdbcUtils

package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
    
    
    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    // 获取连接
    public static Connection getConnection() throws SQLException {
    
    
        return ds.getConnection();
    }
    //释放资源
    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(Connection conn) {
    
    
        try {
    
    
            if(conn != null){
    
    
                //提交事务
                conn.commit();
                //释放连接
                conn.close();
            }
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose(Connection conn) {
    
    
        try {
    
    
            if(conn != null){
    
    
                //回滚事务
                conn.rollback();
                //释放连接
                conn.close();
            }
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
    }
}

​ (5) dao layer code: AccountDao

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

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);
    }
}

​ (6) service layer code: AccountService

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import java.sql.SQLException;

public class AccountService {
    
    

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

​ (7) web layer code: AccountWeb

package com.itheima.transfer.web;

import com.itheima.transfer.service.AccountService;

public class AccountWeb {
    
    

    public static void main(String[] args) {
    
    
        // 模拟数据 : Jack 给 Rose 转账 100
        String outUser = "Jack";
        String inUser = "Rose";
        int money = 100;

        AccountService as = new AccountService();
        boolean result = as.transfer(outUser, inUser, money);

        if (result == false) {
    
    
            System.out.println("转账失败!");
        } else {
    
    
            System.out.println("转账成功!");
        }
    }
}

2.1.2 Introducing transactions

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

public class AccountService {
    
    
    public boolean transfer(String outUser, String inUser, int money) {
    
    
        AccountDao ad = new AccountDao();
        try {
    
    
            // 转出
            ad.out(outUser, money);
            // 模拟转账过程中的异常
            int i = 1/0;
            // 转入
            ad.in(inUser, money);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

​ So here we need to operate transactions to ensure that the transfer-out and transfer-in operations are atomic, and either succeed at the same time or fail at the same time.

(1) API for transaction operations in JDBC

Methods of the Connection interface effect
void setAutoCommit(false) Disable automatic transaction commit (manual instead)
void commit(); commit transaction
void rollback(); rollback transaction

(2) Points to note when 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 needs to be consistent with the connection of the dao layer to access the database

  • In the case of thread concurrency, each thread can only operate its own connection

2.2 Conventional solutions

2.2.1 Realization of conventional schemes

Based on the premise given above, the solutions you usually think of are:

  • Parameter passing: Pass the connection object from the service layer to the dao layer
  • lock

The following is the modified part of the code implementation:

​ (1) AccountService class

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {
    
    

    public boolean transfer(String outUser, String inUser, int money) {
    
    
        AccountDao ad = new AccountDao();
        //线程并发情况下,为了保证每个线程使用各自的connection,故加锁
        synchronized (AccountService.class) {
    
    

            Connection conn = null;
            try {
    
    
                conn = JdbcUtils.getConnection();
                //开启事务
                conn.setAutoCommit(false);
                // 转出
                ad.out(conn, outUser, money);
                // 模拟转账过程中的异常
//            int i = 1/0;
                // 转入
                ad.in(conn, inUser, money);
                //事务提交
                JdbcUtils.commitAndClose(conn);
            } catch (Exception e) {
    
    
                e.printStackTrace();
                //事务回滚
                JdbcUtils.rollbackAndClose(conn);
                return false;
            }
            return true;
        }
    }
}

​ (2) AccountDao class (It should be noted here that the connection cannot be released in the dao layer, but in the service layer, otherwise it will be released in the dao layer, and the service layer will not be available)

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {
    
    

    public void out(Connection conn, String outUser, int money) throws SQLException{
    
    
        String sql = "update account set money = money - ? where name = ?";
        //注释从连接池获取连接的代码,使用从service中传递过来的connection
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();
        //连接不能在这里释放,service层中还需要使用
//        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 schemes

We can see that the above method has indeed solved the problem as required, but if you observe carefully, you will find the disadvantages of this implementation:

  1. Pass the connection directly from the service layer to the dao layer, resulting in an increase in code coupling

  2. Locking will cause threads to lose concurrency and program performance will decrease

2.3 ThreadLocal solution

2.3.1 Implementation of ThreadLocal scheme

For scenarios like this that require data transfer and thread isolation in a project , we might as well use ThreadLocal to solve it:

​ (1) Modification of tool class: Add ThreadLocal

package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
    
    
    //ThreadLocal对象 : 将connection绑定在当前线程中
    private static final ThreadLocal<Connection> tl = new ThreadLocal();

    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();

    // 获取连接
    public static Connection getConnection() throws SQLException {
    
    
        //取出当前线程绑定的connection对象
        Connection conn = tl.get();
        if (conn == null) {
    
    
            //如果没有,则从连接池中取出
            conn = ds.getConnection();
            //再将connection对象绑定到当前线程中
            tl.set(conn);
        }
        return conn;
    }

    //释放资源
    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();
            //提交事务
            conn.commit();
            //解除绑定
            tl.remove();
            //释放连接
            conn.close();
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose() {
    
    
        try {
    
    
            Connection conn = getConnection();
            //回滚事务
            conn.rollback();
            //解除绑定
            tl.remove();
            //释放连接
            conn.close();
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
    }
}

​ (2) Modification of the AccountService class: No need to pass the connection object

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {
    
    

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

        try {
    
    
            Connection conn = JdbcUtils.getConnection();
            //开启事务
            conn.setAutoCommit(false);
            // 转出 : 这里不需要传参了 !
            ad.out(outUser, money);
            // 模拟转账过程中的异常
//            int i = 1 / 0;
            // 转入
            ad.in(inUser, money);
            //事务提交
            JdbcUtils.commitAndClose();
        } catch (Exception e) {
    
    
            e.printStackTrace();
            //事务回滚
           JdbcUtils.rollbackAndClose();
            return false;
        }
        return true;
    }
}

​ (3) Modification of AccountDao class: use as usual

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

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);
        JdbcUtils.release(pstm);
    }

    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);
        JdbcUtils.release(pstm);
    }
}

2.3.2 Benefits of the ThreadLocal scheme

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

  1. Transfer data: save the data bound by each thread, and obtain it directly where needed, avoiding code coupling problems caused by direct parameter transfer

  2. Thread isolation: The data between each thread is isolated from each other but has concurrency, avoiding performance loss caused by synchronization

3. The internal structure of ThreadLocal

​ Through the above study, we have a certain understanding of the role of ThreadLocal. Now let's take a look at the internal structure of ThreadLocal and explore how it can achieve thread data isolation.

3.1 Common misconceptions

​ If we don't look at the source code, we may guess ThreadLocalthat it is designed like this: ThreadLocalcreate one for each , Mapand then use the thread as the local variable to be stored , so that the local variables of each thread can be isolated Effect. This is the simplest design method. The earliest JDK was indeed designed like this, but it is no longer the case.MapkeyMapvalueThreadLocal

1582788729557

3.2 Current Design

However, the JDK later optimized the design scheme. ThreadLocalThe design in JDK8 is: each Threadmaintains one ThreadLocalMap, and this Map keyis ThreadLocalthe instance itself, valuewhich is the real value to be stored Object.

The specific process is as follows:

(1) Each Thread thread has a Map (ThreadLocalMap) inside
(2) The Map stores the ThreadLocal object (key) and the variable copy (value) of the thread
(3) The Map inside the Thread is maintained by ThreadLocal, ThreadLocal is responsible for obtaining and setting thread variable values ​​to map.
​ (4) For different threads, each time the copy value is obtained, other threads cannot obtain the copy value of the current thread, which forms the isolation of copies and does not interfere with each other.

1574155226793

3.3 Benefits of this design

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

(1) After this design, the number Mapof each storage Entrywill be reduced. Because the previous storage quantity Threadwas determined by the quantity, now it is ThreadLocaldetermined by the quantity. In actual use, the number of ThreadLocals is often less than the number of Threads.

(2) When Threaddestroyed, the corresponding ones ThreadLocalMapwill also be destroyed, which can reduce the use of memory.

4. The core method source code of ThreadLocal

​ Based on the internal structure of ThreadLocal, we continue to analyze its core method source code to gain a deeper understanding of its operating principles.

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

method declaration describe
protected T initialValue() Returns the initial value of the current thread-local variable
public void set( T value) Set a local variable bound to the current thread
public T get() Get the local variables bound by the current thread
public void remove() Remove local variables bound by the current thread

​ The following is the detailed source code analysis of these four methods (in order to ensure a clear idea, the ThreadLocalMap part will not be expanded for the time being, and the next knowledge point will be explained in detail)

4.1 set method

(1) Source code and corresponding Chinese comments

  /**
     * 设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
    
    
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    }

 /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
    
    
        return t.threadLocals;
    }
	/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     *
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
    
    
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

(2) Code execution process

A. First get the current thread, and get a Map based on the current thread

B. If the obtained Map is not empty, set the parameters to the Map (the current ThreadLocal reference 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 comments

    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     *
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
    
    
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
    
    
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
    
    
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	初始化 : 有两种情况有执行当前代码
        	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
        	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

    /**
     * 初始化
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
    
    
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

(2) Code execution process

A. First get the current thread, and get a Map based on the current thread

​ B. If the obtained Map is not empty, use the ThreadLocal reference as the key in the Map 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, use the initialValue function to obtain the initial value value, and then use the ThreadLocal reference and value as firstKey and firstValue to create a new Map

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

4.3 remove method

(1) Source code and corresponding Chinese comments

 /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
    
    
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

(2) Code execution process

A. First get the current thread, and get a Map based on the current thread

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

4.4 initialValue method

/**
  * 返回当前线程对应的ThreadLocal的初始值
  
  * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
  * 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
  * 通常情况下,每个线程最多调用一次这个方法。
  *
  * <p>这个方法仅仅简单的返回null {@code null};
  * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
  * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
  * 通常, 可以通过匿名内部类的方式实现
  *
  * @return 当前ThreadLocal的初始值
  */
protected T initialValue() {
    
    
    return null;
}

​ The function of this method is to return the initial value of the thread local variable.

(1) This method is a delayed call method. From the above code, we know that it is executed 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 one directly null.

(3) If you want an initial value other than null, you can override this method. (Remarks: This method is a protectedunique method, obviously designed to be overridden by subclasses)

5. ThreadLocalMap source code analysis

​ When analyzing the ThreadLocal method, we learned that the operation of ThreadLocal actually revolves around ThreadLocalMap. The source code of ThreadLocalMap is relatively complicated, and 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.

1574266262577

(1) Member variables

    /**
     * 初始容量 —— 必须是2的整次幂
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放数据的table,Entry类的定义在下面分析
     * 同样,数组长度必须是2的整次幂。
     */
    private Entry[] table;

    /**
     * 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
     */
    private int size = 0;

    /**
     * 进行扩容的阈值,表使用量大于它的时候进行扩容。
     */
    private int threshold; // Default to 0
    

​ Similar to HashMap, INITIAL_CAPACITY represents the initial capacity of the Map; table is an array of Entry type used to store data; size represents the number of storage in the table; threshold represents the threshold corresponding to the size when expansion is required.

Link: https://www.jianshu.com/p/acfd2239c9f4

Source: Jianshu

Copyright belongs to the author. For commercial reprint, please contact the author for authorization, for non-commercial reprint, please indicate the source.

(2) Storage structure - Entry

/*
 * Entry继承WeakReference,并且用ThreadLocal作为key.
 * 如果key为null(entry.get() == null),意味着key不再被引用,
 * 因此这时候entry也可以从table中清除。
 */
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 KV structure data. However, the key in Entry can only be a ThreadLocal object, which has been restricted in the construction method.

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

5.2 Weak references and memory leaks

​ Some programmers will find that there is a memory leak in the process of using ThreadLocal, and guess that this memory leak is related to the key that uses weak references in Entry. This understanding is actually wrong.

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

(1) Concepts related to memory leaks

  • Memory overflow: memory overflow, there is not enough memory for the applicant to use.
  • Memory leak: Memory leak refers to the heap memory that has been dynamically allocated in the program is not released or cannot be released for some reason, resulting in a waste of system memory, resulting in serious consequences such as slowing down the running speed of the program and even system crashes. The accumulation of memory leaks will eventually lead to memory overflow.

(2) Weakly refer to related concepts

There are 4 types of references in Java: strong, soft, weak, and virtual. The current problem mainly involves strong references and weak references:

​A strong reference ("Strong" Reference) is our 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 (WeakReference) , once the garbage collector finds an object with only weak references, it will reclaim its memory regardless of whether the current memory space is sufficient or not.

(3) If the key uses a strong reference

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

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

1582902708234

​ Assuming that ThreadLocal is used in business code, threadLocal Ref is recycled.

​ But because threadLocalMap's Entry strongly references threadLocal, threadLocal cannot be recycled.

​ Under 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 ThreadLocal instance and value are included in the Entry), causing the Entry memory leak.

​ That is to say, the key in ThreadLocalMap uses strong references, which cannot completely avoid memory leaks.

(5) If the key uses weak references

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

At this time, the memory map of ThreadLocal (solid lines indicate strong references, dotted lines indicate weak references) is as follows:

1582907143471

It is also assumed that ThreadLocal Ref is recycled after using ThreadLocal in the business code.

​ Since ThreadLocalMap only holds a weak reference to ThreadLocal, and there is no strong reference pointing to the threadlocal instance, threadlocal can be successfully recycled by gc. At this time, key=null in Entry.

​ But under the premise that the Entry is not manually deleted and the CurrentThread is still running, there is also a strong reference chain threadRef->currentThread->threadLocalMap->entry -> value, the value will not be recycled, and this value will never be The access is reached, resulting in a value memory leak.

​ That is to say, the key in ThreadLocalMap uses weak references, and there may be memory leaks.

(6) The real reason for the memory leak

​ Comparing the above two situations, we will find that the occurrence of memory leaks has nothing to do with whether the key in ThreadLocalMap uses weak references. So what is the real cause of the memory leak?

Careful students will find that in the above two memory leak situations, there are two prerequisites:

1. 没有手动删除这个Entry
2. CurrentThread依然运行

​ The first point is easy to understand. As long as the ThreadLocal is used and its remove method is called to delete the corresponding Entry, memory leaks can be avoided.

​ The second point is a little more complicated. Since ThreadLocalMap is an attribute of Thread and is referenced by the current thread, its life cycle is as long as Thread. Then after using ThreadLocal, if the current Thread also ends, ThreadLocalMap will naturally be recycled by gc, avoiding memory leaks from the root cause.

​ In summary, the root cause of ThreadLocal memory leak is : Since the life cycle of ThreadLocalMap is as long as Thread, if the corresponding key is not manually deleted, memory leak will result.

(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 leaks cannot be completely avoided, and it has nothing to do with the use of weak references.

There are two ways to avoid memory leaks:

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

  2. After using ThreadLocal, the current Thread also ends

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

​ 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, the key will be judged as null (that is, ThreadLocal is null), and if it is null, the value will be set to null.

​ This means that after using the ThreadLocal and the CurrentThread is still running, even if you forget to call the remove method, weak references can provide one more layer of protection than strong references : the weakly referenced ThreadLocal will be recycled, and the corresponding value will be set in the next ThreadLocalMap call. Any method in get or remove will be cleared to avoid memory leaks.

5.3 Resolution of hash conflicts

​ The resolution of hash conflicts is an important content in Map. Let's study the core source code of ThreadLocalMap by taking the resolution of hash conflicts as a clue.

(1) First start with the set() method of ThreadLocal

  public void set(T value) {
    
    
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            //调用了ThreadLocalMap的set方法
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
    
    
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
    
    
        	//调用了ThreadLocalMap的构造方法
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

We have analyzed this method just now, and its function is to set the local variables bound by the current thread:

A. First get the current thread, and get a Map based on the current thread

B. If the obtained Map is not empty, set the parameters to the Map (the current ThreadLocal reference 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 constructor of ThreadLocalMap is called here)

There are two places in this code that involve the two methods of ThreadLocalMap respectively, and we will analyze these two methods next.

**(2) Construction method`ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)**

 /*
  * firstKey : 本ThreadLocal实例(this)
  * firstValue : 要保存的线程本地变量
  */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    
    
        //初始化table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //计算索引(重点代码)
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //设置值
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        //设置阈值
        setThreshold(INITIAL_CAPACITY);
    }

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

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

a. Regarding firstKey.threadLocalHashCode:

 	private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
    
    
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
    private static AtomicInteger nextHashCode =  new AtomicInteger();
     //特殊的hash值
    private static final int HASH_INCREMENT = 0x61c88647;

​ An AtomicInteger type is defined here, each time the current value is obtained and HASH_INCREMENT is added, HASH_INCREMENT = 0x61c88647this value is related to the Fibonacci sequence (golden section number), and its main purpose is to make the hash code evenly distributed in 2 In the n-th power array, that is, in the Entry[] table, doing so can avoid hash conflicts as much as possible.

b. About& (INITIAL_CAPACITY - 1)

​ The hashCode & (size - 1) algorithm is used to calculate the hash, which is equivalent to a more efficient implementation of the modulo operation hashCode % size. It is precisely because of this algorithm that we require that the size must be an integral power of 2, which can also ensure that the number of hash collisions is reduced on the premise that the index does not cross the boundary.

(3) The set method in ThreadLocalMap

private void set(ThreadLocal<?> key, Object value) {
    
    
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引(重点代码,刚才分析过了)
        int i = key.threadLocalHashCode & (len-1);
        /**
         * 使用线性探测法查找元素(重点代码)
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
    
    
            ThreadLocal<?> k = e.get();
            //ThreadLocal 对应的 key 存在,直接覆盖之前的值
            if (k == key) {
    
    
                e.value = value;
                return;
            }
            // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
           // 当前数组中的 Entry 是一个陈旧(stale)的元素
            if (k == null) {
    
    
                //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
    	//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            /**
             * cleanSomeSlots用于清除那些e.get()==null的元素,
             * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
             * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行				 * rehash(执行一次全表的扫描清理工作)
             */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

 /**
     * 获取环形数组的下一个索引
     */
    private static int nextIndex(int i, int len) {
    
    
        return ((i + 1 < len) ? i + 1 : 0);
    }

​ Code execution flow:

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

B. If the Entry already exists and the key is equal to the incoming key, then directly assign a new value to the Entry at this time,

C. If the Entry exists, but the key is null, call replaceStaleEntry to replace the Entry whose key is empty.

D. Continuously loop detection until it encounters a place that is null. At this time, if it has not returned during the loop process, then create a new Entry at the null position and insert it, and the size increases by 1 at the same time.

​ Finally call cleanSomeSlots to clean up the Entry whose key is null, and finally return whether the Entry is cleaned up, and then judge whether sz >= thresgold meets the rehash condition. If so, the rehash function will be called to perform a full table scan and cleanup.

Key analysis : ThreadLocalMap is used 线性探测法to resolve hash conflicts.

​ This method detects the next address at a time and inserts it until there is an empty address. If there is no empty address in the entire space, an overflow will occur.

​ For example, suppose the current table length is 16, that is to say, if the calculated hash value of the 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, add 14 to 1 to get 15, and take table[15] for judgment. At this time, if there is still a conflict, it will return 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 circular array.

Guess you like

Origin blog.csdn.net/m0_47015897/article/details/132260978