<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官:Java池化技術(shù)你了解多少?

          共 10626字,需瀏覽 22分鐘

           ·

          2022-07-30 15:57

          點擊關(guān)注公眾號,實用技術(shù)文章及時了解

          在我們平常的編碼中,通常會將一些對象保存起來,這主要考慮的是對象的創(chuàng)建成本。比如像線程資源、數(shù)據(jù)庫連接資源或者 TCP 連接等,這類對象的初始化通常要花費比較長的時間,如果頻繁地申請和銷毀,就會耗費大量的系統(tǒng)資源,造成不必要的性能損失。

          并且這些對象都有一個顯著的特征,就是通過輕量級的重置工作,可以循環(huán)、重復(fù)地使用。這個時候,我們就可以使用一個虛擬的池子,將這些資源保存起來,當(dāng)使用的時候,我們就從池子里快速獲取一個即可。

          在 Java 中,池化技術(shù)應(yīng)用非常廣泛,常見的就有數(shù)據(jù)庫連接池、線程池等,本文主講連接池,線程池我們將在后續(xù)的博客中進行介紹。

          公用池化包 Commons Pool 2

          簡介

          我們首先來看一下 Java 中公用的池化包 Commons Pool 2,來了解一下對象池的一般結(jié)構(gòu)。根據(jù)我們的業(yè)務(wù)需求,使用這套 API 能夠很容易實現(xiàn)對象的池化管理。

          <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
          <dependency>
              <groupId>org.apache.commons</groupId>
              <artifactId>commons-pool2</artifactId>
              <version>2.11.1</version>
          </dependency>

          GenericObjectPool 是對象池的核心類,通過傳入一個對象池的配置和一個對象的工廠,即可快速創(chuàng)建對象池。

          public GenericObjectPool
                      final PooledObjectFactory<T> factory, 
                      final GenericObjectPoolConfig<T> config)

          案例

          Redis 的常用客戶端 Jedis,就是使用 Commons Pool 管理連接池的,可以說是一個最佳實踐。下圖是 Jedis 使用工廠創(chuàng)建對象的主要代碼塊。對象工廠類最主要的方法就是makeObject,它的返回值是 PooledObject 類型,可以將對象使用 new DefaultPooledObject<>(obj) 進行簡單包裝返回。

          redis.clients.jedis.JedisFactory,使用工廠創(chuàng)建對象。

          @Override
          public PooledObject<Jedis> makeObject() throws Exception {
            Jedis jedis = null;
            try {
              jedis = new Jedis(jedisSocketFactory, clientConfig);
              //主要的耗時操作
              jedis.connect();
              //返回包裝對象
              return new DefaultPooledObject<>(jedis);
            } catch (JedisException je) {
              if (jedis != null) {
                try {
                  jedis.quit();
                } catch (RuntimeException e) {
                  logger.warn("Error while QUIT", e);
                }
                try {
                  jedis.close();
                } catch (RuntimeException e) {
                  logger.warn("Error while close", e);
                }
              }
              throw je;
            }
          }

          我們再來介紹一下對象的生成過程,如下圖,對象在進行獲取時,將首先嘗試從對象池里拿出一個,如果對象池中沒有空閑的對象,就使用工廠類提供的方法,生成一個新的。

          public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception {
              //此處省略若干行
              while (p == null) {
                  create = false;
                  //首先嘗試從池子中獲取。
                  p = idleObjects.pollFirst();
                  // 池子里獲取不到,才調(diào)用工廠內(nèi)生成新實例
                  if (p == null) {
                      p = create();
                      if (p != null) {
                          create = true;
                      }
                  }
                  //此處省略若干行
              }
              //此處省略若干行
          }

          那對象是存在什么地方的呢?這個存儲的職責(zé),就是由一個叫作 LinkedBlockingDeque的結(jié)構(gòu)來承擔(dān)的,它是一個雙向的隊列。

          接下來看一下 GenericObjectPoolConfig 的主要屬性:

          // GenericObjectPoolConfig本身的屬性
          private int maxTotal = DEFAULT_MAX_TOTAL;
          private int maxIdle = DEFAULT_MAX_IDLE;
          private int minIdle = DEFAULT_MIN_IDLE;
          // 其父類BaseObjectPoolConfig的屬性
          private boolean lifo = DEFAULT_LIFO;
          private boolean fairness = DEFAULT_FAIRNESS;
          private long maxWaitMillis = DEFAULT_MAX_WAIT_MILLIS;
          private long minEvictableIdleTimeMillis = DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
          private long evictorShutdownTimeoutMillis = DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS;
          private long softMinEvictableIdleTimeMillis = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
          private int numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
          private EvictionPolicy<T> evictionPolicy = null
          // Only 2.6.0 applications set this 
          private String evictionPolicyClassName = DEFAULT_EVICTION_POLICY_CLASS_NAME;
          private boolean testOnCreate = DEFAULT_TEST_ON_CREATE;
          private boolean testOnBorrow = DEFAULT_TEST_ON_BORROW;
          private boolean testOnReturn = DEFAULT_TEST_ON_RETURN;
          private boolean testWhileIdle = DEFAULT_TEST_WHILE_IDLE;
          private long timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
          private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;

          參數(shù)很多,要想了解參數(shù)的意義,我們首先來看一下一個池化對象在整個池子中的生命周期。如下圖所示,池子的操作主要有兩個:一個是業(yè)務(wù)線程,一個是檢測線程。

          對象池在進行初始化時,要指定三個主要的參數(shù):

          • maxTotal 對象池中管理的對象上限
          • maxIdle 最大空閑數(shù)
          • minIdle 最小空閑數(shù)

          其中 maxTotal 和業(yè)務(wù)線程有關(guān),當(dāng)業(yè)務(wù)線程想要獲取對象時,會首先檢測是否有空閑的對象。如果有,則返回一個;否則進入創(chuàng)建邏輯。此時,如果池中個數(shù)已經(jīng)達到了最大值,就會創(chuàng)建失敗,返回空對象。

          對象在獲取的時候,有一個非常重要的參數(shù),那就是最大等待時間(maxWaitMillis),這個參數(shù)對應(yīng)用方的性能影響是比較大的。該參數(shù)默認(rèn)為 -1,表示永不超時,直到有對象空閑。

          如下圖,如果對象創(chuàng)建非常緩慢或者使用非常繁忙,業(yè)務(wù)線程會持續(xù)阻塞 (blockWhenExhausted 默認(rèn)為 true),進而導(dǎo)致正常服務(wù)也不能運行。

          面試題

          一般面試官會問:你會把超時參數(shù)設(shè)置成多大呢?

          我一般都會把最大等待時間,設(shè)置成接口可以忍受的最大延遲。比如,一個正常服務(wù)響應(yīng)時間 10ms 左右,達到 1 秒鐘就會感覺到卡頓,那么這個參數(shù)設(shè)置成 500~1000ms 都是可以的。超時之后,會拋出 NoSuchElementException 異常,請求會快速失敗,不會影響其他業(yè)務(wù)線程,這種 Fail Fast 的思想,在互聯(lián)網(wǎng)應(yīng)用非常廣泛。

          帶有evcit 字樣的參數(shù),主要是處理對象逐出的。池化對象除了初始化和銷毀的時候比較昂貴,在運行時也會占用系統(tǒng)資源。比如,連接池會占用多條連接,線程池會增加調(diào)度開銷等。業(yè)務(wù)在突發(fā)流量下,會申請到超出正常情況的對象資源,放在池子中。等這些對象不再被使用,我們就需要把它清理掉。

          超出 minEvictableIdleTimeMillis 參數(shù)指定值的對象,就會被強制回收掉,這個值默認(rèn)是 30 分鐘;softMinEvictableIdleTimeMillis 參數(shù)類似,但它只有在當(dāng)前對象數(shù)量大于 minIdle 的時候才會執(zhí)行移除,所以前者的動作要更暴力一些。

          還有 4 個 test 參數(shù):testOnCreate、testOnBorrow、testOnReturntestWhileIdle,分別指定了在創(chuàng)建、獲取、歸還、空閑檢測的時候,是否對池化對象進行有效性檢測。

          開啟這些檢測,能保證資源的有效性,但它會耗費性能,所以默認(rèn)為 false。生產(chǎn)環(huán)境上,建議只將 testWhileIdle 設(shè)置為 true,并通過調(diào)整空閑檢測時間間隔(timeBetweenEvictionRunsMillis),比如 1 分鐘,來保證資源的可用性,同時也保證效率。

          JMH 測試

          使用連接池和不使用連接池,它們之間的性能差距到底有多大呢?下面是一個簡單的 JMH 測試?yán)樱ㄒ妭}庫),進行一個簡單的 set 操作,為 redis 的 key 設(shè)置一個隨機值。

          @Fork(2
          @State(Scope.Benchmark) 
          @Warmup(iterations = 5, time = 1
          @Measurement(iterations = 5, time = 1
          @BenchmarkMode(Mode.Throughput) 
          public class JedisPoolVSJedisBenchmark 
             JedisPool pool = new JedisPool("localhost"6379); 

             @Benchmark 
             public void testPool() 
                 Jedis jedis = pool.getResource(); 
                 jedis.set("a", UUID.randomUUID().toString()); 
                 jedis.close(); 
             } 

             @Benchmark 
             public void testJedis() 
                 Jedis jedis = new Jedis("localhost"6379); 
                 jedis.set("a", UUID.randomUUID().toString()); 
                 jedis.close(); 
             } 
             //此處省略若干行
          }

          將測試結(jié)果使用 meta-chart 作圖,展示結(jié)果如下圖所示,可以看到使用了連接池的方式,它的吞吐量是未使用連接池方式的 5 倍!

          數(shù)據(jù)庫連接池 HikariCP

          HikariCP 源于日語“光る”,光的意思,寓意軟件工作速度和光速一樣快,它是 SpringBoot 中默認(rèn)的數(shù)據(jù)庫連接池。數(shù)據(jù)庫是我們工作中經(jīng)常使用到的組件,針對數(shù)據(jù)庫設(shè)計的客戶端連接池是非常多的,它的設(shè)計原理與我們在本文開頭提到的基本一致,可以有效地減少數(shù)據(jù)庫連接創(chuàng)建、銷毀的資源消耗。

          同是連接池,它們的性能也是有差別的,下圖是 HikariCP 官方的一張測試圖,可以看到它優(yōu)異的性能,官方的 JMH 測試代碼見 Github。

          一般面試題是這么問的:HikariCP 為什么快呢?主要有三個方面:

          • 它使用 FastList 替代 ArrayList,通過初始化的默認(rèn)值,減少了越界檢查的操作;

          • 優(yōu)化并精簡了字節(jié)碼,通過使用 Javassist,減少了動態(tài)代理的性能損耗,比如使用 invokestatic 指令代替 invokevirtual 指令;

          • 實現(xiàn)了無鎖的 ConcurrentBag,減少了并發(fā)場景下的鎖競爭。

          HikariCP 對性能的一些優(yōu)化操作,是非常值得我們借鑒的,在之后的博客中,我們將詳細(xì)分析幾個優(yōu)化場景。

          數(shù)據(jù)庫連接池同樣面臨一個最大值(maximumPoolSize)和最小值(minimumIdle)的問題。這里同樣有一個非常高頻的面試題:你平常會把連接池設(shè)置成多大呢?

          很多同學(xué)認(rèn)為,連接池的大小設(shè)置得越大越好,有的同學(xué)甚至把這個值設(shè)置成 1000 以上,這是一種誤解。根據(jù)經(jīng)驗,數(shù)據(jù)庫連接,只需要 20~50 個就夠用了。具體的大小,要根據(jù)業(yè)務(wù)屬性進行調(diào)整,但大得離譜肯定是不合適的。

          HikariCP 官方是不推薦設(shè)置 minimumIdle 這個值的,它將被默認(rèn)設(shè)置成和 maximumPoolSize 一樣的大小。如果你的數(shù)據(jù)庫Server端連接資源空閑較大,不妨也可以去掉連接池的動態(tài)調(diào)整功能。

          另外,根據(jù)數(shù)據(jù)庫查詢和事務(wù)類型,一個應(yīng)用中是可以配置多個數(shù)據(jù)庫連接池的,這個優(yōu)化技巧很少有人知道,在此簡要描述一下。

          業(yè)務(wù)類型通常有兩種:一種需要快速的響應(yīng)時間,把數(shù)據(jù)盡快返回給用戶;另外一種是可以在后臺慢慢執(zhí)行,耗時比較長,對時效性要求不高。如果這兩種業(yè)務(wù)類型,共用一個數(shù)據(jù)庫連接池,就容易發(fā)生資源爭搶,進而影響接口響應(yīng)速度。雖然微服務(wù)能夠解決這種情況,但大多數(shù)服務(wù)是沒有這種條件的,這時就可以對連接池進行拆分。

          如圖,在同一個業(yè)務(wù)中,根據(jù)業(yè)務(wù)的屬性,我們分了兩個連接池,就是來處理這種情況的。

          HikariCP 還提到了另外一個知識點,在 JDBC4 的協(xié)議中,通過 Connection.isValid() 就可以檢測連接的有效性。這樣,我們就不用設(shè)置一大堆的 test 參數(shù)了,HikariCP 也沒有提供這樣的參數(shù)。

          結(jié)果緩存池

          到了這里你可能會發(fā)現(xiàn)池(Pool)與緩存(Cache)有許多相似之處。

          它們之間的一個共同點,就是將對象加工后,存儲在相對高速的區(qū)域。我習(xí)慣性將緩存看作是數(shù)據(jù)對象,而把池中的對象看作是執(zhí)行對象。緩存中的數(shù)據(jù)有一個命中率問題,而池中的對象一般都是對等的。

          考慮下面一個場景,jsp 提供了網(wǎng)頁的動態(tài)功能,它可以在執(zhí)行后,編譯成 class 文件,加快執(zhí)行速度;再或者,一些媒體平臺,會將熱門文章,定時轉(zhuǎn)化成靜態(tài)的 html 頁面,僅靠 nginx 的負(fù)載均衡即可應(yīng)對高并發(fā)請求(動靜分離)。

          這些時候,你很難說清楚,這是針對緩存的優(yōu)化,還是針對對象進行了池化,它們在本質(zhì)上只是保存了某個執(zhí)行步驟的結(jié)果,使得下次訪問時不需要從頭再來。我通常把這種技術(shù)叫作結(jié)果緩存池(Result Cache Pool),屬于多種優(yōu)化手段的綜合。

          小結(jié)

          下面我來簡單總結(jié)一下本文的內(nèi)容重點:

          我們從 Java 中最通用的公用池化包 Commons Pool 2 說起,介紹了它的一些實現(xiàn)細(xì)節(jié),并對一些重要參數(shù)的應(yīng)用做了講解;Jedis 就是在 Commons Pool 2 的基礎(chǔ)上封裝的,通過 JMH 測試,我們發(fā)現(xiàn)對象池化之后,有了接近 5 倍的性能提升;接下來介紹了數(shù)據(jù)庫連接池中速度速快的 HikariCP ,它在池化技術(shù)之上,又通過編碼技巧進行了進一步的性能提升,HikariCP 是我重點研究的類庫之一,我也建議你加入自己的任務(wù)清單中。

          總體來說,當(dāng)你遇到下面的場景,就可以考慮使用池化來增加系統(tǒng)性能:

          • 對象的創(chuàng)建或者銷毀,需要耗費較多的系統(tǒng)資源;
          • 對象的創(chuàng)建或者銷毀,耗時長,需要繁雜的操作和較長時間的等待;
          • 對象創(chuàng)建后,通過一些狀態(tài)重置,可被反復(fù)使用。

          將對象池化之后,只是開啟了第一步優(yōu)化。要想達到最優(yōu)性能,就不得不調(diào)整池的一些關(guān)鍵參數(shù),合理的池大小加上合理的超時時間,就可以讓池發(fā)揮更大的價值。和緩存的命中率類似,對池的監(jiān)控也是非常重要的。

          如下圖,可以看到數(shù)據(jù)庫連接池連接數(shù)長時間保持在高位不釋放,同時等待的線程數(shù)急劇增加,這就能幫我們快速定位到數(shù)據(jù)庫的事務(wù)問題。

          平常的編碼中,有很多類似的場景。比如 Http 連接池,OkhttpHttpclient 就都提供了連接池的概念,你可以類比著去分析一下,關(guān)注點也是在連接大小和超時時間上;在底層的中間件,比如 RPC,也通常使用連接池技術(shù)加速資源獲取,比如 Dubbo 連接池、 Feign 切換成 httppclient 的實現(xiàn)等技術(shù)。

          你會發(fā)現(xiàn),在不同資源層面的池化設(shè)計也是類似的。比如線程池,通過隊列對任務(wù)進行了二層緩沖,提供了多樣的拒絕策略等,線程池我們將在后續(xù)的文章中進行介紹。

          線程池的這些特性,你同樣可以借鑒到連接池技術(shù)中,用來緩解請求溢出,創(chuàng)建一些溢出策略。現(xiàn)實情況中,我們也會這么做。那么具體怎么做?有哪些做法?這部分內(nèi)容就留給大家思考了。

          來源:tomcat.blog.csdn.net/article/details/123867269

          程序汪資料鏈接

          程序汪接的7個私活都在這里,經(jīng)驗整理

          Java項目分享  最新整理全集,找項目不累啦 07版

          堪稱神級的Spring Boot手冊,從基礎(chǔ)入門到實戰(zhàn)進階

          臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!

          臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!

          字節(jié)跳動總結(jié)的設(shè)計模式 PDF 火了,完整版開放下載!


          歡迎添加程序汪個人微信 itwang009  進粉絲群或圍觀朋友圈

          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  看黄色毛片电影 | 欧洲精品口爆 | 日韩大屌在线视频 | 微信约操情人高潮在线 | 国产无码乱伦视频 |