ThreadLocal作用、场景、原理

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的应用场景# 数据库连接

 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。

    原文作者:安仔夏天勤奋
    原文地址: https://www.jianshu.com/p/6fc3bba12f38
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞