Java 知识-- ThreadLocal

本文最后更新于:20 天前

概念

ThreadLocal 是线程本地变量

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

应用场景

class ConnectionManager {
     
    private static Connection connect = null;
     
    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。

所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。

这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。

那么这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。

到这里,可能会有朋友想到,既然不需要在线程之间共享这个变量,可以直接这样处理,在每个需要使用数据库连接的方法中具体使用时才创建数据库链接,然后在方法调用完毕再释放这个连接。比如下面这样:

class ConnectionManager {

    private Connection connect = null;

    public Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

class Dao{
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();

        //使用connection进行操作

        connectionManager.closeConnection();
    }
}

这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服务器压力巨大。

那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

class ConnectionManager {

    private final ThreadLocal<Connection> con =
            ThreadLocal.withInitial(()->{
                try {
                    return DriverManager.getConnection("jdbc://", "root","root");
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                    return null;
                }
            }

    public Connection openConnection() {
        return connect.get();
    }
}

综上:ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景。即在不同线程中都希望用到这个变量但不希望出现多线程对这个变量产生任何影响,使用 ThreadLocal 之后,不同线程对于这个变量的操作都只在这个线程内部其作用,其他线程不可见。

而且,ThreadLocal 只是将代码变得更加整齐简洁,通过方法间显示的传递对象自身,也可以达到相同的结果,只是方法间的耦合度较高,看起来不简洁。

原理

首先看 ThreadLocal 的主要方法:

get(),set(),remove()

get

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 注意是 this 不是 t,说明 ThreadLocalMap 的 key 为 ThreadLocal 而非 Thread
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
        }
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}

当使用 ThreadLocal.get() 方法时,首先获取到当前线程的一个引用 t,然后通过 getMap 方法获取到这个线程内部的 ThreadLocalMap 变量 threadLocals,再从这个 threadLocals 变量中拿到这个 ThreadLocal 自己这个 key 所对应的 value

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);
}

void createMap(Thread t, T firstValue){
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果当前线程的 threadLocals 不为空,则直接设置,为空则先创建 Map

remove

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

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

注:
当 JVM GC 时,只是把 ThreadLocalMap 中的 key (WeakReference 弱引用)给回收掉了,value 值没有处理,所以这可能会造成内存泄漏甚至 OOM 内存溢出的问题,所以需要手动调用 ThreadLocal 的 remove 方法,保证 key 和 value 都被正确删除。

分析

因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它,其它线程不会访问该 ThreadLocalMap,自然也无法获取到 ThreadLocalMap 中存储的 ThreadLocal 为 key 所对应的 value,虽然 ThreadLocal 对象是线程都可以访问的,但是 TreadLocalMap 不会在多个线程中共享,即使是ThreadLocal 对象获取到的 value 副本也只和线程自己有关,和其他线程无关,也无法访问到,也就不存在线程安全的问题。

小问题:

ThreadLocalMap 是通过什么机制存储 ThreadLocal 呢?