ThreadLocal面试题 Java并发面试高频 附带源码分析

29 篇文章 0 订阅
5 篇文章 0 订阅

作为Java面试环节中必不可少的并发相关内容,ThreadLocal的地位是很高的。

其可以和HashMap相关联,可以牵涉到static关键字的作用,可以引入Java引用的概念,由于Key是弱引用,而对应的value为强引用,进而可以继续引入一个垃圾回收与OOM的问题。

总之,对ThreadLocal有一个较为清晰的认识,有利于并发场景下的编程与故障排查,也有利于认识Java经典的数据结构。


一句话概括:

每个线程都有一个ThreadLocalMap对象。这个Map里面存了所有线程变量中保存的数据。它实现了数据的隔离性,放入 ThreadLocal中的数据,每个线程分别持有一份副本,相互之间互不影响。

具体的保存方式为:

以ThreadLocal对象的弱引用作为key,ThreadLocal里”存放“的数据作为value,放在该Map里。

ThreadLocal是什么?

ThreadLocal是Java中的一个线程封闭技术,它可以在多线程环境下为每个线程提供一个独立的变量副本,从而保证线程安全。ThreadLocal通常用来解决线程安全问题,比如SimpleDateFormat。SimpleDateFormat是线程不安全的,因为它的parse()方法中有一个Calendar对象是全局共享的,如果在多线程环境中调用parse()方法,就可能会导致线程安全问题。这时,我们可以使用ThreadLocal来解决这个问题,为每个线程提供一个独立的SimpleDateFormat对象。

ThreadLocalMap是什么?

ec23514082e414a2914dc28bb9b94c44.jpeg

每个Thread对象都有一个ThreadLocalMap对象,它是一个键值对映射,键是ThreadLocal对象,值是ThreadLocal对象对应的值。ThreadLocalMap是线程封闭的,即每个线程都有自己的ThreadLocalMap对象,它可以保证线程安全。

两者的关系用示意图表示:

e5f775103d47f67910aee8c64ded0d30.png

ThreadLocalMap在ThreadLocal中定义,是一个静态内部类。

static class ThreadLocalMap {
        static final ThreadLocalMap NOT_SUPPORTED = new ThreadLocalMap();
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;
        private int size = 0;
        private int threshold;

        private void setThreshold(int len) {
            this.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 ThreadLocalMap() {
        }
        // 其他
}

可以看到其属性值有threashold阈值以及一个Entry[] table来存储内容。每个线程线程都有一个ThreadLocalMap,来存储其对应的ThreadLocal值,具体的存取操作将在后面进行解释。

同时注意,该Entry类为ThreadLocalMap的静态内部类,具体源码如下:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                this.value = v;
            }
        }

其继承了弱引用,k是弱引用,value为一个对象的强引用。 

ThreadLocalMap的Entry是WeakReference的子类,这样能保证线程中的映射表的每一个Entry可以被垃圾回收,而不至于发生内存泄露。因为ThreadLocal作为全局的Key,其生命周期很可能比一个线程要长,如果Entry是一个强引用,那么线程对象就一直持有ThreadLocal的引用,而不能被释放。随着线程越来越多,这些不能被释放的内存也就越来越多。

使用ThreadLocal需要注意什么问题?

内存泄漏问题

ThreadLocalMap中的Entry对象是使用弱引用来引用ThreadLocal对象的,而ThreadLocal对象是使用强引用来引用它的值的,这就可能导致内存泄漏问题。如果ThreadLocal对象没有被及时地回收,就会导致ThreadLocalMap中的Entry对象中的key为null,而value不为null,这就是内存泄漏问题。

如 Java 文档所述,弱引用通常用于实现规范化映射。如果映射只包含特定值的一个实例,则称为规范化映射。它没有创建新对象,而是在映射中查找现有对象并使用它。

初始化问题

ThreadLocal对象中的initialValue()方法可以用来初始化ThreadLocal对象对应的值。这个方法会在第一次调用get()方法时被调用,如果没有调用set()方法或者没有给ThreadLocal对象赋初值,就会返回initialValue()方法的返回值。需要注意的是,initialValue()方法只会在第一次调用get()方法时被调用,之后再调用get()方法时,不会再次调用initialValue()方法。

保证ThreadLocal的唯一性

ThreadLocal作为映射表的Key,需要具备唯一的标识,每创建一个新的ThreadLocal,这个标识就变的跟之前不一样了。 如何保证每一个ThreadLocal的唯一性呢?

public class ThreadLocal<T> {
    private static final int HASH_INCREMENT = 0x61c88647;
    // 每一个ThreadLocal对象的HashCode都不一样
    private final int threadLocalHashCode = nextHashCode();
    private static int nextHashCode() {
        // 下一个HashCode,是在已有基础上增加0x61c88647
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

 ThreadLocal内部有一个名为threadLocalHashCode的变量,每创建一个新的ThreadLocal对象,这个变量的值就会增加0x61c88647。 正是因为有这么一个神奇的数字,它能够保证生成的Hash值可以均匀的分布在0~(2^N-1)之间,N是数组长度。 更多关于数字0x61c88647,可以参考Why 0x61c88647?

源码分析

ThreadLocal的构造方法

public class ThreadLocal<T> {
    public ThreadLocal() {}
}

ThreadLocal只存在一个默认的构造方法,空函数,空过程。

ThreadLocal的get()方法

以JDK1.8为例,分为public与private结合的两部分

    public T get() {
        return this.get(Thread.currentThread());
    }

    private T get(Thread t) {
        ThreadLocalMap map = this.getMap(t);
        if (map != null) {
            if (map == ThreadLocal.ThreadLocalMap.NOT_SUPPORTED) {
                return this.initialValue();
            }

            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = e.value;
                return result;
            }
        }

        return this.setInitialValue(t);
    }

ThreadLocal的get()方法会首先获取当前线程对象,然后获取当前线程对象的ThreadLocalMap对象,然后通过ThreadLocalMap对象获取ThreadLocal对象对应的值。如果ThreadLocalMap对象为null,或者ThreadLocalMap对象中没有ThreadLocal对象对应的值,就会调用setInitialValue()方法来获取ThreadLocal对象对应的初始值。

setInitialValue()方法

    private T setInitialValue(Thread t) {
        T value = this.initialValue();
        ThreadLocalMap map = this.getMap(t);

        assert map != ThreadLocal.ThreadLocalMap.NOT_SUPPORTED;

        if (map != null) {
            map.set(this, value);
        } else {
            this.createMap(t, value);
        }

        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal<?> ttl = (TerminatingThreadLocal)this;
            TerminatingThreadLocal.register(ttl);
        }

        return value;
    }


    protected T initialValue() {
        return null;
    }

ThreadLocal的setInitialValue()方法会首先调用initialValue()方法获取ThreadLocal对象对应的初始值,然后将ThreadLocal对象和它对应的初始值存入ThreadLocalMap对象中。简单理解就是附一个空值,让其key能够在Map中进行检索

remove()方法

    public void remove() {
        this.remove(Thread.currentThread());
    }

    private void remove(Thread t) {
        ThreadLocalMap m = this.getMap(t);
        if (m != null && m != ThreadLocal.ThreadLocalMap.NOT_SUPPORTED) {
            m.remove(this);
        }

    }

ThreadLocal的remove()方法会从当前线程的ThreadLocalMap对象中删除ThreadLocal对象。可理解为,找到这个线程对应的key,把它删了。

getMap()方法

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

ThreadLocal的getMap()方法会获取当前线程的ThreadLocalMap对象。

ThreadLocalMap的set()方法

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = this.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.refersTo(key)) {
                    e.value = value;
                    return;
                }

                if (e.refersTo((Object)null)) {
                    this.replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++this.size;
            if (!this.cleanSomeSlots(i, sz) && sz >= this.threshold) {
                this.rehash();
            }

        }

ThreadLocalMap的set()方法会将ThreadLocal对象和它对应的值存入ThreadLocalMap对象中。ThreadLocalMap是一个Entry[]数组,也就是一个链表数组,每个Entry对象都包含一个ThreadLocal对象和它对应的值。ThreadLocal对象是使用弱引用来引用Entry对象的,而Entry对象是使用强引用来引用它的值的。

ThreadLocaMap的remove()方法

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = this.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.refersTo(key)) {
                    e.clear();
                    this.expungeStaleEntry(i);
                    return;
                }
            }

        }

ThreadLocalMap的remove()方法会从ThreadLocalMap对象中删除ThreadLocal对象对应的值。它会首先获取ThreadLocal对象在Entry[]数组中的索引,然后在Entry[]数组中查找ThreadLocal对象,如果找到了,就将Entry对象的值清空,然后从Entry[]数组中删除Entry对象。

ThreadLocalMap的replaceStaleEntry()方法

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;
            tab[staleSlot].value = null;
            return;
        }
        if (k == null)
            break;
    }
    tab[staleSlot].value = null;
    tab[i] = new Entry(key, value);
}

ThreadLocalMap的replaceStaleEntry()方法会将ThreadLocal对象对应的Entry对象替换为新的Entry对象。

 

小节

ThreadLocal是Java中的一个线程封闭技术,它可以在多线程环境下为每个线程提供一个独立的变量副本,从而保证线程安全。ThreadLocalMap是Java中的一个线程封闭对象,它是一个键值对映射,键是ThreadLocal对象,值是ThreadLocal对象对应的值。在使用ThreadLocal时需要注意内存泄漏问题和初始化问题。ThreadLocal的源码可以分为ThreadLocal的构造方法、get()方法、setInitialValue()方法、remove()方法、getMap()方法和ThreadLocalMap的set()方法、remove()方法、replaceStaleEntry()方法等几个部分。


参考资料:

阿里二面:能讲讲 ThreadLocal 的原理吗? - 知乎 (zhihu.com)

Java ThreadLocal的演化、实现和场景 (duanqz.github.io)

Weak References in Java | Baeldung

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值