[Java并发与多线程](十六)ThreadLocal

1、ThreadLocal

ThreadLocal的作用是提供线程内的局部变量不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的传递的复杂度。注意不能将其翻译为线程本地化或本地线程,英语恰当的名称应该叫做:CopyValueInfoEveryThread

1.1 两大使用场景——ThreadLocal的用途

1.1 典型场景1:每个线程需要一个独享的对象

每个Thread内有自己的实例副本,各个线程之间的实例是不共享的,比喻:教材只有一本,一起做笔记有线程安全问题,复印后没问题。
问题描述:
a、2个线程分别用自己的SimpleDateFormat,这没问题;
b、后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅(应该复用对象),但勉强可以接受;
c、但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多了);
d、所有的线程都共用同一个simpleDateFormat对象
e、出现的重复日期问题,可以用synchronized关键字解决——问题是效率就变低了——解决:使用ThreadLocal

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

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            final 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() {
            //保证在每个线程中只有1份
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };

    //Lambda表达式,和上面等效
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

利用ThreadLocal,给每个线程分配自己的dateFormat对象,同时保证了线程安全,还高效利用内存

1.2 典型场景2:每个线程内需要保存全局变量

一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),再从service-2()传到service-3(),以此类推,但是这样做会导致代码冗余且不易维护;目标:每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦

当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但无论用什么,都会对性能有所影响。更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可以达到保存当前线程对应的用户信息的目的;

在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦。

public class ThreadLocalNormalUsage06 {

    public static void main(String[] args) {
        new Service1().process();
    }
}

//生成user对象
class Service1 {
    public void process() {
        User user = new User("超哥");
        //把user放到holder中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
        user = new User("张姐");
        UserContextHolder.holder.set(user);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);

        UserContextHolder.holder.remove();//主动remove,避免OOM
    }
}


class UserContextHolder {
    //定义成静态的,可以在多个方法中直接取到它
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}

1.2 ThreadLocal方法

  1. 它所强调的是同一个请求内(同一个线程内)不同方法间的共享;
  2. 不需重写initialValue()方法,但是必须手动调用set()方法。

1.3 ThreadLocal的两个作用

  1. 让某个需要用到的对象在线程之间隔离(每个线程都有自己的独立的对象);
  2. 可以在任何方法中都可以轻松获取到该对象;

1.4 使用ThreadLocal带来的好处

  1. 达到线程安全;
  2. 不需要加锁,提高执行效率
  3. 更高效地利用内存,节省开销:相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销;
  4. 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数。ThreadLocal使得代码耦合度更低,更优雅。

1.5 ThreadLocal原理

ThreadLocal内部维护的是一个类似MapThreadLocalMap数据结构,key为当前对象的Thread对象,值为Object对象。默认情况下这两个变量都是null,只有当前线程调用ThreadLocal类的setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的get()set()方法。

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();
}
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return 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);
}

最终的变量是放在了当前线程的ThreadLocalMap中,并不是存在 ThreadLocal上,ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThreadLocal类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

搞清楚ThreadThreadLocal以及ThreadLocalMap三者之间的关系
a、1个Thread有且仅有1个ThreadLocalMap对象;
b、1个Entry对象的Key弱引用指向1个ThreadLocal对象
c、1个ThreadLocalMap对象存储多个Entry对象;
d、1个ThreadLocal对象可以被多个线程共享;
e、ThreadLocal对象不持有ValueValue由线程的Entry对象持有。

1.6 ThreadLocal主要方法介绍

  1. T initialValue():初始化
    a、该方法会返回当前线程对应的"初始值",这是一个延迟加载的方法,只有在调用get()的时候,才会触发;
    b、当线程第一次使用get方法访问变量时,将调用此方法,除非先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法;
    c、通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法;
    d、如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
  2. void set(T t):为这个线程设置一个新值
  3. T get():得到这个线程对应的value。如果是首次调用get(),则会调用initialize来得到这个值;
    a、get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value;
    b、注意,这个map以及map中的key和value都是保存在线程中(Thread类中)的,而不是保存在ThreadLocal中;
  4. void remove():删除对应这个线程的值

2、ThreadLocal注意点

2.1 内存泄露

什么是内存泄露(OOM)某个对象不再有用,但是占用的内存却不能被回收
弱引用:特点是,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收;
强引用:通常写的赋值就是强引用;

value的泄露:
ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含了一个对value的强引用;
正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了;

但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,
因为有以下的调用链:Thread -> ThreadLocalMap -> Entry(key 为null) -> Value
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。

JDK已经考虑到了这个问题,所以在set、remove、rehash方法中会扫描key为null的Entry,
并把对应的value设置为null,这样value对象就可以被回收。

但是如果一个ThreadLocal不被使用,那么实际上set、remove、rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄露。

2.2 如何避免内存泄露

调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法;

2.3 空指针异常

在进行get之前,必须先set,否则可能会报空指针异常?
并不是,是由于装箱拆箱导致的。

public class ThreadLocalNPE {
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }

    // long————NPE
    //因为包装是Long,如果是定义long会拆箱,在Long -> long出错的
    public Long get() {
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println(threadLocalNPE.get());
            }
        });
        thread1.start();
    }
}

2.4 共享对象

如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

如果可以不使用ThreadLocal就解决问题,那么就不要强行使用;比如说,我们在任务数很少的时候,在局部变量中新建对象就可以解决问题,那么就不需要使用到ThreadLocal;优先使用框架的支持,而不是自己创造;例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄露。

下一章:第十七章 不可不说的“锁”事

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值