[多线程] ThreadLocal总结

一  问题抛出

  SimpleDateFormat是非线程安全的,在多线程状况下会碰见问题:html

  public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        List<String> dateStrList = Lists.newArrayList(
                "2018-04-01 10:00:01",
                "2018-04-02 11:00:02",
                "2018-04-03 12:00:03",
                "2018-04-04 13:00:04",
                "2018-04-05 14:00:05"
        );
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        for (String str : dateStrList) {
            executorService.execute(() -> {//多线程共享同一个simpleDateFormat对象 try {
                    simpleDateFormat.parse(str);
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

   上述代码在多线程下可能会抛出异常。算法

   解决方案1,使用局部变量:编程

public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        List<String> dateStrList = Lists.newArrayList(
                "2018-04-01 10:00:01",
                "2018-04-02 11:00:02",
                "2018-04-03 12:00:03",
                "2018-04-04 13:00:04",
                "2018-04-05 14:00:05"
        );
        for (String str : dateStrList) {
            executorService.execute(() -> {
                try {
             SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");                    
            simpleDateFormat.parse(str);
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

   这样虽然解决的线程安全问题,可是每次执行都须要建立一个SimpleDateFormat对象,性能不是很好。安全

   解决方案二,使用线程局部变量:  多线程

/** 
 * 使用ThreadLocal以空间换时间解决SimpleDateFormat线程安全问题
 */
public class DateUtil {
    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    @SuppressWarnings("rawtypes")
    private static ThreadLocal threadLocal = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new SimpleDateFormat(DATE_FORMAT);
        }
    };

    public static DateFormat getDateFormat() {
        return (DateFormat) threadLocal.get();
    }

    public static Date parse(String textDate) throws ParseException {
        return getDateFormat().parse(textDate);
    }
}

二  理解ThreadLocal

  ThreadLocal,即线程本地变量。ThreadLocal为变量在每一个线程中都建立了一个副本,那么每一个线程能够访问本身内部的副本变量。这样多个线程均可以随意更改本身线程局部的变量,不会影响到其余线程。并发

  须要注意的是,ThreadLocal提供的只是一个浅拷贝,若是变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题能够经过重写ThreadLocal的initialValue()函数来本身实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。函数

  ThreadLocal与像synchronized这样的锁机制是不一样的。首先,它们的应用场景与实现思路就不同,锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。从性能开销的角度上来说,若是锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。post

  一、ThreadLocal提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每一个线程的变量不同,而同一个线程在任何地方拿到的变量都是一致的,这就是所谓的线程隔离。性能

  二、若是要使用ThreadLocal,一般定义为private static类型,在我看来最好是定义为private static final类型。this

  ThreadLocal能够总结为一句话:ThreadLocal的做用是提供线程内的局部变量,这种变量在线程的生命周期内起做用,减小同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

  先了解一下ThreadLocal类提供的几个方法:

  public T get() { }  //用来获取ThreadLocal在当前线程中保存的变量副本
  public void set(T value) { }  //用来设置当前线程中变量的副本
  public void remove() { }  //用来移除当前线程中变量的副本
  protected T initialValue() { }  //一个protected方法,用来返回此线程局部变量的当前线程的初始值,通常是在使用时进行重写的,它是一个延迟加载方法

一、get()方法解析

  首先咱们来看一下ThreadLocal类是如何为每一个线程建立一个变量的副本的。先看下get方法的实现:

  public T get() {
      //1.首先获取当前线程
      Thread t = Thread.currentThread();
      //2.获取当前线程的ThreadLocalMap对象
      ThreadLocalMap map = getMap(t);
      //3.若是map不为空,以threadlocal实例为key获取到对应Entry,而后从Entry中取出对象便可。
      if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);//这里的this即ThreadLocal对象
          if (e != null)
              return (T)e.value;
      }
      //若是map为空,也就是第一次没有调用set直接get(或者调用过set,又调用了remove)时,为其设定初始值
      return setInitialValue();
  }  

  首先是取得当前线程,而后经过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap,这里的入参为当前线程,返回的是当前线程中的实例变量。

  而后接着下面获取到Entry<key,value>键值对,注意这里获取键值对传进去的是this即当前ThreadLocal对象,而不是当前线程t。若是获取成功,则返回value值。若是map为空,则调用setInitialValue方法初始化value。

   首先看一下getMap方法中作了什么:

  ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
  }

  在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals,线程Thread类里持有了一个threadLocals成员变量:

  ThreadLocal.ThreadLocalMap threadLocals = null;

   ThreadLocalMap是ThreadLocal类的一个内部类,ThreadLocalMap的Entry继承了WeakReference,而且使用ThreadLocal做为键值。

   所以,get()方法的主要操做是获取属于当前线程的ThreadLocalMap,若是这个map不为空,咱们就以当前的ThreadLocal为键,去获取相应的Entry,Entry是ThreadLocalMap的静态内部类,它继承于弱引用,因此在get()方法里面如第10行同样调用e.value方法就能够获取实际的资源副本值。

  可是若是获取到的map为空,说明属于该线程的资源副本还不存在,则须要去建立资源副本,从代码中能够看到是调用setInitialValue()方法,其定义以下:

  private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();//获取到当前线程
        ThreadLocalMap map = getMap(t);//获取到当前线程的成语变量 if (map != null)//若是不为空
            map.set(this, value);//设置值,这里this即当前ThreadLocal对象 else
            createMap(t, value);//若是map为空,则须要先初始化一个map再设置值 return value;
    }

   第2行调用initialValue()方法初始化一个值。接下来是判断线程的ThreadLocalMap是否为空,不为空就直接设置值(键为this,值为value),为空则建立一个Map,调用方法为createMap(),其定义以下: 

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

   在进行get以前,必须先set,不然会报空指针异常。 若是想在get以前不须要调用set就能正常访问的话,必须重写initialValue()方法。所以若是没有执行set操做初始化Thread的threadLocals,则在建立ThreadLocal时必须重写initialValue()方法,不然会抛出异常:

   private static ThreadLocal threadLocal = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new SimpleDateFormat(DATE_FORMAT);
        }
    };

 二、set()方法解析

public void set(T value) {  
    // 获取当前线程对象  
    Thread t = Thread.currentThread();  
    // 获取当前线程本地变量Map  
    ThreadLocalMap map = getMap(t);  
    // map不为空  
    if (map != null)  
        // 存值  
        map.set(this, value);  
    else  
        // 建立一个当前线程本地变量Map  
        createMap(t, value);  
}

  首先经过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,而后将变量的值设置到这个ThreadLocalMap对象中,固然若是获取到的ThreadLocalMap对象为空,就经过createMap方法建立。

  所以ThreadLocal为每一个线程建立变量的副本的具体流程以下:

    (1)首先,在每一个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

    (2)初始时,在Thread里面,threadLocals为空,当经过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,而且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

    (3)而后在当前线程里面,若是要使用副本变量,就能够经过get方法在当前线程的threadLocals里面查找。

三  ThreadLocal使用的通常步骤

  (1)在多线程的类(如ThreadDemo类)中,建立一个private static类型的ThreadLocal对象threadXxx,用来保存线程间须要隔离处理的对象xxx。  

  (2)在ThreadDemo类中,建立一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。  

  (3)在ThreadDemo类的run()方法中,经过getXxx()方法获取要操做的数据,这样能够保证每一个线程对应一个数据对象,在任什么时候刻都操做的是这个对象。

七、ThreadLocal 与 synchronized 的对比

  (1)ThreadLocal和synchonized都用于解决多线程并发访问。可是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每一个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。而synchronized却正好相反,它用于在多个线程间通讯时可以得到数据共享。

  (2)synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

八、一句话理解ThreadLocal:向ThreadLocal里面存东西就是向它里面的Map存东西的,而后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。

四  ThreadLocal中的内存泄漏

  若是ThreadLocal被设置为null后,并且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将会被回收。这样一来,ThreadLocalMap中就会含有key为null的Entry,并且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些没法访问到的value会造成内存泄漏。为了解决这个问题,ThreadLocalMap中的getEntry()、set()和remove()函数都会清理key为null的Entry,如下面的getEntry()函数的源码为例。

    private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> var1) {
            int var2 = var1.threadLocalHashCode & this.table.length - 1;
            ThreadLocal.ThreadLocalMap.Entry var3 = this.table[var2];
            return var3 != null && var3.get() == var1 ? var3 : this.getEntryAfterMiss(var1, var2, var3);
        }

        private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> var1, int var2, ThreadLocal.ThreadLocalMap.Entry var3) {
            ThreadLocal.ThreadLocalMap.Entry[] var4 = this.table;

            for(int var5 = var4.length; var3 != null; var3 = var4[var2]) {
                ThreadLocal var6 = (ThreadLocal)var3.get();
                if (var6 == var1) {
                    return var3;
                }

                if (var6 == null) {
                    this.expungeStaleEntry(var2);
                } else {
                    var2 = nextIndex(var2, var5);
                }
            }

            return null;
        }

  在上文中咱们发现了ThreadLocalMap的key是一个弱引用,那么为何使用弱引用呢?使用强引用key与弱引用key的差异以下:

  • 强引用key:ThreadLocal被设置为null,因为ThreadLocalMap持有ThreadLocal的强引用,若是不手动删除,那么ThreadLocal将不会回收,产生内存泄漏。

  • 弱引用key:ThreadLocal被设置为null,因为ThreadLocalMap持有ThreadLocal的弱引用,即使不手动删除,ThreadLocal仍会被回收,ThreadLocalMap在以后调用set()、getEntry()和remove()函数时会清除全部key为null的Entry。

  但要注意的是,ThreadLocalMap仅仅含有这些被动措施来补救内存泄漏问题。若是你在以后没有调用ThreadLocalMap的set()、getEntry()和remove()函数的话,那么仍然会存在内存泄漏问题。

 参考:

  一、Java并发编程:深刻剖析ThreadLocal  https://www.cnblogs.com/xiaoxi/p/7755253.html

相关文章
相关标签/搜索