体系化深入学习并发编程(四)揭开ThreadLocal的面纱

如果多个线程共享一个非线程安全对象,我们通常考虑使用锁来达到线程安全。实际上,也可以选择不共享这个对象,而是每个线程来创建一个该对象的实例,每个线程只能访问自己创建的实例。
这种被一个线程独有的对象就被称为线程特有对象,相应的持有该对象的线程就称为持有线程
ThreadLocal类相当于线程访问其线程特有对象的代理,各个线程通过这个对象可以创建并访问各自的线程特有对象。
ThreadLocal类可以理解为当前线程访问其线程特有对象的代理对象。
ThreadLocal

ThreadLocal的用途

避免锁的开销保证线程安全

每个线程内部都有自己的实例副本,不对外共享。
比如一个小组共用一份学习资料,每个人都能拿来看,但是如果每个人都去上面做笔记,就会乱套。所以,最好就是每个人都复印一份,在自己的那份上面做笔记就行了。
现在用SimpleDateFormat这个工具类举例子
有这么一个方法获取时间

public String getTime(int seconds) {
        Date date = new Date(seconds*1000);
        SimpleDateFormat DateFormat = new SimpleDateFormat("yyyy-MM-DD hh:mm:ss");
        return DateFormat.format(date);
    }

现在的需求是不重复打印1000次数据,我们考虑使用线程池来实现。

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i <1000; i++) {
            int finalI = i;
            executorService.execute(()->{
                System.out.println(new UseThreadLocal1().getTime(finalI));
            });
        }
    }

能够按照预期输出结果,但是这样有个弊端,这1000条任务,我们创建了1000个对象。
所以采取单例模式的思想,用static来修饰,节省创建和销毁对象的开销

static SimpleDateFormat DateFormat = new SimpleDateFormat("yyyy-MM-DD hh:mm:ss");

不过问题也很明显,那就是多线程环境下的线程安全问题。

1970-01-01 08:15:29
1970-01-01 08:15:29

会打印相同的数据,并不符合我们的要求
我们可以考虑加锁来避免线程冲突

String s = null; 
synchronized (UseThreadLocal3.class){
            Date date = new Date(seconds*1000);
            s = DateFormat.format(date);
}
return s;

将getTime方法用synchronized代码块包裹,就能解决并发问题
可以set集合来验证是否存入1000条记录

但是新的问题又来了,synchronized带来了性能的消耗,一个线程在调用方法时,其他9个线程又会被阻塞。

这里就需要用到ThreadLocal了,让每个线程都有自己的SimpleDateFormat实例,每个线程使用自己的工具类实例,避免多创造过多的对象,同时也避免了线程安全问题和同步带来的性能开销。

创建一个泛型为SimpleDateFormat的ThreadLocal对象

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

将获取方式从新建对象改为从ThreadLocal对象那里获取

SimpleDateFormat dateFormat = dateFormatThreadLocal.get();
return dateFormat.format(date);

最终打印结果和之前加synchronized锁效果相同

隐式参数传递

每个线程内需要保存全局变量,可以让各个方法直接使用,避免参数传递。比如一个类调用另一个类的方法,前者向后者传递参数可以通过ThreadLocal传递而不用通过方法参数传递
这样保证了在同一个请求类(同一个线程类)不同方法之间的数据共享

有这么一个Account类

class Account{
    String name;
    Integer money;
	//省去构造方法和set/get
}

有个ThreadLocal类

class ThreadLocalAccount{
    public static ThreadLocal<Account> threadLocal = new ThreadLocal<>();
}

在一个线程内部,拿主线程为例

使用set方法将存入一个线程特有对象。那么之后的操作,不需要传递参数,而是通过ThreadLocal的get方法,从自己的线程特有对象中获取即可,这样省去了参数的传递。

public static void showName(){
        Account account = ThreadLocalAccount.threadLocal.get();
        System.out.println(account.getName());
    }
public static void showMoney(){
        Account account = ThreadLocalAccount.threadLocal.get();
        System.out.println(account.getMoney());
    }

public static void main(String[] args) throws InterruptedException {
        ThreadLocalAccount.threadLocal.set(new Account("张三",100));
        showName();
        showMoney();
    }

而且保证了线程安全的问题。在这个线程中,设置的是张三的账户,而后续方法获得的都是张三这个账户的信息。在其他线程中,并不能获取到张三这个账户。
比如我们再创建一个子线程来获取,打印结果就为null

new Thread(()->{
            System.out.println(ThreadLocalAccount.threadLocal.get());
 }).start();

两种情况的总结

ThreadLocal的特点:

  1. 每个线程有自己独立的对象
  2. 可以使用get轻松获取到对象

两种情况生成共享对象采用的方法不同

  1. 第一种采用initialValue
  2. 第二种采用set

那么如何抉择呢?
第一种生成对象是由我们来决定的,比如工具类,在初始化时我们就可以将它存入ThreadLocal了。而第二种并不是,比如一个生成账户的业务,是用户传入用户信息来生成的,并不是我们来决定的。所以此时采用set方法。

ThreadLocal的原理

ThreadLocalMap

每个线程通常不仅仅只有一个threadlocal对象,比如同时使用多个工具类,所以一个线程需要存储多个threadlocal
所以在每个Thread对象中有一个ThreadLocalMap对象

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap中存储着不同的threadlocal对象
ThreadLocalMap
ThreadLocalMap是ThreadLocal里的一个静态内部类。

static class ThreadLocalMap

这个类和HashMap相似,不过不同的是解决hash冲突采取的是开放寻址法。
使用的是Entry数组,初始容量为16(必须是2次幂)

private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

每次扩容为原来的2倍,果不其然,在后面找到了和hashMap类似的位运算

int oldLen = oldTab.length;
int newLen = oldLen * 2;
...
int h = k.threadLocalHashCode & (newLen - 1);

ThreadLocal的主要方法

  1. protected T initialValue()

Returns the current thread’s “initial value” for this thread-local variable. This method will be invoked the first time a thread accesses the variable with the get() method, unless the thread previously invoked the set(T) method, in which case the initialValue method will not be invoked for the thread. Normally, this method is invoked at most once per thread, but it may be invoked again in case of subsequent invocations of remove() followed by get().

该方法返回当前线程对应的“初始值”。 该方法将在第一次使用get()方法访问变量时被调用(延迟加载),除非线程先前调用了set(T)方法,在这种情况下, initialValue方法将不会被调用。 通常情况下,这种方法最多每个线程调用一次,但是如果调用了remove方法删除了这个threadlocal,则再次调用Get方法时,会再次初始化

查看源码也可以看见:该方法默认返回null
可以重写该方法来自己实现

protected T initialValue() {
        return null;
    }
  1. public T get()

Returns the value in the current thread’s copy of this thread-local variable

在执行get方法时调用setInitialValue()方法才初始化

public T get() {
		//获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果已经被初始化过
        if (map != null) {
        	//传入当前threadlocal作为key,获取到Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                //返回entry中的value
                return result;
            }
        }
        //如果没有初始化过
        return setInitialValue();
    }
  1. public void set(T value)

Sets the current thread’s copy of this thread-local variable to the specified value

如果调用的是set方法的话,map已经设置了值,map不为空,则返回result

public void set(T value) {
		//获取当前线程
        Thread t = Thread.currentThread();
        //获取线程的threadlocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
       		//如果不为空,就存入
            map.set(this, value);
        else
        	//如果为空,就新建一个map
            createMap(t, value);
    }
  1. public void remove()

Removes the current thread’s value for this thread-local variable.

删除当前线程的threadlocal

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

比如我们在之前的例子中加上这两句

ThreadLocalAccount.threadLocal.remove();
showName();

就会报空指针异常

ThreadLocal可能导致的问题及规避

内存泄漏

内存泄漏(Memory Leak)是指某个对象不再被使用了,但是却无法被垃圾回收器回收而导致的JVM内存无法释放。持续的内存泄漏会使JVM可用的内存减少,直至OOM的发生
那么为什么ThreadLocal会导致内存泄漏呢,我们再次看到它的内部类ThreadLocalMap

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

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

在该类的内部,定义了Entry类,该类继承了弱引用
Entry的key是一个ThreadLocal实例,value则是线程特有对象。
而Entry的构造方法中,key的构造是调用的父类的构造方法,也就是说key是弱引用。而value是直接赋值的强引用
强引用:普通对象引用,只要还有强引用指向一个对象,就表明对象还存活;对于一个对象,如果没有其他的引用关系,只要超过了作用域,或者将引用赋值为Null,就表示可以回收,但是具体回收时机还是看垃圾回收策略。
弱引用:并不能豁免垃圾回收,只是提供一种访问弱引用对象的途径。如果试图访问对象时,对象还在,就直接使用,如果不在,就重新实例化。
也就是说当一个ThreadLocal实例没有对其有可达的强引用时,这个实例可能会被垃圾回收,则key被置为null,此时这个Entry就成为无效条目(Stale Entry),但是这个无效条目对Value的引用又是强引用,导致无法回收线程特有对象。
引用链
Java开发者也考虑了这一问题
提供了这么处理这些Value方法,比如下面这个

private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
}

而在set,remove,rehash,resize等方法中,会调用相应的方法,比如扫描key为null的entry,将其value也设置为null
或者复用无效条目

if (k == null) {
     e.value = null; // Help the GC
}

所以为了避免内存泄漏,不再使用threadlocal时,应该调用remove方法。

共享对象

如果在调用ThreadLocal.set()方法时,传入的是一个static的共享对象,那么即使是ThreadLocal.get()方法取出来的该对象也有可能导致并发安全问题
所以我们需要考虑,如果本身就是支持共享的静态变量,就不要再放入threadlocal当中

Spring中的ThreadLocal

在Spring中许多地方用到了ThreadLocal,比如下面两个

  1. DateTimeContextHolder
    存储了时间上下文
 private static final ThreadLocal<DateTimeContext> dateTimeContextHolder = new NamedThreadLocal("DateTimeContext");

而这个NamedThreadLocal只是对ThreadLocal继承再包装,添加了一个name属性。

public class NamedThreadLocal<T> extends ThreadLocal<T> {
    private final String name;

    public NamedThreadLocal(String name) {
        Assert.hasText(name, "Name must not be empty");
        this.name = name;
    }

    public String toString() {
        return this.name;
    }
}
  1. RequestContextHolder
    这个类的ThreadLocal存储了一些请求的属性
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");

ThreadLocal在web应用中很常见,每次的http请求都会对应一个线程,而线程之间相互隔离,这就是ThreadLocal的典型应用。
如果我们使用的优秀的框架中用到了ThreadLocal,那么就不要再画蛇添足地强行使用ThreadLocal,可能会忘记调用remove而导致内存泄漏问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值