前面也听说了 ThreadLocal 来实现高并发, 以前都是用锁来实现, 看了挺多资料的, 发现其实还是区别挺大的 (感觉严格来说 ThreadLocal 并不算高并发的解决方案), 现在总结一下吧.
高并发中会出现的问题就是线程安全问题, 可以说是多个线程对共享资源访问如何处理的问题, 处理不当会的话, 会出现结果和预期会完全不同.
一般情况下, 多个线程访问一个变量都是公用他们的值, 不过有时候虽然也是访问共享变量, 不过每个线程却需要自己的私有变量. 这个时候 ThreadLocal 就有用武之地了. 下面是个 ThreadLocal 的简单实例:
- public class ThreadLocalExample {
- public static void main(String[] args){
- // 创建一个 ThreadLocal 对象
- ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 设置主线程私有变量值
- threadLocal.set(100);
- // 创建一个新线程
- new Thread(new Runnable(){
- public void run(){
- // 使用共享变量, 设置线程私有变量
- threadLocal.set(50);
- System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
- }
- }).start();
- System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
- }
- }
输出结果:
- main:100
- Thread-0:50
很神奇, 对多个资源之间的共享, 又不想他们之间相互影响, 所以使用这个是挺不错的. 具体应用, spring 中应用我记得挺多的, 连接数据库的每个连接, 还有 session 的存储.
思考了一下, 要我实现的话就用个 map 来存储, 因为这个其实就是键值对, 只不过键是线程唯一标识, 值就是对应的私有变量.
具体看了源码发现差不多, 不过使用内部自己实现的一个 ThreadLocalMap 类, 内部还一个 Entry 类而且 Entry 类继承 weakRefrence(说实话第一次遇到弱应用, 以前只是在 jvm 那本书学习了下), 具体方法如下:
先看下他的 set 方法吧
- public void set(T value) {
- Thread t = Thread.currentThread();
- // 获得所有线程共享的 ThreadLocalMap 对象
- ThreadLocalMap map = getMap(t);
- // 对象已经存在就直接插入键值对
- // 不存在就创建然后再插入
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
getMap 方法的话一个获得所有线程共享的 ThreadLocalMap 对象如下:
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
然后进入 Thread 类进去找一下这个容器, 找到下面:
ThreadLocal.ThreadLocalMap threadLocals = null;
然后创建:
- void createMap(Thread t, T firstValue) {
- // 创建 ThreadLocalMap 对象赋给 threadLocals
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
至此, ThreadLocal 的基本原理就已经很清晰了: 各线程对共享的 ThreadLocal 实例进行操作, 实际上是以该实例为键对内部持有的 ThreadLocalMap 对象进行操作.
还有 get() 方法的话就是利用设置的键进行获取, remove() 方法也是, 其实和 Hashmap 差不多不过解决冲突使用的拉链法 (对了, 下次写一篇 HashHap 的还有 ConcurrentHashMap 的话, 颇有研究). 这里有个问题就是因为这个 ThreadLocalMap 是静态的所以在方法区中 (jdk8 之后为元数据区), 不进行回收的话会造成内存泄漏, 而且可能会出现内存溢出, 所以使用后记得 remove();
基本上其实可以了, 不过好奇 ThreadLocalMap 怎么实现的可以接着往下看, 我也好奇, 所以也偷偷看了, 嘿嘿嘿
那就来分析一下这个 ThreadLocalMap 这个内部类吧.
ThreadLocalMap 属于一个自定义的 map, 是一个带有 hash 功能的静态内部类, 其实和 java.util 包下提供的 Map 类并没有关系. 内部有一个静态的 Entry 类, 下面具体分析 Entry.
- /**
- * The entries in this hash map extend WeakReference, using
- * its main ref field as the key (which is always a
- * ThreadLocal object). Note that null keys (i.e. entry.get()
- * == null) mean that the key is no longer referenced, so the
- * entry can be expunged from table. Such entries are referred to
- * as "stale entries" in the code that follows.
- */
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
偷了一下官方的解释:
主要是说 entry 继承自 WeakReference, 用 main 方法引用的字段作为 entry 中的 key. 当 entry.get() == null 的时候, 意味着键将不再被引用.
注意看到一个 super(k), 说明调用父类的构造, 去看看
- Reference(T referent) {
- this(referent, null);
- }
- Reference(T referent, ReferenceQueue<? super T> queue) {
- this.referent = referent;
- this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
- }
就上面这个其他没了, 看了半天有点没看懂, 然后去学了四种引用回来终于看懂, 由于篇幅过多, 在结尾我给出两篇别人的博客, 可以去看完了, 再回来, 多学点哈哈哈.
再看了下发现这个内部类好多, 但是其实就是 map 的一种实现, 上面也讲了 set 方法那就简单提一下 ThreadLocalMap 的 set() 方法相关的吧, 代码下面:
- 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 对象数组拿到来
- ThreadLocal.ThreadLocalMap.Entry[] tab = table;
- // 长度也拿到来
- int len = tab.length;
- // 通过拿到 key 的 hashcode 值, 进去发现神奇的一幕这里利用通过累加这个值 0x61c88647 来作为 hashcode,
- // 这里提一下往下走发现因为要公用这个属性, 多个实例访问会有问题
- // 所以使用了 AtomicInteger 原子操作来写值
- // 并且与总长度 - 1 做与运算就是取模, 因为扩容都是 2 的 n 次方所以这样直接取模就行, 速度快
- int i = key.threadLocalHashCode & (len-1);
- // 定位到对应的数组位置, 进行冲突判断之类的处理
- for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) { // 这里是冲突遍历
- // 这里里面就拿对应 tabel 下对应位置的当前引用
- ThreadLocal<?> k = e.get();
- // 判断是不是对应的键, 是的话就覆盖
- if (k == key) {
- e.value = value;
- return;
- }
- // 没有的话就生成 Entry 代替掉
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- // 这里就直接插入了
- tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
- // 长度加 1
- int sz = ++size;
- // 判断是否做扩容
- if (!cleanSomeSlots(i, sz) && sz>= threshold)
- rehash();
- }
里面其实挺复杂的, 具体的话就是正常是使用开放定址法处理, 这里使用累加一个定值解决的冲突, 因为多个实例共用, 特殊处理, 厉害厉害.
- //threadLocalHashCode 代码也贴在这里吧, 有兴趣可以直接去看
- private static AtomicInteger nextHashCode = new AtomicInteger();
- private static final int HASH_INCREMENT = 0x61c88647;
- private static int nextHashCode() {
- return nextHashCode.getAndAdd(HASH_INCREMENT);
- }
- private final int threadLocalHashCode = nextHashCode();
总结
看完源码之后神清气爽, 学到了很多啦. 以前对 java 引用只是知道四个引用和对应的相应简单概念, 为了看懂这个 Entry, 去学习了 weakReference 源码, 看了别人的关于四个引用的博客写的真好, 偷偷学习了下, 并且知道怎么使用了. 划重点会用了!!! 当然对于 ThreadLocal 也会用了, 而且好像可以手写一个简单的版本哎, 可以动手试试.
关于四种引用博客, 写的真的很棒.
来源: https://www.cnblogs.com/calaMan/p/11753801.html