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 threadLocal = new ThreadLocal();

    @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对应的所有线程的局部变量都有可能被回收。

你可能感兴趣的:(Java并发编程之ThreadLocal原理)