当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭

一、使用场景

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

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

    以SimpleDateFormat为例,该类是一个线程不安全的类,看下面的例子: ```java public class ThreadLocalNormalUsage { static SimpleDateFormat dateFormat = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); public static void main(String[] args) {
    1. ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
    2. for (int i = 0; i < 1000; i++) {
    3. int finalI = i;
    4. threadPoolExecutor.submit(new Runnable() {
    5. @Override
    6. public void run() {
    7. String date = new ThreadLocalNormalUsage().date(finalI);
    8. System.out.println(date);
    9. }
    10. });
    11. }
    } public String date(int seconds) {
      Date date = new Date(1000 * seconds);
      return dateFormat.format(date);
    
    } }

执行结果如下: 1970-01-01 08:00:08 1970-01-01 08:00:09 1970-01-01 08:00:08 1970-01-01 08:00:08 1970-01-01 08:00:12 …..

可以看出来,为了避免重复创建SimpleDateFormat对象,使用static的SimpleDateFormat,却发现由于SimpleDateFormat是线程不安全的,导致执行结果不符合实际结果。<br />**改进以后:**
```java
public class ThreadLocalNormalUsage02 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    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 ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal=new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        return simpleDateFormatThreadLocal.get().format(date);
    }
}

---------------
执行结果正常。

可以看到,使用ThreadLocal每个线程创建一个SimpleDateFormat副本,相比于每个任务创建一个SimpleDateFormat对象来讲节省了内存,同时相比使用synchronized加锁的方式提高了执行效率。

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

当前用户信息需要被线程内所有方法共享:每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。

image.png

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

image.png
使用threadLocal以后:
image.png
使用:

public class ThreadLocalNormalUsage06 {

    public static void main(String[] args) {
        new Service1().process("");

    }
}

class Service1 {

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

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeFormatter.dateFormatThreadLocal.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);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {

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


}

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

二、使用场景总结

根据共享对象的生成时机不同,选择initialValue或set来保存对象:
    1.在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制。
    2.如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用
    ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用。

三、源码分析

image.png

# Thread类的成员变量,map的原因是一个thread可以存储很多个threadLocal对象。
ThreadLocal.ThreadLocalMap threadLocals = null;

重要的事说一遍:ThreadLocalMap事Thread类的成员变量。

initialValue()和get()

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

protected T initialValue() {
        return null;
    }

可以看到,该方法返回了一个null,所以要求我们在使用的时候重写该方法。在使用get()方法时调用。

//调用方式。
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
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();
    }

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 createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  1. 可以看到,首次调用get方法时,ThreadLocalMap的map为null,所以调用的时setInitialValue()方法,再该方法中的initialValue()方法,即我们重写的该方法。
  2. 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法.
  3. 通常,每个线程最多调用一次此方法,但如果已经调用了remove(后,再调用get(),则可以再次调用此方法,因为remove过后,ThreadLocalMap又变成了null,所以就会再次调用setInitialValue()方法。
  4. 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
  5. get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
  6. 注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中

    void set(T value)

    想清楚ThreadLocalMap结构以后再看这两个方法就很简单了。

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

    void remove()

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

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

补充:

内存泄漏问题:

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

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

可以看到Entry类继承了WeakReference弱引用,super(k)说明可以k被gc垃圾回收, 但是 value = v又是一个强引用.

  • ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用
  • 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了
  • 但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:

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

JDK已经考虑到了这个问题,所以在set,remove, rehash方法中会扫描key为null,并把对应的value设置为null。

解决方法:
阿里规约规定:调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法。