对于多线程并发对数据的修改的情况,其实除了使用锁或者CAS机制之外,有的情况我们完全可以为每一个线程分配单独的数据,这个数量只能在对应的线程下才能访问到,这样就能避免资源的争抢
或者对于单次请求全局的一些信息,比如当前请求对应的用户信息,可以不通过参数的方式依次传递,而是在全局的一个地方维护,比如当请求进来时,就将当前用户的信息存储进去,但是因为我们的服务是多线程的,同时可能有很多的请求,所以需要用户信息有线程隔离的能力,不能访问到或覆盖了别的线程的用户信息
JDK提供的对应功能的类就是ThreadLocal
使用 先来看下最简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private static ThreadLocal<String> nameThreadLocal = new ThreadLocal <>();private static ExecutorService executorService = Executors.newSingleThreadExecutor();public static void main (String[] args) throws Exception { nameThreadLocal.set("zheng" ); testGet(); executorService.shutdown(); } private static void testGet () { String name = nameThreadLocal.get(); System.out.println("相同线程获取名称:" + name); executorService.execute(() -> { String name1 = nameThreadLocal.get(); System.out.println("不同线程获取名称:" + name1); }); }
输出结果如下:
1 2 相同线程获取名称:zheng 不同线程获取名称:null
即在哪个线程下设置的值,则只有在对应的线程下才能获取到,其他线程无法获取和操作
原理 作为一个技术人,只会使用是不能让我们满足的,下面我们就一起看看这个到底是怎么实现的
为了避免大家心急,先来简单说一下结论,之后我们再去看源码(基于JDK1.8)
其实在每个Thread类中,也就是每个线程中都有一个 ThreadLocalMap 类型的变量,这个类和我们平时使用的HashMap等的原理其实是比较相像的,如果大家对于HashMap的原理不太熟悉,可以参考一下我之前的文章 ,这里就不再介绍了
在这个Map中的key就是我们之前定义的ThradLocal实例,获取值时,在当前线程的ThreadLocalMap中根据ThreadLocal实例去匹配即可,由于不同线程是不同的Thread实例,所以ThreadLocalMap是独立的,互相不可见
好,现在开始进入源码分析阶段,先从set
方法开始
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 public void set (T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set(this , value); else createMap(t, value); } ThreadLocalMap getMap (Thread t) { return t.threadLocals; } void createMap (Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap (this , firstValue); }
上面的部分看起来还是比较简单的,我们再接着看下键值对是如何添加到ThreadLocalMap中的(对应上面源码中标注的1处),先看下ThreadLocalMap中的几个关键属性
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; }
弱引用
这里要先提一下弱引用,弱引用是只要发生了GC,就会被回收掉(相关的还有强引用,软引用,虚引用等)
而这里Entry是继承了WeakReference的,但是要注意一下,只有其中的ThreadLocal引用是弱引用,而其中的value并不是弱引用的,在发生垃圾回收时,只有ThreadLocal部分会被回收,value并不会
所以,如果出现Entry e != null && e.get() == null
时,说明其中的key被垃圾回收掉了
接下来分析 ThreadLocalMap的set方法
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 33 34 35 36 37 38 39 40 41 42 43 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 ; } if (k == null ) { replaceStaleEntry(key, value, i); return ; } } tab[i] = new Entry (key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } private static int nextIndex (int i, int len) { return ((i + 1 < len) ? i + 1 : 0 ); }
索引计算及冲突处理
先来对比HashMap说一下槽位的定位及发生哈希冲突时的处理方式,对应上述源码标记1,2处
HashMap :在HashMap中,计算槽位使用的是key对应类的hashCode(),哈希冲突时先使用使用equals方法比较是否是同一个key,真正冲突后在对应槽位形成链表(达到8个后会转为红黑树)
在HashMap中,计算槽位使用的是key对应类的hashCode(),哈希冲突时先使用使用equals方法比较是否是同一个key,真正冲突后在对应槽位形成链表(达到8个后会转为红黑树)
ThreadLocalMap :对比着我们看下ThreadLocalMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int i = key.threadLocalHashCode & (len-1 );private final int threadLocalHashCode = nextHashCode();private static int nextHashCode () { return nextHashCode.getAndAdd(HASH_INCREMENT); } private static AtomicInteger nextHashCode = new AtomicInteger (); private static final int HASH_INCREMENT = 0x61c88647 ;
很明显可以看出来,ThreadLocalMap的key对应的索引位计算并不依赖hashCode方法,而是使用了一个每次创建都会递增的一个值(0x61c88647这个值大家有兴趣可以去搜索了解一下,主要就是为了hash值能均匀的分布在二次方的数组里)
在哈希冲突时,也没有使用equals方法进行后续比较,而是直接使用了==比较,因为它不需要像我们业务处理时根据根据特定逻辑判断是否相等,不同的实例值一定不能互相覆盖,所以直接判断是否是同一个实例即可
再就是发出真实冲突时,没有使用链表,而是接着此索引向后查找到第一个空的槽位,进行插入
下面再来看下上面标示3处的代码,这里主要处理的情况是在插入时,计算到的索引位已经有值,但是其中的key已经被回收掉了,这时候进行占用相关的操作
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 if (k == null ) { replaceStaleEntry(key, value, i); return ; } private void replaceStaleEntry (ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null ; i = prevIndex(i, len)) if (e.get() == null ) slotToExpunge = i; for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null ; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return ; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } tab[staleSlot].value = null ; tab[staleSlot] = new Entry (key, value); if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
前面的内容中还涉及了两个方法(expungeStaleEntry
和 cleanSomeSlots
),我们依次看下
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 33 34 35 36 37 38 39 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; }
最后来看下cleanSomeSlots
方法,这个方法比较简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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; }
对于Map相关类,我们都知道扩容时会进行rehash,所以我们接下来就是看下ThreadLocalMap
的 rehash 方法
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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); } } 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 ; } 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; }
以上就是set
方法的全部内容
最后我们来看下get
方法
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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(); } 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); } 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 ; }
InheritableThreadLocal 虽然ThreadLocal可以让我们创建线程独立的数据,但是有的时候又需要跨线程进行使用,比如在执行任务的时候,需要创建新的线程来加快执行速度,这时候新创建线程的时候需要把当前线程设置到ThreadLocal中的值传递进去,此时可以使用InheritableThreadLocal,使用方法同ThreadLocal
在Thread类中,threadLocals 和 InheritableThreadLocal 都是其中的属性,源码部分如下
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null ; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null ; public Thread () { init(null , null , "Thread-" + nextThreadNum(), 0 ); } private void init (ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null , true ); } private void init (ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null ) { throw new NullPointerException ("name cannot be null" ); } Thread parent = currentThread(); this .group = g; this .daemon = parent.isDaemon(); this .priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this .contextClassLoader = parent.getContextClassLoader(); else this .contextClassLoader = parent.contextClassLoader; this .inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this .target = target; setPriority(priority); if (inheritThreadLocals && parent.inheritableThreadLocals != null ) this .inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this .stackSize = stackSize; tid = nextThreadID(); } }
需要注意的是,在直接创建子线程的时候可以通过inheritableThreadLocal进行传递,但是如果是线程池的场景,则无法这样使用
以上就是ThreadLocal的全部内容,如有错误欢迎指正