ThreadLocal
1、解决并发问题的另一种思路
并发编程带来的种种问题的本质在于多个线程对共享变量操作的无序性、非原子性,解决问题的一个思路是用锁或者CAS操作来保证对共享变量修改的原子性,另一个思路是使共享变量私有化。
比如下面开启了两个线程分别用于增加和减少共享变量value,在不使用synchronizes关键字修饰increase和decrease方法的时候最终输出的结果不一定是10和-10,因为在修改共享变量和输出共享变量之间,共享变量可能会被再次修改。由于synchronize关键字“锁住”了对象number,同一时间只有一个线程可以执行。
public class MutiThreadDemo {
public static class Number{
private int value = 0;
public synchronized void increase() throws InterruptedException {
value=10;
Thread.sleep(1000);
System.out.println("increase"+value);
}
public synchronized void decrease() throws InterruptedException {
value=-10;
Thread.sleep(1000);
System.out.println("decrease"+value);
}
}
public static void main(String[] args) throws InterruptedException {
Number number = new Number();
Thread increaseThread = new Thread(new Runnable() {
@Override
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread decreaseThread = new Thread(new Runnable() {
@Override
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
increaseThread.start();
decreaseThread.start();
}
}
建立CustomThreadLocal类,封装Map的相应API,以ThreadID作为KEY去查询和存储线程私有变量,每次线程要操作私有变量的时候直接去该Map里获得。
package Concurr; import java.util.HashMap; import java.util.Map; public class simpleMap { public static class CustomThreadLocal{ private Map<Long,Object> threadloal = new HashMap<Long, Object>(); private int defaultValue; public CustomThreadLocal(int defaultValue) { this.defaultValue = defaultValue; } public Object get(){ long threadId = Thread.currentThread().getId(); if(threadloal.containsKey(threadId)){ return threadloal.get(threadId); }else { return defaultValue; } } public void set(Object value){ long threadId = Thread.currentThread().getId(); threadloal.put(threadId,value); } } public static class Number{ private CustomThreadLocal threadLocal = new CustomThreadLocal(-1); public void increase() throws InterruptedException { threadLocal.set(10); Thread.sleep(10); System.out.println("increase "+threadLocal.get()); } public void decrease() throws InterruptedException { threadLocal.set(-10); Thread.sleep(10); System.out.println("decrease "+threadLocal.get()); } } public static void main(String[] args) { Number number = new Number(); Thread increaseThread = new Thread(new Runnable() { @Override public void run() { try { number.increase(); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread decreaseThread = new Thread(new Runnable() { @Override public void run() { try { number.decrease(); } catch (InterruptedException e) { e.printStackTrace(); } } }); increaseThread.start(); decreaseThread.start(); } }
这样做的缺点:1、无法保证HashMap的线程安全性,在并发条件下HashMap不是一个线程安全的容器,虽然可以使用线程安全的相关类。 2、无法进行有效的垃圾回收。在一个线程执行完毕后,该线程拥有的私有变量理应也被回收,但在该设计模式下无法回收。
解决思路:为每一个线程创建一个私有的Map来存放该线程的私有变量,这样既解决了并发问题也解决了垃圾回收问题。
首先自定义了一个线程类CommonThread,该类和普通的Thread类相比只多了一个HashMap属性,这也意味着每一个实现CommonThread的对象都多了一个HashMap。每一个类的实例都拥有一个“小口袋”,每次需要存储或者取出数据的时候都去自己的小口袋里拿,彻底避免了并发问题。而且该“小口袋”和线程的声明周期是相同的。
package Concurr; import java.util.HashMap; import java.util.Map; public class simpleMap2 { public static class CommonThread extends Thread{ Map<Integer,Integer> cacheMap = new HashMap(); } public static class CustomThreadLocal{ private int defaultValue; public CustomThreadLocal(int defaultValue){ this.defaultValue = defaultValue; } public Map getMap(){ CommonThread currentThread = (CommonThread)Thread.currentThread(); return currentThread.cacheMap; } public int get(){ Map<Integer,Integer> map = getMap(); if(map.containsKey(this.hashCode())){ return map.get(this.hashCode()); }else { return defaultValue; } } public void put(int value){ Map<Integer,Integer> map = getMap(); map.put(this.hashCode(),value); } } public static class Number{ private CustomThreadLocal threadLocal = new CustomThreadLocal(-1); public void increase() throws InterruptedException { threadLocal.put(10); Thread.sleep(10); System.out.println("increase value: " + threadLocal.get()); } public void decrease() throws InterruptedException { threadLocal.put(-10); Thread.sleep(10); System.out.println("decrease value: " + threadLocal.get()); } } }
2、ThreadLocal使用与简单源码
上面问题一步步解决最终推出了再Thread内部保留一个Map存储线程私有变量的思想,这样正是ThreadLocal思想的概括。ThreadLocal本质上是一个容器,虽然它容器的本质并没有在ThreadLocal中体现出来。如果一个对象的某个方法要运行在多线程环境下,可以使用ThreadLocal容器为其维护线程私有变量,ThreadLocal会为每个线程分配一个独立变量的副本,线程对变量的修改都是对该副本的修改而不会影响其他副本,这样是该名字ThreadLocal所暗示的特性,有一种平行宇宙的感觉。
package Concurr; public class SequenceNumber { private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(); public int getNextNum(){ if(seqNum.get() == null){ seqNum.set(0); return 0; } seqNum.set(seqNum.get()+1); return seqNum.get(); } private static class TestCLient extends Thread{ private SequenceNumber sn; public TestCLient(SequenceNumber sn){ this.sn = sn; } public void run(){ for(int i=0;i<3;i++){ System.out.println(Thread.currentThread().getName()+":"+sn.getNextNum()); } } } public static void main(String[] args) { SequenceNumber sequenceNumber = new SequenceNumber(); System.out.println(sequenceNumber.seqNum); TestCLient cLient1 = new TestCLient(sequenceNumber); TestCLient cLient2 = new TestCLient(sequenceNumber); TestCLient cLient3 = new TestCLient(sequenceNumber); cLient1.start(); cLient2.start(); cLient3.start(); } }
Thread-0:0 Thread-0:1 Thread-0:2 Thread-1:0 Thread-1:1 Thread-1:2 Thread-2:0 Thread-2:1 Thread-2:2
开启了三个线程,虽然每个线程都共享了变量seqNum,但是其打印出来的结果显示三个线程之间没有互相干扰。可以简单的理解成每个线程拿着自己的ThreadID去ThreadLocal里取出属于自己的数据。可以从这个例子中总结出使用ThreadLocal的模板:1、使用ThreadLocal作为static变量 2、需要多线程执行的方法是哟ThreadLocal。
但其实单单看这个例子很像之前写的第二版即用一个公共的Map以ThreadID为key去存储线程变量,其实远非如此。
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() {
//1 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);
//2 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
整个ThreadLocal#get方法可以分为两个步骤
- 获取当前线程的ThreadLocalMap对象。
- 以this作为关键字在ThreadLocalMap里查找value,如果value为null执行setInitialValue。
ThreadLocal#getMap(Thread t)放直接返回Thread对象的ThreadLocalMap属性,该属性本质上是一个map,后续的操作都是针对该map的。源码读到这里可以看到ThreadLocal里并没有存储线程私有变量,真正存储线程私有对象的是线程私有变量ThreadLocalMap,ThreadLocal只是封装了这一查找过程,因为Thread的ThreadLocalMap没有暴露出来。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
ThreadLocal.ThreadLocalMap threadLocals = null;
所以get方法最核心的调用是ThreadLocalMap#getEntry方法,先要从ThreadLocalMap的结构入手。抛开内部实现,ThreadLocalMap是一个散列表,既然是散列表就有key和value,结合下面的getEntry方法可以看出该散列表的key是ThreadLocal,value是需要存储的值。有点绕。所以可以推测出在一个ThreadLocal只能对应一个value。
/** * Get the entry associated with key. This method * itself handles only the fast path: a direct hit of existing * key. It otherwise relays to getEntryAfterMiss. This is * designed to maximize performance for direct hits, in part * by making this method readily inlinable. * * @param key the thread local object * @return the entry associated with key, or null if no such */ 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类型的key和Object类型的value组成。和HashMap节点的区别在两个地方:1、继承的弱引用 2、没有指向下一个节点的指针
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
继承了弱引用是为了提高垃圾回收的效率,一个只用弱引用的对象被成为弱可达,弱可达的对象只要垃圾回收就会被立即回收。
没有指向下一个Entry的指针说明该散列表不是使用拉链法解决Hash冲突,而是开放地址法。所谓开放地址法指的是在发生冲突的时候不建立链表而是在数组中探测一个可用的单元。比如最简单的线性探测法是从冲突的单元开始循环向后搜索可用的单元。具体的在ThreadLocalMap中采用nextIndex方法实现。
/** * Increment i modulo len. */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } /** * Decrement i modulo len. */ private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }
其次看put方法。首先根据key的hash计算出插入的位置,如果位置不满足循环调用nextIndex找到下一个位置。如果寻找的过程中出现了重复key那么覆盖。
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
看源码是为了更好的理解底层过程,总结下来:在一个线程中调用ThreadLocal#get时,以ThreadLocal为Key去当前线程对象维护的ThreadLocalMap中找到该Key对应的Entry,然后取出Value。ThreadLocal并不存储值,它只是一个key。
3、弱引用与内存泄露
内存泄露本质就是一个无用的对象还是GC可达的,GC可达就以为着无法被回收。ThreadLocal产生内存泄露的对象是ThreadLocal对象,一个线程可能会执行许多方法,当线程执行到需要使用ThreadLocal的方法的时候,ThreadLocal对象有一个强引用这个强引用是当前线程对其的强引用,并且在线程内部的ThreadLocalMap里也有一个队ThreadLocal的引用,当使用ThreadLocal的方法结束后理应释放ThreadLocal的内存,但是由于ThreadLocalMap还持有ThreadLocal的引用所以可能带来内存泄露。
由于ThreadLocalMap的声明周期和Thread是相同的,或多或少会带来一定的内存泄露问题。采用弱引用根据弱引用的特点可以减缓内存泄露。所以每次调用完毕后最好使用remove方法。
http://www.importnew.com/22039.html
https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html#_label2