java学习记录--ThreadLocal使用案例
本文借由并发环境下使用线程不安全的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使用案例相关推荐
- Java学习记录五(多线程、网络编程、Lambda表达式和接口组成更新)
Java学习记录五(多线程.网络编程.Lambda表达式和接口组成更新) Java 25.多线程 25.1实现多线程 25.1.1进程 25.1.2线程 25.1.3多线程的实现 25.1.4设置和获 ...
- java多线程阶乘_【java学习记录】11.用多线程实现阶乘的计算过程和结果的读取(thread类ReadThread )...
(源码在上传的压缩包"[java学习记录]11.多线程实现阶乘计算(Thread类和Runnable接口)"中可看到,该压缩包可下载)java * 采用Thread类的子类实现线程 ...
- java学习记录十五:集合二Collections、Set、Map
java学习记录十五:集合二 一.Collections工具类 一.解释 二.常用方法 1.打乱集合顺序 2.按照默认规则排序 3.按指定规则排序 4.批量添加元素 二.可变参数 一.解释 二.写法 ...
- java学习记录16-反射
java学习记录 一.什么叫反射? 二.测试class类的创建方式有哪些 三.类加载内存分析 加载->链接->初始化 四.分析类的初始化 五.获得类的信息 六.动态的创建对象,通过反射 七 ...
- Java学习记录02
Java学习记录02 在学习中,博客的书写不可缺少,相应的markdown基本语法也要掌握,下面是自己在CSDN上查找并学习的链接:Markdown语法 2.6项目实训:基于控制台设计简易打折与累加计 ...
- Java学习记录_1:MOOC《零基础学Java语言》课后习题解
Hey,我是寒水,一名大二学生,电子商务专业在读,正在学习Java中.我试图将在Java学习中遇到的一些困惑和最终的解答发在这个账号上,希望以此来激励我不要放弃学习! 在学习记录这个专题中,我收藏一些 ...
- Java学习记录之简单的图书管理系统(完善中)
电子书管理系统 功能划分 最核心的功能 提供图书管理的服务 添加图书 查询 检索所有的图书 按已知检索图书信息 图书的删除 图书信息的更新 用户 用户的类型 管理员:图书的添加.更新.删除操作 普通用 ...
- Java学习记录(Day4)
学习Java第四天 单例模式 三元素: 构造结构私有化 静态属性指向实例 public static的 getInstance方法,返回第二步的静态属性 饿汉式单例模式 GiantDragon 应该只 ...
- JAVA学习记录DAY 14
今天记录一下类与对象的基础知识. 类就是对象的所有特点和动作的总结,对象就是类的具体体现.假设熊猫是一个类,滚滚是某只熊猫,这只滚滚就应该具备熊猫所有的特点和行为,比如熊猫的毛发黑白相间那么滚滚就应该 ...
最新文章
- 盛大文学推出“一人一书”计划,发布电子书战略
- java华农组合模式,华农《数据库应用》往年考试例卷
- Linux中的可重入函数和不可重入函数
- amd cpu不能在cmd环境下运行java代码_「我们一起学Java02」JDK、JRE、JVM简介,Java开发平台的搭建...
- 关卡 动画 蓝图 运行_UE4无缝过场动画
- python 反传播_岗位 | 滴滴急招反欺诈数据分析实习生(可转正)!字节跳动+德勤实习生项目热招中...
- iOS 简易音乐播放界面
- allegro设置快捷键
- 毛星云版opencv简记
- RANSAC算法理解
- Problem C: 车牌限行
- win10屡次自动打开系统代理服务器的解决办法
- 解决屏幕大小不一导致页面下方出现多余空白的问题
- java摄影网站源码_基于jsp的摄影网站-JavaEE实现摄影网站 - java项目源码
- pwm调速流程图小车_51单片机智能小车《pwm调速按键控制小车动作》
- Windows/Linux下C++对于UUID的跨平台封装
- Kafka_producer
- Android中铃声总结【安卓源码解析一】
- BP神经网络人口预测程序(matlab实现)(转)
- 八爪鱼批量爬取html中的数据,网页数据如何批量采集_视频教程 - 八爪鱼采集器...
热门文章
- Python 技术篇-解决转化后的exe程序被360误杀问题实例演示,python生成的exe被360识别为勒索病毒原因及解决方法
- 【STM32】STM32f4学习之路--嘀嗒定时器
- Centos下本地连接postgresql时出现认证错误的问题
- 格式字符串语法,摘取自JDK6
- 【双系统下给ubuntu18.04扩容】
- 一维数组和二位数组作为函数参数进行传递的方式
- real--复数的实数部分
- vs2012 error c4996: 'fopen': This function or variable may be unsafe
- css实现一侧开口三角形
- 题解 guP2421 【[NOI2002]荒岛野人】