分类: 基础

  • ThreadLocal

    java中的ThreadLocal有哪些应用场景?举几个例子

    1. 线程安全的对象副本
      • 场景描述:在多线程环境下,当多个线程需要访问同一个对象,但又不想因为共享该对象而导致数据混乱时,ThreadLocal 就可以发挥作用。例如,在一个 Web 应用程序中,每个用户请求可能由一个独立的线程来处理。假设应用中有一个SimpleDateFormat对象用于日期格式化,它不是线程安全的。如果多个线程共享一个SimpleDateFormat实例,在并发访问时可能会出现日期格式化错误。
      • 示例代码
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import java.text.SimpleDateFormat;
    import java.util.Date;
    public class ThreadLocalDateFormat {
    private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy - MM - dd HH:mm:ss"));
    public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
    SimpleDateFormat dateFormat = threadLocal.get();
    System.out.println(dateFormat.format(new Date()));
    });
    Thread thread2 = new Thread(() -> {
    SimpleDateFormat dateFormat = threadLocal.get();
    System.out.println(dateFormat.format(new Date()));
    });
    thread1.start();
    thread2.start();
    }
    }
    import java.text.SimpleDateFormat; import java.util.Date; public class ThreadLocalDateFormat { private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy - MM - dd HH:mm:ss")); public static void main(String[] args) { Thread thread1 = new Thread(() -> { SimpleDateFormat dateFormat = threadLocal.get(); System.out.println(dateFormat.format(new Date())); }); Thread thread2 = new Thread(() -> { SimpleDateFormat dateFormat = threadLocal.get(); System.out.println(dateFormat.format(new Date())); }); thread1.start(); thread2.start(); } }
    import java.text.SimpleDateFormat;
    import java.util.Date;
    public class ThreadLocalDateFormat {
        private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy - MM - dd HH:mm:ss"));
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                SimpleDateFormat dateFormat = threadLocal.get();
                System.out.println(dateFormat.format(new Date()));
            });
            Thread thread2 = new Thread(() -> {
                SimpleDateFormat dateFormat = threadLocal.get();
                System.out.println(dateFormat.format(new Date()));
            });
            thread1.start();
            thread2.start();
        }
    }

    代码解释:在这个例子中,通过ThreadLocal为每个线程创建了一个SimpleDateFormat对象的副本。ThreadLocal.withInitial()方法用于初始化ThreadLocal,当线程第一次访问threadLocal.get()时,会调用传入的 lambda 表达式来创建SimpleDateFormat对象。这样,每个线程都有自己独立的SimpleDateFormat,避免了线程安全问题。

    1. 数据库连接管理
      • 场景描述:在数据库访问层,通常希望每个线程都能使用自己独立的数据库连接。如果多个线程共享一个数据库连接,可能会出现数据不一致、连接状态混乱等问题。ThreadLocal可以用来为每个线程分配一个独立的数据库连接。
      • 示例代码
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    public class ThreadLocalDBConnection {
    private static ThreadLocal<Connection<threadLocal> = new ThreadLocal<>();
    public static Connection getConnection() {
    Connection connection = threadLocal.get();
    if (connection == null) {
    try {
    connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
    threadLocal.set(connection);
    } catch (SQLException e) {
    e.printStackTrace();
    }
    }
    return connection;
    }
    public static void closeConnection() {
    Connection connection = threadLocal.get();
    if (connection!= null) {
    try {
    connection.close();
    threadLocal.remove();
    } catch (SQLException e) {
    e.printStackTrace();
    }
    }
    }
    }
    import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ThreadLocalDBConnection { private static ThreadLocal<Connection<threadLocal> = new ThreadLocal<>(); public static Connection getConnection() { Connection connection = threadLocal.get(); if (connection == null) { try { connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password"); threadLocal.set(connection); } catch (SQLException e) { e.printStackTrace(); } } return connection; } public static void closeConnection() { Connection connection = threadLocal.get(); if (connection!= null) { try { connection.close(); threadLocal.remove(); } catch (SQLException e) { e.printStackTrace(); } } } }
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    public class ThreadLocalDBConnection {
        private static ThreadLocal<Connection<threadLocal> = new ThreadLocal<>();
        public static Connection getConnection() {
            Connection connection = threadLocal.get();
            if (connection == null) {
                try {
                    connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
                    threadLocal.set(connection);
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            return connection;
        }
        public static void closeConnection() {
            Connection connection = threadLocal.get();
            if (connection!= null) {
                try {
                    connection.close();
                    threadLocal.remove();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    代码解释:这个代码示例定义了一个ThreadLocal来存储数据库连接。getConnection方法首先尝试从ThreadLocal中获取连接,如果没有获取到(即当前线程还没有创建连接),就创建一个新的连接并存储到ThreadLocal中。closeConnection方法用于关闭连接并从ThreadLocal中移除连接对象,这样可以确保连接资源的正确释放。

    1. 用户上下文信息传递
      • 场景描述:在一个复杂的企业级应用中,可能需要在整个请求处理过程中传递用户相关的上下文信息,如用户 ID、权限级别等。这些信息在不同的业务层(如服务层、数据访问层)都可能会用到,使用ThreadLocal可以方便地在同一线程内传递这些信息。
      • 示例代码
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    public class UserContext {
    private static ThreadLocal<UserInfo> userThreadLocal = new ThreadLocal();
    public static void setUser(UserInfo userInfo) {
    userThreadLocal.set(userInfo);
    }
    public static UserInfo getUser() {
    return userThreadLocal.get();
    }
    public static void clearUser() {
    userThreadLocal.remove();
    }
    }
    class UserInfo {
    private int userId;
    private String userName;
    public UserInfo(int userId, String userName) {
    this.userId = userId;
    this.userName = userName;
    }
    public int getUserId() {
    return userId;
    }
    public String getUserName() {
    return userName;
    }
    }
    public class UserContext { private static ThreadLocal<UserInfo> userThreadLocal = new ThreadLocal(); public static void setUser(UserInfo userInfo) { userThreadLocal.set(userInfo); } public static UserInfo getUser() { return userThreadLocal.get(); } public static void clearUser() { userThreadLocal.remove(); } } class UserInfo { private int userId; private String userName; public UserInfo(int userId, String userName) { this.userId = userId; this.userName = userName; } public int getUserId() { return userId; } public String getUserName() { return userName; } }
    public class UserContext {
        private static ThreadLocal<UserInfo> userThreadLocal = new ThreadLocal();
        public static void setUser(UserInfo userInfo) {
            userThreadLocal.set(userInfo);
        }
        public static UserInfo getUser() {
            return userThreadLocal.get();
        }
        public static void clearUser() {
            userThreadLocal.remove();
        }
    }
    class UserInfo {
        private int userId;
        private String userName;
        public UserInfo(int userId, String userName) {
            this.userId = userId;
            this.userName = userName;
        }
        public int getUserId() {
            return userId;
        }
        public String getUserName() {
            return userName;
        }
    }

    代码解释UserContext类中的ThreadLocal用于存储用户信息(UserInfo对象)。setUser方法用于设置用户信息,getUser方法用于获取用户信息,clearUser方法用于清除用户信息。这样,在一个请求处理的线程中,不同的业务模块可以方便地获取和使用用户上下文信息,而且不会与其他线程的用户信息混淆。

    ThreadLocal是如何实现线程安全的?

    1. 数据存储结构
      • ThreadLocal 是通过为每个线程创建独立的变量副本来实现线程安全的。在 Java 中,每个线程都有一个ThreadLocal.ThreadLocalMap对象,这个ThreadLocalMapThreadLocal的一个内部类,用于存储该线程对应的ThreadLocal变量。
      • 例如,假设有一个ThreadLocal<String>对象,当多个线程访问这个ThreadLocal时,实际上每个线程是在自己的ThreadLocalMap中存储和获取数据,就像每个线程都有一个独立的小仓库来存放自己的数据,互不干扰。
    2. ThreadLocalMap 的工作原理
      • 存储数据:当在一个线程中调用ThreadLocalset方法时,比如threadLocal.set("value");,实际上是在当前线程的ThreadLocalMap中进行存储操作。它以当前ThreadLocal对象为键(通过ThreadLocal对象的threadLocalHashCode属性来唯一标识),要存储的值为值,将这个键值对存储到ThreadLocalMap中。
      • 获取数据:当调用get方法时,如String value = threadLocal.get();,它会首先获取当前线程的ThreadLocalMap,然后以当前ThreadLocal对象为键,从ThreadLocalMap中查找对应的键值对,找到就返回值,找不到就返回null或者执行初始化操作(如果通过withInitial方法设置了初始化函数)。
      • 内存管理与防止内存泄漏ThreadLocalMap中的每个键值对是一个Entry对象,它继承自WeakReference<ThreadLocal<?>>。这意味着EntryThreadLocal对象的引用是弱引用。这样设计的目的是,当ThreadLocal对象没有其他强引用时,它可以被垃圾回收。不过,为了防止内存泄漏,在ThreadLocalsetremove等方法中会清理掉键为nullEntry,因为如果不清理,这些Entry对应的ThreadLocal对象虽然被回收了,但值可能还存在于ThreadLocalMap中,占用内存空间。
    3. 与线程生命周期的关联
      • 因为每个线程都有自己独立的ThreadLocalMap,所以ThreadLocal变量的生命周期与线程的生命周期相关联。当一个线程结束时,与之关联的ThreadLocalMap及其内部存储的数据也会被回收(假设没有其他外部引用)。
      • 例如,在一个 Web 应用服务器中,一个请求处理线程在处理完请求后结束,那么该线程所关联的ThreadLocal变量副本就会被销毁,不会对下一个请求处理线程产生影响,从而保证了线程之间的数据独立性和线程安全。

    ThreadLocal内存泄漏的原因

    1. 弱引用与强引用的概念
      • 在 Java 中,引用分为强引用、软引用、弱引用和虚引用。强引用是最常见的引用方式,只要对象被强引用,垃圾回收器就不会回收它。例如,Object obj = new Object();这里obj就是对new Object()这个对象的强引用。
      • 弱引用则不同,当一个对象只有弱引用指向它时,在垃圾回收器下一次运行时,这个对象就会被回收。WeakReference是 Java 中用于表示弱引用的类。在ThreadLocal的实现中,ThreadLocalMapEntry继承自WeakReference<ThreadLocal<?>>,这意味着EntryThreadLocal对象的引用是弱引用。
    2. ThreadLocal 内存泄漏的触发机制
      • 正常情况:当一个线程使用ThreadLocal时,ThreadLocal对象作为键,存储的值作为值,存放在ThreadLocalMap中。在理想情况下,当线程结束或者ThreadLocal对象的生命周期结束并且没有其他强引用时,ThreadLocal对象会被垃圾回收,同时ThreadLocalMap中的相应Entry也会被清理(因为EntryThreadLocal的引用是弱引用)。
      • 内存泄漏场景:如果ThreadLocal对象的生命周期结束了,但是线程还在运行,由于EntryThreadLocal是弱引用,ThreadLocal对象会被垃圾回收。然而,Entry中的值仍然可能被线程访问,并且由于Entry对象本身(除了对ThreadLocal的弱引用部分)并没有被回收,这就导致存储的值对象无法被回收,从而造成内存泄漏。
      • 例如,假设有一个ThreadLocal<String>对象,在一个长时间运行的线程中使用。当ThreadLocal对象本身被设置为null或者超出了作用域,它被垃圾回收了,但是ThreadLocalMap中的Entry因为值对象(String)还有其他引用(可能在业务逻辑中有其他地方引用了这个String)而无法被清理,这个String对象就会一直占用内存空间。
    3. 防止内存泄漏的措施(remove方法的重要性)
      • 为了防止这种内存泄漏,ThreadLocal提供了remove方法。当ThreadLocal对象不再需要时,应该及时调用remove方法来清除ThreadLocalMap中对应的Entry
      • 例如,在一个 Web 应用中,当一个请求处理结束时,应该在处理请求的线程中清理所有使用过的ThreadLocal对象,如threadLocal.remove();。这样可以确保ThreadLocalMap中的Entry被正确清理,避免因为ThreadLocal对象的弱引用特性而导致的内存泄漏。