【多线程】一文详解ThreadLocal

概念

ThreadLocal叫做线程变量,意思是ThreadLocal填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量
ThreadLocal主要解决了每个线程绑定自己的值,通过使用get()set()方法,获取默认值或者将其值更改为当前线程的副本从而避免了线程安全问题,避免了多线程之间的共享和竞争问题

ThreadLocal 是一个泛型类,提供了一种简单的方式来创建线程本地变量。每个线程都可以通过 ThreadLocal 获取自己的变量副本,而不会影响其他线程的副本
在这里插入图片描述

ThreadLocal的简单应用

ThreadLocal中常见的API

在这里插入图片描述

ThreadLocal的使用案例

class House{
    int saleCount;//卖方总数

    public synchronized void soldHouse(){
        saleCount++;
    }

    //为每一个线程提供一个副本,来记录每个线程分别卖了多少房子
    ThreadLocal<Integer> OnesaleCount=ThreadLocal.withInitial(() -> 0);//初始化
    //每一个线程卖了一个房子,OnesaleCount+1
    public void OnesaleCountByThreadLocal(){
        OnesaleCount.set(1+OnesaleCount.get());
    }

}

public class Code01 {

    public static void main(String[] args) {
        House house=new House();
        //1.建立五个线程,每个线程都去买房,再将每个线程的卖房数计数到总线程上
        for (int i = 0; i < 5; i++) {
                new Thread(()->{
                    int size=new Random().nextInt(6)+1;
                    for (int j = 1; j <= size; j++) {
                        house.soldHouse();
                        house.OnesaleCountByThreadLocal();
                    }
                    System.out.println("线程 : "+Thread.currentThread().getName()+"卖出的房子 : "+house.OnesaleCount.get());
                },String.valueOf(i)).start();
        }
      try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println(house.saleCount);
    }
}

运行结果:
在这里插入图片描述

ThreadLocalsynchronized之间的本质区别

  1. Synchronized是用于线程之间的数据共享,ThreadLocal则是用于线程之间的数据隔离
  2. Synchronized是利用锁机制,使变量或者代码块在某一个时刻只能被一个线程访问.而ThreadLocal每一个线程都提供了变量的副本,使得每个线程在某一个时间访问到的并不是同一个对象,这样就实现了多线程对数据共享的隔离.而Synchronized正好相反,它用于在多个线程间通信时能够获得数据共享.

ThreadLocal源码剖析

Thread,ThreadLocal,ThreadLocalMap之间的关系

这里我们翻看源码得出结论:
在这里插入图片描述

所以:Thread中存在一个ThreadLocalMap类型的成员变量,而ThreadLocalMapThreadLocal静态内部类.
这里我们再用图像来帮忙理解:
在这里插入图片描述

接下来我们根据源码来逐个理解.

Thread

public class Thread implements Runnable {
//.......
ThreadLocal.ThreadLocalMap threadLocals = null;
}

说明:每一个线程都有一个指定的ThreadLocalMap成员属性,而每一个ThreadLocalMap属性中又维护了很多的<ThreadLocal,Value>键值对.

ThreadLocalMap

static class ThreadLocalMap {
//在ThreadLocalMap的内部嵌套了一层静态内部类Entry,此处继承了弱引用WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    
     //KV键值对
     //Key值:ThreadLocal
     //value:该ThreadLocal中存储的变量值
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
}

说明:在ThreadLocalMap中又嵌套了一层静态内部类,即Entry,所以KV键值对本身是由Entry来维护的

观察Entryget()方法中是如何应用的:

public class ThreadLocal<T> {

public T get() {
    //获取到当前线程
    Thread t = Thread.currentThread();
    //得到当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    
    //如果当前线程有ThreadLocalMap
    if (map != null) {
        //获取到当前线程的map表中的kv键值对
        ThreadLocalMap.Entry e = map.getEntry(this);//传入threadlocal对象,相当于key
        //如果键值对不为空
        if (e != null) {
            @SuppressWarnings("unchecked")
            //获取值,且将值返回
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();//设置初始的ThreadLocalMap初始值
}
}

观察Entryset()方法中是如何应用的:

public class ThreadLocal<T> {

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {//map存在,直接将值放入
        map.set(this, value);
    } else {//map不存在,先去创建一个map结构
        createMap(t, value);
    }
}
}

通过阅读源码可以理解:ThreadLocalMap是属于每一个线程Thread所独有的成员属性,我们在调用的getset方法时,可以先获取到当前线程,通过当前线程找到ThreadLocalMap,再根据ThreadLocalMap来寻找其内部的键值对结构<TreadLocal,Value>Entry存放.(EntryThreadLocalMap中的一个静态内部类).

ThreadLocal优点

  1. 线程隔离:每个线程都有自己的变量副本,避免了多线程环境下的共享和竞争问题
  2. 简单易用API 设计简单,使用方便。
  3. 避免锁:通过使用线程局部变量,可以避免使用显式锁,从而提高性能

注意事项

  1. 内存泄漏:如果不调用 remove() 方法,线程局部变量可能会导致内存泄漏,特别是在使用线程池的场景中
  2. 适用场景ThreadLocal 适用于存储用户会话信息数据库连接事务管理需要线程隔离的场景

ThreadLocal中的内存泄漏问题

 public class ThreadLocal<T> {
 public void remove() {
          //通过获取当前线程获取到ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         //如果ThreadLocalMap不为空
         if (m != null)
             m.remove(this);//直接删除掉ThreadLocal所对应的键值对结构
     }
}

remove方法,直接将ThrealLocal 对应的值从当前线程Thread对应的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄漏的问题。

什么是内存泄漏问题?
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏.

弱引用

 static class ThreadLocalMap {
   //这里的Entry继承了WeakReference弱引用对象
   static class Entry extends WeakReference<ThreadLocal<?>> {
       Object value;
       Entry(ThreadLocal<?> k, Object v) {
           super(k);
           value = v;
       }
   }

我们发现,这里的Entry使用了WeakReference<ThreadLocal<?>>进行封装,那么什么是弱引用?这里又为什么使用弱引用呢?


这里我们可以先移步此篇博文【JVM】强引用,软引用,弱引用,虚引用分别是什么分别是什么,了解前置知识.下面我们就假设你已经了解了什么是弱引用

为什么Entry需要使用弱引用

public static void main(String[] args) {
    ThreadLocal<String>  tl=new ThreadLocal<>();
    String v="treadLocal1 开始 ";
    tl.set(v);
    tl.get();
    //主线程结束,栈帧销毁
}

在这里插入图片描述
当该方法执行完毕之后,栈帧销毁,强引用tl没有了.但此时线程还没结束,所以ThreadLocalMap还存在,其中的entry里某个Key引用还指向这个对象:

  • 若这个Key引用是强引用,就会导致Key指向的ThreadLocal对象不能被gc回收,从而导致内存泄漏;
  • 若这个Key引用是弱引用,就能大概率减少内存泄漏的问题.使用弱引用,可以使ThreadLocal对象在方法执行完毕后顺利被回收EntryKey引用指向null.

但如此便可以保证不会发生内存泄漏问题了吗?并不是这样的,如果Key中有null值,还是会存在内存泄漏的问题.

  • ThreadLocalMap使用ThreadLocal弱引用作为Key,如果一个ThreadLocal没有外部的强引用引用他(手动置空),那么系统gc的时候,这个ThreadLocal势必会被回收,那么这样一来,ThreadLocalMap中就会出现KeynullEntry,就没有办法访问这些Keynullvalue,如果当前线程迟迟不结束的话(线程池),这些keynullEntryvalue就会一直存在一个强引用链.
  • 虽然弱引用,保证了Key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get方法或者set方法时,发现keynull才会去回收整个entry,value,因此弱引用不能100%保证内存不泄漏.我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它,尤其是在线程池中,如果我们不手动调用remove方法,那么后面的线程就有可能会获取到上一个线程遗留下来的value值,造成bug.
    在这里插入图片描述

清除脏Entry

我们再一次深入源码,理解一下我们是如何在get方法和set方法中对key=nullentry进行清除的.

//set()方法
public class ThreadLocal<T> {
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //调用set方法
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}
}

-------------------------------------------------------------------------------------

static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
 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)]) {
        //如果此时的key不为空
        if (e.refersTo(key)) {
            e.value = value;//将value值放到value位置上
            return;
        }
        //如果此时的key是null
        if (e.refersTo(null)) {
            //当key是null的时候调用该方法
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
}

replaceStaleEntry方法是怎样实现在keynull时,保证value可以被垃圾回收?在这里插入图片描述

建议

在<阿里巴巴开发规范手册>中,建议ThreadLocal对象使用static来修饰.
说明:ThreadLocal针对一个线程内的所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说类第一次使用的时候被装载,只分配一块存储空间,所有此类的对象(在同一个线程内定义的)都可以操作这个变量.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值