前言
本篇文章希望能够抛砖引玉,如有不对之处,还希望你能留下宝贵的意见,供我继续学习。不必吝啬批评指正,感激不尽~
ThreadLocal是什么
声明:本篇文章采用的是JDK1.8
从官方源码给出的解释来看:
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
结合我的理解:这个类提供的是一个线程(thread)能够独立使用的局部变量,这个变量不能被其他线程访问,而该线程也只能够通过get
或者set
方法访问。简单总结就是线程层面的数据隔离。
什么时候会使用到ThreadLocal
数据库连接
当我们在一个Web应用中,每个HTTP请求通常都在自己的线程中处理。如果在处理请求的过程中需要访问数据库,那么每个请求都需要自己的数据库连接。如果我们不使用ThreadLocal
,而是选择在一个公共的地方存储数据库连接,那么可能会出现多个线程使用同一个数据库连接的情况,这可能会导致数据的混乱和错误。
使用ThreadLocal
可以很好地解决这个问题。我们可以在每个线程中创建一个新的数据库连接,并将这个连接存储在ThreadLocal
中。这样,每个线程都可以通过ThreadLocal
获取到自己的数据库连接,而不会影响到其他线程。
下面这段代码摘自:
https://www.cnblogs.com/dolphin0520/p/3920407.html
private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
浏览器Cookie和Session管理
- 每当我访问一个页面的时候,浏览器都会帮我们从硬盘中找到对应的Cookie发送过去。
- 浏览器是十分聪明的,不会发送别的网站的Cookie过去,只带当前网站发布过来的Cookie过去
在Web应用中,ThreadLocal
常常被用于处理HTTP请求中的用户会话和Cookie信息。每个HTTP请求通常都在自己的线程中处理,因此,每个请求都需要自己的会话和Cookie
信息。如果我们不使用ThreadLocal
,而是选择在一个公共的地方存储会话和Cookie
信息,那么可能会出现多个线程使用同一个会话或Cookie
的情况,这可能会导致数据的混乱和错误。
下面这段代码摘自:
https://www.cnblogs.com/dolphin0520/p/3920407.html
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
使用示例
public class ThreadLocalDemo {
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.showTowThread();
}
//这里Runnable是接口,如果想创建一个Thread建议使用这种方式,避免多继承造成的问题
//回顾知识点下面的匿名类是如何使用的?
public void showTowThread() {
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("Thread1");
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("Thread2");
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
}
}).start();
}
}
输出结果:
Thread-0 Thread1
Thread-1 Thread2
ThreadLocal实现原理
声明:本文采用的是JDK1.8
1. set方法
刚才我们在示例是不是有个threadLocal.set("Thread1")
,看看源代码
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map不为空,直接设置值
if (map != null) {
map.set(this, value);
} else {
//如果为空,创建一个新的map并设值
createMap(t, value);
}
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这里引入了一个新的对象ThreadLocalMap
,先看源码的给出的注释
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//..省略代码
通过上面我们可以发现的是ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储。
我们的值都是存储到这个Map上的,key是当前ThreadLocal对象。
我们又在Thread
的局部变量里找到ThreadLocalMap
的引用:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
那么我们是不是就可以这样一个结论:Thread为每个线程维护了一个ThreadLocalMap这么一个Map,这个Map的key是当前线程的ThreadLocal对象本身,value就是你需要设置的这个值。
2.get方法
public T get() {
//跟set()同样的操作
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果map不为空,直接返回值
if (map != null) {
//Entry可以理解为那个map,源码里可以详细看到
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings(“unchecked”)
T result = (T)e.value;
return result;
}
}
//如果map为空,创建一个新的map并设值
return setInitialValue();
}
这里可以看到set
和get
都进行的操作就是获取当前线程,那么这不就保证了当前线程的ThreadLocalMap
只能由当前线程操作,可不就是线程级的数据隔离吗。
避免内存泄漏
ThreadLocal的对象关系引用图:
public class ThreadLocalForOOM {
/**
* -Xms50m -Xmx50m
*/
static class OOMObject {
private Long[] a = new Long[2 * 1024 * 1024];
}
final static ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
final static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
pool.execute(() -> {
threadLocal.set(new OOMObject());
System.out.println("oom object--->" + finalI);
OOMObject oomObject = threadLocal.get();
System.out.println("oomObject---->" + oomObject);
// threadLocal.remove(); // 记得remove 防止内存泄露,此时一定要在使用完remove
});
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里报错:
java.lang.OutOfMemoryError: Java heap space
Exception in thread "pool-1-thread-20" java.lang.OutOfMemoryError: Java heap space
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
想要避免内存泄露就要手动remove()掉!
注意:
- 首先,
ThreadLocal
不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set()
到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。 - 另外,说
ThreadLocal
使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()
来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()
将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()
时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal
实例是作为map的key来使用的。
总结
本文只是对ThreadLocal
浅显的探讨,也许你还可以找到更有趣的比如:table扩容
、清除方式
、解决hash冲突
。如果本文有讲述不对的地方,也希望你能留下宝贵的指正,感激不尽~
参考博文:
https://juejin.cn/post/6844903586984361992
https://www.cnblogs.com/zhangjk1993/p/6641745.html#_label2
https://www.iteye.com/topic/103804