《Java程序员面试秘笈》—— 1.10 线程局部变量的使用

本节书摘来异步社区《Java 7并发编程实战手册》一书中的第1章,第1.10节,作者:【西】Javier Fernández González,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.10 线程局部变量的使用

共享数据是并发程序最核心的问题之一,对于继承了Thread类或者实现了Runnable接口的对象来说尤其重要。

如果创建的对象是实现了Runnable接口的类的实例,用它作为传入参数创建多个线程对象并启动这些线程,那么所有的线程将共享相同的属性。也就是说,如果你在一个线程中改变了一个属性,所有线程都会被这个改变影响。

在某种情况下,这个对象的属性不需要被所有线程共享。Java并发API提供了一个干净的机制,即线程局部变量(Thread-Local Variable),其具有很好的性能。

本节中,我们将创建两个程序:第一个具有刚才提到的问题,另一个使用线程局部变量机制解决了这个问题。

准备工作
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。

范例实现
按照接下来的步骤实现本节的范例

1.使要实现的范例具有之前提到的共享问题。创建一个名为UnsafeTask的类,它实现了Runnable接口。声明一个私有的java.util.Date属性。

public class UnsafeTask implements Runnable{
 private Date startDate;```
2.实现run()方法,这个方法将初始化startDate属性,并且将值打印到控制台,让线程休眠一个随机时间,然后再次将startDate的值打印到控制台。

@Override
public void run() {
startDate=new Date();
System.out.printf("Starting Thread: %s : %sn",Thread. currentThread().getId(),startDate);
try {

TimeUnit.SECONDS.sleep( (int)Math.rint(Math.random()*10));
} catch (InterruptedException e) {
  e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n",Thread. currentThread().getId(),startDate);

}`
3.实现这个有问题的应用程序的主程序。创建一个包含main()方法的Main类。这个方法将创建一个UnsafeTask类对象,用它作为传入参数创建10个线程对象并启动这10个线程,每个线程的启动间隔2秒。

  public class Core {
    public static void main(String[] args) {
      UnsafeTask task=new UnsafeTask();
      for (int i=0; i<10; i++){
       Thread thread=new Thread(task);
       thread.start();
       try {
          TimeUnit.SECONDS.sleep(2);
       } catch (InterruptedException e) {
         e.printStackTrace();
      }
    }
  }
}```
4.在下面的截屏中,你将看到这个程序执行的结果。每个线程有一个不同的开始时间,但是当它们结束时,三个线程都有相同的startDate属性值。

<div style="text-align: center"><img src="https://yqfile.alicdn.com/99c4b18a096f0ba73d8b725a51b90efe41ab1aee.png" width="" height="">
</div>


5.如之前提到的,我们将使用线程局部变量机制来解决这个问题。

6.创建一个SafeTask类,用以实现Runnable接口

public class SafeTask implements Runnable {
7.声明一个ThreadLocal对象。这个对象是在initialValue()方法中隐式实现的。这个方法将返回当前日期。

private static ThreadLocal startDate= new

ThreadLocal<Date>() {
    protected Date initialValue(){
        return new Date();
    }
};```

8.实现run()方法。它跟UnsafeTask类的run()方法实现了一样的功能,但是访问startDate属性的方式改变了。

@Override
public void run() {
  System.out.printf("Starting Thread: %s : %s\n",Thread. currentThread().getId(),startDate.get());
  try {
    TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  System.out.printf("Thread Finished: %s : %s\n",Thread. currentThread().getId(),startDate.get());
  }```
9.这个范例的入口类与第一个范例一样,只是创建并作为参数参入的Runnable类对象不同而已。

10.运行范例,并分析两个范例之间的不同。

工作原理
在下面的截屏中,你将看到安全线程类的执行结果。现在,这3个线程对象都有它们自己的startDate属性值。

线程局部变量分别为每个线程存储了各自的属性值,并提供给每个线程使用。你可以使用get()方法读取这个值,并用set()方法设置这个值。如果线程是第一次访问线程局部变量,线程局部变量可能还没有为它存储值,这个时候initialValue()方法就会被调用,并且返回当前的时间值。

<div style="text-align: center"><img src="https://yqfile.alicdn.com/c0e418d3668796c2de1d761bbc5873ebbf84c340.png" width="" height="">
</div>

更多信息
上一篇:《Java程序员面试秘笈》—— 1.9 线程中不可控异常的处理


下一篇:Kafka修炼日志(一):单节点使用问题一二