ThreadLocal的使用方法和底层原理

一、ThreadLocal简介

ThreadLocal的概念,顾名思义:本地线程,实现每一个线程都有自己的专属本地变量。

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

二、Thread 使用示例

从上面简介中可以看出,ThreadLocal可以避免线程安全问题。那么我们就看一个线程不安全的例子:

例子一:

SimpleDateFormat 不是线程安全的

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


    public static  Date parse(String dateStr) {
        Date date = null;
        try {
            date = sdf.get().parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}
  public static void main(String[] args) {
    ExecutorService service = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 20; i++) {
      String a= String.valueOf(i);
      service.execute(()->{
        System.out.println(DateUtils.parse("2019-06-01 16:34:30") + "######" + a);
      });
    }
    service.shutdown();
  }
}

执行这段代码,发现报错了。DateFormat 多线程时抛出NumberFormatException

Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.tuniu.htl.ctrip.common.util.DateUtils.parse(DateUtils.java:67)
	at com.tuniu.htl.ctrip.controller.ThreadLocalTest.lambda$main$0(ThreadLocalTest.java:22)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

造成抛异常的原因:
我们可以看到 sdf变量是声明为static的,被static修饰的变量,他是不依赖类的特定实例,一旦变量的值被修改,被所有类的实例锁共享,在类被JVM classloader的时候分配内存,并且是分配在永久区而非堆内存中。
所以在多线程的情况下,同时实例化一个静态非线程安全的变量的时候造成线程安全问题。

同理:例子二:

ThreadLocal的使用方法和底层原理_第1张图片

三、ThreadLocal底层原理

源码:

public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

从上面Thread类 源代码,可以看出ThreadLocal并不是一个Thread,而是Thread的局部变量。Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,**我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。**默认情况下这两个变量都是null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。

Get()方法:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

set()方法:

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

通过源码可以看出,
(1)我们放到Thread里的内容,实际上都是放到ThreadLocalMap中的,而不是方法ThreadLocal中。
(2)在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。
(3)每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。 比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。ThreadLocal 是 map结构是为了让每个线程可以关联多个 ThreadLocal变量。
这也就解释了 ThreadLocal 声明的变量为什么在每一个线程都有自己的专属本地变量。

四、ThreadLocal造成的内存泄漏问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。

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

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

五、用ThreadLocal解决例子一的问题

只需要将sdf放到ThreadLocal中,实现每个线程都要有自己独立的副本

private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(
        () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));


    public static  Date parse(String dateStr) {
        Date date = null;
        try {
            date = sdf.get().parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

执行结果:
ThreadLocal的使用方法和底层原理_第2张图片

你可能感兴趣的:(java并发)