ThreadLocal 的使用心得

本文详细介绍了ThreadLocal的概念、核心方法及应用场景,包括如何实现线程安全的时间格式化和跨类数据传递。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >



1.ThreadLocal 介绍

ThreadLocal 从字面的意思来理解是线程本地变量的意思,也就是说它是线程中的私有变量,每个线程只能使用自己的变量。

ThreadLocal 原本设计是为了解决并发时,线程共享变量的问题,但由于过度设计,如弱引用和哈希碰撞,从而导致它的理解难度大和使用成本高等问题。当然,如果稍有不慎还是导致脏数据、内存溢出、共享变量更新等问题,但即便如此,ThreadLocal 依旧有适合自己的使用场景,以及无可取代的价值,比如本文要介绍了这两种使用场景,除了 ThreadLocal 之外,还真没有合适的替代方案。

2.ThreadLocal 的API

官方说明文档: https://docs.oracle.com/javase/8/docs/api/
在这里插入图片描述

2.1 ThreadLocal 核心方法:

  1. set 方法:用于设置线程独立变量副本。没有 set 操作的 ThreadLocal 容易引起脏数据。
  2. get 方法:用于获取线程独立变量副本。没有 get 操作的 ThreadLocal 对象没有意义。
  3. remove 方法:用于移除线程独立变量副本。没有 remove 操作容易引起内存泄漏。
public class ThreadLocalExample {

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();//创建 ThreadLocal 对象

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + " 存入值:" + threadName);
                threadLocal.set(threadName); // 向 ThreadLocal 中设置值
                print(threadName);
            }
        };
        new Thread(runnable, "MyThread-1").start();
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印线程中的 ThreadLocal 值
     */
    private static void print(String threadName) {
        try {
            String result = threadLocal.get(); // 得到 ThreadLocal 中取出值
            System.out.println(threadName + " 取出值:" + result);
        } finally {
            threadLocal.remove(); // 移除 ThreadLocal 中的值(防止内存溢出)
        }
    }
}

在这里插入图片描述
从上述结果可以看出,每个线程只会读取到属于自己的 ThreadLocal 值。

2.2 ThreadLocal 初始化:initialValue()

public class ThreadLocalExample {

    //创建 ThreadLocal 对象
    private static ThreadLocal<String> threadLocal = new ThreadLocal() {
        @Override
        protected String initialValue() {
            System.out.println("执行 initialValue() 方法");
            return "默认值";
        }
    };

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                print(threadName);
            }
        };
        new Thread(runnable, "MyThread-1").start();
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印线程中的 ThreadLocal 值
     */
    private static void print(String threadName) {
        try {
            String result = threadLocal.get(); // 得到 ThreadLocal 中取出值
            System.out.println(threadName + " 取出值:" + result);
        } finally {
            threadLocal.remove(); // 移除 ThreadLocal 中的值(防止内存溢出)
        }
    }
}

执行结果:
在这里插入图片描述

注意: 当先使用了 threadLocal.set() 方法之后,initialValue() 方法就不会被执行了,如下代码所示:

public class ThreadLocalExample {

    //创建 ThreadLocal 对象
    private static ThreadLocal<String> threadLocal = new ThreadLocal() {
        @Override
        protected String initialValue() { // 注意返回值类型要与泛型一致
            System.out.println("执行 initialValue() 方法");
            return "默认值";
        }
    };

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + " 存入值:" + threadName);
                threadLocal.set(threadName); // 向 ThreadLocal 中设置值
                print(threadName);
            }
        };
        new Thread(runnable, "MyThread-1").start();
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印线程中的 ThreadLocal 值
     */
    private static void print(String threadName) {
        try {
            String result = threadLocal.get(); // 得到 ThreadLocal 中取出值
            System.out.println(threadName + " 取出值:" + result);
        } finally {
            threadLocal.remove(); // 移除 ThreadLocal 中的值(防止内存溢出)
        }
    }
}

在这里插入图片描述
为什么 先执行 set() 之后,初始化代码就不执行了?

要理解这个问题,需要从 ThreadLocal.get() 方法的源码中得到答案,因为初始化方法 initialValue() 在 ThreadLocal 创建时并不会立即执行,而是在调用了 get() 方法只会才会执行,测试代码如下:
在这里插入图片描述
从上述源码可以看出,当 ThreadLocal 中有值时会直接返回值 e.value,只有 Threadlocal 中没有任何值时才会执行初始化方法 initialValue()。

2.3 ThreadLocal 初始化:withInitial()

import java.util.function.Supplier;

public class ThreadLocalExample {

    // 完整写法
//    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
//        @Override
//        public String get() {
//            System.out.println("执行 withInitial() 方法");
//            return "默认值";
//        }
//    });

    // lambda表达式简写
    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认值");

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
//                System.out.println(threadName + " 存入值:" + threadName);
//                threadLocal.set(threadName); // 向 ThreadLocal 中设置值
                print(threadName);
            }
        };
        new Thread(runnable, "MyThread-1").start();
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印线程中的 ThreadLocal 值
     */
    private static void print(String threadName) {
        try {
            String result = threadLocal.get(); // 得到 ThreadLocal 中取出值
            System.out.println(threadName + " 取出值:" + result);
        } finally {
            threadLocal.remove(); // 移除 ThreadLocal 中的值(防止内存溢出)
        }
    }
}

注意: 同理,当先使用了 threadLocal.set() 方法之后,withInitial() 方法就不会被执行了。

3 使用场景

3.1 场景一:ThreadLocal 实现时间格式化

public class SimpleDateFormatTest {
    // 创建 ThreadLocal 并设置默认值
    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));

    public static void main(String[] args) {
        // 创建线程池执行任务
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        // 执行任务
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 执行任务
            threadPool.execute(() -> {
                Date date = new Date(finalI * 1000); // 得到时间对象
                formatAndPrint(date); // 执行时间格式化
            });
        }
        threadPool.shutdown(); // 线程池执行完任务之后关闭
    }

    /**
     * 格式化并打印时间
     */
    private static void formatAndPrint(Date date) {
        String result = dateFormatThreadLocal.get().format(date); // 执行格式化
        System.out.println("时间:" + result);  // 打印最终结果
    }
}

使用 ThreadLocal 也可以解决线程并发问题,并且避免了代码加锁排队执行的问题。

SimpleDateFormat的非线程安全问题见另一篇博文 https://blog.youkuaiyun.com/QiuHaoqian/article/details/116594422

3.2 场景二:跨类传递数据

可以使用 ThreadLocal 来实现线程中跨类、跨方法的数据传递。

比如登录用户的 User 对象信息,需要在不同的子系统中多次使用,如果使用传统的方式,需要使用方法传参和返回值的方式来传递 User 对象,然而这样就无形中造成了类和类之间,甚至是系统和系统之间的相互耦合了,所以此时我们可以使用 ThreadLocal 来实现 User 对象的传递。

演示代码如下:

先在主线程中构造并初始化一个 User 对象,并将此 User 对象存储在 ThreadLocal 中,存储完成之后,就可以在同一个线程的其他类中,如仓储类或订单类中直接获取并使用 User 对象了。

主线程中的业务代码:

public class ThreadLocalByUser {
    public static void main(String[] args) {
        // 初始化用户信息
        User user = new User("小明");
        // 将 User 对象存储在 ThreadLocal 中
        UserStorage.setUser(user);
        // 调用订单系统
        OrderSystem orderSystem = new OrderSystem();
        // 添加订单(方法内获取用户信息)
        orderSystem.add();
        // 调用仓储系统
        RepertorySystem repertory = new RepertorySystem();
        // 减库存(方法内获取用户信息)
        repertory.decrement();
    }
}

User 实体类:

/**
 * 用户实体类
 */
class User {
    public User(String name) {
        this.name = name;
    }
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

ThreadLocal 操作类:

/**
 * 用户信息存储类
 */
class UserStorage {
    public static ThreadLocal<User> USER = new ThreadLocal(); // 存储用户信息

    public static void setUser(User user) {
        USER.set(user);
    }
}

订单类:

/**
 * 订单类
 */
class OrderSystem {
    /**
     * 订单添加方法
     */
    public void add() {
        // 从ThreadLocal得到用户信息
        User user = UserStorage.USER.get();
        // 业务处理代码(忽略)...
        System.out.println(String.format("订单系统收到用户:%s 的请求。", user.getName()));
    }
}

仓储类:

/**
 * 仓储类
 */
class RepertorySystem {
    /**
     * 减库存方法
     */
    public void decrement() {
        // 从ThreadLocal得到用户信息
        User user = UserStorage.USER.get();
        // 业务处理代码(忽略)...
        System.out.println(String.format("仓储系统收到用户:%s 的请求。", user.getName()));
    }
}

执行结果:
在这里插入图片描述
从上述结果可以看出,当在主线程中先初始化了 User 对象之后,订单类和仓储类无需进行任何的参数传递也可以正常获得 User 对象了,从而实现了一个线程中,跨类和跨方法的数据传递。

4 总结

使用 ThreadLocal 可以创建线程私有变量,所以不会导致线程安全问题,同时使用 ThreadLocal 还可以避免因为引入锁而造成线程排队执行所带来的性能消耗;再者使用 ThreadLocal 还可以实现一个线程内跨类、跨方法的数据传递。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值