欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于ThreadLocal 和 Spring单例如何保证线程安全 ThreadLocal 

程序员文章站 2022-05-14 18:00:26
...

转自 : 1、http://www.iteye.com/topic/103804

         2、解密ThreadLocal

            3、 http://blog.csdn.net/partner4java/article/details/7017398

 

相信读者在网上也看了很多关于ThreadLocal的资料,很多博客都这样说:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路;ThreadLocal的目的是为了解决多线程访问资源时的共享问题。如果你也这样认为的,那现在给你10秒钟,清空之前对ThreadLocal的错误的认知!

看看JDK中的源码是怎么写的:

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

翻译过来大概是这样的(英文不好,如有更好的翻译,请留言说明):

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。

可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
举个例子,我出门需要先坐公交再做地铁,这里的坐公交和坐地铁就好比是同一个线程内的两个函数,我就是一个线程,我要完成这两个函数都需要同一个东西:公交卡(北京公交和地铁都使用公交卡),那么我为了不向这两个函数都传递公交卡这个变量(相当于不是一直带着公交卡上路),我可以这么做:将公交卡事先交给一个机构,当我需要刷卡的时候再向这个机构要公交卡(当然每次拿的都是同一张公交卡)。这样就能达到只要是我(同一个线程)需要公交卡,何时何地都能向这个机构要的目的。

有人要说了:你可以将公交卡设置为全局变量啊,这样不是也能何时何地都能取公交卡吗?但是如果有很多个人(很多个线程)呢?大家可不能都使用同一张公交卡吧(我们假设公交卡是实名认证的),这样不就乱套了嘛。现在明白了吧?这就是ThreadLocal设计的初衷:提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。

ThreadLocal基本操作

构造函数

ThreadLocal的构造函数签名是这样的:

/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}

内部啥也没做。

initialValue函数

initialValue函数用来设置ThreadLocal的初始值,函数签名如下:

protected T initialValue() {
return null;
}

该函数在调用get函数的时候会第一次调用,但是如果一开始就调用了set函数,则该函数不会被调用。通常该函数只会被调用一次,除非手动调用了remove函数之后又调用get函数,这种情况下,get函数中还是会调用initialValue函数。该函数是protected类型的,很显然是建议在子类重载该函数的,所以通常该函数都会以匿名内部类的形式被重载,以指定初始值,比如:

package com.winwill.test;
 
/**
* @author qifuguang
* @date 15/9/2 00:05
*/
public class TestThreadLocal {
private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return Integer.valueOf(1);
}
};
}

get函数

该函数用来获取与当前线程关联的ThreadLocal的值,函数签名如下:

public T get()

如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回。

set函数

set函数用来设置当前线程的该ThreadLocal的值,函数签名如下:

public void set(T value)

设置当前线程的ThreadLocal的值为value。

remove函数

remove函数用来将当前线程的ThreadLocal绑定的值删除,函数签名如下:

public void remove()

在某些情况下需要手动调用该函数,防止内存泄露。

代码演示

学习了最基本的操作之后,我们用一段代码来演示ThreadLocal的用法,该例子实现下面这个场景:

有5个线程,这5个线程都有一个值value,初始值为0,线程运行时用一个循环往value值相加数字。

代码实现:

package com.winwill.test;
 
/**
* @author qifuguang
* @date 15/9/2 00:05
*/
public class TestThreadLocal {
private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
 
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new MyThread(i)).start();
}
}
 
static class MyThread implements Runnable {
private int index;
 
public MyThread(int index) {
this.index = index;
}
 
public void run() {
System.out.println("线程" + index + "的初始value:" + value.get());
for (int i = 0; i < 10; i++) {
value.set(value.get() + i);
}
System.out.println("线程" + index + "的累加value:" + value.get());
}
}
}

执行结果为:

线程0的初始value:0
线程3的初始value:0
线程2的初始value:0
线程2的累加value:45
线程1的初始value:0
线程3的累加value:45
线程0的累加value:45
线程1的累加value:45
线程4的初始value:0
线程4的累加value:45

可以看到,各个线程的value值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。

如何实现的

看了基本介绍,也看了最简单的效果演示之后,我们更应该好好研究下ThreadLocal内部的实现原理。如果给你设计,你会怎么设计?相信大部分人会有这样的想法:

每个ThreadLocal类创建一个Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。

没错,这是最简单的设计方案,JDK最早期的ThreadLocal就是这样设计的。JDK1.3(不确定是否是1.3)之后ThreadLocal的设计换了一种方式。

我们先看看JDK8的ThreadLocal的get方法的源码:

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();
}

其中getMap的源码:

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

setInitialValue函数的源码:

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

createMap函数的源码:

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

简单解析一下,get方法的流程是这样的:

  1. 首先获取当前线程
  2. 根据当前线程获取一个Map
  3. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
  4. 如果e不为null,则返回e.value,否则转到5
  5. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

然后需要注意的是Thread类中包含一个成员变量:

ThreadLocal.ThreadLocalMap threadLocals = null;

所以,可以总结一下ThreadLocal的设计思路:
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。
这个方案刚好与我们开始说的简单的设计方案相反。查阅了一下资料,这样设计的主要有以下几点优势:

  • 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点(没有亲测)
  • 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

再深入一点

先交代一个事实:ThreadLocalMap是使用ThreadLocal的弱引用作为Key的

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;
}
}
...
...
}

下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 

然后网上就传言,ThreadLocal会引发内存泄露,他们的理由是这样的:

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。

我们来看看到底会不会出现这种情况。
其实,在JDK的ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,下面是ThreadLocalMap的getEntry方法的源码:

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);
}

getEntryAfterMiss函数的源码:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
 
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

expungeStaleEntry函数的源码:

private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
 
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
 
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
 
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

整理一下ThreadLocalMap的getEntry函数的流程:

  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询

在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

 

 

 

 

 

 

 

 

 

 

 

首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。 

另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。 

如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。 

下面来看一个hibernate中典型的ThreadLocal的应用: 

Java代码  关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 
  1. private static final ThreadLocal threadSession = new ThreadLocal();  
  2.   
  3. public static Session getSession() throws InfrastructureException {  
  4.     Session s = (Session) threadSession.get();  
  5.     try {  
  6.         if (s == null) {  
  7.             s = getSessionFactory().openSession();  
  8.             threadSession.set(s);  
  9.         }  
  10.     } catch (HibernateException ex) {  
  11.         throw new InfrastructureException(ex);  
  12.     }  
  13.     return s;  
  14. }  


可以看到在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap(下面会讲到),而threadSession作为这个值的key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量,而不是public的。 
显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。要是session是多个线程共享使用的,那还不乱套了。 
试想如果不用ThreadLocal怎么来实现呢?可能就要在action中创建session,然后把session一个个传到service和dao中,这可够麻烦的。或者可以自己定义一个静态的map,将当前thread作为key,创建的session作为值,put到map中,应该也行,这也是一般人的想法,但事实上,ThreadLocal的实现刚好相反,它是在每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了,不知道除了这些还有什么其他的好处。 

总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点: 
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。 
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。 

当然如果要把本来线程共享的对象通过ThreadLocal.set()放到线程中也可以,可以实现避免参数传递的访问方式,但是要注意get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决。但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中。 

ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。 

下面来看看ThreadLocal的实现原理(jdk1.5源码) 

Java代码  关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 
  1. public class ThreadLocal<T> {  
  2.     /** 
  3.      * ThreadLocals rely on per-thread hash maps attached to each thread 
  4.      * (Thread.threadLocals and inheritableThreadLocals).  The ThreadLocal 
  5.      * objects act as keys, searched via threadLocalHashCode.  This is a 
  6.      * custom hash code (useful only within ThreadLocalMaps) that eliminates 
  7.      * collisions in the common case where consecutively constructed 
  8.      * ThreadLocals are used by the same threads, while remaining well-behaved 
  9.      * in less common cases. 
  10.      */  
  11.     private final int threadLocalHashCode = nextHashCode();  
  12.   
  13.     /** 
  14.      * The next hash code to be given out. Accessed only by like-named method. 
  15.      */  
  16.     private static int nextHashCode = 0;  
  17.   
  18.     /** 
  19.      * The difference between successively generated hash codes - turns 
  20.      * implicit sequential thread-local IDs into near-optimally spread 
  21.      * multiplicative hash values for power-of-two-sized tables. 
  22.      */  
  23.     private static final int HASH_INCREMENT = 0x61c88647;  
  24.   
  25.     /** 
  26.      * Compute the next hash code. The static synchronization used here 
  27.      * should not be a performance bottleneck. When ThreadLocals are 
  28.      * generated in different threads at a fast enough rate to regularly 
  29.      * contend on this lock, memory contention is by far a more serious 
  30.      * problem than lock contention. 
  31.      */  
  32.     private static synchronized int nextHashCode() {  
  33.         int h = nextHashCode;  
  34.         nextHashCode = h + HASH_INCREMENT;  
  35.         return h;  
  36.     }  
  37.   
  38.     /** 
  39.      * Creates a thread local variable. 
  40.      */  
  41.     public ThreadLocal() {  
  42.     }  
  43.   
  44.     /** 
  45.      * Returns the value in the current thread's copy of this thread-local 
  46.      * variable.  Creates and initializes the copy if this is the first time 
  47.      * the thread has called this method. 
  48.      * 
  49.      * @return the current thread's value of this thread-local 
  50.      */  
  51.     public T get() {  
  52.         Thread t = Thread.currentThread();  
  53.         ThreadLocalMap map = getMap(t);  
  54.         if (map != null)  
  55.             return (T)map.get(this);  
  56.   
  57.         // Maps are constructed lazily.  if the map for this thread  
  58.         // doesn't exist, create it, with this ThreadLocal and its  
  59.         // initial value as its only entry.  
  60.         T value = initialValue();  
  61.         createMap(t, value);  
  62.         return value;  
  63.     }  
  64.   
  65.     /** 
  66.      * Sets the current thread's copy of this thread-local variable 
  67.      * to the specified value.  Many applications will have no need for 
  68.      * this functionality, relying solely on the {@link #initialValue} 
  69.      * method to set the values of thread-locals. 
  70.      * 
  71.      * @param value the value to be stored in the current threads' copy of 
  72.      *        this thread-local. 
  73.      */  
  74.     public void set(T value) {  
  75.         Thread t = Thread.currentThread();  
  76.         ThreadLocalMap map = getMap(t);  
  77.         if (map != null)  
  78.             map.set(this, value);  
  79.         else  
  80.             createMap(t, value);  
  81.     }  
  82.   
  83.     /** 
  84.      * Get the map associated with a ThreadLocal. Overridden in 
  85.      * InheritableThreadLocal. 
  86.      * 
  87.      * @param  t the current thread 
  88.      * @return the map 
  89.      */  
  90.     ThreadLocalMap getMap(Thread t) {  
  91.         return t.threadLocals;  
  92.     }  
  93.   
  94.     /** 
  95.      * Create the map associated with a ThreadLocal. Overridden in 
  96.      * InheritableThreadLocal. 
  97.      * 
  98.      * @param t the current thread 
  99.      * @param firstValue value for the initial entry of the map 
  100.      * @param map the map to store. 
  101.      */  
  102.     void createMap(Thread t, T firstValue) {  
  103.         t.threadLocals = new ThreadLocalMap(this, firstValue);  
  104.     }  
  105.   
  106.     .......  
  107.   
  108.     /** 
  109.      * ThreadLocalMap is a customized hash map suitable only for 
  110.      * maintaining thread local values. No operations are exported 
  111.      * outside of the ThreadLocal class. The class is package private to 
  112.      * allow declaration of fields in class Thread.  To help deal with 
  113.      * very large and long-lived usages, the hash table entries use 
  114.      * WeakReferences for keys. However, since reference queues are not 
  115.      * used, stale entries are guaranteed to be removed only when 
  116.      * the table starts running out of space. 
  117.      */  
  118.     static class ThreadLocalMap {  
  119.   
  120.     ........  
  121.   
  122.     }  
  123.   
  124. }  



可以看到ThreadLocal类中的变量只有这3个int型: 

Java代码  关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 
  1. private final int threadLocalHashCode = nextHashCode();  
  2. private static int nextHashCode = 0;  
  3. private static final int HASH_INCREMENT = 0x61c88647;  


而作为ThreadLocal实例的变量只有 threadLocalHashCode 这一个,nextHashCode 和HASH_INCREMENT 是ThreadLocal类的静态变量,实际上HASH_INCREMENT是一个常量,表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量,而nextHashCode 的表示了即将分配的下一个ThreadLocal实例的threadLocalHashCode 的值。 

可以来看一下创建一个ThreadLocal实例即new ThreadLocal()时做了哪些操作,从上面看到构造函数ThreadLocal()里什么操作都没有,唯一的操作是这句: 

Java代码  关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 
  1. private final int threadLocalHashCode = nextHashCode();  


那么nextHashCode()做了什么呢: 

Java代码  关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 
  1. private static synchronized int nextHashCode() {  
  2.     int h = nextHashCode;  
  3.     nextHashCode = h + HASH_INCREMENT;  
  4.     return h;  
  5. }  

就是将ThreadLocal类的下一个hashCode值即nextHashCode的值赋给实例的threadLocalHashCode,然后nextHashCode的值增加HASH_INCREMENT这个值。 

因此ThreadLocal实例的变量只有这个threadLocalHashCode,而且是final的,用来区分不同的ThreadLocal实例,ThreadLocal类主要是作为工具类来使用,那么ThreadLocal.set()进去的对象是放在哪儿的呢? 

看一下上面的set()方法,两句合并一下成为 

Java代码  关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 
  1. ThreadLocalMap map = Thread.currentThread().threadLocals;  


这个ThreadLocalMap 类是ThreadLocal中定义的内部类,但是它的实例却用在Thread类中: 

Java代码  关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 
  1. public class Thread implements Runnable {  
  2.     ......  
  3.   
  4.     /* ThreadLocal values pertaining to this thread. This map is maintained 
  5.      * by the ThreadLocal class. */  
  6.     ThreadLocal.ThreadLocalMap threadLocals = null;    
  7.     ......  
  8. }  



再看这句: 

Java代码  关于ThreadLocal 和 Spring单例如何保证线程安全
            
    
    
        ThreadLocal 
  1. if (map != null)  
  2.     map.set(this, value);  


也就是将该ThreadLocal实例作为key,要保持的对象作为值,设置到当前线程的ThreadLocalMap 中,get()方法同样大家看了代码也就明白了,ThreadLocalMap 类的代码太多了,我就不帖了,自己去看源码吧。 

 

 

相关标签: ThreadLocal