Use Java ThreadLocal with caution

  Pi Ke        2015-11-03 07:31:57       23,641        0    

According to Oracle documentation, ThreadLocal is a class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread. In short, ThreadLocal variables are variables belong to a thread, not a class or an instance of a class.

One common use of ThreadLocal is when you want to access some non thread-safe objects in threads without using synchronization mechanisms like synchronized block and locks. These variables will not share states among different threads, so there is no synchronization problem; while at the same since each thread will have only one instance of the ThreadLocal object, it saves memory.

For example, the class below generates unique identifiers local to each thread. A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.

 import java.util.concurrent.atomic.AtomicInteger;

 public class ThreadId {
     // Atomic integer containing the next thread ID to be assigned
     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Thread local variable containing each thread's ID
     private static final ThreadLocal threadId =
         new ThreadLocal() {
             @Override protected Integer initialValue() {
                 return nextId.getAndIncrement();
         }
     };

     // Returns the current thread's unique ID, assigning it if necessary
     public static int get() {
         return threadId.get();
     }
 }

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

Since ThreadLocal will keep a reference to the object it refers to as long as the thread is alive, there is a risk of memory leak in some applications, especially in Java EE applications. As in Java EE applications, to improve the performance, thread pool will be used most probably, this means that a thread will not be terminated when it completes its task, instead it will be returned to the thread pool and wait for another request. This means that if a class which has a ThreadLocal object defined and it is loaded in the thread, the ThreadLocal will never be GCed until the application terminates. This in turn will cause memory leaks. So the best practice is to clean up the ThreadLocal reference by calling ThreadLocal's remove() method.

If you do not clean up when you're done, any references it holds to classes loaded as part of a deployed webapp will remain in the permanent heap and will never get garbage collected. Redeploying/undeploying the webapp will not clean up each Thread's reference to your webapp's class(es) since the Thread is not something owned by your webapp. Each successive deployment will create a new instance of the class which will never be garbage collected.

You will end up with out of memory exceptions due to java.lang.OutOfMemoryError: PermGen space and after some googling will probably just increase -XX:MaxPermSize instead of fixing the bug.

Below is a simple demonstration on how a memory leak may happen when using ThreadLocal. This demo will simulate a container which creates a few threads to handle lots of requests. Each request will reference a ThreadLocal object. 

First, we will create a heavy object which has a reference to a field with 10MB in size and it will be handled by each request created later.

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;
		}
	}
}

Next, we will create a request class which will be passed to the worker thread and getting handled.

public class Request {
	ThreadLocal local = new ThreadLocal();
	
	public void setLocal(HeavyObject object){
		local.set(object);;
	}
	
	public void showLocal(){
		System.out.println(local.get());
	}
}

Followed will be a WorkerThread class to handle all the requests.

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;
	}
}

Finally, we will create the main testing class.

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;
	}
}

The main testing class will create a few WorkerThreads and each WorkerThread will handle a few requests which reference some ThreadLocal objects. Since the WorkerThread is never stopped before exiting this program, all the ThreadLocal objects created will persist in heap and the heap is eaten up with more and more requests created and finally you will get the OutOfMemoryError.

Now as a comparison, we create a NormalLocal class which will be referenced in Request. This time, no ThreadLocal is created in the Request class.

public class NormalLocal {
	HeavyObject object = null;
	
	public void set(HeavyObject object){
		this.object = object;
	}
	
	public Object get(){
		return this.object;
	}
}

Now the new Request class looks like:

public class Request {
	NormalLocal local = new NormalLocal();
	
	public void setLocal(HeavyObject object){
		local.set(object);;
	}
	
	public void showLocal(){
		System.out.println(local.get());
	}
}

With this change, no OutOfMemoryError will occur anymore since the memory is GCed when the request is not used anymore.

If you do end up experiencing these problems, you can determine which thread and class is retaining these references by using Eclipse's Memory Analyzer.

So be careful when you are using ThreadLocal to ease your work.Otherwise not only your memory but also yourself may be bitten by it.

THREADLOCAL  JAVA  MEMORY LEAK 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Another Internet Explorer joke

If Internet Explorer is brave enough to ask you to be your default browser, you're brave enough to ask that girl out. Do we have to be so cruel to IE?