ThreadLocal原理解析及常用场景


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();
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex·Guangzhou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值