Java并发编程学习八:ThreadLocal

一、ThreadLocal使用场景

Java为保证并发情况下的线程安全,提供了多种可用的方案,典型的如:各种锁(包括synchronize关键字)、原子类、ThreadLocal类等。前面对锁以及原子类已经进行了较为详细的介绍,这里继续介绍ThreadLocal。

ThreadLocal 有两种典型的使用场景:

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

以这样一个场景为例:多个线程,要求每个线程打印的时间不重复,这里需要借助SimpleDateFormat类,该类是一个线程不安全的工具类。

如果线程数为1000,这些线程共享一个SimpleDateFormat对象,由于SimpleDateFormat类是线程不安全的,为了保证线程安全,必然要加上锁。

public class ThreadLocalDemo01 {

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

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;
            threadPool.submit(new Runnable() {

                @Override
                public void run() {

                    String date = new ThreadLocalDemo01().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (ThreadLocalDemo01.class) {

            s = dateFormat.format(date);
        }
        return s;
    }
}

执行结果如下:

00:00
00:01
00:06
...
15:56
16:37
16:36

打印结果满足要求,但是这样的效率会非常低。首先,1000个线程的创建和销毁需要耗费大量资源;其次,由于SimpleDateFormat线程不安全而加上synchronized 关键字,还会带来同步的开销。

那么继续简化一下,对于创建过多线程带来的开销,采用之前提到过的线程池可以很好地降低,其内部的线程复用,最终创建的线程数会远远低于1000个;对于同步的开销,是由于多个线程共享一个SimpleDateFormat对象造成的,如果让每个线程都拥有一个自己的 simpleDateFormat 对象,就不会存在线程之间的竞争,也就不需要同步。

public class ThreadLocalDemo02 {

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

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;
            threadPool.submit(new Runnable() {

                @Override
                public void run() {

                    String date = new ThreadLocalDemo02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
        return dateFormat.format(date);
    }
}

打印结果如下:

00:00
00:07
00:04
00:02
...
16:29
16:28
16:27
16:26
16:39

打印的结果同样满足要求。

上面的案例里,通过让每个线程都拥有一个自己的 simpleDateFormat 对象来保证线程安全,这其实就是ThreadLocal的解决方案。同样采用上面的优化方案,这次用ThreadLocal来进行解决

public class ThreadLocalDemo03 {

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

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;
            threadPool.submit(new Runnable() {

                @Override
                public void run() {

                    String date = new ThreadLocalDemo03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);
        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("mm:ss");
        }
    };
}

这里ThreadLocal 会去帮每个线程去生成它自己的 simpleDateFormat 对象。

2. 用作线程内独立保存的信息

在实际的业务场景中,有一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。这些业务内容在不同的方法中都有使用到,这种情况下,一般的做法是将这些业务内容层层传递给需要使用的方法。

例如,service-1()、service-2()、service-3()、service-4()代表不同的业务处理逻辑,这些方法都需要用户信息User,就需要把这个 user 对象层层传递下去,从 service-1() 传到 service-2(),再从 service-2() 传到 service-3()。

file

这样的做法会造成代码冗余,如果使用一个静态的Map,在执行 service-1() 的时候,把用户信息给 put 进去,后面需要拿用户信息的时候,直接从静态的 User map 里面 get 就可以了。同时考虑到HashMap不够安全,可以使用 synchronized,或者直接把 HashMap 替换成 ConcurrentHashMap来保证线程安全,如下所示:

file

ThreadLocal的作用(实际上应该说是ThreadLocalMap)就类似于上面提到的静态的Map,但是与上面采用加锁或者ConcurrentHashMap的cas操作来保证线程安全不同,ThreadLocal为每一个线程单独生成User对象,从而避免了线程之间的竞争,保证了线程安全。

file

具体的使用方法如下:

public class ThreadLocalDemo07 {

    public static void main(String[] args) {

        new Service1().service1();

    }
}

class Service1 {

    public void service1() {

        User user = new User("test");
        UserContextHolder.holder.set(user);
        new Service2().service2();
    }
}

class Service2 {

    public void service2() {

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

class Service3 {

    public void service3() {

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

class UserContextHolder {

    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {

    String name;

    public User(String name) {

        this.name = n
    }
}

运行结果如下:

Service2拿到用户名:test
Service3拿到用户名:test

二、ThreadLocal vs Thread vs ThreadLocalMap

ThreadLocal、Thread、ThreadLocalMap这三者之间的关系密不可分,具体的关系可以参考以下示意图:

file

总结一下,一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。

具体分析源码如下,先看ThreadLocal的getMap 方法

ThreadLocalMap getMap(Thread t) {

    return t.threadLocals;
}

// Thread的成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;

Thread 中有一个名为threadLocals 的成员变量,初始值为null,而它就是ThreadLocalMap 类型,ThreadLocal的getMap方法就是返回给定Thread的成员变量threadLocals 。

再看ThreadLocal的set方法

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 ;如果 map == null 则去创建这个 map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。

在最后再看ThreadLocalMap ,以下是自定义在 ThreadLocal 类中的 ThreadLocalMap 类:

static class ThreadLocalMap {

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

ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量,在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。我们可以把 Entry 理解为一个 map,其键值对为:

  • 键,当前的 ThreadLocal;
  • 值,实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。

ThreadLocalMap 类似于HashMap ,但是与HashMap 不同的是,HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链;ThreadLocalMap 解决 hash 冲突采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。

三、ThreadLocal的相关问题

1. ThreadLocal能否解决共享资源的多线程访问

先说结论,不能!

ThreadLocal确实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。

ThreadLocal 解决线程安全问题的时候,相比于使用“锁”而言,换了一个思路,把资源变成了各线程独享的资源,非常巧妙地避免了同步操作。具体而言,它可以在 initialValue 中 new 出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。这是 ThreadLocal 解决并发问题的最主要思路。

如果把放到 ThreadLocal 中的资源用 static 修饰,让它变成一个共享资源的话,那么即便使用了 ThreadLocal,同样也会有线程安全问题。

public class ThreadLocalStatic {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i  < 1000; i++) {

            int finalI = i;
            threadPool.submit(new Runnable() {

                @Override
                public void run() {

                    String date = new ThreadLocalStatic().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

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

class ThreadSafeFormatter {

    public static ThreadLocal <SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal <SimpleDateFormat>() {

        @Override
        protected SimpleDateFormat initialValue() {

            return ThreadLocalStatic.dateFormat;
        }
    }
}

执行结果如下:

00:15
00:15
00:05
00:16
...

打印结果中00:15 被多次打印了,发生了线程安全问题。如果我们需要放到 ThreadLocal 中的这个对象是共享的,是被 static 修饰的,那么此时其实根本就不需要用到 ThreadLocal,即使用了 ThreadLocal 并不能解决线程安全问题。如果想要保证它的线程安全,应该用其他的方法,比如说可以使用 synchronized 或者是加锁等其他的方法来解决线程安全问题。

2. ThreadLocal vs synchronized

前面提到过,ThreadLocal和synchronized都能保证线程安全,两者之间的区别是啥呢?

这要从ThreadLocal的两个应用场景出发

首先是ThreadLocal用作线程独享的对象,通过让每个线程独享自己的副本,避免了资源的竞争从而保证了线程安全。而synchronized 内部基于monitor锁,限制在同一时刻最多只有一个线程能访问资源,从而保证了线程安全。相比于 ThreadLocal 而言,synchronized 的效率会更低一些,但是花费的内存也更少。

其次是ThreadLocal用作线程内独立保存的信息,这种场景更注重的效果是避免传参,此时 ThreadLocal 和 synchronized 是两个不同维度的工具。

3. 内存泄露

内存泄露,是指当某一个对象不再有用的时候,占用的内存却不能被回收。

在使用ThreadLocal时就会面临内存泄露的风险

a. Key的泄露

之前提到过,每个Thread内部都有一个ThreadLocalMap。线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。

当业务代码不再使用某个ThreadLocal时,执行ThreadLocal instance = null 操作,但是ThreadLocalMap 的 Entry 中还引用着这个ThreadLocal示例。也就是说,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。这就会存在内存泄露的风险。

ThreadLocalMap为了避免这种内存泄露风险,其内部的Entry 继承了 WeakReference 弱引用,对Key是一个弱引用。

static class Entry extends WeakReference<ThreadLocal<?>> {

    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {

        super(k);
        value = v;
    }
}

弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,这样就不会因为Entry 中还引用着ThreadLocal示例,而导致无法GC回收无用的ThreadLocal示例。

b. Value的泄露

上面提到ThreadLocalMap内部的Entry 继承了 WeakReference 弱引用,它对Key是弱引用,但是它对Value却是强引用。上面的代码中,value = v就是强引用的体现。

正常情况下,当线程结束,Value就没有了任何强引用,此时可以正常垃圾回收掉。但是如果线程迟迟不结束,而Value早就不再使用,由于Entry的强引用,Value不会被垃圾回收掉。

如图所示,显示的是线程内的引用关系,对于Value,引用链路为:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例。

file

为了解决这个问题,ThreadLocal 设计在执行set、remove、rehash 等方法时,会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。

c. 内存泄露如何避免

上面提到了Key和Value两种情况的内存泄露,而Key引起的内存泄露由于引入了弱引用,已经得到了解决。但是对于Value的泄露,需要及时的调用set、remove、rehash 等方法。

也就是说,为了避免Value引起的内存泄露,在使用完了 ThreadLocal 之后,应该手动去调用它的 remove 方法。

查看一下remove的源码

public void remove() {

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

首先获取线程的ThreadLocalMap,再调用ThreadLocalMap的remove方法,将key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了。