ThreadLocal的使用与解析

ThreadLocal

​ 用来解决对某一变量访问的冲突问题。ThreadLocal维护变量时,为每个使用该变量的线程提供一个独立的变量副本。因此多线程访问变量时,不会相互影响,因此不存在多线程安全问题,也不影响程序的执行能力

​ 由于在每个线程中都创建了副本,所以需要考虑它对资源的消耗。

​ 线程的局部变量, 是每一个线程所单独持有的**,其他线程不能对其进行访问, **通常是类中的 private static 字段。对该字段初始值的一个拷贝,将状态与某一个线程相关联

方法使用详解

// 用来获取ThreadLocal在当前线程中保存的变量副本
public T get() { } 
//	set()用来设置当前线程中变量的副本
public void set(T value) { } 
//remove()用来移除当前线程中变量的副本
public void remove() { }
//用来在使用时进行重写的
protected T initialValue() { } 

​ Thread内部通过名为threadLocals的变量(ThreadLocalMap类)来维护ThreadLocal变量表

​ threadLocals为每个线程储存自身的ThreadLocal变量,最小单位是Entry,使用ThreadLocal变量为key,ThreadLocal要保存的副本变量作为value。一个线程中,可能存在多个ThreadLocal变量

基本使用

public class ThreadLocalTest {
    public static void main(String[] args) throws InterruptedException {
        final ThreadLocalTest test = new ThreadLocalTest();
		//在主线程进行了set操作
        test.set();
        System.out.println(test.getLong());		//1
        System.out.println(test.getString());	//main

        Thread thread1 = new Thread() {
            public void run() {
                // 将ThreadLocal变量存储到该线程的threadLocals中
                test.set(); 
                System.out.println(test.getLong());		//9
                System.out.println(test.getString());	//Thread-0
            };
        };
        thread1.start();
        thread1.join();
        System.out.println(test.getLong());		//1
        System.out.println(test.getString());	//main
    }

    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();
    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }
    public long getLong() {
        return longLocal.get();
    }
    public String getString() {
        return stringLocal.get();
    }
}

应用场景

数据库连接

Class A implements Runnable{
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        public Connection initialValue() {
                return DriverManager.getConnection(DB_URL);
        }
    };

    public static Connection getConnection() {
           return connectionHolder.get();
    }
}

Session管理

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

源码分析

https://blog.csdn.net/qq32933432/article/details/80212873

https://blog.csdn.net/v123411739/article/details/78698834

ThreadLocal的定义

// hash code
private final int threadLocalHashCode = nextHashCode();
 
// AtomicInteger类型,从0开始
private static AtomicInteger nextHashCode =
    new AtomicInteger();
 
// hash code每次增加1640531527 
private static final int HASH_INCREMENT = 0x61c88647;
 
// 下一个hash code
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//ThreadLocal的hashcode()从0开始,每新建一个ThreadLocal,对应的hashCode加0x61c88647

ThreadLocalMap的定义

//ThreadLocalMap为自定义hash映射,仅用于维护线程本地变量值
//ThreadLocal的内部类,内部维护一个Entry数组,数组的key为ThreadLocal,value为ThreadLocal
//每个线程都有一个ThreadLocalMap类型的threadLocals变量
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

set()

public void set(T value) {
    Thread t = Thread.currentThread();
	// 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t); 
    // this为调用该方法的ThreadLocal对象
    if (map != null) 
        map.set(this, value);
    // 创建一个新的ThreadLocalMap, 并新建一个Entry放入该ThreadLocalMap
    else
        createMap(t, value);    
}
//相关引用
ThreadLocalMap getMap(Thread t) {
	// 返回线程t的threadLocals属性
    return t.threadLocals;	
}
//set()
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
	// 计算出索引的位置
    int i = key.threadLocalHashCode & (len-1);  
 
    // 从索引位置开始遍历,由于不是链表结构,因此通过nextIndex方法来寻找下一个索引位置
	// 当遍历到的Entry为空时结束遍历	
    for (Entry e = tab[i];
         e != null;	
         e = tab[i = nextIndex(i, len)]) {  
		 // 拿到Entry的key,也就是ThreadLocal
        ThreadLocal<?> k = e.get();   
 
        // 如该Entry的key和传入的key相等, 则用传入的value替换掉原来的value  
        if (k == key) {
            e.value = value;
            return;
        }
 
        // 该Entry的key为空, 则代表该Entry需要被清空, 
        if (k == null) {
        	// 该方法会继续寻找传入key的安放位置, 并清理掉key为空的Entry  
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 放在数组的最后一个位置
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // cleanSomeSlots()用来清理掉key为空的Entry,如返回true,则至少清理了1个元素, 则不需要扩容;如返回false则判断sz是否大于阈值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
		// 扩容
        rehash();   
}
//replaceStaleEntry()
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
	// 清除元素的开始位置(记录索引位置最前面的)
    int slotToExpunge = staleSlot;	
    // 向前遍历,直到遇到Entry为空
    for (int i = prevIndex(staleSlot, len); 
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
			// 记录最靠前的一个key为null的索引位置
            slotToExpunge = i;	
			
	// 向后遍历,直到遇到Entry为空
    for (int i = nextIndex(staleSlot, len); 
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
 
        // 如该Entry的key和传入的key相等, 则将传入的value替换掉该Entry的value
        if (k == key) {
            e.value = value;
 
            // 将i位置和staleSlot位置的元素对换
            tab[i] = tab[staleSlot];	
            tab[staleSlot] = e;
 
            if (slotToExpunge == staleSlot) 
                slotToExpunge = i;
            // 从slotToExpunge位置开始清除key为空的Entry
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
 
        // 如果第一次遍历到key为null的元素,并且上面的向前寻找key为null的遍历没有找到,
        // 则将slotToExpunge设置为当前的位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
 
    // 如果key没有找到,则新建一个Entry,放在staleSlot位置
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
 
    // 如果slotToExpunge!=staleSlot,代表除了staleSlot位置还有其他位置的元素需要清除
    // 需要清除的定义:key为null的Entry,调用cleanSomeSlots方法清除key为null的Entry
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
//cleanSomeSlots()
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
		// 下一个索引位置
        i = nextIndex(i, len);	
        Entry e = tab[i];
		// 遍历到key为null的元素
        if (e != null && e.get() == null) {	
			// 重置n的值
            n = len;	
			// 标志有移除元素
            removed = true;	
			// 移除i位置及之后的key为null的元素
            i = expungeStaleEntry(i);	
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

get()

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    	//获取对应的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);  
		//找到对应的值
        if (e != null) {       
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
	//该线程的ThreadLocalMap为空则初始化一个
    return setInitialValue();   
}
//setInitialValue()
private T setInitialValue() {
	// 默认null,需要用户自己重写该方法,
    T value = initialValue();	
	// 当前线程
    Thread t = Thread.currentThread();	
	// 拿到当前线程的threadLocals
    ThreadLocalMap map = getMap(t);	
    // threadLocals不为空则将当前的ThreadLocal作为key,null作为value,插入到ThreadLocalMap
    if (map != null)	
        map.set(this, value);
    // threadLocals为空则调用创建一个ThreadLocalMap,并新建一个Entry放入该ThreadLocalMap, 
    // 调用set方法的ThreadLocal和value作为该Entry的key和value
    else
        createMap(t, value);
    return value;
}
//getEntry()
private Entry getEntry(ThreadLocal<?> key) {
	//根据hash code计算出索引位置 
    int i = key.threadLocalHashCode & (table.length - 1);    
    Entry e = table[i];
    // 如果该Entry的key和传入的key相等, 则为目标Entry, 直接返回
    if (e != null && e.get() == key)    
        return e;
    // 否则,e不是目标Entry, 则从e之后继续寻找目标Entry
    else
        return getEntryAfterMiss(key, i, e);
}
//getEntryAfterMiss()
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
 
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到目标Entry,直接返回
        if (k == key)  
            return e;
        // 调用expungeStaleEntry清除key为null的元素
        if (k == null)
            expungeStaleEntry(i);
        else
			// 下一个索引位置
            i = nextIndex(i, len); 
		// 下一个遍历的Entry			
        e = tab[i]; 
    }
    return null;    
}

remove()

public void remove() {
	// 获取当前线程的ThreadLocalMap 
	ThreadLocalMap m = getMap(Thread.currentThread()); 
	if (m != null)
		m.remove(this);    
 }
//remove()
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 根据hashCode计算出当前ThreadLocal的索引位置
    int i = key.threadLocalHashCode & (len-1);  
    // 从位置i开始遍历,直到Entry为null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
		// 如果找到key相同的
        if (e.get() == key) {   
			// 则调用clear方法, 该方法会把key的引用清空
            e.clear(); 	
			//调用expungeStaleEntry方法清除key为null的Entry
            expungeStaleEntry(i);
            return;
        }
    }
}
// 从staleSlot开始, 清除key为空的Entry, 并将不为空的元素放到合适的位置,最后返回Entry为空的位置
private int expungeStaleEntry(int staleSlot) {  
    Entry[] tab = table;
    int len = tab.length;
 
    // 将tab上staleSlot位置的对象清空
    tab[staleSlot].value = null;    
    tab[staleSlot] = null;
    size--;
 
    // Rehash until we encounter null
    Entry e;
    int i;
	// 遍历下一个元素, 即(i+1)%len位置的元素
    for (i = nextIndex(staleSlot, len); 
	// 遍历到Entry为空时, 跳出循环并返回索引位置
         (e = tab[i]) != null;	
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get(); 
		// 当前遍历Entry的key为空, 则将该位置的对象清空
        if (k == null) {    
            e.value = null;
            tab[i] = null;
            size--;
		// 当前遍历Entry的key不为空
        } else {    
			// 重新计算该Entry的索引位置
            int h = k.threadLocalHashCode & (len - 1);  
			// 如果索引位置不为当前索引位置i
            if (h != i) {   
			// 则将i位置对象清空, 替当前Entry寻找正确的位置
                tab[i] = null;  
 
                // 如果h位置不为null,则向后寻找当前Entry的位置
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;   
}

rehash()

private void rehash() {
	// 调用expungeStaleEntries方法清理key为空的Entry
    expungeStaleEntries();  
 
    // 如果清理后size超过阈值的3/4, 则进行扩容
    if (size >= threshold - threshold / 4)  
        resize();
}
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
	// 新表长度为旧表2倍
    int newLen = oldLen * 2;	
	// 创建新表
    Entry[] newTab = new Entry[newLen];	
    int count = 0;
	// 遍历所有元素
    for (int j = 0; j < oldLen; ++j) {	
		// 拿到对应位置的Entry
        Entry e = oldTab[j];	
        if (e != null) {
            ThreadLocal<?> k = e.get();
			// 如果key为null,将value清空
            if (k == null) {	
				// Help the GC
                e.value = null; 
            } else {
            	// 通过hash code计算新表的索引位置
                int h = k.threadLocalHashCode & (newLen - 1);
                // 如果新表的该位置已经有元素,则调用nextIndex方法直到寻找到空位置
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
				// 将元素放在对应位置
                newTab[h] = e;	
                count++;
            }
        }
    }
	// 设置新表扩容的阈值
    setThreshold(newLen);	
	// 更新size
    size = count;	
	// table指向新表
    table = newTab;	
}

问题

为什么会造成内存泄漏

​ ThreadLocal本身并不存储值,它只是作为一个(弱引用)key保存到ThreadLocalMap中,但entry有强引用链。因此key可能会被回收,但entry不会,这样ThreadLocalMap中存在一些key为null的键值对。而ThreadLocalMap生命周期与线程一致,如果没有手动删除此类键值对的机制,其内存不会被回收,且无法方位,最终导致内存泄露
防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但不能完全保证,如线程被放回线程池后一直闲置,或使用时没有调用上述三个方法

为什么使用弱引用,内存泄漏是否是弱引用的过?

​ 如key使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏
如key使用弱引用,引用的ThreadLocal的对象被回收了,由于ThreadLocalMap还持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除
因此,内存泄漏的原因是ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏

如何避免内存泄漏?

​ 每次使用完ThreadLocal,都调用它的remove()方法,清除数据
注意:并不是所有使用ThreadLocal的地方,都在最后remove(),如他们的生命周期需要和项目的生存周期一样长

猜你喜欢

转载自blog.csdn.net/mLuoya/article/details/87829252