# 静态类
我们先介绍一下静态类,这在之后的讲解会用到。静态类只能是内部类,如果外部类使用 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 简介
线程安全的解决思路可以有:
互斥同步:
synchronized
和ReentrantLock
等加锁。非阻塞同步:
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>
。所以例子中,从图像来看是这样的。
查看 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); | |
} |
最后需要强调的是,我们在外部使用的是 ThreadLocal
的 get
和 set
(以及 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 版