多线程共享变量的维护是非常头痛的问题,采用乐观悲观策略,悲观策略简单地做法我们可以对共享变量加锁实现,但是锁的开销是比较大的,因此我们也可以通过乐观策略,采用类似 CAS(Compare And Set) 的方法进行维护,当然,在读多写少的情况下,我们还可以采用 Copy-On-Write写时复制 的来控制共享变量,其中最经典的实现那就是JDK的 CopyOnWriteArrayList 了。
好了,说了这么多,下面正式进入正题。我们可以想这么一个问题,在多线程环境中如何对每一条线程的生命周期进行跟踪,跟具体地说,在 web 项目中,我们通常会对某一个请求从进入系统到退出系统的整个生命周期进行日志记录,又或者说是对一个事务的全过程进行跟踪记录,通常的做法是采用为每个线程建立一个叫 transactionId 的值,用于标识每个线程并进程跟踪,下图红圈即为 transactionId 的值。
那么你可能会说,我也可以不使用 transactionId 来跟踪啊,我采用线程名不就好了吗?道理是这样,但是当你想对改值进行自定义呢?比如获取客户端的ip地址、被调用的接口名呢???
好了,假设我们采用 transactionId 来跟踪线程,那么如果这个 transactionId 是个共享变量的话,那我们不就是得对其做相关线程同步的操作,线程那么多,那不烦死,笨死!嗯, TheadLocal 就是在这种情况下而生了,一个 TheadLocal 代表一个变量,你千万不要以为它代表一个线程,不然我会晕死!这个变量天生就是线程安全的。为什么呢?因为它的工作原理是会为每条线程做一份变量的拷贝,各个线程的变量不会相互影响,自然就不会有线程安全问题咯!
酱紫,我画张图吧!
TheadLocal 的底层实现原理是通过 ThreadLocalMap 实现,时间匆忙,其原理不在本文讲解范围内,读者自行学习源码!还有一点思想值得学习,多线程一般的同步机制采用了“以时间换空间”的方式,比如定义一个static变量,同步访问,而ThreadLocal则采用了“以空间换时间”的方式。
package com.wokao66.concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalTest {
//线程本地变量
private static ThreadLocal<Integer> sessionId = new ThreadLocal<>();
public static void main(String[] args) {
//线程1
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
//我对ThreadLocal的值进行修改,自定义
sessionId.set(1);
try {
Thread.sleep(1000);
//模拟一个线程的生命周期多次获取ThreadLocal变量的值,验证是否在当前线程有做一份拷贝
for (int i = 0; i < 5; i++) {
System.err.println("[" + Thread.currentThread().getName() + "]" + sessionId.get());
}
} catch (InterruptedException e) {}
}
});
//线程2
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
//我对ThreadLocal的值进行修改,自定义
sessionId.set(2);
try {
Thread.sleep(1000);
//模拟一个线程的生命周期多次获取ThreadLocal变量的值,验证是否在当前线程有做一份拷贝
for (int i = 0; i < 5; i++) {
System.err.println("[" + Thread.currentThread().getName() + "]" + sessionId.get());
}
} catch (InterruptedException e) {}
}
});
//线程1
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
//我对ThreadLocal的值进行修改,自定义
sessionId.set(3);
try {
Thread.sleep(1000);
//模拟一个线程的生命周期多次获取ThreadLocal变量的值,验证是否在当前线程有做一份拷贝
for (int i = 0; i < 5; i++) {
System.err.println("[" + Thread.currentThread().getName() + "]" + sessionId.get());
}
} catch (InterruptedException e) {}
}
});
ExecutorService service = Executors.newFixedThreadPool(30);
service.execute(thread1);
service.execute(thread2);
service.execute(thread3);
}
}
控制台输出
[pool-1-thread-1]1 [pool-1-thread-2]2 [pool-1-thread-1]1 [pool-1-thread-2]2 [pool-1-thread-1]1 [pool-1-thread-1]1 [pool-1-thread-2]2 [pool-1-thread-2]2 [pool-1-thread-2]2 [pool-1-thread-1]1 [pool-1-thread-3]3 [pool-1-thread-3]3 [pool-1-thread-3]3 [pool-1-thread-3]3 [pool-1-thread-3]3
可以看到每条线程对应的 ThreadLocal 是一样的,证明了确实是有做拷贝操作!
使用 ThreadLocal 首先一点那就是对变量的访问不需要同步控制,常用的场景如下:
MDC 机制 需要注意的是,既然 TreadLocal 是以采用空间换取时间的思想,所以可以想象 TreadLocal 并不适合来定义大对象,因为大对象的话每个线程都有拷贝,线程一多,性能必定受到牵连,甚至JVM抛出 ERROR