並行設計模式--Thread Specific Storge模式

多線程的不安全在於共享了變量實例,因此Thread Specific Storge模式的思路是把變量與單一線程綁定,那麼就不存在共享,自然就避免了加鎖消耗以及其他高併發所需要的策略。 Thread Specific Storge一般有兩種策略:1. ThreadLocal策略,也就是與當前線程實例綁定。 2. 借用模式對象池策略,由對象池進行管理,控制對象只能同一時間被一個單線程使用。

ThreadLocal設計與應用

ThreadLocal策略

ThreadLocal策略比較簡單,其原理是在Thread類中私有化一個屬性變量java.lang.ThreadLocal.ThreadLocalMap,該Map存儲着與當前線程綁定的相關變量。一個ThreadLocal的基本使用如下:

private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> {
  System.out.println("t2觸發初始化");
  return "threadLocal2";
});

public static void main(String[] args) {
  threadLocal1.set("threadLocal1");
  // 觸發t2的初始化操作,並返回t2的值
  System.out.println(threadLocal2.get());
  // 得到t1的值
  System.out.println(threadLocal1.get());
}

上述代碼在內存中的結構如下,其對象本身ThreadLocal會作爲ThreadLocalMap的key存儲。

既然是Map結構,那麼會有幾個問題: ThreadLocalMap是如何解決hash衝突的? ThreadLocalMap是一個簡單的Map實現,其沒有構造對應的衝突鏈,而是當遇到衝突時順延到下一個槽位,也就是常說的開放地址法,具體邏輯可以在java.lang.ThreadLocal.ThreadLocalMap#set中看到。

ThreadLocalMap的擴容機制是什麼? 擴容要提到負載因子,其負載因子計算爲threshold = len * 2 / 3,當元素個數大於該值時會觸發擴容,擴容操作把之前元素拷貝進來後替換掉之前的數組。

使用ThreadLocal複用對象

在Java中有一些線程不安全的對象需要被頻繁創建,比如StringBuilder,那麼就可以利用ThreadLocal複用這些對象。 在BigDecimal中有如下類,其本身是包裝了StringBuilder,並提供重置方法。

static class StringBuilderHelper {
      final StringBuilder sb;    // Placeholder for BigDecimal string

      StringBuilderHelper() {
          sb = new StringBuilder();
      }

      // Accessors.
      StringBuilder getStringBuilder() {
          sb.setLength(0);
          return sb;
      }
}

在使用前需要把該類使用ThreadLocal包裹

private static final ThreadLocal<StringBuilderHelper>
    threadLocalStringBuilderHelper = new ThreadLocal<StringBuilderHelper>() {
    @Override
    protected StringBuilderHelper initialValue() {
        return new StringBuilderHelper();
    }
};

利用ThreadLocal這樣設計解決了線程不安全的問題,然後提高對象複用性,尤其是大字符串的拼接會讓StringBuilder不停的擴容,頻繁創建對性能影響還是挺大的。

對象池策略

借還策略下的對象池模式也經常被用來解決非線程安全的類在多線程環境下的使用,所謂的借還模式如下所示

public static void main(String[] args) throws Exception {
  GenericObjectPool<SimpleDateFormat> pool = new GenericObjectPool<>(new SimplePoolObjectFactory());
  // 借對象,如果池中沒有對象則主動去創建然後再返回
  SimpleDateFormat dateFormat = pool.borrowObject();
  try {
    System.out.println(dateFormat.format(new Date()));
  } finally {
    // 用完釋放,返回池中
    pool.returnObject(dateFormat);
  }
}

上述代碼中pool是一個多線程可以共享的實例,其必須保證對象的借出與歸還的原子性,當對象被借出時那麼對象就與當前線程綁定了起來,對象池保證了其他線程操作時不會再次獲取到該實例,因此對象不存在共享,也就不存在多線程併發問題。

對象池的控制原理

apache common pool2爲例,其GenericObjectPool的實現原理主要是ConcurrentMapLinkedBlockingDeque(非JDK版本),如下圖所示:

對象池本質上是一個集生產與消費,且支持可回收的工廠。生產則對應着用戶獲取對象時,如果當前idleObjects中不存在則主動去創建對象,消費則對應着Client的borrowObject操作,可回收則是returnObject還回池中操作。作爲工廠其由責任對生產出的產品個數與消費能力的變化進行調整,因此還需要有一個後臺線程做這件事,對應着是org.apache.commons.pool2.impl.BaseGenericObjectPool.Evictor類定時清理策略。

對應的核心操作解析: borrowObject操作 borrowObject操作主要是從對象池也就是上述的LinkedBlockingDeque<PooledObject<T>> idleObjects中取出實體,當實體不存在的時候要主動去創建,

// 取出隊首元素,該方法並不會產生阻塞
p = idleObjects.pollFirst();
if (p == null) {
    // 沒有獲取到對應元素則主動創建
    p = create();
    if (p != null) {
        create = true;
    }
}

如果上述過程中仍然沒有獲取到對象,則根據配置選擇是否阻塞當前調用,阻塞則使用BlockingDeque的take操作或者poll(time)操作

if (p == null) {
    // 根據最大等待獲取時間採取不同的等待策略
    if (borrowMaxWaitMillis < 0) {
        // take操作無限等待
        p = idleObjects.takeFirst(); 
    } else {
        // 有限時間的等待
        p = idleObjects.pollFirst(borrowMaxWaitMillis, 
                TimeUnit.MILLISECONDS);
    }
}

returnObject操作 returnObject操作主要是把使用過的對象還回池中,反映到操作上就是把一個對象放入LinkedBlockingDeque<PooledObject<T>> idleObjects的隊首或者隊尾,當可用對象過多,則是使用直接銷燬對象的策略。

// 最大可用對象數量
final int maxIdleSave = getMaxIdle();
// 池關閉或者池已經滿了則主動銷燬掉釋放過來的對象
if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
    try {
        destroy(p);
    } catch (final Exception e) {
        swallowException(e);
    }
} else {
    // 根據隊列配置選擇頭插法或者尾插法
    if (getLifo()) {
        idleObjects.addFirst(p);
    } else {
        idleObjects.addLast(p);
    }
    // 關閉則清理池
    if (isClosed()) {
        clear();
    }
}

removeAbandoned操作 removeAbandoned主要應對內存中對象實例進行清理,當Client使用完對象卻沒有還回,此時該對象就應該被清理掉。 清理策略主要針對被借出的對象,對象被借出時該對象上有對應的時間標記,因此遍歷池中所有對象,清除狀態爲被借出,並且借出時間大於指定時間的對象即可。

// 獲取全部對象的迭代器
final Iterator<PooledObject<T>> it = allObjects.values().iterator();
// 遍歷池中產生的所有對象
while (it.hasNext()) {
    final PooledObject<T> pooledObject = it.next();
    synchronized (pooledObject) {
        // 遍歷池中已被借出,並且借出時間大於指定時間的對象
        if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
                pooledObject.getLastUsedTime() <= timeout) {
            // 標記爲待清理
            pooledObject.markAbandoned();
            // 清理對象列表
            remove.add(pooledObject);
        }
    }
}

Evictor驅逐線程 Evictor是一個TimerTask的定時任務,其主要功能是清理可用對象數量,保證idleObjects中的數量最小可用。 Evictor對應的操作在org.apache.commons.pool2.impl.GenericObjectPool#evict方法中,其邏輯是遍歷idleObjects中可用對象,使用策略接口EvictionPolicy判斷是否符合銷燬條件,符合則銷燬,邏輯比較簡單。

EvictionPolicy的默認策略爲對象在idleObjects的存活時間大於配置的清理時間,並且當前idleObjects的數量對象大於最小可用對象配置的情況下進行回收。

@Override
public boolean evict(final EvictionConfig config, final PooledObject<T> underTest,
        final int idleCount) {
    // 清理策略是根據當前對象的空閒時間與配置空閒時間比較
    if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
            config.getMinIdle() < idleCount) ||
            config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
        return true;
    }
    return false;
}

總結

Thread Specific Storge模式的本質是不共享數據,從而解決了多線程下競爭的問題,一般情況下對於構造成本比較小的數據直接使用ThreadLocal,需要時則直接創建一個與當前線程所綁定。構造成本比較大的對象比如各種連接池則使用對象池方式。

參考

Java多線程編程實戰指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章