ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其getiset方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来
特别注意:ThreadLocal设计的目的是帮助当前线程有属于自己的变量,并不是为了解决并发或共享变量的问题
需求1:: 5个销售卖房子,集团高层只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部分发送奖金
public class DeadlockExample {
//5个销售卖房子,集团高层只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部分发送奖金
public static void main(String[] args) throws InterruptedException {
Hourse hourse = new Hourse();
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
int size = new Random().nextInt(5) + 1;
System.out.println(Thread.currentThread().getName()+"线程卖了"+size+"套");
for (int j = 0; j < size; j++) {
hourse.saleHourse();
}
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("一共卖了"+hourse.getSaleCount());
}
}
class Hourse {
int saleCount = 0;
public synchronized void saleHourse() {
++saleCount;
}
public int getSaleCount() {
return saleCount;
}
}
3线程卖了1套
1线程卖了5套
2线程卖了5套
4线程卖了4套
0线程卖了5套
一共卖了20
需求2: 5个销售卖完随机数房子,各自独立销售额度,自己业绩按提成走,分灶吃饭,各个销售自己动手,丰衣足食
public class DeadlockExample {
//5个销售卖房子,集团高层只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部分发送奖金
public static void main(String[] args) throws InterruptedException {
Hourse hourse = new Hourse();
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
for (int j = 0; j < size; j++) {
hourse.saleSyncHourse();
hourse.saleThreadLocal();
}
System.out.println(Thread.currentThread().getName() + "线程卖了" + hourse.saleVolume.get() + "套");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("一共卖了" + hourse.getSaleCount());
}
}
class Hourse {
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
int saleCount = 0;
public int getSaleCount() {
return saleCount;
}
public synchronized void saleSyncHourse(){
++saleCount;
}
public void saleThreadLocal(){
saleVolume.set(1+saleVolume.get());
}
}
4线程卖了4套
1线程卖了3套
2线程卖了1套
3线程卖了2套
0线程卖了1套
一共卖了11
原因:因为实际开发的场景中, 我们都是使用线程池,此时有些线程就会存在反复复用的情况,如果没有清理线程的ThreadLocal变量,则到了线程复用的时候,可能还是携带者过去的老数据,导致业务逻辑混乱
结论1:Thread类包含ThreadLocal,ThreadLocal包含了ThreadLocalMap
结论2:ThreadLocalMap中包含了继承弱引用的Entry
结论3:ThreadLocalMap其实就是以ThreadLocal为key ,任意对象obejct为valu的entry
谈到这个问题,我们需要先回归下基础知识
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用(Java默认new一个对象就是强引用)
class MyClass{
@Override
protected void finalize() throws Throwable {
System.out.println("我被调用了,我快要死了");
}
}
public class ReferenceDemo {
public static void main(String[] args) throws InterruptedException {
MyClass myClass = new MyClass();
System.out.println("gc object "+myClass);
myClass =null;
//主动触发gc
System.gc();
Thread.sleep(500);
System.out.println("gc object "+myClass);
}
}
gc object com.tvu.deathLock.MyClass@43556938
我被调用了,我快要死了
gc object null
软引用
class MyClass{
@Override
protected void finalize() throws Throwable {
System.out.println("我被调用了,我快要死了");
}
}
public class ReferenceDemo {
public static void main(String[] args) throws InterruptedException {
SoftReference<MyClass> myClassSoftReference = new SoftReference<>(new MyClass());
System.gc();
System.out.println("gc after:" + myClassSoftReference.get());
}
}
gc 内存够用 after:com.tvu.deathLock.MyClass@43556938
弱引用
Map>imageCache = new HashMap>();
虚引用
1:上面ThreadLocalRef he CurrentThreadRef 分别代码ThreadLocal和CurrentThread的引用,他们分别存储在栈里面
ThreadLocal 再进行set/get/remove的时候都对空的entry进行检查和清除;
set()
虽然set和get方法会帮我们清理entry为null的值,但是如果一个ThreadLocal对象被创建后,在某个线程中调用了set方法设置了一个值,但是在后续的代码中没有调用get或remove方法来清除这个值,那么这个值就会一直存在于这个ThreadLocal对象中,直到线程结束。如果这个线程是一个长期运行的线程,那么这个值就会一直存在于内存中,占用一定的空间,从而导致内存泄漏。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
//计算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
//entry存储的个数,因为是构造函数,所以是1
size = 1;
setThreshold(INITIAL_CAPACITY);
}
===========================threadLocalHashCode 相关代码======================================
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
//HASH_INCREMENT 是为了让hash均匀的分布,尽量避免
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
==========================INITIAL_CAPACITY - 1)============================================
学过HashMap的都知道,容量被设置为2的n次方,当进行-1的时候,此时2进制的最后一位都是1,此时相当于提高了效率,比如此时1和0进行4预算,此时如果为1的话,后面基本上就不用看啦
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;
//计算索引,刚刚分析过啦
int i = key.threadLocalHashCode & (len-1);
//使用线性查找法查找元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//threadlocal对应的key存在,覆盖之前的值
if (k == key) {
e.value = value;
return;
}
//如果不存在,说明被回收啦,当前数据中entry是一个陈旧的元素
if (k == null) {
//用新元素代替旧元素,这个方法进行了不少清理垃圾的动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
//如果key不存在,并且没有找到陈旧的元素,则在空位置上进行新创建新的entry
tab[i] = new Entry(key, value);
int sz = ++size;
//清除e.get() = null的元素
//这种数据关联key关联的对象,已经被回收啦,所以entry(table (index)可以设置为null
//如果没有清楚任何的entry,并且当前的使用量达到了负载银子,那么进行rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
总结:
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能