ThreadLocal是并发编程的常用类,它提供一种线程绑定机制,可以基于此对象将某个对象绑定当前线程中,也可以从当前线程获取某个对象,利用它可以实现在同一个线程内数据共享,从而有效解决线程安全问题。
常用方法:
- initialValue() 创建要绑定的对象(其实就是set过程的封装)
- set() 绑定对象到当前线程
- get() 从当前线程获取对象
- remove() 从当前线程移出对象(可有效防止内存泄漏)
1、ThreadLocal原理
ThreadLocalMap是ThreadLocal的静态内部类,每个线程都有一个ThreadLocalMap对象,一个ThreadLocalMap对象可以有多个Entry对象,Entry中的key为ThreadLocal类型的对象,Entry中的value就是要绑定到当前线程中的对象。
每次set值都是获取当前线程的ThreadLocalMap,然后以当前ThreadLocal实例为key放进去,get值也是先获取当前线程的ThreadLocalMap,然后从中取出值,这样就是线程绑定。
2、内存泄漏问题
ThreadLocalMap.Entry继承的是WeakReference,即这个ThreadLocalMap的Key是弱引用。在GC时会回收掉。当线程的生命周期大于ThreadLocal的生命周期时(大部分情况都是的,因为线程通过线程池管理会重复利用),那么就可能存在ThreadLocalMap<null, Object>的情况(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在),这个Object就是泄漏的对象。
关于为何使用热引用,参考文章:https://blog.csdn.net/zhang_zhenwei/article/details/90051849
为避免内存泄漏,应在线程结束前,在try……finally……中,执行ThreadLocal的remove()方法。
3、ThreadLocal应用
3.1、场景一:保证线程安全
ThreadLocal保证线程安全的原理是,通过其线程绑定机制,为每个线程都保存了一个数据副本,这样每个线程都只修改自己的副本,而不会影响其他线程的副本,确保了线程安全。
众所周知,日期类Date会有线程安全问题是因为格式化类SimpleDateFormat是线程不安全的类:
@RestController
@RequestMapping("/thread")
public class ThreadController {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@RequestMapping("/test1")
public void test1() throws Exception{
final String[] strArr = new String[] {"2021-02-09 10:24:00", "2021-02-10 20:48:00", "2021-02-11 12:24:00"};
int max = 2;
int min = 0;
Random random = new Random();
int s = random.nextInt(max)%(max-min+1) + min;
System.out.println(Thread.currentThread().getName()+ "\t" + sdf.parse(strArr[s]));
}
}
使用jmeter模拟并发环境:
可以看到,其中的有些线程抛出了运行时异常 NumberFormatException,而有些则输出了奇怪的日期结果。很明显,在多线程环境中 SimpleDateFormat 确实存在线程安全的问题。
SimpleDateFormat之所以线程不安全是因为它内部有一个存放日期的成员变量Calendar:
当每次调用format()方法或parse()方法时,都会对这个对象进行重新设置值和清除值:
当SimpleDateFormat以成员变量的方式使用时,Calendar对象也会被多线程共用,这就会造成线程安全问题!
关于成员变量为何会有线程安全问题,详见:JVM内存模型解析
解决方案:
- 一是以局部变量的方式使用SimpleDateFormat,这样不会产生线程共享,但是每调用一次方法都会new一个SimpleDateFormat对象,在并发高的环境下会增加内存压力;
- 二是使用ThreadLocal类进行线程绑定,代码示例如下:
日期工具类:
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>();
private static SimpleDateFormat getDateFormat() {
SimpleDateFormat dateFormat = local.get();
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
local.set(dateFormat);
}
return dateFormat;
}
public static String format(Date date) {
return getDateFormat().format(date);
}
public static Date parse(String dateStr) throws Exception {
return getDateFormat().parse(dateStr);
}
}
@RestController
@RequestMapping("/thread")
public class ThreadController {
@RequestMapping("/test1")
public void test1() throws Exception{
final String[] strArr = new String[] {"2021-02-09 10:24:00", "2021-02-10 20:48:00", "2021-02-11 12:24:00"};
int max = 2;
int min = 0;
Random random = new Random();
int s = random.nextInt(max)%(max-min+1) + min;
System.out.println(Thread.currentThread().getName()+ "\t" + DateUtil.parse(strArr[s]));
}
}
测试结果显示正常:
3.2、场景二:保存全局信息
实例需要在多个方法中共享,但不希望被多线程共享。
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
UserThreadLocal:
public class UserThreadLocal {
/**
* 1.如果需要存储单个对象 写对象类型
* 2.如果需要保存多个数据 使用Map集合
*/
private static ThreadLocal<User> thread = new ThreadLocal<>();
//往ThreadLocal中存数据
public static void set(User user) {
thread.set(user);
}
//从ThreadLocal中取数据
public static User get() {
return thread.get();
}
//清楚ThreadLocal中的数据,防止内存泄漏
public static void remove() {
thread.remove();
}
}
UserInterceptor:
@Component
public class UserInterceptor implements HandlerInterceptor {
@Autowired
private JedisCluster jedisCluster;
/**
* 重写preHandle方法实现用户登录校验
* boolean:
* true:请求放行
* false:请求拦截,且一般有重定向执行
* 用户登录校验的业务实现:
* 1.判断用户时候登录cookie("JT_TICKET",ticket)
* 2.检查redis中是否有数据
* 3.return true
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//获取用户登录信息
Cookie[] cookies = request.getCookies();
String ticket=null;
if (cookies.length>0) {
for (Cookie cookie : cookies) {
if ("JT_TICKET".equals(cookie.getName())) {
ticket=cookie.getValue();
break;
}
}
}
//判断ticket取值是否为空,不为null时校验redis中是否有数据
if (!StringUtils.isEmpty(ticket)) {
String userJSON = jedisCluster.get(ticket);
if (!StringUtils.isEmpty(userJSON)) {
User user = ObjectMapperUtil.toObject(userJSON, User.class);
UserThreadLocal.set(user);//把当前获取到的用户登录数据绑定到当前线程
return true;//redis中有数据表示用户已经登录,放行请求
}
}
//重定向到登录页面
response.sendRedirect("/user/login.html");
return false;//拦截请求
}
/**
* 删除ThreadLocal防止内存泄漏
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
UserThreadLocal.remove();
}
}
动态获取UserId:
/**
* 修改购物车商品数量(如果{参数}的名称与对象中的属性一致,
* 则可以使用对象直接取值.)
*/
@RequestMapping("/update/num/{itemId}/{num}")
@ResponseBody
public SysResult updateNum(Cart cart) {
Long userId = UserThreadLocal.get().getId();
cart.setUserId(userId);
cartService.updateNum(cart);
return SysResult.success();
}