ThreadLocal的細節和設計模式

提要: java

1.都知道ThreadLocal都是線程局部變量,能夠比做是個Thread->T的MAP,那麼有個問題了,若是一個類維護了一個TL的局部變量,隨着不一樣的線程訪問,這個TL會變得很大麼?咱們須要在線程結束前調用TL.remove來刪除TL變量麼,若是不刪除會不會空間沒法釋放致使OOM呢? sql

2.在寫某些會被多線程訪問的代碼時,某些實例變量須要作成線程私有,那麼就會出如今使用這些變量時都使用threadLocal.get(),這樣的junk code,有好的代碼結構能夠優化他麼? 數據庫

 

============================================================================ 設計模式

解答: 安全

 

1.其實ThreadLocal並不把變量保存在本身裏,而是保存到線程t裏, session

   摘自ThreadLocal 多線程

Java代碼 複製代碼 
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

     

Java代碼 複製代碼  收藏代碼
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

   

Java代碼 複製代碼  收藏代碼
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

Java代碼 複製代碼  收藏代碼
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

 

Java代碼 複製代碼  收藏代碼
public class SqlMapSessionImpl implements SqlMapSession {

  protected SqlMapExecutorDelegate delegate;
  protected SessionScope sessionScope;
  protected boolean closed;
...
}
 

 

摘自SessionScope

 

Java代碼 複製代碼  收藏代碼
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

 

Java代碼 複製代碼  收藏代碼
public Object queryForObject(SessionScope sessionScope, String id, Object paramObject) throws SQLException {
    return queryForObject(sessionScope, id, paramObject, null);
  }
 所以經過這樣的回調,巧妙的解決了junkcode,讓代碼更加清晰。

 

 

================================================================================

模仿一下

 

咱們須要抽象一個數據源DataProvider,DataProvider能夠有不少實現類,例如FileDataProvider,MysqlDataProvider,這個DataProvider數據的獲取方式變成Iterator的方式。

 

Java代碼 複製代碼  收藏代碼
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的寫法(代碼只截取部分,有個意思)

Java代碼 複製代碼  收藏代碼
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)

Java代碼 複製代碼  收藏代碼
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

 

Java代碼 複製代碼  收藏代碼
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)

Java代碼 複製代碼  收藏代碼
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變量進行調用。

Java代碼 複製代碼  收藏代碼
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

相關文章
相關標籤/搜索