并发编程-线程安全策略之线程封闭

Posted 爱上口袋的天空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程-线程安全策略之线程封闭相关的知识,希望对你有一定的参考价值。

脑图

概述

我们通过介绍使用线程安全的不可变对象可以保证线程安全。

除了上述方法,还有一种办法就是:线程封闭。

 线程封闭的三种方式

  • Ad-hoc 线程封闭 ,完全由程序控制实现,不可控,不要使用
  • 堆栈封闭 方法中定义局部变量。不存在并发问题

堆栈封闭其实就是方法中定义局部变量。不存在并发问题。

多个线程访问一个方法的时候,方法中的局部变量都会被拷贝一份到线程的栈中(Java内存模型),所以局部变量是不会被多个线程所共享的。

局部变量的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

  • ThreadLocal 线程封闭 将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制

ThreadLocal类:线程本地变量。如果将变量使用ThreadLocal来包装,那么每个线程往这个ThreadLocal中读写都是线程隔离的,互相之间不会影响。

它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

 Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

每个线程在往某个ThreadLocal里set值的时候,都会往自己的ThreadLocalMap里存,get也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

示例

堆栈封闭

多个线程访问一个方法,该方法中的局部变量都会被拷贝一份儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

局部变量,没啥好说的 ,直接看ThreadLocal实现线程安全吧

ThreadLocal

假设我们将用户信息放到ThreadLocal中,然后从ThreadLocal中获取该用户信息。 这个例子中的场景不是很严谨,仅仅仅是为了演示ThreadLocal的用法

这里我们通过拦截器(过滤器也行) ,在调用Controller之前 ,重写拦截器的preHandle方法,通常情况下在该方法中从session中获取user信息,将写入到ThreadLocal, 重写afterCompletion方法不管是方法执行正常还是异常都会执行该方法,在该方法中移除threadlocal中的值,否则累计太多容易造成内溢出。

Step1. ThreadLocal操作类 

通常情况下都要具备三个方法 add get remove 。特别是remove,否则容易造成内存溢出

package com.artisan.example.threadLocal;

import lombok.extern.slf4j.Slf4j;

/**
 * 通常情况下都要具备三个方法  add  get  remove 
 * 特别是remove,否则容易造成内存泄漏
 * @author yangshangwei
 *
 */
@Slf4j
public class RequestHolder {

	private final static ThreadLocal<ArtisanUser>  USER_HOLDER = new ThreadLocal<ArtisanUser>();
	
	public static void addCurrentUser(ArtisanUser artisanUser) {
		//  将当前线程作为key, artisanUser作为value 存入ThreadLocal类的ThreadLocalMap中
		USER_HOLDER.set(artisanUser);
		log.info("将artisanUser:{} 写入到ThreadLocal",artisanUser.toString());
	}
	
	public static ArtisanUser getCurrentUser() {
		//  通过当前线程这个key ,获取存放在当前线程的ThreadLocalMap变量中的value
		ArtisanUser artisanUser = USER_HOLDER.get();
		log.info("从ThreadLocal中获取artisanUser:{}",artisanUser.toString());
		return artisanUser;
		
	}
	
	public static void removeCurrentUser() {
		log.info("从ThreadLocal中移除artisanUser:{}", getCurrentUser());
		//  通过当前线程这个key获取当前线程的ThreadLocalMap,从中移除value
		USER_HOLDER.remove();
	}
	
}

Step2. 自定义过滤器

在过滤器中 ,重写对应的方法 添加 和 移除 threadLocal

package com.artisan.interceptors;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.checkerframework.checker.index.qual.LengthOf;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.artisan.example.threadLocal.ArtisanUser;
import com.artisan.example.threadLocal.RequestHolder;

import lombok.extern.slf4j.Slf4j;

/**
 * 实现 Handlerlnterceptor接口,覆盖其对应的方法即完成了拦截器的开发
 * 
 * @author yangshangwei
 *
 */
@Slf4j
public class MyInterceptor implements HandlerInterceptor {

	/**
	 * preHandle在执行Controller之前执行 
	 * 返回true:继续执行处理器逻辑,包含Controller的功能 
	 * 返回false:中断请求
	 * 
	 * 处理器执行前方法
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		log.info("MyInterceptor-处理器执行前方法preHandle,返回true则不拦截后续的处理");
		
		// 模拟user存在session中
		ArtisanUser user = new ArtisanUser();
		user.setName("artisan");
		user.setAge(20);
		request.getSession().setAttribute("user", user);
		
		// 将用户信息添加到ThreadLocal中
		RequestHolder.addCurrentUser((ArtisanUser)request.getSession().getAttribute("user"));
		
		return true;
	}

	/**
	 * postHandle在请求执行完之后渲染ModelAndView返回之前执行
	 * 
	 * 处理器处理后方法
	 */
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		
	}

	/**
	 * afterCompletion在整个请求执行完毕后执行,无论是否发生异常都会执行
	 * 
	 * 处理器完成后方法
	 * 
	 * 
	 * 在这个方法中移除当前用户
	 */
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		log.info("MyInterceptor-处理器完成后方法afterCompletion");
		RequestHolder.removeCurrentUser();
	}

}

Step3. 注册拦截器,配置拦截规则

注册拦截器,配置拦截规则

package com.artisan.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.artisan.interceptors.MyInterceptor;


/**
 * 实现 WebMvcConfigurer 接 口, 最后覆盖其addInterceptors方法进行注册拦截器
 * @author yangshangwei
 *
 */

// 标注为配置类
@Configuration 
public class WebConfig implements WebMvcConfigurer {
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		//  注册拦截器到 Spring MVC 机制, 然后 它会返 回 一个拦截器注册
		InterceptorRegistration regist =  registry.addInterceptor(new MyInterceptor());
		// 指定拦截匹配模式,限制拦截器拦截请求
		regist.addPathPatterns("/artisan/threadLocal/*");
		
	}

}

Step4. Controller层调用

通过RequestHolder.getCurrentUser() 获取存到ThreadLocal中的user信息

package com.artisan.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.artisan.example.threadLocal.ArtisanUser;
import com.artisan.example.threadLocal.RequestHolder;

@RestController
@RequestMapping("/artisan/threadLocal")
public class ThreadLocalTestController {

	
	@GetMapping("/getCurrentUser")
	public ArtisanUser getCurrentUser() {
		return RequestHolder.getCurrentUser();
	}
}

Step5. 测试

启动Spring Boot 工程,打开postman,请求

http://localhost:8080/artisan/threadLocal/getCurrentUser

postman 或者浏览器

控制层可以直接通过RequestHold这个threalocal封装类直接获取到存放在ThreadLocal中的变量信息,说明OK。

观察后台日志:

 

以上是关于并发编程-线程安全策略之线程封闭的主要内容,如果未能解决你的问题,请参考以下文章

并发编程:线程安全策略

并发编程-线程安全策略之不可变对象

并发编程-线程安全策略之两种类型的同步容器

并发编程-线程安全策略之并发容器(J.U.C)中的集合类

《java并发编程实战》读书笔记3--对象的组合

Java并发编程实例封闭