ThreadLocal面试 [20]

1.ThreadLocal是什么,有什么特性

ThreadLocal是线程变量,ThreadLocal中设置的变量属于该线程,该变量对于其他线程是隔离的。ThreadLocal为每个变量创建了一个副本变量,存储在线程中,每个线程可以访问内部的副本变量

特性:
1.线程隔离:不同的线程之间线程隔离
2.并发:ThreadLocal是发生在并发场景下的
3.传递数据:同一线程之间的变量是可以互相传递的。比如Dao和service的线程的变量

2.ThreadLocal底层的数据结构

在这里插入图片描述

3.ThreadLocal的Map的初始值,加载因子是多少

16,2/3,这里说明一下,concurrenthashmap和hashmap的加载因子都是3/4,这是根据泊松分布计算出来的。太大造成哈希冲突,太小内存使用率不高

4.ThreadLocal底层的Hash算法是什么

黄金分割数算法
一个常数为HASH_INCREMENT
可以做到均匀分布

    public static final int HASH_INCREMENT = 0x61c88647;
    public static void main(String[] args){
        int hashCode = 0;
        int bucketIndex;
        int length = 16;
        for (int i = 0; i < length; i++) {
            hashCode = HASH_INCREMENT * i + HASH_INCREMENT;
            bucketIndex = hashCode & (length - 1);
            System.out.println(bucketIndex);
        }

    }

均匀分布那么哈希冲突会低

5.ThreadLocal如何解决hash碰撞

开放地址发:线性探测,Hashmap使用的链地址法解决

在这里插入图片描述

在这里插入图片描述

6.ThreadLocal的扩容机制

1 private T setInitialValue() { //源码
2 map.set(this, value);//源码
3 if (!cleanSomeSlots(i, sz) && sz >= threshold)//源码,cleanSomeSlots使用expungeStaleEntry(),提前移走过期Entry
4 rehash();//源码
5 expungeStaleEntries();//源码,使用expungeStaleEntry()
6 if (size >= threshold - threshold / 4)//源码,10-10/4=8,在清理过期Entery后如果长度大于等于8,则进行扩容,
7 resize();//源码, int newLen = oldLen * 2;如果长度是8,则会扩容到16,到原来的2倍
8 setThreshold(newLen);//源码,新的门限是新长度的2/3
2/3 * 3/4 = 1/2 ,即长度大于等于原长度的1/2时,会扩容到原来的2倍

在清理过期Entery后如果长度大于等于原长度的2/3时会进行rehash,再次清理过期Entery后如果长度大于等于原长度的1/2时会进行扩容。

过期Entry的意思就是ThreadLocal totalMap 该变量为空,里面没有内容了

7.ThreadLocal的get方法的流程

    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     *
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	初始化 : 有两种情况有执行当前代码
        	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
        	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

    /**
     * 初始化
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

​ A. 首先获取当前线程, 根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D

​ C. 如果e不为null,则返回e.value,否则转到D

​ D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

8.ThreadLocal的Key是强还是软引用

弱引用,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

9.ThreadLocal的Key可能过期吗

不会过期,随着垃圾收集器的回收而回收

10.ThreadLocal的set方法流程

  /**
     * 设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    }

 /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     *
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

A. 首先获取当前线程,并根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

​ C. 如果Map为空,则给该线程创建 Map,并设置初始值

11.如何避免ThreadLocal内存泄露

使用完ThreadLocal,调用其remove方法删除对应的Entry

使用完ThreadLocal,当前Thread也随之运行结束

12.ThreadLocal的应用场景

1.数据库连接池

2.HTTP Cookie

因为javaee三层架构里面,如果使用事务,要在service层里面进行开启事务(因为处理逻辑业务都是service层),然后又因为如果我们要使用事务,那么就必须保证执行sql语句的connection连接和开启事务的connection连接都要保持是同一个对象,所以我们要确保在service层和dao层的两个connection连接都是同一个,但是怎么保证connection连接对象都是同一个呢
一个是通过方法传参的方式进行数据的一层一层的传递,但是这样不好,因为你把架构分成三层的目的就是为了数据的处理和逻辑业务的处理分离开来(就是dao层处理数据,service层处理业务),connection连接对象我们应该是在service层出现的,但是你却放到了dao层,这样数据的处理和逻辑业务的处理没有分离开来,javaee的三层开发就没有他的效果了,所以这一种方式的解决方法不好,所以我们就通过ThreadLocal的方式来存储这个connection对象,这样就能够保证在service层和dao层的数据保证一致了

13.ThreadLocal和Synchronized的区别

在这里插入图片描述

14.ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
在这里插入图片描述

    /**
     * 初始容量 —— 必须是2的整次幂
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放数据的table,Entry类的定义在下面分析
     * 同样,数组长度必须是2的整次幂。
     */
    private Entry[] table;

    /**
     * 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
     */
    private int size = 0;

    /**
     * 进行扩容的阈值,表使用量大于它的时候进行扩容。
     */
    private int threshold; // Default to 0
    

Entry

/*
 * Entry继承WeakReference,并且用ThreadLocal作为key.
 * 如果key为null(entry.get() == null),意味着key不再被引用,
 * 因此这时候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;
    }
}

15.为什么用弱引用

因为ThrealLocalMap是以ThreadLocal为key,存放的值为value的map,所有ThreadLocal可以达到线程隔离的效果

如果是强引用,就不会被gc掉,造成内存泄露
如果是弱引用,大概率会减少内存泄露的问题.就可以gc后指向key的引用为null

虽然是弱引用,但是v指向的value是需要ThreadLocalMap调用get,set时候发现key为null才会回收整个entry,value。所以不会百分之百解决内存泄露,所以需要手动remove方法删除。

16.需要注意的问题

1.ThreadLocal不解决线程共享问题

2.适用于线程隔离且在方法间共享

3.通过隐式的在不同线程内创建独立实例副本避免了实例线程的安全问题

4.每一个线程有一个只属于自己的Map并维护ThreadLocal对象与具体实例的映射,这个Map由于只被持有它的线程使用,所以不存在线程安全问题,不用加锁

5.set,get,remove都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry方法回收null的Entry,防止内存泄露,数据安全加固的方法

17.value释放的时机

get的时候如果发现key被回收的话则会释放value,然后把新的get的key放入,value为null

在这里插入图片描述

set的时候如果发现key为null的时候,并且会将临近的一些key为null的也清理,为启发式的扫描

在这里插入图片描述
在这里插入图片描述
remove,一般使用静态变量ThreadLocal,所以一般不会使用get,set去去除,所以只能使用remove去除

猜你喜欢

转载自blog.csdn.net/qq_43141726/article/details/120243402