详细领悟ThreadLocal变量

关于对ThreadLocal变量的理解,我今天查看一下午的博客,本身也写了demo来测试来看本身的理解究竟是不是那么回事。从看到博客引出不解,到仔细查看ThreadLocal源码(JDK1.8),我以为我颇有必要记录下来我这大半天的收获,
今天我研究的最多的就是这两篇文章说理解。我在这里暂称为A文章和B文章。如下是两篇博文地址,我是在看完A文章后,颇有疑问,特别是在A文章后的各位网页的评论中,更加坚决我要弄清楚ThreadLocal究竟是怎么一回事。
A文章:http://blog.csdn.net/lufeng20/article/details/24314381
B文章:http://www.cnblogs.com/dolphin0520/p/3920407.htmlhtml

首先,咱们从字面上的意思来理解ThreadLocal,Thread:线程,这个毫无疑问。那Local呢?本地的,局部的。也就是说,ThreadLocal是线程本地的变量,只要是本线程内均可以使用,线程结束了,那么相应的线程本地变量也就跟随着线程消失了。程序员

如下内容是我的参考他人文章,理解总结出来,误差之处,欢迎指正。安全

全篇包括两个部分,我但愿你们对ThreadLocal源码已经有必定了解,我在文章中没有具体分析源码:多线程

第一部分是说明ThreadLocal不是用来作变量共享的。ide

第二部分是深刻了解ThreadLocal后获得的结论,谈谈什么状况用ThreadLocal,以及用ThreadLocal有什么好处。工具

1、ThreadLocal不是用来解决多线程下访问共享变量问题的

我想你们都知道,多线程状况下,对共享变量的访问是须要同步的,否则会引发不可预知的问题。测试

接下来我就是,我极力想要说明的:ThreadLocal不是用来解决这个问题的!!!!! ThreadLocal能够在本线程持有一个共享变量的副本,对吧。你们都这么说。this

我举个栗子,如果在线程的ThreadLocal中set一个程序中惟一的共享变量,该ThreadLocal仅仅是保存了一个共享变量的引用值,共享变量的实例对象在内存中只有一个。spa

下面咱们先测试一下,是否是这样:
我先定义一个Person类,咱们假定这个Person是要被共享的吧···哈哈(TheradLocal实际上不是这样用的.net

class Person {
    private String name;
    Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    } 
}

而后建立一个target实现Runnable接口:

/**
 * Person 是共享变量
 * @author bubble
 *
 */
class Target implements Runnable {
    private static Person person = new Person("张三");
    public Target() {}
    
    @Override
    public void run() {
    //线程中建立一个ThreadLocal变量,并将共享变量建立一个本线程副本
       ThreadLocal<Person> df = new ThreadLocal<Person>();
       df.set(person);
    //对本线程副本中的值进行改变
       df.get().setName("李四");
       System.out.println("线程" + Thread.currentThread().getName() + "更改ThreadLocal中Person的名字为:" + df.get().getName());       
    }
    
    public Person getPerson() {
        return person;
    }  
}

最后咱们来测试一下,到底线程中,对共享变量的本地副本是怎么一回事:

public class ThreadLocalTest {
    public static void main(String[] args) throws InterruptedException {
        Target target = new Target();
        Thread thread = new Thread(target);
        thread.start();    //建立一个线程,改变线程中共享变量的值   
        t1.join();  //等待线程执行完毕
        //主线程访问共享变量,发现Person的值被改变
         System.out.println("线程" + Thread.currentThread().getName() + "中共享变量Person的名字:" + target.getPerson().getName());
    }      
}

咱们来看看运行结果:

咱们能够看到,Thread-0线程虽然建立了一个ThreadLocal,而且将共享变量放入,可是线程内改变了共享变量的值,依然会对共享变量自己进行改变。

参考源码,咱们能够看到ThreadLocal调用set(T value)方法时,是将调用者ThreadLocal做为ThreadLocalMap的key值,value做为ThreadLocalMap的value值。

咱们看看ThradLocal类里面到底有什么:

红色箭头标注出了四个咱们经常使用的方法,而且ThreadLocal里定义了一个内部类ThreadLocalMap,可是注意一下,虽然它定义了这样一个内部类,但ThreadLocal自己真的没有持有ThreadLocalMap的变量,

这个ThreadLocalMap的持有者是Thread。

因此,文章A中,在开头说了这样一段:

ThreadLocal是如何作到为每个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

正确应该是:在Thread类里面有一个ThreadLocalMap,用于存储每个线程的变量的引用,这个Map中的键为ThreadLocal对象,而值对应的是ThreadLocal经过set放进去的变量引用。

我在这里一直强调的是,ThreadLocal经过set(共享变量)而后再经过ThreadLocal方法get的是共享变量的引用!!!  若是多个线程都在其执行过程当中将共享变量加入到本身的ThreadLocal中,那就是每一个线程都持有一份共享变量的引用副本,注意是引用副本,共享变量的实例只有一个。因此,ThreadLocal不是用来解决线程间共享变量的访问的事儿的。想要控制共享变量在多个线程之间按照程序员想要的方式来进行,那是锁和线程间通讯的事,和ThreadLocal没有半毛钱的关系。

总的来讲:每一个线程中都有一个本身的ThreadLocalMap类对象,能够将线程本身的对象保持到其中,各管各的,线程执行期间均可以正确的访问到本身的对象。

2、ThreadLocal到底该怎么用

说了这么多,我以为仍是举栗子来讲明一下,ThreadLocal到底该怎么用,有什么好处。

你们都知道,SimpleDateFomat是线程不安全的,由于里面用了Calendar 这个成员变量来实现SimpleDataFormat,而且在Parse 和Format的时候对Calendar 进行了修改,calendar.clear(),calendar.setTime(date)。总之在多线程状况下,如果用同一个SimpleDateFormat是要出问题的。那么问题来了,为了线程安全,是否是在每一个线程使用SimpleDateFormat的时候都手动new出来一个新的用?  这得多麻烦啊,通常来讲,在开发时,SimpleDateFormat这样的类咱们是放在工具类里面的,阿里巴巴Java开发手册里面这样推荐DateUtils:

public class DateUtils {
    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

这里重写了initialValue方法,新建了一个SimpleDateFormat对象并返回,这样咱们就能够在任何线程任何地方想要执行日期格式化的时候,就能够像以下方式来执行,而且线程之间互相没有影响:

 DateUtils.df.get().format(new Date());

咱们来看看为何这么作线程之间就没有影响了。假设如今有线程A和线程B同时执行以上语句,那么两个线程是怎么操做的呢?

线程A:df.get的时候,首先尝试得到线程A本身ThreadLocalMap,若是是第一次get,因为咱们没有set,而是重写了initialValue方法,因此在A线程第一次get时没有ThreadLocalMap,这时线程A会

new一个线程A本身的ThreadLocalMap出来,将df(注意df是ThreadLocal变量)做为这个map的键,将initialValue中返回的值(注意是new出来的)做为map的值。这个时候A线程里面就有一个ThreadLocalMap了,而且里面保存了一个SimpleDateFormat的引用。那么从如今开始,线程A的生存期间,再次调用df.get(),都将得到一个A线程的ThreadLocalMap,而且经过df做为键获得相应的SimpleDateFormat;

线程B:df.get的时候,首先尝试得到线程B本身ThreadLocalMap,若是是第一次get,因为咱们没有set,而是重写了initialValue方法,因此在B线程第一次get时没有ThreadLocalMap,这时线程B会

new一个线程B本身的ThreadLocalMap出来,将df(注意df是ThreadLocal变量,这里的df和线程A中的df是同一个,可是又有什么关系呢,map不同)做为这个map的键,将initialValue中返回的值(注意是new出来的,这里是线程B在执行df.get时本身new出来的,再也不是线程A中的那个了)做为map的值。这个时候A线程里面就有一个ThreadLocalMap了,而且里面保存了一个SimpleDateFormat的引用。那么从如今开始,线程B的生存期间,再次调用df.get(),都将得到一个B线程的ThreadLocalMap,而且经过df做为键获得相应的SimpleDateFormat(这里和线程A中已是另一个不一样的对象了);

 

这下大概明白为何说这样用就线程安全了吧,这里的线程安全并非指访问的同一个对象,而是每一个线程建立本身的对象(SimpleDateFormat)来用,各自用各自的,固然线程安全了。。。

固然你们能够说,这和本身在线程里面每次用的时候new出来一个有什么区别呢,对,没区别,可是这样方便啊,并且能够保持线程里面只有惟一一个SimpleDateFormat对象,你要每用一次new一次,那就消耗内存了撒。可能你会说,那我只new一个,那个方法用的时候经过参数传递过去就行。。。。。  不嫌麻烦的话我也无话可说。哈哈。。  然而ThreadLocal却太方便了。。。   敬仰神人居然能创造出ThreadLocal。这才是ThreadLocal

 

总结一下:

ThreadLocal真的不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。 
一、每一个线程中都有一个本身的ThreadLocalMap类对象,能够将线程本身的对象保持到其中,各管各的,线程能够正确的访问到本身的对象。 
二、将一个共用的ThreadLocal静态实例做为key(上面得df),将不一样对象的引用保存到不一样线程的ThreadLocalMap中,而后在线程生命周期内执行的各处经过这个静态ThreadLocal实例的get()方法取得本身线程保存的那个对象,避免了将这个对象(指的是SimpleDateFormat)做为参数传递的麻烦。

 

补充一下:

通常状况下,经过ThreadLocal.set() 到线程中的对象是该线程本身使用的对象,其余线程是不须要访问的,也访问不到的。各个线程中访问的是不一样的对象。

 若不用DateUtils工具类,彻底能够在线程开始的时候这样执行:

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        ThreadLocal<SimpleDateFormat> df = new ThreadLocal<>();
        df.set(sdf);

而后在线程生命周期的任何地方调用:

 df.get().format(new Date());

效果是同样的,但是这没有工具类方便嘛。。。

 

本文我的理解后整理,文章中存在不少表述不清楚的地方,欢迎留言讨论。

 参考文章:

 A文章:http://blog.csdn.net/lufeng20/article/details/24314381
 B文章:http://www.cnblogs.com/dolphin0520/p/3920407.html

 C文章:http://www.iteye.com/topic/103804

相关文章
相关标签/搜索