一  问题抛出

  SimpleDateFormat是非线程安全的,在多线程情况下会遇见问题:

  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就是用空间换时间。

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

  2、如果要使用ThreadLocal,通常定义为private static类型,在我看来最好是定义为private static final类型。

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

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

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

1、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);
}
};

 2、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()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

7、ThreadLocal 与 synchronized 的对比

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

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

8、一句话理解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()函数的话,那么仍然会存在内存泄漏问题。

参考:

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

ThreadLocal总结的更多相关文章

  1. ThreadLocal简单理解

    在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...

  2. Android线程管理之ThreadLocal理解及应用场景

    前言: 最近在学习总结Android的动画效果,当学到Android属性动画的时候大致看了下源代码,里面的AnimationHandler存取使用了ThreadLocal,激起了我很大的好奇心以及兴趣 ...

  3. Threadlocal使用Case

    Threadlocal能够为每个线程分配一份单独的副本,使的线程与线程之间能够独立的访问各自副本.Threadlocal 内部维护一个Map,key为线程的名字,value为对应操作的副本. /** ...

  4. 多线程映射工具——ThreadLocal

    ThreadLocal相当于一个Map<Thread, T>,各线程使用自己的线程对象Thread.currentThread()作为键存取数据,但ThreadLocal实际上是一个包装了 ...

  5. ThreadLocal 工作原理、部分源码分析

    1.大概去哪里看 ThreadLocal 其根本实现方法,是在Thread里面,有一个ThreadLocal.ThreadLocalMap属性 ThreadLocal.ThreadLocalMap t ...

  6. ThreadLocal<T>的是否有设计问题

    一.吐槽 ThreadLocal<T>明显是.NET从JAVA中来的一个概念,但是这种设计是否出现了问题. 很明显,在JAVA中threadLocal直接是Thread的成员,当然随着th ...

  7. 理解ThreadLocal —— 一个map的key

    作用: 当工作于多线程中的对象使用ThreadLocal维护变量时,threadLocal为每个使用该变量的线程分配一个独立的变量副本. 接口方法: protected T initialValue( ...

  8. JavaSe:ThreadLocal

    JDK中有一个ThreadLocal类,使用很方便,但是却很容易出现问题.究其原因, 就是对ThreadLocal理解不到位.最近项目中,出现了内存泄漏的问题.其中就有同事在使用ThreadLocal ...

  9. 0041 Java学习笔记-多线程-线程池、ForkJoinPool、ThreadLocal

    什么是线程池 创建线程,因为涉及到跟操作系统交互,比较耗费资源.如果要创建大量的线程,而每个线程的生存期又很短,这时候就应该使用线程池了,就像数据库的连接池一样,预先开启一定数量的线程,有任务了就将任 ...

  10. ThreadLocal 源码剖析

    ThreadLocal是Java语言提供的用于支持线程局部变量的类.所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量(每个线程一个拷贝).在各个Java web的各种框架 ...

随机推荐

  1. cordova本地浮动框提示插件使用:cordova-plugin-x-toast

    1. 添加插件:cordova plugin add cordova-plugin-x-toast 2. 调用方法(浮动提示插件,弹出本地浮动提示框): $cordovaToast.show(mess ...

  2. golang-http-post

    func httpPost() { resp, err := http.Post("https://www.abcd123.top/api/v1/login", "app ...

  3. 1.1.26 word内容导入PPT

    1.在开始菜单栏选择[视图]>[大纲].进入大纲后,对文本设置大纲级别. 2.设置好后,在[word选项]>下拉菜单中找到[不在功能区命令]>选择[发送到PPT].

  4. Linux printf命令详解

    Linux printf命令 printf命令模仿了C语言中的printf()函数.主要作用是输出文本,按照我们指定的格式输出文本.还有一个输出文本的命令echo,在输出文本时,echo会换行.pri ...

  5. UEFI引导过程及windows引导修复

    UEFI启动是一种新的主板引导项.传统引导方式就是Legacy模式. CSM的选项是UEFI模拟Legacy模式启动,选中后则可使用Legacy模式启动机器. Legacy模式仅支持传统的MBR分区, ...

  6. bzoj5108: [CodePlus2017]可做题

    Description qmqmqm希望给sublinekelzrip出一道可做题.于是他想到了这么一道题目:给一个长度为n的非负整数序列ai,你需 要计算其异或前缀和bi,满足条件b1=a1,bi= ...

  7. docker18.09.5 安装与启动、容器、镜像

    docker安装与启动 yum -y update 1.卸载老版本的 docker 及其相关依赖yum remove -y docker docker-common container-selinux ...

  8. Elasticsearch-6.7.0系列(三)5601端口 kibana——ES的UI界面

    https://artifacts.elastic.co/downloads/kibana/kibana-6.7.0-linux-x86_64.tar.gz      Kibana下载 有了elast ...

  9. java8与函数编程资料

    Functional programming Java 8 idioms Java SE 8's new Streams API Spring 技术布道师 Josh Long 来华:用 Show 代码 ...

  10. Linux系统编程——Daemon进程

    目录 Daemon进程介绍 前提知识 Daemon进程的编程规则 Daemon进程介绍 Daemon运行在后台也称作"后台服务进程". 它是没有控制终端与之相连的进程.它独立与控制 ...