简析ThreadLocal

看到ThreadLocal的时候多少总会跟线程安全关联在一起,因为在线程安全中涉及到共享数据,但是如果不使用共享数据如何来保证线程安全呢?网上有文章分析说,ThreadLocal的出现是为了从另外的一个角度来解决线程安全的问题,以空间换时间,每个线程拥有一份属于自己的数据副本,线程在运行过程中彼此不互相打扰,进水不犯河水。
这是对ThreadLocal的一个初步感性的认识,但是真正去理解的时候,又发现了ThreadLocalMap这个玩意,它到底和ThreadLocal是什么关系呢,一刚开始接触的时候确实会半天不知道在说什么,希望本文能够整理出一份清晰的脉络,以飨自己和其他人。

ThreadLocal的应用场景

很多时候当我们知道知识、技术或者其他等等在什么时候会用到的时候,往往会理解的更加迅速与透彻。这一小节会给出两个应用案例,一个是JDK注释文档上的官方案例,一个是借鉴的网上的资料。
在这之前先来看看,JDK源代码ThreadLocal类最开始的英文注释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g. a user ID or Transaction ID).

翻译过来大概意思就是ThreadLocal提供了线程本地的变量(可以理解为数据),不同“线程同行”有不同的变量,那怎么拿到自己的那一份呢?就是通过通过ThreadLocal对象的get方法获取每个线程自己的数据,当然了,设置的话通过set方法。
好了,那这个ThreadLocal对象一般怎么用呢,怎么玩呢?最后一句说了,ThreadLocal实例对象一般典型的是作为一个类的私有的静态field,与线程的一些状态(这里的状态是个广义的状态,意思应该就是跟线程相关的数据)关系起来。更好的是官方JDK注释给了使用案例。

case x0

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);
 
    // ThreadLocal作为静态私有变量,就是用来获取和存储每个线程自己的线程ID,帮你屏蔽了内部的实现,压根不用管为什么每个线程能获取到自己的不同ID。
    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
        //第一次get时候,会默认调用该方法初始化,暂时不理解没关系,你可以当成是默认值,后面会继续从源码角度分析。
        protected Integer initialValue() {
            return nextId.getAndIncrement();
        }
    };
 
    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
          return threadId.get();
    }
    //还可以实现set方法,手动设置线程本地变量
}

这个官方给出的案例,相当于ThreadId类包裹了ThreadLocal,给我们提供了一种方便的获取和设置线程本地数据的途径。

case x1

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {
    //静态私有的ThreadLocal对象
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        //设置默认值
        @Override
        protected Connection initialValue() {
            Connection conn = null;
            try {
                Class.forName("oracle.jdbc.driver.OracleDriver");
                conn = DriverManager.getConnection(
                        "jdbc:oracle:thin:@localhost:13002:orcl", "root",
                        "root");
            } catch (SQLException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return conn;
        }
    };

    public static Connection getConnection() {
        return connectionHolder.get();
    }

    public static void setConnection(Connection conn) {
        connectionHolder.set(conn);
    }
}

按照我们前面的思路来看的话,不同的线程通过getConnection()获取到的connection是不同的,各自使用各自不同的链接来操作数据库,而每个线程总是会用自己一开始获取的connection,只要这个connection不被清掉。

多个线程

我们先不管内部实现,先来测试看看,用多个线程去获取链接看是什么情况:

    public static void main(String[] args) throws SQLException {
        for (int i=0;i<10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Connection conn=ConnectionManager.getConnection();
                    System.out.println(conn.toString());
                    try {
                        conn.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

测试数据结果如下:

oracle.jdbc.driver.T4CConnection@5b51da40
oracle.jdbc.driver.T4CConnection@3b0b482d
oracle.jdbc.driver.T4CConnection@37acaf5
oracle.jdbc.driver.T4CConnection@6f2f3e6a
oracle.jdbc.driver.T4CConnection@2b7e572f
oracle.jdbc.driver.T4CConnection@42d1696b
oracle.jdbc.driver.T4CConnection@2b8f0eac
oracle.jdbc.driver.T4CConnection@87d61dc
oracle.jdbc.driver.T4CConnection@a84b73
oracle.jdbc.driver.T4CConnection@747870b0
单个线程
    public static void main(String[] args) throws SQLException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    Connection conn=ConnectionManager.getConnection();
                    System.out.println(conn.toString());
                    try {
                        conn.close();
                        //ConnectionManager.removeConnection();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

测试数据:

oracle.jdbc.driver.T4CConnection@3975fb62
oracle.jdbc.driver.T4CConnection@3975fb62
oracle.jdbc.driver.T4CConnection@3975fb62

通过上面的测试正面我们前面的猜想是正确的,这样也就实现了Connection对象在多个线程中的完全隔离。据说在Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码非常相似,但是还没有进行验证。

ThreadLocal源码探析

get方法

在ThreadLocal的使用过程,用到的最多的就是get和set方法,其实就是存取每个线程自己的本地变量数据。先看get方法的源码是如何实现的:

    public T get() {
        Thread t = Thread.currentThread();
        //获得当前这个线程自己的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            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;
    }

在这里我们一开始所说的ThreadLocalMap浮出水面,这个到底是什么呢?其实很简单,说白了就是当前这个线程自己内部的一个属性变量threadLocals,在Thread线程类中,代码如下:

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

拿到这个map之后(假设已经被初始化过不为null),这个时候还没结束,真正取出这个存放的值是通过this取出来的,this是什么?this在前两个案例中就是这个静态的私有的nextId和connectionHolder,也就是ThreadLocal对象,通过它作为key,在每个线程自己的ThreadLocalMap中取出了线程本地的变量值。
所以思路还是很清晰的,每个线程有个map,我们在类似于connectionManager这些类中可以定义很多个ThreadLocal对象,所以根据不同的ThreadLocal对象作为key值,可以在map拿到对应的值。
到这儿其实get方法的分析可以结束了,但是其实可以继续看看getEntry的构造:

    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

setInitialValue设置默认值

在刚才的get方法中,是假设线程所拥有的ThreadLocalMap已经被初始化,但是如果当第一次调用get方法时候,还没有初始化呢?根据上面的代码片段可以看到是调用setInitialValue()方法,方法源代码如下:

    private T setInitialValue() {
        //调用我们覆盖重写的initialValue(),返回一个默认值
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            //创建一个新的ThreadLocalMap,并且赋值给当前的线程
            createMap(t, value);
        return value;
    }
    void createMap(Thread t, T firstValue) {
        //以this作为key值,放进map中
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

以上两小节就是关于get方法的这个流程中主要代码实现。

set方法

set方法就更简单了,跟setInitialValue相比就是自己手动设置线程本地变量,而不是通过默认值的方式。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

关于ThreadLocal是否存在内存泄漏问题

从上面源码分析中,我们大致可以看出引用关系,当前线程>ThreadLocalMap>table>Entry>弱引用ThreadLocal和强引用value。当线程运行结束的时候,线程对象会被回收,线程内部的ThreadLocalMap也会被回收,table和Entry更不用说的,也会被回收,因此value回收。
但是也有一种情况值得考虑,就是当时线程池的时候,这个时候线程池里面的线程有的是经常活跃的,就不能这么来了,具体可以参考这篇文章,博主做了一个关于用线程池的测验,就是在最后不使用线程本地变量的时候,通过ThreadLocal的remove方法清除变量,这样也就解决了线程池可能存在的内存泄漏问题。

小结

其实问题也没我们想象的那么复杂,我们使用线程本地变量就是跟ThreadLocal打交道就行了,至于ThreadLocalMap只是每个线程Thread内部维护的一个map属性。
内部get实现的时候就是通过当前线程拿到自己的map,然后以ThreadLocal的实例为key值拿到属于自己线程的value值,就这样了。

原文首发于博客:http://zouzls.github.io/

-EOF-

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容