zoukankan      html  css  js  c++  java
  • 人人都能学会系列之ThreadLocal

    1、概览

    本文我们来看下java.lang包中的ThreadLocal,它赋予我们给每个线程存储自己数据的能力。

    2、ThreadLocal API

    ThreadLocal允许我们存储的数据只能被特定的线程``访问
    我们现在存储一个整形并把它和一个特定的线程绑定:

    ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
    

    接下来当我们在某个线程中想使用这个值的时候,我们只需要调用get()或set()方法,简单的说,我们可以把ThreadLocal理解成数据都存在一个map中,使用线程对象作为key。

    当我们在当前线程中调用threadLocalValue的get()方法时,我们能拿到整形值1:

    threadLocalValue.set(1);
    Integer result = threadLocalValue.get();
    

    我们可以使用ThreadLocal的withInitial()方法并传入一个supplier来创建一个实例:

    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
    

    想要删除这个值的时候我们只需要调用一下remove()方法就好了

    threadLocal.remove();
    

    在叙述怎么合适的使用ThreadLocal之前我们先来看一个不用ThreadLocal的例子,然后我们再修改例子比较一下。

    3、存储用户数据在ConcurentHashMap中

    有这么一个程序需要给每一个用户ID存储对应的用户上下文信息:

    public class Context {
        private String userName;
     
        public Context(String userName) {
            this.userName = userName;
        }
    }
    

    我们给每个用户新起一个线程,创建了一个实现了Runnable接口的SharedMapWithUserContext类,run()方法中的UserRepository会查询数据库返回传入用户ID的用户上下文信息。

    接下来我们把用户信息以用户ID为key存入ConcurentHashMap中:

    public class SharedMapWithUserContext implements Runnable {
      
        public static Map<Integer, Context> userContextPerUserId
          = new ConcurrentHashMap<>();
        private Integer userId;
        private UserRepository userRepository = new UserRepository();
     
        @Override
        public void run() {
            String userName = userRepository.getUserNameForUserId(userId);
            userContextPerUserId.put(userId, new Context(userName));
        }
     
        // standard constructor
    }
    

    我们来测试一下代码,给两个用户ID创建两个线程,在运行结束设置断言:userContextPerUserId的大小为2:

    SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
    SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
    new Thread(firstUser).start();
    new Thread(secondUser).start();
     
    assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);
    

    4、存储用户数据在ThreadLocal中

    我们重写一下我们的例子,这次把用户信息存在ThreadLocal中,每个线程都有自己的ThreadLocal实例。

    我们在使用的时候要特别小心因为每个ThreadLocal实例都关联了一个特定的线程,在我们的例子中,我们给每个用户ID创建了一个专用的线程,并且这是我们自己创建出来的,我们可以完全控制它们。(为什么这么说后面会解释到)

    run()方法拿到用户信息构造上下文对象并使用ThreadLocal的set()方法存储起来:

    public class ThreadLocalWithUserContext implements Runnable {
      
        private static ThreadLocal<Context> userContext 
          = new ThreadLocal<>();
        private Integer userId;
        private UserRepository userRepository = new UserRepository();
     
        @Override
        public void run() {
            String userName = userRepository.getUserNameForUserId(userId);
            userContext.set(new Context(userName));
            System.out.println("thread context for given userId: "
              + userId + " is: " + userContext.get());
        }
         
        // standard constructor
    }
    

    我们开启两个线程测试一下:

    ThreadLocalWithUserContext firstUser 
      = new ThreadLocalWithUserContext(1);
    ThreadLocalWithUserContext secondUser 
      = new ThreadLocalWithUserContext(2);
    new Thread(firstUser).start();
    new Thread(secondUser).start();
    

    代码运行完能看到ThreadLocal在每个线程中都设置了值:

    thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
    thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}
    

    5、小心把ThreadLocal和ExecutorService一起使用

    • 当前例子
      如果我们使用ExecutorService并往里面提交Runnable任务,使用ThreadLocal会出现不确定的结果。因为我们不能确定操作每个用户ID的Runnable任务每次都是被相同的线程操作,因此ThreadLocal会被不用的用户ID公用。

    • 不同的业务使用场景不同,也不能一棒子打死,比如下面这个例子
      最近在使用Spring的状态机的时候,处理一个动作发起的逻辑需要调用状态机实例的sendEvent方法发送消息,状态机返回当前事件是否处理成功,但不方便使用的是这个sendEvent返回的值是个boolean类型,里面如果有错误,我拿不到错误信息,抛异常也会被内部捕获到返回false:

    boolean sendEvent(Message<E> event);
    

    这个例子和上述不同的地方在于我使用状态机在每个Runnable任务执行中只需要拿到本次运行的信息,而不需要把当前信息和后面提交的任务共享使用。所以我调用get()拿到我在另一处逻辑里设置的错误信息时,我立即调用remove()表示本次ThreadLocal结束了。

    状态机判断逻辑:

    if (checkInvoice == null) {
        @SuppressWarnings("unchecked")
        ThreadLocal<String> requestError = (ThreadLocal<String>) messageHeaders.get(THREAD_LOCAL_NAME);
        Objects.requireNonNull(requestError).set("发票不能为空");
        return false;
    }
    

    状态机外部处理逻辑:

    // 返回的结果是Guard的返回结果
    boolean isHandleSuccess = stateMachine.sendEvent(message);
    if (isHandleSuccess) {
        S currentState = stateMachine.getState().getId();
        if (previousState != currentState) {
            stateMachinePersister.persist(stateMachine, uniqueKey);
        } else {
            log.info("状态没有变更,不需要持久化状态。previousState={},currentState={}",
                    previousState, currentState);
        }
        if (previousState.checkNextState(currentState)) {
            return buildSuccessResult(object, event.desc() + SUCCESS_TEXT);
        } else {
            String error = requestError.get();
            log.info("ThreadLocal value:{}", error);
            requestError.remove();
            return buildFailureResult(object, error);
        }
    }
    
  • 相关阅读:
    如何设计工作流引擎?
    产品特点概述驰骋工作流
    驰骋工作流程如何与您的系统进行耦合
    进制转换
    DNS欺骗(转) Anny
    Remember My Account Number(2 schema) Anny
    Mysql中的临时表使用方法(转) Anny
    Mysql任务调度(Event Schedular) Anny
    Using ssh connect to Amazon EC2 instance Anny
    Amazon EC2名词项目笔记(转) Anny
  • 原文地址:https://www.cnblogs.com/mrcharleshu/p/13167097.html
Copyright © 2011-2022 走看看