ThreadLocal学习整理

来源:互联网 发布:淘宝购物的流程 编辑:程序博客网 时间:2024/06/05 16:09
    文章仅仅用于个人的学习记录,基本上内容都是网上各个大神的杰作,此处摘录过来以自己的理解学习方式记录一下。
    个人最为认可和推崇的大神文章:
    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.iteye.com/topic/103804

1、概述。
       ThreadLocal是java提供的一个内部类位于java.lang包下面。其实从名字上就能看出来ThreadLocal类的作用。thread、local。local有局部的、本地的意思。整个名字就可以理解为:线程局部对象。在这个线程运行程序过程中,你可以从当前线程的任何地方来获取这个局部变量。这样就减少了同一个线程内多个流程之间(如函数、组件)一些公共变量的传递的复杂程度,且可以实现各个线程隔离变量。一般情况下,通过ThreadLocal.set(T t)到线程中的局部对象,只需要该线程自己使用的对象,其它线程是不需要访问,也不能访问的(获取不到map?),ThreadLocal的应用场合,最适合按线程多实例(一个线程一个实例)的对象的访问,并且这个对象(在当前的线程中)很多地方要用到。
      每个ThreadLocal只能放一个对象。要是需要放其它对象作为线程的另外的局部变量,那么就需要在new 一个新的ThreadLocal出来,然后以这个新的ThreadLocal作为key,需要放入的对象作为value,放在ThreadLocalMap当中。

2、ThreadLocal源码分析(注意ThreadLocal是一个泛型类T)
   2.1 ThreadLocalMap
      首先ThreadLocal中有一个非常重要的静态内部类ThreadLocalMap,这个类是是一个专门定制的hashmap集合。(具体和普通的区别,没有深入研究,但是,它为了ThreadlLocal专门作出类似内存回收等的处理,还有只能存放一组数据等),此处就把它当成一个map了。而在线程类Therad中有一个成员变量:ThreadLocal.ThreadLocalMap threadLocals = null; 初始化为null,且为默认修饰符。这样每个Thread对象都有一个成员变量threadLocals ,注意这是一个集合。
      其实调用ThreadLocal.set(T t)的方法,最终就是以key = (你new的ThreadLocal对象),value = t(传入的要保存到线程局部变量中的对象)。
   2.2 函数和方法。
      构造方法: 什么也没做,注意一些静态的此时早已加载。
public ThreadLocal() {
}
      成员函数:
      a、initialValue  默认直接返回null。只有在直接get的时候会调用一下。如果先前set过,不会调用这个。
protected T initialValue() {
return null;
}
      此函数,可以子类重写然后给定每个对象的初始值,如你存入的是Integer那么可以默认让它是个1.
       
public class TestThreadLocal {
private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return Integer.valueOf(1);
}
};
}
     b、get()函数
       
public T get() { // 返回的是泛型定义的数据
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//以当前线程的id为key,去获取ThreadLocalMap
if (map != null) {//不等于null的时候证明已经初始化过Thread的成员变量threadLocals.
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
      getMap函数中只有一条语句return t.threadLocals;直接返回当前线程的threadLocals.也就是ThreadLocalMap.由于不同的线程id不同,那么返回的这个ThreadLocalMap也会不同,(就算接下来在ThreadLocalMap中的key相同也无关大雅,因为前面不同的map)。这样我们就可以从ThreadLocalMap中取出自己存入的局部变量值了。
      如果当前正在运行的线程没有相关联的 ThreadLocal.ThreadLocalMap threadLocals = null; 就是没初始化这个map。那么直接调用setInitialValue去初始化变量值。 
private T setInitialValue() {
T value = initialValue(); //默认返回的是null,子类可以重写。(或者匿名内部类)
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//当这个map(一定注意其实不是集合)存在时,直接把初始化的值,存进去。
else
createMap(t, value);//不存在map,也就是Thread的成员变量threadLocals未初始化时创建一个。
return value; //返回默认值.
}
       createMap函数中只有一条语句:t.threadLocals = new ThreadLocalMap(this, firstValue); 以当前的ThreadLocal为key,默认值为value。有一个很关键Thread t = Thread.currentThread();不同的线程t肯定不同,这样一旦调用过createMap.那么这个线程Thread类的成员变量threadLocals就不为null了。就会尝试直接往里面放值,不会多次create。而且不同的线程产生不同的ThreadLocalMap.这样互不影响,相互隔离。

      c、set(T value) 函数
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) //一旦有初始化就不会去再创建。
map.set(this, value);//由此也能看出这个ThreadLocal机制也只能存储一个线程局部变量,除非在new一个ThreadLocal对象。
else
createMap(t, value);
}
      Thread变量threadLocals已经初始化的时候,直接往这个ThreadLocalMap存入value,以当前的ThreadLocal为key。未初始化的时候,调用createMap,实例化ThreadLocalMap。并且以key = ThreadLocalMap,value = T(传入的值)。很多人都说传过来的是变量的副本,但是以java角度来看,传入的是这个变量的引用,也没有看见在new一个变量什么的。这也算是副本吗?

      d、remove()函数。
       
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this); //直接从ThreadLocalMap中移除key = 当前ThreadLocal对象的Value
}
       对于ThreadLocalMap内部的具体的set、getEntry、remove等函数,此处暂无学习,暂时就当成一个map看待。这样通过remove就移除掉了存入的值,防止内存泄露,因为有时候各种原因系统把key就是当前的ThreadLocal对象销毁了。此时假如当前Thread还在,那么Thread.threadLocals也就还在,也就是ThreadLocalMap还在,它当中的entry有key、value。当key被置为null的时候,此时value值应该是永远无法访问的,也不需要存在的。但是由于有这条强引用链的存在,它无法被回收。所以有时候需要手动调用来强制回收。主要的函数就是这几个了ThreadLocal源码的难点应该是ThreadLocalMap内部的具体实现,此处暂未学习。
   2.3、实现的整体逻辑
        在较早版本的jdk中,ThreadLocal的实现逻辑概念如下:
           每个ThreadLocal类中有一个集合,然后以线程ID作为key,要存入的对象作为value。这样每个线程用自己的对象,并且在线程运行的任何地方通过map获取。
        参考现在的JDK源码,此处是1.7的。通过前面的函数的描述我们可知。现在通过Thread和TreadLocal的配合:
           每个Thread通过自己的成员变量threadLocals维护一个ThreadLocalMap映射表,这个映射表的key是当前的ThreadLocal实力本身,value是真正需要存储的Object。(由于每个线程有通过自己Thread ID来的ThreadLocalMap,所以各个线程之间不会冲突。并且由于是以ThreadLocal本身为实例的且,
        只有这一个key,所以不可以存放多个变量,只会覆盖。想要同一个线程维护多个局部变量,那么只能在new一个ThreadLocal,然后通过这个ThreadLocal对象往里面添加了)。
            网上查的第二种方案比第一种方案的优势。
            1、第二种设计方案每个Map的Entry数量变小了:之前是Thread数量,现在是ThreadLocal数量,这样会大大提高性能。
            2、Thread销毁以后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。
    2.4、关于是否会产生内存泄漏的问题。(完全摘录文章开头处的连接,建议大家去看一下。)
         从源码中可以看出ThreadLocalMap类中的ThreadLocal使用的是弱引用。
        
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
 
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
         那么一旦ThreadLocal没有外部的强引用引用它,当系统多gc时候。这个ThreadLocal就会被回收。这样一来ThreadMap中就会出现key为null的value。当线程不结束的时候由于引用链:Thread引用--->Thread--->ThreadLocalMap----->Entry--->value的存在,就会造成内存泄漏。其实JDK中的ThreadLocalMap设计中已经考虑到这种情况了,并且有如下措施。
        在你通过ThreadLocal调用get的时候,最终会调用到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);// 当key为null的时候
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; //一旦为null,移除这个value
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;
}
        ThreadLocalMapde getEntry函数的整体流程:
        1、首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
                2、如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询。
       Thread也就是说在整个get过程中遇到的key为null的Entry都会被擦除,那么value的上一个引用链就不存在了,自然会被回收。set也有类似的操作。这样在你每次调用ThreadLocal的get函数去获取值或者调用set函数去设置值的时候,都回去做这个操作防止内存泄漏。当然最保险的还是通过手动调用remove函数直接移除。
3、额外知识点小结:
   1、ThreadLocal官方建议已定义成private static的这样让Thread不那么容易就会被回收。
   2、这样开来设置个线程的成员变量也可以实现。但是要获取这个变量肯恩高就比较麻烦了,甚至可能需要在各个流程中传递。
   3、真正涉及到共享变量的时候ThreadlLocal是解决不了的。它最多是当每个线程都需要这个实例,如一个打开数据库的对象时,保证每个线程拿到一个进而去做操作,互不影响。但是这个对象其实并不是共享的!!!!!!(是都可以实例化一个直接用)
      看到一个评论感觉说的不错,配合个人理解整理一下:
      a、假设我有一个类。在实例化的时候需要消耗大量的系统资源和时间(如获取数据库实例),此时我就会考虑用单例模式。网上找的例子
class ConnectionManager {
private static Connection connect = null;
public static Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();//单利模式获取连接对象
}
return connect;
}
public static void closeConnection() {
if(connect!=null)
connect.close(); //关闭连接
}
}
     以上就会产生问题:
         1、多线程使用的时候,有可能同时进入到if connect进而产生多个connect实例。
         2、connect是在这个类中共享的,这样就会导致一个线程正在conncet的时候两一个正在close。此时就有一个知识点了,以前经常看到但是没注意过。就是应该为这两处加锁,并且这两个锁应该是同一把锁!!(我说看android源码有时候这个和那个一样,有时候又和另一个一样。以前没考虑过,原来这冲突操作就是考虑的标准啊)。假如仅仅是对于以上情况处理的话,在不考虑使用同步锁的情况下(相对来说浪费时间),而此时其实connect完全不需要共享的,每个线程有自己的connect,然后去打开、关闭即可。这样可通过如下方式调用.(还是网上的)
class ConnectionManager {
private Connection connect = null;
public Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public void closeConnection() {
if(connect!=null)
connect.close();
}
}
class Dao{
public void insert() {
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.openConnection();
//使用connection进行操作,打开关闭等
connectionManager.closeConnection();
}
}
       此时Connect变量不是static,是每个对象都有的。然后在如inster操作时,使用每个线程都用自己的。每次都是在方法内连接(某一个特别小的时间段cpu就执行自己)也不会出现线程安全问题。但有个致命影响:服务器压力比较大,频繁的开启和关闭严重影响性能。我觉由于不是拷贝的副本(感觉传入引用不算),用ThreadLocal,为每一个变量一个对象也不行,因为这是同一个变量。
4、一个简单的实现:(记录一下)
public class TestThreadLocal {
//大家都用这一个也没事,因为首先ThreadLocalMap就不同,然后就算是相同的ThreadLocal取出来的也不同
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 0
原创粉丝点击