ThreadLocal模式

19-04-25 jdon
              

目的

将全局变量固定到线程,以防被其他线程破坏。如果在可调用对象或可运行对象中使用非只读的类变量或静态变量,则需要这样做。

通过应用本地线程模式Thread Local Pattern,您可以在处理请求的整个过程中跟踪应用程序实例或区域设置。在Java中,ThreadLocal类的工作方式类似于静态变量,但它只绑定到当前线程!这允许我们以线程安全的方式使用静态变量。 

示例

在Java中,线程局部变量由ThreadLocal类对象实现。该类提供线程局部变量。ThreadLocal包含T类型的变量,可通过get / set方法访问。例如,持有Integer值的ThreadLocal变量如下所示:

private static final ThreadLocal<Integer> myThreadLocalInteger = new ThreadLocal<Integer>();

源代码

Java ThreadLocal类提供线程局部变量。这些变量不同于它们的正常对应变量,因为访问一个变量(通过get或set方法)的每个线程都有自己的独立初始化变量副本。ThreadLocal实例通常是希望将状态与线程(例如,用户ID或事务ID)相关联的类中的私有静态字段。通过应用ThreadLocal模式,您可以在处理请求期间跟踪应用程序实例或区域设置。ThreadLocal类的工作方式类似于静态变量,但它只绑定到当前线程!这允许我们以线程安全的方式使用静态变量。在Java中,线程局部变量由ThreadLocal类对象实现。ThreadLocal包含一个T类型的变量,可以通过get / set方法访问它。SimpleDateFormat是基本的Java类之一,不是线程安全的。如果不为每个线程隔离SimpleDateFormat实例,则会出现问题。

在本例中,simpledateformat的用法是线程安全的。这是一个Thread Local Pattern的示例。

类图

步骤1:创建DateFormatCallable类,使用SimpleDateFormat将字符串日期转换为日期格式。日期格式和日期值将由构造函数传递给Callable。构造函数创建SimpleDateFormat的实例并将其存储在ThreadLocal类变量中。

public class DateFormatCallable implements Callable<Result> {
  // class variables (members)
  private ThreadLocal<DateFormat> df;    //TLTL   
  // private DateFormat df;                 //NTLNTL

  private String dateValue; // for dateValue Thread Local not needed
  

  /**
   * The date format and the date value are passed to the constructor
   * 
   * @param inDateFormat
   *          string date format string, e.g. "dd/MM/yyyy"
   * @param inDateValue
   *          string date value, e.g. "21/06/2016"
   */
  public DateFormatCallable(String inDateFormat, String inDateValue) {
    final String idf = inDateFormat;                 //TLTL
    this.df = new ThreadLocal<DateFormat>() {        //TLTL
      @Override                                      //TLTL
      protected DateFormat initialValue() {          //TLTL
        return new SimpleDateFormat(idf);            //TLTL
      }                                              //TLTL
    };                                               //TLTL
    // this.df = new SimpleDateFormat(inDateFormat);    //NTLNTL
    this.dateValue = inDateValue;
  }

  /**
   * @see java.util.concurrent.Callable#call()
   */
  @Override
  public Result call() {
    System.out.println(Thread.currentThread() + " started executing...");
    Result result = new Result();

    // Convert date value to date 5 times
    for (int i = 1; i <= 5; i++) {
      try {
        // this is the statement where it is important to have the
        // instance of SimpleDateFormat locally
        // Create the date value and store it in dateList
        result.getDateList().add(this.df.get().parse(this.dateValue));   //TLTL
//      result.getDateList().add(this.df.parse(this.dateValue));           //NTLNTL
      } catch (Exception e) {
        // write the Exception to a list and continue work
        result.getExceptionList().add(e.getClass() + ": " + e.getMessage());
      }

    }

    System.out.println(Thread.currentThread() + " finished processing part of the thread");

    return result;
  }
}

步骤2:创建将由Callable DateFormatCallable返回的Result对象。

public class Result {
  // A list to collect the date values created in one thread
  private List<Date> dateList = new ArrayList<Date>();

  // A list to collect Exceptions thrown in one threads (should be none in
  // this example)
  private List<String> exceptionList = new ArrayList<String>();
  
  /**
   * 
   * @return List of date values collected within an thread execution
   */
  public List<Date> getDateList() {
    return dateList;
  }

  /**
   * 
   * @return List of exceptions thrown within an thread execution
   */
  public List<String> getExceptionList() {
    return exceptionList;
  }
}

第3步:测试。创建ThreadLocalStorageDemo类使用Java类SimpleDateFormat将String日期值15/12/2015转换为Date格式。它使用4个线程执行20次,每个线程执行5次。在DateFormatCallable中使用ThreadLocal一切都运行良好。但是如果你注释了ThreadLocal变量(标有“// TLTL”)并在非ThreadLocal变量中注释(用“// NTLNTL”标记),你可以看到没有ThreadLocal会发生什么。很可能您会得到错误的日期值及/或异常。

public class ThreadLocalStorageDemo {
  /**
   * Program entry point
   * 
   * @param args
   *          command line args
   */
  public static void main(String args) {
    int counterDateValues = 0;
    int counterExceptions = 0;

    // Create a callable
    DateFormatCallable callableDf = new DateFormatCallable("dd/MM/yyyy", "15/12/2015");
    // start 4 threads, each using the same Callable instance
    ExecutorService executor = Executors.newCachedThreadPool();

    Future<Result> futureResult1 = executor.submit(callableDf);
    Future<Result> futureResult2 = executor.submit(callableDf);
    Future<Result> futureResult3 = executor.submit(callableDf);
    Future<Result> futureResult4 = executor.submit(callableDf);
    try {
      Result result = new Result[4];
      result[0] = futureResult1.get();
      result[1] = futureResult2.get();
      result[2] = futureResult3.get();
      result[3] = futureResult4.get();

      // Print results of thread executions (converted dates and raised exceptions)
      // and count them
      for (int i = 0; i < result.length; i++) {
        counterDateValues = counterDateValues + printAndCountDates(result[i]);
        counterExceptions = counterExceptions + printAndCountExceptions(result[i]);
      }

      // a correct run should deliver 20 times 15.12.2015
      // and a correct run shouldn't deliver any exception
      System.out.println("The List dateList contains " + counterDateValues + " date values");
      System.out.println("The List exceptionList contains " + counterExceptions + " exceptions");

    } catch (Exception e) {
      System.out.println("Abnormal end of program. Program throws exception: " + e); 
    }
    executor.shutdown();
  }

  /**
   * Print result (date values) of a thread execution and count dates
   * 
   * @param res  contains results of a thread execution
   */
  private static int printAndCountDates(Result res) {
    // a correct run should deliver 5 times 15.12.2015 per each thread
    int counter = 0;
    for (Date dt : res.getDateList()) {
      counter++;
      Calendar cal = Calendar.getInstance();
      cal.setTime(dt);
      // Formatted output of the date value: DD.MM.YYYY
      System.out.println(
          cal.get(Calendar.DAY_OF_MONTH) + "." + cal.get(Calendar.MONTH) + "." + +cal.get(Calendar.YEAR));
    }
    return counter;
  }

  /**
   * Print result (exceptions) of a thread execution and count exceptions
   * 
   * @param res  contains results of a thread execution
   * @return number of dates
   */
  private static int printAndCountExceptions(Result res) {
    // a correct run shouldn't deliver any exception
    int counter = 0;
    for (String ex : res.getExceptionList()) {
      counter++;
      System.out.println(ex);
    }
    return counter;
  }
}[/i][/i]

适用性

在以下任何情况下使用线程本地存储

  • 当您在Callable / Runnable对象中使用非只读的类变量时,在并行运行的多个线程中使用相同的Callable实例。

 

  • 当您在Callable / Runnable对象中使用非只读的静态变量时,Callable / Runnable的多个实例可以在并行线程中运行。