Jdk源码分析

文章40 |   阅读 10205 |   点赞0

来源:https://yumbo.blog.csdn.net/category_10384063.html

ThreadLocal源码的解读--内存泄漏原理,以及处理方式

x33g5p2x  于2021-12-18 转载在 其他  
字(12.2k)|赞(0)|评价(0)|浏览(300)

第一要提到的内容、ThreadLocal的常规使用( 后面有彩蛋!)

  1. 使用set(T value)方法将值存入ThreadLocalMap,如果map不存在,则进入set()内部的else分支创建一个ThreadLocalMap并将值value存入map,在创建的过程中会将这个map保存到当前线程对象的成员属性上
  2. 然后调用get()方法获取值,get()内部会先从线程对象取出ThreadLocalMap,然后再取出其中的值,并返回
  3. 使用完后我们需要调用remove()方法清除ThreadLocalMap中存的内容,防止内存泄漏
注意点:ThreadLocalMap看起来像一个map,可平常使用的过程中就相当于一个变量而已,只能存一个value,如果同一个线程调用两次set,那么只保存后面的set值!
ThreadLocal起到了什么作用?ThreadLocal的应用场景在哪里?

举个例子:
 在一个web应用中,我们常常用到了数据库连接池(本质就是线程池),一般的业务流程是从controller 到 service 到 dao,而service和dao中就会涉及到事务。事务又和Connection对象有关。
事务的一般流程:
setAutoCommit(false);//设置事务非自动提交
… //prepareStateMent,Connection等操作省略
commit();//提交事务
rollback();//事务回滚

举个事务的例子

A 向 B 转账100元,假设我们分成两个操作:A减100,B加100。(实际上一条SQL就能解决问题)
A减100 和 B加100会用数据库连接池的两个线程来完成,两个线程,如何保证使用的是同一个Connection?

上面的事务流程:
service层得到connection对象通过setAutoCommit(false);开启事务
开启事务后执行完dao层不会像原先那样直接就将数据库中A减100,而是等后面的commit操作一起提交
dao层B加100 等待后面的commit一起提交
service层通过connection对象进行commit(); // 提交事务,A减100,B加100同时完成
如果异常则rollback();进行事务回滚操作。

在上面的事务流程中需要保证service层和dao层的connection对象是同一个,一种最简单的方式是通过方法的传参就可以解决(这种方式高度耦合)而且一般我们通过mybatis等框架操作时,设计方法时并没有要自己创建Connection对象,并传入。而是使用数据库连接池直接通过连接池进行crud操作。这种传参的方式就不讲了,因为早期还没有框架的时候就有很多人这样做。我们谈一谈mybatis框架和数据库连接池。

思考数据库连接池和Mybatis他们之间的工作原理

这篇文章是讲ThreadLocal的,因此你是否可以根据上面的事务流程自己设计出mybatis关于事务的设计呢?
其实我们就是要解决service和dao之间的共享数据Connection对象传递问题
上面的事务流程在框架的流程大致可以这样:
在service层的方法中
 设置Connection对象,开启事务 setAutoCommit(false);
 A.decr(100); // 得到Connection对象,A进行减100
 B.add(100); // 得到Connection对象,B进行加100
 commit(); // 提交事务
 异常则回滚rollback();
如果我们使用ThreadLocal将Connection对象保存进执行service的线程对象(serviceThread)中,然后在A.add()和B.decr()两个线程中只需要传入serviceThread就可以得到共享数据Connection对象进行事务操作

数据库连接池是什么?怎么设计数据库连接池?

数据库连接池本质就是线程池(提前创建好带有Connection对象的线程对象)。
 那么如果我们自己设计数据库连接池,当然只是理论上,线程池可以从serviceThread线程获取ThreadLocalMap,如果内部存有Connection对象则直接使用,如果没有connection对象,我们就用池内创建好的Connection对象,并将这个Connection对象存入serviceThread线程对象的ThreadLocalMap中。这样就保证了前后的Connection对象是同一个。

流程图

第二个要提到的内容,简化源码后的结构(内存泄漏看后面部分)

下面的源码我没有将完整版注释后的ThreadLocal贴出来,只是看下整体结构

public class ThreadLocal<T> {

    private final int threadLocalHashCode = nextHashCode();         // 下一个hash值
    private static final int HASH_INCREMENT = 0x61c88647;           // hash增量
    private static AtomicInteger nextHashCode = new AtomicInteger();// hash值的原子变量

    public ThreadLocal() {} // 构造方法

    /** * get方法、重点阅读 */
    public T get() {
        Thread t = Thread.currentThread();  // 获取当前线程
        ThreadLocalMap map = getMap(t);     // 获取当前线程绑定的ThreadLocalMap
        if (map != null) {                  // 如果map不为null
            ThreadLocalMap.Entry e = map.getEntry(this); // 获取当前线程绑定的数据的封装对象Entry(内部类)
            if (e != null) {                // 如果当前数据不为null
                T result = (T) e.value;     // 获取当前数据绑定的数据,进行了强转为泛型
                return result;              // 返回数据
            }
        }
        return setInitialValue();           // map为null返回null
    }

    /** * set方法、重点阅读 */
    public void set(T value) {
        Thread t = Thread.currentThread();  // 获取当前线程
        ThreadLocalMap map = getMap(t);     // 获取当前线程绑定的 ThreadLocalMap
        if (map != null) {                  // map不为null
            map.set(this, value);           // 给当前线程绑定的map中添加数据,key是当前线程、值是传入的value
        } else {
            createMap(t, value);            // 当前线程没有map,创建一个map将传入的value存入map中并绑定到当前线程对象
        }
    }
    /** * remove方法、 * 移除当前线程绑定的数据, * 每次使用完后就需要进行remove防止内存泄漏!!! */
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread()); // 获取当前线程绑定的map
        if (m != null) {            // 存在map
            m.remove(this);    // 清除当前线程绑定的数据
        }
    }

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {} // 返回一个新的ThreadLocal变量


    /** * 外部使用不到的方法 */
    protected T initialValue() { return null; } // 初始值null
    private static int nextHashCode() { }       // hash值加上0x61c88647并且将加完后的值返回
    private T setInitialValue() {}              // 返回null并设置初始值null和set方法一样的逻辑,只是value为null
    ThreadLocalMap getMap(Thread t) { }         // 得到线程t绑定的ThreadLocalMap
    void createMap(Thread t, T firstValue) {}   // 创建ThreadLocalMap并添加key=t,value=firstValue到map中并绑定到当前线程
    boolean isPresent() {}                      // 是否存有数据,有则返回true、没有则返回false
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {} // 创建ThreadLocalMap,只在构造方法中使用
    T childValue(T parentValue) {}              //
    // ThreadLocal类的扩展,获得规定的初始值Supplier(使用供给型的接口),实际上不需要了解,这个类我们外面使用不到
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
        private final Supplier<? extends T> supplier;
        SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); }
        @Override
        protected T initialValue() { return supplier.get(); }
    }

    /** * */
    static class ThreadLocalMap {
        private static final int INITIAL_CAPACITY = 16; // 初始容量
        private Entry[] table;                          // 存储数据的结构
        private int size = 0;                           // 当前存了多少个数据
        private int threshold;                          // 扩容条件默认 10
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;                               // 真正存到ThreadLocal中的数据
            Entry(ThreadLocal<?> k, Object v) {}        // Entry构造方法
        }

        // 构造方法
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];                            // 创建长度为16的Entry数组
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  // 通过hash值&运算得到索引值
            table[i] = new Entry(firstKey, firstValue);                     // 将数据存入索引值所在的数组中
            size = 1;                                                       // 添加了第一个值,size变为1
            setThreshold(INITIAL_CAPACITY);                                 // 设置扩容值 16*2/3 = 10
        }

        /** * 和上面的构造函数差不多,只不过传入的是一个map * 因此需要讲map中数据存入到线程所绑定的map中 */
        private ThreadLocalMap(ThreadLocalMap parentMap) {}

        /** * 定义的方法用于在构造方法中使用,目的就是给成员变量赋值 */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {}
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {}
        private void resize() {}                            // 扩容
        private void rehash() {}                            // 再hash
        private Entry getEntry(ThreadLocal<?> key) {}       // 根据key获取Entry
        private void setThreshold(int len) {  }             // 设置扩容条件值:threshold = len * 2 / 3;
        private static int nextIndex(int i, int len) {}     // 上一个索引:return ((i + 1 < len) ? i + 1 : 0);
        private static int prevIndex(int i, int len) {}     // 下一个索引:return ((i - 1 >= 0) ? i - 1 : len - 1);
        private void set(ThreadLocal<?> key, Object value) {} // 添加数据
        private void remove(ThreadLocal<?> key) {}          // 移除key所对应的Entry数据
        private int expungeStaleEntry(int staleSlot) {}     // 返回下一个null值得插槽索引
        private boolean cleanSomeSlots(int i, int n) {}     // 如果索引i所对应的数据被移除返回true
        private void expungeStaleEntries() {}               // 删除所有过时元素
    }
}

第三个要提到的内容、弱引用

弱引用的特点是,如果一个对象只存在弱引用,当jvm进行gc操作时就会回收弱引用的对象。
下面是一个案例

import java.lang.ref.WeakReference;

public class WeakReferenceDemo {
    public static void main(String[] args) {
        String helloWorldString = new String("hello world!!!"); // 在堆中根据常量字符串创建一个新的字符串对象
        WeakReference<String> stringWeakReference = new WeakReference<>(helloWorldString);// 创建一个弱引用,将弱引用指向堆中的那个字符串

        /** * 置 null 的作用 * 去除helloWorldString强引用字符串"hello world!!!", * 因为对象一旦被强引用指向,即使内存不够用,宁愿报错也不会被回收该对象,相当于"hello world!!!"原先有两个引用指向这个对象 */
        helloWorldString=null; // 这种情况就是成员属性ThreadLocal对象随着类实例的销毁而销毁(被回收,相当于ThreadLocal=null)
        System.out.println("打印一下弱引用的字符串:"+stringWeakReference.get());//没有进行gc前弱引用能得到对象
        System.gc();//进行垃圾回收
        System.out.println("弱引用的字符串被垃圾回收了,得到的字符串是:"+stringWeakReference.get());

    }
}

执行结果

打印一下弱引用的字符串:hello world!!!
弱引用的字符串被垃圾回收了,得到的字符串是:null

关于 强引用、软引用、弱引用、虚引用的介绍

第四个要提的内容、谈一谈内存泄漏问题

以下面的类为例

class ConnectionThreadLocal {
    ThreadLocal<Connectioon> tl = new ThreadLocal<>();// 这是一个强引用

    public Connectioon getConnection() {
        return tl.get();
    }

    public void setConnection(Connectioon connection) {
        tl.set(connection);
    }

}

在使用ThreadLocal的过程中如果ConnectionThreadLocal对象实例被回收,那么内部的tl也会被回收,导致强引用就消失了,这样只存在弱引用tl(这个弱引用来自带有ThreadLocalMap对象的那个线程)。

保存了ThreadLocalMap的线程如果一直没有死,那么就存在thread-->threadlocals(ThreadLocalMap的实例对象)-->entry-->value导致value一直在内存中被强引用,无法回收,这样就造成了内存的泄漏。
用图表示:

其实关于内存泄漏这种问题时始终都存在的,只要线程一直存活,如果threadlocals成员变量如果没有被置null,那么就会造成内存泄漏。

重点部分、为什么要设计弱引用?

我们看一下Entry的定义

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;                               // 真正存到ThreadLocal中的数据
    Entry(ThreadLocal<?> k, Object v) {}        // Entry构造方法
}

ThreadLocal设计为弱引用的目的是避免内存泄漏,如何避免的呢?

实际上内存泄漏问题是无法避免的,只不过:Josh Bloch and Doug Lea 设计时他们将ThreadLocalMap中的key设置为弱引用有这样一个作用,当内存不足时,会进行gc,弱引用就会失效,key会被gc回收。
 而value则会伴随着ThreadLocal的生命周期一起消亡。而在Thread.exit()方法中就定义了清除ThreadLocalMap的语句threadLocals = null;。threadLocals 就是ThreadLocalMap的一个实例变量,通过ThreadLocal的set会给线程创建这个实例并赋值给这个threadLocals

下面是Thread.exit()的源码:
private void exit() {
    if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
        TerminatingThreadLocal.threadTerminated();
    }
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    /* Aggressively null out all reference fields: see bug 4006245 */
    target = null;
    /* Speed the release of some of these resources */
    threadLocals = null; // 清除threadLocalMap,就是这条语句
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
}

接着上面的文章,不要把注意力转移到源码上了,贴源码只是为了摆事实。

关键点解释:虽然避免不了内存泄漏,但是gc过后会导致key也就是ThreadLocal对象会被回收,而线程我们会重复利用不会进行销毁(线程池),那么当我们使用这个线程执行另一个ThreadLocal对象的set\get操作时,由于原先map中有entry,但是我们取不出entry

map不为空对于源码的逻辑如下

map.set(this,value) 会执行Thread LocalMap的set方法,而this则是新的threadLocal对象

下面这个方法中的注释要认真看,结合我博客内容就会理解ThreadLocal的核心原理

其中避免内存泄漏的那条语句是:replaceStaleEntry(key, value, i); // 翻译过来的意思:取代陈旧Entry
看完我加的注释对于内存泄漏就算彻底搞清楚了。

/** * ThreadLocalMap的set方法 */
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    /** * 看起来这里是运算得到的,实际上key.threadLocalHashCode这个值是固定的 * 因为是通过常量 HASH_INCREMENT 进行原子操作,所以值是固定规律的 * 如果是第一次进来始终都会得到同一个i的值,因此entry是同一个 */
    int i = key.threadLocalHashCode & (len - 1);

    // 遍历数组,相当于遍历map,得到entry
    // 关于内存泄漏,我感觉作者会把泄漏的内存在这次中通过重新赋值的操作去除泄漏的内存
    // 是不是呢,我们看下这个for循环做了哪些事情
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get(); // 弱引用的方式得到ThreadLocal对象
        /** * k == key 判断弱引用对象是否和存储的那个对象相同 * 如果是内存泄漏,而且是我博客中讲的这种重复利用线程的情况 * 那么k 是不等于 key的,也就是false */
        if (k == key) { // 如果相等,说明是同一个ThreadLocal对象进行的set,需要保留后一次更新的值value
            e.value = value; // 将更新的value赋值进去
            return;          // 这种情况就是我博客开头讲到的两次set操作只保留后面的一次,相当一只能存一个值
        }
        /** * 到这里,说明进行了gc,原先的threadLocal被垃圾回收了,也是我前面流程图中的那种情况 * key则是当前threadLocal对象 * k则是原先的entry的弱引用 */
        if (k == null) {  // 因为gc导致弱引用的ThreadLocal对象被回收,这里判空就是这种情况的处理方式
            // 将i这个位置的entry替换成新的key和value。这里entry中旧值的value强引用就会被去除,就可以进行回收了
            replaceStaleEntry(key, value, i); // 翻译过来的意思:取代陈旧Entry
            return;
        }
    }

    // 到这里则说明是第一次进入e==null,直接添加entry进去就可以了,简单
    tab[i] = new Entry(key, value); // 创建了一个Entry对象,将value存入
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

最后彩蛋部分:为什么使用完ThreadLocal后要调用remove方法?

先看下下面两个测试案例代码

案例一、Thread自带一个map的情况
public class ThreadLocalDemo {
    public static void main(String[] args) {

        new Thread(()->{
            Thread thread = Thread.currentThread(); // 执行这里会自带一个map
            System.out.println("hello");
        },"B").start();
    }
}

在hello字符串上加一个断点进行调试,我们观察一些线程变量thread中的成员变量threadLocals

案例二、Thread没有map的情况
public class ThreadLocalDemo {
    public static void main(String[] args) {

        Thread t=new Thread(()->{
            while (true){
                
            }
        },"A");
        t.start();
        System.out.println("hello");
    }
}

同样在hello上加一个断点,观察线程变量 t 的threadLocals

也就是说这个这个map不一定是通过ThreadLocal对象创建的,那么在使用的过程中,如果map已存在,则是直接用这个map进行set添加想要保存的数据value进去
源码截图如下

如果在案例一的代码中,通过ThreadLocal的set方法给线程中添加数据进去后,存是存进去了,但是map不是浪费了空间?还有这个我们用不到的数据我们该如何处理?

关于ThreadLocal的源码,需要注意下面这两个static成员属性

原子类变量private static AtomicInteger nextHashCode = new AtomicInteger();
和hash常量private static final int HASH_INCREMENT = 0x61c88647;

注意这里面的static,说明是属于类的,所有ThreadLocal实例共享的一个数据,这两个属性就是可以得到下一个hash值(实际就是下一个索引的含义)
而nextHashCode每次的增量都是0x61c88647

我们看一下remove()方法做了哪些事情

remove的调用链如下,关键再最后一个方法,其实就是将对应的引用清除

/** * remove方法、 * 移除当前线程绑定的数据, * 每次使用完后就需要进行remove防止内存泄漏!!! */
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread()); // 获取当前线程绑定的map
    if (m != null) {            // 存在map
        m.remove(this);    // key是当前ThreadLocal对象绑定的数据
    }
}
/** * 移除弱引用的ThreadLocal */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table; // 得到entry数组
    int len = tab.length;// 数组长度
    int i = key.threadLocalHashCode & (len - 1);// 得到threadLocal对象key所对应的索引值i
    // 遍历i,应该和set方法有类似的做法,只不过set是进行复写将原引用去除,这里应该是通过null去除
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear(); // 清除弱引用
            expungeStaleEntry(i);// 将索引i的entry的value通过null去除引用
            return;
        }
    }
}
/** * 清除索引为staleSlot的entry,实际就是置null */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;    // 得到entry
    int len = tab.length;   // entry长度
    tab[staleSlot].value = null;// 直接将这个插槽的entry引用的value置null
    tab[staleSlot] = null;      // 将entry也置null
    size--;                     // map去除了一个元素
    Entry e;                    // 一个entry变量
    int i;                      // 一个int变量
    // 上面操作已经将staleSlot所对应的entry的引用清除了
    // 下面这个循环应该是,清除立即回收entry不可用的元素,因为key的hash值设定为final常量(通过这个常量我们得到的索引始终是一个位置)
    // 所以不可能将后面的entry往前移动,下面的for循环功能应该是清除gc导致key为null的所有entry元素
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        // 根据staleSlot索引所对应的entry中的hash值,再通过HASH_INCREMENT进行&运算得到下一个entry索引i,(就是得到staleSlot的下一个元素位置)
        // 判断i这个位置entry是否为null,如果是则说明后面没有元素了就结束循环,循环增量则是再一次调用nextIndex到下一个索引
        // e!=null这样判断后面有没有元素是有缺陷的,因为可能后面还有元素,因为gc导致为中间这个e被清除为null

        /** * 得到key */
        ThreadLocal<?> k = e.get(); // i所对应的entry元素e不为null则尝试得到key,如果gc回收了,那么就是null,如果没有回收则说明需要后移
        // 如果k为null则需要清除这个entry
        if (k == null) {
            // 说明 i 这个位置的key被回收,那么这个位置是一个垃圾,需要清除引用
            e.value = null; // 进行清除引用
            tab[i] = null;  // 清除数组引用entry
            size--;         // 相当于map个数减一
        } else {
            // 到这里则说明,当前entry可能还用的到,需要将这个e移动一下
            int h = k.threadLocalHashCode & (len - 1); // 当前元素的索引
            if (h != i) {       // 计算出的这个索引和当前索引i不等,说明这个数据是随机产生的,因为某些原因线程启动的时候这里会带上entry
                tab[i] = null;  // 将当前非法的e清除
                while (tab[h] != null) // 找到下一个为null的位置
                    h = nextIndex(h, len);// 循环找下一个元素索引,实际上就像是一个圆形的循环,因为h是通过Hash码按位&得到的索引
                tab[h] = e; // 将e这个元素往后移动到这个空位置。
            }
        }
    }
    return i;
}

相关文章