ThreadLocal 类
通常情况下,我们创建的非局部变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程则无法访问和修改。
ThreadLocal 类允许我们创建一个共享变量(这个变量可以被多个线程访问),但是这个共享变量的值包含多个副本,每个副本对应一个线程(且这个副本只能被同一个线程读写的)。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的 ThreadLocal 变量的值。
ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本(实际是不同的实例)。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题
Demo
public class ThreadLocalTest1 {
public static class MyRunnable implements Runnable {
private ThreadLocal<Integer> v = new ThreadLocal<Integer>();
@Override
public void run() {
threadLocal.set((int) (Math.random() * 100D));
try{
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(threadLocal.get());
System.out.println(threadLocal.hashCode());
System.out.println("-------");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), "A");
Thread t2 = new Thread(new MyRunnable(), "B");
t1.start();
t2.start();
}
}
运行结果
26
907247395
-------
67
907247395
-------
分析
即:线程A与线程B在 ThreadLocal 共享实例中保存的整型变量是各自独立的,互不相干,只要在每个线程内部使用 set 方法赋值,然后在线程内部使用 get 方法就能取到对应的值。
由于在不同线程中 ThreadLocal 实例的 hashCode 是相同的,说明不同线程访问的均是同一个 ThreadLocal 实例。
ThreadLocal 原理
既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案是 ThreadLocal 维护一个 Map,键是 Thread,值是它在该 Thread 内的实例。线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。该方案如下图所示:
该方案可满足上文提到的每个线程内一个独立备份的要求。每个新线程访问该 ThreadLocal 时,需要向 Map 中添加一个映射,而每个线程结束时,应该清除该映射。这里就有两个问题:
- 增加线程与减少线程均需要写 Map,故需保证该 Map 线程安全。虽然从ConcurrentHashMap的演进看Java多线程核心技术一文介绍了几种实现线程安全 Map 的方式,但它或多或少都需要锁来保证线程的安全性。
- 线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。
如何创建ThreadLocal变量
以下代码展示了如何创建一个 ThreadLocal 变量:
private ThreadLocal myThreadLocal = new ThreadLocal();
我们可以看到,通过这段代码实例化了一个 ThreadLocal 对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个 ThreadLocal 实例,但是每个线程却只能访问到自己通过调用 ThreadLocal 的 set() 方法设置的值。即使是两个不同的线程在同一个 ThreadLocal 对象上设置了不同的值,他们仍然无法访问到对方的值。
如何访问ThreadLocal变量
一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值:
myThreadLocal.set("A thread local value”);
可以通过下面方法读取保存在ThreadLocal变量中的值:
String threadLocalValue = (String) myThreadLocal.get();
get() 方法返回一个 Object 对象,set() 对象需要传入一个 Object 类型的参数。
为ThreadLocal指定泛型类型
我们可以创建一个指定泛型类型的 ThreadLocal 对象,这样我们就不需要每次对使用 get() 方法返回的值作强制类型转换了。下面展示了指定泛型类型的 ThreadLocal 例子:
private ThreadLocal myThreadLocal = new ThreadLocal<String>();
现在我们只能往 ThreadLocal 对象中存入 String 类型的值了。并且我们从 ThreadLocal 中获取值的时候也不需要强制类型转换了。
如何初始化ThreadLocal变量的值
由于在 ThreadLocal 对象中设置的值只能被设置这个值的线程访问到,线程无法在 ThreadLocal 对象上使用 set() 方法保存一个初始值,并且这个初始值能被所有线程访问到。
但是,我们可以通过创建一个 ThreadLocal 的子类并且重写 initialValue() 方法,来为一个 ThreadLocal 对象指定一个初始值。就像下面代码展示的那样:
private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "This is the initial value";
}
};
一个完整的ThreadLocal例子
下面是一个完整的可执行的 ThreadLocal 例子:
public class ThreadLocalExample {
public static class MyRunnable implements Runnable {
private ThreadLocal threadLocal = new ThreadLocal();
@Override
public void run() {
threadLocal.set((int) (Math.random() * 100D));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(threadLocal.get());
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
上面的例子创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
Reference
- Java ThreadLocal的使用 - http://ifeve.com/java-threadlocal%E7%9A%84%E4%BD%BF%E7%94%A8/
- java并发编程学习: ThreadLocal使用及原理 - https://www.cnblogs.com/yjmyzz/p/threadlocal-demo.html
- Java进阶(七)正确理解Thread Local的原理与适用场景 - http://www.jasongj.com/java/threadlocal/