Use Java ThreadLocal with caution

  Pi Ke        2015-11-03 07:31:57       23,705        0          English  简体中文  Tiếng Việt 

根据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来简化工作时要小心。否则,不仅您的内存,您自己也可能会被它“咬到”。

THREADLOCAL  JAVA  MEMORY LEAK 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Concurrency Programming with Ruby