根据Oracle文档,ThreadLocal是一个提供线程局部变量的类。这些变量与其普通变量不同,因为每个访问它的线程(通过其get或set方法)都有自己独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,这些类希望将状态与线程关联。简而言之,ThreadLocal变量是属于线程的变量,而不是类的变量或类的实例。
ThreadLocal的一个常见用途是当您想在不使用同步机制(如同步块和锁)的情况下在线程中访问一些非线程安全的对象时。这些变量不会在不同线程之间共享状态,因此不存在同步问题;同时,由于每个线程只有一个ThreadLocal对象的实例,因此可以节省内存。
例如,下面的类生成对每个线程都唯一的标识符。线程的ID在其第一次调用ThreadId.get()时分配,并在后续调用中保持不变。
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// 用于保存下一个要分配的线程ID的原子整数
private static final AtomicInteger nextId = new AtomicInteger(0);
// 用于保存每个线程ID的线程局部变量
private static final ThreadLocal threadId =
new ThreadLocal() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// 返回当前线程的唯一ID,如有必要则分配ID
public static int get() {
return threadId.get();
}
}
只要线程处于活动状态并且ThreadLocal实例可访问,每个线程都会隐式地持有对线程局部变量副本的引用;线程结束后,其所有线程局部实例副本都将被垃圾回收(除非存在对这些副本的其他引用)。
由于ThreadLocal会在线程存活期间一直保持对它引用的对象的引用,因此在某些应用程序中,尤其是在Java EE应用程序中,存在内存泄漏的风险。在Java EE应用程序中,为了提高性能,很可能会使用线程池,这意味着线程在完成任务后不会终止,而是返回到线程池并等待另一个请求。这意味着如果一个类定义了一个ThreadLocal对象并且它在该线程中加载,那么ThreadLocal将永远不会被GC回收,直到应用程序终止。这反过来会导致内存泄漏。因此,最佳实践是通过调用ThreadLocal的remove()方法来清除ThreadLocal引用。
如果您在完成后没有清理,它持有的对作为已部署Web应用程序一部分而加载的类的任何引用都将保留在永久堆中,并且永远不会被垃圾回收。重新部署/卸载Web应用程序不会清理每个Thread对Web应用程序类(es)的引用,因为Thread不是Web应用程序拥有的东西。每次后续部署都将创建一个永远不会被垃圾回收的类的新的实例。
最终,您将遇到java.lang.OutOfMemoryError: PermGen space之类的内存不足异常,并且在进行一些谷歌搜索后,可能会增加-XX:MaxPermSize而不是修复错误。
下面是一个简单的演示,说明在使用ThreadLocal时如何发生内存泄漏。此演示将模拟一个容器,该容器创建一些线程来处理大量请求。每个请求都将引用一个ThreadLocal对象。
首先,我们将创建一个重量级对象,它引用一个大小为10MB的字段,它将由稍后创建的每个请求处理。
public class HeavyObject {
int limit = 10*1024*1024;
byte[] bytes = new byte[limit];
public HeavyObject(){
for(int j=0; j<limit; j++){
bytes[j] = 10;
}
}
}
接下来,我们将创建一个请求类,它将传递给工作线程并进行处理。
public class Request {
ThreadLocal local = new ThreadLocal();
public void setLocal(HeavyObject object){
local.set(object);;
}
public void showLocal(){
System.out.println(local.get());
}
}
接下来是一个WorkerThread类,用于处理所有请求。
public class WorkerThread implements Runnable{
private ThreadLocal local = new ThreadLocal<Byte[]>();
private volatile boolean isStopped = false;
public void run(){
System.out.println("Running...");
while(!isStopped){
try {
System.out.println(local.get());
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void setList(byte[] bytes){
local.set(bytes);
}
public void handleRequest(Request request){
request.showLocal();
}
public void end(){
isStopped = true;
}
}
最后,我们将创建主要的测试类。
public class ThreadLocalTest {
public static void main(String[] args){
int count = 5;
int i = 0;
long totalEatenMemory = 0;
long maxEatenMemory = Long.MIN_VALUE;
long minEatenMemory = Long.MAX_VALUE;
long startTime = System.nanoTime();
while(++i <= count){
long eatenMmeory = test();
if(eatenMmeory > maxEatenMemory){
maxEatenMemory = eatenMmeory;
}
if(eatenMmeory < minEatenMemory){
minEatenMemory = eatenMmeory;
}
totalEatenMemory += eatenMmeory;
}
long endTime = System.nanoTime();
System.out.println("Tests run : "+count+"; Avg eaten memory : "+(totalEatenMemory*1.0/(count*1000))+" KB");
System.out.println("Max eaten memory : "+maxEatenMemory);
System.out.println("Min eaten memory : "+minEatenMemory);
System.out.println("Total time elapsed : "+((endTime-startTime)*1.0/1000000L)+"ms");
System.out.println("Complete running");
}
private static long test(){
int count = 200;
int i = 0;
WorkerThread worker = new WorkerThread();
Thread thread = new Thread(worker);
thread.start();
try{
long initialMemory = Runtime.getRuntime().freeMemory();
while(++i <= count){
System.out.println("Free memory "+Runtime.getRuntime().freeMemory()+" before count "+i);
HeavyObject object = new HeavyObject();
System.out.println("Free memory "+Runtime.getRuntime().freeMemory()+" after count "+i);
Request request = new Request();
request.setLocal(object);
worker.handleRequest(request);
System.out.println("Free memory "+Runtime.getRuntime().freeMemory()+" at count "+i);
}
long endMemory = Runtime.getRuntime().freeMemory();
long eatenMemory = (initialMemory-endMemory);
System.out.println("Eaten memory : "+eatenMemory);
return eatenMemory;
} catch (Exception ex){
ex.printStackTrace();
} finally {
worker.end();
}
return 0;
}
}
主要的测试类将创建一些WorkerThreads,每个WorkerThreads将处理一些引用一些ThreadLocal对象的请求。由于WorkerThread在退出此程序之前从未停止,因此创建的所有ThreadLocal对象都将保留在堆中,并且随着创建越来越多的请求,堆被消耗殆尽,最终您将得到OutOfMemoryError。
现在,作为比较,我们创建一个NormalLocal类,它将在Request中被引用。这次,在Request类中没有创建ThreadLocal。
public class NormalLocal {
HeavyObject object = null;
public void set(HeavyObject object){
this.object = object;
}
public Object get(){
return this.object;
}
}
现在新的Request类如下所示:
public class Request {
NormalLocal local = new NormalLocal();
public void setLocal(HeavyObject object){
local.set(object);;
}
public void showLocal(){
System.out.println(local.get());
}
}
通过此更改,将不再发生OutOfMemoryError,因为当不再使用请求时,内存将被GC回收。
如果您最终遇到这些问题,可以使用Eclipse的内存分析器来确定哪个线程和类正在保留这些引用。
因此,在使用ThreadLocal来简化工作时要小心。否则,不仅您的内存,您自己也可能会被它“咬到”。
No comment for this article.