[Java] Análisis detallado de ThreadLocal

Análisis exhaustivo de ThreadLocal

conocimiento previo

  • Tener una cierta base javase y javaweb
  • Familiarizado con la palabra clave sincronizada
  • Familiarizado con HashMap
  • Familiarizado con la tecnología JDBC

objetivo de aprendizaje

  • Comprender la introducción de ThreadLocal
  • Domine los escenarios de aplicación de ThreadLocal
  • Comprender la estructura interna de ThreadLocal
  • Comprender el código fuente del método central de ThreadLocal
  • Comprender el código fuente de ThreadLocalMap

1. Introducción a ThreadLocal

1.1 Introducción oficial

/**
 * 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> {
    
    
    ...

De la descripción en la documentación oficial de Java: la clase ThreadLocal se usa para proporcionar variables locales dentro del hilo. Cuando se accede a esta variable en un entorno de subprocesos múltiples (se accede a través de métodos get y set), puede garantizar que las variables de cada subproceso sean relativamente independientes de las variables en otros subprocesos. Las instancias de ThreadLocal suelen ser de tipo estático privado y se utilizan para asociar hilos y contextos de hilos.

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

1.2 Uso básico

1.2.1 Métodos comunes

​ Antes de usarlo, conozcamos algunos métodos comunes de ThreadLocal

declaración de método describir
Subproceso Local() Crear un objeto ThreadLocal
conjunto vacío público (valor T) Establecer una variable local vinculada al hilo actual
T público obtener () Obtener las variables locales vinculadas por el hilo actual
eliminación de vacío público () Eliminar las variables locales vinculadas por el hilo actual

1.2.2 Casos de uso

我们来看下面这个案例	, 感受一下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();
        }
    }
}

imprimir resultado:

1574149020726

A partir de los resultados, se puede ver que la excepción se produce cuando varios subprocesos acceden a la misma variable y los datos entre subprocesos no están aislados. Veamos un ejemplo del uso de ThreadLocal para resolver este problema.

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

imprimir resultado:

imagen-20230813162503280

A juzgar por los resultados, esta es una buena solución al problema del aislamiento de datos entre múltiples subprocesos, lo cual es muy conveniente.

1.3 Clase ThreadLocal y palabra clave sincronizada

1.3.1 método de sincronización sincronizada

Algunos amigos aquí pueden pensar que en el ejemplo anterior, podemos lograr esta función agregando bloqueos. Veamos primero el efecto logrado con el bloque de código sincronizado:

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

imprimir resultado:

1578321788844

A partir de los resultados, se puede encontrar que el bloqueo puede resolver este problema, pero aquí enfatizamos el problema del aislamiento de datos de subprocesos, no el problema de compartir datos entre subprocesos múltiples. En este caso, es inapropiado usar la palabra clave sincronizada. .

1.3.2 La diferencia entre ThreadLocal y sincronizado

Aunque el modo ThreadLocal y la palabra clave sincronizada se usan para tratar el problema del acceso simultáneo de subprocesos múltiples a las variables, las perspectivas e ideas de los dos tratan el problema son diferentes.

sincronizado ThreadLocal
principio El mecanismo de sincronización adopta el método de "intercambiar tiempo por espacio" y solo proporciona una copia de las variables para permitir que diferentes subprocesos se pongan en cola para acceder ThreadLocal adopta el método de "cambiar espacio por tiempo", proporcionando a cada subproceso una copia de las variables, para lograr un acceso simultáneo sin interferir entre sí.
enfocar Sincronización de acceso a recursos entre múltiples hilos En subprocesos múltiples, los datos entre cada subproceso están aislados entre sí.
总结: 在刚刚的案例中,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。

2. Escenario de aplicación_Caso comercial

A través de la introducción anterior, básicamente hemos entendido las características de ThreadLocal. Pero, ¿en qué se usa exactamente? A continuación, veamos un caso: las operaciones transaccionales.

2.1 Caja de transferencia

2.1.1 Construcción de escena

Aquí primero creamos un escenario de transferencia simple: hay una cuenta de tabla de datos, que contiene dos usuarios, Jack y Rose, y el usuario Jack transfiere dinero al usuario Rose.

La implementación del caso utiliza principalmente la base de datos mysql, JDBC y el marco C3P0. El siguiente es el código detallado:

(1) Estructura del proyecto

1574045241145

(2) Preparación de datos

-- 使用数据库
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) Herramientas y archivos de configuración de C3P0

<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) Clase de herramienta: 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) código de capa dao: 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) código de capa de servicio: 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) código de capa web: 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 Introducción de transacciones

La transferencia en este caso implica dos operaciones DML: una transferencia de salida y una transferencia de entrada. Estas operaciones deben ser atómicas e indivisibles. De lo contrario, pueden ocurrir excepciones de modificación de datos.

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

Entonces, aquí necesitamos operar transacciones para garantizar que las operaciones de transferencia de salida y transferencia de entrada sean atómicas, y tengan éxito al mismo tiempo o fallen al mismo tiempo.

(1) API para operaciones de transacciones en JDBC

Métodos de la interfaz de conexión efecto
void setAutoCommit(falso) Deshabilitar la confirmación automática de transacciones (manual en su lugar)
compromiso nulo(); confirmar transacción
anular la reversión (); transacción de reversión

(2) Puntos a tener en cuenta al iniciar una transacción:

  • Para garantizar que todas las operaciones estén en una transacción, la conexión utilizada en el caso debe ser la misma: la conexión de la capa de servicio para abrir la transacción debe ser coherente con la conexión de la capa dao para acceder a la base de datos.

  • En el caso de la concurrencia de subprocesos, cada subproceso solo puede operar su propia conexión

2.2 Soluciones convencionales

2.2.1 Realización de esquemas convencionales

En base a la premisa anterior, las soluciones en las que suele pensar son:

  • Paso de parámetros: pase el objeto de conexión de la capa de servicio a la capa dao
  • cerrar

La siguiente es la parte modificada de la implementación del código:

(1) Clase de servicio de cuenta

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) clase AccountDao (Cabe señalar aquí que la conexión no se puede liberar en la capa dao, sino en la capa de servicio; de lo contrario, se liberará en la capa dao y la capa de servicio no estará disponible)

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 Desventajas de los esquemas convencionales

Podemos ver que el método anterior de hecho ha resuelto el problema según lo requerido, pero si observa cuidadosamente, encontrará las desventajas de esta implementación:

  1. Pase la conexión directamente desde la capa de servicio a la capa dao, lo que resulta en un aumento en el acoplamiento de código

  2. El bloqueo hará que los subprocesos pierdan concurrencia y el rendimiento del programa disminuirá

2.3 Solución ThreadLocal

2.3.1 Implementación del esquema ThreadLocal

Para escenarios como este que requieren transferencia de datos y aislamiento de subprocesos en un proyecto , también podríamos usar ThreadLocal para resolverlo:

(1) Modificación de la clase de herramienta: 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) Modificación de la clase AccountService: no es necesario pasar el objeto de conexión

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) Modificación de la clase AccountDao: usar como de costumbre

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 Beneficios del esquema ThreadLocal

De los casos anteriores, podemos ver que en algunos escenarios específicos, la solución ThreadLocal tiene dos ventajas sobresalientes:

  1. Transferir datos: guarde los datos enlazados por cada subproceso y obténgalos directamente donde sea necesario, evitando problemas de acoplamiento de código causados ​​por la transferencia directa de parámetros

  2. Aislamiento de subprocesos: los datos entre cada subproceso están aislados entre sí pero tienen concurrencia, lo que evita la pérdida de rendimiento causada por la sincronización.

3. La estructura interna de ThreadLocal

A través del estudio anterior, tenemos una cierta comprensión del papel de ThreadLocal. Ahora echemos un vistazo a la estructura interna de ThreadLocal y exploremos cómo puede lograr el aislamiento de datos de subprocesos.

3.1 Conceptos erróneos comunes

Si no nos fijamos en el código fuente, podemos suponer ThreadLocalque está diseñado así: ThreadLocalcree uno para cada uno y Mapluego use el subproceso como la variable local que se almacenará , de modo que las variables locales de cada subproceso puedan ser Efecto aislado. Este es el método de diseño más simple. El primer JDK fue diseñado así, pero ya no es así.MapkeyMapvalueThreadLocal

1582788729557

3.2 Diseño actual

Sin embargo, el JDK luego optimizó el esquema de diseño.El ThreadLocaldiseño en JDK8 es: cada uno Threadmantiene uno ThreadLocalMap, y este Map keyes ThreadLocalla instancia en sí, valueque es el valor real a almacenar Object.

El proceso específico es el siguiente:

(1) Cada subproceso tiene un Mapa (ThreadLocalMap) dentro
(2) El Mapa almacena el objeto ThreadLocal (clave) y la copia variable (valor) del subproceso
(3) El Mapa dentro del Subproceso es mantenido por ThreadLocal, ThreadLocal es responsable de obtener y establecer valores de variables de hilo para mapear.
(4) Para diferentes subprocesos, cada vez que se obtiene el valor de copia, otros subprocesos no pueden obtener el valor de copia del subproceso actual, lo que forma el aislamiento de las copias y no interfiere entre sí.

1574155226793

3.3 Beneficios de este diseño

​ Este diseño es justo lo contrario de lo que decíamos al principio, este diseño tiene las siguientes dos ventajas:

(1) Después de este diseño, se reducirá el número Mapde cada almacenamiento . EntryDebido a que la cantidad de almacenamiento anterior Threadestaba determinada por la cantidad, ahora está ThreadLocaldeterminada por la cantidad. En el uso real, la cantidad de ThreadLocals suele ser menor que la cantidad de subprocesos.

(2) Cuando se destruyan, también se destruirán Threadlos correspondientes , lo que puede reducir el uso de la memoria.ThreadLocalMap

4. El código fuente del método central de ThreadLocal

Sobre la base de la estructura interna de ThreadLocal, continuamos analizando el código fuente de su método central para obtener una comprensión más profunda de sus principios operativos.

Además del método de construcción, ThreadLocal tiene los siguientes cuatro métodos expuestos al mundo exterior:

declaración de método describir
protegido T valor inicial () Devuelve el valor inicial de la variable local de subproceso actual
conjunto vacío público (valor T) Establecer una variable local vinculada al hilo actual
T público obtener () Obtener las variables locales vinculadas por el hilo actual
eliminación de vacío público () Eliminar las variables locales vinculadas por el hilo actual

El siguiente es el análisis detallado del código fuente de estos cuatro métodos (para garantizar una idea clara, la parte ThreadLocalMap no se expandirá por el momento, y el siguiente punto de conocimiento se explicará en detalle)

4.1 establecer método

(1) Código fuente y comentarios en chino correspondientes

  /**
     * 设置当前线程对应的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) Proceso de ejecución de código

A. Primero obtenga el hilo actual y obtenga un mapa basado en el hilo actual

B. Si el mapa obtenido no está vacío, establezca los parámetros en el mapa (la referencia ThreadLocal actual se usa como clave)

C. Si el Mapa está vacío, cree un Mapa para el subproceso y establezca el valor inicial

4.2 obtener método

(1) Código fuente y comentarios en chino correspondientes

    /**
     * 返回当前线程中保存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) Proceso de ejecución de código

A. Primero obtenga el hilo actual y obtenga un mapa basado en el hilo actual

​ B. Si el Mapa obtenido no está vacío, utilice la referencia ThreadLocal como clave en el Mapa para obtener la Entrada e correspondiente en el Mapa, de lo contrario vaya a D

​ C. Si e no es nulo, devolver e.value, de lo contrario, ir a D

D. Si el mapa está vacío o e está vacío, use la función initialValue para obtener el valor del valor inicial y luego use la referencia y el valor de ThreadLocal como primera clave y primer valor para crear un nuevo mapa.

Resumen: obtenga primero la variable ThreadLocalMap del subproceso actual, devuelva el valor si existe, cree y devuelva el valor inicial si no existe.

4.3 método de eliminación

(1) Código fuente y comentarios en chino correspondientes

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

(2) Proceso de ejecución de código

A. Primero obtenga el hilo actual y obtenga un mapa basado en el hilo actual

B. Si el Mapa obtenido no está vacío, elimine la entrada correspondiente al objeto ThreadLocal actual

4.4 método de valor inicial

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

​ La función de este método es devolver el valor inicial de la variable local del hilo.

(1) Este método es un método de llamada retrasada. Del código anterior, sabemos que se ejecuta cuando se llama al método get antes de llamar al método set, y solo se ejecuta una vez.

(2) La implementación predeterminada de este método devuelve uno directamente null.

(3) Si desea un valor inicial que no sea nulo, puede anular este método. (Observaciones: este método es un protectedmétodo único, obviamente diseñado para ser anulado por subclases)

5. Análisis del código fuente de ThreadLocalMap

Al analizar el método ThreadLocal, aprendimos que la operación de ThreadLocal en realidad gira en torno a ThreadLocalMap. El código fuente de ThreadLocalMap es relativamente complicado y lo analizamos desde los siguientes tres aspectos.

5.1 Estructura básica

​ ThreadLocalMap es una clase interna de ThreadLocal, no implementa la interfaz de Map, implementa la función de Map de forma independiente, y su Entry interno también se implementa de forma independiente.

1574266262577

(1) Variables miembro

    /**
     * 初始容量 —— 必须是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
    

Al igual que HashMap, INITIAL_CAPACITY representa la capacidad inicial del mapa; la tabla es una matriz de tipo de entrada utilizada para almacenar datos; el tamaño representa la cantidad de almacenamiento en la tabla; el umbral representa el umbral correspondiente al tamaño cuando se requiere expansión.

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

Fuente: Jianshu

Los derechos de autor pertenecen al autor. Para reimpresión comercial, comuníquese con el autor para obtener autorización, para reimpresión no comercial, indique la fuente.

(2) Estructura de almacenamiento - Entrada

/*
 * 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;
    }
}

En ThreadLocalMap, Entry también se usa para guardar datos de estructura KV. Sin embargo, la clave en Entry solo puede ser un objeto ThreadLocal, que se ha restringido en el método de construcción.

​ Además, Entry hereda WeakReference, es decir, key (ThreadLocal) es una referencia débil y su propósito es desvincular el ciclo de vida del objeto ThreadLocal y el ciclo de vida del hilo.

5.2 Referencias débiles y fugas de memoria

Algunos programadores encontrarán que hay una fuga de memoria en el proceso de uso de ThreadLocal y supondrán que esta fuga de memoria está relacionada con la clave que usa referencias débiles en Entry. Este entendimiento es realmente incorrecto.

Primero revisemos varios conceptos sustantivos involucrados en esta pregunta y luego analicemos el problema.

(1) Conceptos relacionados con las fugas de memoria

  • Desbordamiento de memoria: desbordamiento de memoria, no hay suficiente memoria para que la utilice el solicitante.
  • Fuga de memoria: la fuga de memoria se refiere a que la memoria del montón que se ha asignado dinámicamente en el programa no se libera o no se puede liberar por algún motivo, lo que resulta en una pérdida de memoria del sistema, lo que tiene consecuencias graves, como la ralentización de la velocidad de ejecución del el programa e incluso el sistema fallan. La acumulación de fugas de memoria eventualmente conducirá a un desbordamiento de memoria.

(2) Débilmente se refieren a conceptos relacionados

Hay 4 tipos de referencias en Java: fuerte, suave, débil y virtual. El problema actual involucra principalmente referencias fuertes y referencias débiles:

Una referencia fuerte (referencia "fuerte") es nuestra referencia de objeto común más común. Siempre que haya una referencia fuerte que apunte a un objeto, puede indicar que el objeto todavía está "vivo" y que el recolector de basura no reciclará este objeto

​Referencia débil , una vez que el recolector de basura encuentra un objeto con solo referencias débiles, reclamará su memoria independientemente de si el espacio de memoria actual es suficiente o no.

(3) Si la clave usa una referencia fuerte

Suponiendo que la clave en ThreadLocalMap usa una referencia segura, ¿habrá una pérdida de memoria?

En este momento, el mapa de memoria de ThreadLocal (la línea sólida indica una referencia sólida) es el siguiente:

1582902708234

Suponiendo que ThreadLocal se usa en el código comercial, threadLocal Ref se recicla.

Pero debido a que la entrada de threadLocalMap hace referencia fuertemente a threadLocal, threadLocal no se puede reciclar.

​ Bajo la premisa de que la Entrada no se elimina manualmente y el Subproceso Actual todavía se está ejecutando, siempre hay una cadena de referencia sólida subproceso->subprocesoactual->subprocesoMapaLocal->entrada, y la Entrada no se reciclará (la instancia y el valor de SubprocesoLocal son incluido en la entrada), lo que provoca la fuga de memoria de la entrada.

Es decir, la clave en ThreadLocalMap usa referencias sólidas, que no pueden evitar por completo las fugas de memoria.

(5) Si la clave utiliza referencias débiles

Luego, la clave en ThreadLocalMap usa una referencia débil, ¿habrá una pérdida de memoria?

En este momento, el mapa de memoria de ThreadLocal (las líneas sólidas indican referencias sólidas, las líneas punteadas indican referencias débiles) es el siguiente:

1582907143471

También se supone que ThreadLocal Ref se recicla después de usar ThreadLocal en el código comercial.

Dado que ThreadLocalMap solo tiene una referencia débil a ThreadLocal, y no hay una referencia fuerte que apunte a la instancia de threadlocal, gc puede reciclar threadlocal con éxito. En este momento, key=null en Entry.

​ Pero bajo la premisa de que la Entrada no se elimina manualmente y el Subproceso actual aún se está ejecutando, también hay una cadena de referencia sólida threadRef->currentThread->threadLocalMap->entry -> value, el valor no se reciclará y este valor nunca se alcanzará el acceso, lo que provocará una fuga de memoria de valor.

Es decir, la clave en ThreadLocalMap usa referencias débiles y puede haber pérdidas de memoria.

(6) La verdadera razón de la fuga de memoria

Al comparar las dos situaciones anteriores, encontraremos que la ocurrencia de pérdidas de memoria no tiene nada que ver con si la clave en ThreadLocalMap usa referencias débiles. Entonces, ¿cuál es la verdadera causa de la pérdida de memoria?

Los estudiantes cuidadosos encontrarán que en las dos situaciones de fuga de memoria anteriores, hay dos requisitos previos:

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

​ El primer punto es fácil de entender, siempre que se use ThreadLocal y se llame a su método de eliminación para eliminar la entrada correspondiente, se pueden evitar las fugas de memoria.

​ El segundo punto es un poco más complicado, ya que ThreadLocalMap es un atributo de Thread y es referenciado por el hilo actual, su ciclo de vida es tan largo como el de Thread. Luego, después de usar ThreadLocal, si el subproceso actual también finaliza, gc reciclará naturalmente ThreadLocalMap, evitando pérdidas de memoria desde la causa raíz.

En resumen, la causa principal de la fuga de memoria de ThreadLocal es : dado que el ciclo de vida de ThreadLocalMap es tan largo como el de Thread, si la clave correspondiente no se elimina manualmente, se producirá una fuga de memoria.

(7) ¿Por qué usar referencias débiles?

Según el análisis de ahora, sabemos que no importa qué tipo de referencia use la clave en ThreadLocalMap, las fugas de memoria no se pueden evitar por completo y no tienen nada que ver con el uso de referencias débiles.

Hay dos formas de evitar pérdidas de memoria:

  1. Después de usar ThreadLocal, llame a su método de eliminación para eliminar la Entrada correspondiente

  2. Después de usar ThreadLocal, el hilo actual también termina

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

​ En otras palabras, siempre que recuerde llamar a remove a tiempo después de usar ThreadLocal, no habrá problema si la clave es una referencia fuerte o débil. Entonces, ¿por qué las claves usan referencias débiles?

De hecho, en el método set/getEntry en ThreadLocalMap, la clave se considerará nula (es decir, ThreadLocal es nula) y, si es nula, el valor se establecerá en nulo.

Esto significa que después de usar ThreadLocal y CurrentThread todavía se está ejecutando, incluso si olvida llamar al método de eliminación, las referencias débiles pueden proporcionar una capa más de protección que las referencias fuertes : el ThreadLocal con referencia débil se reciclará y el valor correspondiente se establecerá en la próxima llamada ThreadLocalMap. Cualquier método en get o remove se borrará para evitar pérdidas de memoria.

5.3 Resolución de conflictos hash

​ La resolución de conflictos hash es un contenido importante en Map. Estudiemos el código fuente principal de ThreadLocalMap tomando como pista la resolución de conflictos hash.

(1) Primero comience con el método set() de 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);
    }

Hemos analizado este método hace un momento, y su función es establecer las variables locales vinculadas por el hilo actual:

A. Primero obtenga el hilo actual y obtenga un mapa basado en el hilo actual

B. Si el mapa obtenido no está vacío, establezca los parámetros en el mapa (la referencia ThreadLocal actual se usa como clave)

( El método set de ThreadLocalMap se llama aquí)

C. Si el Mapa está vacío, cree un Mapa para el subproceso y establezca el valor inicial

( El constructor de ThreadLocalMap se llama aquí)

Hay dos lugares en este código que involucran los dos métodos de ThreadLocalMap respectivamente, y analizaremos estos dos métodos a continuación.

**(2) Método de construcción`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);
    }

El constructor primero crea una matriz Entry con una longitud de 16, luego calcula el índice correspondiente a firstKey, lo almacena en la tabla y establece el tamaño y el umbral.

​Análisis clave : int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1).

a. En cuanto a 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;

​ Aquí se define un tipo AtomicInteger, cada vez que se obtiene el valor actual y se agrega HASH_INCREMENT, HASH_INCREMENT = 0x61c88647este valor está relacionado con la secuencia de Fibonacci (número de la sección dorada), y su objetivo principal es hacer que el código hash se distribuya uniformemente en 2 En el Enésima matriz de potencia, es decir, en la tabla Entry[], hacerlo puede evitar conflictos de hash tanto como sea posible.

B. Acerca de& (INITIAL_CAPACITY - 1)

El algoritmo hashCode & (tamaño - 1) se usa para calcular el hash, que es equivalente a una implementación más eficiente de la operación de módulo hashCode % tamaño. Es precisamente debido a este algoritmo que requerimos que el tamaño debe ser una potencia integral de 2, lo que también puede garantizar que la cantidad de colisiones hash se reduzca bajo la premisa de que el índice no cruza el límite.

(3) El método set en 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);
    }

Flujo de ejecución de código:

A. Primero, calcule el índice i de acuerdo con la clave y luego busque la Entrada en la posición de i.

B. Si la Entrada ya existe y la clave es igual a la clave entrante, entonces asigne directamente un nuevo valor a la Entrada en este momento,

C. Si la entrada existe, pero la clave es nula, llame a replaceStaleEntry para reemplazar la entrada cuya clave está vacía.

D. Detección de bucle continuo hasta que encuentre un lugar que sea nulo. En este momento, si no ha regresado durante el proceso de bucle, cree una nueva entrada en la posición nula e insértela, y el tamaño aumenta en 1 al mismo tiempo. tiempo.

Finalmente llame a cleanSomeSlots para limpiar la Entrada cuya clave es nula, y finalmente devuelva si la Entrada está limpia, y luego juzgue si sz >= thresgold cumple con la condición de repetición. Si es así, se llamará a la función de repetición para realizar una revisión completa. escaneo y limpieza de tablas.

Análisis clave : ThreadLocalMap se utiliza 线性探测法para resolver conflictos hash.

Este método detecta la siguiente dirección a la vez y la inserta hasta que haya una dirección vacía. Si no hay una dirección vacía en todo el espacio, se producirá un desbordamiento.

Por ejemplo, supongamos que la longitud de la tabla actual es 16, es decir, si el valor hash calculado de la clave es 14, si ya hay un valor en la tabla [14] y su clave es inconsistente con la clave actual, entonces se produce un conflicto hash. En este momento, agregue 14 a 1 para obtener 15, y tome la tabla [15] para juzgar. En este momento, si todavía hay un conflicto, volverá a 0, tome la tabla [0], y así sucesivamente hasta que se pueda insertar.

De acuerdo con la descripción anterior, la tabla Entry[] se puede considerar como una matriz circular.

Supongo que te gusta

Origin blog.csdn.net/m0_47015897/article/details/132260978
Recomendado
Clasificación