提要:
1.都知道ThreadLocal都是线程局部变量,可以比作是个Thread->T的MAP,那么有个问题了,如果一个类维护了一个TL的局部变量,随着不同的线程访问,这个TL会变得很大么?我们需要在线程结束前调用TL.remove来删除TL变量么,如果不删除会不会空间无法释放导致OOM呢?
2.在写某些会被多线程访问的代码时,某些实例变量需要做成线程私有,那么就会出现在使用这些变量时都使用threadLocal.get(),这样的junk code,有好的代码结构可以优化他么?
============================================================================
解答:
1.其实ThreadLocal并不把变量保存在自己里,而是保存到线程t里,
摘自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
ThreadLocal.ThreadLocalMap threadLocals = null;
因此,TheadLocal本身并不保存变量,而是委托线程去保存。而这里又有个问题,这样做可以保证ThreadLocal不会受到线程的生命周期影响,我们也不需要显示remove。那么这里又有个问题了,线程Map里始终维护了TheadLocal->T的变量,如果维护ThreadLocal的对象被GC掉,线程本地变量里的ThreadLocal变量却依然被引用,并不会被gc。这样会不会有内存泄露呢?
实际上是不会的,这个归功于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