Cree una arquitectura segura para subprocesos sin bloqueos: domine el principio de ThreadLocal en Java y aplíquelo con flexibilidad

ThreadLocal es una herramienta de almacenamiento de variables a nivel de subproceso proporcionada por Java, que permite que cada subproceso tenga su propia copia de variable independiente, y cada subproceso puede operar de forma independiente su propia copia de variable sin interferir entre sí. Este artículo presentará el principio y los escenarios de uso de ThreadLocal en detalle y lo explicará a través de ejemplos de código.

1. El principio de ThreadLocal

1.1 Resumen

ThreadLocal proporciona una forma sencilla de implementar el confinamiento de subprocesos, que asocia datos con subprocesos para garantizar que cada subproceso tenga su propia copia independiente de datos, evitando así problemas de seguridad de subprocesos. En un entorno de subprocesos múltiples, el uso de ThreadLocal puede implementar fácilmente el aislamiento de datos entre subprocesos, lo que garantiza que cada subproceso pueda acceder a sus propios datos.

1.2 Estructura de datos

ThreadLocal utiliza internamente una estructura de datos especial para almacenar la copia variable de cada hilo.Esta estructura de datos se llama ThreadLocalMap. Cada objeto ThreadLocal se usa como una clave, correspondiente a un valor, que representa la copia variable del hilo. ThreadLocalMap es una clase estática interna de la clase ThreadLocal, que se utiliza para almacenar variables locales de subprocesos.

ThreadLocal es una variable compartida de subprocesos. ThreadLoacl tiene una clase interna estática ThreadLocalMap, cuya clave es el objeto ThreadLocal y el valor es el objeto Entry, y ThreadLocalMap es privado para cada subproceso.

  • set Establece el valor de ThreadLocalMap.
  • get Obtiene ThreadLocalMap.
  • remove Elimina un objeto de tipo ThreadLocalMap.

1.3 Principio de implementación

El principio de implementación de ThreadLocal se puede resumir brevemente en los siguientes pasos:

  1. Cree un objeto ThreadLocalMap dentro de cada subproceso para almacenar una copia de las variables del subproceso.
  2. Cuando sea necesario utilizar variables locales de subprocesos, obtenga el objeto ThreadLocalMap correspondiente al subproceso actual a través del método get().
  3. 在 ThreadLocalMap 中以当前 ThreadLocal 对象作为 key,获取或设置变量副本。

具体流程如下图所示:

image.png

简单描述也就是这样:

main Thread:          Thread1:                        Thread2:
┌─────────┐          ┌─────────┐       ┌─────────┐    ┌─────────┐
│ Thread  │          │ Thread  │       │ Thread  │    │ Thread  │
├─────────┤          ├─────────┤       ├─────────┤    ├─────────┤
│         ├──┐       │         ├──┐    │         ├───>│         │
└─────────┘  │       └─────────┘  │    └─────────┘    └─────────┘
             │                    │
     get()   │     set("Data1")   │
   ┌──────────┼──────────────────┼───────────────────┐
   │          │                  │                   │
┌───────────┐  │    ┌───────────┐ │    ┌───────────┐  │
│ ThreadMap │  │    │ ThreadMap │ │    │ ThreadMap │  │
├───────────┤  │    ├───────────┤ │    ├───────────┤  │
│ ThreadMap ├──┼───>│ ThreadMap ├──┼───>│ ThreadMap │  │
└───────────┘  │    └───────────┘ │    └───────────┘  │
               │                  │                   │
           ┌───────────┐      ┌───────────┐       ┌───────────┐
           │ ThreadLocal1 │      │ ThreadLocal1 │       │ ThreadLocal1 │
           ├───────────┤      ├───────────┤       ├───────────┤
           │     Data1     │      │     Data2     │       │     Data3  │
           └───────────┘      └───────────┘       └───────────┘

二、ThreadLocal 的使用场景

2.1 线程安全问题

在多线程环境中,多个线程访问共享数据时可能出现线程安全问题,例如数据被意外修改、并发写入等。此时,可以使用 ThreadLocal 将数据与线程关联起来,确保每个线程都操作自己的数据副本,从而避免线程安全问题。

2.2 传递上下文信息

在一些需要跨层传递上下文信息的场景下,使用 ThreadLocal 可以简化代码实现。例如,在 Web 应用中,用户的登录信息通常需要在多个组件间传递,可以使用 ThreadLocal 来存储用户登录信息,每个组件获取登录信息时直接从 ThreadLocal 中获取,避免了繁琐的参数传递过程。

2.3 数据库连接管理

在使用数据库连接池时,每个线程从连接池中获取连接执行数据库操作,并在处理完毕后将连接释放到连接池中。此时,可以使用 ThreadLocal 来管理数据库连接,确保每个线程都使用自己独立的连接,避免多线程并发访问同一个连接引发的问题。

2.4 其他应用场景

除了上述场景,ThreadLocal 还可以用于实现定制化的线程封闭策略,例如在线程池中复用线程时,通过使用 ThreadLocal 可以隔离线程之间的数据。

总的概括就是:

(1)每个线程需要有自己单独的实例

(2)实例需要在多个方法中共享,但不希望被多线程共享

三、ThreadLocal 的使用方式

下面举几个具体的例子来演示 ThreadLocal 的使用方式。

3.1 示例一:传递用户信息

假设有一个 Web 应用,需要在不同层级的组件中传递用户的登录信息。首先,我们定义一个包含用户信息的类 User:

public class User {
    private String username;
    // ...
    
    // 构造方法和getter/setter 省略
}

接下来,在一个拦截用户请求的过滤器中,将用户信息存储到 ThreadLocal 中:

public class UserFilter implements Filter {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 从请求中获取用户信息
        User user = extractUserFromRequest(request);
        
        // 将用户信息存储到 ThreadLocal 中
        userThreadLocal.set(user);
        
        try {
            chain.doFilter(request, response);
        } finally {
            // 请求处理完毕后清除 ThreadLocal 中的数据
            userThreadLocal.remove();
        }
    }
    
    private User extractUserFromRequest(ServletRequest request) {
        // 从请求中提取用户信息
        // ...
        return new User("Alice");
    }
}

在其他组件中,可以通过 ThreadLocal 获取当前线程对应的用户信息:

public class SomeComponent {
    public void doSomething() {
        User user = UserFilter.userThreadLocal.get();
        // 使用用户信息进行操作
        // ...
    }
}

在上述示例中,通过 ThreadLocal 将用户信息存储在不同的线程中,避免了在不同组件间传递参数的麻烦,实现了上下文信息的传递。

3.2 示例二:数据库连接管理

在一个多线程的数据库访问场景中,使用 ThreadLocal 可以实现每个线程使用自己的数据库连接,封装数据库连接的获取和释放过程。

首先,定义一个数据库连接管理类:

public class ConnectionManager {
    private static final ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>();
    
    public static Connection getConnection() {
        Connection connection = connectionThreadLocal.get();
        if (connection == null) {
            // 创建新的数据库连接
            connection = createConnection();
            connectionThreadLocal.set(connection);
        }
        return connection;
    }
    
    public static void releaseConnection() {
        Connection connection = connectionThreadLocal.get();
        if (connection != null) {
            // 关闭数据库连接
            closeConnection(connection);
            connectionThreadLocal.remove();
        }
    }
    
    private static Connection createConnection() {
        // 创建数据库连接
        // ...
        return new Connection();
    }
    
    private static void closeConnection(Connection connection) {
        // 关闭数据库连接
        // ...
    }
}

然后,在数据库访问的代码中,通过 ConnectionManager 来获取和释放数据库连接:

public class UserDao {
    public void save(User user) {
        Connection connection = ConnectionManager.getConnection();
        try {
            // 使用数据库连接进行数据保存操作
            // ...
        } finally {
            ConnectionManager.releaseConnection();
        }
    }
}

在上述示例中,每个线程都会通过 ThreadLocal 存储自己的数据库连接,避免了多线程并发访问同一个连接引发的问题。

四、存在的问题

ThreadLocal 存在的问题以及解决方法:

4.1 内存泄漏问题

ThreadLocal 的内部实现是通过 ThreadLocalMap 来维护每个线程的局部变量,并且 ThreadLocalMap 中的 Entry 对象使用弱引用来引用 ThreadLocal 对象。这就意味着,在没有其他强引用指向 ThreadLocal 对象时,ThreadLocal 对象可能被垃圾回收器回收,而相应的线程局部变量的值仍然保留在 ThreadLocalMap 中,从而导致内存泄漏问题。

解决方法: 为了避免内存泄漏,需要在使用完 ThreadLocal 后手动调用 remove() 方法清理对应的线程局部变量。通常可以通过在 finally 块中进行清理操作,以确保即使发生异常,也能正确清理 ThreadLocal。下面是一个示例代码:

class MyThreadLocalExample {
   private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

   public static void main(String[] args) {
       try {
           // 设置线程局部变量的值
           threadLocal.set(123);

           // 执行业务逻辑...
       } finally {
           // 清理线程局部变量
           threadLocal.remove();
       }
   }
}

4.2 线程复用时的数据共享问题

在线程池等多线程复用的场景中,通过 ThreadLocal 存储的线程局部变量可能会被 “复用” 给其他线程使用,从而导致数据共享问题。也就是说,在某些情况下,多个线程共享同一个 ThreadLocal 的值,这不符合我们使用 ThreadLocal 的初衷。

解决方法: 对于线程池等多线程复用的场景,可以考虑使用 InheritableThreadLocal 来解决数据共享问题。InheritableThreadLocal 是 ThreadLocal 的一个子类,它允许子线程从父线程中继承线程局部变量的值。下面是一个简单的示例代码:

class MyInheritableThreadLocalExample {
   private static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();

   public static void main(String[] args) {
       // 设置线程局部变量的值
       threadLocal.set(123);

       // 创建子线程并执行任务
       Thread childThread = new Thread(() -> {
           // 子线程可以继承父线程的线程局部变量的值
           int value = threadLocal.get();
           System.out.println("子线程获取到的值:" + value);
       });
       childThread.start();
   }
}

4.3 使用 ThreadLocal 的弱引用

在一些场景下,我们不希望 ThreadLocal 对象长期持有对线程局部变量的引用,以避免潜在的内存泄漏问题。可以使用 WeakReference 或者自定义的 WeakThreadLocal 来实现 ThreadLocal 的弱引用版本。这样,在没有其他强引用指向 ThreadLocal 对象时,ThreadLocal 对象就可以被垃圾回收。

解决方法: 下面是一个解决方法的demo代码,展示如何使用 WeakReference 来实现 ThreadLocal 的弱引用版本:

class MyWeakThreadLocalExample {
   private static ThreadLocal<WeakReference<Integer>> threadLocal = new ThreadLocal<>();

   public static void main(String[] args) {
       // 设置线程局部变量的值
       threadLocal.set(new WeakReference<>(123));

       // 业务逻辑...

       // 获取线程局部变量的值
       WeakReference<Integer> reference = threadLocal.get();
       Integer value = reference.get();
       System.out.println("线程局部变量的值:" + value);
   }
}

通过及时清理 ThreadLocal、使用 InheritableThreadLocal 或者使用 ThreadLocal 的弱引用,可以解决 ThreadLocal 存在的问题,并保证线程安全和正确性。应根据具体场景选择合适的解决方法。

五、ThreadLocal和Synchronized的区别

很多同学分不清:ThreadLocal和Synchronized这两种Java多线程编程中用于实现线程安全的两种机制。也不太明白如何去用它们。下面我简单归纳一下吧,它们在实现方式、适用场景和效果上确实是有一些区别的。

(1)实现方式:

  • ThreadLocal:ThreadLocal是一种基于线程的局部变量实现机制。每个线程都有自己独立的ThreadLocal实例,并且每个线程可以访问各自的ThreadLocal实例,线程之间的变量互不干扰。ThreadLocal内部使用一个Map结构,在每个线程内部维护一个变量副本。
  • Synchronized:Synchronized是通过互斥锁(也称为监视器锁)来实现线程安全的。当一个线程获取到锁时,其他线程需要等待,直到持有锁的线程释放锁才能执行相应的代码块。

(2)适用场景:

  • ThreadLocal:ThreadLocal适用于需要在线程之间隔离数据的场景,每个线程可以独立地操作自己的变量副本。常见的使用场景包括Web应用程序的请求处理、数据库事务管理等。
  • Synchronized:Synchronized适用于多个线程共享同一个资源的场景,需要确保在同一时间只有一个线程访问共享资源,从而避免数据竞争和不一致性。常见的使用场景包括共享数据的读写、临界区的保护等。

(3)效果:

  • ThreadLocal:通过ThreadLocal可以实现线程间的数据隔离,每个线程都有自己独立的变量副本,并且修改不会影响其他线程。这样可以避免使用锁带来的开销,提高并发性能。但需要注意合理管理ThreadLocal实例,避免内存泄漏。
  • Synchronized:使用Synchronized可以保证线程安全,确保共享资源在同一时间只能被一个线程访问,从而避免数据竞争和不一致性。Synchronized通过获取锁来控制对共享资源的访问,保证了线程安全。但是,使用锁会引入额外的开销,并且当多个线程竞争同一个锁时,可能会导致线程阻塞和性能下降。

以下是ThreadLocal和Synchronized的对比情况:

比较类型 ThreadLocal Synchronized
实现方式 基于线程的局部变量 通过互斥锁(监视器锁)实现
适用场景 需要在线程之间隔离数据的场景 多个线程共享同一个资源的场景
效果 线程间数据隔离,避免锁的开销,提高并发性能 线程安全,保证共享资源在同一时间只能被一个线程访问
锁的粒度 线程级别 对象级别
并发性能 可以提高并发性能 引入额外的开销,可能导致线程阻塞
内存管理 需要注意合理管理ThreadLocal实例,避免泄漏 无需额外的内存管理
使用复杂度 相对较低,简单易用 相对较高,需要手动控制加锁和释放锁
编程范式 面向变量副本 面向共享资源

ThreadLocal适用于需要在线程间隔离数据的场景,可以提高并发性能,但需要注意管理ThreadLocal实例。Synchronized适用于多个线程共享同一个资源的场景,保证线程安全,但可能引入额外的开销和线程阻塞。 ThreadLocal 可以实现线程级别的变量存储,确保每个线程都拥有自己独立的变量副本,避免线程安全问题。ThreadLocal 的使用场景包括线程安全问题、传递上下文信息、数据库连接管理等。通过合理地运用 ThreadLocal,可以简化多线程编程,提高代码的可读性和可维护性。

Supongo que te gusta

Origin juejin.im/post/7248137735310966845
Recomendado
Clasificación