Bootstrap

JAVA多线程基础篇 8、线程隔离与ThreadLocal

在多线程并发环境中,要保证线程并发安全总体有2类方法。

  • 使用锁,在访问资源时是互斥的、原子性的。

    • 这个锁可以是Synchronized方法 或 Lock。
    • 或者CAS方式的乐观锁。
  • 使用线程隔离的方法。

    • 变量在线程内部,在实际运行过程中只有这个线程可以读取。

    • 变量在线程外部定义,但是每个线程只能操作属于该线程的变量副本。即 ThreadLocal类型的变量。

1. ThreadLocal的使用示例

定义了两个ThreadLocal类型的变量,线程t1设置的值,对于线程t2是不可见的。

public class ThreadLocalTest1 {
    private static ThreadLocal<String> thl1 = new ThreadLocal<>();
    private static ThreadLocal<Integer> thl2 = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            thl1.set("abc");
            thl2.set(12);
            thl1.remove();
            thl2.remove();
            System.out.println(thl1.get()+","+ thl2.get());
        });

        Thread t2 = new Thread(()->{
            System.out.println(thl1.get()+","+ thl2.get());
        });

        t1.start();
        t2.start();
    }
}

运行结果:

ThreadLocal

2. ThreadLocal的使用场景

2.1 线程隔离的数据库连接与事务

定义一个数据库连接,每个线程拿到的数据库连接都是本线程对应的数据库连接。

public class ConnectionManager {

    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
        @Override
        //初始化数据。
        //延迟调用方法,在线程第一次调用get或set时才执行,并且只执行1次。默认返回null。
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public Connection getConnection() {
        return dbConnectionLocal.get();
    }
}

2.2 线程隔离的session会话

在每个线程session会话都是本线程对应的session。

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (Exception ex) {  
    }  
    return s;  
}  

3. ThreadLocal原理

  • Thread类内部定义了一个ThreadLocalMap 对象ThreadLocal.ThreadLocalMap threadLocals = null;当线程调用threadlocal对象的set(Object o)和get方法时,实际上是维护ThreadLocalMap对象。

    • 如果这个对象尚未初始化,则初始化
    • set时,以threadlocal对象为key,o 为value。
    • get时,以threadlocal对象为key取出value。
  • 举例说明:

    • thl1.set("abc");thl2.set(12);两条命令会在线程内的ThreadLocalMap对象上插入两条记录,这两条记录的key分别是thl1,thl2。值分别是"abc",12。
    • thl1.get(); thl2.get();命令执行时,实际上会在当前线程内的ThreadLocalMap对象上,查找key为thl1、thl2的值。
  • 也就是说,每个线程内部其实都有一个专属的“记录”——ThreadLocalMap,当写入和读取时,都会以threadlocal对象的引用为key,去存储读取。

4. ThreadLocal与内存泄露

内存泄露是指内存空间不可用,即使jvm进行垃圾回收也无法有效回收垃圾。

4.1 Java语言将Entry设计为弱引用

设想:当线程内对ThreadLocal对象使用方法完毕后,此时没有对象指向ThreadLocal对象,按理说这个对象可以被回收了。但是由于ThreadLocalMap里以key,value的形式存储了ThreadLocal对象。导致仍然有链接指向ThreadLocal对象,不能被回收。

因此,Java语言将Entry类设置为弱引用,当线程内对ThreadLocal对象使用方法完毕后,JVM可以在垃圾回收时,清除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;
            }
        }

4.2 记得手动remove

由于ThreadLocalMap的生命周期和Thread(当前线程)一样长。

尽管java语言做出了良好的设计,但是若当前线程一直不结束,又或者由于线程在线程池中,结束后不被销毁,那么ThreadLocal被回收后,ThreadLocalMap就会存在null,但value不为null的Entry。无效数据会移植存储在内存中,无法回收,导致内存泄露。

所以在使用完ThreadLocal对象后,要及时调用ThreadLocal.remove()方法,手动删除对应value。

总结

ThreadLocal的设计是为了能够在当前线程中有属于自己的变量,其原理是每个线程内部其实都存储了一个ThreadLocalMap来记录保存。ThreadLocal对象存在内存泄露的风险,需要手工remove。

多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。

https://github.com/forestnlp/concurrentlab

如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。

您的支持是对我最大的鼓励。

;