我正在参加「掘金·启航计划」
持续坚持原创输出,点击蓝字关注我吧
本文的主题是ThreadLocal,作者将其用于接口自动化框架设计,并解决用例并发执行带来的线程不安全问题。
首先介绍ThreadLocal的原理与使用,其次介绍使用场景以及接口自动化的简单演示。
一、解析ThreadLocal
ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,可以称为线程本地变量。
查看ThreadLocal实现源码,会发现有四个方法比较重要。
public T get()public void set(T value)protected T initialValue()public void remove()
下面通过实例给大家演示下ThreadLocal的原理与用法。
set就是设置值,get就是获取值,如果没有值,返回null,看上去,ThreadLocal就是一个单一对象的容器,比如:
public class ThreadLocalBasic { static ThreadLocal<Integer> local = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException{ // main 线程,设置local值为100 local.set(100); Thread child = new Thread(){ // Thread-0 线程 @Override public void run(){ System.out.println(Thread.currentThread().getName() + ": "+ local.get()); local.set(200); System.out.println(Thread.currentThread().getName() + ": "+local.get()); } }; child.start(); child.join(); System.out.println(Thread.currentThread().getName()+ ": " + local.get()); }}
编辑
通过结果发现,main线程对local变量的设置对Thread-0线程不起作用,Thread-0线程对local变量的改变也不会影响main线程,它们访问的虽然是同一个变量local,但每个线程都有自己的独立的值,这就是线程本地变量的含义。
initialValue用于提供初始值,可以通过匿名内部类的方式提供,当调用get方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回null。remove删掉当前线程对应的值,如果删掉后,再次调用get,会再调用initialValue获取初始值。
public class ThreadLocalInit { static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){ @Override protected Integer initialValue(){ return 100; } }; public static void main(String[] args) { System.out.println(local.get()); local.set(200); local.remove(); System.out.println(local.get()); }}
编辑
二、ThreadLocal使用场景
日期处理
ThreadLocal是实现线程安全的一种方案,比如对于DateFormat/SimpleDateFormat,每个线程使用自己的DateFormat,就不存在安全问题了,在线程的整个使用过程中,只需要创建一次,又避免了频繁创建的开销。
public class ThreadLocalDateFormat { static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String date2String(Date date){ return sdf.get().format(date); } public static Date string2Date(String str) throws ParseException{ return sdf.get().parse(str); }}
三、接口自动化上下文信息
ThreadLocal的典型用途是提供上下文信息,比如在一个Web服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,使用ThreadLocal就很方便,这样又在并发的场景下保证了线程安全。
下面以登录接口,测试查询接口为例,登录后直接将token存入ThreadLocal的token中,然后在queryTest使用token传递给queryBook接口用于鉴权。
public class ThreadLocalDemoTest { static ThreadLocal<String> token = new ThreadLocal<String>(); // 模拟登陆,将token set到上下文 @BeforeTest public static void login(){ token.set(LoginService.login("admin")); } @Test public void queryTest(){ String response = QueryBookService.queryBook(token.get()); Assert.assertEquals(response, "管理员查询图书"); }}
当然了,上述例子比较简单,线程本地变量仅存储token;如果需要存储更多的信息,那该咋办?
我们可以对ThreadLocal进行封装成ThreadLocalUtil,开发一个用于存储map的put和取map的getContextMap方法。
private final static ThreadLocal<Map<String, Object>> THREAD_CONTEXT = new MapThreadLocal();public static void put(String key, Object value) { getContextMap().put(key, value);}static Map<String, Object> getContextMap() { return THREAD_CONTEXT.get(); }
用例实践:
public class ThreadLocalTest { // 模拟登陆,将token set到上下文 @BeforeTest public static void login(){ ThreadLocalUtil.put("token", LoginService.login("admin")); ThreadLocalUtil.put("username", "软件质量保障"); } @Test public void queryTest(){ String response = QueryBookService.queryBook(ThreadLocalUtil.getContextMap().get("token").toString()); Assert.assertEquals(response, "管理员查询图书"); }}