ThreadLocal使用

付威     2019-05-01   4700   13min  

ThreadLocal是一个以当前线程为key,存储变量的map容器,能够解决线程安全问题。

首先我们先看下ThreadLocal的使用:

public class ThreadLocalDemo {
	
	ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> new Integer(0));
	
	public int getNext() {
		Integer value = count.get();
		value++;
		count.set(value);
		
		return value;
		
	}
	
	public static void main(String[] args) {
		 new Thread(() -> {
			ThreadLocalDemo demo = new ThreadLocalDemo();
			while (true) {
				System.out.println(Thread.currentThread().getName() + " " + demo.getNext());
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
		
		new Thread(() -> {
			ThreadLocalDemo demo = new ThreadLocalDemo();
			while (true) {
				System.out.println(Thread.currentThread().getName() + " " + demo.getNext());
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
		new Thread(() -> {
			ThreadLocalDemo demo = new ThreadLocalDemo();
			while (true) {
				System.out.println(Thread.currentThread().getName() + " " + demo.getNext());
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
	 
	}
}


打印结果如下:

ThreadLocal

从上面的结果可以看出,ThreadLocal对应的变量可以被线程单独访问,能够避免线程安全问题。

为什么ThreadLocal能够解决线程问题呢?我们先看下ThreadLocal的get方法:


public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
//getMap(t)的方法
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

看到这个方法,我总觉的这段代码写的有问题,好像和自己写的代码差不多。。。(JDK源码的水平也有差的)

ThreadLocal

ThreadLocal和Thread的关系如下:

ThreadLocal

当看到这个引用图的时候,隐隐觉得会有问题,在当前的线程中,如果线程不消亡,那ThreadLocalMap 的map实例就永远不会被回收。

因为在GC每次计算对象还有没有引用的时候, 判断永远是有在用用。这样程序中set的值就永远存在,这样就导致了内存的泄露。

在线程池中,由于线程的复用性,就容易造成ThreadLocal内存泄露的问题,所以最好在线程执行完成后,调用remove手工删除数据。

对于上面的这个问题,jdk也有对应的优化,具体的方法是,ThreadLocalMap使用弱引用(对象为空的时候会释放)但优化的范围有限,具体的引用关系如下:

ThreadLocal

上面的优化过程,如果ThreadLocal被置空,则LocalThreadMap只会有弱引用,原来的ThreadLocal对象(为了方便,称为对象A)也会在下次直接被GC回收,所以在下次GC的时候, 对象A的引用会成为null.

但这样在LocalThreadMap中会带来一个问题,这样导致了在Key-Value中,key的引用的地址的值成为了null,这样也就导致了value无法被找到,也会造成内存泄露。

LocalThreadMap中在每次get()/set()/remove()操作map的中的值的时候,会自动清理key为null的值,这样就能够避免了泄露的问题。具体的源码如下:

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

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        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;
        }
 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

ThreadLocal多少还是有点风险,除非明确能够使用和删除ThreadLocal的值,否则尽量还是慎用。

(本文完)

作者:付威

博客地址:http://blog.laofu.online

如果觉得对您有帮助,可以下方的RSS订阅,谢谢合作

如有任何知识产权、版权问题或理论错误,还请指正。

本文是付威的网络博客原创,自由转载-非商用-非衍生-保持署名,请遵循:创意共享3.0许可证

交流请加群113249828: 点击加群   或发我邮件 laofu_online@163.com

付威

获得最新的博主文章,请关注上方公众号