理解多线程中的ThreadLocal?
1.ThreadLocal是什么以及作用?
概述:通过Thread的源码可以得知,ThreadLocal是Thread的一个局部变量,用来存储每个线程的变量副本,其中真正存储数据的是ThreadLocal中的一个静态内部类ThreadLocalMap中的一个Entry类型的数组,之所以会用到数组来存储,是由于我们每个线程可能需要存储多个不同类型的ThreadLocal变量副本。
作用:很好地解决了线程安全的问题,在多个线程访问同一变量时,通过线程隔离的方式,为每个线程创建一个属于自己的变量副本,这样线程之间操作的只是属于自己的那个副本,所以自然不会造成数据共享安全的问题。
2.源码分析?
变量副本的存储方式:如上图所示,ThreadLocal1表示存储的是int类型的变量,ThreadLocal2表示存储的是String类型的变量,左右两边则表示线程1和线程2,实际上,在我们通过ThreadLocal的set方法设置值的时候,值是以Entry条目的形式存储在ThreadLocal中的静态内部类ThreadLocalMap中,考虑到多种类型的变量是,以Entry数组的形式存储,Entry的key则是ThreadLocal类型的变量引用【注意,这里我们理解的线程的独有的变量副本其实是Thread的一个ThreadLocalMap类型的成员变量,而不是ThreadLocal】,Entry的value则为我们设置进去的Object类型的变量值。在对重复值的情况处理,在set方法中会调用一个nextHashCode()方法去解决重复值的问题。
3.相关api及其源码解析?
ThreadLocal类中提供了几个方法:
1.public T get() { }
2.public void set(T value) { }
3.public void remove() { }
4.protected T initialValue(){ }
3.1 get()方法是用来获取ThreadLocal在当前线程中保存的变量副本;在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。
在Thread类中定义:ThreadLocal.ThreadLocalMap threadLocals = null; public T get() { //(1)获取当前线程 Thread t = Thread.currentThread(); //(2)获取当前线程的threadLocals变量 ThreadLocalMap map = getMap(t); //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值 if (map != null) { //根据key获取对应的条目Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量 return setInitialValue(); } private T setInitialValue() { //protected T initialValue() {return null;} T value = initialValue(); //获取当前线程 Thread t = Thread.currentThread(); //以当前线程作为key值,去查找对应的线程变量,找到对应的map ThreadLocalMap map = getMap(t); //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值 if (map != null) map.set(this, value); //如果map为null,说明首次添加,需要首先创建出对应的map else createMap(t, value); return value; } private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
3.2 set()用来设置当前线程中变量的副本;如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals。
public void set(T value) { //(1)获取当前线程(调用者线程) Thread t = Thread.currentThread(); //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map ThreadLocalMap map = getMap(t); //(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值 if (map != null) map.set(this, value); //(4)如果map为null,说明首次添加,需要首先创建出对应的map else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
3.3 remove()用来移除当前线程中变量的副本,remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量;
1 public void remove() { 2 //获取当前线程绑定的threadLocals 3 ThreadLocalMap m = getMap(Thread.currentThread()); 4 //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量 5 if (m != null) 6 m.remove(this); 7 }
4.内存泄漏问题?
4.1 分析:由ThreadLocalMap源码可以得知ThreadLocal是一个弱引用,实质就是只要只要发生GC的操作,jvm就会回收掉该ThreadLocal对象。如果所示,key为ThreadLocal的一个实例对象,在发生GC时,由于其是弱引用,所以此时的key为null,自然,我们在ThreadLocalMap中无法通过key为null去获取对应的value;Entry 的 value 就会一直存在一条强 引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永 远不会被访问到了,所以存在着内存泄露。
4.2 解决:一般来说,只有当线程结束时,Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value这条强引用链才会断开,current thread,ThreadLocalMap,value才会被GC回收,所以我们一般在不使用ThreadLocal变量之后都会手动调用remove()去强制清除Entry中的value值,避免长期占有内存,造成内存泄漏。
4.3 ThreadLocal分别使用强弱引用的情况:
key使用强引用:引用 ThreadLocal 的对象被回收了,但是 ThreadLocalMap还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 的对象实例不会 被回收,导致 Entry 内存泄漏。
key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的对象实例也会被 回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都有机会被回收。
比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可 以多一层保障。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引 用。
总结
JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
JVM 利用调用 remove、get、set 方法的时候,回收value。
当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、
get、set 方法,那么将导致内存泄漏。
使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的
重复运行的,从而也就造成了 value 可能造成累积的情况。
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; } }
5.线程不安全的场景?
我们知道每个线程独有的ThreadLocal,在ThreadLocalMap中保存的是ThreadLocal对象的引用,之所以不同,是因为线程保存的是不同的ThreadLocal对象引用,但是如果引用是同一个的话,则会造成线程无法隔离,会造成数据共享的问题。如下代码,如果Number对象加上static,则Number对象只会被实例化一次,也就是说不管多少个线程访问value这个threadlocal常量,都是用的同一个泛型为Number对象的ThreadLocal引用,所以该变量不能当成副本使用,此时会导致线程不安全。
解决办法:1.将如下的Number对象不用static修饰,这样每个线程每次访问的Number对象都是新创建的
2.在ThreadLocal初始化的时候,重写父类的initialValue()方法,给其赋一个初始值。
public static Number number = new Number(0); public static ThreadLocal<String> value = new ThreadLocal<String>(){ @Override protected String initialValue() { return "sunjiahao"; } };
6.使用场景?
6.1 ThreadLocal的应用场景# 数据库连接
public Connection initialValue() { return DriverManager.getConnection(DB_URL); } }; public static Connection getConnection() { return connectionHolder.get(); }
6.2 ThreadLocal的应用场景# Session管理
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; }
6.3 ThreadLocal的应用场景# 多线程
* 描述 Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。 * 因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码, * 它们也无法访问到对方的ThreadLocal变量。 */ public class ThreadLocalExsample { /** * 创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法, * 并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了, * 则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象, * 因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。 */ public static class MyRunnable implements Runnable { /** * 例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。 * 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的 * set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值, * 他们仍然无法访问到对方的值。 */ private ThreadLocal threadLocal = new ThreadLocal(); @Override public void run() { //一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值 threadLocal.set((int) (Math.random() * 100D)); try { Thread.sleep(2000); } catch (InterruptedException e) { } //可以通过下面方法读取保存在ThreadLocal变量中的值 System.out.println("-------threadLocal value-------"+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(); } } 运行结果 -------threadLocal value-------38 -------threadLocal value-------88
总结
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
1.在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null
- 【多线程】彻底理解ThreadLocal
- 通通透透理解ThreadLocal,实现安全的多线程
- 多线程中ThreadLocal的理解
- 【JAVA秒会技术之多线程】彻底理解ThreadLocal
- 多线程之ThreadLocal理解、应用及源码分析
- JavaScript可否多线程? 深入理解JavaScript定时机制
- 基本多线程的理解
- 线程是什么?线程和进程的区别,怎么最好的理解使用和快熟掌握多线程?多线程的各种坑以及多线程锁的简介和使用
- 理解ThreadLocal(转)
- 快速理解Linux多线程及Epoll
- 深入理解Qt多线程
- 我是一个线程(对理解多线程很有帮助)-转载
- Java多线程之详解ThreadLocal类(一)
- ThreadLocal深入理解
- threadLocal的理解
- 深入理解Java多线程中的wait(),notify()和sleep()
- 【Python3 笔记】Python3 进程和线程 多进程 多线程 ThreadLocal 异步I/O 分布式进程
- ThreadLocal源码理解
- 笔记-ThreadLocal简单理解
- 理解ThreadLocal