`

ThreadLocal的细节和设计模式

阅读更多

提要:

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

 

 

  • 大小: 24.2 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics