JUC(7)ThreadLocal

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候都有自己的、独立初始化的变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份)。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全的问题)

生活类比

  • synchronized或者lock就像只有一份签到表,多个同学只能按序,同一时间只能一个同学签字。
  • ThreadLocal则是人人都有签到表,每个同学自己用自己的,不用再加锁。(以空间换时间,为每一个线程都提供了一份变量的副本,从而实现同时访问,互不干扰同时访问)

API介绍

  • protected T initialValue():initialValue():返回此线程局部变量的当前线程的"初始值"
    (等价于JDK 1.8加入的withInitial()方法)
  • static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier):创建线程局部变量
  • T get():返回当前线程的此线程局部变量的副本中的值
  • void set(T value):将当前线程的此线程局部变量的副本设置为指定的值
  • void remove():删除此线程局部变量的当前线程的值
Modifier and Type Method and Description
T get()Returns the value in the current thread's copy of this thread-local variable.
protected T initialValue()Returns the current thread's "initial value" for this thread-local variable.
void remove()Removes the current thread's value for this thread-local variable.
void set(T value)Sets the current thread's copy of this thread-local variable to the specified value.
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)Creates a thread local variable.

HelloWorld

public class ThreadLocalDemo {
    public static void main(String[] args) {
        House house = new House();
        new Thread(() -> {
            try {
                for (int i = 0; i < 3; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName() + " sales " + house.threadLocal.get());
            } finally {
                house.threadLocal.remove();
            }
        }, "t1").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 8; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName() + " sales " + house.threadLocal.get());
            } finally {
                house.threadLocal.remove();
            }
        }, "t2").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName() + " sales " + house.threadLocal.get());
            } finally {
                house.threadLocal.remove();
            }
        }, "t3").start();

        System.out.println(Thread.currentThread().getName() + " sales " + house.threadLocal.get());
    }
}

@Data
class House {
    public static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public void saleHouse() {
        Integer integer = threadLocal.get();
        integer++;
        threadLocal.set(integer);
    }
}
  1. 【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用, 如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用 try-finally 块进行回收。

    正例:

    objectThreadLocal.set(userInfo);

    try {

    ​ // ...

    } finally {

    ​ objectThreadLocal.remove();

    }

    摘自阿里巴巴《Java开发手册》

通过上面的Demo代码我们可以发现:

  • 每个Thread内有自己的实例副本且该副本只由当前线程自己使用
  • 既然其他Thread不可访问,那就不存在多线程共享的问题
  • 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
  • 加入synchronized或者lock控制线程的访问顺序,而ThreadLocal人手一份,大家各自安好,没必要抢夺

SimpleDateFormat与ThreadLoacl

再翻阿里的《Java开发手册》,我们注意到了关于ThreadLocal,在并发处理第5条,有一条很有意思的描述

  1. 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。

    正例:注意线程安全,使用 DateUtils。亦推荐如下处理:

    private static final ThreadLocal df = new ThreadLocal() {

    ​ @Override

    ​ protected DateFormat initialValue() {

    ​ return new SimpleDateFormat("yyyy-MM-dd");

    ​ }

    };

说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

一般我们写工具类,自然像这种日期转换通用的东西,自然都是static方便调用,但是,在这种前提下使用SimpleDateFormat是存在线程安全的问题的。

public class DateUtils
{
    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parseDate(String stringDate) throws Exception
    {
        return sdf.parse(stringDate);
    }

    public static void main(String[] args) throws Exception
    {
        for (int i = 1; i <=30; i++) {
            new Thread(() -> {
                try {
                    System.out.println(DateUtils.parseDate("2021-8-1 15:30:00"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

当我们运行时,居然出现了各式各样的异常,即使没出异常,结果也是不正确的(过滤了部分正确的结果):

java.lang.NumberFormatException: For input string: "3E02"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Long.parseLong(Long.java:714)
	at java.base/java.lang.Long.parseLong(Long.java:839)
	at java.base/java.text.DigitList.getLong(DigitList.java:195)
	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2197)
	at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2241)
	at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1542)
	at java.base/java.text.DateFormat.parse(DateFormat.java:394)
	at com.zoran.completablefuturetasktest.DateUtils.parseDate(DateUtils.java:18)
	at com.zoran.completablefuturetasktest.DateUtils.lambda$main$0(DateUtils.java:26)
	at java.base/java.lang.Thread.run(Thread.java:831)
Sun Aug 01 15:30:00 CST 2021
Fri Aug 01 15:30:00 CST 4
Tue Aug 01 15:00:00 CST 3020
Sun Oct 03 03:30:00 CST 2021
Wed Aug 01 15:30:00 CST 1515
java.lang.NumberFormatException: For input string: ""
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Long.parseLong(Long.java:724)
	...
java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 19
	at java.base/java.text.DigitList.append(DigitList.java:151)
	at java.base/java.text.DecimalFormat.subparseNumber(DecimalFormat.java:2457)
	at java.base/java.text.DecimalFormat.subparse(DecimalFormat.java:2316)
	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2149)
	at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934)
	at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1542)
	...

我们看看Java官方对SimpleDateFormat的描述:

Synchronization

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

官方描述了SimpleDateFormat的日期格式是不同步的,建议我们为每个线程创建独立的格式实例,如果多个线程同时访问一个格式,则它必须保持外部同步。

原因

SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的.这样就会导致一个问题:如果你的SimpleDateFormat是个static的, 那么多个thread 之间就会共享这个SimpleDateFormat, 同时也是共享这个Calendar引用。

解决1:加锁,但在static方法里加锁不太合适,

解决2:将SimpleDateFormat定义成局部变量,但是每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收

解决3:使用ThreadLocal

解决4:使用DatetimeFormatter

底层源码分析

Thread.java中定义了一个ThreadLocalMap,而ThreadLocal类中定义了一个内部类ThreadLocalMap,再次体会ThreadLocal—各自线程,人手一份

  • Thread类中有一个ThreadLocal,ThreadLocalMap threadLocals = null的变量,这个ThreadLocal相当于是Thread类和ThreadLocalMap的桥梁,在ThreadLocal中有静态内部类ThreadLocalMap,ThreadLocalMap中有Entry对象

  • 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放

  • t.threadLocals = new ThreadLocalMap(this, firstValue),如下源码我们可以知道每个线程都会创建一个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);
    }
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    

    对应的ThreadLocalMap.Entry:

    static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;
    
      Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
      }
    }
    

    方法详解

        public void set(T value) {
            Thread t = Thread.currentThread();
            // return t.threadLocals; 获取当前线程中存取的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // 如果存在 存取Entry到Entry数组中
                map.set(this, value);
            } else {
                // t.threadLocals = new ThreadLocalMap(this, firstValue);
                // 新建一个ThreadLocalMap并存取以当前ThreadLocal对象为key,值为value
                createMap(t, value);
            }
        }
    
            ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                // 计算当前ThreadLocal存储在数组的索引
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
    
        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
              // 从ThreadLocalMap对象中获取当前ThreadLocal对应的value
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
          // 初始化 : 有两种情况有执行当前代码
          // 第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
          // 第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
            return setInitialValue();
        }
    
        private T setInitialValue() {
          // return null;
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                map.set(this, value);
            } else {
                createMap(t, value);
            }
            if (this instanceof TerminatingThreadLocal) {
                TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
            }
            return value;
        }
    
    		//remove方法,移除当前线程ThreadLocalMap中当前的ThreadLocal对象
         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null) {
                 m.remove(this);
             }
         }
    

ThreadLocal的内存泄漏问题

阿里巴巴开发手册强调threadLocal变量使用后必须进行清理,否则在多线程环境下尤其是线程池环境下会导致内存泄漏(不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露)和业务逻辑问题。

首先Entry官方定义他是继承了弱引用的,为什么使用弱引用呢?

public void function01(){
    ThreadLocal tl = new ThreadLocal<Integer>();    //line1
    tl.set(2021);                                   //line2
    tl.get();                                       //line3
}

line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象。

当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象,如果这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏。

如果这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null

当key为null后,

  • ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(线程池),这些key为null的Entry的value就会一直存在一条强引用链Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
  • 虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value
  • 因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug
  • 如果当前thread运行结束,threadLocal,threadLocalMap, Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收,但在实际使用中我们往往会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心

set、get方法会去检查所有键为null的Entry对象

总结

  • ThreadLocal本地线程变量,以空间换时间,线程自带的变量副本,人手一份,避免了线程安全问题
  • 每个线程持有一个只属于自己的专属Map并维护了Thread Local对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry,cleanSomeSlots, replace StaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法
  • 用完之后一定要remove操作
posted @ 2021-08-05 23:31  Zoran0104  阅读(68)  评论(0编辑  收藏  举报