在Java多线程并发环境下,如果我们需要对某一个变量进行操作的话,很有可能将造成线程安全问题,为了解决这种线程安全问题,我们可以给操作这个变量的方法或代码块加各种锁,虽然可以实现线程安全,但这样做会使系统性能受一定的影响,又比如在某个业务场景下,需要多个线程来同时操作每个用户或每笔订单的信息,为了保证线程安全,需要将这些变量都作为每个线程独有的变量,这种情况下,我们可以考虑使用Java中提供的ThreadLocal类来实现,它可以实现每个线程都有属于自己的本地变量副本,从而实现变量的线程安全及其他业务需求。
在了解ThreadLocal的原理前,先贴一个例子来验证ThreadLocal是否实现了每个线程都拥有自己的变量副本
public class ThreadLocalMain {
private static ThreadLocal local = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
local.set("马超");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",local -> "+local.get());
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
local.set("曹操");
System.out.println(Thread.currentThread().getName() + ",local -> "+local.get());
}).start();
}
}
根据上面的运行结果,两个线程操作同一个ThreadLocal中的值,确实没有出现覆盖的情况。
那么它是如何实现的呢?
下面通过ThreadLocal的源码来了解它的实现原理。
这里我们主要了解它的两个核心方法
set(T value);
get();
先从set方法开始
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
从方法上方的注释及代码来看,可以知道这个方法可以将一个传入的值set到当前线程的本地副本中。
在方法第一行代码中,获取到当前的线程t,再通过getMap方法传入当前线程,来获取这个线程中的threadLocals变量,这个变量是ThreadLocal中的一个内部类ThreadLocalMap,用来保存变量副本。接着判断这个获取到的这个map是否为空(有没有被实例化,因为这个类是延迟构造的),如果不为空则调用ThreadLocalMap的set方法传入当前的引用this及value来设置其中的值,否则调用createMap方法来为当前线程实例化一个ThreadLocalMap,ThreadLocalMap类的内部维护一个Entry[] table数组变量,用来保存本地线程变量的副本,它的结构类似于一个map集合,键为当前操作的这个ThreadLocal(也就是传入的this),值为set方法传入的value。
我们可以点进来看一下这个createMap方法是如何实例化一个ThreadLocalMap的。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
通过这个ThreadLocalMap的构造函数,可以看到传入的ThreadLocal作为key,和传入的value通过hash计算来找到table中的某个位置进行存放。
看完set方法的原理后,了解了ThreadLocal是如何存储每个线程的独立变量副本了。
最后再来看一下get方法,和set方法同理,先贴一下源码
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
既然set方法是将值保存在Thread中的ThreadLocalMap,那么get方法也就是通过当前的ThreadLocal作为key,从ThreadLocalMap中的table变量表中取出对应的变量。在代码的最后一行中,setInitialValue()方法用于当你将ThreadLocal作为key取不到数据时(ThreadLocalMap为空,也就是没有调用set方法,直接调用get),会将一个null值保存到table变量表中,此时并不会报空指针异常,而是拿到一个null值。
另外ThreadLocalMap在使用的时候也会造成一些新的问题,例如如果我们的线程一般都是交给线程池来管理的,而线程池的核心就是重复使用这些已经创建好的线程来执行任务,所以当我们的一个线程执行完毕后,后面进来的任务执行时如果分配到这个线程,在未调用set方法情况下就很有可能会get到这个线程中ThreadLocalMap保存的变量,造成读到的数据为脏数据。
除了这个问题外,还可能有引起内存泄漏的问题,原因在于在ThreadLocalMap中保存的变量key使用了指向ThreadLocal的弱引用(WeakReference),当ThreadLocal没有任何强引用关系后,这个在ThreadLocalMap中引用这个ThreadLocal的key也会被回收,变成null,而它对应的value值是不会被回收的,那么将造成内存泄漏问题,除非在当前该线程执行完毕出栈后,也就不存在这个value无法被回收的情况,但是如果我们使用线程池,线程即使执行完任务也不会被销毁,而是在线程池中保持活跃,等待新的任务,这种情况下,将可能引起内存泄漏问题。
为了解决以上出现的问题,我们在使用完这个变量后,最好调用一下remove方法将其删除。
最后,如果你觉得写的还可以的话,请在右上角(app左下角)点个赞吧~