本文借由并发环境下使用线程不安全的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>(){@Overrideprotected 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>(){@Overrideprotected 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>(){@Overrideprotected 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-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.cnblogs.com/shuilangyizu/p/8621733.html

java学习记录--ThreadLocal使用案例相关推荐

  1. Java学习记录五(多线程、网络编程、Lambda表达式和接口组成更新)

    Java学习记录五(多线程.网络编程.Lambda表达式和接口组成更新) Java 25.多线程 25.1实现多线程 25.1.1进程 25.1.2线程 25.1.3多线程的实现 25.1.4设置和获 ...

  2. java多线程阶乘_【java学习记录】11.用多线程实现阶乘的计算过程和结果的读取(thread类ReadThread )...

    (源码在上传的压缩包"[java学习记录]11.多线程实现阶乘计算(Thread类和Runnable接口)"中可看到,该压缩包可下载)java * 采用Thread类的子类实现线程 ...

  3. java学习记录十五:集合二Collections、Set、Map

    java学习记录十五:集合二 一.Collections工具类 一.解释 二.常用方法 1.打乱集合顺序 2.按照默认规则排序 3.按指定规则排序 4.批量添加元素 二.可变参数 一.解释 二.写法 ...

  4. java学习记录16-反射

    java学习记录 一.什么叫反射? 二.测试class类的创建方式有哪些 三.类加载内存分析 加载->链接->初始化 四.分析类的初始化 五.获得类的信息 六.动态的创建对象,通过反射 七 ...

  5. Java学习记录02

    Java学习记录02 在学习中,博客的书写不可缺少,相应的markdown基本语法也要掌握,下面是自己在CSDN上查找并学习的链接:Markdown语法 2.6项目实训:基于控制台设计简易打折与累加计 ...

  6. Java学习记录_1:MOOC《零基础学Java语言》课后习题解

    Hey,我是寒水,一名大二学生,电子商务专业在读,正在学习Java中.我试图将在Java学习中遇到的一些困惑和最终的解答发在这个账号上,希望以此来激励我不要放弃学习! 在学习记录这个专题中,我收藏一些 ...

  7. Java学习记录之简单的图书管理系统(完善中)

    电子书管理系统 功能划分 最核心的功能 提供图书管理的服务 添加图书 查询 检索所有的图书 按已知检索图书信息 图书的删除 图书信息的更新 用户 用户的类型 管理员:图书的添加.更新.删除操作 普通用 ...

  8. Java学习记录(Day4)

    学习Java第四天 单例模式 三元素: 构造结构私有化 静态属性指向实例 public static的 getInstance方法,返回第二步的静态属性 饿汉式单例模式 GiantDragon 应该只 ...

  9. JAVA学习记录DAY 14

    今天记录一下类与对象的基础知识. 类就是对象的所有特点和动作的总结,对象就是类的具体体现.假设熊猫是一个类,滚滚是某只熊猫,这只滚滚就应该具备熊猫所有的特点和行为,比如熊猫的毛发黑白相间那么滚滚就应该 ...

最新文章

  1. 盛大文学推出“一人一书”计划,发布电子书战略
  2. java华农组合模式,华农《数据库应用》往年考试例卷
  3. Linux中的可重入函数和不可重入函数
  4. amd cpu不能在cmd环境下运行java代码_「我们一起学Java02」JDK、JRE、JVM简介,Java开发平台的搭建...
  5. 关卡 动画 蓝图 运行_UE4无缝过场动画
  6. python 反传播_岗位 | 滴滴急招反欺诈数据分析实习生(可转正)!字节跳动+德勤实习生项目热招中...
  7. iOS 简易音乐播放界面
  8. allegro设置快捷键
  9. 毛星云版opencv简记
  10. RANSAC算法理解
  11. Problem C: 车牌限行
  12. win10屡次自动打开系统代理服务器的解决办法
  13. 解决屏幕大小不一导致页面下方出现多余空白的问题
  14. java摄影网站源码_基于jsp的摄影网站-JavaEE实现摄影网站 - java项目源码
  15. pwm调速流程图小车_51单片机智能小车《pwm调速按键控制小车动作》
  16. Windows/Linux下C++对于UUID的跨平台封装
  17. Kafka_producer
  18. Android中铃声总结【安卓源码解析一】
  19. BP神经网络人口预测程序(matlab实现)(转)
  20. 八爪鱼批量爬取html中的数据,网页数据如何批量采集_视频教程 - 八爪鱼采集器...

热门文章

  1. Python 技术篇-解决转化后的exe程序被360误杀问题实例演示,python生成的exe被360识别为勒索病毒原因及解决方法
  2. 【STM32】STM32f4学习之路--嘀嗒定时器
  3. Centos下本地连接postgresql时出现认证错误的问题
  4. 格式字符串语法,摘取自JDK6
  5. 【双系统下给ubuntu18.04扩容】
  6. 一维数组和二位数组作为函数参数进行传递的方式
  7. real--复数的实数部分
  8. vs2012 error c4996: 'fopen': This function or variable may be unsafe
  9. css实现一侧开口三角形
  10. 题解 guP2421 【[NOI2002]荒岛野人】