Java并发编程之深入解析ThreadLocal

前言:在学习多线程的过程中,ThreadLocal是必备的知识点。在很多情况下,我们只知道ThreadLocal的用法以及它在解决共享参数的频繁传递与线程安全问题方面有不错的表现,但是其底层的实现还是很模糊,那么这篇文章我们就来深入的解析下ThreadLocal。

一:ThreadLocal的简单介绍

ThreadLocal的从表面来看是本地线程,其实他是Thread的本地变量,是每个线程独享的本地变量,每个线程都有自己的ThreadLocal,各线程之间的这个变量是相互隔离的。

二:ThreadLocal的两种使用场景以及具体实现

  • 场景一:每个线程需要一个独享的对象,通常是工具类,比如典型的SimpleDateFormat和Random等。
  • 场景二:每个线程内需要保存线程内的全局变量,这样线程在执行多个方法的时候,可以在多个方法中获取这个线程内的全局变量,避免了过度参数传递的问题。
2.1:场景一下ThreadLocal的具体实现

我们日常生活中也有用到ThreadLocal的场景,例如我们读书的时候,去图书馆借书且图书馆只有这么一本,但是学校里有很多学生都需要看这本书,此时书就不够用,只能某一个人使用,我们就只能一个人去借然后等他还回去下一个人才可以去借。如果多个人一起抢着去借的话,就可能导致在抢书的过程中发生冲突或者毁坏图书的行为发生。此时,最好的办法就是我们和学校反馈,让学校多买几本书放到图书馆供学生阅读。这样就大大提高了阅读效率,保证每个学生都能到这本书,这样就不会发生争抢,避免了安全问题。

上面这种场景中,我们就可以把学生当作线程,这本书就当作共享变量,供多个线程去争抢。如果不能很好的处理,就会发生并发安全问题。下面我们通过一个Java的小例子来进入到ThreadLocal的学习。

假如我们有一个需求,那就是在多线程环境下,去格式化时间为指定格式yyyy-MM-dd HH:mm:ss,假设首次我们在线程不多的情况下来演示:

/**
 * 描述:两个线程打印日期
 */
public class ThreadLocalNormalUsage00 {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(10);
                System.out.println(date);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(5000);
                System.out.println(date);
            }
        }).start();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
}

此时,在线程数不对的情况下,我们这个实现貌似还是可以的,但是当我们线程数量很多的时候,我们就会创建很多的SimpleDateFormat对象到内存中去,这就导致内存的压力过大,显然也是不合理的。于是我们就会想能不能只创建一个对象供其他线程共享。

/**
 * 描述:     1000个打印日期的任务,用线程池来执行
 */
public class ThreadLocalNormalUsage01 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });
        }
      //关闭线程池,新任务加入不进来,旧任务执行完毕后关闭线程池
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}

执行结果:

在这里插入图片描述

从上面结果就可以很直接的看出线程的不安全性,原因在于SimpleDateFormat是一个线程不安全的类,其实例对象在多线程环境下作为共享数据,就会导致线程的不安全。此时,我们想到的可能是加锁synchronized.

 public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (ThreadLocalNormalUsage01.class) {
            s = dateFormat.format(date);
        }
        return s;
    }

当然,这个加锁机制的确是可以帮我们实现我们的需求,但是加锁的代价未免太大,而本文介绍的ThreadLocal就可以派上用场了。

下面我们引入ThreadLocal,需求还是一样,通过多线程来进行时间的格式化。上面有提到ThreadLocal的概念,它是Thread的本地变量,是每个线程独享的本地变量,每个线程都有自己的ThreadLocal,各线程之间的这个变量是相互隔离。所以我们的思路就是:通过ThreadLocal来给每一个线程复制一个副本供他们使用,这样就不会存在多线程共享一个变量而导致的线程安全问题了。

在这里插入图片描述

使用ThreadLocal的时候,我们就可以在不使用synchronized关键字的情况下也可以做到多线程下实现只创建一个SimpleDateFormat对象且能被多个线程同时使用。

/**
 * 描述:     利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
 */
public class ThreadLocalNormalUsage02 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

上面的代码使用到了ThreadLocal,将SimpleDateFormat对象用ThreadLocal包装了一层,使得多个线程内部都有一个SimpleDateFormat对象副本,每个线程使用自己的SimpleDateFormat,这样就不会产生线程安全问题了。那么以上介绍的是ThreadLocal的第一大场景的使用,也就是利用到了ThreadLocal的initialValue()方法,使得每个线程内都具备了一个SimpleDateFormat副本。

2.2:场景二下ThreadLocal的具体实现

我们在开发过程中经常出现这样的情况,一个参数需要被线程内所有方法共享,我们经常做的就是通过参数进行传递。

在这里插入图片描述

但是这样的传递显得我们的代码很不优雅,代码冗余且不易维护。我们希望做到每个线程内保存全局变量,可以让不同的方法直接使用,避免参数的多次传递。

假如现在有这样一个需求,当用户请求服务的时候记录用户的基本信息,以供各方法使用。新建一个用户类,类成员变量包括姓名,性别,手机号,我们需要定义三个方法来分别获取学生的姓名、性别和手机号,那么我们传统的做法是

public class ThreadLocalUsage03 {

    public static void main(String[] args) {
        User user = init();
        new NameService().getName(user);
        new SexService().getSex(user);
        new PhoneService().getPhone(user);
    }

    private static User init() {
        User user = new User();
        user.name = "Orange";
        user.sex = "female";
        user.phone = "13288888888";
        return user;
    }

}

class User {

    /**
     * 姓名、性别、手机号
     */
    String name;
    String sex;
    String phone;

}

class NameService {

    public void getName(User user) {
        System.out.println(user.name);
    }

}

class SexService {

    public void getSex(User user) {
        System.out.println(user.sex);
    }

}

class PhoneService {

    public void getPhone(User user) {
        System.out.println(user.phone);
    }

}


显然,这样的做法就很显得代码和冗余,且不好维护。此时,我们可能会想到可以将学生信息存入到一个共享的Map中,需要学生信息的时候直接去Map中取,如下图所示:

在这里插入图片描述

其实这也是一种思路,但是在并发环境下,如果要使用Map,那么就需要使用同步的Map,比如ConcurrentHashMap或者Collections.SynchronizedMap(),前者底层用的是CAS和锁机制,后者直接使用的是synchronized,性能也不尽人意。

此时,我们就可以使用ThreadLocal来实现,每个线程内维护一个用户信息,这些信息在同一个线程内相同,但是在不同线程使用的业务内又是不同的。ThreadLocal在不影响性能的情况下,也无需层层传递参数,就可以达到保存当前线程对应的用户信息。

public class ThreadLocalUsage04 {

    public static void main(String[] args) {
        init();
        new NameService().getName();
        new SexService().getSex();
        new ScoreService().getScore();
    }

    private static void init() {
        User user = new User();
        user.name = "Orange";
        user.sex = "female";
        user.phone = "13288888888";
        ThreadLocalProcessor.userThreadLocal.set(user);
    }

}

class ThreadLocalProcessor {

    public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

}

class User {

    /**
     * 姓名、性别、手机号
     */
    String name;
    String sex;
    String phone;

}

class NameService {

    public void getName() {
        System.out.println(ThreadLocalProcessor.userThreadLocal.get().name);
    }

}

class SexService {

    public void getSex() {
        System.out.println(ThreadLocalProcessor.userThreadLocal.get().sex);
    }

}

class ScoreService {

    public void getScore() {
        System.out.println(ThreadLocalProcessor.userThreadLocal.get().phone);
    }

}


上面的代码就省去了频繁的传递参数,也没有使用到锁机制,同样满足了需求,思想其实和上面将用户信息存储到Map中的思想差不多,只不过这里不是将学生信息存储到Map中,而是存储到了ThreadLocal中,原理图如下所示:

在这里插入图片描述

通过两个常见场景的学习,我们对ThreadLocal也有了一定的了解,我们也明白了ThreadLocal的两个作用

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)

  • 在任何方法中都可以轻松获取到该对象

2.3:场景一和场景二小结
  • 在场景一下我们用到了initilValue方法在ThredLocal第一次get的时候来初始化对象,对象的初始化时机是由我们控制的。
  • 在场景二下保存到threadLocal里面对象的生成时机不由我们随意控制,拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便我们后续使用。

三:ThreadLocal的原理以及源码解析

在我们阅读ThreadLocal的源码之前,有必要先弄清楚Thread、ThreadLocal、ThreadLocalMap三者之间的关系。来看一张图片,帮助我们了解这三者之间的联系。

在这里插入图片描述

上图基本展示了Thread、ThreadLocalMap、和ThreadLocal之间的关系,每个Thread中都有一个ThreadLocalMap成员变量,ThreadLocalMap中是ThreadLocal为key,需要存储的数据为value的Entry数组。这意味着同一个Thread中通过ThreadLocalMap来保存不同的ThreadLocal,说明其可能会用到很多个不同的ThreadLocal对象。

在Thread中有两个变量,threadLocals和inheritableThreadLocals,他们初始值都是null,他们类型都是ThreadLocal.ThreadLocalMap,也就是ThreadLocal类的一个静态内部类ThreadLocalMap。

    ThreadLocal.ThreadLocalMap threadLocals = null;

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

先看下ThreadLocalMap的一些成员变量

 				//初始化默认的数量
        private static final int INITIAL_CAPACITY = 16;

        //Entry数组
        private Entry[] table;

       	//数组内部元素个数
        private int size = 0;

        // 数组扩容阈值,默认为0,创建了ThreadLocalMap对象后会被重新设置
        private int threshold;

ThreadLocalMap中维护一个数据结构类型为Entry的数组,节点类型如下代码所示:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

从上面的代码中可以看出Entry继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型,这里有提到弱引用,先不讨论,问候一并总结。

接着来看下ThreadLocalMap的构造方法:

/**
  * Construct a new map initially containing (firstKey, firstValue).
  * ThreadLocalMaps are constructed lazily, so we only create
  * one when we have at least one entry to put in it.
  */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

从构造方法的注释我们可以看出来,这个方法是一个懒加载方式,只有拥有一个entry的时候才会创建。

通过以上分析我们清楚了这三者的关系,也清楚了我们基本是通过ThreadLocal来操作ThreadLocalMap。接下来我们就去了解ThreadLocal中的一些常见方法set、get、remove等。

3.1:ThreadLocal类中的set方法

从场景二的中我们使用ThredLocal后可以看出来,我们在初始化的时候直接把User通过ThreadLocal的set方法放入到ThreadLocalMap中,以方便后续使用。

public void set(T value) {
  			//获取当前操作的线程
        Thread t = Thread.currentThread();
  			//接着获取到这个线程中的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
  			//如果map不为空,那么就直接set值进去,将本ThreadLocal对象作为键
        if (map != null)
            map.set(this, value);
        else
            //为空的话创建map 
            createMap(t, value);
    }

  ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
      
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上面ThreadLocalMap中的set方法我们单独拉出来解析一下:

 private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;

            //计算当前ThreadLocal对象作为键在Entry数组中的下标索引
            int i = key.threadLocalHashCode & (len-1);
						//遍历数组,现获取到指定下标的元素,如果为null那么就跳出for循环,执行下面的
   					//如果不为null,那么获取到下一个下标的元素,然后去做判断
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
              	//获取元素的key值
                ThreadLocal<?> k = e.get();
								//key值相同,那么进行值的替换
                if (k == key) {
                    e.value = value;
                    return;
                }
								//如果key为null,那么把值添加进去
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
						//在下标为 i 的位置添加元素
            tab[i] = new Entry(key, value);
            int sz = ++size;
   					//判断是否达到了扩容的条件,如果达到了,那么就进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

这里需要注意的是,其实nextIndex方法就是大名鼎鼎的开放寻址法的应用。这一点和HashMap不一样,HashMap存储HashEntry对象发生哈希冲突的时候采用的是链表方式进行存储,而这里是去寻找下一个合适的位置。

3.2:ThreadLocal类中的get方法
 public T get() {
   			//获取当前操作线程
        Thread t = Thread.currentThread();
   			//获取到当前线程中的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
          	// 如果map不为空,那么尝试获取Entry数组中以当前ThreadLocal对象为键的Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
          	//有就返回
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
   			// 如果Map为空或者在Entry数组中没有找到以当前ThreadLocal对象为键的Entry对象,
        // 那么就在这里进行值初始化,值初始化的过程是将null作为值,当前ThreadLocal对象作为键,
        // 存入到当前线程的ThreadLocalMap对象中
        return setInitialValue();
    }

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

 protected T initialValue() {
        return null;
    }

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
3.3:ThreadLocal类中的remove方法

使用ThreadLocal这个工具的时候,一般提倡使用完后及时清理存储在ThreadLocalMap中的值,防止内存泄露。

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
// 具体的删除指定的值,也是通过遍历寻找,找到就删除,找不到就算了
 private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

这里加以总结:
线程类Thread内部持有ThreadLocalMap的成员变量,而ThreadLocalMap是ThreadLocal的内部类,ThreadLocal操作了ThreadLocalMap对象内部的数据,对外暴露的都是ThreadLocal的方法API,隐藏了ThreadLocalMap的具体实现,理清了这一点,ThreadLocal就很容易理解了。

四:ThreadLocalMap内存泄露问题

内存泄漏:某个对象不再有用,但是其占用的内存却得不到释放。ThreadLocalMap内存泄露问题体现在下面的代码中

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

上面我们可以看出ThreadLocalMap中的Entry的key是用到了弱引用。而弱引用的特点就是如果一个对象只被弱引用关联,那么这个对象就可以被GC回收。由于我们的Entry的key是用到了弱引用,但是我们的value又是个强引用,这就有可能导致内存泄漏的问题。正常情况下,当线程终止后,保存在ThreadLocal中的value会被垃圾回收,因此也就没有任何强引用了。但是如果线程不终止,或者保持很久,那么key对应的value就不会被回收。在使用线程池的时候就会出现这种情况,线程池里面的线程会反复使用,并不会终止,除非线程池关闭。因此,此时就会导致value无法回收,就有可能会出现OOM(OutOfMemoryError)。

考虑到有可能会出现内存泄漏的问题,JDK也已经帮我们做了些处理,在set、remove、rehash、方法中会扫描key为null的Entry,并把对应的value置为null,这样value也会被回收。但是一旦ThreadLocal不被使用,那么实际上set、remove等方法也不会再去调用了,还是存在内存泄漏的问题,所以我们最好的做法就是用完ThreadLocal后,人为调用remove方法

这里简单介绍一下Java内的四大引用:

  • 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。比如String str = new String(“Hello ThreadLocal”);,其中str就是一个强引用。
  • 软引用:如果一个对象具有软引用,在JVM发生内存溢出之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会调用垃圾回收期回收掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
  • 弱引用:这里讨论ThreadLocalMap中的Entry类的重点,如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象被回收掉之后,再调用get方法就会返回null。
  • 虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。

五:ThreadLocalMap中的空指针问题

/**
 * 描述:  演示空指针异常
 */
public class ThreadLocalNPE {

    ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }

    public long get() {
        //这个地方的转化会导致空指针异常
        return longThreadLocal.get();
    }
  
   public Long get() {
        //改成Long就可以避免空指针异常
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println(threadLocalNPE.get());
            }
        });
        thread1.start();
    }
}

了解更多干货,欢迎关注我的微信公众号:爪哇论剑
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值