如果创建一个实现Runnable接口的类对象,然后使用相同的Runnable对象执行不同的线程对象,那么所有的线程将共享相同的属性。这意味着如果改变某个线程的一个属性,那么所有线程都会因此而受影响。
某些时候,我们需要一个不会被所有线程共享的属性。Java并发API提供一个具备高性能的清洁机制,称为线程局部变量。不过线程局部变量也有缺点,由于它们在线程执行期间保留值,这在线程重复使用的情况下会出现问题。
本节中,开发两个程序:一个暴露之前描述中出现的问题,另一个使用线程局部变量机制解决这个问题。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤完成范例:
-
首先,实现第一个程序。创建名为UnsafetTask的类,并指定其实现Runnable接口。定义一个私有的java.util.Date属性:
public class UnsafeTask implements Runnable { private Date startDate;
-
实现UnsafeTask对象的run()方法,此方法将初始化startDate属性,在控制台中输出属性值,随机休眠一段时间,然后再次输出startDate属性值:
@Override public void run() { startDate = new Date(); System.out.printf("Starting Thread: %s : %s\n", 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); }
-
现在实现这个错误程序的主类,创建包含main()方法的Main类。这个方法会创建UnsafeTask类的对象,并且使用这个对象启动10个线程,每个线程之间休眠2秒钟:
public class Main { 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(); } } } }
-
在下图中,可以看到程序的执行结果。每个线程有一个不同的启动时间,但是当线程结束时,属性值发生了变化,所以输出了错误数据。例如查看编号13的线程:
-
如之前提到的,我们将使用线程局部变量机制解决这个问题。
-
创建名为SafeTask的类,指定其实现Runnable接口:
public class SafeTask implements Runnable{
-
定义ThreadLocal类的对象,此对象具有一个包含initialValue()方法的隐性实现。此方法将返回实际日期:
private static ThreadLocal<Date> startDate = new ThreadLocal<Date>(){ protected Date initialValue(){ return new Date(); } };
-
实现run()方法,此方法与UnsafeTask类的run()方法功能相同,但是它改变了存取startDate属性的方式,我们将使用startDate对象的get()方法:
@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()); }
-
主类与非安全程序的主类内容相同,唯一不同的是Runnable类的名称发生变化。
-
运行程序,分析区别。
工作原理
在下图中,可以看到安全程序的执行结果。10个线程对象具有各自的startDate属性值:
线程局部变量机制存储每个使用这些变量的线程的一个属性值,使用get()方法读取这个值,以及set()方法改变这个值。当第一次存取线程局部变量时,如果它调用的线程对象没有值,线程局部变量将调用initialValue()方法给线程赋值并且返回这个初始值。
扩展学习
线程局部类提供了remove()方法,用来删除线程调用的线程局部变量存储的值。
Java并发API包含InheritableThreadLocal类,继承从一个线程创建的线程变量值。如果线程A有一个线程局部变量值,并且A创建了线程B,B将会具有和A中的线程局部变量相同的值。在线程局部变量中,通过覆盖此类的childValue()方法初始化子线程的值,并将父线程的值以参数形式接收。