JDK中有一个ThreadLocal类,使用很方便,但是却很容易出现问题。究其原因, 就是对ThreadLocal理解不到位。最近项目中,出现了内存泄漏的问题。其中就有同事在使用ThreadLocal时,没有用好。所以特写下此文。

ThreadLocal设计

ThreadLocal的类图:

·每个Thread都有一个自己的ThreadLocalMap,默认是null。当线程运行过程中,首次使用某个ThreadLocal对象时,会初始化这个ThreadLocalMap。

·ThreadLocalMap中包括一个Entry数组。我们在程序中是不能直接使用ThreadLocalMap的。在每次使用ThreadLocal时(set,get,remove),都会自动的使用ThreadLocalMap,进行一定的清理工作。参见方法expungeStaleEntry。

·Entry继承了WeakReference,Key是ThreadLocal对象,Value时目标对象。并且key是被Weak Reference的。所以如果一个ThreadLocal对象TL1被GC后,也就是key是null。

·每个ThreadLocal对象都有一个initValue。在继承ThreadLocal类时,可以对它的initialValue()进行重写。ThreadLocal对外提供了3个常用方法:get,set,remove。

此外,当线程被kill(通过interrupe,或者线程自然结束)时,ThreadLocalMap会被设置为null,那么Map中的各个变量也会被回收(如果不被其它变量引用的话)。

ThreadLocalMap、ThreadLocal说明

ThreadLocalMap

ThreadLocalMap中的expungeStaleEntry(int)方法的可能被调用的处理有:

通过这个图,不难看出这个方法在ThreadLocal的set,get,remove时都会被调用。

从该方法代码中,可以看出是先清理指定的Entry,再遍历,如果发现有Entry的key是null,清理之。Key =null,也就是ThreadLocal对象是null。所以当程序中,将ThreadLocal对象设置为null,在该线程继续执行时,如果执行另一个ThreadLocal时,就会触发该方法。就有可能清理掉key是null的那个ThreadLocal对应的值。

ThreadLocal方法说明

set() 将目标对象作为当前线程的局部变量的值设置。也就是说,变量名是ThreadLocal对象,变量值是目标对象。

get(),从当前对象中取出ThreadLocal变量的值。如果map中不并在该ThreadLocal变量,就会自动调用initialValue方法。并将该值设置到map中。

remove(),从当前线程中移除ThreadLocal变量的值。此时key,value都不存在map中。

使用ThreadLocal后的内存模型

假设程序中指定2个ThreadLocal的变量TL1、TL2,并且他们都是static时。

private static ThreadLocal<ClassB> TL1 =  new ThreadLocal<ClassB>();
private static ThreadLocal<ClassA> TL2 =  new ThreadLocal<ClassA>();

在运行时有2个线程,每个线程中都设置了这两个线程局部变量TL1,TL2。那么内存模型将是这样的:

在线程Thread1执行过程中,如果执行顺序是这样的:

TL1.set(ObjB1);  //将TL1作为变量名,OBjB1作为变量值设置到Thread1中。
TL1.get();        //从当前线程Thread1中取出变量TL1的值。它的值应该是OBjB1
TL2.set(ObjA1);  //将TL2作为变量名,OBjA1作为变量值设置到Thread1中。
TL2.get();        //从当前线程Thread1中取出变量TL1的值。它的值应该是OBjA1

当执行remove时,应会将该值从ThreadLocalMap中移出。

如何正确使用ThreadLocal呢?

在说如何正确使用之前,先来看一个例子:

package com.fjn.jdk.thread_concurrent.threadlocal;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.junit.Test;

public class ThreadLocalTest {

    @Test
    public void testUsingThreadPool() {
        ExecutorService executor = Executors.newCachedThreadPool();
        int workerThreadNumber =10;
        CountDownLatch countDowner = new CountDownLatch(workerThreadNumber);
        for (int i = 0; i < workerThreadNumber; i++) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                // clear interrupted flag
                Thread.interrupted();
            }
            executor.submit(new Task(countDowner));
        }

        try {
            countDowner.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdownNow();
    }
}

class NamedThreadLocal<T> extends ThreadLocal<T> {
    private String name;

    public NamedThreadLocal(String name) {
        this.name = name;
    }

    public String toString() {
        return "The thread local [" + name + "] is " + this.get();
    }
}

class BusService {
    private CooperationService cooperateService = CooperationService.getService();

    public void doService(String str) {

        try {
            cooperateService.recordStartTime();
            Thread.sleep(500);
            doSomething(str);
            Thread.sleep(500);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        } finally {
            cooperateService.clear();
// 此时ThreadLocalMap中已不存在该变量。
        }

        // 正常情况下是不能要这一行代码的:
        // 因为这样做又会重新在ThreadLocalMap中添加一个ThreadLocal变量,并设置值为initailValue
        System.out.println(cooperateService.getStartTime());
    }

    private void doSomething(String str) {
        System.out.println(" do something beging ...");
        System.out.println("start time is : " + cooperateService.getStartTime());
        System.out.println(str);
        System.out.println(" do something end ...");
    }
}

class CooperationService {
    private static CooperationService ins = new CooperationService();
    private static ThreadLocal<Long> threadLocal = new NamedThreadLocal<Long>("Test") {
        protected Long initialValue() {
            return -1l;
        };
    };

    private CooperationService(){

    }

    public static CooperationService getService(){
        return ins;
    } 

    public void recordStartTime() {
        long now = System.currentTimeMillis();
        System.out.println("record start time : " + now);
        threadLocal.set(now);
    }

    public Long getStartTime() {
        return threadLocal.get();
    }

    public void clear() {
        threadLocal.remove();
    }
}

class Task implements Runnable {
    private final CountDownLatch countDownLatch;

    public Task(CountDownLatch countDowner) {
        this.countDownLatch = countDowner;
    }

    @Override
    public void run() {
        BusService service = new BusService();
        service.doService("hello");
        if (countDownLatch != null) {
            countDownLatch.countDown();
        }
    }

}

上面的例子中,使用线程池执行一个Task。Task的内容是调用相关的业务服务,在业务处理过程中,会先记录下业务处理的开始时间,处理过程中可能会使用到这个开始时间,譬如说要将开始处理时间记录到DataBase中。在用完之后,线程要完成任务之前,调用remove将该变量从线程中移出。

在调用ThreadLocal方法get,set,remove时,正确的姿势应该是:

TL1.set(obj);

Try{
     // . . .
     TL1.get();
     // . . .
}
Finally{
     TL1.remove();
}

那么ThreaLocal变量该如何声明呢?

要分为下面几个使用场景了:

1)如果ThreadLocal可以完全在一个方法中使用,不会到另一个方法中使用该ThreadLocal对象。对于这种情况:ThreadLocal完全可以作为该方法的局部变量。例如:

Class Hello {

     public void hello  () {
          // . . .
          ThreadLocal<Long> tl  = new ThreadLocal<Long>();
          tl.set(obj);
          // . . .
          tl.remove();
     }

}

但这种情况毕竟是少数,因为在这种情况下,完全可以不用ThreadLocal的。

2)如果ThreadLocal是在同一个类中的不同方法中使用,可以将ThreadLocal作为该类的字段。

public class Hello {
[static] ThreadLocal<Long> tl = new ThreadLocal<Long>();

public void method1(){
      //  . . .
      tl1.set();
      // . . .

method2();
}

private void method2(){
     tl.get();
   // . . .
    tl.remove();
}
}

这种情况用的相对来说,会多一些。在这种情况下,通常建议的做法是将ThreadLocal声明为static的,但也不是必须的。

3)ThreadLocal要跨类使用,一种方案是将ThreadLocal设置为public static的。另一种方案是像上面的例子一样。

错误使用ThreadLocal造成内存泄漏

实际使用过程中,大家很少会去使用ThreadLocal.remove()方法。另外,还有人会用ThreadLocal来做cache,那就更不可能去使用remove方法了。

前面提到,当线程结束时,ThreadLocalMap也会被自动清理,ThreadLocalMap中的对象如果没有其它的引用,就会被GC掉,这样就不会发生内存泄漏了。

如果线程A没有结束,某个ThreadLocalA对象如果被设置为null,线程A再次执行时,如果还使用到其它的ThreadLocalB。ThreadLocalA关联到的对象也会从ThreadLocalMap中移出。这样也可以避免内存泄漏。

如果线程A没有结束,某个ThreadLocalA对象如果被设置为null,线程A再次使用时,如果是执行的其它的任务,没有使用到任何的ThreadLocal,那么就会造成内存泄漏。

在使用ThreadLocal时,理解它很重要,更要知道怎么用它。

JavaSe:ThreadLocal的更多相关文章

  1. Java多线程10:ThreadLocal的作用及使用

    ThreadLocal的作用 从上一篇对于ThreadLocal的分析来看,可以得出结论:ThreadLocal不是用来解决共享对象的多线程访问问题的,通过ThreadLocal的set()方法设置到 ...

  2. Java多线程9:ThreadLocal源码剖析

    ThreadLocal源码剖析 ThreadLocal其实比较简单,因为类里就三个public方法:set(T value).get().remove().先剖析源码清楚地知道ThreadLocal是 ...

  3. java并发编程(3):ThreadLocal

    转载:http://www.cnblogs.com/dolphin0520/p/3920407.html 一. 对ThreadLocal的理解 ThreadLocal,很多地方叫做线程本地变量,也有地 ...

  4. 并发编程(四):ThreadLocal从源码分析总结到内存泄漏

    一.目录      1.ThreadLocal是什么?有什么用?      2.ThreadLocal源码简要总结?      3.ThreadLocal为什么会导致内存泄漏? 二.ThreadLoc ...

  5. 一个故事讲明白线程的私家领地:ThreadLocal

    张大胖上午遇到了一个棘手的问题,他在一个AccountService中写了一段类似这样的代码: Context ctx = new Context(); ctx.setTrackerID(.....) ...

  6. java多线程18: ThreadLocal的作用

    从上一篇对于ThreadLocal的分析来看,可以得出结论:ThreadLocal不是用来解决共享对象的多线程访问问题的,通过ThreadLocal的set()方法设置到线程的ThreadLocal. ...

  7. Java并发(4):ThreadLocal

    一.对ThreadLocal的理解 ThreadLocal是java.lang包中的一个类,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多.可能很多朋友都知道ThreadLoca ...

  8. 正确理解ThreadLocal:ThreadLocal中的值并不一定是完全隔离的

    首先再讨论题主的这个观点之前我们要明确一下ThreadLocal的用途是什么? ThreadLocal并不是用来解决共享对象的多线程访问问题. 看了许多有关ThreadLocal的博客,看完之后会给人 ...

  9. 多线程之:ThreadLocal

    Java中ThreadLocal类可以使创建的变量只被同一个线程进行读和写操作,即使有多个线程同时执行同一段代码,并且这段代码中又有一个指向同一个ThreadLocal变量的引用,这些线程依然不能看到 ...

随机推荐

  1. C#非常重要基础之多态

    前几天看了一位同志的博客,写的是关于他自己去支付宝面试的经历.过程大体是这样的:问答的时候,前面部分,作者都应答如流,说起自己经验如何之丰富,最后面试官问了作者一个问题:请简述多态的概念和作用.结果这 ...

  2. Entity Framework 6 Recipes 2nd Edition(10-9)译 -> 在多对多关系中为插入和删除使用存储过程

    10-9. 在多对多关系中为插入和删除使用存储过程 问题 想要在一个无载荷的多对多关系中使用存储过程(存储过程只影响关系的连接表) 解决方案 假设有一个多对多关系的作者( Author)表和书籍( B ...

  3. [译]ZOOKEEPER RECIPES-Locks

    锁 全局式分布式锁要求任何时刻没有两个客户端会获得同一个锁对象,这可以通过使用ZooKeeper实现.像优先级队列一样,首先需要定义一个锁节点. 在ZooKepeer的发布中src/recipes/l ...

  4. Atitit MATLAB 图像处理attilax总结

    Atitit MATLAB 图像处理attilax总结 1.1. 下载 Matlab7.0官方下载_Matlab2012 v7.0 官方简体中文版-办公软件-系统大全.html1 1.2. Matla ...

  5. iOS--高级技术

    1.iOS---搜索功能 2.iOS--通讯录.蓝牙.内购.GameCenter.iCloud.Passbook等系统服务开发汇总 3.iOS-技巧性总结 4.iOS-调试技巧 5.iOS-即时通讯- ...

  6. 【Win 10 应用开发】手写识别

    记得前面(忘了是哪天写的,反正是前些天,请用力点击这里观看)老周讲了一个14393新增的控件,可以很轻松地结合InkCanvas来完成涂鸦.其实,InkCanvas除了涂鸦外,另一个大用途是墨迹识别, ...

  7. Grunt基本使用-V1.0

    浅语:grunt中文网:http://www.gruntjs.net/ 第一步:Grunt 依赖 Node.js 所以在安装之前确保你安装了 Node.js.然后开始安装 Grunt. 实际上,安装的 ...

  8. css权威指南-基本视觉格式化(水平与垂直)

    1.基本概念     (1)正常流:是指西方语言文本从左向右,从上向下显示.如果要让一个元素不在正常流中国,唯一的办法                     就是使之成为浮动或定位元素.     ( ...

  9. jQuery之常用且重要方法梳理(target,arguments,slice,substring,data,trigger,Attr)-(一)

    1.jquery  data(name) data() 方法向被选元素附加数据,或者从被选元素获取数据. $("#btn1").click(function(){ $(" ...

  10. ElasticSearch 5学习(1)——安装Elasticsearch、Kibana和X-Pack

    安装准备: 安装Elasticsearch唯一的要求是安装官方新版的Java,包括对应的Jdk. 安装Elasticsearch 首先到官网下载最新版本的Elasticsearch压缩包. 可以使用命 ...