小言_互联网的博客

你了解ThreadLocal吗?

423人阅读  评论(0)

她不清楚自己孤独的原因
唯一能够表达出来的就是
这不是我所期望的世界。

介绍

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
 
threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。

解决问题

问题1

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

你想象的代码

public class TestThreadLocal {

    public static void main(String[] args) {
        //多个线程分别调用date方法
        ......
    }

    public static String date(int second) {
        Date date = new Date(second);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return simpleDateFormat.format(date);
    }
}

这个代码最大的问题是什么?
每个线程分别调用date方法,方法内部需要创建一个SimpleDateFormat对象,那线程如果很多的话,就会创建很多个SimpleDateFormat对象,可能会造成OOM。

为什么不用一个共享变量SimpleDateFormat达到复用的目的,因为SimpleDateFormat线程不安全。

为什么不加锁?效率太低。

使用ThreadLocal

public class TestThreadLocal {

    public static void main(String[] args) {
        //多个线程分别调用date方法
        ......

    }

    public static String date(int second) {
        Date date = new Date(second);
        //get方法获取ThreadLocal设置的初始值。
        SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.threadLocal.get();
        return simpleDateFormat.format(date);
    }
}

class ThreadSafeFormatter {
	//使用ThreadLocal
    static ThreadLocal<SimpleDateFormat> threadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

问题2

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

你想象的代码

public class TestThreadLocal {
    public static void main(String[] args) {

        //每个线程有自己的用户信息
        User user=new User("Jeck");
        //执行操作
        new Service1().process(user);

    }
}
//模块1
class Service1{
    public void process(User user){
        //执行完操作,调用下一个模块进行操作
        new Service2().process(user);
    }
}
//模块2
class Service2{
    public void process(User user){
        //执行完操作,调用下一个模块进行操作
        new Service3().process(user);
    }
}
//模块3
class Service3{
    public void process(User user){
        //流程结束,删除user信息
    }
}

这段代码最大的弊端是什么?
参数传递的麻烦。

使用ThreadLocal

public class TestThreadLocal {
    public static void main(String[] args) {

        //每个线程有自己的用户信息
        User user=new User("Jeck");
        //保存到ThreadLocal中
        ThreadSafeUser.threadLocal.set(user);
        //执行操作
        new Service1().process();
    }
}
//模块1
class Service1{
    public void process(){
        //从ThreadLocal中获取User用户信息
        User user=ThreadSafeUser.threadLocal.get();
        //执行完操作,调用下一个模块进行操作
        new Service2().process();
    }
}
//模块2
class Service2{
    public void process(){
        //从ThreadLocal中获取User用户信息
        User user=ThreadSafeUser.threadLocal.get();
        //执行完操作,调用下一个模块进行操作
        new Service3().process();
    }
}
//模块3
class Service3{
    public void process(){
        //流程结束,删除user信息
        ThreadSafeUser.threadLocal.remove();
    }
}
class ThreadSafeUser {
    static ThreadLocal<User> threadLocal =new ThreadLocal<User>();
}

以上两种场景,生成对象时机不同。

根据共享对象的生成时机不同,选择initialValue或set来保存对象,后面会介绍几种方法。

好处

  1. 线程安全
  2. 不需要加锁,提高执行效率
  3. 更高效的利用内存,节省开销
  4. 避免传参麻烦

Thread?ThreadLocal?ThreadLocalMap?


每个Thread对象都持有一个ThreadLocalMap成员变量。
每个ThreadLocalMap中存有多个ThreadLocal对象,ThreadLocal作为key,value是泛型。

主要方法

initialValue:

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

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

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

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

set:

为这个线程设置新值。

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

get:

得到这个线程对应的value。如果是首次调用get(),则会调用initialValue来得到这个值。

先取出当前线程的ThreadLocalMap,然后调用map.getEntry()方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的Value。

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

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

remove:

删除对应这个线程的值。

ThreadLocalMap

ThreadLocalMap这个类,也就是Thread.threadLocals

ThreadLocalMap类是每个线程ThreadL类里面的变量,里面最重要的一个键值对数组Entry[] table,可以认定是一个Map,键值对:
键:这个ThreadLocal
值:实际需要的成员变量。

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

注意点

内存泄漏

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

通过看源码

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

    Entry(ThreadLocal<?> k, Object v) {
    	//key使用父类的弱引用
        super(k);
        //value是强引用
        value = v;
    }
}

弱引用的特点是:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收。

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的内存泄漏。

如何避免?

使用完ThreadLocal之后调用remove()方法进行删除。


转载:https://blog.csdn.net/qq_15038565/article/details/106002835
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场