轻松理解ThreadLocal的原理及应用场景
阅读原文时间:2021年04月20日阅读:1

ThreadLocal是什么?

ThreadLocal会给每个线程提供一个变量副本, 把共享变量的数据都拷贝到自己的变量副本中, 每个线程都只操作自己变量副本中的变量, 不会操作到主内存中的共享变量. 这样可以防止在并发的情况下,因其他线程对共享变量的修改而对当前线程造成影响.

ThreadLocal的应用场景

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。而这种情况下也可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

package day4_4;

public class TestThreadLocal {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();


    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName() + "线程");
    }

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        final TestThreadLocal test = new TestThreadLocal();


        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());


        Thread thread1 = new Thread() {
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            }

            ;
        };
        thread1.start();
        thread1.join();

        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

执行结果:

可以看到,每个线程的ThreadLocal类型变量是不一样的.

ThreadLocal怎么实现每个线程保存的变量数据不一样呢?

从源码中看到:

// ThreadLocal的set方法
    public void set(T value) {
    // 获取当前线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
        // 保存到Thread类中的threadLocals 中,键为ThreadLocal对象,值为ThreadLocal对应的值
            map.set(this, value);
        else
            createMap(t, value);
    }
    // 获取Thread类中的ThreadLocalMap 
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
        // 每个线程都有一个ThreadLocal.ThreadLocalMap类的threadLocals 变量
        ThreadLocal.ThreadLocalMap threadLocals = null;

从源码中可以看出,ThreadLocal类型的变量在保存时实际是保存在Thread类的threadLocals 变量中的, threadLocals 是一个ThreadLocalMap类, 保存的是ThreadLocal对象和对应的值的键值对. 那么, 为什么键是ThreadLocal对象呢?因为每个线程中可以有多个ThreadLocal类型的变量, 所以要将ThreadLocal对象当做键才能在执行get方法时获取到对应的值.

ThreadLocal的get方法

    public T get() {
        Thread t = Thread.currentThread();
        //获取当前线程的threadLocals 
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        // 根据ThreadLocal对象this从threadLocals 中获取对应的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

get方法也是根据当前的ThreadLocal对象从当前线程的threadLocals 中获取值

ThreadLocal的remove方法

//remove方法, 同理,从线程中的threadLocals 删除
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocal总结

在每个线程Thread类内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

  • 通过ThreadLocal创建的副本实际是存储在每个线程自己的threadLocals中的;
  • 为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
  • 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
  • ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
  • 适用于业务逻辑不依赖副本变量的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案

ThreadLocal问题:

1 ThreadLocal内存泄漏?, 强引用弱引用?, GC? .
2 ThreadLocalMap实现,解决哈希冲突的方法