ThreadLocal原理、作用、内存泄漏及使用场景

ThreadLocal的用途

两大使用场景:

1.典型场景一:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

2.典型场景二:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。

每个线程需要一个独享的对象

每个Thread内有自己的实例副本,不共享

比喻:一个班教材只有一本,一起做笔记有线程安全问题(并发读写),每个人复印一本后没问题。

1.SimpleDateFormat的进化之路

2个线程打印时间

/**
 * 2个线程打印日期
 */
public class ThreadLocalNormalUsage00 {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(10);
                System.out.println(date);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(1007);
                System.out.println(date);
            }
        }).start();
    }

    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");
        return dateFormat.format(date);
    }
}

30个线程打印时间

/**
 * 、10个线程打印日期
 */
public class ThreadLocalNormalUsage01 {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 30; i++) {
            int finalI =i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage01().date(finalI);
                    System.out.println(date);
                }
            }).start();
            Thread.sleep(100);
        }
    }
    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");
        return dateFormat.format(date);
    }
}

当需求变成了1000个,那么必然要用线程池,否则消耗内存太多

当所有线程都共同用一个SimpleDateFormat对象时发生线程安全问题

**
 *1000个线程打印日期的任务,用线程池来执行
 */
public class ThreadLocalNormalUsage03 {

    private static ExecutorService threadLocal = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadLocal.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadLocal.shutdown();
    }
    public String date(int seconds) {
        //参数的单位是1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}

打印结果出现相同时间

使用加锁解决线程安全问题,直接给format方法加锁

public String date(int seconds) {
        //参数的单位是1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (ThreadLocalNormalUsage04.class) {
            s = dateFormat.format(date);
        }
        return s;
    }

虽然加锁能够解决问题,但在高并发情况下效率低下,这时用ThreadLocal可以解决性能问题

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

    private static ExecutorService threadLocal = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadLocal.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadLocal.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.dateFormatThreadLocal.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");
        }
    };
    //lambda表达式写法
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(
            () -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
    );
}

每个线程内需要保存全局变量

一个比较繁琐的2解决方案是把user作为参数层层传递,从service-1(),传到service-2(),再从service-2()传到service-3(),一次类推,但是这样做会导致代码冗余且不易维护

1.用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、userID等)

2.这些信息在同一个线程内相同,但是不同的线程使用的业务内容不相同的

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

4.强调的是同一个请求内(同一个线程内)不同方法间的共享

5.不需要重写initialValue()方法,但是必须手动调用set()方法

实例:当前信息需要被线程内所有方法共享
在之前基础上演进,使用UserMap

当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但无论用什么,都会对性能有所影响

更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可达到保存当前线程对应的用户信息目的。

/**
 * 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("超哥");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {

        User user = UserContextHolder.holder.get();
        System.out.println("service2拿到用户名: "+user.name);
        new Service3().process();
    }
}

class Service3 {
    public void process() {

        User user = UserContextHolder.holder.get();
        System.out.println("service3拿到用户名 "+user.name);
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
    String name;

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

ThreadLocal两个作用

1.让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)

2,在任何方法中都可以轻松获取到该对象

两种不同的实现:

根据共享对象的生成时机不同,选择initialValue或set来保存对象

1.场景一(initialValue):在ThreadLocal第一次get的时候把对象初始化出来,对象的初始化时机可以由我们来控制

2场景二(set):如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set()直接放到我们的ThreadLocal中去,以便后续使用。

使用ThreadLocal带来的好处

1.达到线程安全

2.不需要加锁,提高执行效率

3.更高效地利用内存、节省开销:相比于每个任务都新建立一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销

4.免去传参的繁琐:无论是场景一的工具类还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传递同样的参数。ThreadLocal使得代码耦合度更低,更优雅。

ThreadLocal原理、源码分析

搞清楚Thread、ThreadLocal以及ThreadLocalMap三者之间的关系

1.每个Thread对象中都持有一个ThreadLocalMap成员变量

Thread中成员变量ThreadLocalMap

 ThreadLocal.ThreadLocalMap threadLocals = null;

2.主要方法介绍

initialValue():

① 该方法会返回当前线程对应的"初始值",这是一个延迟加载的方法,只有在调用get()的时候,才会触发。

② 当线程第一次使用get()方法变量时,将调用initialValue()方法,除非线程先前调用了set()方法,在这种情况下,不会为线程调用initialValue()方法

③ 通常,每个线程最多调用一次initialValue()方法,但如果已经调用remove后,再调用get(),则可以再次调用此方法

④ 如果不重写initialValue()方法,这个方法会返回为null,一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象

initialValue()方法默认返回null,所以我们重写initialValue()方法

protected T initialValue() {
        return null;
    }

void set(T t):为这个线程设置一个新值

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

T get():得到这个线程对应的value,如果是首次调用get(),则会调用initialize来得到这个值,get()方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry()方法,把ThreadLocal的引用作为参数传入,取出Map中属于本ThreadLocal的Value

注意:这个map以及map中key和value中都是保存在线程中的,而不是保存在ThreadLocal中的

 public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//获取Thread中成员变量ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

如果map等于null,调用setInitialValue(),setInitialValue()调用initialValue()

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;
    }

void remove():删除对应这个线程的值

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();
    }

remove()源码

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap类

1.ThreadLocalMap类,也就是Thread.ThreadLocals

2.ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,
键值对:
键:这个ThreadLocal
值:实际需要的成员变量,比如user或者simpleDateFormat对象

类似HashMap,但也有不同之处,解决冲突方式:

HashMap拉链法:

ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链

两种使用场景的相同之处

1.通过源码分析可看出,setInitialValue和直接set最后都是利用map.set()方法来设置值

2.也就是说,最后都会对应到ThreadLocalMap的一个Entry,只不过起点和入口不一样

ThreadLocal导致内存泄漏

内存泄漏:某个对象不再有用,但是占用的内存却不能被回收

1.Key的泄漏:ThreadLocalMap中的Entry继承自WeakReference,是弱引用

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

2.弱引用的特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收,弱引用不会阻止GC

Value的泄漏:ThreadLocalMap的每个Entry都是对key的弱引用,同时,每个value都包含了一个对value的强引用

正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了

但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,例如用线程池时同一个线程反复使用,因为有以下调用链:

因为value和Thread之间还存在这个强引用链路,所以导致vaLue无法回收,就可能出现OOM

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

 private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

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

如何避免内存泄漏

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

ThreadLocal的空指针问题

设置好的包装类的对象

ThreadLocal<Long> longThreadLocal = new ThreadLocal<>(); 

如果get返回为基本类型则报错空指针异常

本来get没有赋值前为null,当把对象类型转为基本类型转换不到,导致空指针异常

ThreadLocal注意点

1.在进行get之前,必须先set,否则会报空指针异常?所以并不是,只是装箱、拆箱导致的而不是ThreadLocal的问题

2.共享对象:如果在每个线程中ThreadLocal.set()进去的东西本身就是多线程共享的同一个对象,比如sttic对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题,所以不应该在ThreadLocal中放置静态的对象

3.如果可以不使用ThreadLocal就解决问题,那么不要强行使用,例如在任务数很少的时候,在局部变量中就可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal

4.优先使用框架的支持,而不是自己创造,例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove方法等,造成内存泄漏

ThreadLocal实际应用场景

在Spring中的实例分析

1.DateTimeContextHolder类,里面用了ThreadLocal存储时间的上下文

2.每次Http请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值