ThreadLocal学习与内存泄漏解析

一、ThreadLocal是干嘛的

ThreadLocal是一个线程局部变量,也就是属于线程私有,在不同线程之中不共享,在同一个线程之中是共享的。可以想象,一个变量会在该类的多个方法中使用,就可以把这个变量设计到形参中,各个方法传递使用。但在多个方法设计中,都增加那样的参数,接口变得复杂,维护成本也增大。这时候就可以用上ThreadLocal。ThreadLocal可以使用的地方很多,后面再细说。

二、ThreadLocal怎么使用

ThreadLocal只有四个public方法(get,set,remove,withInitial),和一个protected方法(initialValue),get用来获取变量值;set方法设置变量值;remove方法清空;withInitial初始化变量值;作用域为protect类型的initialValue方法供子类重写,初始化变量值。
当你需要一种线程内部的全局变量的时候可以使用ThreadLocal。我们先来比较一下正常的全局变量,和ThreadLocal类型的全局变量有啥区别
正常的:

public class SimpleTest {
    private static Integer local = 0;

    @Test
    public void testLocal() throws InterruptedException {
        for(int i = 0;i < 3; i++) {
            new Thread(new LocalRunnable()).start();
        }
    }
    static class LocalRunnable implements Runnable {
        @Override
        public void run() {
            for(int i = 0; i< 1000 ;i ++) {
                local +=1;
                System.out.println(Thread.currentThread().getName()+"-"+local);
            }
        }
    }
   }

定义全局变量local,然后在testLocal中创建三个线程去执行加一操作。
打印结果:
在这里插入图片描述
这里我们不看并发问题,因为ThreadLocal本身不是用来解决并发问题的。仅考虑local变量是被我们三个线程共享了。每个线程操作了local后都会影响其他线程的操作。如果我们把local改为ThreadLocal类型的会是什么样的?
ThreadLocal类型:

public class SimpleTest {
  
   private static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
       @Override
       protected Integer initialValue() {
           return 0;
       }
   };
   @Test
   public void testLocal() throws InterruptedException {
       for(int i = 0;i < 3; i++) {
           new Thread(new LocalRunnable()).start();
       }
      }
   static class LocalRunnable implements Runnable {
       @Override
       public void run() {
           for(int i = 0; i< 5 ;i ++) {
               local.set(local.get() +1);
               System.out.println(Thread.currentThread().getName()+"-"+local.get());
           }
       }
   }
  }

我们将local对象改为ThreadLocal类型的全局静态变量,打印结果如下:
在这里插入图片描述
可以看出,三个线程打印结果互不干扰。现在的local变量是线程间隔离的,但在线程内部是共享的。因此ThreadLocal适用于每个线程中需要有自己单独的实例,且该实例需要在多个方法中被调用,也即线程之间隔离,而在方法或类中共享的场景。

三、ThreadLocal源码浅析

这里我们简单的看一下ThreadLocal常用的几个方法实现原理。ThreadLocal内部维护了一个静态类ThreadLocalMap,ThreadLocalMap内维护了一个静态类Entry存储key,value。ThreadLocalMap可以扩容,也需要解决哈希碰撞,我们可以近似的看做一个HashMap。
需要注意的是Entry继承了WeakReference,也就是Entry持有的是key的弱引用,value的强引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  • 我们先来看看get()方法:
 public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

先获取当前线程,然后通过当前线程获取ThreadLocalMap,我们看这个getMap方法

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

,返回的是t.threadLocals,也就是ThreadLocalMap是在Thread中保存的实例。获取到map之后我们我们通过this(当前的Thread对象)获取到Entry,如果能通过key获取到value,就返回value,是null,就调用setInitialValue方法给个初始值,如果子类没有重写initialValue方法(那个protected方法),那么默认value设置为null。

  • set方法也好理解,还是先获取到ThreadLocalMap,然后往里面插入值,key就是当前的ThreadLocal对象,value值。
 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  • remove方法就不说了,清空Entry。
四、内存泄漏分析

为什么说使用ThreadLocal可能会导致内存泄漏呢,因为我们Entry对key是弱引用,但对value是强引用。简单说一下java中的四中引用,如果一个 对象被强引用,那么它永远都不会被GC回收,即使OOM;如果一个对象被软引用,在内存够的时候不会被回收,但内存不够了,就会回收;如果一个对象被弱引用,只要发生GC,就会回收;虚引用相当于没有引用,通常是为了当虚引用对象被GC回收的时候能收到一个系统通知,用来跟踪GC回收的活动。
所以我们这里entry的key,如果被回收了,但是value值还存在,就是导致存在key为null的Entry。如果我们当前线程一直不结束,就会导致这样的值越来越多造成内存泄漏。所以在源码中,当我们调用get,set,remove方法的时候,都会去检查是否有key为null的entry,将value也设置null,便于回收。但是我们最好在使用完毕后手动调用remove方法,该放法会将key的null的value置空,并将key和value都为null的entry置空。
为什么不把value也设置成弱引用呢?
因为从我们使用的方式可以看出,我们通常持有的都是ThreadLocal对象的强引用,通过ThreadLocal的get方法获取我们的value,而不会去直接持有value的强引用。如果把value设置成弱引用,会被GC回收,就没法玩了。
为什么Key不设置成强引用?
因为如果key为弱引用,当持有ThreadLocal强引用的对象声明周期结束或者对象替换掉指向这个key的引用后,强引用就会断掉,之后key会被gc回收,那么就便于我们的get、set等方法将entry清空。

五、为什么推荐声明为private static

在ThreadLocal类的注释上面,推荐将ThreadLocal申明为private static类型。在大多数的框架代码中,都是使用的private static final申明的。静态变量和实例变量其中一个区别就是,实例变量属于类实例,每创建一个类实例 就会创建一个实例变量。静态变量属于类,创建实例变量并不会新建一个静态变量,静态变量在类字节码加载后就创建了,所有的类实例公用静态变量。因为我们ThreadLocal作用就是为了在同个线程内共享变量,所以没必要每创建一个实例就新建一个ThreadLocal变量,申明为静态节省内存资源。但是同时要注意一个问题了,因为申明为静态类型了,所以当类没有销毁时,该ThreadLocal变量被一引用,当我们使用完毕后一定要注意手动调用remove方法。

六、使用场景例举
  • 我们知道SimpleDateFormat是非线程安全的,比如当我们调用parse方法的时候,他会调用Calendar的clear方法,然后在调用set方法,如果多线程并发,一个线程在set的时候被另一个clear掉了,parse就会出问题了。当然我们SimpleDateFormat申明的是全局变量,要是局部变量就不说了。这个时候用可以用ThreadLocal来定义SimpleDateFormat了。这样SimpleDateFormat在单个线程中共享,在线程之间是隔离的,也就可以避免并发问题了。
  • spring事务管理
    事务是和线程绑定起来的,Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。
public abstract class TransactionSynchronizationManager {

	private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
//线程绑定资源,map里key是DataSource,value是ConnectionHolder,也就是绑定某个数据源的Connection
	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");
//事务注册的事务同步器
	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
			new NamedThreadLocal<>("Transaction synchronizations");
//当前线程名称
	private static final ThreadLocal<String> currentTransactionName =
			new NamedThreadLocal<>("Current transaction name");
//当前事务只读状态
	private static final ThreadLocal<Boolean> currentTransactionReadOnly =
			new NamedThreadLocal<>("Current transaction read-only status");
//当前事务隔离级别
	private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
			new NamedThreadLocal<>("Current transaction isolation level");
//事务同步开启
	private static final ThreadLocal<Boolean> actualTransactionActive =
			new NamedThreadLocal<>("Actual transaction active");

......
  • 日志打印,同一个线程的日志或者同一个事务的日志一起打印等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值