JAVA多线程·ThreadLocal实现原理、使用场景和相关问题

一、概述

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

二、实现原理

1. 方法

public T get(); //获取ThreadLocal在当前线程中保存的变量副本
public void set(T value); //设置当前线程中变量的副本
public void remove(); //用来移除当前线程中变量的副本
protected T initialValue(); //延迟加载方法,需要时进行重写

2. get方法的实现

    public T get() {
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //将ThreadLocal作为key,获取对应的value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T) e.value;
                //获取成功,返回value
                return result;
            }
        }
        //获取失败,为线程创建ThreadLocalMap,并添加key-value键值对
        return setInitialValue();
    }

ThreadLocalMap用来存储键值对映射,其中键为WeakReference<ThreadLocal>对象,值为要保存的变量。提供getEntry、set和remove方法用来访问、设置和删除键值对。(数据结构为数组,初始容量16,负载因子2/3,解决冲突的方法是再hash法,即在当前hash的基础上再自增一个常量。)

3. set方法的实现

    public void set(T value) {
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //已经存在ThreadLocalMap,直接修改
            map.set(this, value);
        else
            //不存在ThreadLocalMap,先初始化再修改
            createMap(t, value);
    }

4. remove方法的实现

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

5. 总结

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,用来存储实际的变量副本,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,Thread里面threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,会先对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,保存到threadLocals中。

之后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

三、使用方式

class Test{
  
  private static class Person{
    
    private String name;
    private int age;
    
    public Person(String name, int age){
      this.name = name;
      this.age = age;
    }
    
    public String getName(){
      return name;
    }
    
    public int getAge(){
      return age;
    }
    
    public void setAge(int age){
      this.age = age;
    }
   
    public String toString() {
      return "Person@" + Integer.toHexString(hashCode()) + ": {name=" + name + ", age=" + age + "}";
    }
  }
  
  private static ThreadLocal<Person> mPerson= new ThreadLocal<Person>(){
    protected Person initialValue(){
      return new Person("Lynn", 20);
    }
  };
  
  public static void main(String[] args){
    ThreadA threadA = new ThreadA();
    ThreadB threadB = new ThreadB();
    threadA.start();
    threadB.start();
  }
  
  static class ThreadA extends Thread{
    public void run(){
      Person person = mPerson.get();
      System.out.println("ThreadA:" + person.toString());
      person.setAge(18);
      System.out.println("ThreadA:" + person.toString());
    }
  }
  
  static class ThreadB extends Thread{
    public void run(){
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      Person person = mPerson.get();
      System.out.println("ThreadB:" + person.toString());
      person.setAge(20);
      System.out.println("ThreadB:" + person.toString());
    }
  }
  
}

输出结果:

ThreadA:Person@6e9ef235: {name=Lynn, age=20}
ThreadA:Person@6e9ef235: {name=Lynn, age=18}
ThreadB:Person@6e5070e8: {name=Lynn, age=20}
ThreadB:Person@6e5070e8: {name=Lynn, age=25}

表明各个线程访问到的是不一样的对象。

注意:

要保证各个线程访问到的是不一样的对象,需要保证initialValue方法或者Supplier<? extends S>返回的是不一样的对象。如果返回的是相同的对象引用,会导致各个线程访问到的也是相同的对象。

将之前的创建对象代码改为:

  private static Person lynn = new Person("Lynn", 20);

  private static ThreadLocal<Person> mPerson = new ThreadLocal<Person>() {
    protected Person initialValue() {
      return lynn;
    }
  };

输出结果为:

ThreadA:Person@6d74829c: {name=Lynn, age=20}
ThreadA:Person@6d74829c: {name=Lynn, age=18}
ThreadB:Person@6d74829c: {name=Lynn, age=18}
ThreadB:Person@6d74829c: {name=Lynn, age=25}

获得到的相同的对象。

四、使用场景

使用场景可以归纳成两点:

  1. 解决对象共享问题。每个线程访问自己的变量副本,保证安全性。
  2. 作为一种对象保存和访问方式。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

常用场景

1. 安全地访问线程不安全对象

比如使用SimpleDateFormat对象进行日期格式化。由于SimpleDateFormat本身不是线程安全的,因此使用ThreadLocal来给每个线程缓存一个SimpleDateFormat实例,保证线程可以安全的访问SimpleDateFormat。

2. 数据库连接池实现

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
}
};
 
public static Connection getConnection() {
return connectionHolder.get();
}

3. session管理

下面这段代码摘自:http://www.iteye.com/topic/103804

private static final ThreadLocal threadSession = new ThreadLocal();
 
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;
}

五、其他

1. 内存泄漏问题

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

解决方案:

ThreadLocalMap自身做了优化,在每次get/set时,将访问到的key为null的Entry置为null,擦除对value的引用。

手动调用remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容