Java-JUC-ThreadLocal

ThreadLocal

  • 提供线程内的局部变量,不同的线程之间不会互相干扰,只在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
  • 特点,应用了弱引用
  1. 线程并发,多线程并发的场景下使用
  2. 传递数据,通过ThreadLocal在同一线程下,不同组件中传递
  3. 线程隔离,每个线程变量都是独立的,不会互相影响

使用

  • 一个线程往ThreadLocal放,另一个线程取不到,有隔离特点
static ThreadLocal<Person> tl = new ThreadLocal<>();
public static void main(String[] args) {
    new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //null
        System.out.println(tl.get());
    }).start();
    new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tl.set(new Person());
        //com.java.threadlocal.Person@4ebf4ac7
        System.out.println(tl.get());
    }).start();
}

各部分关系

  • 一个Thread有一个ThreadLocalMap
  • 一个ThreadLocalMap包含多个Entry对
  • 一个Entry对为一个ThreadLocal对象和value构成
  • 一个ThreadLocal可以作为多个ThreadThreadLocalMapkey
  • 一个Thread只能通过自己的ThreadLocalMap,根据ThreadLocal获取对应的value

  • JDK8后,这种设计方式每个ThreadLocalMap存储的键值对少,每个Thread维护自己的ThreadLocalMap,一个ThreadLocalMap的键值对数量由ThreadLocal决定,而实际开发中,并不是很多,避免哈希冲突

JDK8之间,由ThreadLocal维护一个MapThread-value作为键值对,个数由线程决定

  • Thread销毁之后,ThreadLocalMap也会随之销毁,减少内存使用

和sychronized区别

  • 共同点,都能用于处理多线程并发访问变量的问题
  • sychronized时间换空间,只提供一份变量,让不同线程排队访问,侧重点在于多个线程访问资源的同步
  • ThreadLocal空间换时间,为每个线程都提供一个线程独享的变量,实现同时访问而不互相干扰,侧重点在于每个线程之间的数据隔离

spring事务中的应用

  • 保证所有操作都在一个事务中,每个操作使用的连接都必须是同一个

数据层和服务层的connection是同一个

  • 线程并发的情况下,每个线程只能操作各自的connection
  • 普通解决方案,需要将连接作为参数传入,并且要用synchronized保证线程安全

增加代码耦合度,影响性能

源码实例

  • @Transactional最终调用DataSourceTransactionManager,利用ThreadLocal传递connection
  • doBegin首先检查是否有连接对象,没有则获取一个,并且会设置给newConnectionHolder

  • doBegin会检查是否是新的连接,如果是将新连接通过TransactionSynchronizationManagerThreadLocalMap绑定

  • 设置给resources,以map类型存储,key是数据源,value为连接,说明一个线程,对应的一个数据源,对应一个连接

  • resources实际上就是个ThreadLocal,里面的元素类型为Map<Object, Object>

在MyBatis的应用

  • 关于分页PageHelper,会根据当前数据库连接,选择合适的分页方式
PageHelper.startPage(2, 1);
List<Account> accounts = accountMapper.findAll();
for (Account account : accounts) {
    System.out.println(account);
  • startPage
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<E> page = new Page(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    //当执行过orderBy的时候
    Page<E> oldPage = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    //关键
    setLocalPage(page);
    return page;
}
  • setLocalPage,给ThreadLocalMap设置Page对象
protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

  • PageInterceptor拦截器中,调用intercept,完成所有操作调用afterAll

  • afterAll,将当前线程对应的dialectpage清理,remove
public void afterAll() {
    AbstractHelperDialect delegate = this.autoDialect.getDelegate();
    if (delegate != null) {
        delegate.afterAll();
        this.autoDialect.clearDelegate();
    }

    clearPage();
}
  • getDelegate实际上也是从ThreadLocal获取当前线程的AbstractHelperDialect,应用了代理模式,最终由PageHelper来增强删除
public AbstractHelperDialect getDelegate() {
    return this.delegate != null ? this.delegate : (AbstractHelperDialect)this.dialectThreadLocal.get();
}
  • clearPage调用remove
public static void clearPage() {
    LOCAL_PAGE.remove();
}

Set

  • 主要工作
  1. 设置值,如果没有ThradLocalMap就为其创建
  2. 在实际设置的过程中,如果找到k相等的,就替换;如果找到k==null,就进行一次清理工作,并在清理同时,如果找到k相等的,同样替换,如果没有相等的,就放找到为null的地方
  • 获取到当前线程,放value是放入当前线程对于的map里,mapkey为当前ThreadLocal对象
public void set(T value) {
    //当前线程
    Thread t = Thread.currentThread();
    //获取map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //key - 当前ThreadLocal的实例对象
        map.set(this, value);
    else
        //没有则创建
        createMap(t, value);
}
  • getMap对应的为Thread的成员变量threadLocals,每出现一个线程,就会初始化一个ThreadLocalMap类型的threadLocals,专属于的该线程的map
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • createMap 初始化当前线程对应的ThreadLocalMap
void createMap(Thread t, T firstValue) {
     //当前ThreadLocal的实例对象作为key
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • ThreadLocalMap构造
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //table 为 Entry(继承了WeakReference)数组 INITIAL_CAPACITY 默认 16
    //容量必须是2的整数次幂
    table = new Entry[INITIAL_CAPACITY];
    //线性探测法,找到一个下标
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //设置扩容阈值,当大于它时,需要扩容
    setThreshold(INITIAL_CAPACITY);
}
  • set,实际进行set的操作,在当前线程对应的map中遍历
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //遍历所有的Entry 线性探测法
    for (Entry e = tab[i];
         e != null;
         //实际上是循环遍历 i + 1 < len ? i + 1 : 0
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) { //相等则替换 不相等,就找下一个,此时hash冲突了
            e.value = value;
            return;
        }
        if (k == null) {
            //发现一个为null的key
            //1.第一次遍历做一次整体的清理,并保存第一个为null的地方,防止后续突然增加大量数据
            //2.第二次遍历找跟当前key是否有相等的,有或没有都放到i的位置,原先的位置置null
            //3.清理所有`entry`指向null的下标
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //清理为null的元素
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

get

  • 主要工作
  1. 获取ThreadLocal实例对象对应的值,如果没有就返回null
  2. 如果当前Thread没有ThreadLocalMap为其创建,并将ThreadLocal-null加入
  3. 搜索时,第一次尝试直接命中,如果找不到,尝试遍历搜索,同时清理k == nullEntry
  • get,范型写法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //根据`ThreadLocal`实例对象获得`Entry`
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            //抑制没被使用的警告
            @SuppressWarnings("unchecked")
            //强转
            T result = (T)e.value;
            return result;
        }
    }
    //当前线程没有对应的`map` 或者 没有找到当前key对应的value 返回null
    return setInitialValue();
}
  • 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,遍历搜索,在遍历的同时,清除为nullEntry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) { //没找到 返回null
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null) //找到为null的,直接清除
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
  • setInitialValue,为其创建一个map
private T setInitialValue() {
    //返回null
    T value = initialValue();
    Thread t = Thread.currentThread();
    //一样的,返回`ThreadLocalMap`
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //有map设置当前key
        map.set(this, value);
    else
        //否则为其创建一个
        createMap(t, value);
    //实际上就是null
    return value;
}
  • initialValue,实际上仅是返回null,可以继承ThreadLocal重写此方法,自定义返回初始值
  • 不了解的可以看这篇博客:https://www.cnblogs.com/pxza/
protected T initialValue() {
    return null;
}

remove

  • remove,存在map,找到key删除
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
  • ThreadLocalMapremove,遍历map进行删除
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();
            //清理这个Entry的同时,做一次整体清理
            expungeStaleEntry(i);
            return;
        }
    }
}

Entry

  • Entry继承WeakReference,实际上是指向ThreadLocal实例对象的虚引用
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

为什么使用弱引用

  • set/get方法中,会对knull进行判断并清理
  • 如果使用强引用,当不需要使用当前ThreadLocal,将当前ThreadLocal的实例对象置为空即可被回收,但是在ThreadLocalMap中的Entry仍然指向当前ThreadLocal,无法被回收,会产生内存泄漏,只有ThreadMap能被回收,才能回收
  • 如果是弱引用,将ThreadLocal的生命周期和Thread解绑,只需要把当前外部使用的ThreadLocal的实例对象置为空即可,内部Entry指向的为弱引用,只要GC就会被回收,但是Entry中的value仍然存在,被Entry对象指向,无法被回收,也会产生内存泄漏

在不使用当前Entry时,需要tl.remove();,调用get/set中仍然会remove,但是存在长时间不调用get/set的情况

扫描二维码关注公众号,回复: 13713210 查看本文章
  • 当线程来自于线程池,在归还线程的时候,ThreadLocalMap没有被清理掉,会影响下次使用,并导致空间越来越大

内存泄漏

  • 真实原因跟Entry是否是弱引用没有关系,根源是使用完ThreadLocal没有及时remove,导致Map越来越大
  1. 没有手动删除Entry
  2. 线程一直存在,ThreadLocalMap生命周期跟Thread一样
  • 使用弱引用,避免ThreadLocalMap中仍指向ThreadLocal无法被回收
  • 使用完毕后,要及时remove,防止Entry指向的value不能被及时回收

扩容

  • setThreshold,初始化为2/3
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
  • rehash,先清理
private void rehash() {
    //清掉空的位置
    expungeStaleEntries();
    //如果清空后,仍然大于 3/4,扩容
    if (size >= threshold - threshold / 4)
        resize();
}
  • resize真正扩容,复制一份,扩容两倍
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //两倍
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    for (int j = 0; j < oldLen; ++j) {
        //复制一份
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
}

哈希冲突

  • 由于ThreadLocalMap本身就需要不断的进行整体遍历remove,可以结合开放地址法,解决哈希冲突
  • 解决哈希冲突的核心
  • https://www.cnblogs.com/pxza/
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  • 相关部分
private final int threadLocalHashCode = nextHashCode();
//Integer的原子操作,静态方法,从0开始,所以每个实例对象的哈希值都是不同的
private static AtomicInteger nextHashCode = new AtomicInteger();
//魔数,跟斐波那契数列有关,主要为了让哈希码能够均匀的分布在2的n次方数组内,不容易堆积在一起
private static final int HASH_INCREMENT = 0x61c88647;
//
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
  • 相当于取模运算,hashcode % size
& (INITIAL_CAPACITY - 1)
  • 线性探测法 nextIndex ((i + 1 < len) ? i + 1 : 0);
  1. 一次探测下一个地址,知道有空的地址后插入,若整个空间都找不到空的地址会溢出
  2. 如果当前长度为16,计算出来的i为14,此时tab[14]上有值,且key不相等,发生了hash冲突,那就+1找下一个,循环遍历

猜你喜欢

转载自blog.csdn.net/qq_38082146/article/details/123452359