# 静态类

我们先介绍一下静态类,这在之后的讲解会用到。静态类只能是内部类,如果外部类使用 static 会报错。

首先,静态类被 static 修饰,静态代码(方法,属性等)的使用方式不依赖于实例对象。静态类中可以有非静态属性,要使用非静态属性就需要依赖实例对象,而且这个非静态属性并不是所有类共享。

public class Test {
    static class A {
        private int count = 0;
        public  void get() { System.out.println(count); }
        public  void set() { count++; }
    }
    public void fun() {
        A a1 = new A();A a2 = new A();
        a1.set();a2.get();
    }
}
public class Main{
    public static void main(String[] args){
        Test t1 = new Test();
        t1.fun();
    }
}
// 结果是 0,说明 a1 和 a2 不共享 count

静态类不能访问外部类的非静态成员

# ThreadLocal 简介

线程安全的解决思路可以有:

  • 互斥同步: synchronizedReentrantLock 等加锁。

  • 非阻塞同步: CAS,AtomicXXX 等硬件实现原子操作。

  • 无同步方案:栈封闭,本地存储(ThreadLocal),可重入代码。

本文将会讲解本地存储 -- ThreadLocal

ThreadLocal 是一个将在多线程中为每一个线程创建单独的变量副本的类;当使用 ThreadLocal 来维护变量时, ThreadLocal 会为每个线程创建单独的变量副本,避免因多线程操作共享变量而导致的数据不一致的情况。

先看一下基本的使用,一定要先看看

public class ThreadLocalTest {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
    public static void main(String[] args){
        // 创建第一个线程
        Thread threadA = new Thread(()->{
            threadLocal.set("ThreadA:" + Thread.currentThread().getName());
            System.out.println("线程A本地变量中的值为:" + threadLocal.get());
        },"threadA");
        
        // 创建第二个线程
        Thread threadB = new Thread(()->{
            threadLocal.set("ThreadB:" + Thread.currentThread().getName());
            System.out.println("线程B本地变量中的值为:" + threadLocal.get());
        },"threadB");
        
        // 启动线程 A 和线程 B
        threadA.start();
        threadB.start();
    }
}
// 每次的打印结果可能不一致
// 线程 B 本地变量中的值为:ThreadB:ThreadB
// 线程 A 本地变量中的值为:ThreadA:ThreadA

也许你还是没理解 ThreadLocal 到底为我们解决了什么问题,我们以数据库连接为例讲解:

直接使用 JDBC,需要你通过 DriverManage 手动获取 Connection ,有时为了方便,我们通常将这个行为封装到一个工具类中,每次需要获取一个 connection 时,就去调用这个类的 getConnection ,为了节省资源,我们还将 connection 设为静态的。如果这是单线程的,那自然是没有问题,如果是多线程的话,有些线程还在使用 connection 执行操作,而有的线程却调用 close ,就会出错,如果不是 static 的连接,又会因为频繁创建,销毁 connection 导致服务器压力过大。而 ThreadLocal 就为每一个线程创建了一个 connection 副本,既实现了线程安全,又避免了服务器压力过大。

# 相关的类

# Entry

该类是 ThreadLocalMap 的内部静态类,它是实际存储单元,也就是说,线程 Thread 将资源存储在 ThreadLocalMap 属性 threadLocals 中,而其实真正存储的是 Entry ,以键值对的形式存储。

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

我们可以看到, Entry 里面有一个非静态属性 Object value ,其实存储的就是资源副本。在上面的例子中, value 其实就是 String 类型,在线程 A 中的值就是: ThreadA:Thread-0 ;在线程 B 中的值 i 就是: ThreadB:Thread-1

Entry 存储的另一个属性就是 ThreadLocal ,到这我们可以知道 Entry 是以键值对的形式存储资源的 <ThreadLocal,Object> 。所以例子中,从图像来看是这样的。

Entry关系图

查看 ThreadLocalMap 源码可以看到,其实其内部有一个 Entry 数组,查找时是根据哈希码查找的。

# ThreadLocalMap

这是 ThreadLocal 的一个静态内部类,根据其给定的方法可以快速查找当前线程对应的 Entry 。先来看一下其相关属性

private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // The next size value at which to resize,Default to 0

我们再看一下 ThreadLocalMap 是如何查找 Entry 的。

private Entry getEntry(ThreadLocal<?> key) {
    // 通过 ThreadLocal 的哈希码拿到数组下标
    int i = key.threadLocalHashCode & (table.length - 1);
    
    // 获取对于 Entry
    Entry e = table[i];
    
    // 处理哈希冲突
    if (e != null && e.get() == key)
        return e;
    else
        // 这里就是当发生哈希冲突,在 table 数组中循环向后移位查找
        return getEntryAfterMiss(key, i, e);
}

最后需要强调的是,我们在外部使用的是 ThreadLocalgetset (以及 remove ),这些函数的实现其实就是调用其内部类 ThreadLocalMap ,所以 getEntry 接收的参数其实就是其外部类的实例。

知道其大致结构和使用,剩下的分析方法类似,不再赘述。

# ThreadLocal 详解

已经学习了前面两个内部类的原理,我们这里直接看源码

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap threadLocals = getMap(t);// 获得线程 t 的 threadLocals 属性
    
    if (threadLocals != null) {
        ThreadLocalMap.Entry e = threadLocals.getEntry(this);// 通过 ThreadLocal 实例找到值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 线程的 ThreadLocalMap 没有初始化,就会初始化
    return setInitialValue();
}
// 初始化值,其实主要是因为 threadLocals==null,该函数要调用 createMap
private T setInitialValue() {
    T value = initialValue();// 返回 null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);// 对线程 t 创建 ThreadLocalMap
    
    return value;
}

这里重点提一下 createMap 函数

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

静态内部类的一个好处就是,它可以先实例化而不用管外部类是否实例化。静态类的另一个意义就是外部类需要访问内部类而内部类不需要访问外部类。从上面的讲解可以看到, ThreadLocalMap 唯一需要依赖的就是外部类的实例化对象。

再看一下 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);
}

# 补充

之前我对这些知识的讲解,为了更容易让读者理解,说得比较通俗,为了构建一个系统的知识体系,这里会引用其他博客对整个 ThreadLocal 的讲解。

# ThreadLocalMap 对象是什么

本质上来讲,它就是一个 Map , 但是这个 ThreadLocalMap 与我们平时见到的 Map 有点不一样

  • 它没有实现 Map 接口;
  • 它没有 public 的方法,最多有一个 default 的构造方法,因为这个 ThreadLocalMap 的方法仅仅在 ThreadLocal 类中调用,属于静态内部类
  • 该类仅仅用了一个 Entry 数组来存储 Key , Value ; Entry 并不是链表形式,而是每个 bucket 里面仅仅放一个 Entry ;

上文讲到查找是与哈希码有关,这里我们可以学习一下 ThreadLocalMap 是怎么处理哈希冲突的

private void set(ThreadLocal<?> key, Object value) {
    //set 函数 -- 将 & lt;k,v > 插入到数组中
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);// 获取下标
    // 根据 threshold 可知,table 中一定有 1/3 以上为 null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {// 如果该位置的 key 相同,说明之前已经为 key 找到对应的下标
            e.value = value;
            return;
        }
        if (k == null) {//k 为 null,说明之前有 key 占有过,但是被清空了,所以当前 key 可以直接写进去
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	
    // 此时还没将 & lt;key,value > 放入 table 中,当前 i 的 Entry 为 null,可以放进去
    tab[i] = new Entry(key, value);
    int sz = ++size;
    
    // 一些移出和扩容操作
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
//nextIndex 函数,也就是出现了哈希冲突,如何找下一个下标 -- 直接加 1
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

再看一下 get 方法上面已经讲了,此处不再赘述。

# 参考

https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html

https://blog.csdn.net/vking_wang/article/details/14225379

https://mp.weixin.qq.com/s/mo3-y-45_ao54b5T7ez7iA

https://www.xttblog.com/?p=3087

https://blog.csdn.net/whut2010hj/article/details/81413887

https://segmentfault.com/a/1190000018399795

冰河《深入理解高并发编程》第 1 版