ThreadLocal 可以解决“线程安全问题”。
也可以作为上下文暂存数据以备后续步骤获取。
但是 ThreadLocal 用不好的确容易产生故障,因而有些团队不允许使用 ThreadLocal。
最核心的一个原因是很容易忘记清理,在线程池环境下复用导致串环境。
那么,有什么优雅的解法没?本文给出自己的一个解法。
package basic.thread; import com.alibaba.ttl.TransmittableThreadLocal; import java.util.HashMap; import java.util.Map; public class ThreadContext { private static final ThreadLocal<Map<String, Object>> CONTEXT = new TransmittableThreadLocal<>(); /** * 初始化上下文 */ public static void initContext() { Map<String, Object> con = CONTEXT.get(); if (con == null) { CONTEXT.set(new HashMap<>(8)); } else { CONTEXT.get().clear(); } } /** * 清除上下文 */ public static void clearContext() { CONTEXT.remove(); } /** * 获取上下文内容 */ public static <T> T getValue(String key) { Map<String, Object> con = CONTEXT.get(); if (con == null) { return null; } return (T) con.get(key); } /** * 设置上下文参数 */ public static void putValue(String key, Object value) { Map<String, Object> con = CONTEXT.get(); if (con == null) { CONTEXT.set(new HashMap<>(8)); con = CONTEXT.get(); } con.put(key, value); } }
写入如下:
public Result<R> executeAbility(T ability) { //初始化上下文 ThreadContext.initContext(); try { //省略核心业务代码 } finally { ThreadContext.clearContext(); } }
相信绝大多数人会止步于此,但我认为这还是不够的。
如何才能避免忘掉清理 threadlocal 呢?
JDK 源码中有没有类似的案例呢?
想想IO 读写文件后,也是需要采用类似的做法去释放资源,JDK 提供了 try-with-resource 让释放资源更简单,使用者不需要手动写 finnaly 去释放资源。
普通案例:
使用 try-with-resource
另外我们知道,可以通过实现 AutoCloseable 来自定义 try-with-resource 的资源。
但最后发现并不是很适配,因为在传递上下文这种场景下, ThreadLocal 工具类通常都是静态的,而且即使不适用静态,获取属性时还要将该对象传递下去,不是很方便。
当然,如果大家不想以静态的方式使用,也可以考虑实现 AutoClosebale 接口,使用 try-with-resource 的机制。
我们是否也可以采用类似的机制呢?
可以直接将初始化和清理方法私有化,提供无参和带返回值的封装,使用 Runnbale 和 Callable 将调用作为参数传入,在封装的方法中封装 try- finally 逻辑。
package basic.thread; import com.alibaba.ttl.TransmittableThreadLocal; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; public class ThreadContext { private static final ThreadLocal<Map<String, Object>> CONTEXT = new TransmittableThreadLocal<>(); /** * 初始化上下文 */ private static void initContext() { Map<String, Object> con = CONTEXT.get(); if (con == null) { CONTEXT.set(new HashMap<>(8)); } else { CONTEXT.get().clear(); } } /** * 清除上下文 */ private static void clearContext() { CONTEXT.remove(); } /** * 获取上下文内容 */ public static <T> T getValue(String key) { Map<String, Object> con = CONTEXT.get(); if (con == null) { return null; } return (T) con.get(key); } /** * 设置上下文参数 */ public static void putValue(String key, Object value) { Map<String, Object> con = CONTEXT.get(); if (con == null) { CONTEXT.set(new HashMap<>(8)); con = CONTEXT.get(); } con.put(key, value); } /** * 自动回收的封装 */ public static void runWithAutoClear(Runnable runnable){ initContext(); try{ runnable.run(); }finally{ CONTEXT.remove(); } } /** * 自动回收的封装 */ public static <T> T callWithAutoClear(Callable<T> callable){ initContext(); try{ try { return callable.call(); } catch (Exception e) { throw new RuntimeException(e); } }finally{ CONTEXT.remove(); } } }
使用参考:
public Result<R> executeAbility(T ability) { return AbilityContext.callWithAutoClear(()->{ // 业务核心代码 }); }
只要思想不滑坡,办法总比困难多。
我们应该想办法去解决问题,而不是你回避问题。
当看到有些解决方案仍然容易出错时,应该想办法去做进一步的改进。
当然,如果不想使用 ThreadLocal 还想暂存对象给后续环节使用,可以定义上下文对象,在不同的执行步骤间传递。
类似的文章还有:《Map 有变动时触发特定行为实现》
创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。