springboot 使用ThreadLocal的实例代码

目录
  • springboot 使用ThreadLocal
  • ThreadLocal在springboot使用中的坑

springboot 使用ThreadLocal

本文参考慕课教程给出一个在spring boot中使用ThreadLocal实现线程封闭的实例。

首先创建一个包含ThreadLocal成员变量的实例:

public class RequestHolder {
    private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>();
    public static void add(Long id) {
        requestHolder.set(id);
    }

    public static Long getId() {
        return requestHolder.get();
    }

    public static void remove() {
        requestHolder.remove();
    }
}

编写一个Controller类,请求该类的test()方法获取ThreadLocal中存储的id:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/threadLocal")
public class ThreadLocalController {

    @RequestMapping("/test")
    @ResponseBody
    public Long test() {
        return RequestHolder.getId();
    }
}

编写过滤器,在请求到达Servlet之前(请求->tomcat容器->filter->servlet->inteceptor->controller),将当前线程的id添加到ThreadLocal中:

import com.mmall.concurrency.example.threadLocal.RequestHolder;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Slf4j
public class HttpFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        log.info("do filter, {}, {}", Thread.currentThread().getId(), request.getServletPath());
        //在ThreadLocal中添加当前线程的id
        RequestHolder.add(Thread.currentThread().getId());
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {
    }
}

编写拦截器,当请求处理完成后(从Controller返回后),清除ThreadLocal中的id,避免内存泄漏。

import com.mmall.concurrency.example.threadLocal.RequestHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class HttpInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle");
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("ThreadId:"+RequestHolder.getId());
        RequestHolder.remove();
        log.info("afterCompletion");
        return;
    }
}

最后,我们需要在spring boot启动类上注册我们定义的Filer及Inteceptor,并设置拦截路径。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication
public class ConcurrencyApplication extends WebMvcConfigurerAdapter{
 public static void main(String[] args) {
  SpringApplication.run(ConcurrencyApplication.class, args);
 }

 @Bean
 public FilterRegistrationBean httpFilter() {
  FilterRegistrationBean registrationBean = new FilterRegistrationBean();
  registrationBean.setFilter(new HttpFilter());
  registrationBean.addUrlPatterns("/threadLocal/*");
  return registrationBean;
 }

 @Override
 public void addInterceptors(InterceptorRegistry registry) {
  registry.addInterceptor(new HttpInterceptor()).addPathPatterns("/**");
 }
}

在浏览器或者postman中输入http://localhost:8080/threadLocal/test

观察输出结果:

2018-11-09 11:16:51.287  INFO 34076 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-11-09 11:16:51.290  INFO 34076 --- [           main] c.m.concurrency.ConcurrencyApplication   : Started ConcurrencyApplication in 1.718 seconds (JVM running for 2.132)
2018-11-09 11:17:03.060  INFO 34076 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-11-09 11:17:03.060  INFO 34076 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2018-11-09 11:17:03.072  INFO 34076 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 12 ms
2018-11-09 11:17:03.078  INFO 34076 --- [nio-8080-exec-2] com.mmall.concurrency.HttpFilter         : do filter, 29, /threadLocal/test
2018-11-09 11:17:03.090  INFO 34076 --- [nio-8080-exec-2] com.mmall.concurrency.HttpInterceptor    : preHandle
2018-11-09 11:17:03.124  INFO 34076 --- [nio-8080-exec-2] com.mmall.concurrency.HttpInterceptor    : ThreadId:29
2018-11-09 11:17:03.124  INFO 34076 --- [nio-8080-exec-2] com.mmall.concurrency.HttpInterceptor    : afterCompletion

从打印的日志结果中,我们看到在Filter中我们将当前线程的id 29添加到了ThreadLocal中,随后在Inteceptor中打印并删除了id。

ThreadLocal在springboot使用中的坑

ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景。现在在Springboot中我做如下场景的使用:

使用 Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个 Integer 的值,来暂且代表需要在线程中保存的用户信息,这个值初始是 null。在业务逻辑中,我先从 ThreadLocal 获取一次值,然后把外部传入的参数设置到 ThreadLocal 中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。

@RestController
public class threadLocal {
    private ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);
    @RequestMapping("wrong")
    public Map wrong(@RequestParam("userId") Integer userId) {
        //设置用户信息之前先查询一次ThreadLocal中的用户信息
        String before = Thread.currentThread().getName() + ":" + currentUser.get();
        //设置用户信息到ThreadLocal
        currentUser.set(userId);

            //设置用户信息之后再查询一次ThreadLocal中的用户信息
            String after = Thread.currentThread().getName() + ":" + currentUser.get();
            //汇总输出两次查询结果
            Map result = new HashMap();
            result.put("before", before);
            result.put("after", after);
            return result;
    }
}

为了让问题快速的重现,我在配置文件中设置一下 Tomcat 的参数,把工作线程池最大线程数设置为 1,这样始终是同一个线程在处理请求:

server.tomcat.max-threads=1

运行程序后先让用户 1 来请求接口,可以看到第一和第二次获取到用户 ID 分别是 null 和 1,符合预期:随后用户 2 来请求接口,这次就出现了 Bug,第一和第二次获取到用户 ID 分别是 1 和 2,显然第一次获取到了用户 1 的信息,原因就是 Tomcat 的线程池重用了线程。

在 Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境中,并不能认为没有显式开启多线程就不会有线程安全问题,所以使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。修改后代码如下:

@RestController
public class threadLocal {
    private ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);
    @RequestMapping("wrong")
    public Map wrong(@RequestParam("userId") Integer userId) {
        //设置用户信息之前先查询一次ThreadLocal中的用户信息
        String before = Thread.currentThread().getName() + ":" + currentUser.get();
        //设置用户信息到ThreadLocal
        currentUser.set(userId);
        try {
            //设置用户信息之后再查询一次ThreadLocal中的用户信息
            String after = Thread.currentThread().getName() + ":" + currentUser.get();
            //汇总输出两次查询结果
            Map result = new HashMap();
            result.put("before", before);
            result.put("after", after);
            return result;
        } finally {
         //增加移除处理
            currentUser.remove();
        }
    }
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • java ThreadLocal使用案例详解

    本文借由并发环境下使用线程不安全的SimpleDateFormat优化案例,帮助大家理解ThreadLocal. 最近整理公司项目,发现不少写的比较糟糕的地方,比如下面这个: public class DateUtil { private final static SimpleDateFormat sdfyhm = new SimpleDateFormat( "yyyyMMdd"); public synchronized static Date parseymdhms(String

  • 使用springboot单例模式与线程安全问题踩的坑

    springboot单例模式与线程安全问题踩的坑 最近有客户反映,使用公司产品时,偶尔会存在崩溃情况,自己测试无问题,然后去查日志,是报空指针. 于是顺藤摸瓜 往上找,好嘛,之前的开发使用了成员变量,感觉问题就是在这里了,因为众所周知,springboot 采用的是单例模式,所以,使用成员变量时一定要谨慎. 下面上一张该类的截图: 大家可能看到了,该类上面加上了@Scope("prototype") 注解,该注解的作用是将该类变成多例模式.讲道理因为变为了多例,应该不会有线程问题了.

  • spring boot中多线程开发的注意事项总结

    前言 Springt通过任务执行器(TaskExecutor)来实现多线程和并发编程.使用ThreadPoolTaskExecutor可实现一个基于线程池的TaskExecutor.而实际开发中任务一般是非阻碍的,即异步的,所以我们要在配置类中通过@EnableAsync 开启对异步任务的支持,并通过实际执行Bean的方法中使用@Async注解来声明其是一个异步任务. 基于springboot的多线程程序开发过程中,由于本身也需要注入spring容器进行管理,才能发挥springboot的优势.

  • springboot 使用ThreadLocal的实例代码

    目录 springboot 使用ThreadLocal ThreadLocal在springboot使用中的坑 springboot 使用ThreadLocal 本文参考慕课教程给出一个在spring boot中使用ThreadLocal实现线程封闭的实例. 首先创建一个包含ThreadLocal成员变量的实例: public class RequestHolder { private final static ThreadLocal<Long> requestHolder = new Thr

  • SpringBoot集成swagger的实例代码

    Swagger 是一款RESTFUL接口的文档在线自动生成+功能测试功能软件.本文简单介绍了在项目中集成swagger的方法和一些常见问题.如果想深入分析项目源码,了解更多内容,见参考资料. Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.总体目标是使客户端和文件系统作为服务器以同样的速度来更新.文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步.Swagger 让部署管理和使用功能强大的API从未如此简单. 对于

  • SpringBoot QQ邮箱发送邮件实例代码

    目录 1.获取QQ邮箱授权码 2.导入邮箱发送依赖启动器 3.配置文件yml添加邮件服务配置 4.编写接口IMailService 5.编写实现MailServiceImpl 6.Controller调用 7.thymeleaf模板 mailTemplate.html 总结 SpringBoot整合邮件任务(QQ邮箱发送) 1.获取QQ邮箱授权码 2.导入邮箱发送依赖启动器 使用定制邮件模板的方法实现通用邮件发送,Thymeleaf构建邮件模板需要一起导入依赖. <!-- Mail --> &

  • SpringBoot整合MongoDB完整实例代码

    目录 一.新建项目 二.docker-compose 配置mongoDB 三.SpringBoot配置MongoDB 问题:Exception authenticating MongoCredential 四.编写测试类 五.源码地址 一.新建项目 我们这次直接从IEDA创建项目,具体配置如下,还是万年的Java8. 二.docker-compose 配置mongoDB docker-compose.yml的具体配置如下,注意的是本地的文件夹data2022可以根据需要改成自己的名称,如果本地还

  • Springboot整合https的实例代码

    目录 1 简介 2 密码学基础 2.1 密码体制 2.2 两种加密方式 2.2.1 对称加密 2.2.2 非对称加密 2.3 证书 3 Springboot整合HTTPS 3.1 先让Web跑起来 3.2 生成密钥文件jks 3.3 重新配置并重启 3.4 使用PKS12格式 总结 1 简介 HTTP是不安全的,我们需要给它套上SSL,让它变成HTTPS.本文章将用实例介绍Springboot整合HTTPS. 2 密码学基础 要谈https就要谈Security,自然就要谈安全:谈及安全,就必然

  • springboot整合 beatlsql的实例代码

    BeetSql是一个全功能DAO工具, 同时具有hibernate 优点 & Mybatis优点功能,适用于承认以SQL为中心,同时又需求工具能自动能生成大量常用的SQL的应用. beatlsql 优点 开发效率 无需注解,自动使用大量内置SQL,轻易完成增删改查功能,节省50%的开发工作量 数据模型支持Pojo,也支持Map/List这种快速模型,也支持混合模型 SQL 模板基于Beetl实现,更容易写和调试,以及扩展 维护性 SQL 以更简洁的方式,Markdown方式集中管理,同时方便程序

  • springboot集成activemq的实例代码

    ActiveMQ ActiveMQ 是Apache出品,最流行的,能力强劲的开源消息总线.ActiveMQ 是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现,尽管JMS规范出台已经是很久的事情了,但是JMS在当今的J2EE应用中间仍然扮演着特殊的地位. 特性 多种语言和协议编写客户端.语言: Java,C,C++,C#,Ruby,Perl,Python,PHP.应用协议: OpenWire,Stomp REST,WS Notification,XMPP,AMQP

  • SpringBoot整合JPA的实例代码

    JPA全称Java Persistence API.JPA通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中. JPA 的目标之一是制定一个可以由很多供应商实现的API,并且开发人员可以编码来实现该API,而不是使用私有供应商特有的API. JPA是需要Provider来实现其功能的,hibernate就是JPA Provider中很强的一个,应该说无人能出其右.从功能上来说,JPA就是Hibernate功能的一个子集. 添加相关依赖 添加spring

  • Java springboot 整合 Nacos的实例代码

    Nacos注册中心使用 1)工程添加依赖包 <!-- nacos注册中心依赖包 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 监控检查--> <dependency&g

  • SpringBoot集成Druid的实例代码

    快速开始 依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency> 如果需要配置日志: <dependency> <groupId>log4j</groupId>

随机推荐