上一节介绍了不可变对象,通过在某些情况下将不会修改的类对象设计成不可变对象,来让对象在多个线程间保证对象是线程安全的。归根到底,相当于躲避开了并发问题,实现好的并发是一件很困难的事情,所以很多时候都想躲避并发,避免并发除了设计成不可变对象其实还有一个简单的方法就是线程封闭。
什么是线程封闭?
其实就是把对象封装到一个线程里,只有这一个线程能看到这个对象。那么这个对象就算不是线程安全的,也不会出现线程安全方面的问题了,因为他只能在一个线程里面进行访问。那么如何实现线程封闭呢?
如何实现线程封闭?
- Ad-hoc 线程封闭:程序控制实现,最糟糕,忽略
- 堆栈封闭:局部变量,无并发问题。是我们现实中使用最多的封闭了,简单说就是局部变量。多个线程访问一个方法的时候,方法中的局部变量都会被拷贝一份到线程栈中,所以局部变量是不会被线程共享的,因此不会出现并发问题。所以全局变量容易引起并发问题。
- ThreadLocal线程封闭:特别好的封闭方法。ThreadLocal内部维护了一个map,key是线程的名称,value就是封闭的对象。
ThreadLocal
正常来讲我们每一个请求对服务器来讲都是一个线程在运行,我们希望线程间隔离,一个线程在被后端服务器实际处理的时候,可以通过Filter过滤器取出当前的用户,然后将数据存放在ThreadLocal中,当线程被接口的service以及其他相关类进行处理的时候很可能需要在取出当前用户,这时就可以随时随地从ThreadLocal中直接拿到之前存储的值这样用起来就很方便了。如果我们不这样做,会有什么麻烦呢?因为我们的登录用户通常是从request中取出来的,因此需要带上request或者从request中取出来的用户信息,从controller层开始不停的往下传,甚至会传到一些util类中,这样会使得代码看起来很臃肿。当使用ThreadLocal和Filter,就可以很方便的在接口处理之前,前取出相关的信息,在接口实际处理的时候,什么时候需要什么时候再把信息取出来,这样代码在设计的时候就容易多了,不至于把request从controller一直传递下去。
具体使用实例如下:
新建一个类:
public class RequestHolder {
//因为当前没有登录用户,我们用线程id来充当
private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>();
/**
* 请求进入到后端服务器,但是还没有实际处理的时候调用add,可以使用Filter
* @param id
*/
public static void add(Long id) {
//虽然只传入id,但是threadLocal会取出当前线程id放到map中的key,value是传入的值
requestHolder.set(id);
}
public static Long getId() {
return requestHolder.get();
}
/**
* 如果不做remove的话,会造成内存泄漏,数据永远不会释放掉
* 需要在接口真正处理完成之后进行调用,可以使用interceptor
*/
public static void remove() {
requestHolder.remove();
}
}
这个类就用来存放ThreadLocal。
新建一个Filter:
@Slf4j
public class HttpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// httpServletRequest.getSession().getAttribute("user");
log.info("do filter , {}, {}", Thread.currentThread().getId(), ((HttpServletRequest) request).getServletPath());
RequestHolder.add(Thread.currentThread().getId());
// 如果这个Filter不想拦截住这个请求,只想做单独的数据处理时,要调用chain.doFilter,使得拦截器处理完
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
新建一个Interceptor:
@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 {
RequestHolder.remove();
log.info("afterCompletion");
return ;
}
}
配置filter和interceptor:
@Configuration
public class Config implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean<HttpFilter> httpFilter(){
FilterRegistrationBean<HttpFilter> filterRegistrationBean = new FilterRegistrationBean<>();
// 设置filter
filterRegistrationBean.setFilter(new HttpFilter());
// 拦截规则
filterRegistrationBean.addUrlPatterns("/threadLocal/*");
return filterRegistrationBean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HttpInterceptor()).addPathPatterns("/**");
}
}
新建一个controller进行测试:
@RestController
@RequestMapping("threadLocal")
public class ThreadLocalController {
@GetMapping("/test")
public Long test() {
return RequestHolder.getId();
}
}
在浏览器中输入localhost:8080/threadLocal/test可以输出线程的id,与后台的输出id一致。