大家好,我是那个在多线程坑里摸爬滚打了七年的菜鸡程序员。上周写接口时遇到个诡异问题:两个线程同时操作一个日期格式化工具类,结果返回的时间居然串了!老员工丢给我一句 “用 ThreadLocal 啊”,当时我心里直犯嘀咕:这玩意儿听起来像 “线程本地变量”,但到底怎么用?为啥能解决线程安全?今天就把我啃源码、查资料、踩坑无数的心得掰碎了讲,咱用人话聊技术,顺便穿插点打工人的辛酸泪。
刚听到 ThreadLocal 时,我脑补的是 “每个线程自带一个 Local 变量”,后来发现差不多就这意思!打个比方:假设你和室友合租,共用一个杯子(共享变量),结果他喝完没洗,你喝的时候就得先刷杯子(加锁)。但如果每人发一个专属杯子(ThreadLocal),各用各的,再也不用抢了 ——ThreadLocal 就是让每个线程拥有自己的变量副本,互不干扰。
举个代码栗子:
public class ThreadLocalDemo {
// 声明一个ThreadLocal,泛型是你要存的数据类型,这里存String
private static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
threadLocal.set("我是线程A的专属数据");
System.out.println(threadLocal.get()); // 输出:我是线程A的专属数据
threadLocal.remove(); // 用完记得删,不然会有内存泄漏风险,后面细说
}).start();
new Thread(() -> {
threadLocal.set("我是线程B的专属数据");
System.out.println(threadLocal.get()); // 输出:我是线程B的专属数据
threadLocal.remove();
}).start();
}
}
这里每个线程调用set()时,都会在自己的 “小本本” 里记一笔,get()时只拿自己的记录,再也不用担心多线程抢变量打架了。是不是比 synchronized 加锁简单多了?我第一次用的时候简直想给发明者磕头 —— 再也不用写ReentrantLock那种反人类的代码了!
好奇心驱使我翻了翻源码,发现这玩意儿的底层实现其实有点 “心机”:
刚工作时写日志模块,用SimpleDateFormat格式化时间,结果多线程下频繁报错。查资料才知道这玩意儿不是线程安全的,多个线程共用一个实例会互相干扰。这时候 ThreadLocal 简直是救星:
public class DateFormatUtils {
// 每个线程创建自己的DateFormat实例,互不干扰
private static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static String formatDate(Date date) {
return DATE_FORMAT.get().format(date);
}
}
现在每个线程都有自己的SimpleDateFormat,再也不会出现 “线程 A 的日期变成线程 B 的” 这种魔幻剧情了。我愿称 ThreadLocal 为 “工具类线程安全救星”!
在 Web 开发中,经常需要在一个请求的多个方法里获取用户 ID,传统做法是每个方法都传参数,麻烦得像唐僧念经。用 ThreadLocal 可以存整个请求的上下文:
// 在拦截器里设置用户信息
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String userId = request.getHeader("userId");
UserContextHolder.setUserId(userId); // 把userId存到ThreadLocal
return true;
}
}
// 自定义一个工具类封装ThreadLocal
public class UserContextHolder {
private static final ThreadLocal USER_ID = new ThreadLocal<>();
public static void setUserId(String userId) {
USER_ID.set(userId);
}
public static String getUserId() {
return USER_ID.get();
}
public static void remove() {
USER_ID.remove();
}
}
这样在 Controller、Service、甚至 Utils 里,都能直接UserContextHolder.getUserId(),再也不用层层传参了。我第一次用的时候感觉自己像开了挂,写代码的手速都快了三倍!
以前写 JDBC 时,每个线程需要自己的数据库连接,不然会出现 “线程 A 关了线程 B 的连接” 这种惨案。用 ThreadLocal 存连接,每个线程拿自己的,用完remove(),安全又省心:
public class ConnectionHolder {
private static final ThreadLocal CONNECTION = new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = CONNECTION.get();
if (conn == null) {
// 从数据源获取新连接
conn = DataSourceUtils.getConnection();
CONNECTION.set(conn);
}
return conn;
}
}
不过现在有数据库连接池框架帮我们处理这些,但原理相通 ——ThreadLocal 就是让每个线程管好自己的一亩三分地。
我曾在一个静态工具类里用 ThreadLocal 存临时数据,结果多线程调用时数据乱套了。原因是:静态方法的 ThreadLocal 是类级别的,所有线程共享这个 ThreadLocal 实例,但每个线程存的值是存在自己的threadLocals里的,这本身没问题。但如果忘记remove(),线程池里的线程会复用,导致下一个任务拿到上一个任务的数据 —— 就像你点了杯奶茶,结果拿到别人喝剩的,恶心不?正确做法:每次用set()之后,不管是否报错,都在finally里remove(),养成好习惯。
见过有同事用 ThreadLocal 存整个业务对象,比如把一个巨大的UserInfo对象存进去,结果线程没及时清理,内存直接爆炸。ThreadLocal 的定位是 “线程内的局部变量”,适合存小数据(比如用户 ID、请求 ID),别拿它当大胃王,啥都往里塞。
刚开始我纠结过这个问题,后来老员工一句话点醒我:如果变量是 “线程独有的数据”(比如每个线程的上下文),用 ThreadLocal;如果是 “多个线程需要共享修改的数据”(比如计数器),必须加锁。比如统计在线用户数,每个线程都要修改总数,这时候 ThreadLocal 没用,得用synchronized或AtomicInteger。
折腾了一个月 ThreadLocal,我现在的感受是:这玩意儿就像武侠小说里的暗器 —— 学会了能轻松解决线程安全问题,用错了会伤自己。总结几个关键点:
最后送大家一句打油诗:ThreadLocal 真是妙,线程数据各自保,用完记得要删掉,内存泄漏跑不了,场景选对效率高,代码写得呱呱叫!
如果你在写多线程代码时遇到变量乱串的问题,试试 ThreadLocal 吧 —— 相信我,那种不用写锁就能保证线程安全的感觉,简直比周五提前半小时下班还爽!有啥问题欢迎留言,咱们一起在踩坑路上互相搀扶~