ThreadLocal基本原理及运用

意识上传
• 阅读 7838

ThreadLocal简述

下面我们看一下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提供本地线程变量。这个变量里面的值(通过get方法获取)是和其他线程分割开来的,变量的值只有当前线程能访问到,不像一般的类型比如Person,Student类型的变量,只要访问到声明该变量的对象,即可访问其全部内容,而且各个线程的访问的数据是无差别的。Thread的典型应用是提供一个与程序运行状态相关静态变量,比如一次访问回话的表示符号:USERID,或者一次事务里面的事务id:Transaction ID。

基本原理

线程本地变量是和线程相关的变量,一个线程则一份数据。我们通过ThreadLocal保存的数据最终是保存在Thread类的ThreadLocalMap threadLocals变量中。ThreadlocalMap是一个Map结构,其中key为我们声明的ThreadLocal对象,value即为我们使用ThreadLocal保存的线程本地变量.

当我们调用ThreadLocal变量set方法时,那么为将TheadLocal作为key,set方法的参数做为value保存在当前线程的threadLocals中.调用get方法时类似,调用get方法时,会去Thread的threadLocals中去寻找key为ThreadLocal 变量的值

源码如下:


//Thread.threadLocals变量声明
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. 
 */
ThreadLocal.ThreadLocalMap threadLocals = null;

// ThreadLocal set get方法

/**
 * 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 map = getMap(t);// getMap方法即去获取当前线程的ThreadLocalMap变量。
    if (map != null)
        map.set(this, value);//以this(ThreadLocal本身)为Key,参数value为值进行保存
    else
        createMap(t, value);
}

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


/**
 * 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();
}

下面是测试代码:

static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

@Test
public  void test01(){
    Thread thread1 = new Thread(){
        @Override
        public void run() {
            stringThreadLocal.set("threadName===>"+Thread.currentThread().getName());
            System.out.println(this.getName()+" thread get the value:"+stringThreadLocal.get());

        }
    };
    Thread thread2 = new Thread(){
        @Override
        public void run() {
            stringThreadLocal.set("threadName===>"+Thread.currentThread().getName());
            System.out.println(this.getName()+" thread get the value:"+stringThreadLocal.get());

        }
    };
    Thread thread3 = new Thread(){
        @Override
        public void run() {
            stringThreadLocal.set("threadName===>"+Thread.currentThread().getName());
            System.out.println(this.getName()+" thread get the value:"+stringThreadLocal.get());
        }
    };

    thread1.start();
    thread2.start();
    thread3.start();
    System.out.println("main线程调用set方法之前:"+stringThreadLocal.get());
    stringThreadLocal.set("main 线程set的值");
    System.out.println("main线程调用set方法之后:"+stringThreadLocal.get());
}

可以看到不同线程设置的值在该线程是能够正确的取到。由于Thread的threadLocals变量只能在Thread所在的包下才能够访问,因此不能对该变量进行直接访问以验证设置的值在Thread.currentThread对象里面。但如果你调试以上代码,设置值之后访问Thread.currentThread.threadLocals会看到之前设置的值。其中key为声明的ThreadLocal对象。

ThreadLocal进行参数传递

这算是比较正统的ThreadLocal用法,这可能也是ThreadLocal设计的初衷:用来保存于状态相关的变量,比如访问者的用户信息,事务的事务标识。这里演示一下使用ThreadLocal来传递用户信息,实际上当前流行的大部分权限框架都是使用的ThreadLocal变量来保存用户信息的。

下面是测试代码:

//参数传递测试

    @Test
    public void test02(){
        //参数主要利用ThreadLocal是线程的局部变量,只要在同一个线程中,之前设置的值后面就能取到,从而达到参数值传递的效果。
        //在前面在线程变量中添加值
        stringThreadLocal.set("name");
        paramThreadLocal.set(new HashMap(){
            {
                put("id","1");
                put("name","xiaoTOT");
                put("gender","M");
            }
        });
        testParam();
    }

    private void testParam() {
        //从线程本地变量获取参数
        Map map = paramThreadLocal.get();
        map.forEach((key,value)->{
            System.out.println("key:"+key+" & value="+value);
        });
    }
    

ThreadLocal改造改造在单例模式中的运用

单例模式的好处无用质疑,可以减少对象的创建。对于那些创建非常费时的对象尤其明显。并且如果能够用单例解决的问题经历使用单例解决,这样能减轻运行时的压力。

  1. 对于一个对象倘若没有成员变量,单例非常简单,不用去担心多线程同时对成员变量修改而产生的线程安全问题。

  2. 对于一个拥有成员变量的对象使用单例就需要考虑到线程安全问题。多线程访问又可以分为下面两个方面:

a:成员变量需要多线程同步,比如账户对象(ACCOUNT)中的成员变量余额(amount).amount成员变量需要在多线程的访问下保证各个线程保证绝对的同步,即无论什么时候线程内的值都是一样。我们可以通过加同步关键字synchronized,volatile来解决。

b,成员变量不需要线程同步,每个线程访问自己线程内部的对象。比如一个服务类对数据库的链接成员变量,每个线程分配一个连接即可。类似这种场景,我们最简单的方式是使用多例模式来解决。单更好的方式是通过threadLocal来解决。

下面是使用ThreadLocal改造单例模式的示例:

//ThreadLocal在单例模式改造的运用
@Test
public  void test03(){
    //单例模式的好处无用质疑,可以减少对象的创建。对于那些创建非常费时的对象尤其明显。并且如果能够用单例解决的问题经历使用单例解决,这样能减轻运行时的压力。
    //1,对于一个对象倘若没有成员变量,单例非常简单,不用去担心多线程同时对成员变量修改而产生的线程安全问题。
    //2,对于一个拥有成员变量的对象使用单例就需要考虑到线程安全问题。多线程访问又可以分为下面两个方面:
    // a:成员变量需要多线程同步,比如账户对象(ACCOUNT)中的成员变量余额(amount).amount成员变量需要在多线程的访问下保证各个线程保证绝对的同步,即无论什么时候线程内的值都是一样。
    // 我们可以通过加同步关键字synchronized,volatile来解决。
    // b,成员变量不需要线程同步,每个线程访问自己线程内部的对象。比如一个服务类对数据库的链接成员变量,每个线程分配一个连接即可。类似这种场景,我们最简单的方式是使用多例模式来解决。
    //单更好的方式是通过threadLocal来解决。

    //多例模式
    Thread thread = new Thread(){
        @Override
        public void run() {

            DBConnect dbConnect = new DBConnect();
            DemoService demoService = new DemoService();
            demoService.setConnect(dbConnect);
            demoService.doSomeThing();

        }
    };

    Thread thread2 = new Thread(){
        @Override
        public void run() {

            DBConnect dbConnect = new DBConnect();
            DemoService demoService = new DemoService();
            demoService.setConnect(dbConnect);
            demoService.doSomeThing();

        }
    };

    thread.start();
    thread2.start();

    // 单例模式改造
    // 由DemoService构造器可以看出,构造这个对象是非常耗时的。并且还不能使用单例模式,因为DBConnect是不能多线程访问的。遇到这种情况那就使用ThreadLocal来改造吧。
    //如果能修改DemoService源码,修改源码即可。若不能修该源码(比如DemoService是一个三方包)单DemoService不是final的,即可以通过继承修改。

    DemoService demoService1 = new ThreadLocalDemoService();

    Thread threadA = new Thread(){
        @Override
        public void run() {

            demoService1.setConnect(new DBConnect());
            demoService1.doSomeThing();

        }
    };

    Thread threadB = new Thread(){
        @Override
        public void run() {

            demoService1.setConnect(new DBConnect());
            demoService1.doSomeThing();

        }
    };

    threadA.start();
    threadB.start();


}

static class DemoService{

    //这个对象不能线程同时访问,应该是一个线程就建立一个连接到数据库。不同的线程不能使用同一个连接。
    DBConnect connect;

    public DemoService(){
        try {
            Thread.sleep(5l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
    public void setConnect(DBConnect connect){
        this.connect = connect;
    }


    public void doSomeThing(){
        connect.updateSomeData();
    }
}

//使用ThreadLocal改造成员变量,使其可以使其可以使用单例模式
static class ThreadLocalDemoService extends DemoService {

    ThreadLocal<DBConnect> connectThreadLocal = new ThreadLocal<>();

    public ThreadLocalDemoService() {
        super();
    }



    public void doSomeThing(){
        connectThreadLocal.get().updateSomeData();
    }

    public void setConnect(DBConnect dbConnect){
        connectThreadLocal.set(dbConnect);
    }

}



class DBConnect {
    private String transactionName = Thread.currentThread().getName()+"的事务";

    public void updateSomeData(){
        System.out.println(transactionName + " update some data");
    }

}

其中DemoService中有个成语变量DBConnect connect,由于资源不能同时被连个线程使用,比如socket链接发送数据,或者数据库事务,一个线程不能影响另外一个线程的事务。 这个时候我们没有办法只有对DemoService采用多例模式,单由因为DemoService创建会耗费大量时间。类似的例子很多,比如一个对象中,可能只有少数成员变量不能够多线程访问,大多数是能多线程访问的,这时为了一个成员变量去将单例改成多例也是非常糟糕的。这时若我们使用ThreadLocal就能够完美解决。

备注

值得注意的是,ThreadLocal随着当前线程的销毁而销毁,如果程序中采用线程池,在上一次任务运行完之后,记得清掉之前ThreadLocal数据。

引用

实际上学习和使用ThreadLocal之前,也百多过很多ThreadLocal相关的文章。最开始是拜读了学习Spring必学的Java基础知识(6)----ThreadLocal@ITEYE这篇文章,才了解到ThreadLocal这个东西。最好为了详细了解,看到了第二篇文章,并且之前看过的关于ThreadLocal的文章与这篇文章内容基本上都一样,都在讲关于Threadloal为解决线程安全问题提供了新思路,当时被看得一头雾水。最好看到第三篇的帖子。然后结合源代码才对ThreadLocal的本质很好的了解。如果你看完本篇文章还不是非常明白,可以详细参阅第三篇引用,这个帖子的讨论还是非常精彩的,能给你很多启迪作用。需要说明的是第二篇CSDN相关讲解实际上是有问题的。你可以看看文章下方的评论。

  1. 学习Spring必学的Java基础知识(6)----ThreadLocal@ITEYE

  2. 彻底理解ThreadLocal@CSDN

  3. 正确理解ThreadLocal@ITEYE

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
美凌格栋栋酱 美凌格栋栋酱
9个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Easter79 Easter79
4年前
sql注入
反引号是个比较特别的字符,下面记录下怎么利用0x00SQL注入反引号可利用在分隔符及注释作用,不过使用范围只于表名、数据库名、字段名、起别名这些场景,下面具体说下1)表名payload:select\from\users\whereuser\_id1limit0,1;!(https://o
Wesley13 Wesley13
4年前
Activiti 工作流入门指南
<divclass"htmledit\_views"id"content\_views"<h1<aname"t0"</a概览</h1<p如我们的介绍部分所述,Activiti目前分为两大类:</p<ul<li<p<ahref"https://activiti.gitbook.io/activiti7deve
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
4年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
4年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
4年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0