如今,并发应用程序最关键的方面之一就是共享数据。 当我们创建实现Runnable接口的线程,然后使用同一Runnable对象启动各种Thread对象时,所有线程都共享在runnable对象内部定义的相同属性。
这实质上意味着,如果我们更改线程中的任何属性,则所有线程都将受到此更改的影响,并且将在第一个线程中看到修改后的值。 有时这是期望的行为,例如 多个线程增加/减少相同的计数器变量;
但有时我们要确保每个线程必须在其自己的线程实例副本上工作,并且不影响其他数据,这时ThreadLocal将是一个不错的选择。
什么时候我们需要使用ThreadLocal?
例如,您正在开发某电商网站交易系统。您需要为每个请求此控制器流程的每个客户生成唯一的交易ID,并且需要将此交易ID传递到manager / DAO类中的业务方法以进行记录。一种解决方案是将该事务ID作为参数传递给所有业务方法。但这不是一个好的解决方案,因为代码是多余且不必要的。
为了解决这个问题,您可以在这里使用ThreadLocal变量。您可以在控制器或任何预处理器中生成事务ID。并在ThreadLocal中设置该交易ID。此后,无论此控制器调用什么方法,它们都可以从threadlocal访问此事务ID。还要注意,应用程序控制器一次将处理一个以上的请求,并且由于每个请求都是在框架级别在单独的线程中处理的,因此事务ID对于每个线程都是唯一的,并且可以从该线程的整个执行路径进行访问。
ThreadLocal class
Java Concurrency API使用ThreadLocal类为性能良好的线程局部变量提供了一种干净的机制。
public class ThreadLocal<T> extends Object {...}
此类提供线程局部变量。 这些变量与普通变量不同,因为每个访问线程(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。
ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或交易ID)。
此类具有以下方法:
- get():返回此线程局部变量在当前线程副本中的值。
- initialValue():为此线程局部变量返回当前线程的“初始值”。
- remove():为此线程局部变量删除当前线程的值。
- set(T value):将此线程局部变量的当前线程副本设置为指定值。
如何使用ThreadLocal?
下面的示例使用两个线程局部变量,即threadId和startDate。 根据建议,这两个字段均被定义为“私有静态”字段。 “ threadId”将用于标识当前正在运行的线程,“ startDate”将用于获取线程开始执行的时间。 以上信息将打印在控制台中,以验证每个线程是否维护了自己的变量副本。
/**
* @author 王琦 <QQ.Email>1124602935@qq.com</QQ.Email>
* @date 19/12/3 中午12:50
* @description
*/
public class DemoTask implements Runnable {
private static final AtomicInteger nextId = new AtomicInteger();
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
private static final ThreadLocal<String> startDate = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return dateFormat.format(System.currentTimeMillis());
}
};
// Returns the current thread's unique ID
public int getThreadId(){
return threadId.get();
}
@Override
public void run() {
System.out.printf("Starting Thread: %s : %s\n", getThreadId(), startDate.get());
try {
TimeUnit.SECONDS.sleep((int)Math.rint(Math.random() * 10));
} catch (Exception e){
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n\n", getThreadId(), startDate.get());
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new DemoTask());
thread1.start();
thread1.join();
Thread thread2 = new Thread(new DemoTask());
thread2.start();
thread2.join();
Thread thread3 = new Thread(new DemoTask());
thread3.start();
thread3.join();
}
}
现在,要验证变量本质上是否能够维持其状态,而与多个线程的多次初始化无关,让我们创建此任务的三个实例。 启动线程; 然后验证他们在控制台中打印的信息。
public static void main(String[] args) throws InterruptedException {
new Thread(new DemoTask()).start();
Thread.sleep(1000);
new Thread(new DemoTask()).start();
Thread.sleep(1000);
new Thread(new DemoTask()).start();
}
控制台输出:
Starting Thread: 0 : 2019-12-03 15:09:29
Starting Thread: 1 : 2019-12-03 15:09:30
Starting Thread: 2 : 2019-12-03 15:09:31
Thread Finished: 0 : 2019-12-03 15:09:29
Thread Finished: 1 : 2019-12-03 15:09:30
Thread Finished: 2 : 2019-12-03 15:09:31
为了我们可以清楚地识别出线程本地值对于每个线程实例都是安全的,调整下打印结果的顺序:
Starting Thread: 0 : 2019-12-03 15:09:29
Thread Finished: 0 : 2019-12-03 15:09:29
Starting Thread: 1 : 2019-12-03 15:09:30
Thread Finished: 1 : 2019-12-03 15:09:30
Starting Thread: 2 : 2019-12-03 15:09:31
Thread Finished: 2 : 2019-12-03 15:09:31
结论
局部线程的最常见用法是当您拥有一些不是线程安全的对象,但又想避免使用同步的关键字/块来同步对该对象的访问时。 而是给每个线程使用其自己的对象实例。
同步或threadlocal的一个很好的选择是使变量成为局部变量。 局部变量始终是线程安全的。 唯一可能阻止您执行此操作的是应用程序设计约束。
谨慎使用
在wabapp服务器中,它可能保留一个线程池,因此应在响应客户端之前删除ThreadLocal var,因为当前线程可能被下一个请求重用。 另外,如果您在完成操作后不进行清理,则它对作为已部署的Webapp的一部分加载的类的任何引用都将保留在永久堆中,并且永远不会收集垃圾。
原文链接:RelaxHeart网 / Tec博客 / Java ThreadLocal Variables – When and How to Use?