ThreadLocal解析

来源:互联网 发布:虚拟光驱软件手机版 编辑:程序博客网 时间:2024/06/02 14:09

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方法的流程是这样的:

  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会引发内存泄露,他们的理由是这样的:

如上图,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函数的流程:

  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它,防止内存泄露。


在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=11Thread[Thread-2],counter=12Thread[Thread-2],counter=13Thread[Thread-0],counter=11Thread[Thread-0],counter=12Thread[Thread-0],counter=13Thread[Thread-1],counter=11Thread[Thread-1],counter=12Thread[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

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 30岁了没有朋友怎么办 产检宝宝腿短怎么办 2岁宝宝不学说话怎么办 4岁了还不会说话怎么办 两周岁还不说话怎么办 2岁了不会说话怎么办 一岁宝宝蛀牙了怎么办 分手了想联系他怎么办 和婆家人闹翻了怎么办 2岁宝宝不好断奶怎么办 2岁宝宝断奶哭闹怎么办 脑子感觉变笨了怎么办 5岁数学不开窍怎么办 小孩拼音太差了怎么办 我生了爸爸孩子怎么办 三岁宝宝特别犟怎么办 孩子说老师打他怎么办 孩子在幼儿园不合群怎么办 孩与厌学不想学怎么办 1岁宝宝太活泼怎么办 3岁宝宝不爱看书怎么办 6岁不好好吃饭怎么办 二岁宝宝不吃饭怎么办 2岁半宝宝不爱吃饭怎么办 宝宝一岁了不爱吃饭怎么办 一岁多的宝宝不爱吃饭怎么办 小孩不吃饭还吐怎么办 一岁婴儿不吃饭怎么办 写字久了肩膀疼怎么办 6岁儿童不写字怎么办 孩子字写得丑怎么办 6岁儿童怕写字怎么办 高三学生上课困怎么办 高三学生压力大怎么办 数学会做的做错怎么办 脑子很笨反应慢怎么办 好学生考砸了怎么办 孩子写作业范愁怎么办 对粗心的孩子该怎么办 做计算题总出错怎么办 孩子字写得不好怎么办