您的位置:首页 > 其它

ThreadLocal解析

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

ThreadLocal类为每一个线程都维护了自己独有的变量拷贝。每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,那就没有任何必要对这些线程进行同步,它们也能最大限度的由CPU调度,并发执行。并且由于每个线程在访问该变量时,读取和修改的,都是自己独有的那一份变量拷贝,变量被彻底封闭在每个访问的线程中,并发错误出现的可能也完全消除了。对比前一种方案,这是一种以空间来换取线程安全性的策略。


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()
在某些情况下需要手动调用该函数,防止内存泄露。

有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的设计换了一种方式。

我们先看看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方法的流程是这样的:
首先获取当前线程
根据当前线程获取一个Map
如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
如果e不为null,则返回e.value,否则转到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会引发内存泄露,他们的理由是这样的:

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

永远无法回收,造成内存泄露。
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例.
这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收

我们来看看到底会不会出现这种情况。

其实,在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
函数的流程:
首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
如果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它,防止内存泄露。

在JDK的早期版本中,提供了一种解决多线程并发问题的方案:java.lang.ThreadLocal类。ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题。

  ThreadLocal本身并不是一个线程,而是通过操作当前线程中的一个内部变量来达到与其他线程隔离的目的。之所以取名为ThreadLocal,所期望表达的含义是其操作的对象是线程的一个本地变量。

 

Thread.java

public class Thread implements Runnable {
// 这里省略了许多其他的代码
ThreadLocal.ThreadLocalMap threadLocals = null;
}


 

ThreadLocal.java

public class ThreadLocal<T> {
// 这里省略了许多其他代码
// 将value 的值保存于当前线程的本地变量中
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 调用getMap 方法获得当前线程中的本地变量ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap 已存在,直接使用
if (map != null)
// 以当前的ThreadLocal 的实例作为key,存储于当前线程的
// ThreadLocalMap 中,如果当前线程中定义了多个不同的ThreadLocal
// 的实例,则它们会作为不同key 进行存储而不会互相干扰
map.set(this, value);
else
// 如果ThreadLocalMap 不存在,则为当前线程创建一个新的
createMap(t, value);
}

// 获取当前线程中以当前ThreadLocal 实例为key 的变量值
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取当前线程中以当前ThreadLocal 实例为key 的变量值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T) e.value;
}
// 当map 不存在时,设置初始值
return setInitialValue();
}

// 从当前线程中获取与之对应的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

// 创建当前线程中的ThreadLocalMap
void createMap(Thread t, T firstValue) {
// 调用构造函数生成当前线程中的ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// ThreadLoaclMap 的定义
static class ThreadLocalMap {
//这里省略了许多代码
}
}


 
ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLo-calMap变量。
线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的。
在创建ThreadLocalMap之前,会首先检查当前线程中的ThreadLocalMap变量是否已经存在,如果不存在则创建一个;如果已经存在,则使用当前线程已创建的ThreadLo-calMap。
使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key进行存储。

 

ThreadLocal模式至少从两个方面完成了数据访问隔离,即横向隔离和纵向隔离。
纵向隔离——线程与线程之间的数据访问隔离。这一点由线程的数据结构保证。因为每个线程在进行对象访问时,访问的都是各个线程自己的ThreadLocalMap。  
横向隔离——同一个线程中,不同的Thread-Local实例操作的对象之间相互隔离。这一点由ThreadLocalMap在存储时采用当前ThreadLocal的实例作为key来保证。

 

深入比较ThreadLocal模式与synchronized关键字
ThreadLocal是一个Java类,通过对当前线程中的局部变量的操作来解决不同线程的变量访问的冲突问题。所以,ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本。
Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量在访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量是多个线程共享的。
同步机制(synchronized关键字)采用了“以时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。

要完成ThreadLocal模式,其中最关键的地方就是创建一个任何地方都可以访问到的ThreadLocal实例。而这一点,我们可以通过类变量来实现,这个用于承载类变量的类就被视作是一个共享环境。

 

public class Counter {
// 新建一个静态的ThreadLocal 变量,并通过get 方法将其变为一个可访问的对象
private static ThreadLocal<Integer> counterContext = new
ThreadLocal<Integer>() {
protected synchronized Integer initialValue() {
return 10;
}
};

// 通过静态的get 方法访问ThreadLocal 中存储的值
public static Integer get() {
return counterContext.get();
}

// 通过静态的set 方法将变量值设置到ThreadLocal 中
public static void set(Integer value) {
counterContext.set(value);
}

// 封装业务逻辑,操作存储于ThreadLocal 中的变量
public static Integer getNextCounter() {
counterContext.set(counterContext.get() + 1);
return counterContext.get();
}
}


 

public class ThreadLocalTest extends Thread {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("Thread[" + Thread.currentThread().getName() + "],counter=" + Counter.getNextCounter());
}
}
}


 

public class Test {
public static void main(String[] args) throws Exception {
ThreadLocalTest testThread1 = new ThreadLocalTest();
ThreadLocalTest testThread2 = new ThreadLocalTest();
ThreadLocalTest testThread3 = new ThreadLocalTest();
testThread1.start();
testThread2.start();
testThread3.start();
}
}


 

我们来运行一下上面的代码,并看看输出结果:

Thread[Thread-2],counter=11

Thread[Thread-2],counter=12

Thread[Thread-2],counter=13

Thread[Thread-0],counter=11

Thread[Thread-0],counter=12

Thread[Thread-0],counter=13

Thread[Thread-1],counter=11

Thread[Thread-1],counter=12

Thread[Thread-1],counter=13

 

ThreadLocal模式最合适的使用场景:在同一个线程的不同开发层次中共享数据。

ThreadLocal模式的两个主要步骤:  
建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。  
在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)。

 

参考文章: http://qifuguang.me/2015/09/02/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3%E5%AF%86ThreadLocal/ http://www.cnblogs.com/brucecloud/p/5031295.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: