刮痧ThreadLocal

多线程与一致性

为了提高我们程序的性能,很多时候我们都会使用多线程以解决各种场景,但随之而来的是多线程带来的数据一致性问题该如何解决。

如何解决一致性问题?
  • 排队:如果多个线程操作‘同一份数据’,那就排个队吧,一个一个来,这样后面一个线程总能得到最新的修改值,例如操作系统中的锁,管程,屏障等都是这种排队机制。<ins style="box-sizing: border-box;">缺点是:性能低。</ins>
  • 投票:投票的机制就是多个人同时决策一件事,这个就涉及到了算法,往往会产生很多其他问题,比如欺诈
  • 避免:直观意思就是避免多个线程之间产生一致性问题,那该如何去做呢?例如git,ThreadLocal正是采用的这种避免的方式来完成多线程的执行
ThreadLocal定义:

定义:ThreadLocal提供了线程局部变量,一个线程局部变量在多个线程中分别由独立的值(副本)。

提问:既然是每个线程独有的,为什么不直接在调用线程的时候,在相应的线程方法里声明这个局部变量呢?

:同一个线程可能会调用到很多不同的类和方法,这样就要在不同的地方用到这个变量,自己去实现,代价太大,用ThreadLocal更加方便,且线程安全。

线程模型
线程模型

对应每个线程来说都有自己的独占数据,这些数据事进行来分配的,每个线程都有一个ThreadLocalMap对象,它本身是一个hash表,里面会放一些线程的局部变量,而ThreadLocal的核心也是这个ThreadLocalMap。

4种核心应用场景
1.资源持有:

例如有三个不同的类,在一次web请求中调用这三个类,但是用户是一个,那么用户数据就可以保存在一个线程里。 如图:


资源持有
2.线程一致:

例如JDBC事务,我们每次对数据库操作都会走getConnection,jdbc保证只要你是同一个线程过来的请求,不管是哪一个part,都返回的是同一个连接,就是使用ThreadLocal来做的,达到维护一致性的目的。Mybatis使用SqlSessionManager保证了我们同一个线程取出来的连接总是同一个。它是如何做到的呢?其实很简单,就是内部使用了一个ThreadLocal。


线程 一致
3.线程安全:

如果一个线程的调用链路比较长,中间出现异常,那我们可以把出错信息放在ThreadLocal里,然后在后续的链路中使用这个值,可以达到多线程在处理这个场景的时候保证线程安全。


线程安全
4.并发计算:

例如一个大的任务,拆分成多个小任务,分别计算,最后再进行结果汇总,那么我们可以把每个线程的计算结果放进ThreadLocal中,最后进行汇总计算。 实现案例:比如需要统计一段时间内某个接口的调用量


并发计算
线程不安全实现:
@RestController
@RequestMapping("orders")
public class OrderController {

    private Integer count = 0;

    @GetMapping("/visit")
    public Integer visit() throws InterruptedException {
        count++;
        Thread.sleep(100);
        return 0;
    }

    @GetMapping("/stat")
    public Integer stat() {
        return count;
    }
}

count++操作,首先我们是从内存里面读取原来的值,放在了线程本地内存里。然后进行 +1 操作,再写回到内存里。这个时候如果多个线程操作的话,有可能线程A这边还没来得及写,线程B那边读取的是原来的值。这样子的话就会造成数据不一致的问题。结果就会比预期的小。 结果明显是count的值与我们所期望的值不一致

如何解决?

当然方法很多,比如加锁,但今天我们要用ThreadLocal实现

@RestController
@RequestMapping("orders")
public class OrderController {

    private static final ThreadLocal<Integer> TL = ThreadLocal.withInitial(() -> 0);

    @GetMapping("/visit")
    public Integer visit() throws InterruptedException {
        Thread.sleep(100);
        TL.set(TL.get() + 1);
        return 0;
    }

    @GetMapping("/stat")
    public Integer stat() {
        return TL.get();
    }
}

这样即可达到我们的计数目的。

还有很多方法可以实现,比如我们经常用的原子类Automatic或者synchronized等,他们的实现思想不同,加锁和原子类使用的是【排队】思想,而ThreadLocal使用的是【避免】思想,效率更高。

刮痧ThreadLocal源码

API: ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

1.set:
public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}

void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
 }
 createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。

先拿到当前的线程,然后通过它去拿到一个Map,如果这个Map存在,就把value塞进去,否则就创建一个新的。

2.get:
public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}

private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}

先通过getMap方法拿到当前线程对应的Map,然后从里面取出value。如果没有value,就调用ThreadLocal提供的初始化方法,初始化一个值。

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

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

我们在开发一个多线程的程序时,往往会使用线程池。而线程池的功能就是线程的复用。那如果线程池和ThreadLocal在一起就可能会造成问题,所以使用完ThreadLocal,显式调用一下remove方法。

ThreadLocal不支持继承性,ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}


关注微信公众号:晏子哒哒

在刮痧技术路上,我们一同成长

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