Use Java ThreadLocal with caution

English 简体中文 Tiếng Việt
Summary

ThreadLocal provides variables where each thread maintains its own independent copy, useful for managing non-thread-safe objects without explicit synchronization. While beneficial for isolating state, a significant risk of memory leaks exists, particularly in Java EE applications utilizing thread pools. Since pooled threads are reused and not terminated, ThreadLocal instances, if not explicitly cleared, can retain references to objects, preventing garbage collection. This leads to persistent memory consumption and eventual OutOfMemoryError, often misdiagnosed as a need for increased PermGen space. To prevent such issues, developers must diligently call ThreadLocal's remove() method to clean up resources after use.

根据Oracle文档,ThreadLocal是一个提供线程局部变量的类。这些变量与其普通变量不同,因为每个访问它的线程(通过其getset方法)都有自己独立初始化的变量副本。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来简化工作时要小心。否则,不仅您的内存,您自己也可能会被它“咬到”。

JAVA MEMORY LEAK THREADLOCAL

  RELATED

  COMMENTS

0

No comment for this article.