京東面試官:公司項(xiàng)目中 Java 的多線程一般用在哪些場(chǎng)景?


戳這里,加關(guān)注哦~
多線程使用的主要目的在于:
1、吞吐量:你做WEB,容器幫你做了多線程,但是他只能幫你做請(qǐng)求層面的。簡(jiǎn)單的說,可能就是一個(gè)請(qǐng)求一個(gè)線程?;蚨鄠€(gè)請(qǐng)求一個(gè)線程。如果是單線程,那同時(shí)只能處理一個(gè)用戶的請(qǐng)求。
2、伸縮性:也就是說,你可以通過增加CPU核數(shù)來提升性能。如果是單線程,那程序執(zhí)行到死也就利用了單核,肯定沒辦法通過增加CPU核數(shù)來提升性能。
鑒于是做WEB的,第1點(diǎn)可能你幾乎不涉及。那這里我就講第二點(diǎn)吧。
舉個(gè)簡(jiǎn)單的例子:
假設(shè)有個(gè)請(qǐng)求,這個(gè)請(qǐng)求服務(wù)端的處理需要執(zhí)行3個(gè)很緩慢的IO操作(比如數(shù)據(jù)庫(kù)查詢或文件查詢),那么正常的順序可能是(括號(hào)里面代表執(zhí)行時(shí)間):
- 讀取文件1 (10ms)
- 處理1的數(shù)據(jù)(1ms)
- 讀取文件2 (10ms)
- 處理2的數(shù)據(jù)(1ms)
- 讀取文件3 (10ms)
- 處理3的數(shù)據(jù)(1ms)
- 整合1、2、3的數(shù)據(jù)結(jié)果 (1ms)
單線程總共就需要34ms。
那如果你在這個(gè)請(qǐng)求內(nèi),把a(bǔ)b、cd、ef分別分給3個(gè)線程去做,就只需要12ms了。
所以多線程不是沒怎么用,而是,你平常要善于發(fā)現(xiàn)一些可優(yōu)化的點(diǎn)。然后評(píng)估方案是否應(yīng)該使用。假設(shè)還是上面那個(gè)相同的問題:但是每個(gè)步驟的執(zhí)行時(shí)間不一樣了。
- 讀取文件1 (1ms)
- 處理1的數(shù)據(jù)(1ms)
- 讀取文件2 (1ms)
- 處理2的數(shù)據(jù)(1ms)
- 讀取文件3 (28ms)
- 處理3的數(shù)據(jù)(1ms)
- 整合1、2、3的數(shù)據(jù)結(jié)果 (1ms)
單線程總共就需要34ms。
如果還是按上面的劃分方案(上面方案和木桶原理一樣,耗時(shí)取決于最慢的那個(gè)線程的執(zhí)行速度),在這個(gè)例子中是第三個(gè)線程,執(zhí)行29ms。那么最后這個(gè)請(qǐng)求耗時(shí)是30ms。比起不用單線程,就節(jié)省了4ms。但是有可能線程調(diào)度切換也要花費(fèi)個(gè)1、2ms。因此,這個(gè)方案顯得優(yōu)勢(shì)就不明顯了,還帶來程序復(fù)雜度提升。不太值得。
那么現(xiàn)在優(yōu)化的點(diǎn),就不是第一個(gè)例子那樣的任務(wù)分割多線程完成。而是優(yōu)化文件3的讀取速度??赡苁遣捎镁彺婧蜏p少一些重復(fù)讀取。
首先,假設(shè)有一種情況,所有用戶都請(qǐng)求這個(gè)請(qǐng)求,那其實(shí)相當(dāng)于所有用戶都需要讀取文件3。那你想想,100個(gè)人進(jìn)行了這個(gè)請(qǐng)求,相當(dāng)于你花在讀取這個(gè)文件上的時(shí)間就是28×100=2800ms了。那么,如果你把文件緩存起來,那只要第一個(gè)用戶的請(qǐng)求讀取了,第二個(gè)用戶不需要讀取了,從內(nèi)存取是很快速的,可能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); //耗時(shí)28ms
fileName2Data.put(fName, data);
}
//process with data
}
}
看起來好像還不錯(cuò),建立一個(gè)文件名和文件數(shù)據(jù)的映射。如果讀取一個(gè)map中已經(jīng)存在的數(shù)據(jù),那么就不不用讀取文件了。
可是問題在于,Servlet是并發(fā),上面會(huì)導(dǎo)致一個(gè)很嚴(yán)重的問題,死循環(huán)。因?yàn)椋琀ashMap在并發(fā)修改的時(shí)候,可能是導(dǎo)致循環(huán)鏈表的構(gòu)成?。。。ň唧w你可以自行閱讀HashMap源碼)如果你沒接觸過多線程,可能到時(shí)候發(fā)現(xiàn)服務(wù)器沒請(qǐng)求也巨卡,也不知道什么情況!
好的,那就用ConcurrentHashMap,正如他的名字一樣,他是一個(gè)線程安全的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); //耗時(shí)28ms
fileName2Data.put(fName, data);
}
//process with data
}
}
這樣真的解決問題了嗎,這樣雖然只要有用戶訪問過文件a,那另一個(gè)用戶想訪問文件a,也會(huì)從fileName2Data中拿數(shù)據(jù),然后也不會(huì)引起死循環(huán)。
可是,如果你覺得這樣就已經(jīng)完了,那你把多線程也想的太簡(jiǎn)單了,騷年!你會(huì)發(fā)現(xiàn),1000個(gè)用戶首次訪問同一個(gè)文件的時(shí)候,居然讀取了1000次文件(這是最極端的,可能只有幾百)。What the fuckin hell!!!
難道代碼錯(cuò)了嗎,難道我就這樣過我的一生!
好好分析下。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個(gè)線程同時(shí)到這里,同時(shí)發(fā)現(xiàn)data為null
if(data==null){
data = readFromFile(fName); //耗時(shí)28ms
fileName2Data.put(fName, data);
}
//process with data
}
}
上面注釋的“偶然”,這是完全有可能的,因此,這樣做還是有問題。
因此,可以自己簡(jiǎn)單的封裝一個(gè)任務(wù)來處理。
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個(gè)線程同時(shí)到這里,同時(shí)發(fā)現(xiàn)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打出來的,不保證可以直接運(yùn)行。
多線程最多的場(chǎng)景:web服務(wù)器本身;各種專用服務(wù)器(如游戲服務(wù)器);
多線程的常見應(yīng)用場(chǎng)景:
- 后臺(tái)任務(wù),例如:定時(shí)向大量(100w以上)的用戶發(fā)送郵件;
- 異步處理,例如:發(fā)微博、記錄日志等;
- 分布式計(jì)算
來源:cnblogs.com/kenshinobiy/p/4671314.html
最近好文分享1、一次畢生難忘的 Java 內(nèi)存泄漏排查經(jīng)歷!
2、面試必問!JDK 中定時(shí)器是如何實(shí)現(xiàn)的?
3、說實(shí)話!你知道 Java 中的回調(diào)機(jī)制嗎?
4、IDEA 真牛逼,900行 "又臭又長(zhǎng)" 的類重構(gòu)!5、數(shù)數(shù) FastJson 那些年犯下的'血案'...6、Java 最多支持多少個(gè)線程?你會(huì)怎么答?
……
更多請(qǐng)掃碼關(guān)注 ? Java核心技術(shù)一個(gè)分享Java核心技術(shù)干貨的公眾號(hào)
點(diǎn)擊閱讀原文獲取面試題~
