ThreadLocal应用和原理解析

ThreadLocal主要是实现线程隔离,解决线程安全问题。

ThreadLocal的使用

package thread;

public class ThreadLocalDemo {

   static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0; //初始化一个值
        }
    };

    public static void main(String[] args) {
       Thread[] thread = new Thread[5];
       for(int i = 0;i < 5; i++){
         thread[i] = new Thread(()->{
             int num = local.get(); //获得的值都是0
             local.set(num+5); //设置到local中
             local.remove();
             System.out.println(Thread.currentThread().getName()+"-"+num);
         });
       }
       for (int i= 0; i < 5;i++){
           thread[i].start();
       }
    }
}

在这里插入图片描述

ThreadLocal案例

package thread;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalTest {
    //非线程安全
    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal();

    private static DateFormat getDateFormat(){
        //从当前线程的范围内获得一个DateFormat
        DateFormat dateFormat = threadLocal.get();
        if(dateFormat == null){
            dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            //Thread.currentThread范围内设置一个SimpleDateFormat
            threadLocal.set(dateFormat);
        }
        return dateFormat;
    }

    public static Date parse(String strDate) throws ParseException {
       return getDateFormat().parse(strDate);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20;i++){
            executorService.execute(()->{
                try {
                    System.out.println(parse("2022-07-09 10:00:00"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

在这里插入图片描述

ThreadLocal的原理

  • ThreadLocal能够实现线程的隔离,当前保存的数据,只会存储在当前线程范围内。
    在这里插入图片描述

主要方法

  • set():在当前线程范围内,设置一个值存储到ThreadLocal中,这个值仅对当前线程可见。相当于在当前线程范围内建立了副本。
  • get():从当前线程范围内取出set方法设置的值。
  • remove():移除当前线程中存储的值。
  • withInitial:java8中的初始化方法。

源码分析

public void set(T value) {
    Thread t = Thread.currentThread();
  // 如果当前线程已经初始化了map。
  // 如果没有初始化,则进行初始化。
    ThreadLocalMap map = getMap(t);
    if (map != null) //修改value
      map.set(this, value);
    else //初始化
      createMap(t, value);
 }

createMap

void createMap(Thread t, T firstValue) {
        //绑定当前线程
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  table = new Entry[INITIAL_CAPACITY];  //默认长度为16的数组
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  //计算数组下标
  table[i] = new Entry(firstKey, firstValue); //把key/value存储到i的位置.
  size = 1;
  setThreshold(INITIAL_CAPACITY);
}
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();
// i的位置已经存在了值, 就直接替换。
    if (k == key) {
      e.value = value;
      return;
   }
//如果key==null,则进行replaceStaleEntry(替换空余的数组)
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
   }
 }
  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}
  • 把当前的value保存到entry数组中
  • 清理无效的key
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
               int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;
  Entry e;
  int slotToExpunge = staleSlot;
  for (int i = prevIndex(staleSlot, len);
    (e = tab[i]) != null;
    i = prevIndex(i, len))
    if (e.get() == null)
      slotToExpunge = i;
      // Find either the key or trailing null slot of run, whichever
  // occurs first
  for (int i = nextIndex(staleSlot, len);
    (e = tab[i]) != null;
    i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
    if (k == key) {
      e.value = value;
      tab[i] = tab[staleSlot];
      tab[staleSlot] = e;
      if (slotToExpunge == staleSlot)
        slotToExpunge = i;
      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
      return;
   }
    // If we didn't find stale entry on backward scan, the
    // first stale entry seen while scanning for key is the
    // first still present in the run.
    if (k == null && slotToExpunge == staleSlot)
      slotToExpunge = i;
 }
  // If key not found, put new entry in stale slot
  tab[staleSlot].value = null;
  tab[staleSlot] = new Entry(key, value);
  // If there are any other stale entries in run, expunge them
  if (slotToExpunge != staleSlot)
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
  • 如果当前值对应的entry数组中key为null,那么该方法会向前查找到还存在key失效的entry,进行清理。
  • 通过线性探索的方式,解决hash冲突的问题。

key的清理过程

  • 向前有脏Entry向后查找到可覆盖的Entry
    在这里插入图片描述
  • 向前有脏Entry向后未查找到可覆盖的Entry
    在这里插入图片描述
  • 向前没有脏Entry向后找到可覆盖的Entry
    在这里插入图片描述
  • 向前没有脏Entry向后未找到可覆盖的Entry
    在这里插入图片描述

内存泄漏问题

上面代码中的expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现get 和set 方法都可能触发清理方法 expungeStaleEntry() ,所以正常情况下是不会有内存溢出的 但是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再使用的时候调用remove(),加快垃圾回收,避免内存溢出。

退一步说,就算我们没有调用get 和set 和remove 方法,线程结束的时候,也就没有强引用再指向ThreadLocal 中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会被回收掉,但是有一种危险是,如果线程是线程池的, 在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap 和里面的元素是不会回收掉的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泡^泡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值