1. 程式人生 > >Java中如何限制方法訪問的併發數

Java中如何限制方法訪問的併發數

併發程式設計一直是Java基礎中的高地,但很多隻要有兩三年Java基礎的工程師,會在簡歷中很明確的標明“熟悉多執行緒程式設計、有高併發程式設計經驗”,來凸顯自己程式設計能力有多厲害,但實際上可能只是看了《Java Concurrency in Practice》的幾個章節而已。其實對很多業務研發工程師來說,高併發程式設計經驗並不是必備的核心競爭力之一,很多需要加鎖或者統計的場景,大都可以交給外部系統如Redis來做,即多執行緒併發場景的轉移。
那麼作為面試官,如何簡單快速考察面試者的多執行緒程式設計能力?可能方法有很多,筆者喜歡用到的一個題目如下:“對於單個Java應用,我們如何限制其中某個方法methodA()被呼叫的併發數不能超過100,如果超過100,超出的請求就直接返回null或拋異常”。筆者會要求面試者在白板上寫出相應程式碼片段(當然並不會要求面試者一定要完整寫出用到的類名或方法名)。
這個題看起來並不難,但仔細想想也不那麼簡單,大約30%的面試者會給出Semaphore的解決方案,例如:

    private static Semaphore semaphore = new Semaphore(100);

    public static Integer methodA() {
        if(!semaphore.tryAcquire()) {
            return null;
        }

        try {
        // TODO 方法中的業務邏輯
        } finally {
            semaphore.release();
        }
    }

Semaphore訊號量是一種比較完美的解決方案,程式碼簡單而高效,一旦面試者給出這個方案,我們可以順便考察下訊號量相關的知識點。

但還有沒有其他的思路或解決方案呢?筆者接觸到的面試者,還給出過執行緒池的方案,因為最高併發數是100,那麼表明同時最多隻能有100個執行緒訪問該方法,面試者一般會這麼寫:

    private final static ExecutorService pool = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES, new SynchronousQueue<>());

    public static Integer methodAWrapper() {
        try {
            Future<Integer> future = pool.submit(() -> methodA());
            return future.get();
        } catch (Exception e) {
          return null;
        }
    }

    public static Integer methodA() {
        // TODO 方法中的業務邏輯
    }

其實嚴格意義來講,執行緒池的這種方案無法完美做到“如果超過100,超出的請求就直接返回null或拋異常”,哪怕是使用SynchronousQueue佇列。但沒關係,最關鍵的考察點並不在超限如何返回。當面試者寫出這種方案,也可以順便考察下執行緒池相關的知識點。
大多數面試者想到的是寫個計數器,例如:

    private static AtomicInteger counter = new AtomicInteger(0);

    public static Integer methodA() {

        int value = counter.incrementAndGet();
        if(value > 100) {
            return null;
        }

        try {
            // TODO 方法中的業務邏輯
        } finally {
            counter.decrementAndGet();
        }
    }

於是我一般會問,為啥是選擇incrementAndGet方法,而不是選擇getAndIncrement?仔細看看會不會有其他問題?大多數面試者經過提示都能發現這裡get、incr和比較不是原子操作,會產生“競態條件”(race condition)。我們先把這種計數器方案叫做方案A,像想到計數器方案的面試者,也有小部分會這樣寫:

    private static AtomicInteger counter = new AtomicInteger(0);

    public static Integer methodA() {

        int value = counter.get();
        if(value > 100) {
            return null;
        }
        
        counter.incrementAndGet();

        try {
            // TODO 方法中的業務邏輯
        } finally {
            counter.decrementAndGet();
        }
    }

我們把這種計數器的實現方案叫做方案B,這兩種方案都會有“競態條件”的問題,但產生的現象不一樣。

對於方案A,在極端高併發的情況下,每個呼叫methodA的請求,都會對計數器進行+1,即使我們在finally對計數器進行了-1,也阻止value的值繼續上漲,導致遠大於100,得到的結果是所有請求沒機會執行業務邏輯,即“餓死”現象。

對於方案B,由於是活的執行業務邏輯的許可後再進行的+1操作,很顯然在高併發情況下會導致執行業務邏輯的執行緒數超過100。

很多Java老司機覺得自己不會犯這種錯誤,但實際上,最近阿里的開源專案Sentinel就有類似方案A的問題,Sentinel中使用責任鏈的模式,來對每筆呼叫進行統計、攔截,這裡給出構建責任鏈DefaultSlotsChainBuilder類的程式碼片段:

public class DefaultSlotsChainBuilder implements SlotsChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        chain.addLast(new NodeSelectorSlot());
        chain.addLast(new ClusterBuilderSlot());
        chain.addLast(new LogSlot());
        chain.addLast(new StatisticSlot()); // 統計資料,類似於上文提到的incrementAndGet
        chain.addLast(new SystemSlot());    // 檢查是否超過閾值,類似於上文提到的value > 100
        chain.addLast(new AuthoritySlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());

        return chain;
    }
}

而在分散式服務框架Dubbo的早期版本(例如2.5.3),在對Provider提供執行緒限制保護的executes
(例如:<dubbo:service interface="com.manzhizhen.dubbo.server.service.Dubbo2Service"
ref="dubbo2Service" version="1.0.0" timeout="3000" executes="10" />)的實現方案,就踩了上述方案B的坑。

迴歸正題,也有面試者給出了阻塞佇列的方案,即:

    private static BlockingQueue<Integer> reqQueue = new ArrayBlockingQueue<>(100);

    public static Integer methodA() {

        if(!reqQueue.offer()) {
            return null;
        }

        try {
            // TODO 方法中的業務邏輯
        } finally {
            reqQueue.poll();
        }
    }

阻塞佇列的方案也是不錯的,程式碼簡單,效率也還行。

當然,也有面試者說用Hystrix,嗯嗯,這也是可以的。

做個總結吧,最優方案當然是使用Semaphore,但Semaphore無法統計實際高峰期時的併發量有多少(很多場景需要通過實際最高的併發值來優化我們系統),文章很短,希望對大家能有所幫助。

相關推薦

Java如何限制方法訪問併發

併發程式設計一直是Java基礎中的高地,但很多隻要有兩三年Java基礎的工程師,會在簡歷中很明確的標明“熟悉多執行緒程式設計、有高併發程式設計經驗”,來凸顯自己程式設計能力有多厲害,但實際上可能只是看了《Java Concurrency in Practice》的幾個章節而已

WebForm.aspx 頁面通過 AJAX 訪問WebForm.aspx.cs類方法,獲取據(轉)

html ref doc tran jquery helper event query sender WebForm.aspx 頁面通過 AJAX 訪問WebForm.aspx.cs類中的方法,獲取數據 WebForm1.aspx 頁面 (原生AJAX請求,寫法一) &l

JAVA限制接口流量的方法

format 阻塞 java create orm ase test iter() nts   JAVA中限制接口流量可以通過Guava的RateLimiter類或者JDK自帶的Semaphore類來實現,兩者有點類似,但是也有區別,要根據實際情況使用。簡單來說, Rat

java方法是基本類型和引用類型的區別

關系 傳遞 類型 參數 變量 找到 外部 內存地址 chan 方法參數是基本類型時,傳遞的是值。 方法參數是引用類型時,傳遞的是內存地址值 當參數是基本類型時,在調用方法時將值傳遞到方法中,運行方法,運行結束方法退出,對原本main中定義的變量沒有任何操作(方法中沒有re

靜態方法只允許訪問靜態據,那麽,如何在靜態方法訪問類的實例成員(即沒有附加static關鍵字的字段或方法)?

static關鍵字 實例成員 clas 靜態 image eth sys 靜態方法 http package test.two; public class jingtaihanshu { int x = 3; static int y

JNI訪問Java方法

#include <stdio.h> #include "com_ican_twy_JniTest.h" #include <Windows.h> #include <string.h> //訪問非靜態方法 JNIEXPORT void JNICA

java使用代理訪問網路的幾種方法

有些時候我們的網路不能直接連線到外網, 需要使用http或是https或是socket代理來連線到外網, 這裡是java使用代理連線到外網的一些方法, 希望對你的程式有用.方法一使用系統屬性來完成代理設定, 這種方法比較簡單, 但是不能對單獨的連線來設定代理:

java方法

允許 多個 != 返回值 如果 系統底層 所在 格式 影響 方法的概念: 簡單的說方法就是完成特定功能的代碼塊 使用方法的好處: 降低程序的冗余度 , 便於後期維護 , 提高封裝性 方法的定義格式修飾符 返回值類型 方法名(數據類型 參數名1,數據類型 參數名2,.

java四種訪問修飾符

pub oid 默認 成員變量 修飾 對象 fault 其中 () Java中的四種訪問修飾符:public、protected、default(無修飾符,默認)、private。 四種修飾符可修飾的成分(類、方法、成員變量) public protect

JavaSemaphore(信號量) 據庫連接池

each jdb 同步方法 [] pop 線程 emp use builder 計數信號量用來控制同時訪問某個特定資源的操作數或同時執行某個指定操作的數量 A counting semaphore.Conceptually, a semaphore maintains a

java容器類型的

style 修改 add 對象 容器類 round fcc element pub StingBiulder,數組,ArrayList StringBiulder:只針對字符串的長度可變。 數組:數組的長度是固定不變的。 ArrayList:長度可變。 構造方法:   Ar

javanative方法的使用

cat 語言 pub jvm 配置 his fde 應用程序 nic native關鍵字說明其修飾的方法是一個原生態方法,方法對應的實現不是在當前文件,而是在用其他語言(如C和C++)實現的文件中。Java語言本身不能對操作系統底層進行訪問和操作,但是可以通過JNI接口調用

Java通過方法創建一個http連接並請求

servlet 讀取 mage equals contex method ade temp password 1.Java代碼創建一個連接並請求該連接返回的數據 doGet()方法,execute()方法中調用 package demo2.x.com;

pythonprint()函的“,”與javaSystem.out.print()函的“+”

兩個 java 新特性 highlight 不同 連接 .py sys pre python中的print()函數和java中的System.out.print()函數都有著打印字符串的功能。 python中: print("hello,world!") 輸出結果為:h

java 的String類型據添加雙引號

str odi strong string類 version nco color font 添加 轉義符 \ 加上引號 \" <?xml version="1.0"encoding="GBK"?>String temp = "<?xml versio

C#通過反射獲取類方法和參個數,反射調用方法帶參

new [] 反射 電腦 ram col sta body create using System; using System.Reflection; namespace ConsoleApp2 { class Program { sta

JAVAJDBC連接Mysql據庫簡單測試

batch 數據庫 count() found rman too zone close static 一、引用庫   maven庫:mysql:mysql-connector-java:6.0.6 二、SDK環境   JAVA JDK10 三、測試代碼   

java四種訪問修飾符區別及詳解全過程

HP dnv ax1 pci gda fmm utc dos rdp 客戶端程序員:即在其應用中使用數據類型的類消費者,他的目標是收集各種用來實現快速應用開發的類。   類創建者:即創建新數據類型的程序員,目標是構建類。     訪問控制存在的原因:a、讓客戶端程序員無法觸

Java方法覆蓋(Overriding)和方法重載(Overloading)是什麽意思?

沒有 編譯錯誤 列表 限制 值類型 條件 IV 什麽 java   方法覆蓋也稱為重寫,重寫即子類重新定義了父類的方法。 重寫:   1、重寫的方法必須與原方法有相同的方法名、參數列表和返回值類型(Java SE5之後返回值類型可以是其類型的子類型)   2、被重寫的方法不

Java構造方法、實例方法、類方法的區別

而在 class syn 此外 alt 創建 子類 語法規則 自身 1. 構造方法 構造方法負責對象的初始化工作,為實例變量賦予合適的初始值。必須滿足以下的語法規則: 方法名與類名相同; 不要返回類型(例如return、void等); 不能被