1 ThreadLocol基本认识
- 上图中每个线程都拥有自己的ThreadLocal实例对象,将该对象作为key,在hreadLocalMap的获取线程私有的变量值(object类型的value)。
1-1 概述
ThreadLocal作用:通过每个线程内部的ThreadLocalMap的key(ThreadLocal实例)去访问自己独立的线程内部变量。
注意点:threadLocal的实例推荐设置为private static
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
1-2 简单使用
下面代码:
- Test1中String类型变量在多个线程中共享,
- Test2中定义String类型的ThreadLocal实例,线程变量的设置与获取都通过实例进行。
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
class Test1 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
class Test2{
ThreadLocal<String> t1 = new ThreadLocal<>();
public String getContent() {
String s = t1.get();
return s;
}
public void setContent(String content) {
t1.set(content); // 将变量与ThreadLocal进行绑定
}
}
@Slf4j
public class Test{
public static void main(String[] args) {
/*测试普通的成员变量*/
Test1 test1 = new Test1();
for (int i = 0; i < 5; ++i) {
Thread thread = new Thread(() -> {
test1.setContent(Thread.currentThread().getName());
try {
Thread.sleep(new Random().nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("获取{}数据", test1.getContent());
});
thread.start();
}
/*测试ThreadLocal*/
// Test2 test2 = new Test2();
// for (int i = 0; i < 5; ++i) {
// Thread thread = new Thread(() -> {
// test2.setContent(Thread.currentThread().getName());
// try {
// Thread.sleep(new Random().nextInt(100));
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// log.debug("获取{}数据", test2.getContent());
//
// });
// thread.start();
// }
}
}
不使用ThreadLocal的执行结果:
- 所有线程都获得最后一次修改的数据
21:35:56.498 [Thread-3] DEBUG MIANSHI.Test - 获取Thread-4数据
21:35:56.522 [Thread-0] DEBUG MIANSHI.Test - 获取Thread-4数据
21:35:56.541 [Thread-1] DEBUG MIANSHI.Test - 获取Thread-4数据
21:35:56.545 [Thread-4] DEBUG MIANSHI.Test - 获取Thread-4数据
21:35:56.553 [Thread-2] DEBUG MIANSHI.Test - 获取Thread-4数据
使用ThreadLocal的执行结果:
21:34:53.833 [Thread-2] DEBUG MIANSHI.Test - 获取Thread-2数据
21:34:53.837 [Thread-4] DEBUG MIANSHI.Test - 获取Thread-4数据
21:34:53.842 [Thread-1] DEBUG MIANSHI.Test - 获取Thread-1数据
21:34:53.869 [Thread-3] DEBUG MIANSHI.Test - 获取Thread-3数据
21:34:53.920 [Thread-0] DEBUG MIANSHI.Test - 获取Thread-0数据
总结:作为成员属性的ThreadLocal能够保证线程之间变量的独立设置与访问(实现线程之间变量读写的隔离)。
问题:ThreadLocal与synchronzied的区别?
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制的本质是对变量进行加锁,这样多个线程对变量的操作串行执行 | 采用“空间换时间”的方式,为每个线程提供变量的副本,从而实现同时访问而不相互干扰 |
侧重点 | 用于线程的之间的同步 | 线程之间的数据隔离 |
2 ThreadLocal应用:数据库转账操作案例
需求分析:数据库中用户表中存储多个用户的的账户余额,实现代码操作修改数据表中账户余额,实现多个用户之间的转账操作。
关键点:对于每个转账操作必须保证两个用户的数据修改操作要么同时成功,要么同时失败。
具体实现策略:采用数据库的事务,在具体代码实现时需要注意两点:
- 服务层与数据访问层的数据库连接对象必须是同一个对象
比较笨的策略:将服务层的对象通过参数的形式传递到数据访问层
- 采用多线程操作数据库,数据访问层中每个线程操作的数据库对象必须是同一个对象。
同一时刻,需要进行用户A与用户B的转账以及用户C与用户D转账。
转账流程:
step1:获取进行操作的数据库连接对象。
step2:通过连接对象设置转账参数
step3:通过连接提交转账事务
step4:关闭连接
如果不加以限制,会出现线程1执行完step1和step2,可能出现线程2执行了相同的代码段,将step1与step2建立的连接与设置的参数改成了自己的,然后线程1用线程2的连接与参数操作,这样相当于线程2的转账操作执行了两遍。
针对上述问题,比较笨的策略:就是在step1-step3的代码段进行加锁。保证每个线程操作的数据库对象不会因为多线程原因变成其他的连接对象
采用ThreadLocal管理数据库的连接进行转账操作的操作方法:
改造连接获取代码,在类中定义ThreadLocal<Connection>实例
转账步骤:
step1:直接获取当前线程绑定的连接对象。
step2:如果当前线程的连接对象是空的,
2.1 从数据库的连接池中获取连接
2.2 将此连接对象与当前数据访问层的线程绑定。
注意点:上述过程中,数据库的连接被释放的话,必须解除连接对象与ThreadLocal实例的绑定,避免内存泄露。
问题:ThreadLocal在实际编码中有哪些益处?
在特定场景下有以下两个好处:
1)数据获取比较方便,数据通过ThreadLocal实例与每个线程绑定,不需要另外传递参数,降低了代码之间的耦合度
2)比较方便的实现了线程之间访问数据的隔离。每个线程维护自己的数据,不需要同步机制,性能好,并发性好
3 ThreadLoal的深入理解
3-1 ThreadLocal的设计思想概述
JDK早期的的设计思想(图片上半部分):每个ThreadLocal实例维护1个map,然后使用线程对象作为map的key,要存储的局部变量就是map的value,这种根据key访问value的方式确保了各个线程变量隔离的效果。
JDK1.8的设计思想(图片下半部分):每个Thread维护1个Map,Map的key是ThreadLocal实例本身,value是要存储的变量的副本值。
- Thead对应的map交给ThreadLocal管理,变量值的设置与获取都通过ThreadLocal实例进行。
Thread.java的源码中定义了两个ThreadLocal相关的Map:
/*ThreadLocalMap是ThreadLocal的静态内部类*/
ThreadLocal.ThreadLocalMap threadLocals = null; // Thread中与threadLocal关联的字段
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // Thread中与threadLocal关联的字段
问题:JDK8的ThreadLocal的设计方案相比较早期的方案设计有哪些好处?
1)每个map存储的Entry更少。
到底线程数目多,还是ThreadLocal实例多
2)当Thread销毁时,该线程对应的ThreadLocalMap也会随之销毁,减少内存的使用。
3-2 ThreadLocal中map相关字段
/*存储当前的ThreadLocal实例对应的的hashcode*/
private final int threadLocalHashCode = nextHashCode();
/*静态原子整数类型用于为每个ThreadLocal实例生成对应的hashcode,hashcode的获取策略是从0开始线性增加固定值)*/
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}
/*创建Map的函数*/
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {}
/*ThreadLocal的map被定义为静态内部类,*/
static class ThreadLocalMap {}
总结:ThreadLocal总体上可以看作一个比较特殊的map,其内部的实现也是围绕map的操作展开,该map比较特殊在于其key为ThreadLocal实例,value是变量副本。
ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。
3-3 ThreadLocal源码
thread.java中与ThreadLocal关联的字段:
ThreadLocal.ThreadLocalMap threadLocals = null; // 与threadLocal关联的字段
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 与threadLocal关联的字段
提供调用的方法名称 | 作用 | 备注 |
---|---|---|
set | 设置当前key对应的value | key是ThreadLocal实例本身,value是要存储的变量的副本值 |
get | 获取当前key对应的value | |
remove | 删除key和value键值对 | 不需要的键值对一定要删除,避免内存泄漏问题 |
3-3-1 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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set方法流程总结:
step1:获取当前线程的ThreadLocalMap
step2:ThreadLocalMap不为空,则设置值
step3:为空,则新建ThreadLocalMap并设置值。
3-3-2 get方法
public T get() {
/*step1: 获取当前线程对象的ThreadLocal.ThreadLocalMap对象*/
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
/*step2: map不为空,则根据key返回value*/
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
/*step3: map为空,则创建map并设置对于key的value为null并返回*/
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() { // 默认初始值为null,可以被重写
return null;
}
set/get总结:
- ThreadLocal的静态内部类map允许value值为null
- map的创建是懒惰的,只有第一次访问和设置map为空的情况下才创建map。
3-3 remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4 ThreadLocal中ThreadLocalMap的源码分析
ThreadLocalMap是ThreadLocal的内部类,没有实现map接口,内部的entry(键值对)也是独立实现的。
- key是继承弱引用对象的ThreadLocal实例
- value是想要存放的变量值
4-1 ThreadLocalMap包含的重要的属性
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // value可以是任意类型的对象
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0; // 必须是2的倍数
private int threshold; // Default to 0
private void setThreshold(int len) { // 扩容的阈值是table长度的2/3
threshold = len * 2 / 3;
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
// [ˈentri]
特点:
1)ThreadLocal的map的entry采用了弱引用
2)Thread的扩容阈值(装载因子)是 2/3,不同于之前的hashmap为 0.75,这应该与其hash碰撞的处理策略相关。
---装载因子是时间与空间代价的tradeoff,较高的的装填因子会降低空间的开销但是会提升查找代价(反应在set/get方法上)
3)hash函数的计算方式与hashmap一致,都是通过与运算等效替代模运算,要求table的大小必须是2的幂。
4-2 ThreadLocalMap中key的弱引用的作用
作用:key采用弱引用主要是为了方便及时的释放hashtable中无用的键值对,从而一定程度上能够缓解内存泄漏问题。具体的实现机制如下:
1)如果没有手动调用map的remove方法删除无用的键值对,采用弱引用的key在内存不足的场合会由于垃圾回收变为null.
2)在map的getEntry/set方法中对key的null的Entry进行额外处理,将其对应的value也设为null
总结:这种情形下,Entry对象的回收,其key是由于弱引用在内存不足的场合被回收,value是必须等待key为null后再次访问map才能被回收。仍然存在
内存泄漏的风险
- 存在这样的可能性即key变为null,程序代码中再也没有访问过map中这个key。因此在实际中确定线程使用完这个ThreadLocal实例对象,一定要在map中及时remove这个实例对象。
getEntry()源码
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
/*情况1:计算出的桶下标对应的Entry不为空,并且其key就是目标关键字,直接返回对应的value*/
if (e != null && e.get() == key)
return e;
/*情况2:从当前位置向下线性查找*/
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
/*线性查找的过程中会有三种情况发生:
1)找到了对应的entry并返回
2)发现key为null的entry,将其value
*/
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;
}
// expunge [ɪkˈspʌndʒ] :擦除
// stale [steɪl]: 陈腐的; 不新鲜的; 走了味的;
/*释放Entry元素,传入需要擦除的entry的桶下标*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
/*step1:将该位置置位null,并且将该entry的value置为null*/
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
/*step2:从删除位置的下一个元素开始进行再散列,显然必定会出现hash碰撞,
这个时候仍然采用线性探测的方法为entry分配桶下标。
注意:删除的过程中可能会碰到其他key为null,value不为null的entry,同样清理掉。
*/
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;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
问题:弱引用在ThreadLocalMap中有什么作用(重要)?
总体作用:将map的key设置为弱引用,能够较为方便的及时的对无用的键值对对象进行垃圾回收,提升了map的性能,一定程度上也缓解了内存泄漏问题。
- 弱引用无法避免内存泄漏,内存泄漏根本原因在于编码过程没有及时remove掉无用的键值对。
具体机制:ThreadLocalMap在实现set/get/remove方法时,在线性查找的过程中,同时会将那些key为null的entry的value设置为null,让其被垃圾回收。
补充:ThreadLocalMap采用线性探测的策略解决hash碰撞,在删除entry后同时进行了rehash。
- 如果仅仅只是删除,就会留了一个洞,洞后面的entry就再也找不到了,因此需要对后面的元素重新进行一次set操作,将洞填上。(相当于做了一次紧凑操作)
4-3 ThreadLocalMap的内存泄漏问题
ThreadLocal的内存泄漏问题如何避免?
方式1:线程使用完ThreadLocal,及时调用remove方法释放其相关联的entry。
方式2:线程使用完ThreadLocal,当前的Thread也停止运行。
方式2在某些场景下无法被控制,比如线程池中是不会轻易停止线程的。
因此实际使用ThreadLocal避免内存的泄漏的策略就是及时调用remove,j
remove()源码
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i); // 可以看到remove方法中也会清理那些key为null的entry
return;
}
}
}
4-4 ThreadLocalMap的hash补充
补充1:key的hashcode计算采用线性分配的方式,每个ThreadLocal的实例对象会分配唯一的theadLocalHashcode用于hash函数计算
- 采用原子整数类型保证hashcode分配的线程安全性
动机:为每个实例对象所选取的hashcode遵循数学规律,能够让entry均匀的分配在2的幂长度的table中。
/*静态原子整数类型用于为每个ThreadLocal实例生成对应的hashcode,hashcode的获取策略是从0开始线性增加固定值)*/
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}
补充2: hash函数实现采用&运算,要求entry的数组长度是2的幂,
- 相比较HashMap采用key.hashcode()方法获取hashcode,ThreadLocalMap由于key只能是ThreadLocal实例,因此采用补充1的方式固定的为每个实例对象分配hashcode.
ThreadLocalMap源码
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
HashMap源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 也是采用&运算,hash表的大小是2的幂
tab[i] = newNode(hash, key, value, null);
...
补充3:hash碰撞的解决策略是线性探测法,区别于HashMap的链表法(红黑树)
- 线性探测法删除元素的时候不仅仅需要删除元素,还要对后续的元素进行rehash。
- 线性探测时候将table当作环形数组处理
总结:理解ThreadLocalMap的key的hashcode如何生成的,hash函数,以及hash冲突的策略。