ThreadLocal实现原理揭秘

ThreadLocal是什么?
对java多线程有了解的人都清楚,在多个线程程序中,会出现线程安全性问题,即多线程是不安全的,线程不安全的场景有很多,这里不一一赘述,这里重点是多线程不安全的场景之一————类成员变量在多线程环境下是不安全的。举例如下:

package testpackage;

import lombok.Data;

@Data
public class Class2 {

  private int i;

  private ThreadLocal<Integer> j = new ThreadLocal<>();
}

//------------------------万恶的分割线----------------------------------------

package testpackage;

public class Class1 {
  
  public static void main(String[] args) {
   Class2 class2 = new Class2();
   class2.setI(1);
    new Thread(()->{
       System.out.println("子线程开始");
       class2.setI(10);
       System.out.println("子线程 i=" + class2.getI());
       System.out.println("子线程结束");
    }).start();
    System.out.println("主线程休眠3秒");
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("主线 i ="+class2.getI());
    System.out.println("主结束");
  }

}

int类型的i为Class2的类成员变量,Class1里有两个线程,一个为主线程,一个为从主线程新开的子线程,假定两个线程都需要使用i,理想状态是:主线程与子线程互不干扰,即变量i在两个线程中应该是独立互不干扰的(就好像人民币是文明社会人类共有的概念【定义的变量i】,但是每个人拥有的人民币却是他们自己的),上述代码运行结果如下:

主线程休眠3秒
子线程开始
子线程 i=10   --重点
子线程结束
主线 i =10    --重点
主结束

发现了么?主线程的 i 结果为10,按理想状态,变量 i 在子线程中改为了10,但这个改动应与主线程无关,因为这是两个线程,两个线程间的应该互不干扰才是线程安全的,但上述代码实际运行情况是子线程修改了 i 的值后,对主线程的 i的使用产生了影响。
上述现象既是开篇提到的 类成员变量在多线程环境下是不安全的

那么,问题来了,我如果需要使用线程安全的变量该怎么办呢?这个问题的答案就是————ThreadLocal,使用ThreadLocal类型的变量即可使其线程安全,具体使用方法如下:

package testpackage;

import lombok.Data;

@Data
public class Class2 {

  private int i;

  private ThreadLocal<Integer> j = new ThreadLocal<>();
}

//------------------------万恶的分割线----------------------------------------

package testpackage;

public class Class1 {

  public static void main(String[] args) {
   Class2 class2 = new Class2();
  //主线程设置 i 的值为1
   class2.setI(1);
  //主线程设置 j 的值为1
   class2.getJ().set(1);
    new Thread(()->{
       System.out.println("子线程开始");
       class2.setI(10);
       class2.getJ().set(11);
       System.out.println("子线程 i=" + class2.getI());
       System.out.println("子线程 j=" + class2.getJ().get());
       System.out.println("子线程结束");
    }).start();
    System.out.println("主线程休眠3秒");
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("主线 i ="+class2.getI());
    System.out.println("子线程 j=" + class2.getJ().get());
    System.out.println("主结束");
  }

}

上述代码的运行情况如下:

主线程休眠3秒
子线程开始
子线程 i=10
子线程 j=11
子线程结束
主线 i =10
主线程 j=1
主结束

结果证明,ThreadLocal的使用保证了变量在多线程中的安全性。

那么,ThreadLocal是如何实现变量线程间的安全性的呢,是如何保证变量在多个线程间独立互不干扰呢?现在,就开始对ThreadLocal的实现原理逐一分析:

首先,从Thread说起:

public
class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
 /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    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内部结构图如下:


image.png

从上面的结构图,我们已经窥见ThreadLocal的核心机制:

  • 每个Thread线程内部都有一个ThreadLocalMap。
  • ThreadLocalMap里面存储ThreadLocal对象(key)和线程的变量副本(value)
  • 但是,Thread内部的ThreadLocalMap是由ThreadLocal维护的,由ThreadLocal负责向ThreadLocalMap获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

ThreadLocal的主要方法解析:

1.ThreadLocal主要有以下方法
public T get()
public void set(T value)
public void remove()
  • get()方法用于获取当前线程的副本变量值。
  • set()方法用于保存当前线程的副本变量值。
  • remove()方法移除当前前程的副本变量值。

我们先来看看 get 方法:

public
class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

 static class ThreadLocalMap {
 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
  private Entry[] table;

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

  }
}
//---------------------------------------万恶的分割线----------------------------------------------

/**
get方法就做了如下几件事:
1.获取当前线程
2.拿到当前线程Thread对象后,拿到线程类成员threadLocals
3.调用getEntry以ThreadLocal
**/
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();
    }

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

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

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

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    胜浩_ae28阅读 5,001评论 0 23
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,778评论 3 53
  •   一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺...
    OmaiMoon阅读 1,623评论 0 12
  • 下面我就以面试问答的形式学习我们的——ThreadLocal类(源码分析基于JDK8) 问答内容 1、问:Thre...
    Sophia_dd35阅读 2,063评论 1 36
  • 时间并没有白白流逝,而是把多余的、不快的、腐败的东西冲刷下来带走了。时间就是忘却。它使古旧的东西变得美丽,...
    安安Ann阅读 164评论 0 0