提要: java
1.都知道ThreadLocal都是線程局部變量,能夠比做是個Thread->T的MAP,那麼有個問題了,若是一個類維護了一個TL的局部變量,隨着不一樣的線程訪問,這個TL會變得很大麼?咱們須要在線程結束前調用TL.remove來刪除TL變量麼,若是不刪除會不會空間沒法釋放致使OOM呢? sql
2.在寫某些會被多線程訪問的代碼時,某些實例變量須要作成線程私有,那麼就會出如今使用這些變量時都使用threadLocal.get(),這樣的junk code,有好的代碼結構能夠優化他麼? 數據庫
============================================================================ 設計模式
解答: 安全
1.其實ThreadLocal並不把變量保存在本身裏,而是保存到線程t裏, session
摘自ThreadLocal 多線程
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
摘自Thread maven
ThreadLocal.ThreadLocalMap threadLocals = null;
所以,TheadLocal自己並不保存變量,而是委託線程去保存。而這裏又有個問題,這樣作能夠保證ThreadLocal不會受到線程的生命週期影響,咱們也不須要顯示remove。那麼這裏又有個問題了,線程Map裏始終維護了TheadLocal->T的變量,若是維護ThreadLocal的對象被GC掉,線程本地變量裏的ThreadLocal變量卻依然被引用,並不會被gc。這樣會不會有內存泄露呢? ide
其實是不會的,這個歸功於WeakReference的使用 函數
WeakReference是一種引用容器,他雖然會維持R的引用,可是若是除了WeakReference外沒有其餘Object引用R,那麼weakreference會在R被GC時,刪除他。
因此回到ThreadLocal,若是已經沒有對象引用ThreadLocal,那麼線程中的ThreadLocaMap就會踢掉這個被回收的ThreadLocal。
詳見Thread.ThreadLocalMap
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } ..... /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; }
能夠看到Entry是一個TheadLocal的弱引用,其成員變量保存了映射的value,Entry[]是線程的全部ThreadLocal變量。相似一個LinkedHashMap。
2.如何避免使用ThreadLocal的線程安全類滿屏的t.get()這樣的junkcode
在Ibatis中sqlMapClient配置爲一個bean,可是每一個線程在使用smc時總有本身的局部變量,例如Transaction,這樣就跟咱們的場景同樣。可是咱們並無看到sqlMapClient使用t.get()這樣的代碼。他是怎麼作到的呢:
如圖,sqlMapClient是全局共享的,他的queryForObject實際上是委託一個sqlMapSessionImpl實現的,
sqlMapSessionImpl是線程成私有的,保存在smc的TheadLocal變量裏的。
摘自SqlMapClient
protected SqlMapSessionImpl getLocalSqlMapSession() { SqlMapSessionImpl sqlMapSession = (SqlMapSessionImpl) localSqlMapSession.get(); if (sqlMapSession == null || sqlMapSession.isClosed()) { sqlMapSession = new SqlMapSessionImpl(this); localSqlMapSession.set(sqlMapSession); } return sqlMapSession; }
而sqlMapSessionImpl做爲一個實例變量,他是不可能完成數據庫操做的,他是委託了SqlMapExecutorDelegate的方法,SMED是在sqlMapClient裏的變量,能夠理解爲是變相的回調操做。而SMSI裏維護了一個SessionScope,這個是一個線程上下文裏的變量,
摘自SqlMapSessionImpl
public class SqlMapSessionImpl implements SqlMapSession { protected SqlMapExecutorDelegate delegate; protected SessionScope sessionScope; protected boolean closed; ... }
摘自SessionScope
public class SessionScope { private static long nextId; private long id; // Used by Any private SqlMapClient sqlMapClient; private SqlMapExecutor sqlMapExecutor; private SqlMapTransactionManager sqlMapTxMgr; private int requestStackDepth; // Used by TransactionManager private Transaction transaction; private TransactionState transactionState; ... }SqlMapExecutorDelegate纔是數據庫執行的真正地方,那麼既然要實現線程安全的操做,勢必有個SqlMapExecutorDelegate不能再維護TheadLocal變量了,所以SqlMapExecutorDelegate的操做都帶有SessionScope這個入參。
摘自SqlMapExecutorEelegate
public Object queryForObject(SessionScope sessionScope, String id, Object paramObject) throws SQLException { return queryForObject(sessionScope, id, paramObject, null); }所以經過這樣的回調,巧妙的解決了junkcode,讓代碼更加清晰。
================================================================================
模仿一下
咱們須要抽象一個數據源DataProvider,DataProvider能夠有不少實現類,例如FileDataProvider,MysqlDataProvider,這個DataProvider數據的獲取方式變成Iterator的方式。
public abstract class DataProvider implements Iterator<Row> { public abstract Set<String> listFieldsName(); public abstract Long size(); public abstract void setPath(String path); public String getName() { return this.getClass().toString(); } public abstract String getDesc(); public void remove() { throw new UnsupportedOperationException(); } }
那麼junk-code的寫法(代碼只截取部分,有個意思)
import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.*; @Component public class FileDataProvider extends DataProvider { private ThreadLocal<ReaderInfo> readerInfo = new ThreadLocal<ReaderInfo>(); class ReaderInfo { public String csvFullPath = null; public BufferedReader reader = null; public File file = null; public boolean hasMore = false; public boolean isColumn = true; public List<String> columnNameList = null; } public boolean hasNext() { try { if (readerInfo.get().file == null) readerInfo.get().file = new File(readerInfo.get().csvFullPath); if (readerInfo.get().reader == null) readerInfo.get().reader = new BufferedReader(new FileReader(readerInfo.get().file)); if (readerInfo.get().reader.ready()) { readerInfo.get().hasMore = true; if (readerInfo.get().isColumn) { readerInfo.get().isColumn = false; readerInfo.get().columnNameList = new ArrayList<String>(); String line = readerInfo.get().reader.readLine(); StringTokenizer st = new StringTokenizer(line, ","); while (st.hasMoreTokens()) { readerInfo.get().columnNameList.add(st.nextToken()); } } return true; } else { readerInfo.get().hasMore = false; readerInfo.get().reader.close(); readerInfo.get().reader = null; return false; } } catch (IOException e) { throw new RuntimeException(e); } } ... }
在hashNext中須要訪問readerInfo裏的path和file都用到了readerInfo.get()。
-------------如下是模仿sqlMapClient作的改造--------------------
1.把readerfino獨立出來(變個名字sessionScope)
public class SessionScope { public String csvFullPath = null; public BufferedReader reader = null; public File file = null; public boolean hasMore = false; public boolean isColumn = true; public List<String> columnNameList = null; }
2.編寫一個使用sessionScope做爲入參的讀取器FileProviderExecutor
import com.alibaba.cainiao.hellomaven.impl.Field; import com.alibaba.cainiao.hellomaven.impl.Row; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.StringTokenizer; public class FileProviderExecutor { public boolean hasNext(SessionScope sessionScope) { try { if (sessionScope.file == null) sessionScope.file = new File(sessionScope.csvFullPath); if (sessionScope.reader == null) sessionScope.reader = new BufferedReader(new FileReader(sessionScope.file)); if (sessionScope.reader.ready()) { sessionScope.hasMore = true; if (sessionScope.isColumn) { sessionScope.isColumn = false; sessionScope.columnNameList = new ArrayList<String>(); String line = sessionScope.reader.readLine(); StringTokenizer st = new StringTokenizer(line, ","); while (st.hasMoreTokens()) { sessionScope.columnNameList.add(st.nextToken()); } } return true; } else { sessionScope.hasMore = false; sessionScope.reader.close(); sessionScope.reader = null; return false; } } catch (IOException e) { throw new RuntimeException(e); } } ... }
3.寫一個FileDataSessionProvider,維護sessionScope,和FileProviderExecutor,FPE使用構造函數傳遞進來(固然也能夠吧FPE換成FileDataProvider)
public class FileDataSessionProvider { private SessionScope sessionScope; private FileProviderExecutor fileProviderExecutor = null; public FileDataSessionProvider(FileProviderExecutor executor) { this.fileProviderExecutor = executor ; sessionScope = new SessionScope(); } public Set<String> listFieldsName() { return fileProviderExecutor.listFieldsName(sessionScope); } public Long size() { return fileProviderExecutor.size(sessionScope); } public void setPath(String path) { this.sessionScope.csvFullPath = path; } public boolean hasNext() { return fileProviderExecutor.hasNext(sessionScope); } public Row next() { return fileProviderExecutor.next(sessionScope); } }
全部的函數委託Executor執行,傳入參數sessionScope
4.最後,封裝FileDataProvider,維護一個TheadLocal變量,裏面存放FileDataSessionProvider,實現DataProvider,獲取TheadLocal變量進行調用。
public class FileDataProvider extends DataProvider { FileProviderExecutor fileProviderExecutor = new FileProviderExecutor(); private ThreadLocal<FileDataSessionProvider> fileDataSessionProviderThreadLocal = new ThreadLocal<FileDataSessionProvider>(); private FileDataSessionProvider getLocalSessionProvider() { FileDataSessionProvider fileDataSessionProvider = fileDataSessionProviderThreadLocal.get(); if (fileDataSessionProvider == null) { fileDataSessionProvider = new FileDataSessionProvider(this.fileProviderExecutor); fileDataSessionProviderThreadLocal.set(fileDataSessionProvider); } return fileDataSessionProvider; } @Override public Set<String> listFieldsName() { return this.getLocalSessionProvider().listFieldsName(); } @Override public Long size() { return this.getLocalSessionProvider().size(); } @Override public void setPath(String path) { this.getLocalSessionProvider().setPath(path); } @Override public String getDesc() { return "測試下"; } public boolean hasNext() { return this.getLocalSessionProvider().hasNext(); } public Row next() { return this.getLocalSessionProvider().next(); } public FileProviderExecutor getFileProviderExecutor() { return fileProviderExecutor; } public void setFileProviderExecutor(FileProviderExecutor fileProviderExecutor) { this.fileProviderExecutor = fileProviderExecutor; } }
在getLocalSessionProvider()中,若是該線程沒有訪問過就建立sessionProvider,傳入Executor。這樣就完成了封裝,繞過了對ThreadLocal變量內部成員變量的反覆讀取。這樣的設計模式讓代碼層次更清晰,可是呢缺增長了代碼量和理解難度,因此能夠看狀況選擇使用。
不過這樣的寫法在設計模式裏能找到對應的類型麼?
FINISH