Java高并发--ThreadLocal使用及源码分析

1概述

线程本地变量(ThreadLocal)位每一个线程创建一个变量副本,从而线程就可以单独使用自己拥有的变量副本,而相互之间不产生影响。而线程本地变量与线程同步机制中的共享变量有什么区别呢?很明显,每个线程对共享变量的修改对于其余线程是可见的,而每个线程对线程本地变量的修改对于其余线程是不可见的。

2示例

package com.liutao.concurrent;

public class ThreadLocalDemo {
    public static ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        int num;
        for (int i = 0; i < 3;i++){
            num = ThreadLocalDemo.integerThreadLocal.get();
            System.out.println(Thread.currentThread().getName()+":"+num);
            ThreadLocalDemo.integerThreadLocal.set(num+1);
        }
    }
}

执行结果:

Thread-0:0
Thread-1:0
Thread-2:0
Thread-1:1
Thread-0:1
Thread-1:2
Thread-2:1
Thread-2:2
Thread-0:2

我们可以看见上面三个线程执行的内容是相同的,相互之间没有任何影响。

针对ThreadLocal常用的四个方法如下:

  • get:返回当前线程中变量副本的值。
  • set:设置当前线程中变量副本的值。
  • remove:移除当前线程中变量副本的值。
  • initialValue:返回变量副本的初始值。当线程第一次调用变量副本的时候将会调用这个方法,除非在调用变量副本的get方法之前已经调用了set方法进行设置。当然上面的示例我们可以看见,我们重写了initialValue方法。

3源码分析

3.1ThreadLocalMap源码分析

通过查看ThreadLocal的set、get方法我们可以发现ThreadLocal的实现主要是依靠了ThreadLocalMap这个内部类。那么首先我们来看ThreadLocalMap这个内部类的实现。

通过查看源码我们可以发现ThreadLocalMap是使用Entry来进行key和value的存储的。

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 和线程产生关联的值 */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

这个Entry继承自WeakReference,并使用ThreadLocal作为key。可以看见针对弱引用,当key为null的时候将被GC回收。

那么针对ThreadLocalMap我们仅仅分析两个核心方法即可。

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be 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[] tab = table;
            int len = tab.length;

            //根据ThreadLocal的hashcode查找对应元素在数组中的位置
            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;
                }
  
                //key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            
            //ThreadLocal对应的key不存在,也没有Entry值,则创建一个新的
            tab[i] = new Entry(key, value);
            int sz = ++size;

           //cleanSomeSlots清楚key == null 的Entry,如果没有清除成功,并且数组中的元素大于了阀值则refresh
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

针对上面的源码,我们可以看见ThreadLocalMap初始的时候有一个table,并且初始容量为16。

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

nextIndex方法的源码如下:

 private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

可以发现轮询table获取的Entry的最大索引为len-1,当大于len-1的时候索引为0。

针对cleanSomeSlots和refresh,我们可以看见,最终都是调用expungeStableEntry来删除Entry的value并置空节点。

/**
 * 清除成旧的Entry(key==null)
 */
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;
        }


 private void rehash() {
            expungeStaleEntries();

            
            if (size >= threshold - threshold / 4)
                resize();
        }

/**
 * 清除整个table的成旧数据
 */
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);
            }
        }
/**
 * 从新hash计算从staleSlot节点到下一个空节点中的节点
 */
private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            //删除指定节点位置的值并置空节点
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            //从新计算hash,直到遇到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;

                        
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

通过上面我们分析了ThreadLocalMap的set方法,接下来我们分析getEntry,源码如下:

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

通过key计算得到索引值过后直接从table中获取值,如果获取到就返回,没有获取到就调用getEntryAfterMiss,那么getEntryAfterMiss又做了什么呢?

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

从i位置开始查找entry,当找到 entry不为空,并且找到对应的key的时候,就返回entry,如果key ==null就直接清除掉位置i。

前面分析了ThreadLocalMap中的核心方法,那么针对ThreadLocal中的方法相对来说就简单多了。

3.2ThreadLocal源码分析

(1)get方法

public T get() {
        Thread t = Thread.currentThread();
        
        //获取当前线程的ThreadLocalMap
        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();
    }

返回当前线程中变量副本的值,如果没有 ,就初始化。

(2)set方法

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

设置当前线程的变量副本的值为value,当然如果ThreadLocalMap不存在,则先创建ThreadLocalMap,然后再进行初始值设置。

针对remove和initialValue就不继续讲解了,都比较简单。

4应用场景

最常见的ThreadLocal的使用时用来解决数据库连接和session管理。

具体示例,后期在源码中发现添加。

猜你喜欢

转载自blog.csdn.net/onroad0612/article/details/81179482