专栏首页Spark学习技巧谈谈java的ThreadLocal

谈谈java的ThreadLocal

简单介绍

ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内。

跳出误区

需要重点强调的的是,不要拿ThreadLocal和synchronized做类比,因为这种比较压根就是无意义的!sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问。而ThreadLocal从本质上讲,无非是提供了一个“线程级”变量作用域,它是一种线程封闭(每个线程独享变量)技术,更直白点讲,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”。

没有ThreadLocal的时候,一个线程在其生命周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。

来看个简单的例子

假设我们要为每个线程关联一个唯一的序号,在每个线程周期内,我们需要多次访问这个序号,这时我们就可以使用ThreadLocal了.(当然下面这个例子没有完全体现出跨层级跨方法的调用,理解就可以了)

package concurrent;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by chengxiao on 2016/12/12.
 */
public class ThreadLocalDemo {
    public static void main(String []args){
        for(int i=0;i<5;i++){
            final Thread t = new Thread(){
                @Override
                public void run(){
                    System.out.println("当前线程:"+
                    Thread.currentThread().getName()+",已分配ID:"
                    +ThreadId.get());
                }
            };
            t.start();
        }
    }
    static   class ThreadId{
        //一个递增的序列,使用AtomicInger原子变量保证线程安全
        private static final AtomicInteger nextId = new AtomicInteger(0);
        //线程本地变量,为每个线程关联一个唯一的序号
        private static final ThreadLocal<Integer> threadId =
                new ThreadLocal<Integer>() {
                    @Override
                    protected Integer initialValue() {
                        return nextId.getAndIncrement();//相当于nextId++,
                        由于nextId++这种操作是个复合操作而非原子操作,
                        会有线程安全问题(可能在初始化时就获取到相同的ID,
                        所以使用原子变量
                    }
                };

       //返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,
       后面看源码就了解了
        public static int get() {
            return threadId.get();
        }
    }
}

执行结果,可以看到每个线程都分配到了一个唯一的ID,同时在此线程范围内的"任何地点",我们都可以通过ThreadId.get()这种方式直接获取。

当前线程:Thread-4,已分配ID:1
当前线程:Thread-0,已分配ID:0
当前线程:Thread-2,已分配ID:3
当前线程:Thread-1,已分配ID:4
当前线程:Thread-3,已分配ID:2 

set操作,为线程绑定变量

public void set(T value) {
    //1.首先获取当前线程对象
    Thread t = Thread.currentThread();
    //2.获取该线程对象的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //如果map不为空,执行set操作,以当前threadLocal对象为key,
    //实际存储对象为value进行set操作
    if (map != null)
        map.set(this, value);
    else
    //如果map为空,则为该线程创建ThreadLocalMap
        createMap(t, value);
}

可以看到,ThreadLocal不过是个入口,真正的变量是绑定在线程上的。

ThreadLocalMap getMap(Thread t) {
//线程对象持有ThreadLocalMap的引用
    return t.threadLocals;
}

下面给是Thread类中的定义,每个线程对象都拥有一个ThreadLocalMap对象

ThreadLocal.ThreadLocalMap threadLocals = null;

现在,我们能看出ThreadLocal的设计思想了:

1.ThreadLocal仅仅是个变量访问的入口;

2.每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有对象的引用;

3.ThreadLocalMap以当前的threadlocal对象为key,以真正的存储对象为value。get时通过threadlocal实例就可以找到绑定在当前线程上的对象。

乍看上去,这种设计确实有些绕。我们完全可以在设计成Map<Thread,T>这种形式,一个线程对应一个存储对象。

ThreadLocal这样设计的目的主要有两个:

一是可以保证当前线程结束时相关对象能尽快被回收;

二是ThreadLocalMap中的元素会大大减少,我们都知道map过大更容易造成哈希冲突而导致性能变差。

我们再来看看get方法

public T get() {
 //1.首先获取当前线程
 Thread t = Thread.currentThread();
 //2.获取线程的map对象
 ThreadLocalMap map = getMap(t);
 //3.如果map不为空,以threadlocal实例为key
 //获取到对应Entry,然后从Entry中取出对象即可。
 if (map != null) {
     ThreadLocalMap.Entry e = map.getEntry(this);
     if (e != null)
         return (T)e.value;
 }
 //如果map为空,也就是第一次没有调用set直接get
 //(或者调用过set,又调用了remove)时,为其设定初始值
 return setInitialValue();
}

setInitialValue

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;
}

initialValue方法,默认是null,访问权限是protected,即允许重写。

protected T initialValue() {
return null;
}

谈到这儿,我们应该已经对ThreadLocal的设计目的及设计思想有一定的了解了。

还有一个会引起疑惑的问题,我们说ThreadLocal为每一个线程维护一个独立的变量副本,那么是不是说各个线程之间真正的做到对于对象的“完全自治”而不对其他线程的对象产生影响呢?其实这已经不属于对于ThreadLocal的讨论,而是你出于何种目的去使用ThreadLocal。如果我们为一个线程关联的对象是“完全独享”的,也就是每个线程拥有一整套的新的 栈中的对象引用+堆中的对象,那么这种情况下是真正的彻底的“线程独享变量”,相当于一种深度拷贝,每个线程自己玩自己的,对该对象做任何的操作也不会对别的线程有任何影响。

另一种更普遍的情况,所谓的独享变量副本,其实也就是每个线程都拥有一个独立的对象引用,而堆中的对象还是线程间共享的,这种情况下,自然还是会涉及到对共享资源的访问操作,依然会有线程不安全的风险。所以说,ThreadLocal无法解决线程安全问题。

所以,需不需要完全独享变量,进行完全隔离,就取决于你的应用场景了。可以想象,对象过大的时候,如果每个线程都有这么一份“深拷贝”,并发又比较大,对于服务器的压力自然是很大的。像web开发中的servlet,servlet是线程不安全的,一请求一线程,多个线程共享一个servlet对象;而早期的CGI设计中,N个请求就对应N个对象,并发量大了之后性能自然就很差。

ThreadLocal使用注意

ThreadLocal<T>的出现是一种空间换时间的思想的运用,是为了多线程环境下单线程内变量共享的问题。它的原理就是每个线程通过ThreadLocal.ThreadLocalMap,保存当前线程中所有ThreadLocal变量引用的key和值。相当于每个线程有各自的变量副本,线程内共享这个变量数据,线程间互不影响。

ThreadLocal<T>有它自己的使用场景,比如Spring中用它了解决Session、Connection等多线程并发访问问题,但不能它不能用来代替为了解决多线程安全问题的同步关键字,因为它实际上没有多线程间的变量共享,而线程安全问题是指多线程间变量共享,且共享变量可修改,进而可能会出现多线程并发修改共享变量的问题,这种需要通过同步手段解决。

ThreadLocal<T>变量一般要声名成static类型,即当前线程中只有一个T类型变量的实例,线程内可共享该实例数据且不会出问题,如将其声名成非static,则一个线程内就存储多个T类型变量的实例,有点存储空间的浪费,一般很少有这样的应用场景。另外根据实际情况,ThreadLocal变量声名时也多加上private final关键词表明它时类内私有、引用不可修改。

在线程池环境下,由于线程是一直运行且复用的,使用ThreadLocal<T>时会出现这个任务看到上个任务ThreadLocal变量值以及内存泄露等问题,解决方法就是在当前任务执行完后将ThreadLocal变量remove或设置为初始值,类似在Struts2 框架中Filter里的处理方法。

Spark中如何使用ThreadLocal的呢?

spark使用的是InheritableThreadLocal,该类扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。通常,子线程的值与父线程的值是一致的;但是,通过重写这个类中的 childValue 方法,子线程的值可以作为父线程值的一个任意函数。

当必须将变量(如用户 ID 和 事务 ID)中维护的每线程属性(per-thread-attribute)自动传送给创建的所有子线程时,应尽可能地采用可继承的线程局部变量,而不是采用普通的线程局部变量。

InheritableThreadLocal,在spark中主要有三个地方用到:

1,SparkContext内部

2,DynamicVariable内部

动态变量是scala的特性

3,InputFileBlockHolder内部

本文部分摘取自网络,如有侵权,请联系浪尖删除。

本文分享自微信公众号 - Spark学习技巧(bigdatatip)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-04-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java多线程和并发基础面试问答

    多线程和并发问题是Java技术面试中面试官比较喜欢问的问题之一。在这里,从面试的角度列出了大部分重要的问题,但是你仍然应该牢固的掌握Java多线程基础知识来对应...

    Spark学习技巧
  • 一文搞定Java并发编程面试考点

    任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thre...

    Spark学习技巧
  • java线程池模型

    一, 线程池与普通线程 1 普通线程 Java实现多线程,常见的有以下三种方式: 1 继承Thread,重写该类的run()方法 2 实现Runnable 实现...

    Spark学习技巧
  • 高级java必须清楚的概念:原子性、可见性、有序性

    原子性、可见性、有序性是多线程编程中最重要的几个知识点,由于多线程情况复杂,如何让每个线程能看到正确的结果,这是非常重要的。 原子性 原子性是指一个线程的操作是...

    Java技术栈
  • java面试必备之ThreadLocal

    按照传统的经验,如果某个对象是非线程安全的,在多线程环境下对象的访问需要采用synchronized进行同步。但是模板类并未采用线程同步机制,因为线程同步会降低...

    JKXQJ
  • 40个Java多线程问题总结

    java多线程分类中写了21篇多线程的文章,21篇文章的内容很多,个人认为,学习,内容越多、越杂的知识,越需要进行深刻的总结,这样才能记忆深刻,将知识变成自己的...

    Java架构师历程
  • 个人珍藏的80道多线程并发面试题(11-20答案解析)

    个人珍藏的80道Java多线程/并发经典面试题,现在给出11-20的答案解析哈,并且上传github哈~

    捡田螺的小男孩
  • springBoot 线程池异步编程

    2、基于注解进行参数的配置 在config包下,创建PayThreadPoolConfig.java配置类:

    居士
  • 如何编写线程安全的代码?

    相信有很多同学在面对多线程代码时都会望而生畏,认为多线程代码就像一头难以驯服的怪兽,你制服不了这头怪兽它就会反过来吞噬你。

    用户1516716
  • 100道Java并发和多线程基础面试题大集合(含解答),这波面试稳了~

    这些多线程的问题来源于各大网站,可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都看过,但是本文写作的重心就是所有的问题都会按照自己的理解回...

    程序员白楠楠

扫码关注云+社区

领取腾讯云代金券