threadlocalmap java




當類暴露給線程池時,清理ThreadLocal資源真的是我的工作嗎? (4)

我使用ThreadLocal

在我的Java類中,我有時主要使用ThreadLocal作為避免不必要的對象創建的方法:

@net.jcip.annotations.ThreadSafe
public class DateSensitiveThing {

    private final Date then;

    public DateSensitiveThing(Date then) {
        this.then = then;
    }

    private static final ThreadLocal<Calendar> threadCal = new ThreadLocal<Calendar>()   {
        @Override
        protected Calendar initialValue() {
            return new GregorianCalendar();
        }
    };

    public Date doCalc(int n) {
        Calendar c = threadCal.get();
        c.setTime(this.then):
        // use n to mutate c
        return c.getTime();
    }
}

我這樣做是出於正當的原因GregorianCalendar是那些光榮的有狀態,可變,非線程安全的對象之一,它提供跨多個調用的服務,而不是代表一個值。 此外,它被認為是“昂貴的”實例化(這是否真實不是這個問題的重點)。 (總的來說,我真的很佩服它:-))

Tomcat如何發牢騷

但是,如果我在任何聚集線程的環境中使用這樣的類 - 並且我的應用程序無法控制這些線程的生命週期 - 那麼就有可能發生內存洩漏。 Servlet環境就是一個很好的例子。

事實上,當一個webapp停止時,Tomcat 7就像這樣嘶嘶作響:

嚴重:Web應用程序[]創建了一個ThreadLocal,其鍵為[org.apache.xmlbeans.impl.store.CharUtil $ 1](值[[email protected]])和一個值類型為[java.lang.ref.SoftReference](值[[email protected]]),但在Web應用程序停止時無法將其刪除。 線程將隨著時間的推移而更新,以避免可能的內存洩漏。 2012年12月13日下午12:54:30 org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks

(在特定情況下,甚至我的代碼都沒有這樣做)。

誰應該受到責備?

這似乎不太公平。 Tomcat責備 (或我班級的用戶)做正確的事。

最終,這是因為Tomcat希望重用它為我提供的線程,以及其他 Web應用程序。 (呃 - 我覺得很髒。)可能,這對Tomcat而言並不是一個很好的策略 - 因為線程確實有/導致狀態 - 不要在應用程序之間共享它們。

但是,這項政策至少是常見的,即使這是不可取的。 我覺得我有義務 - 作為ThreadLocal用戶,為我的類提供一種方法來“釋放”我的類附加到各種線程的資源。

但該怎麼辦呢?

這裡做什麼是正確的?

對我來說,似乎servlet引擎的線程重用策略與ThreadLocal背後的意圖不一致。

但也許我應該提供一個工具來允許用戶說“與這個類關聯的惡意,特定於線程的特定狀態,即使我無法讓線程死掉並讓GC做它的事情?”。 我甚至可以這樣做嗎? 我的意思是,我不能安排ThreadLocal#initialValue()在過去某個時間看過ThreadLocal#initialValue()每個Thread上調用。 或者還有另一種方式嗎?

或者我應該對我的用戶說“去為自己做一個體面的類加載器和線程池實現”?

編輯#1 :澄清瞭如何在不知道線程生命週期的vanailla實用程序類中使用threadCal 編輯#2 :修復了DateSensitiveThing的線程安全問題

https://code.i-harness.com


嘆了口氣,這是個老消息

嗯,這個派對有點晚了。 2007年10月,Josh Bloch( java.lang.ThreadLocal和Doug Lea的合著者) wrote

“線程池的使用需要極其謹慎。線程池的粗略使用與線程本地的粗略使用相結合可能導致意外的對象保留,正如許多地方所指出的那樣。”

人們抱怨ThreadLocal與線程池的錯誤交互,即便如此。 但喬希做了製裁:

“性能的每線程實例.Aaron的SimpleDateFormat示例(上圖)就是這種模式的一個例子。”

一些教訓

  1. 如果將任何類型的對象放入任何對像池中,則必須提供一種“稍後”刪除它們的方法。
  2. 如果您使用ThreadLocal “匯集”,那麼您可以選擇這樣做。 要么:a)您知道在您的申請完成時,您放置值的Thread將終止; 或者b)您可以稍後安排調用ThreadLocal #set()的相同線程 ,以便在應用程序終止時調用ThreadLocal #remove()
  3. 因此,將ThreadLocal用作對像池將對應用程序和類的設計造成沉重的代價。 好處不是免費的。
  4. 因此,使用ThreadLocal可能是一個不成熟的優化,即使Joshua Bloch敦促您在“Effective Java”中考慮它。

簡而言之,決定使用ThreadLocal作為對“每個線程實例池”的快速,無競爭訪問的形式並不是一個輕率的決定。

注意:ThreadLocal除了“對像池”之外還有其他用途,這些課程不適用於ThreadLocal只是臨時設置的情況,或者存在真正的每線程狀態的情況踪跡。

圖書館實施者的後果

Threre是庫實現者的一些後果(即使這些庫是項目中的簡單實用程序類)。

或者:

  1. 你使用ThreadLocal,完全意識到你可能會因為額外的行李而“污染”長時間運行的線程。 如果要實現java.util.concurrent.ThreadLocalRandom ,則可能是合適的。 (如果你沒有在java.*實現,Tomcat可能仍會對你的庫的用戶抱怨java.* ) 有趣的是要注意java.*使用ThreadLocal技術的規則。

要么

  1. 你使用ThreadLocal,給你的類/包的客戶:a)選擇放棄優化的機會(“不要使用ThreadLocal ......我不能安排清理”); 和b)一種清理ThreadLocal資源的方法(“可以使用ThreadLocal ......我可以安排所有使用你在完成它時調用LibClass.releaseThreadLocalsForThread()線程。

但是,讓你的圖書館“難以正常使用”。

要么

  1. 您為客戶提供了提供自己的對像池實例(可能使用ThreadLocal或某種同步)的機會。 (“好的,我可以給你一個new ExpensiveObjectFactory<T>() { public T get() {...} }如果你認為它真的是必要的”。

還不錯。 如果對象真的那麼重要並且創建起來很昂貴,那麼顯式池化可能是值得的。

要么

  1. 你決定對你的應用程序來說這不值得,並找到一種不同的方法來解決問題。 那些昂貴的,可變的,非線程安全的對象會讓你感到痛苦......使用它們真的是最好的選擇嗎?

備擇方案

  1. 常規對像池,具有所有競爭同步。
  2. 不匯集對象 - 只需在本地範圍內實例化它們並稍後丟棄。
  3. 不匯集線程(除非你可以在你喜歡的時候安排清理代碼) - 不要在JaveEE容器中使用你的東西
  4. 線程池,它足夠聰明,可以清理ThreadLocals,而不會對你產生任何影響。
  5. 線程池,它在“每個應用程序”的基礎上分配線程,然後在應用程序停止時讓它們死掉。
  6. 線程池容器和應用程序之間的協議,允許註冊“應用程序關閉處理程序”,容器可以安排在已經用於服務應用程序的線程上運行......在將來的某個時候,該線程是下一個可用。 例如。 servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})

在未來的JavaEE規範中,看到最後3個項目的標準化會很高興。

Bootnote

實際上, GregorianCalendar實例化非常輕量級。 這是對setTime()的不可避免的調用,它引發了大部分工作。 它也不會在線程執行的不同點之間保持任何重要狀態。 將Calendar放入ThreadLocal不太可能給你帶來的回報超過你的成本...除非分析肯定顯示new GregorianCalendar()中的熱點。

相比之下, new SimpleDateFormat(String)很昂貴,因為它必須解析格式字符串。 解析後,對象的“狀態”對於以後由同一線程使用是很重要的。 這更合適。 但實例化一個新的可能仍然“比較便宜”,而不是給你的課程額外的責任。


在考慮了這一年之後,我認為JavaEE容器在不相關的應用程序的實例之間共享池化的工作線程是不可接受的。 這根本不是“企業”。

如果您真的要共享線程, java.lang.Thread (至少在JavaEE環境中)應該支持setContextState(int key)forgetContextState(int key) (鏡像setClasLoaderContext() )等方法,這些方法允許容器隔離特定於應用程序的ThreadLocal狀態,因為它在各種應用程序之間交換線程。

java.lang命名空間中進行此類修改之後,應用程序部署者只能採用“一個線程池,相關應用程序的一個實例”規則,並且應用程序開發人員認為'這個線程是我的,直到ThreadDeath我們做部分'。


我認為JDK的ThreadPoolExecutor可以在任務執行後進行ThreadLocals清理,但我們知道它沒有。 我認為它至少可以提供一個選項。 原因可能是因為Thread只提供對其TreadLocal映射的包私有訪問,因此ThreadPoolExecutor無法在不更改Thread的API的情況下訪問它們。

有趣的是,ThreadPoolExecutor These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals... beforeExecutionafterExecution都有受保護的方法存根,API說: These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals... These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals... 所以我可以設想一個實現ThreadLocalCleaner接口的Task和我們的自定義ThreadPoolExecutor,它在afterExecution上調用task的cleanThreadLocals();


由於線程不是由你創建的,它只是由你租用的,我認為在停止使用之前需要清理它是公平的 - 就像你在返回時填滿租來的汽車的坦克一樣。 Tomcat可以自己清理所有東西,但是它幫了你一個忙,提醒你忘記的東西。

ADD:你使用準備好的GregorianCalendar的方式是完全錯誤的:因為服務請求可以是並發的,並且沒有同步, doCalc可以採用另一個請求調用的getTime ater setTime 。 引入同步會使事情變慢,因此創建新的GregorianCalendar可能是更好的選擇。

換句話說,您的問題應該是:如何保留準備好的GregorianCalendar實例池,以便將其數量調整為請求率。 因此,至少需要一個包含該池的單例。 每個Ioc容器都有管理單例的方法,而且大多數都有現成的對像池實現。 如果您還沒有使用IoC容器,請開始使用一個(String,Guice),而不是重新發明輪子。