Java并发编程之ThreadLocal原理

ThreadLocal是什么

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

Thread-local,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
下面来看一个简单的示例:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ParseDate implements Runnable{

   int i = 0;

   public ParseDate(int i) {
       this.i = i;
   }

   private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

   @Override
   public void run() {
       try {
           Date date = sdf.parse("2018-05-20 12:00:"+i%60);
           System.out.println(i+":1"+date);
       } catch (ParseException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       //用线程池创建线程,
       ExecutorService es = Executors.newFixedThreadPool(10);
       for (int i = 0; i < 1000; i++) {
           es.execute(new ParseDate(i));
       }
   }
}

运行代码后会出现这种错误:


image.png

造成这样错误的原因是在多线程中使用simpleDateFormat.parse()方法并不是线程安全的,因此,正在线程池中共享这个对象必然会导致报错。

一种可行的解决方案是在simpleDateFormat.parse()前后加锁,这个也是我们一般的处理思路。但这里我们不这么做 ,我们使用ThreadLocal为每一个线程都产生simpleDateFormat对象实例:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ParseDate2 implements Runnable{

    int i = 0;

    public ParseDate2(int i) {
        this.i = i;
    }

    private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();

    @Override
    public void run() {
        try {
            if (threadLocal.get() == null) {
                threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            }
            Date date = threadLocal.get().parse("2018-05-20 12:00:"+i%60);
            System.out.println(i+":1"+date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //用线程池创建线程,
        ExecutorService es = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            es.execute(new ParseDate2(i));
        }
    }
}

注意这一段 if (threadLocal.get() == null),如果当前线程不持有SimpleDateFormat对象实例。那么就新建一个并把它设置到当前线程中,如果已经持有,则 直接使用。

从这里可以看到,为每一个线程都分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的,ThreadLoacl只是起到简单容器的作用。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。

ThreadLoacl的实现原理

ThreadLocal是如何保证这些对象只能被当前线程所访问呢?那我们下面来看一下具体ThreadLocal是如何实现的。

我们需要关注的,自然是ThreadLocal的set()方法和get()方法。先看一下set()方法:


image.png

在set时,首先通过Thread.currentThread()获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设置ThreadLocalMap中。ThreadLocalMap是Thread的内部成员。


image.png

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

在进行变更get()操作时,自然是将这个map中的数据拿出来:


image.png

首先,get()方法也是先获取当前线程的ThreadLocalMap对象。然后通过将自己作为key获取vaule。

ThreadLocal的问题

在了解ThreadLocal的内部后,我们自然会引出一个问题,那就是这些变量是维护在Thread类的内部,这也意味着只要线程不退出,对象的引用就会一直存在。

当线程退出时,Thread类会进行一些清理工作,其中包括清理ThreadLocalMap。我们看一下Thread类的exit()方法。


image.png

exit()方法在线程退出前,有系统回调,进行资源清理。

因此如果我们使用线程池,那就意味着当前线程未必退出(比如固定大小的线程池,线程总是存在)。如果这样,将一些比较大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocals这个Map中,可能会使系统出现内存泄漏(你设置了对象到ThreadLocal中,但是不清理它,在你是用几次后,这个对象不再有用了,但是它却无法被回收)。

此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性的关闭数据库连接一样。如果你确实不需要这个对象了,那么就应该告诉虚拟机,请把它回收掉,防止内存泄漏。


image.png

另外一种有趣的情况是JDK有可能允许你像释放普通变量一样释放ThreadLocal。比如,我们有时候为了加速垃圾回收,会特意写object =null之类的代码,如果这么做,obj所指向的对象就更容易被垃圾回收器发现,从而加速垃圾回收。

同理,对于ThreadLocal的变量,我们也可以手动将其设置为null,比如threadLocal =null。那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342