Android源码解析Handler系列第(二)篇--- ThreadLocal详解

转载请注明文章出处LooperJing

在上篇文章Android源码解析Handler系列第(一)篇说了Message的内部维持的全局池机制。这一篇仍然是准备知识,因为在Handler中有ThreadLocal的身影,大家知道,Handler创建的时候会采用当前线程的Looper来构造消息循环系统,那么Handler内部如何获取到当前线程的Looper呢?这就要使用ThreadLocal了,ThreadLocal可以在不同的线程之中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper,所以,ThreadLocal是理解Looper的关键之一。

先看一下官方对这个类的解释

/**
 * Implements a thread-local storage, that is, a variable for which each thread
 * has its own value. All threads share the same {@code ThreadLocal} object,
 * but each sees a different value when accessing it, and changes made by one                                                      
 * thread do not affect the other threads. The implementation supports
 * {@code null} values.
 *
 * @see java.lang.Thread                                                                                                                                                                                                                                                                                   
 * @author Bob Lee
 */

大概意思就是说:实现了一个线程本地存储,也就是说,每个线程的一个变量,有自己的值。所有线程共享同一个 ThreadLocal对象,但每个线程访问它时,看到一个不同的值,如果一个线程改变了这个值,不影响其他线程,ThreadLocal支持NULL值。理解好费劲,只能说我翻译的太差!

重新解释一下:当工作于多线程中的对象使用ThreadLocal 维护变量时,ThreadLocal 为 每个使用该变量的线程分配一个独立的变量副本。每个线程独立改变自己的副本,而不影响其他线程所对应的变量副本。这就是它的基本原理,ThreadLocal主要的 API很简单。

public void set(T value):将值放入线程局部变量中

public T get():从线程局部变量中获取值

public void remove():从线程局部变量中移除值(有助于 JVM 垃圾回收)

protected T initialValue():返回线程局部变量中的初始值(默认为 null)

ThreadLocal为各个线程保存一个变量副本,这句话是关键,怎么理解?先来一个简单的DEMO

public class ThreadLocalTest {
  
    public static void main(String[] args) throws InterruptedException {
        
        
        ThreadLocal<Boolean> mBooleanThreadLocal=new ThreadLocal<Boolean>(){
            
            @Override
            protected Boolean initialValue() {
            
                //初始值是false
                return false;
            }
        };
        mBooleanThreadLocal.set(true);
    
        System.out.println("[Thread#main]mBooleanThreadLocal=" +mBooleanThreadLocal.hashCode()+"  "+ mBooleanThreadLocal.get());

        new Thread("Thread#1") {
            @Override
            public void run() {
                mBooleanThreadLocal.set(false);
                System.out.println( "[Thread#1]mBooleanThreadLocal="+mBooleanThreadLocal.hashCode() +"  "+ mBooleanThreadLocal.get());
            };
        }.start();

        new Thread("Thread#2") {
            @Override
            public void run() {
                System.out.println( "[Thread#2]mBooleanThreadLocal="+mBooleanThreadLocal.hashCode() + "  "+mBooleanThreadLocal.get());
            };
        }.start();
    }
}
输出结果:

[Thread#main]mBooleanThreadLocal=366712642      true
[Thread#1]mBooleanThreadLocal=366712642      false
[Thread#2]mBooleanThreadLocal=366712642      false

在上面的代码中,在主线程中设置mBooleanThreadLocal的值为true,在子线程1中设置mBooleanThreadLocal的值为false,在子线程2中不设置mBooleanThreadLocal的值,然后分别在3个线程中通过get方法去mBooleanThreadLocal的值,所以,主线程中应该是true,子线程1中应该是false,而子线程2中由于没有设置值,所以应该是初始值false。

再看一个DEMO

public class ThreadLocalTest {
        
        //创建一个Integer型的线程本地变量
    public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[5];
        for (int j = 0; j < 5; j++) {       
               threads[j] = new Thread(new Runnable() {
                @Override
                public void run() {
                     //获取当前线程的本地变量,然后累加5次
                    int num = local.get();
                    for (int i = 0; i < 5; i++) {
                        num++;
                    }
                                        //重新设置累加后的本地变量
                    local.set(num);
                    System.out.println(Thread.currentThread().getName() + " : "+ local.get());

                }
            }, "Thread-" + j);
        }

        for (Thread thread : threads) {
            thread.start();
        }
    }
}
输出结果:

Thread-1 : 5
Thread-3 : 5
Thread-4 : 5
Thread-0 : 5
Thread-2 : 5

开了5个线程,每个线程累加后的结果都是5,各个线程处理自己的本地变量值,线程之间互不影响。

读到这里,相信你对ThreadLocal的作用已经了解了,ThreadLocal和synchronized是有区别的,概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响,所以ThreadLocal和synchronized都能保证线程安全,但是应用场景却大不一样。

现在从源码中去找找答案,为什么各个线程都能保留一份副本,做到多并发的时候,线程互不影响呢?

ThreadLocal的构造函数是空的,啥也没有。

public ThreadLocal() {
}

看一下里面的set方法

    /**
     * Sets the value of this variable for the current thread. If set to
     * {@code null}, the value will be set to null and the underlying entry will
     * still be present.
     *
     * @param value the new value of the variable for the caller thread.
     */
    public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }

首先获得了当前线程实例,将实例传入values方法中,获得一个Values对象,第一次获得的Values对象是空的,就调用initializeValues初始化一个values对象,最后把当前对象作为key,value作为值,放进values中。现在的重点就是搞懂Values是什么?Values原来是ThreadLocal的静态内部类。在ThreadLocal.Values中有一个table成员,而这个table就以key,value的形式存储了线程的本地变量。key是ThreadLocal<T>类型的对象的弱引用,而Value则是线程需要保存的线程本地变量T。

 /** 
  * 存放数据的数组。他存放数据的形式有点像map,是ThreadLocal与value相对应, 长度总是2的N次方
  */
 private Object[] table;

table表结构

Value的主要API有下面几个

  • void put(ThreadLocal<?> key, Object value):往table里添加一个键值对
  • void add(ThreadLocal<?> key, Object value):也是往table里面添加键值对
  • Object getAfterMiss(ThreadLocal<?> key):在首位置没找到值的时候通过这个方法来找到给定key的值
  • void remove(ThreadLocal<?> key):删掉给定key对应的值


    ThreadLocal.Values的其他成员

Values是调用put方法把传进来的键值对给存到table表里面去,这里不去分析它是怎么实现的了(可以移步http://blog.csdn.net/luoyanglizi/article/details/51510233

 /**
         * Sets entry for given ThreadLocal to given value, creating an
         * entry if necessary.
         */
        void put(ThreadLocal<?> key, Object value) {
            cleanUp();

            // Keep track of first tombstone. That's where we want to go back
            // and add an entry if necessary.
            int firstTombstone = -1;

            for (int index = key.hash & mask;; index = next(index)) {
                Object k = table[index];

                if (k == key.reference) {
                    // Replace existing entry.
                    table[index + 1] = value;
                    return;
                }

                if (k == null) {
                    if (firstTombstone == -1) {
                        // Fill in null slot.
                        table[index] = key.reference;
                        table[index + 1] = value;
                        size++;
                        return;
                    }

                    // Go back and replace first tombstone.
                    table[firstTombstone] = key.reference;
                    table[firstTombstone + 1] = value;
                    tombstones--;
                    size++;
                    return;
                }

                // Remember first tombstone.
                if (firstTombstone == -1 && k == TOMBSTONE) {
                    firstTombstone = index;
                }
            }
        }

上面是ThreadLocal存的过程,现在看取的过程。

    /**
     * Returns the value of this variable for the current thread. If an entry
     * doesn't yet exist for this variable on this thread, this method will
     * create an entry, populating the value with the result of
     * {@link #initialValue()}.
     *
     * @return the current value of the variable for the calling thread.
     */
    @SuppressWarnings("unchecked")
    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }

首先取出当前线程的Values对象,跟set方法一样,如果这个对象为null那么就返回初始值, 如果Values对象不为null,那就取出它的table数组并找出ThreadLocal的reference对象在table数组中的位置,然后table数组中的下一个位置所存储的数据就是ThreadLocal的值。从可以看出,ThreadLocal的set和get方法所操作的对象都是当前线程的Values对象的table数组,如果一个变量使用了ThreadLocal,通过ThreadLocal的set和get方法,每一个线程的都会有一份这个变量的副本(键值对的形式进行存取),这就是为什么多线程并发的时候,线程互不影响的操作一个变量。

最后在举一个ThreadLocal的应用的例子:JDBC连接mysql数据库,把 Connection 放到了 ThreadLocal 中,这样每个线程之间就隔离了,不会相互干扰。

public class DBUtil {
    // 数据库配置
    private static final String driver = "com.mysql.jdbc.Driver";
    private static final String url = "jdbc:mysql://localhost:3306/demo";
    private static final String username = "xxx";
    private static final String password = "xxx";

    // 定义一个用于放置数据库连接的局部线程变量(使每个线程都拥有自己的连接)
    private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();

    // 获取连接
    public static Connection getConnection() {
        Connection conn = connContainer.get();
        try {
            if (conn == null) {
                Class.forName(driver);
                conn = DriverManager.getConnection(url, username, password);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            connContainer.set(conn);
        }
        return conn;
    }

    // 关闭连接
    public static void closeConnection() {
        Connection conn = connContainer.get();
        try {
            if (conn != null) {
                conn.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            connContainer.remove();
        }
    }
}

Please accept mybest wishes for your happiness and success

参考链接:
https://my.oschina.net/clopopo/blog/149368
http://blog.csdn.net/singwhatiwanna/article/details/48350919
https://my.oschina.net/huangyong/blog/159725

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

推荐阅读更多精彩内容