【Java知识点详解 8】ThreadLocal

huatechinfo
发布于 2021-1-14 17:12
浏览
0收藏

一、基本介绍
ThreadLocal的作用就是:线程安全。

ThreadLocal的本质就是一个内部的静态的map,key是当前线程的句柄,value是需要保持的值。

由于是内部静态map,不提供遍历和查询的接口,每个线程只能获取自己线程的value。

这样,就线程安全了,又提供了数据共享的能力,perfect。

 

二、ThreadLocal的应用场景
1、数据库连接

package com.guor.thread;
 
import java.sql.Connection;
import java.sql.DriverManager;
 
public class ThreadDao {
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>();
    public Connection initialValue(){
        return DriverManager.getConnection(DB_URL);
    }
    
    public static Connection getConnection(){
        return connectionHolder.get();
    }
}


2、session管理

package com.guor.thread;
 
import javax.websocket.Session;
 
public class ThreadDao {
    private static final ThreadLocal threadSession = new ThreadLocal();
    public static Session getSession() throws Exception{
        Session session = (Session)threadSession.get();
        if(session==null){
            session = getSessionFactory().openSession();
            threadSession.set(session);
        }
        return session;
    }
}

三、对ThreadLocal的理解
1、ThreadLocal是线程本地存储,在每个线程中都创建了一个ThreadLocalMap对象,每个线程可以访问自己内部ThreadLocalMap对象内的value。

2、代码实例

package com.guor.thread;
 
public 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();
    }
}


假设有这样一个数据链接管理类,这段代码在单线程中使用没有任何问题,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:

① 这里面的两个方法都没有进行同步,很可能在openConnection方法中会多次创建connect

② 由于connect是共享变量,那么在调用connect的地方需要使用同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程用closeConnection关闭链接。

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

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

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

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

package com.guor.thread;
 
public 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要大。


四、深入解析ThreadLocal类
在上面谈到了对ThreadLocal的一些理解,那我们下面来看一下具体ThreadLocal是如何实现的。

1、ThreadLocal基本方法

public T get() { }  
public void set(T value) { }  
public void remove() { }  
protected T initialValue() { }  


get()方法是用来获取ThreadLocal在当前线程中保存的变量副本
set()用来设置当前线程中变量的副本;
remove()用来移除当前线程中变量的副本;
initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法;
2、set方法源码

public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}


在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}


如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示:

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


createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。

3、get方法源码
在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}
 
private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}


4、remove方法的实现
remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量。

public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }


5、方法综合使用
每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

下面通过一个例子来证明通过ThreadLocal能达到在每个线程中创建变量副本的效果:

package com.guor.thread;
 
public class Test {
    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 Test test = new Test();
 
        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());
    }
}

【Java知识点详解 8】ThreadLocal-鸿蒙开发者社区
从这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样。最后一次在main线程再次打印副本值是为了证明在main线程中和thread1线程中的副本值确实是不同的。

6、总结一下
(1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

(2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;

(3)在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。


五、ThreadLocal不支持继承性
同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

package com.guor.thread;
 
public class ThreadLocalTest2 {
 
    //(1)创建ThreadLocal变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 
    public static void main(String[] args) {
        //在main线程中添加main线程的本地变量
        threadLocal.set("mainVal");
        //新创建一个子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程中的本地变量值:"+threadLocal.get());
            }
        });
        thread.start();
        //输出main线程中的本地变量值
        System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
    }
}

六、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题
1、基础概念 
首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。

(1)强引用
Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

(2)软引用
简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中

(3)弱引用
这里讨论ThreadLocalMap中的Entry类的重点。

如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null

(4)虚引用
虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)

 

2、分析ThreadLocalMap内部实现
上面我们知道ThreadLocalMap内部实际上是一个Entry数组,我们先看看Entry的这个内部类

/**
 * 是继承自WeakReference的一个类,该类中实际存放的key是
 * 指向ThreadLocal的弱引用和与之对应的value值(该value值
 * 就是通过ThreadLocal的set方法传递过来的值)
 * 由于是弱引用,当get方法返回null的时候意味着坑能引用
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** value就是和ThreadLocal绑定的 */
    Object value;
 
    //k:ThreadLocal的引用,被传递给WeakReference的构造方法
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
//WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {
    super(referent); //referent:ThreadLocal的引用
}
 
//Reference构造方法
Reference(T referent) {
    this(referent, null);//referent:ThreadLocal的引用
}
 
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}


在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

3、总结
THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

收藏
回复
举报
回复
    相关推荐