天天看點

面試官:說說項目中 Java 多線程一般都用于哪些應用場景?

面試官:說說項目中 Java 多線程一般都用于哪些應用場景?

>>号外:關注“Java精選”公衆号,菜單欄->聚合->幹貨分享,回複關鍵詞領取視訊資料、開源項目。

多線程使用的主要目的在于:

1、吞吐量:你做WEB,容器幫你做了多線程,但是他隻能幫你做請求層面的。簡單的說,可能就是一個請求一個線程。或多個請求一個線程。如果是單線程,那同時隻能處理一個使用者的請求。

2、伸縮性:也就是說,你可以通過增加CPU核數來提升性能。如果是單線程,那程式執行到死也就利用了單核,肯定沒辦法通過增加CPU核數來提升性能。

鑒于你是做WEB的,第1點可能你幾乎不涉及。那這裡我就講第二點吧。

--舉個簡單的例子:

假設有個請求,這個請求服務端的處理需要執行3個很緩慢的IO操作(比如資料庫查詢或檔案查詢),那麼正常的順序可能是(括号裡面代表執行時間):

a、讀取檔案1  (10ms)

b、處理1的資料(1ms)

c、讀取檔案2  (10ms)

d、處理2的資料(1ms)

e、讀取檔案3  (10ms)

f、處理3的資料(1ms)

g、整合1、2、3的資料結果 (1ms)

單線程總共就需要34ms。

那如果你在這個請求内,把ab、cd、ef分别分給3個線程去做,就隻需要12ms了。

是以多線程不是沒怎麼用,而是,你平常要善于發現一些可優化的點。然後評估方案是否應該使用。

假設還是上面那個相同的問題:但是每個步驟的執行時間不一樣了。

a、讀取檔案1  (1ms)

b、處理1的資料(1ms)

c、讀取檔案2  (1ms)

d、處理2的資料(1ms)

e、讀取檔案3  (28ms)

f、處理3的資料(1ms)

g、整合1、2、3的資料結果 (1ms)

單線程總共就需要34ms。

如果還是按上面的劃分方案(上面方案和木桶原理一樣,耗時取決于最慢的那個線程的執行速度),在這個例子中是第三個線程,執行29ms。那麼最後這個請求耗時是30ms。比起不用單線程,就節省了4ms。但是有可能線程排程切換也要花費個1、2ms。是以,這個方案顯得優勢就不明顯了,還帶來程式複雜度提升。不太值得。

那麼現在優化的點,就不是第一個例子那樣的任務分割多線程完成。而是優化檔案3的讀取速度。

可能是采用緩存和減少一些重複讀取。

首先,假設有一種情況,所有使用者都請求這個請求,那其實相當于所有使用者都需要讀取檔案3。那你想想,100個人進行了這個請求,相當于你花在讀取這個檔案上的時間就是28×100=2800ms了。那麼,如果你把檔案緩存起來,那隻要第一個使用者的請求讀取了,第二個使用者不需要讀取了,從記憶體取是很快速的,可能1ms都不到。

僞代碼:

public class MyServlet extends Servlet{
    private static Map<String, String> fileName2Data = new HashMap<String, String>();
    private void processFile3(String fName){
        String data = fileName2Data.get(fName);
        if(data==null){
            data = readFromFile(fName);    //耗時28ms
            fileName2Data.put(fName, data);
        }
        //process with data
    }
}
           

看起來好像還不錯,建立一個檔案名和檔案資料的映射。如果讀取一個map中已經存在的資料,那麼就不不用讀取檔案了。

可是問題在于,Servlet是并發,上面會導緻一個很嚴重的問題,死循環。因為,HashMap在并發修改的時候,可能是導緻循環連結清單的構成!!!(具體你可以自行閱讀HashMap源碼)如果你沒接觸過多線程,可能到時候發現伺服器沒請求也巨卡,也不知道什麼情況!

好的,那就用ConcurrentHashMap,正如他的名字一樣,他是一個線程安全的HashMap,這樣能輕松解決問題。

public class MyServlet extends Servlet{
    private static ConcurrentHashMap<String, String> fileName2Data = new ConcurrentHashMap<String, String>();
    private void processFile3(String fName){
        String data = fileName2Data.get(fName);
        if(data==null){
            data = readFromFile(fName);    //耗時28ms
            fileName2Data.put(fName, data);
        }
        //process with data
    }
}
           

這樣真的解決問題了嗎,這樣雖然隻要有使用者通路過檔案a,那另一個使用者想通路檔案a,也會從fileName2Data中拿資料,然後也不會引起死循環。

可是,如果你覺得這樣就已經完了,那你把多線程也想的太簡單了,騷年!

你會發現,1000個使用者首次通路同一個檔案的時候,居然讀取了1000次檔案(這是最極端的,可能隻有幾百)。What the fuckin hell!!!

難道代碼錯了嗎,難道我就這樣過我的一生!

好好分析下。Servlet是多線程的,那麼

public class MyServlet extends Servlet{
    private static ConcurrentHashMap<String, String> fileName2Data = new ConcurrentHashMap<String, String>();
    private void processFile3(String fName){
        String data = fileName2Data.get(fName);
        //“偶然”-- 1000個線程同時到這裡,同時發現data為null
        if(data==null){
            data = readFromFile(fName);    //耗時28ms
            fileName2Data.put(fName, data);
        }
        //process with data
    }
}
           

上面注釋的“偶然”,這是完全有可能的,是以,這樣做還是有問題。

是以,可以自己簡單的封裝一個任務來處理。

public class MyServlet extends Servlet{
    private static ConcurrentHashMap<String, FutureTask> fileName2Data = new ConcurrentHashMap<String, FutureTask>();
    private static ExecutorService exec = Executors.newCacheThreadPool();
    private void processFile3(String fName){
        FutureTask data = fileName2Data.get(fName);
        //“偶然”-- 1000個線程同時到這裡,同時發現data為null
        if(data==null){
            data = newFutureTask(fName);
            FutureTask old = fileName2Data.putIfAbsent(fName, data);
            if(old==null){
                data = old;
            }else{
                exec.execute(data);
            }
        }
        String d = data.get();
        //process with data
    }
     
    private FutureTask newFutureTask(final String file){
        return  new FutureTask(new Callable<String>(){
            public String call(){
                return readFromFile(file);
            }
 
            private String readFromFile(String file){return "";}
        }
    }
}
           

以上所有代碼都是直接在bbs打出來的,不保證可以直接運作。

多線程最多的場景:web伺服器本身;各種專用伺服器(如遊戲伺服器);

多線程的常見應用場景:

1、背景任務,例如:定時向大量(100w以上)的使用者發送郵件;

2、異步處理,例如:發微網誌、記錄日志等;

3、分布式計算

作者:歉信君

cnblogs.com/kenshinobiy/p/4671314.html

往期精選  點選标題可跳轉

面試中不可忽視的 15 個 Java 冷知識,你知道多少?

面試官問:Redis 資料過多導緻記憶體占滿,會當機嗎?如何記憶體回收?

MySQL 中誤删表資料,如何快速恢複丢失的資料?

如何重構千行“又臭又長”的類,IntelliJ IDEA 幾分鐘搞定,真牛逼!

為什麼阿裡巴巴禁止使用 Executors 建立線程池,而是通過 ThreadPoolExecutor 方式?

面試官:RabbitMQ 如何保障消息 100% 投遞成功、消息幂等性?

面試官:說一說 MySQL 與 PostgreSQL 的差別,如何技術選型?

Spring 循環依賴,源碼詳細分析,真的必須要三級緩存嗎?

為什麼 MySQL 不建議使用 NULL 作為列預設值?

為什麼放棄使用 Hibernate、JPA、Mybatis 資料持久化架構,最終選擇 JDBCTemplate!

Spring Cloud+OAuth2+Spring Security+Redis 實作微服務統一認證授權,附源碼

面試官:說說項目中 Java 多線程一般都用于哪些應用場景?

點個贊,就知道你“在看”!