一种无锁“共享”方案
既然多个线程同时访问共享数据会有竞争问题,那么每个线程维护单独的数据副本,表面上是访问同一数据,但是实际不共享
Sample 1 2 3 4 5 ThreadLocal<Integer> counter = new ThreadLocal<Integer>(); public void increase () { counter.set(counter.get() + 1 ); }
功能上类似于用线程id为key的map,每个线程独立
Sample 1 Map<Long,Integer> counterMap;
内部实现
虽然在功能上像个Map,但是实现上完全不同
Map对所有键值对进行集中管理,而ThreadLocal是调用的线程各自维护,谁用谁负责
所有的操作都是基于Thread.currentThread()
,每个线程都有内部的Map结构ThreadLocal.ThreadLocalMap threadLocals
,用于存储线程用到的变量
变量和线程之间是低耦合的,在真正使用时才建立联系。线程提前不知道会使用多少ThreadLocal, ThreadLocal也不知道会有多少线程
ThreadLocalMap
名字里有map,但根本没实现Map接口,是个专有的特殊结构
ThreadLocalMap作为静态内类,只被ThreadLocal使用,所有方法是私有的
底层依然是Entry数组,但是ThreadLocal本身作为key是弱引用实现,即考虑到大数据量的自动释放
如果没有弱引用,即使用户不再使用,整个Entry还在线程map中持有,不会被回收
ThreadLocal$ThreadLocalMap.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static class ThreadLocalMap { static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } } private static final int INITIAL_CAPACITY = 16 ; private Entry[] table; private int size = 0 ; private int threshold; }
哈希冲突的解决不是链表,而是开放地址
查找时判断是不是直接命中
ThreadLocal$ThreadLocalMap.java 1 2 3 4 5 6 7 8 9 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); }
没命中的情况下,会顺次向后循环查找
值得注意的是如果发现从Entry中获取的key值为null,那么说明已经通过弱引用机制回收了,会把Entry清理出去
ThreadLocal$ThreadLocalMap.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 static int nextIndex (int i, int len) { return ((i + 1 < len) ? i + 1 : 0 ); }
弱引用被动清理,ThreadLocal作为key变成null,但是Entry中的value还在
自动清理必须是下次访问即getEntry才可能触发,如果长时间不使用,所对应的value还是无法及时释放
特别是搭配线程池使用时,由于线程会重用,不会销毁,造成内存泄漏,最好及时清理
重用也会导致包含遗留数据,注意在开始时及时调用remove
清除遗留数据
ThreadLocal$ThreadLocalMap.java 1 2 3 4 5 6 7 8 9 10 11 12 private void remove (ThreadLocal<?> key) { 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return ; } } }
清除逻辑是级联的,不光会清除当前位置,还会重新哈希,过程中可能同时清除其他失效位置
效果是如果释放一个位置,如果原来有哈希冲突的数据,重新哈希后会再次填上释放位置,即如果存在数据那么应当在理论位置,如果位置是null说明数据不存在
ThreadLocal$ThreadLocalMap.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 private int expungeStaleEntry (int staleSlot) { Entry[] tab = table; int len = tab.length; 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 ) { e.value = null ; tab[i] = null ; size--; } else { int h = k.threadLocalHashCode & (len - 1 ); if (h != i) { tab[i] = null ; while (tab[h] != null ) h = nextIndex(h, len); tab[h] = e; } } } return i; }
ThreadLocal
ThreadLocal内部没有任何数据,只维护了一个哈希值,整体角色是作为一个key存在
ThreadLocal.java 1 2 3 public class ThreadLocal <T > { private final int threadLocalHashCode = nextHashCode(); }
存值操作
线程的ThreadLocal.ThreadLocalMap threadLocals
初始是null,用到时才建立
ThreadLocal只作为key,并不持有value,value是ThreadLocalMap内的Entry持有
ThreadLocal.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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); }
取值操作
存在默认值逻辑,即使没有set过,get一样会把ThreadLocal加入map中
ThreadLocal.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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(); }
初始化操作
ThreadLocal
有一个提供初始值的initialValue
函数,默认是null,子类可以重写
当线程调用get没找到值时就会调用该函数,包含两种情况
线程第一次调用get并且之前没被set
线程调用remove清空后再调用get
ThreadLocal.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 private T setInitialValue () { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set(this , value); else createMap(t, value); return value; } protected T initialValue () { return null ; }
只有key不存在时才会调用默认值方法
Sample 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ThreadLocal<String> tl = new ThreadLocal<String>() { @Override protected String initialValue () { return "aaa" ; } }; System.out.println(tl.get()); tl.set(null ); System.out.println(tl.get()); tl.remove(); System.out.println(tl.get());
清理操作
获取当前线程上的map, 从中删除自己
ThreadLocal.java 1 2 3 4 5 public void remove () { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null ) m.remove(this ); }
InheritableThreadLocal
ThreadLocal还有一个子类,可以用于传递给子线程
和普通的ThreadLocal不同的是,在Thread类中保存的位置不同,这个位置的数据会复制给子线程
InheritableThreadLocal.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class InheritableThreadLocal <T > extends ThreadLocal <T > { protected T childValue (T parentValue) { return parentValue; } ThreadLocalMap getMap (Thread t) { return t.inheritableThreadLocals; } void createMap (Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this , firstValue); } }
JDK使用
java.util.concurrent.ThreadLocalRandom
Random本身是线程安全的,ThreadLocal版本可以避免多线程下对随机seed的争抢,提升性能
应用
SimpleDateFormat
线程不安全,过程中通过设值给内部Calendar,然后进行操作。多线程下一起操作Calendar状态不可控,可能产出和预期不一样的结果
解决方案是可以用ThreadLocal包装
Sample 1 2 3 4 5 6 private static final ThreadLocal<DateFormat> FORMAT = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue () { return new SimpleDateFormat("yyyy-MM-dd" ); } }
Java8后可以使用线程安全的DateTimeFormatter