ThreadLocal作用场景原理

Posted 馥钰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal作用场景原理相关的知识,希望对你有一定的参考价值。

黑马程序员Java基础教程由浅入深全面解析threadlocal_哔哩哔哩_bilibili

ThreadLocal数据结构

内存泄露 - threadLocalMap key为强引用 

内存泄露 - threadLocalMap key为弱引用 

 

手动remove entry 

 ThreadLocal作用、场景、原理 - 简书1.ThreadLocal 是什么? 在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的...https://www.jianshu.com/p/6fc3bba12f38

1.ThreadLocal 是什么?

在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

 * 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)

1.1.ThreadLocal 的作用?

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

1.1.2.ThreadLocal的应用场景?

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。在下面会例举几个场景。

2.android源码中也可以看到ThreadLocal的身影

这里以andorid 源码的Handler为例子。看看Handker是怎么用ThreadLocal的。Handler就必须获取当前线程的 Looper 对象,而每一个线程的 Looper 是不一致的。因为每一个线程都会有一个 Looper 对象,因此使用 ThradLocal 去保存和获取当前线程的 Looper 就可以达到这个的效果。

2.1. Looper 内部的关于在 ThreadLocal 中存储 Looper 和 获取 Looper 的源码。

//Looper.prepare();
​
private static void prepare(boolean quitAllowed) 
 if (sThreadLocal.get() != null) 
 throw new RuntimeException("Only one Looper may be created per thread");
 
 //将创建的 Looper 对象保存到 sThreadLocal 中。
 sThreadLocal.set(new Looper(quitAllowed));

​
​
//从 ThreadLocal 取出 Looper 对象
public static @Nullable Looper myLooper() 
 return sThreadLocal.get();

3.ThreadLocal的内部原理

我们从源码中了解ThreadLocal的原理,下面来看一下具体ThreadLocal是如何实现的。

ThreadLocal类中提供了几个方法:

1.public T get()

2.public void set(T value)

3.public void remove()

4.protected T initialValue()

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。

3.1.先看下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);
 if (map != null) 
 ThreadLocalMap.Entry e = map.getEntry(this);
 if (e != null) 
 @SuppressWarnings("unchecked")
 T result = (T)e.value;
 return result;
 
 
 return 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);
 return value;

第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。 如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法返回value。

看看getMap(t)做了些什么

 * 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;
 

在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。 那么我们继续取Thread类中取看一下成员变量threadLocals是什么?继续查看源码

 * 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类的一个内部类,我们继续取看ThreadLocalMap的实现。

再看setInitialValue()方法

setInitialValue()很容易理解,就是如果map不为空,就设置键值对,为空,再创建Map,看一下createMap的实现。

 * 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
 */
 void createMap(Thread t, T firstValue) 
 t.threadLocals = new ThreadLocalMap(this, firstValue);
 

如果使用ThreadLocal时,先进行get之前,必须先set,否则会报空指针异常

public class ThreadLocalExsample 

    ThreadLocal<Long> longLocal = new ThreadLocal<>();
    public void set() 
        longLocal.set(Thread.currentThread().getId());
    
    public long getLong() 
        return longLocal.get();
    
 public static void main(String[] args) 
        ThreadLocalExsample test = new ThreadLocalExsample();
        //注意:没有set之前,直接get,报null异常了
        System.out.println("-------threadLocal value-------" + test.getLong());
    

ThreadLocal的应用场景# 数据库连接

 public Connection initialValue() 
 return DriverManager.getConnection(DB_URL);
 
;  

public static Connection getConnection()   
 return connectionHolder.get();
  

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;

ThreadLocal的应用场景# 多线程

 * @Author 安仔夏天很勤奋
 * Create Date is  2019/3/21
 *
 * 描述 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

得出结论

ThreadLocal 中 set 和 get 操作的都是对应线程的 table数组,因此在不同的线程中访问同一个 ThreadLocal 对象的 set 和 get 进行存取数据是不会相互干扰的。

总结

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

  1. 实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

  2. 为何threadLocals的类型ThreadLocalMap的键为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;

  3. 在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

ThreadLocal的使用和实现原理

ThreadLocal是什么?

ThreadLocal提供线程本地变量,每个线程拥有本地变量的副本,各个线程之间的变量互不干扰。ThreadLocal实现在多线程环境下去保证变量的安全。以下来源于ThreadLocal类的注释。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.

ThreadLocal的使用

JDK中的例子,定一个一个私有的,静态的ThreadLocal变量,并且重写了initialValue方法,每个线程都会调用这个方法,获得初始化值。

JDK中的例子

如果需要我们可以重写initialValue方法,实现自己的业务逻辑。

使用其实很简单,保存值就调用set方法,获得值调用get方法,最后调用remove方法删除数据,防止内存泄漏和数据混乱。

ThreadLocal的数据结构

Thread类中有个变量threadLocals,这个类型为ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,就是一个普通的Java类,但是实现的类似map的功能。

ThreadLocal数据结构

每个线程都要自己的一个map,map是一个数组的数据结构存储数据,每个元素是一个Entry,entry的key是threadlocal的引用,也就是当前变量的副本,value就是set的值。

ThreadLocal的源码分析

1、Thread类中有个变量threadLocals,类型为ThreadLocal.ThreadLocalMap,这个就是保存每个线程的私有数据。

threadLocals

2、ThreadLocalMap是ThreadLocal的内部类,每个数据用Entry保存,其中的Entry继承与WeakReference,用一个键值对存储,键为ThreadLocal的引用。为什么是WeakReference呢?如果是强引用,即使把ThreadLocal设置为null,GC也不会回收,因为ThreadLocalMap对它有强引用。

Entry的定义

3、ThreadLocal中的set方法的实现逻辑,先获取当前线程,取出当前线程的ThreadLocalMap,如果不存在就会创建一个ThreadLocalMap,如果存在就会把当前的threadlocal的引用作为键,传入的参数作为值存入map中。

set方法

4、ThreadLocal中get方法的实现逻辑,获取当前线程,取出当前线程的ThreadLocalMap,用当前的threadlocak作为key在ThreadLocalMap查找,如果存在不为空的Entry,就返回Entry中的value,否则就会执行初始化并返回默认的值。

get方法

5、ThreadLocal中remove方法的实现逻辑,还是先获取当前线程的ThreadLocalMap变量,如果存在就调用ThreadLocalMap的remove方法。ThreadLocalMap的存储就是数组的实行,因此需要确定元素的位置,找到Entry,把entry的键值对都设为null,最后也Entry也设置为null。其实这其中会有哈希冲突,具体见下文。

remove方法

解决哈希冲突

ThreadLocal中的hash code非常简单,就是调用AtomicInteger的getAndAdd方法,参数是个固定值0x61c88647。

上面说过ThreadLocalMap的结构非常简单只用一个数组存储,并没有链表结构,当出现Hash冲突时采用线性查找的方式,所谓线性查找,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。如果产生多次hash冲突,处理起来就没有HashMap的效率高,为了避免哈希冲突,使用尽量少的threadlocal变量

最后

使用场景:一些ORM框架的Session管理,web系统的会话管理等

简单总结:一个ThreadLocal只能保存一个变量的副本,如果需要多个,就得创建多个变量;我们确定使用完需要执行remove避免内存泄漏。

以上是关于ThreadLocal作用场景原理的主要内容,如果未能解决你的问题,请参考以下文章

『Java面经』ThreadLocal 实现原理是什么 & 有哪些引用类型及使用场景?

ThreadLocal的使用及原理分析

ThreadLocal 你真的会用吗?

ThreadLocal 你真的会用吗?

分析Threadlocal内部实现原理,并解决Threadlocal的ThreadLocalMap的hash冲突与内存泄露

深入浅出多线程编程实战ThreadLocal详解(介绍使用原理应用场景)