美文网首页
Java并发编程之ThreadLocal原理

Java并发编程之ThreadLocal原理

作者: embers1996 | 来源:发表于2018-05-20 17:52 被阅读0次

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

    相关文章

      网友评论

          本文标题:Java并发编程之ThreadLocal原理

          本文链接:https://www.haomeiwen.com/subject/wjmzdftx.html