ThreadLocal的原理与使用
一、ThreadLocal是什么
ThreadLocal,它不是一个线程,而是线程的一个本地化对象。ThreadLocal是为了解决多个线程同时访问一个变量时的并发问题,在多线程环境下,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。这个变量里面的值是和其他线程分割开来的,变量的值只有当前线程能访问到。保证了多线程环境下当前线程中变量的安全性。
二、实现原理
ThreadLocal为每一个线程维护一个变量副本,在ThreadLocal类中有一个静态内部类ThreadLocalMap,用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。
1.ThreadLocal的接口方法
ThreadLocal类提供的四个接口方法:
(1) void set(Object value)用来设置当前线程中变量的副本。
(2) public Object get()用来获取ThreadLocal在当前线程中保存的变量副本。
(3) public void remove()用来删除当前线程中变量的副本,该方法是JDK 5.0新增的方法,目的是为了减少内存的占用,防止内存泄漏。虽然当线程结束后,线程的变量副本会被自动垃圾回收,但是在现成结束前会有造成内存泄漏的情况,可以调用该方法来避免,后面会详细介绍。
(4) protected Object initialValue()返回该线程局部变量的初始值,该方法一般在子类中重写用来初始化变量副本。如果不在子类中重写,这个方法会延迟调用,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,在ThreadLocal中的实现直接返回null。
下面来先看一下get()方法的源码:
/**
* 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); //获取当前线程对应的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue(); //若当前线程还未创建ThreadLocalMap,
// 则调用此方法创建并返回初始值。
}
接着看getMap()方法中做了什么:
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回的是当前线程t中的一个成员变量threadLocals
}
接着去Thread类中看一下threadLocals是什么东西:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
其实就是一个ThreadLocal的内部类ThreadLocalMap对象,接着看一下ThreadLocalMap是如何实现的:
static class ThreadLocalMap {
//map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
/**
* 初始化容量为16
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存储线程的中所有ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
*/
private Entry[] table;
接下来看一下是如何创建ThreadLocalMap的,也就是get()方法中最后一句调用的setInitialValue:
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue(); //是第四个接口方法,返回初始化的值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value); //创建ThreadLocalMap
return value;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
至此,可以看到为每个线程保存变量副本的方法也就是为每个线程new一个ThreadLocalMap对象threadLocals,以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。然后使用接口方法分别对副本变量进行操作。
三、使用场景
1.数据库连接管理
同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。
以下代码摘自:https://blog.csdn.net/sean417/article/details/69948561
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
Connection conn = null;
try {
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test", "username",
"password");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
}
2.ThreadLocal实例
public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
protected Long initialValue() {
return Thread.currentThread().getId();
};
};
ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;
protected String initialValue() {
return Thread.currentThread().getName();
};
};
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
四、内存泄露问题
1.threadlocal实例为null
在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, CurrentThread, Map, value将全部被GC回收。 所以只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。
2.线程对象未回收
线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候, 线程池中对线程管理都是采用线程复用的方法。在线程池中线程非常难结束甚至于永远不会结束。就可能出现内存泄露。五、与同步机制的比较
相同点:
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题
不同点:
同步机制中通过对象的锁机制保证同一时间只有一个线程来访问变量,这是该变量是多个线程共享的,使用同步机制要缜密的分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放掉锁等复杂的问题,程序设计和编写难度大。
ThreadLocal则为每一个线程提供了一个独立的变量副本,从而隔离了多个线程对访问数据的冲突。因为每一个线程都有自己的变量副本,从而也就没有必要进行同步了。ThreadLocal提供了线程安全的对象的封装,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
总的来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间的”方式——访问串行化,对象共享化。而ThreadLocal则采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal和线程同步机制是多线程访问的两种不同的方式,分别在不同的应用场景中发挥作用。
参考文档:
https://blog.csdn.net/lhqj1992/article/details/52451136
https://blog.csdn.net/see__you__again/article/details/51244946