前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 并发编程·ThreadLocal

Java 并发编程·ThreadLocal

作者头像
数媒派
发布2022-12-01 11:43:13
2600
发布2022-12-01 11:43:13
举报
文章被收录于专栏:产品优化

ThreadLocal

对于多任务,Java 标准库提供的线程池可以方便地执行这些任务,同时复用线程。那么如何在一个线程内传递状态?

如下栗子,一个内部需要调用若干其他方法,同时传递参数 user。

代码语言:javascript
复制
public void process(User user) {
    checkPermission(user);
    doWork(user);
    saveStatus(user);
    sendResponse(user);
}

这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。

给每个方法增加一个 context 参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User 对象就传不进去了。

Java 标准库提供了一个特殊的 ThreadLocal,它可以在一个线程中传递同一个对象。

ThreadLocal 实例通常总是以静态字段初始化如下:

代码语言:javascript
复制
static ThreadLocal<String> threadLocalUser = new ThreadLocal<>();

使用方式:

代码语言:javascript
复制
void processUser(user) {
    try {
        threadLocalUser.set(user);
        step1();
        step2();
    } finally {
        threadLocalUser.remove();
    }
}

void step1() {
    User u = threadLocalUser.get();
    printUser();
}

void step2() {
    User u = threadLocalUser.get();
    checkUser(u.id);
}

注意到普通的方法调用一定是同一个线程执行的,所以,step1()step2() 方法内,threadLocalUser.get() 获取的 User 对象是同一个实例。

实际上,可以把 ThreadLocal 看成一个全局 Map<Thread, Object>,每个线程获取 ThreadLocal 变量时,总是使用 Thread 自身作为 key:

代码语言:javascript
复制
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

因此,ThreadLocal 相当于给每个线程都开辟了一个独立的存储空间,各个线程的 ThreadLocal 关联的实例互不干扰。

最后,特别注意 ThreadLocal 一定要在 finally 中清除。这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果 ThreadLocal 没有被清除,该线程执行其他代码时,会把上一次的状态带进去。

为了保证能释放 ThreadLocal 关联的实例,我们可以通过 AutoCloseable 接口配合 try (resource) {...} 结构,让编译器自动为我们关闭。例如,一个保存了当前用户名的 ThreadLocal 可以封装为一个 UserContext 对象:

代码语言:javascript
复制
public class UserContext implements AutoCloseable {

    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public UserContext(String user) {
        ctx.set(user);
    }

    public static String currentUser() {
        return ctx.get();
    }

    @Override
    public void close() {
        ctx.remove();
    }
}

使用方式:

代码语言:javascript
复制
try (var ctx = new UserContext("Bob")) {
    // 可任意调用UserContext.currentUser():
    String currentUser = UserContext.currentUser();
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象

这样就在 UserContext 中完全封装了 ThreadLocal,外部代码在 try (resource) {...} 内部可以随时调用 UserContext.currentUser() 获取当前线程绑定的用户名。

小结

ThreadLocal 空间换时间,synchronized 时间换空间。

  • ThreadLocal 表示线程的“局部变量”,它确保每个线程的 ThreadLocal 变量都是各自独立的;
  • ThreadLocal 适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
  • 使用 ThreadLocal 要用 try ... finally 结构,并在 finally 中清除。

DEMO

ThreadLocal SimpleDateFormat

代码语言:javascript
复制
/**
 * 10 个线程执行 1000 次打印格式化日期,每个线程有自己的格式化对象
 */
public class ThreadLocalNormalUsage03 {

    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String date = new ThreadLocalNormalUsage03().date(finalI);
                System.out.println(date);
            });
        }
        threadPool.shutdown();
    }

    private String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = dateFormatThreadLocal.get();
        return simpleDateFormat.format(date);
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ThreadLocal
    • 小结
      • DEMO
        • ThreadLocal SimpleDateFormat
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档