多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal
是除了加锁这种同步方式之外的另一种保证多线程访问变量时的线程安全的方法;如果每个线程对变量的访问都是基于线程自己的变量这样就不会存在线程不安全问题。
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized
来保证同一时刻只有一个线程对共享变量进行操作。这种情况下其实还可以将变量放到ThreadLocal
类型的对象中,使变量在每个线程中都有独立拷贝,在一个线程中对变量的任何操作都不会影响到其它线程的变量。在很多情况下,ThreadLocal
比直接使用synchronized
同步机制解决线程安全问题更简单,更方便,同时还能保证程序的性能。
Java中的ThreadLocal
是用哈希表实现的,每个线程里都有一个ThreadLocalMap
属性,里面就以Map
的形式存储了多个ThreadLocal
对象。当在线程中调用ThreadLocal
操作方法时,都会通过当前Thread
线程对象拿到线程里的ThreadLocalMap
,再通过ThreadLocal
对象从ThreadLocalMap
中锁定数据实体(ThreadLocalMap.Entry
)。
ThreadLocal
暴露了5个基本的操作和构造方法,主要的功能有:构造方法、设值方法、取值方法和资源回收;上面我们已经简单阐述了ThreadLocal
的实现原理,这里我们再通过解析其中的代码来详细说说它是怎么做到线程隔离的。
ThreadLocal
是一个泛型类,只提供了一个构造方法,通过泛型可以指定要存储的值的类型;这个构造方法通常可以单独使用,也可以配合protected T initialValue()
方法 从而在实例化对象时提供一个初始值,因为这是一个protected
方法,所以我们需要在实例化ThreadLocal
对象时覆盖该方法:
private static ThreadLocal threadLocal = new ThreadLocal() {
@Override
protected Integer initialValue() {
return 5; // 这里设置期望的初始值
}
};
当然这样未免太过繁琐了,代码也比较冗余!官方自然也考虑到了这一点,所以提供了一个静态的设置初始值的方法withInitial
:
public static ThreadLocal withInitial(Supplier extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
有Supplier
供给接口,这意味着我们可以使用如下方式设置初始值:
private static ThreadLocal threadLocal1 = ThreadLocal.withInitial(() -> 1L);
这样的形式明显要优雅很多,但SuppliedThreadLocal
又是个什么东西?其实它只是ThreadLocal
类中一个简单的静态内部类罢了:
static final class SuppliedThreadLocal extends ThreadLocal {
private final Supplier extends T> supplier;
SuppliedThreadLocal(Supplier extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override // 这里通过覆盖ThreadLocal的initialValue方法设置初始值
protected T initialValue() { return supplier.get(); }
}
要保存的数据通过set
方法设置,多次调用set
方法并不会保存多个数据,而是发生覆盖,一个ThreadLocal
正常只能保存一个数据:
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 拿到当前线程中的ThreadLocalMap
if (map != null) {
map.set(this, value); // 线程中存在ThreadLocalMap,设值
} else {
createMap(t, value); // 线程中不存在ThreadLocalMap,创建后再设值
}
}
在没有使用set
方法设值之前,调用get
方法获取的将是initialValue
方法设置的值(没有覆盖该方法返回就是null
),否则返回的就是set
方法设置的值。我们通过代码来解析其中的原理:
public T get() {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 拿到当前线程保存的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 这里传入的this就是当前的ThreadLocal对象,拿到ThreadLocal对应的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; // 拿到ThreadLocal的值
return result;
}
}
return setInitialValue(); // 调用setInitialValue方法返回初始值
}
代码中的getMap(t)
方法返回当前线程的ThreadLocalMap
类型的变量:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 返回线程中的ThreadLocalMap
}
而map.getEntry(this)
这里通过传入当前的ThreadLocal
对象(线程Thread
中有一个ThreadLocalMap
类型属性,存储了多个ThreadLocal
)拿到了ThreadLocalMap.Entry
,它是ThreadLocal
的静态内部类ThreadLocalMap
的静态内部类,代码如下:
static class ThreadLocalMap {
static class Entry extends WeakReference> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
代码很简单,其实ThreadLocal
就是通过这个类以弱引用的方式与value
绑定在了一起,通过ThreadLocal
对象就能获取到对应ThreadLocal
中存储的值。
在我们还没有使用set
方法为ThreadLocal
设置值的情况下,get
方法会返回setInitialValue
方法的值,可以看看这个方法的具体实现:
private T setInitialValue() {
T value = initialValue(); // 这里获取初始值,如果我们有重写了initialValue方法的话就会返回设置的初始值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value); // 存在当前Thread对应的ThreadLocalMap,直接设值
} else {
createMap(t, value); // 不存在当前Thread对应的ThreadLocalMap,为当前线程创建一个ThreadLocalMap并设值
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal>) this);
}
return value;
}
上面最主要的是createMap
这个方法,它的作用是给传递的线程创建一个对应的ThreadLocalMap
并把值存进去,可以看到新创建的ThreadLocalMap
被赋值给了线程中的threadLocals
变量,这也说明对应的数据都是存储在各个线程中的,所以每个线程对数据的操作自然不会影响其它线程的数据:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue); // this就是操作的ThreadLocal对象,firstValue就是要保存的值
}
当我们不再需要保存的数据时,应该通过remove
方法将当前线程中保存的值移除掉使对象得到GC
(调用remove
方法将把ThreadLocal
对象从当前线程的ThreadLocalMap
移除):
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); // 拿到当前线程中的ThreadLocalMap
if (m != null) {
m.remove(this); // 从ThreadLocalMap移除key为当前ThreadLocal对象的记录
}
}
调用remove
方法会清空使用set
方法设置的值,此时如果再次调用了get
方法,由于ThreadLocal
对应的记录已经不存在,所以将会执行return setInitialValue();
这段代码,这里将会调用initialValue
方法从而返回初始值。
最后我们再结合一个小栗子来解释ThreadLocal
在多线程下的表现:
public class ThreadLocalDemo {
private static ThreadLocal threadLocal = new ThreadLocal() {
// 复写initialValue方法为ThreadLocal设置一个初始值,并获取调用了threadLocal的线程id
@Override
protected Integer initialValue() {
System.out.println("当前的线程id:" + Thread.currentThread().getId());
return 10;
}
};
public static void main(String[] args) {
// main方法就对应一个线程了,我们在主线程中对threadLocal的值进行修改
System.out.println("~~~~~~~~~~~~主线程~~~~~~~~~~~~~");
System.out.println("在主线程中获取threadLocal的值:" + threadLocal.get());
threadLocal.set(100); // 改变threadLocal的值
System.out.println("在主线程中再次获取threadLocal的值:" + threadLocal.get());
System.out.println("~~~~~~~~~~~~新线程~~~~~~~~~~~~~");
// 新创一个线程,并获取threadLocal的值
new Thread(() -> System.out.println("在新的线程中获取threadLocal的值:" + threadLocal.get())).start();
}
}
上面我们有一个静态的threadLocal
变量,通过在new
的时候覆盖initialValue
方法(延迟加载,不会立即调用)为它设置了一个初始值10,并顺便在方法中输出使用threadLocal
变量的线程的id,接着在获取了threadLocal
的初始值后重新设置了一个数值100;在改变了threadLocal
值的那个线程中确实看到了改变后的结果,然而在新线程中却有了“意料之外”的结果:
之所以会有这样的结果其实因为ThreadLocal
是线程隔离的,我们看到的是在操作同一个变量,但是Java会为每一个线程都创建一个threadLocal
的副本变量,每个线程操作的其实都是属于它的那个副本变量,而不是公共的那个threadLocal
;每个线程对threadLocal
的任何操作都不会影响到其它线程的threadLocal
!
上面我们通过覆盖initialValue
方法为threadLocal
设置了一个默认值,如果不设置初始值,那么获取到的值就是null
,这是ThreadLocal
的初始化方法决定的;
其实ThreadLocal
并没有那么的神秘莫测,但它在多线程编程中的地位却是毋庸置疑的,用好了ThreadLocal
能够帮助你写出优雅简洁的多线程代码。在使用synchronized
同步代码的时候,如果没法保证同步代码的(细)粒度,则会使得程序的性能下降,而使用ThreadLocal
时完全不用考虑性能问题,因为没有了多线程的竞争,也就不用额外的同步判断等开销。总而言之,当遇到多线程操作同一个共享变量需要保证线程安全的时候,你应该首先考虑使用ThreadLocal
而不是synchronized
!