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>(){
            @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-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

参考资料:

深入浅出的学习Java ThreadLocal

SimpleDateFormat的线程安全问题与解决方案

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

您可能感兴趣的文章:

  • Java多线程编程之ThreadLocal线程范围内的共享变量
  • 深入解析Java中ThreadLocal线程类的作用和用法
  • java 中ThreadLocal 的正确用法
  • Java 并发编程之ThreadLocal详解及实例
  • 实例详解Java中ThreadLocal内存泄露
  • Java中的线程同步与ThreadLocal无锁化线程封闭实现
  • 实例讲解Java并发编程之ThreadLocal类
  • Java多线程编程中ThreadLocal类的用法及深入
  • Java 中ThreadLocal类详解
  • 简单分析Java线程编程中ThreadLocal类的使用
时间: 2018-03-19

实例讲解Java并发编程之ThreadLocal类

ThreadLocal类可以理解为ThreadLocalVariable(线程局部变量),提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当前执行线程在调用set时设置的最新值.可以将ThreadLocal<T>视为 包含了Map<Thread,T>对象,保存了特定于该线程的值. 概括起来说,对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间

Java 中ThreadLocal类详解

ThreadLocal类,代表一个线程局部变量,通过把数据放在ThreadLocal中,可以让每个线程创建一个该变量的副本.也可以看成是线程同步的另一种方式吧,通过为每个线程创建一个变量的线程本地副本,从而避免并发线程同时读写同一个变量资源时的冲突. 示例如下: import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import ja

Java 并发编程之ThreadLocal详解及实例

Java 理解 ThreadLocal 摘要: ThreadLocal 又名线程局部变量,是 Java 中一种较为特殊的线程绑定机制,用于保证变量在不同线程间的隔离性,以方便每个线程处理自己的状态.进一步地,本文以ThreadLocal类的源码为切入点,深入分析了ThreadLocal类的作用原理,并给出应用场景和一般使用步骤. 一. 对 ThreadLocal 的理解 1). ThreadLocal 概述 ThreadLocal 又名 线程局部变量,是 Java 中一种较为特殊的 线程绑定机制

java 中ThreadLocal 的正确用法

java 中ThreadLocal 的正确用法 用法一:在关联数据类中创建private static ThreadLocalThreaLocal的JDK文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread.如果我们希望通过某个类将状态(例如用户ID.事务ID)与线程关联起来,那么通常在这个类中定义private s

Java多线程编程中ThreadLocal类的用法及深入

ThreadLocal,直译为"线程本地"或"本地线程",如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名. 早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用.其实它还是挺有用的,不相信的话,我们一起

简单分析Java线程编程中ThreadLocal类的使用

一.概述   ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLocalVar更加合适.线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突.   从线程的角度看,每个线程都保持一个对

Java多线程编程之ThreadLocal线程范围内的共享变量

模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程. package com.ljq.test.thread; import java.util.HashMap; import java.util.Map; import java.util.Random; /** * 线程范围内的共享变量 * * 三个模块共享数据,主线程模块和AB模块 * * @author Administrator * */ public class ThreadScopeS

深入解析Java中ThreadLocal线程类的作用和用法

ThreadLocal与线程成员变量还有区别,ThreadLocal该类提供了线程局部变量.这个局部变量与一般的成员变量不一样,ThreadLocal的变量在被多个线程使用时候,每个线程只能拿到该变量的一个副本,这是Java API中的描述,通过阅读API源码,发现并非副本,副本什么概念?克隆品? 或者是别的样子,太模糊.   准确的说,应该是ThreadLocal类型的变量内部的注册表(Map<Thread,T>)发生了变化,但ThreadLocal类型的变量本身的确是一个,这才是本质!  

实例详解Java中ThreadLocal内存泄露

案例与分析 问题背景 在 Tomcat 中,下面的代码都在 webapp 内,会导致WebappClassLoader泄漏,无法被回收. public class MyCounter { private int count = 0; public void increment() { count++; } public int getCount() { return count; } } public class MyThreadLocal extends ThreadLocal<MyCount

Java中的线程同步与ThreadLocal无锁化线程封闭实现

Synchronized关键字 Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(

java中synchronized(同步代码块和同步方法)详解及区别

 java中synchronized(同步代码块和同步方法)详解及区别 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问题,当有多条线程同时访问共享数据时,如果进行同步,就会发生错误,Java提供的解决方案是:只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他

java 中同步方法和同步代码块的区别详解

java 中同步方法和同步代码块的区别详解 在Java语言中,每一个对象有一把锁.线程可以使用synchronized关键字来获取对象上的锁.synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁). 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问

JAVA线程同步实例教程

线程是Java程序设计里非常重要的概念,本文就以实例形式对此加以详细解读.具体分析如下: 首先,线程加锁有什么用处呢?举个例子:比如你现在有30000块大洋在银行存着,现在你到银行取钱,当你输入密码完成后,已经输入取款金额,比如你输入的是20000,就是在银行给你拿钱这个时刻,你老婆也去银行取这笔钱,你老婆同样取20000,因为此时你的账上仍然是30000,所以银行同样的操作在你老婆那端又进行了一遍,这样当你们两个完成各自操作后,银行记录的你账上还应该有10000块存款,这样是不是很爽.解决这个

Java中CountDownLatch进行多线程同步详解及实例代码

Java中CountDownLatch进行多线程同步详解 CountDownLatch介绍 在前面的Java学习笔记中,总结了Java中进行多线程同步的几个方法: 1.synchronized关键字进行同步. 2.Lock锁接口及其实现类ReentrantLock.ReadWriteLock锁实现同步. 3.信号量Semaphore实现同步. 其中,synchronized关键字和Lock锁解决的是多个线程对同一资源的并发访问问题.信号量Semaphore解决的是多副本资源的共享访问问题. 今天

java 实现线程同步的方式有哪些

什么是线程同步? 当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题. 实现同步机制有两个方法: 1.同步代码块: synchronized(同一个数据){} 同一个数据:就是N条线程同时访问一个数据. 2. 同步方法: public synchronized 数据返回类型 方法名(){} 就是使用 synchronized 来修饰某个方法,则该方法称为同步方法.对于同步方法而言,无需显示指定同步监视器,同步

C#中线程同步对象的方法分析

本文实例讲述了C#中线程同步对象的方法.分享给大家供大家参考.具体分析如下: 在编写多线程程序时无可避免会遇到线程的同步问题.什么是线程的同步呢? 举个例子:如果在一个公司里面有一个变量记录某人T的工资count=100,有两个主管A和B(即工作线程)在早一些时候拿了这个变量的值回去,过了一段时间A主管将T的工资加了5块,并存回count变量,而B主管将T的工资减去3块,并存回count变量.好了,本来T君可以得到102块的工资的,现在就变成98块了.这就是线程同步要解决的问题. 在.Net的某

分析Java中ArrayList与LinkedList列表结构的源码

一.ArrayList源码分析(JDK7) ArrayList内部维护了一个动态的Object数组,ArrayList的动态增删就是对这个对组的动态的增加和删除. 1.ArrayList构造以及初始化 ArrayList实例变量 //ArrayList默认容量 private static final int DEFAULT_CAPACITY = 10; //默认空的Object数组, 用于定义空的ArrayList private static final Object[] EMPTY_ELE

Java中的锁分类的详细介绍

在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类.介绍的内容如下: 公平锁/非公平锁 可重入锁 独享锁/共享锁 互斥锁/读写锁 乐观锁/悲观锁 分段锁 偏向锁/轻量级锁/重量级锁 自旋锁 上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释. 公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁. 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的

详解java中产生死锁的原因及如何避免

1. Java中导致死锁的原因 Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了.导致了死锁.这是最容易理解也是最简单的死锁的形式.但是实际环境中的死锁往往比这个复杂的多.可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依