Java并发 - ThreadLocal

219 阅读6分钟

ThreadLocal的作用

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

ThreadLocal的两大使用场景

  • 典型场景1 :每个线程需要一个独享的对象 (通常是工具类,典 型需要使用的类有SimpleDateFormat和Random -- 这些工具类都是线程不安全的)
  • 典型场景2 :每个线程内需要保存全局变量(例如在拦截器中获 取用户信息) , 可以让不同方法直接使用,避免参数传递的麻烦

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

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

比喻:教材只有一-本,一起做笔记有线程安全问题。复印后没问题

  • 每次调用创建一个新对象(SimpleDateFormat为线程独占独享) -- 线程安全
public class ThreadLocalNormalUsage00 {

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

    public static void main(String[] args) {
        ThreadLocalNormalUsage00 threadLocalNormalUsage00 = new ThreadLocalNormalUsage00();

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(threadLocalNormalUsage00.date(finalI));
                }
            });
        }
        executorService.shutdown();

    }


    public String date(int seconds) {
        // 参数是从1970.1.1 00:00开始
        Date date = new Date(1000*seconds);
        SimpleDateFormat format = new SimpleDateFormat(
                "yyyy-MM-dd hh:mm:ss");
        return format.format(date);
    }

}

  • 使用类变量/类实例变量SimpleDateFormat -- 线程不安全

这两种变量都存在于Java运行时数据区域的线程共享区

public class ThreadLocalNormalUsage01 {

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

    public static void main(String[] args) {
        ThreadLocalNormalUsage00 threadLocalNormalUsage00 = new ThreadLocalNormalUsage00();

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(threadLocalNormalUsage00.date(finalI));
                }
            });
        }
        executorService.shutdown();

    }

//    static SimpleDateFormat format = new SimpleDateFormat(
//            "yyyy-MM-dd hh:mm:ss");
    
    SimpleDateFormat format = new SimpleDateFormat(
            "yyyy-MM-dd hh:mm:ss");

    public String date(int seconds) {
        // 参数是从1970.1.1 00:00开始
        Date date = new Date(1000*seconds);
        return format.format(date);
    }

}

  • 使用ThreadLocal(为每个线程创建SimpleDateFormat副本) -- 线程安全
public class ThreadLocalNormalUsage02 {

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

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalNormalUsage01 threadLocalNormalUsage00 = new ThreadLocalNormalUsage01();

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(threadLocalNormalUsage00.date(finalI));
                }
            });
        }
        executorService.shutdown();

    }


    public String date(int seconds) {
        // 参数是从1970.1.1 00:00开始
        Date date = new Date(1000*seconds);
        SimpleDateFormat simpleDateFormat = threadLocal.get();
        // 输出的类实例是一样的
        System.err.println(simpleDateFormat);
        return simpleDateFormat.format(date);
    }

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

}

场景二:每个线程内需要保存全局变量

  • 强调的是同一个请求内(同一个线程内)不同方法间的共享
  • 不需重写initialValue()方法,但是必须手动调用set()方法
@Controller
@RequestMapping("/threadLocal")
public class ThreadLocalController {

    @RequestMapping("/test")
    @ResponseBody
    public Long test() {
        return RequestHolder.getId();
    }

}
public class RequestHolder {

    private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>();

    /**
     * 在请求即将进入后端服务器之前进行调用
     *
     * @param id
     */
    public static void add(Long id) {
        requestHolder.set(id);
    }

    public static Long getId() {
        return requestHolder.get();
    }

    /**
     * 接口处理完之后进行
     * 如果不进行释放,将会造成内存泄露
     */
    public static void remove() {
        requestHolder.remove();
    }

}

场景使用选择

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

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

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


使用ThreadLocal带来的好处

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

ThreadLocal原理

Thread,ThreadLocal,ThreadLocalMap之间的关系

QNtS8x.png
图片来自 www.cnblogs.com/aspirant/p/…

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

ThreadLocal中的方法

  • get():返回此线程局部变量的当前线程副本中的值。
  • initialValue():返回此线程局部变量的当前线程的“初始值”。这是一个延迟加载的方法,只有调用 get()的时候,才会触发
  • remove():移除此线程局部变量当前线程的值。
  • set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。

ThreadLocal存在的问题

  • 空指针异常
ThreadLocal<Long> LongThreadLocal = new ThreadLocal<Long>();
public void set() {
    longThreadLocal.set(Thread. currentThread().getId() ) ;
}
// 注意,要将long改为Long
public long get() {
    return LongThreadLocal.get() ;
}

如果没有set值的话,上面的get()方法会出现空指针异常。因为ThreadLocal指定了泛型,如果是get的返回值为基本类型的话,会进行一次拆箱操作,如果get结果为null,则会抛出异常。

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

  • 内存泄漏问题

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。


  • 参考课程:慕课网 - 玩转Java并发工具,精通JUC,成为并发多面手