前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【原创】Java并发编程系列30 | ThreadLocal

【原创】Java并发编程系列30 | ThreadLocal

作者头像
java进阶架构师
发布2020-08-11 11:16:00
2030
发布2020-08-11 11:16:00
举报
文章被收录于专栏:Java进阶架构师Java进阶架构师

线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,而大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。这篇文章介绍另种解决线程安全的思路——ThreadLocal:

  1. 介绍
  2. 使用
  3. 源码
  4. 内存泄露
  5. 总结

1. 介绍

线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。

ThreadLocal提供了另一种解决思路,让每个线程拥有自己私有的内存空间,将线程私有的数据存入这个私有空间内,线程与线程之间相互隔离,这样就不会有线程安全问题。

数据结构

每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。

threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。

2. 使用

ThreadLocal核心方法:

get():返回当前线程副本中该ThreadLocal对应的值。

initialValue():返回当前线程副本中的该ThreadLocal对应对应的“初始值”。

remove():移除当前线程副本中该ThreadLocal对应的值。

set(T value):当前线程副本中该ThreadLocal对应的值为value。

使用举例:

每个线程保存一个私有的int值count,5个线程count从0加到10,线程之间互不影响。

代码语言:javascript
复制
/**
 * 每个线程保存一个私有的int值count
 * 5个线程count从0加到10,线程之间互不影响
 */
public class ThreadLocalDemo {
    private static ThreadLocal<Integer> countLocal = new ThreadLocal<Integer>(){
        public Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args){
        for (int i = 1; i <= 5; i++) {
            new Thread("Thread_" + i) {
                public void run() {
                    for (int j = 1; j <= 10; j++) {
                        countLocal.set(countLocal.get() + 1);
                        System.out.println(getName() + ": count=" + countLocal.get());
                    }
                };
            }.start();
        }
    }
}

输出结果如下:

代码语言:javascript
复制
Thread_3: count=1
Thread_5: count=1
Thread_5: count=2
Thread_4: count=1
Thread_4: count=2
Thread_2: count=1
Thread_2: count=2
Thread_2: count=3
Thread_2: count=4
Thread_1: count=1
Thread_2: count=5
Thread_4: count=3
Thread_5: count=3
Thread_5: count=4
Thread_3: count=2
Thread_5: count=5
Thread_5: count=6
Thread_5: count=7
Thread_5: count=8
Thread_4: count=4
Thread_4: count=5
Thread_4: count=6
Thread_2: count=6
Thread_2: count=7
Thread_2: count=8
Thread_2: count=9
Thread_1: count=2
Thread_2: count=10
Thread_4: count=7
Thread_5: count=9
Thread_3: count=3
Thread_3: count=4
Thread_5: count=10
Thread_4: count=8
Thread_1: count=3
Thread_4: count=9
Thread_3: count=5
Thread_4: count=10
Thread_1: count=4
Thread_3: count=6
Thread_1: count=5
Thread_3: count=7
Thread_1: count=6
Thread_3: count=8
Thread_1: count=7
Thread_3: count=9
Thread_1: count=8
Thread_3: count=10
Thread_1: count=9
Thread_1: count=10

可以看到,即使5个线程并发执行,但是每个线程内部的count都是按1-10的顺序相加的。

3. 源码

3.1 数据结构

每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。

threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。

代码语言:javascript
复制
/**
 * 线程局部变量threadLocals为ThreadLocal.ThreadLocalMap类型
 */
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

/**
 * ThreadLocal$ThreadLocalMap 散列表结构
 * key=ThreadLocal value=Object
 */
static class ThreadLocalMap {
    private Entry[] table;
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

3.2 get()

在理解的ThreadLocal的存储结构之后,再看get()和set()方法就很简单了。

get():
  1. 获取当前线程thread。
  2. 获取当前线程thread.threadLocals,threadLocals是map结构。
  3. map的key是ThreadLocal类型,获取map中当前threadLocal对应的value值。
  4. 如果map=null,就创建map并赋初值。
代码语言:javascript
复制
public T get() {
    // 获取当前线程私有的map thread.threadLocals 
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 获取map的value值
    if (map != null) {
        // map的key是ThreadLocal类型
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map=null,初始化map,下文有讲解
    return setInitialValue();
}

/**
 * 返回ThreadLocalMap类型的thread.threadLocals 
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


/**
 * 初始化value,map=null时会初始化map
 */
private T setInitialValue() {
    T value = initialValue();// 初始值
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);// 将初始值存入map
    else
        createMap(t, value);// map=null时初始化map
    return value;
}

/**
 * 返回map中value的初始值
 * 默认为null,一般需要重写该方法以获得非null值
 */
protected T initialValue() {
    return null;
}
set():
  1. 获取当前线程thread。
  2. 获取当前线程thread.threadLocals,threadLocals是map结构。
  3. map的key是ThreadLocal类型,设置map中当前threadLocal对应的value值。
  4. 如果map=null,就创建map并赋值。
代码语言:javascript
复制
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

4. 注意问题

4.1 每个线程最好只存一个ThreadLocal

线性探测解决Hash冲突:根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

如下,ThreadLocalMap.set()方法:

代码语言:javascript
复制
private void set(ThreadLocal<?> key, Object value) {
    // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
    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)]) {
        ThreadLocal<?> k = e.get();
        // key 存在,直接覆盖
        if (k == key) {
            e.value = value;
            return;
        }
        /*
         * key=null而value!=null,因为key是弱引用
         * 用新的key-value将旧的null-value替换掉
         */
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 清除陈旧的Entry(key == null)
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。

4.2 引发内存泄露

ThreadLocal使用中会有内存泄露问题。

ThreadLocalMap的key是弱引用,而Value是强引用。源码如下:

代码语言:javascript
复制
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

ThreadLocalMap的key是弱引用,发生GC时弱引用key会被回收;而value是强引用,GC时不会被回收。

所以ThreadLocalMap中就会出现key为null的Entry,因为key为null,这Entry是不能被访问到的。如果当前线程一直没结束的话,一直有这个引用链:Thread --引用--> ThreaLocalMap --引用--> Entry --引用--> value,这个value就无法被回收,导致内存泄露。

解决:

  1. ThreadLocalMap的set()、cleanSomeSlots()等方法中都做了相应处理,检查存在key=null而value!=null的Entry就会删掉;
  2. 在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

5. 总结

同步机制 VS ThreadLocal

同步机制是通过控制线程访问共享对象的顺序,类似“时间换空间”,同一时刻共享对象只能被一个线程访问造成整体上响应时间增加,但是对象只占有一份内存。

而ThreadLocal是为每一个线程分配一个该对象,各用各的互不影响。类似“空间换时间”,为每个线程都分配了一份对象,自然而然内存使用率增加,但整体上时间效率要增加很多。

ThreadLocal存储结构

每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。

threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。

注意问题

ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。

由于ThreadLocalMap的key是弱引用,ThreadLocal使用中会有内存泄露问题。在使用完ThreadLocal之后调用remove方法删除值,可避免内存泄露问题。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java进阶架构师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 介绍
    • 数据结构
    • 2. 使用
      • 使用举例:
      • 3. 源码
        • 3.1 数据结构
          • 3.2 get()
            • get():
            • set():
        • 4. 注意问题
          • 4.1 每个线程最好只存一个ThreadLocal
            • 4.2 引发内存泄露
              • 同步机制 VS ThreadLocal
              • ThreadLocal存储结构
              • 注意问题
          • 5. 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档