ThreadLocal

ThreadLocal提供线程私有变量,这些变量和普通的变量不同,因为每个线程通过get或set方法持有它们独自的变量并且单独初始化变量副本。ThreadLocal的对象一般应该用private static来修饰从而关联上一个线程的状态。
每个线程持有引用来关联线程本地变量副本只要这个线程还活着并且ThreadLocal对象可以访问。在线程结束后线程本地变量的副本会被GC回收(除非存在其它能关联到这些变量的引用)

1、构造

我们通常这样来构造它:

private static ThreadLocal<Integer> a = new ThreadLocal<Integer>();

源码中它只有一个构造器:

public ThreadLocal() {
}

构造完对象后我们会往里面塞值,比如:
a.set(1);
这样,它会调用set方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
} 
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

执行流程如下:
1、获取属于当前线程的ThreadLocalMap 的对象map
2、若map不为空就将当前线程插入map里
3、为空,就创建一个ThreadLocalMap对象,将ThreadLocal作为参数key传进去

一开始ThreadLocalMap 肯定为空,所以一定会去构造一个ThreadLocalMap 对象,然后往里塞值。在这里ThreadLocalMap 是ThreadLocal的一个内部类,它是实现set和get方法的核心,下面着重去看它:
然后它会调用以下构造器,传入的是作为key值的当前线程私有的ThreadLocal对象和value值:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

1、创建了一个Entry类型的数组,初始值默认为 INITIAL_CAPACITY=16
2、和hashmap里的数组定位一样,用了除留余数法算出当前线程和值在Entry数组里的位置
3、创建一个Entry对象,用将属于当前线程的ThreadLocal作为firstKey,并将其插入Entry数组里
4、size 数组中的元素个数为1,并设置阈值确保最坏装载因子为len*2/3

这里调用了Enrty类并创建了一个数组对象和普通类对象,来看看这个Entry类:

static class Entry extends WeakReference<ThreadLocal<?>> {
        /** 和当前线程相关联的值 */
        Object value;
        /*构造器,赋值*/
        Entry(ThreadLocal<?> k, Object v) {
        /*继承父类WeakReference构造器,将ThreadLocal对象设置为弱引用*/
            super(k);
            value = v;
        }
    }

官方对它的解释:Entry类继承了WeakReference,使用了它的主要引用域作为key值(通常是ThreadLocal对象)。空key(entry.get()== null)意味着这个key再也不能被引用,所以这个entry可以从表中被清理出去。
ThreadLocalMap是一个只适合维持线程本地值而定制的hash表。为了帮助处理大量或长时间的使用,这个表使用了WeakReferences类型的数据来作为key。

2、set方法

上面解释了若初始状态ThreadLocalMap为空的插值场景,如果ThreadLocalMap不为空呢?

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
map.set(this, value);

getMap:获取属于当前线程的ThreadLocalMap ,然后调用set进行插值
下面是ThreadLocalMap中set方法:

   private void set(ThreadLocal<?> key, Object value) {
        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)]) {
            ThreadLocal<?> k = e.get();
            /*旧值替换新值*/
            if (k == key) {
                e.value = value;
                return;
            }
            /*替换位置上key和value*/
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

1、和上面一样计算出这个版本ThreadLocal在ThreadLocalMap数组中的位置i,然后进行插值
2、若位置i上的key与当前key相等则用新值替换旧值,然后退出循环;若key为空则替换,然后退出循环;否则就一直使用线性探测法往后搜,然后重复之前的操作
3、最后size+1,并且调用cleanSomeSlots方法判断,若之后的元素被清理过则不需要rehash;若之后的元素没有被清理,并且size要大于阈值,则进行一次再hash

private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

线性探测法,若i + 1 >len则将i置0,由此操作可确定ThreadLocalMap是一个环相对的,有以下代码:

 private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

上面是往后探测,这里往前

3、get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

1、获取属于当前线程的ThreadLocalMap
2、若ThreadLocalMap 为空则调用setInitialValue方法创建一个ThreadLocalMap对象并将value设置为null然后返回;或者ThreadLocalMap 不为空但getEntry方法调用值为空,就将当前key对应的value设置为null然后返回。这里setInitialValue方法和set方法相类似,不再赘述

3、否则返回取得的值

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);
    }

这里要注意:每个线程可能拥有几个不同版本的ThreadLocal对象,比如ThreadLocal< Integer > ,ThreadLocal< Long>,ThreadLocal< Date>等,但是同一线程的所有版本ThreadLocal对象和相对应的值都放在同一个ThreadLocalMap里,所以不同版本调用threadLocalHashCode 算出来的hash值是不同的,对应的在同一个ThreadLocalMap中存放的位置也不同
1、算出所在位置
2、若位置上有值并且两者是同一版本的ThreadLocal对象,返回这个值
3、否则返回 getEntryAfterMiss方法的结果

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

传入的参数:作为key的ThreadLocal对象,getEntry返回的位置i,位置i中的元素k
1、重新尝试 getEntry方法,若成功则返回
2、若k中ThreadLocal对象为null,则调用expungeStaleEntry方法进行清理,然后在此方法中找到相应的位置i值并返回,然后将值插入到此位置
3、否则使用线性探测法往后找到相应的值并返回,然后将值插入到此位置

4、清理

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

调用ThreadLocalMap的remove方法进行删除

 private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
        /*算出位置*/
        int i = key.threadLocalHashCode & (len-1);
         /*对位置i上的元素做清理*/
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    public void clear() {
    this.referent = null;
}

1、算出位置
2、调用clear方法,clear方法是Reference类的方法,将ThreadLocal对象的引用置为null
3、使用expungeStaleEntry方法对其清理

 private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        /*将此位置的value置空,并将此位置的元素整体置空*/
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
        /*将此位置的value置空,并将此位置的元素整体置空*/
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;
           /*在我们找到空位之前一直rehash*/
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

这个方法传入的参数是ThreadLocal对象在环形数组中的位置staleSlot
1、将位置i上的元素的value置为null,并将这个元素置为null,size-1
2、一个循环,从位置i+1开始搜索,只要相应位置上有值,即key和value有一者不为null,则执行第3或4步
3、判断key值(也就是线程的ThreadLocal对象):若key为空,就执行第1步操作,然后继续循环
3、key不为null则rehash,重新计算线程的ThreadLocal对象在数组中的位置,直到我们找到一个空位,进入第4步
4、若即key和value都为null,返回此空位i

set方法会调用的清理方法

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];
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }

这个方法会在新元素被添加进数组时调用,传入的参数为:位置i和表长n
1、置标记位为false
2、从位置i+1开始往后搜,若找到一个位置,此位置上key值为空,则置标记位为true,调用 expungeStaleEntry 方法进行清理
3、每次调用expungeStaleEntry 方法成功size就会减一,每次循环都会将改变的值右移1位并付给长度n,直到n=0才会退出循环

5、rehash

private void rehash() {
        expungeStaleEntries();
        if (size >= threshold - threshold / 4)
            resize();
    }
private void expungeStaleEntries() {
        Entry[] tab = table;
        int len = tab.length;
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            if (e != null && e.get() == null)
                expungeStaleEntry(j);
        }
    }

以数组长度为循环周期,将所有陈旧的值(key==null)进行一次处理 ,调用了expungeStaleEntry方法
扩容:

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;
    }

扩容,将容量变为以前的两倍,并将陈旧的元素置空然后在新数组中重定位

基本用法:
一、简单的塞值、取值

public class ThreadLocalTest extends Thread{
	private final static ThreadLocal threadLocal = new ThreadLocal();
	
	private final static ReentrantLock lock = new ReentrantLock();
	
	static int i = 0;
	
	public void run() {
		lock.lock();
		synchronized(this) {
			for(int j=1;j<=40000;j++) {
				i = i+j;
				threadLocal.set(i);
				System.out.println(threadLocal.get());
			}
		}
		lock.unlock();
	}
	
	public static void main(String[] args) throws InterruptedException {
		ThreadLocalTest test1 = new ThreadLocalTest();
		ThreadLocalTest test2 = new ThreadLocalTest();
		test1.start();
		test2.start();
		test1.join();
		test2.join();
	}
}

我们在这里开启了三个线程:主线程、副线程一、副线程二,接着我们首先调用了主线程的join方法将主线程阻塞,然后我们测试时就可以不受到主线程的影响了。
然后我们要对两个线程的run方法进行加锁,一面出现共享数据出问题的情况。
接下来调用threadLocal的set和get方法向里面塞值和取值

二、hibernate中的ThreadLocal:

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;  
}  

这样,每个线程都会创建一个属于自己的session,然后就可以对自己的session做操作了。当然,每个线程使用的肯定是属于它们自己ThreadLocal中的session值,也不可能取到别的线程的ThreadLocal或者session,因此各个线程的session相互不会干扰
(此处参考博客 https://www.iteye.com/topic/103804)

三、数据库连接

  private static final ThreadLocal data = new ThreadLocal();  
  Connection conn = null;
    public static Connection getConnection()  {  
        conn = (Connection) data .get();  
        try {  
            if (conn == null) {  
                conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
                data.set(conn);  
            }  
        } catch (Exception e) {  
            e.printStackTrace();
        }  
        return conn;  
    }  

每个线程都会创建一个属于自己的connection对象,各个线程的connection相互不会干扰。即threadlocal让同一个线程使用同一个conn,从而保证事务。

四、ReentrantReadWriteLock中的应用

ReentrantReadWriteLock的内部类Sync中的一些属性或方法:

//HoldCounter的一个对象
private transient HoldCounter cachedHoldCounter;
//此类为每个线程都有的
static final class HoldCounter {
           //代表每个线程的计数值(加锁lock次数)
            int count = 0;
            //线程ID
            final long tid = getThreadId(Thread.currentThread());
        }
//ThreadLocalHoldCounter的对象
private transient ThreadLocalHoldCounter readHolds;
//此类继承了ThreadLocal类,并重写了initialValue
//即倘若使用get取值为0,就会先将HoldCounter对象设为值,然后返回此对象
static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

//下面是加读锁的部分方法
HoldCounter rh = cachedHoldCounter;
//若此线程之前没有获取锁或因为并发问题导致线程ID与当前线程ID不一致
    if (rh == null || rh.tid != getThreadId(current))
    //先设置初值(调用get方法后会会调用以上initialValue方法取设置初值)
       cachedHoldCounter = rh = readHolds.get();
    else if (rh.count == 0)
       readHolds.set(rh);
    //计数值加一
    rh.count++;

//下面是释放读锁的部分方法
HoldCounter rh = cachedHoldCounter;
//若此线程之前没有获取锁或因为并发问题导致线程ID与当前线程ID不一致
    if (rh == null || rh.tid != getThreadId(current))
    //设置初值
        rh = readHolds.get();
    //获取当前线程的计数值
    int count = rh.count;
    //若计数值小于1,即当前线程未尺有锁
    if (count <= 1) {
        //删除当前线程的HoldCounter 属性
        readHolds.remove();
            if (count <= 0)
               throw unmatchedUnlockException();
            }
    //计数值减一
    --rh.count;

其实ReentrantReadWriteLock对ThreadLocal的使用也是简单的get和set操作,再读锁方面,ThreadLocal为每个线程提供了一个计数值count,加一次读锁,计数值加一;释放一次,计数值减一;并且通过这个每个线程都持有的、并且互不相干的count值来和AQS中的原子值state相互配合来达到对线程加锁/释放锁的目的

以下片段摘自网络:
针对内存泄漏问题:
我们在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
请确保在我们完成对线程的ThreadLocal对象进行操作后立即删除此对象,并且最好在将线程返回到线程池之前执行remove操作。 最佳做法是使用remove()而不是set(null) ,因为这将导致WeakReference立即被删除,并与值一起被删除。
web容器(如tomcat)一般都是使用线程池处理用户到请求, 此时用ThreadLocal要特别注意内存泄漏的问题, 一个请求结束了,处理它的线程也结束,但此时这个线程并没有死掉,它只是归还到了线程池中,这时候应该清理掉属于它的ThreadLocal信息.所以我们最好在使用线程的ThreadLocal对象后立即将其释放(即调用remove()方法)从而避免内存泄漏。

总结:
每个线程(Thread类)内部都有一个属于自己的ThreadLocalMap对象,里面存放着键(当前ThreadLocal对象)值(属于自己的值)对;若此时有一个共享值,我们可以通过此对象来给需要访问此共享值的线程们每人拷贝一份此对象的副本,从而达到共享对象线程私有、并且每个线程对各自的共享对象副本互不干扰(也有线程安全的意思在里面)的目的。并且我们也要注意它的内存泄漏问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值