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