ThreadLocal黑科技解密:从原理到实战,一文搞定线程数据隔离难题!

ThreadLocal简介

ThreadLocal的作用, 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题。同时,也实现了线程内的资源共享

ThreadLocal的特点:

  1. ThreadLocal 可以为当前线程关联一个数据(可以是普通变量,可以是对象,也可以是数组、集合)。(它可以像Map一样存取数据,key为当前线程)
  2. 每一个ThreadLocal对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个ThreadLocal对象实例。
  3. 每个ThreadLocal对象实例定义的时候,一般都是private static修饰
  4. ThreadLocal中保存数据。在线程销毁后,会由VM虚拟自动释放。

线程数据存储三大神器对比

特性ThreadLocal 线程本地synchronized 同步Lock 锁
数据可见性线程私有共享区同步访问共享区可控访问
锁机制无锁操作悲观锁显式锁控制
性能损耗接近零损耗高竞争下性能衰减中等控制开销
适用场景线程上下文传递临界区资源保护复杂锁逻辑实现

ThreadLocal核心机制图解

**在这里插入图片描述**
部分源码

public class ThreadLocal<T> {
  --begin  跨线程共享
  //黄金分割数哈希
  private final int threadLocalHashCode = nextHashCode();
  
  private static AtomicInteger nextHashCode =
        new AtomicInteger();
  --end  跨线程共享
  
  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();
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }
     
     --begin 线程私有
     static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

       
        private static final int INITIAL_CAPACITY = 16;
        .....

以空间换时间:为每个使用该变量的线程提供一个独立的的副本,所以每一个线程都可以改变自己的变量副本,而不会影响其他线程所对应的变量副本。
原理:每一个Thread对象均含有一个 ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值。ThreadLocalMap由一个个Entry 对象构成,Entry继承自WeakReference<ThreadLocal<?>>,一个Entry由ThreadLocal对象和object构成。由此可见,Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。
1.当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。 再以当前ThreadLocal对象为key,资源对象作为value,放入当前线程的ThreadLocalMap对象中。
2.get方法执行过程类似。ThreadLocal首先会获取当前线程对象, 然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
3.调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
注意:

  • 用来存取数据:set()/get()
  • 使用ThreadLocal存储的数据,线程安全
  • 用完记得调用remove方法释放

多维度应用及常见解析

常见使用场景

使用场景
1.在进行对象跨层传递的时候,使用ThreadLoca可以避免多次传递,打破层次间的约束。
2.线程间数据隔离
3.进行事务操作,用于存储线程事务信息。
4.数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbcconnection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离。

线程间数据隔离,每个线程需要独享对象

这种通常是工具类,如SimpleDateFormatRandom
举例:SimpleDateFormat。(当多个线程共用这样一个SimpleDateFormat,但是这个类是不安全的)

  • 2个线程分别用自己的SimpleDateFormat,这没问题;
  • 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅,但勉强可以接受
  • 但是当需求变成了1000,那么必然要用线程池,消耗内存太多;
  • 但是每一个SimpleDateFormat我们都需要创建一遍,那么太耗费new对象了,改成static共用的,所有线程都共用一个simpleDateFormat对象,但这是线程不安全的,容易出现时间一致的情况,在调用的时候,可加锁来解决,但还是不优雅;
  • 用ThreadLocal来解决该问题,给每个线程分配一个simpledateformat,可这个threadlocal是安全的;
package threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 描述:利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
 */
public class ThreadLocalNormalUsage05 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
//        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

跨层传递用户信息

在进行对象跨层传递的时候,使用ThreadLoca可以避免多次传递参数,打破层次间的约束。
如:拦截器中获取登陆用户id并传递

1、定义UserContext类
ublic class UserContext {
    private static final ThreadLocal<Long> tl = new ThreadLocal<>();

    /**
     * 保存当前登录用户信息到ThreadLocal
     * @param userId 用户id
     */
    public static void setUser(Long userId) {
        tl.set(userId);
    }

    /**
     * 获取当前登录用户信息
     * @return 用户id
     */
    public static Long getUser() {
        return tl.get();
    }

    /**
     * 移除当前登录用户信息
     */
    public static void removeUser(){
        tl.remove();
    }
}

慎用场景

第一点(线程池里线程调用ThreadLocal):因为线程池里对线程的管理都是线程复用的方法,所以在线程池里线程非常难结束,更有可能的是永远不会结束。这就意味着线程的持续时间是不可估测的,甚至会与JVM的生命周期一致。
第二点(在异步程序里):ThreadLocal的参数传递是不可靠的,因为线程将请求发送后,不会在等待远程返回结果就继续向下运行了,真正的返回结果得到以后,可能是其它的线程在处理。
第三点:在使用完ThreadLocal,推荐要调用一下remove()方法,这样会防止内存溢出这种情况的发生,因为ThreadLocal为弱引用。如果ThreadLocal在没有被外部强引用的情况下,在垃圾回收的时候是会被清理掉的,如果是强引用那就不会被清理。

ThreadLocal内存泄露原因,如何避免?

前置知识回顾:
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中, 用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocalMap 为什么使用弱引用作为key?
若key使用强引用(x)
当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
若key使用弱引用(true)
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有 ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。 当key为null,在下一次ThreadLocalMap调用set()、get()、remove()方法的时候会被清除value值。
因此,Threadlocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,Thread可能需要长时间运行(如线程池中的线程),如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存 在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值