ThreadLocal的使用

原文:http://java.dzone.com/articles/java-thread-local-%E2%80%93-how-use  [翻墙看]

Thread Local是一个有趣且实用的概念,却是很多开发人员不知如何使用的。在本篇文章中,我将给大家解释什么是ThreadLocal和如何使用,并附带示例代码。
由于这个概念在开始有些难以理解,所以我将解释地尽可能简单(注:你不使用这段示例代码,因为它是在生产环境上的。掌握它的概念并加以改善,,,笔者已将其改为可单独运行)

什么是ThreadLocal

ThreadLocal可以理解为变量访问的范围,例如request、session范围,而这里是线程范围的。你可以在ThreadLocal中设置任何对象,该对象对特定的线程的访问将是
“全局”并且是“局部”的。“全局”又是“局部”?让我解释它:
◆ 保存在ThreadLocal中的值对线程来说是全局的,意味着在这个线程的任何地方都可以访问到。如果这个线程调用了经过几个类的方法,那么所以这些类的方法都能看到其
他方法(因为他们在相同的线程中执行)在这个ThreadLocal中设置的变量。这个值不需要被显示的传递。看起来你使用的是全局的变量。
◆ 保存在ThreadLocal中的值对线程来说又是局部的。意味着每个线程拥有它自己的ThreadLocal变量。一个线程不能访问/修改其他线程的ThreadLocal变量
好了,这就是ThreadLocal的概念,希望你已经理解它(如果没有,请留言)

使用时候使用ThreadLocal?

上段中已经看到了什么是ThreadLocal。现在让我们讨论下你什么时候用ThreadLocal这样情况。
我说明一种使用ThreadLocal的情况。设置你使用了Servlet调用一些业务方法。你有个需求即:为记录日志每一个请求该servlet的过程生成一个唯一的业务标识并把它传递到业务方法中。
一个解决方法是把这个业务标识作为一个参数传递给每一个方法。但这不是不念旧恶好的解决方案因为代码是冗余且没有必要的。
要解决它,就要用到ThreadLocal,你生成一个业务标识并把它设置到ThreadLocal中,在这之后,任何一个业务方法,这个servlet调能从这个变量中访问业务标识。
这个Servlet可能服务于不止一个请求。因为每个请求都是一个线程,因此业务标识对每个线程是唯一的(局部),且对线程的整个执行过程是可见的(全局)。

如何使用ThreadLocal

Java提供了ThreadLocal类,你可以设置获取线程范围的变量。下面是一个示例代码演示了上面解释的东西。
下面是Context.java文件,该类包含了业务标识字段。

   

 /*
     * 包含业务唯一标识的类
     * */
    public class Context {
        private String transactionId;
     
        public String getTransactionId() {
            return transactionId;
        }
     
        public void setTransactionId(String transactionId) {
            this.transactionId = transactionId;
        }
     
    }

下面创建MyThreadLocal.java类,它做为引用Context对象的容器

 

   /**
     * 其中引用了Context类
     */
    public class MyThreadLocal {
        private static final ThreadLocal<Context> userThreadLocal = new ThreadLocal<Context>();
        public static void set(Context user){
            userThreadLocal.set(user);
        }
        public static void unset(){
            userThreadLocal.remove();
        }
        public static Context get(){
            return userThreadLocal.get();
        }
        
    }

上面的代码,创建了一个静态的ThreadLocal字段,可以使用它的set/get方法设计ThreadLocal变量。
下面创建主类:ThreadLocalDemo.java。生成并将业务标识设置到ThreadLocal中然后在业务方法中调用。

   

public class ThreadLocalDemo implements Runnable{
        private static AtomicInteger ai = new AtomicInteger(0);
        public void run() {
            Context context = new Context();
            context.setTransactionId(getName());
            MyThreadLocal.set(context);
            System.out.println("request["+Thread.currentThread().getName()+"]:"+context.getTransactionId());
            new BusinessService().businessMethod();
            MyThreadLocal.unset();
        }
        
        private String getName() {
            return ai.getAndIncrement()+"";
        }
     
        public static void main(String[] args) {
            ThreadLocalDemo tld  = new ThreadLocalDemo();
            new Thread(tld).start();
            new Thread(tld).start();
        }
     
    }
    public class BusinessService {
     
        public void businessMethod() {
            Context context = MyThreadLocal.get();
            System.out.println("service["+Thread.currentThread().getName()+"]:"+context.getTransactionId());
        }
     
    }


运行结果:
request[Thread-0]:0
request[Thread-1]:1
service[Thread-1]:1
service[Thread-0]:0
从上面看来,我们即使没有明确地传递业务标识,它的值仍然可以从业务方法中访问到并打印出来。另外在两个线程中的
业务标识字段互不相同(分别是0和1)

补充
ThreadLocal提及的一个重要的好处是,就是作为同步关键字synchronized的替代方案,在强调事务的环境(in transaction-intensive )
中提供扩展性。封装在ThreadLocal的以优美而简洁的方式自动变得线程安全。因为它能保存在不同的线程中非共享的。

“在不同线程中不会共享ThreadLocal变量”并不完全正确,如果ThreadLocal存在继承关系的话,在线程间可以共享变量(未验证)

------------------------------------------------------------------------------------------------------------------------

另一个文档表述更形象一些:

张大胖上午遇到了一个棘手的问题,他在一个AccountService中写了一段类似这样的代码:

Context ctx = new Context();
ctx.setTrackerID(.....)

然后这个AccountService 调用了其他Java类,不知道经过了多少层调用以后,最终来到了一个叫做AccountUtil的地方,在这个类中需要使用Context中的trackerID来做点儿事情:

很明显,这个AccountUtil没有办法拿到Context对象, 怎么办?张大胖想到,要不把Context对象一层层地传递下去,这样AccountUtil不就可以得到了吗?

可是这么做改动量太大!涉及到的每一层函数调用都得改动,有很多类都不属于自己的小组管理,还得和别人协调。 更要命的是有些类根本就没有源码,想改都改不了。这也难不住我,张大胖想:可以把那个set/get TrackerID的方法改成静态(static)的,这样不管跨多少层调用都没有问题!

public class Context{
    public static String getTrackerID(){
        ......
    }
    public static void setTrackerID(String id){
        ......
    }
}

这样就不用一层层地传递了,Perfect!

张大胖得意洋洋地把代码提交给Bill做Review。 

Bill看了一眼就指出了致命的问题: 多线程并发的时候出错!

张大胖恨不得找个地缝钻进去:又栽在多线程上面了,这次犯的还是低级错误!

线程1调用了Context.setTrackerID(), 线程2 也调用了Context.setTrackerID(),数据互相覆盖,不出乱子才怪。

张大胖感慨地说:“像我这样中情况,需要在某处设置一个值,然后经过重重方法调用,到了另外一处把这个值取出来,又要线程安全,实在是不好办啊, 对了,我能不能把这个值就放到线程中? 让线程携带着这个值到处跑,这样我无论在任何地方都可以轻松获得了!”

Bill说:“有啊,每个线程都有一个私家领地! 在Thread这个类中有个专门的数据结构,你可以放入你的TrackerID,然后到任何地方都可以把这个TrackerID给取出来。”

“这么好? ” 

张大胖打开JDK中的Thread类,仔细查看,果然在其中有个叫做threadLocals的变量,还是个Map类型 , 但是在Thread类中却没有对这个变量操作的方法。 

看到张大胖的疑惑,Bill说:“也许你注意到了,这个变量不是通过Thread的访问的,对他的访问委托给了ThreadLocal这个类。

“那我怎么使用它?”

“非常简单, 你可以轻松创建一个ThreadLocal类的实例:

ThreadLocal<String> threadLocalA= new ThreadLocal<String>();

线程1: threadLocalA.set("1234");
线程2: threadLocalA.set("5678");

像‘1234’, ‘5678’这些值都会放到自己所属的线程对象中。”

 

“等你使用的时候,可以这么办:”

线程1: threadLocalA.get()  --> "1234"
线程2: threadLocalA.get() --> "5678"

“明白了,相当于把各自的数据放入到了各自Thread这个对象中去了,每个线程的值自然就区分开了。 可是我不明白的是为什么那个数据结构是个map 呢?”

“你想想,假设你创建了另外一个threadLocalB:”

ThreadLocal<Integer> threadLocalB = new ThreadLocal<Integer>();

线程1: threadLocalB.set(30);
线程2: threadLocalB.set(40);

 

那线程对象的Map就起到作用了:

“明白了,这个私家领地还真是好用,我现在就把我那个Context给改了,让它使用ThreadLocal:”

public class Context {
    private static final ThreadLocal<String> mThreadLocal 
        = new ThreadLocal<String>();

    public static void setTrackerID(String id) {
        mThreadLocal.set(id); 
    }   
    public static String getTrackerID() {
        return mThreadLocal.get();
    }   

}

小结:

ThreadLocal这个名字起得有点让人误解, 很容易让人认为是“本地线程”, 其实是用来维护本线程的变量。 对照着上面的原理讲解,我想大家可以自行去看ThreadLocal的源码,轻松理解。

ThreadLocal 并不仅仅是Java中的概念,其他语言例如Python,C#中也有,作用类似。

ThreadLocal在日常工作中用得不多,但是在框架(如Spring)中是个基础性的技术,在事务管理,AOP等领域都能找到。

转自:https://blog.csdn.net/mylovepan/article/details/19963895

https://mp.weixin.qq.com/s/aM03vvSpDpvwOdaJ8u3Zgw

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值