本文借由并发环境下使用线程不安全的SimpleDateFormat优化案例,帮助大家理解ThreadLocal.


最近整理公司项目,发现不少写的比较糟糕的地方,比如下面这个:

public class DateUtil {

    private final static SimpleDateFormat sdfyhm = new SimpleDateFormat(
"yyyyMMdd"); public synchronized static Date parseymdhms(String source) {
try {
return sdfyhm.parse(source);
} catch (ParseException e) {
e.printStackTrace();
return new Date();
}
} }
首先分析下:
该处的函数parseymdhms()使用了synchronized修饰,意味着该操作是线程不安全的,所以需要同步,线程不安全也只能是SimpleDateFormat的parse()方法,查看下源码,在SimpleDateFormat里面有一个全局变量
protected Calendar calendar;

Date parse() {

    calendar.clear();

  ... // 执行一些操作, 设置 calendar 的日期什么的

  calendar.getTime(); // 获取calendar的时间

}

该clear()操作会造成线程不安全.

此外使用synchronized 关键字对性能有很大影响,尤其是多线程的时候,每一次调用parseymdhms方法都会进行同步判断,并且同步本身开销就很大,因此这是不合理的解决方案.


改进方法

线程不安全是源于多线程使用了共享变量造成,所以这里使用ThreadLocal<SimpleDateFormat>来给每个线程单独创建副本变量,先给出代码,再分析这样的解决问题的原因.

/**
* 日期工具类(使用了ThreadLocal获取SimpleDateFormat,其他方法可以直接拷贝common-lang)
* @author Niu Li
* @date 2016/11/19
*/
public class DateUtil { private static Map<String,ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>(); private static Logger logger = LoggerFactory.getLogger(DateUtil.class); public final static String MDHMSS = "MMddHHmmssSSS";
public final static String YMDHMS = "yyyyMMddHHmmss";
public final static String YMDHMS_ = "yyyy-MM-dd HH:mm:ss";
public final static String YMD = "yyyyMMdd";
public final static String YMD_ = "yyyy-MM-dd";
public final static String HMS = "HHmmss"; /**
* 根据map中的key得到对应线程的sdf实例
* @param pattern map中的key
* @return 该实例
*/
private static SimpleDateFormat getSdf(final String pattern){
ThreadLocal<SimpleDateFormat> sdfThread = sdfMap.get(pattern);
if (sdfThread == null){
//双重检验,防止sdfMap被多次put进去值,和双重锁单例原因是一样的
synchronized (DateUtil.class){
sdfThread = sdfMap.get(pattern);
if (sdfThread == null){
logger.debug("put new sdf of pattern " + pattern + " to map");
sdfThread = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
logger.debug("thread: " + Thread.currentThread() + " init pattern: " + pattern);
return new SimpleDateFormat(pattern);
}
};
sdfMap.put(pattern,sdfThread);
}
}
}
return sdfThread.get();
} /**
* 按照指定pattern解析日期
* @param date 要解析的date
* @param pattern 指定格式
* @return 解析后date实例
*/
public static Date parseDate(String date,String pattern){
if(date == null) {
throw new IllegalArgumentException("The date must not be null");
}
try {
return getSdf(pattern).parse(date);
} catch (ParseException e) {
e.printStackTrace();
logger.error("解析的格式不支持:"+pattern);
}
return null;
}
/**
* 按照指定pattern格式化日期
* @param date 要格式化的date
* @param pattern 指定格式
* @return 解析后格式
*/
public static String formatDate(Date date,String pattern){
if (date == null){
throw new IllegalArgumentException("The date must not be null");
}else {
return getSdf(pattern).format(date);
}
}
}

测试

在主线程中执行一个,另外两个在子线程执行,使用的都是同一个pattern

    public static void main(String[] args) {
DateUtil.formatDate(new Date(),MDHMSS);
new Thread(()->{
DateUtil.formatDate(new Date(),MDHMSS);
}).start();
new Thread(()->{
DateUtil.formatDate(new Date(),MDHMSS);
}).start();
}

日志分析

put new sdf of pattern MMddHHmmssSSS to map
thread: Thread[main,5,main] init pattern: MMddHHmmssSSS
thread: Thread[Thread-0,5,main] init pattern: MMddHHmmssSSS
thread: Thread[Thread-1,5,main] init pattern: MMddHHmmssSSS

分析

可以看出来sdfMap put进去了一次,而SimpleDateFormat被new了三次,因为代码中有三个线程.那么这是为什么呢?

对于每一个线程Thread,其内部有一个ThreadLocal.ThreadLocalMap threadLocals的全局变量引用,ThreadLocal.ThreadLocalMap里面有一个保存该ThreadLocal和对应value,一图胜千言,结构图如下:

那么对于sdfMap的话,结构图就变更了下

那么日志为什么是这样的?分析下:

1.首先第一次执行DateUtil.formatDate(new Date(),MDHMSS);

//第一次执行DateUtil.formatDate(new Date(),MDHMSS)分析
private static SimpleDateFormat getSdf(final String pattern){
ThreadLocal<SimpleDateFormat> sdfThread = sdfMap.get(pattern);
//得到的sdfThread为null,进入if语句
if (sdfThread == null){
synchronized (DateUtil.class){
sdfThread = sdfMap.get(pattern);
//sdfThread仍然为null,进入if语句
if (sdfThread == null){
//打印日志
logger.debug("put new sdf of pattern " + pattern + " to map");
//创建ThreadLocal实例,并覆盖initialValue方法
sdfThread = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
logger.debug("thread: " + Thread.currentThread() + " init pattern: " + pattern);
return new SimpleDateFormat(pattern);
}
};
//设置进如sdfMap
sdfMap.put(pattern,sdfThread);
}
}
}
return sdfThread.get();
}

这个时候可能有人会问,这里并没有调用ThreadLocal的set方法,那么值是怎么设置进入的呢?
这就需要看sdfThread.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();
}

也就是说当值不存在的时候会调用setInitialValue()方法,该方法会调用initialValue()方法,也就是我们覆盖的方法.

对应日志打印.

put new sdf of pattern MMddHHmmssSSS to map
thread: Thread[main,5,main] init pattern: MMddHHmmssSSS

2.第二次在子线程执行DateUtil.formatDate(new Date(),MDHMSS);

//第二次在子线程执行`DateUtil.formatDate(new Date(),MDHMSS);`
private static SimpleDateFormat getSdf(final String pattern){
ThreadLocal<SimpleDateFormat> sdfThread = sdfMap.get(pattern);
//这里得到的sdfThread不为null,跳过if块
if (sdfThread == null){
synchronized (DateUtil.class){
sdfThread = sdfMap.get(pattern);
if (sdfThread == null){
logger.debug("put new sdf of pattern " + pattern + " to map");
sdfThread = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
logger.debug("thread: " + Thread.currentThread() + " init pattern: " + pattern);
return new SimpleDateFormat(pattern);
}
};
sdfMap.put(pattern,sdfThread);
}
}
}
//直接调用sdfThread.get()返回
return sdfThread.get();
}

分析sdfThread.get()

//第二次在子线程执行`DateUtil.formatDate(new Date(),MDHMSS);`
public T get() {
Thread t = Thread.currentThread();//得到当前子线程
ThreadLocalMap map = getMap(t);
//子线程中得到的map为null,跳过if块
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//直接执行初始化,也就是调用我们覆盖的initialValue()方法
return setInitialValue();
}

对应日志:

Thread[Thread-0,5,main] init pattern: MMddHHmmssSSS

同理第三次执行和第二次类似.直接调用sdfThread.get(),然后调用initialValue()方法,对应日志

Thread[Thread-1,5,main] init pattern: MMddHHmmssSSS

总结

在什么场景下比较适合使用ThreadLocal?stackoverflow上有人给出了还不错的回答。
When and how should I use a ThreadLocal variable?
One possible (and common) use is when you have some object that is not thread-safe, but you want to avoid synchronizing access to that object (I’m looking at you, SimpleDateFormat). Instead, give each thread its own instance of the object.

参考代码:

https://github.com/nl101531/JavaWEB 下Util-Demo

参考资料:
http://www.importnew.com/21479.html
http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html

作者:此博废弃_更新在个人博客
链接:https://www.jianshu.com/p/5675690b351e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

java学习记录--ThreadLocal使用案例(转)的更多相关文章

  1. java学习记录--ThreadLocal使用案例

    本文借由并发环境下使用线程不安全的SimpleDateFormat优化案例,帮助大家理解ThreadLocal. 最近整理公司项目,发现不少写的比较糟糕的地方,比如下面这个: public class ...

  2. Java 学习记录

    •Eclipse相关 Eclipse常用设置 解决 Eclipse 项目中有红色感叹号的详细方法(图文) JRE System Library [JavaSE-1.8](unbound) •Java ...

  3. Java学习记录第一章

    学习Java第一章的记录,这一章主要记录的是Java的最基础部分的了解知识,了解Java的特性和开发环境还有Java语言的优缺点. 计算机语言的发展大概过程:机器语言--->汇编语言---> ...

  4. Java学习之ThreadLocal

    转自:http://www.cnblogs.com/doit8791/p/4093808.html#3197185 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量.这时该变量是多个线程 ...

  5. Java学习记录 : 画板的实现

    接触java不满一个月,看厚厚的java入门简直要醉,故利用实例来巩固所学知识. 画板的实现其实从原理来说超级简单,可能一会儿就完成了. 但作为一名强迫症患者,要实现和win下面的画板一样的功能还是需 ...

  6. JAVA学习记录(一)————JAVA中的集合类

    这个图是总体的框架图,主要是两个接口Collection和Map都继承接口Iterator(Iterable),为了实现可以使用迭代器.Collection和Map类似平级关系. 1.这里我先学习下A ...

  7. JAVA学习记录<一>

    一: JAVA初体验: 1.JAVA简介: 2.环境搭建: 3:MyEclipse的使用简介: 4:程序的移植:项目的导入,导出. 5:学习JAVA的经验: 多写,多问,总结和复习!!!

  8. Java学习记录-Jdk包简单介绍

    java.applet Java语言编写的一些小应用程序 java.awt AWT 是Abstract Window ToolKit (抽象窗口工具包)的缩写,这个工具包提供了一套与本地图形界面进行交 ...

  9. Java学习记录-注解

    注解 一.org.springframework.web.bind.annotation ControllerAdviceCookieValue : 可以把Request header中关于cooki ...

随机推荐

  1. 初遇PHP(一)

    因为想给自己弄一个微信公众号,顺便提升一下自己,所以有了以下内容,本次学习的最终目标是能用php制作套微信公众号,然后转成Java.为什么要这么麻烦呢,其一是买的资料书是php的,其二是顺水推舟刚好可 ...

  2. Python学习8——魔法方法、特性和迭代器

    Python中很多名称比较古怪,开头和结尾都是两个下划线.这样的拼写表示名称有特殊意义,因此绝不要在程序中创建这样的名称.这样的名称中大部分都是魔法(方法)的名称.如果你的对象实现了这些方法,他们将在 ...

  3. vue 动态添加对象属性

    昨天使用vue发现直接给对象添加属性,并不能触发响应更新,后来看文档发现要通过this.$set 函数动态添加才可用,eg: this.$set( obj, key, data)

  4. Centos7.3安装Mysql5.7.26(glibc即linux通用版)

    1.检查防火墙是否关闭 //查看防火墙状态 firewall-cmd --state //关闭防火墙 systemctl stop firewalld systemctl disable firewa ...

  5. 在Windows平台上运行Tomcat

    从之前的学习中知道,可以调用Bootstrap类将Toomcat作为一个独立的应用程序来运行,在Windows平台上,可以调用startup.bat批处理文件来启动Tomcat,或运行shutdown ...

  6. C# 常用类库说明

    Array类 用括号声明数组是C#中使用Array类的记号.在后台使用C#语法,会创建一个派生于抽象基类Array的新类.这样,就可以使用Array类为每个C#数组定义的方法和属性了. Array类实 ...

  7. js循环遍历性能

    定length for循环 (有length) 不定length for循环(使用数组length) 不定length for循环(判断数组length是否存在) forEach(Array自带,对某 ...

  8. O030、Launch 和 shut off 操作详解

    参考https://www.cnblogs.com/CloudMan6/p/5460464.html   本节详细分析 instance launch 和 shut off 操作 ,以及如何在日志中快 ...

  9. Typora入门:全网最全教程

    目录 简介 Markdown介绍 常用快捷键 块元素 换行符 标题级别 引用文字 无序列表 有序列表 任务列表 代码块 数学表达式 插入表格 脚注 分割线 目录(TOC) 跨度元素 链接 网址 图片 ...

  10. hbase shell 基本操作

    hbase shell  基本操作 启动HBASE [hadoop@master ~]$hbase shell      2019-01-24 13:53:59,990 WARN  [main] ut ...