暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

NO.43 线程的私有资产: ThreadLocal

技术夜未眠 2018-01-16
181


加入读书践行群,每天一个知识点,持续精进!


碎片时间|体系学习

这是程序员chatbook第97篇原创

今天是
2018年的第 16 


今日难度系数 :🌟🌟🌟

预计阅读时间 : 5 分钟



00、引用


ThreadLocal,即线程变量,是一个以ThreadLocal对象为key、任意对象为值的键-值对存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。一般地,可以通过set()方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。


举例:我们同样以学生时代大家在澡堂洗澡的经历为例进行说明。


之前,有1000个同学同时去洗澡,但是澡堂只有1个洗澡喷头,这意味着我们需要保证这1000个同学不能去哄抢这个喷头,不然大家都不能完成洗澡;最后,通过锁机制保证了1000个同学顺利完成了洗澡。这是大家熟知的“锁机制”。


后来,学校新购买了喷头共有1000个,这样就保证了每个同学都能有一个喷头,那么所有同学都可以同时洗澡,不用再抢喷头了。这就是今天我们要谈的“ThreadLocal机制”。


ThreadLocal机制并不是用于多线程之间进行数据共享,而是为线程提供了可以拥有自己局部变量的技术途径,该变量只有当前线程才能访问,进而也保证了线程安全的,这是典型的“用空间换时间”的思路。


01、ThreadLocal说明


ThreadLocal为每个使用该变量的线程提供独立的变量副本,每个变量都维护了自己的、完全独立的一个变量副本,所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。


ThreadLocal<T>类的核心方法及功能介绍,详见代码1。


1

//设置当前线程的线程局部变量的值

public void set(T value)

//返回当前线程所对应的线程局部变量值

public T get() 

//将当前线程局部变量的值删除。当线程结束后,对应该线程的局部变量将自动被GC回收
//所以,显式调用该方法清除线程的局部变量并不是必须的操作,但是可以加快内存回收的速度

public void remove()


//返回当前线程局部变量的初始值,该方法是一个protected方法,是为了让子类覆盖而设计;
//该方法是一个延迟调用方法,在线程第一次调用get()或set(Object)方法时才执行,
//并且有且只执行一次。该方法缺省实现返回为null

protected T initialValue()


为了说明如何使用ThreadLocal的核心API,最好的方式就是编写一个典型用例,详见代码2。

2

//ThreadLocal典型应用示例

public class ThreadLocalDemo {

   //入职的第一天,单位给新员工办了一张ICBC卡

   //为了保证办卡成功,预充值了10RMB

   private static ThreadLocal<Integer> ICBCCard = new ThreadLocal<Integer>(){

       public Integer initialValue(){

           return 10;

       }

   };

   //模拟发一笔工资

   public int transfer(int income){

ICBCCard.set(ICBCCard.get() + income);

return ICBCCard.get();

   }

   //模拟发工资的场景

   private static class PayOff extends Thread{

private ThreadLocalDemo demo;

public PayOff(ThreadLocalDemo t){

   demo = t;

}

//将工资发放后的余额变化情况进行打印

public void report(int total){

   System.out.println(Thread.currentThread().getName() + " => ICBC余额 : "+ total+"RMB");

}

//模拟发放多笔工资

public void run(){

   //发通讯补助

   int total = demo.transfer(1000);

   report(total);

   //发交通补助

   total=demo.transfer(800);

   report(total);

   //发餐饮补助

   total=demo.transfer(600);

   report(total);

   //发绩效奖金

   //TODO

           
//很重要:每个线程用完以后都要执行删除操作
           
demo
.getThreadLocl().remove();

       }

   }


   //测试客户端

   public static void main(String[] args) {

ThreadLocalDemo tl = new ThreadLocalDemo();

PayOff user1 = new PayOff(tl);

user1.setName("员工1");

PayOff user2 = new PayOff(tl);

user2.setName("员工2");

PayOff user3 = new PayOff(tl);

user3.setName("员工3");

user1.start();

user2.start();

user3.start();

   }

}


结果

员工3 => ICBC余额 : 1010RMB

员工2 => ICBC余额 : 1010RMB

员工1 => ICBC余额 : 1010RMB

员工2 => ICBC余额 : 1810RMB

员工3 => ICBC余额 : 1810RMB

员工2 => ICBC余额 : 2410RMB

员工1 => ICBC余额 : 1810RMB

员工3 => ICBC余额 : 2410RMB

员工1 => ICBC余额 : 2410RMB


从程序的输出结果可以非常清晰的看出:虽然每位员工在程序中是共享了ICBCCard,但是每位员工的收入都在独立的变化,并且互不影响,这是因为ThreadLocal为每个线程提供了单独的副本。


02、ThreadLocal实现机理


ThreadLocal实现线程隔离的秘密是什么了?


在揭开面纱之前,我们先来认识ThreadLocal实现中重要的数据结构——ThreadLocalMap。该数据结构可以理解为是一个类似HashMap的存储结构,其实现使用了弱引用。弱引用是比强引用弱得多的引用。JVM在GC时,一旦发现弱引用就会立即回收。ThreadLocalMap内部由一系列Entry构成,每个Entry都是WeakReference <ThreadLocal>,详细定义见代码3。


3

/**

  * The entries in this hash map extend WeakReference, using

  * its main ref field as the key (which is always a

  * ThreadLocal object).  Note that null keys (i.e. entry.get()

  * == null) mean that the key is no longer referenced, so the

  * entry can be expunged from table.  Such entries are referred to

  * as "stale entries" in the code that follows.

  */

static class Entry extends WeakReference<ThreadLocal<?>> {

    /** The value associated with this ThreadLocal. */

   Object value;


    Entry(ThreadLocal<?> k, Object v) {

        super(k);

       value = v;

    }

}


代码3中,参数k就是ThreadLocalMap的key,就是ThreadLocal的实例,作为弱引用使用;参数v就是ThreadLocalMap的value。


set与put操作是ThreadLocal的两个核心函数,下面我们结合这个两个函数的实现,来说明一下ThreadLocal的实现机理。其中set函数见代码3,put函数见代码4。

4

     /**

     * Sets the current thread's copy of this thread-local variable

     * to the specified value.  Most subclasses will have no need to

     * override this method, relying solely on the {@link #initialValue}

     * method to set the values of thread-locals.

     *

     * @param value the value to be stored in the current thread's copy of

     *        this thread-local.

     */

    public void set(T value) {

    //获取当前线程

        Thread t = Thread.currentThread();

        

        //获取一个和当前线程相关的ThreadLocalMap

        ThreadLocalMap map = getMap(t);

        

        if (map != null)

            map.set(this, value);

        else

            createMap(t, value);

    }

    

    /**

     * Get the map associated with a ThreadLocal. Overridden in

     * InheritableThreadLocal.

     *

     * @param  t the current thread

     * @return the map

     */

    //获取一个和当前线程相关的ThreadLocalMap

    ThreadLocalMap getMap(Thread t) {

        return t.threadLocals;

    }


    /**

     * Create the map associated with a ThreadLocal. Overridden in

     * InheritableThreadLocal.

     *

     * @param t the current thread

     * @param firstValue value for the initial entry of the map

     */

    void createMap(Thread t, T firstValue) {

        t.threadLocals = new ThreadLocalMap(this, firstValue);

    }


在进行set操作时,首先获得当前线程对象,然后通过getMap(Thread t)得到和当前线程绑定的ThreadLocalMap,如果map不为空,则将变量值设置到这个ThreadLocalMap中;如果map为空,就通过createMap方法进行创建。这里我们要特别注意getMap函数,通过该函数我们知道ThreadLocalMap是定义在Thread中成员变量


设置到ThreadLocal中的数据,都写入到了ThreadLocalMap中。其中key为ThreadLocal当前对象,value就是我们设置的值。而ThreadLocalMap本身就保持了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。

5

   /**

     * Returns the value in the current thread's copy of this

     * thread-local variable.  If the variable has no value for the

     * current thread, it is first initialized to the value returned

     * by an invocation of the {@link #initialValue} method.

     *

     * @return the current thread's value of this thread-local

     */

    public T get() {

        Thread t = Thread.currentThread();

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

    }

    

    /**

     * Variant of set() to establish initialValue. Used instead

     * of set() in case user has overridden the set() method.

     *

     * @return the initial value

     */

    private T setInitialValue() {

        T value = initialValue();

        Thread t = Thread.currentThread();

        ThreadLocalMap map = getMap(t);

        if (map != null)

            map.set(this, value);

        else

            createMap(t, value);

        return value;

    }


在进行get操作时,也是首先获取当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据。


通过对上述两个核心函数的讨论,下面我们来揭开ThreadLocal的线程隔离的秘密:其核心在于ThreadLocalMap类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取,每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程所持有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。


推而广之,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离。根据ThreadLocalMap的机理,因为不同的ThreadLocal对象可作为不同键,相应的也可设置成相应的不同的值。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,可类比为在一个HashMap对象中存储一个键值对和多个键值对一样,其原理见图1。

图1 ThreadLocalMap原理


03、小结


本文详细讨论了ThreadLocal的用途、典型用法及其实现机理,欢迎老铁们在留言区写下你的感悟与大家共同交流。


划重点

  1. ThreadLocal不是用来解决多线程访问共享对象的方法

  2. 通过ThreadLocal中的set函数,设置到线程中的对象是该线程自己使用的对象,其他线程不需要访问,也不能访问。各个线程都有自己的不同的变量副本,各自安好

  3. ThreadLocal能实现线程隔离的秘密在于ThreadLocalMap,每个线程都有自己的ThreadLocalMap,其中ThreadLocal的实例作为key来使用

  4. 如果ThreadLocal对象set设置进去的对象本来就是多个线程共享的同一个对象,那么多个线程调用ThreadLocal的get方法后,获得的还是这个共享对象,那么仍然存在并发访问问题

  5. ThreadLocal比synchronized同步机制解决线程安全问题更简单,并拥有更好的并发性能,是“空间换时间”的典型应用

  6. 使用ThreadLocal,一般都声明为静态变量,并且不再使用后需要显式调用remove方法,否则将导致内存泄漏


【参考资料】

1 Java多线程编程实战指南, 黄文海, 中国工信出版集团。

2 实战Java高并发程序设计, 葛一鸣,郭超, 中国工信出版集团,电子工业出版社。


如果觉得文章有用,感谢老铁们转发分享,让更多的小伙伴建立连接!


本文延伸阅读

上文1:NO.40  比较交换:无锁的原理及陷阱

上文2:NO.39 天下无锁 :原子操作类

推荐1:习惯决定命运,高效程序员的习惯

推荐2:编写可读代码的艺术


程序员Chatbook

          

程序员都关注了,来不及解释,长按图片,快上车


文章转载自技术夜未眠,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论