ThreadLocal使用场景与原理

目录

ThreadLocal的使用场景

ThreadLocal与synchronized的区别

Thread、ThreadLocal及ThreadLocalMap的关系

调用remove()方法避免内存泄漏


ThreadLocal的使用场景

  • ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
  • ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

通过例子验证一下:

我们知道SimpleDateFormat在多线程并发访问下会出现线程安全问题。

/**
 * 线程不安全demo
 *
 * @author hujy
 * @version 1.0
 * @date 2020-06-29 20:57
 */
public class ThreadNotSafeDemo {

    private static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

    public String date(int seconds) {
        // 创建不同的date
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String date = new ThreadNotSafeDemo().date(finalI);
                System.out.println(date);
            });
        }
        threadPool.shutdown();
    }
}

打印运行结果: 

上面代码中每次循环都会创建不同的date对象,但是在多线程并发创建的场景下,打印的结果中出现了大量重复值,说明产生了线程安全问题。

通过ThreadLocal保证线程安全:

/**
 * ThreadLocal线程安全demo
 *
 * @author hujy
 * @version 1.0
 * @date 2020-06-29 21:19
 */
public class ThreadSafeDemo {

    private static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                try {
                    String date = new ThreadSafeDemo().date(finalI);
                    System.out.println(date);
                } finally {
                    ThreadSafeFormatter.dateFormatThreadLocal.remove();
                }
            });
        }
        threadPool.shutdown();
    }
}

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = 
            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}

打印运行结果:

运行结果都是唯一的值,说明通过ThreadLocal可以实现共享变量的线程安全。


ThreadLocal与synchronized的区别

  • ThreadLocal 是通过让每个线程独享自己的副本,避免了资源的竞争。
  • synchronized 主要用于临界资源的分配,在同一时刻限制最多只有一个线程能访问该资源。
  • ThreadLocal 并不是用来解决共享资源的多线程访问的问题,因为每个线程中的资源只是副本,并不共享。因此ThreadLocal适合作为线程上下文变量,简化线程内传参。

Thread、ThreadLocal及ThreadLocalMap的关系

想要了解Threadlocal的工作原理,就必须了解Thread、ThreadLocal以及ThreadLocalMap这三个类之间的关系。

ThreadLocalMap是ThreadLocal类的静态内部类,本质是一个Map,key的类型就是我们定义的ThreadLocal对象,value则是我们具体要保存的变量参数。

 public class ThreadLocal<T> {
    ...
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
            ...
            private Entry[] table;
        }
    }

而Thread中含有ThreadLocal.ThreadLocalMap类型的成员变量threadLocals。

public class Thread implements Runnable {    
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ...
}

因此这三个类的关系可以总结为:

一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。

另外ThreadLocalMap在解决hash冲突的方式与HashMap不同,HashMap采用的是拉链发,而ThreadLocalMap采用的是线性探索法,即发生冲突时,向下继续寻找空的位置。


调用remove()方法避免内存泄漏

通过ThreadLocalMap的源码可以看到,Entry中的key被定义为弱引用类型,当发生GC时,key会被直接回收,无需手动清理。

而value属于强引用类型,被当前的Thread对象关联,所以说value的回收取决于Thread对象的生命周期。如果说一个线程执行完毕,线程Thread随之被释放,那么value便不存在内存泄漏的问题。然而,我们一般会通过线程池的方式来复用Thread对象来节省资源,这就会导致一个Thread对象的生命周期会非常长,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。

因此,我们在使用完ThreadLocal变量后,要手动调用remove()方法来清理ThreadLocalMap(一般在finally代码块中)。

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

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章