ThreadLocal的误用可能会导致OutOfMemoryError

推荐Java Code Geeks上的两篇文章:


When and how to use a ThreadLocal

http://www.javacodegeeks.com/2013/10/when-and-how-to-use-a-threadlocal.html
 

——误用ThreadLocal可能会导致OutOfMemoryError。

Instead of grinding my teeth, I decided to open up the topic by publishing two articles, first of which you are currently reading. In the post I explain the motivation behind ThreadLocal usage. In the second post currently in progress I will open up the ThreadLocal bonnet and look at the implementation.

【正例】为不同地区的用户显示不同的日期格式

Let us start with an imaginary scenario in which ThreadLocal usage is indeed reasonable. For this, say hello to our hypothetical developer, named Tim. Tim is developing a webapp, in which there is a lot of localized content. For example a user from California would expect to be greeted with date formatted using a familiar MM/dd/yy pattern, one from Estonia on the other hand would like to see a date formatted according to dd.MM.yyyy. So Tim starts writing code like this:

    public String formatCurrentDate() {
        DateFormat df = new SimpleDateFormat( "MM/dd/yy");
         return df.format( new Date());
    }
 
     public String formatFirstOfJanyary1970() {
        DateFormat df = new SimpleDateFormat( "MM/dd/yy");
         return df.format( new Date( 0));
    }

改进:将DateFormat提取出来作为实例变量。

After a while, Tim finds this to be boring and against good practices – the application code is polluted with such initializations. So he makes a seemingly reasonable move by extracting the DateFormat to an instance variable. After making the move, his code now looks like the following:

    private DateFormat df = new SimpleDateFormat( "MM/dd/yy");
 
     public String formatCurrentDate() {
         return df.format( new Date());
    }
 
     public String formatFirstOfJanyary1970() {
         return df.format( new Date( 0));
    }

问题:DateFormat非线程安全

Happy with the refactoring results, Tim tosses an imaginary high five to himself, pushes the change to the repository and walks home. Few days later the users start complaining – some of them seem to get completely garbled strings instead of the former nicely formatted dates.

Investigating the issue Tim discovers that the DateFormat implementation is not thread safe. Meaning that in the scenario above, if two threads simultaneously use the formatCurrentDate() and formatFirstOfJanyary1970() methods, there is a chance that the state gets mangled and displayed result could be messed up.

解决方案1:同步

So Tim fixes the issue by limiting the access to the methods to make sure one thread at a time is entering at the formatting functionality. Now his code looks like the following:

    private DateFormat df = new SimpleDateFormat( "MM/dd/yy");
 
     public synchronized String formatCurrentDate() {
         return df.format( new Date());
    }
 
     public synchronized String formatFirstOfJanyary1970() {
         return df.format( new Date( 0));
    }
 
新问题:导致程序生产力下降,同步导致性能瓶颈-wait。
 
After giving himself another virtual high five, Tim commits the change and goes to a long-overdue vacation. Only to start receiving phone calls next day complaining that the throughput of the application has dramatically fallen. Digging into the issue he finds out that synchronizing the access has created an unexpected bottleneck in the application. Instead of entering the formatting sections as they pleased, threads now have to wait behind one another.

解决方案2:使用ThreadLocal,为每个线程创建不同的DateFormat实例变量。

Reading further about the issue Tim discovers a different type of variables called ThreadLocal. These variables differ from their normal counterparts in that each thread that accesses one (via ThreadLocal’s get or set method) has its own, independently initialized copy of the variable. Happy with the newly discovered concept, Tim once again rewrites the code:

    public static ThreadLocal df = new ThreadLocal() {
        protected DateFormat initialValue() {
             return new SimpleDateFormat( "MM/dd/yy");
        }
    };
 
     public String formatCurrentDate() {
         return df.get().format( new Date());
    }
 
     public String formatFirstOfJanyary1970() {
         return df.get().format( new Date( 0));
    }

Going through a process like this, Tim has through painful lessons learned a powerful concept. Applied like in the last example, the result serves as a good example about the benefits.

可能的风险1:如果使用非JDK中的DateFormat,可能不是被bootstrap classloader加载;则使用之后还会存留在线程池中。而线程池中线程的生命周期会超过应用的生命周期,会阻止ClassLoader被垃圾回收,导致OutOfMemoryError。

But the newly-found concept is a dangerous one. If Tim had used one of the application classes instead of the JDK bundled DateFormat classes loaded by the bootstrap classloader, we are already in the danger zone. Just forgetting to remove it after the task at hand is completed, a copy of that Object will remain with the Thread, which tends to belong to a thread pool. Since lifespan of the pooled Thread surpasses that of the application, it will prevent the object and thus a ClassLoader being responsible for loading the application from being garbage collected. And we have created a leak, which has a chance to surface in a good old java.lang.OutOfMemoryError: PermGen space form

 

可能的风险2:把ThreadLocal用作获取全局上下文。

Another way to start abusing the concept is via using the ThreadLocal as a hack for getting a global context within your application. Going down this rabbit hole is a sure way to mangle your application code with all kind of unimaginary dependencies coupling your whole code base into an unmaintainable mess.

 

Threading stories: ThreadLocal in web applications

http://www.javacodegeeks.com/2012/05/threading-stories-threadlocal-in-web.html
 
This week I spend reasonable time to eliminate all our ThreadLocal variables in our web applications. The reason was that they created classloader leaks and we coudn’t undeploy our applications properly anymore.

Classloader leaks happen when a GC root keeps referencing an application object after the application was undeployed. If an application object is still referenced after undeploy, then the whole class loader can’t be garbage collected cause the considered object references your applications class file which in turn references the classloader. This will cause an OutOfMemoryError after you’ve undeployed and redeployed a couple of times.

——如果在应用被undeployed之后,GC root还保持着某个应用对象的引用;→  则整个classloader都不会被垃圾回收;→  则会导致ClassLoader Leak。导致OutOfMemoryError

ThreadLocal is one classic candidate that can easily create classloader leaks in web applications. The server is managing its threads in a pool. These threads live longer than your web application. In fact they don’t die at all until the underlying JVM dies. Now, if you put a ThreadLocal in a pooled thread that references an object of your class you *must* be careful. You need to make sure that this variable is removed again using ThreadLocal.remove().

——服务器总是把Thread放到线程池中,而线程池会被你的应用程序活得更久。所以在使用ThreadLocal时要小心,你需要调用ThreadLocal.remove()来删除ThreadLocal变量。

The issue in web applications is: where is the right place to safely remove ThreadLocal variables? Also, you may not want to modify that “removing code” every time a colleague decided to add another ThreadLocal to the managed threads.

We’ve developed a wrapper class around thread local that keeps all the thread local variables in one single ThreadLocal variable. Here is the code.

——那么应当何时调用ThreadLocal.remove()呢?

public class ThreadLocalUtil {
 
     private final static ThreadLocal <ThreadVariables > THREAD_VARIABLES = new ThreadLocal <ThreadVariables >() {
 
         /**
         * @see java.lang.ThreadLocal#initialValue()
         */

        @Override
         protected ThreadVariables initialValue() {
             return new ThreadVariables();
        }
    };
 
     public static Object getThreadVariable(String name) {
         return THREAD_VARIABLES.get().get(name);
    }
 
     public static Object getThreadVariable(String name, InitialValue initialValue) {
        Object o = THREAD_VARIABLES.get().get(name);
         if (o == null) {
            THREAD_VARIABLES.get().put(name, initialValue.create());
             return getThreadVariable(name);
        } else {
             return o;
        }
    }
 
     public static void setThreadVariable(String name, Object value) {
        THREAD_VARIABLES.get().put(name, value);
    }
 
     public static void destroy() {
        THREAD_VARIABLES.remove();
    }
}
 
public class ThreadVariables extends HashMap <String, Object > { }
 
public abstract class InitialValue {
 
     public abstract Object create();
 
}

The advantage of the utility class is that no developer needs to manage the thread local variable lifecycle individually. The class puts all the thread locals in one map of variables.

The destroy() method can be invoked where you can safely remove all thread locals in your web application. In our case thats a ServletRequestListener -> requestDestroyed() method. You will also need to place finally blocks elsewhere. Typical places are near the HttpServlet, in the init(), doPost(), doGet() methods. This may remove all thread locals in the pooled worker threads after the request is done or an exception is thrown unexpectedly. Sometimes it happens that the main thread of the server leaks thread local variables. If that is the case you need to find the right places where to call the ThreadLocalUtil -> destroy() method. To do that figure out where the main thread actually *creates* the thread variables. You could use your debugger to do that.

——这个工具类将所有ThreadLocal变量放到一个map中,不需要程序员独自管理ThreadLocal变量的生命周期。destroy()能够移除所有ThreadLocal变量,可以在ServletRequestListener.requestDestroyed()中调用它。

Many guys out there suggest to ommit ThreadLocal in web applications for several reasons. It can be very difficult to remove them in a pooled thread environment so that you can undeploy the applications safely. ThreadLocal variables can be useful, but it’s fair to consider other techniques before applying them. An alternative for web applications to carry request scope parameters is the HttpServletRequest. Many web frameworks allow for generic request parameter access as well as request/session attribute access, without ties to the native Servlet/Portlet API. Also many framework support request scoped beans to be injected into an object tree using dependency injection. All these options fulfill most requirements and should be considered prior to using ThreadLocal.

——ThreadLocal可以用其他技术替代。例如 HttpServletRequest
 
 
 
 

猜你喜欢

转载自blog.csdn.net/vking_wang/article/details/13168743