我們都知道MySQL的TableCache是表定義的緩存,江湖上流傳著各種對這個參數的調優方法。
tablecache的作用,就是節約讀取表結構文件的開銷。對於tablecache是否命中,其實tablecache是針對於線程的,每個線程有自己的緩存,只緩存本線程的表結構定義。不過我們發現,strace中沒有關於表結構文件的open操作(只有stat操作,定位表結構文件是否存在),也就是說tablecache不命中鬧亮罩,不一定需要讀取表結構文件。這種感覺好像是:在不命中tablecache時,命中了另外一個表結構緩存。
運維建議:
我們讀一下MySQL的文檔,關於table_open_cache的建議值公式:建議值=最大並發數*join語句涉及的表的最液鬧大個數。
通過實驗我們鍵迅容易理解:table_cache是針對於線程的,所以需要最大並發數個緩存。另外,一個語句join涉及的表,需要同時在緩存中存在。所以最小的緩存大小,等於語句join涉及的表的最大個數。將這兩個數相乘,就得到了MySQL的建議值公式。
② 線程並發訪問如何解決的一個簡單demo程
synchronized關鍵字主要解決多線程共享數據同步問題。
ThreadLocal使粗銷用場合主要解決多線程中數據因並發產生不一致問題。
ThreadLocal和Synchonized都用於解決多線程並發訪問。但是ThreadLocal與synchronized有本質的區別:
synchronized是利用鎖的機制,使變數或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線凳凳蠢程都提供了變數的副本,使 得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而Synchronized卻正好相反,它用於在多個線程間通信 時能夠獲得數據共享。
Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。棗陪當然ThreadLocal並不能替代synchronized,它們處理不同的問題域。Synchronized用於實現同步機制,比ThreadLocal更加復雜。
③ 求一段死循環的c#多線程並發訪問一個網址的代碼
首先回答你的問題:
public int A{ get{ return _a; }set{_a=value;}}
這叫封裝屬性,可以在get或set里對值進行處理,比如這個值不能大於100
可以修改為:
set{if(value<=100){_a=value;}} 不用屬性的話,每個調用_a的地方都要加這段代碼,多麻煩
剛好做了一個類似的測試,比你的要求復雜一些,你自己輪沖森看吧:
// 點擊按鈕,開始循環測試
private void button1_Click(object sender, EventArgs e)
{
textBox4.Text = string.Empty;
string testUrl = txtUrl.Text; // 測試地址
string postData = "killId=" + txtKillid.Text; // 測試的參判鋒數,要post的數據
int threadNum = int.Parse(txtThreadNum.Text); // 發起的線程數,每個線程為一個新的Session,你臘畝要無限的話,可以設置為int.MaxValue
int threadTime = 10; // 每個線程跑10次,這是用於要用同一個Session測試10次的情況
for (int i = 0; i < threadNum; i++)
{
string[] arr = { testUrl, postData, killTime, i.ToString() };
new Thread(Post).Start(arr); // 開始當前線程測試
}
}
public void Post(object arr)
{
string[] para = arr as string[];
string testUrl = para[0];
string postData = para[1];
int killTime = int.Parse(para[2]);
int threadNum = int.Parse(para[3]);
CookieContainer cookie = new CookieContainer();
while (killTime > 0)
{
HttpWebRequest request = WebRequest.Create(testUrl) as HttpWebRequest;
request.ContentType = "application/x-www-form-urlencoded";
request.Method = "POST";
request.CookieContainer = cookie;
byte[] bodyBytes = Encoding.UTF8.GetBytes(postData);
request.ContentLength = bodyBytes.Length;
using (Stream reqStream = request.GetRequestStream())
{
reqStream.Write(bodyBytes, 0, bodyBytes.Length);
reqStream.Flush();
}
request.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1;)";
killTime--;
using (WebResponse response = request.GetResponse())
using (Stream sr = response.GetResponseStream())
using (StreamReader reader = new StreamReader(sr))
{
// 把網頁返回的內容輸出到TextBox中
SetText(textBox4,
threadNum + "," + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + ":" + reader.ReadToEnd());
}
cookie = request.CookieContainer;
Thread.Sleep(100);
}
}
/// <summary>
/// 用於線程里訪問TextBox的線程委託
/// </summary>
/// <param name="tb"></param>
/// <param name="txt"></param>
private delegate void SetTextDelegate(TextBox tb, string txt);
private void SetText(TextBox tb, string txt)
{
if (!tb.InvokeRequired)
{
tb.Text += txt + "\r\n";
}else
{
SetTextDelegate de =SetText;
Invoke(de, tb, txt);
//de.Invoke(txt);
}
}
④ 線程和緩存是什麼意思
線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以並發執行。由於線程之間的相互制約,致使線程在運行中呈現出間斷性。線程也有就緒、阻塞和運行三種基本狀態。在單個程序中同時運行多個線程完成不同的工作,稱為多線程。
緩存是指臨時文件交換區,電腦把最常用的文件從存儲器里提出來臨時放在緩存里,就像把工具和材料搬上工作台一樣,這樣會比用時現去倉庫取更方便。因為緩存往往使用的是RAM(斷電即掉的非永久儲存),所以在忙完後還是會把文件送到硬碟等存儲器里永久存儲。電腦里最大的緩存就是內存條,緩存的大小與速度是直接關繫到硬碟的傳輸速度的重要因素,能夠大幅度地提高硬碟整體性能。
⑤ 線程池 ExecutorService
並發,啟動大量任務線程時,頻繁的線程創建/銷毀會造成浪費。(創建、運行、銷毀都需要時間),採用線程池,線程復用技術,提高性能。
線程池實現類,ThreadPoolExecutor 類。
ThreadPoolExecutor 構造方法,實現不同類型線程池。
corePoolSize,核心線程數。
maximumPoolSize,允許的最大線程,超過報異常。
keepAliveTime,非核心線程活躍時間。
TimeUnit,時間度量。
BlockingQueue<Runnable>,任務隊列,(無限、有限隊列、棧)。
ThreadFactory,線程工廠。
不推薦直接使用 ThreadPoolExecutor 構造方法創建。
1,緩存
核心線禪拍燃程是0,最大線程 MAX_VALUE ,任務隊列 SynchronousQueue 是一個管道,不存儲,線程活躍時間60秒。
適用短期大量任務的場景。
某一時間段高並發任務,創建大量線程,任務結束後,線程變空閑,60s以內重用,處理新任務,空閑到達60s,銷毀賀虛。
2,固定數量
核心線程、最大線程,自定義,LinkedBlockingQueue 任務隊列,不設置空閑時間,無界隊列。
適用長期任務場景。
任務隊列無界,僅核心線程工作,keepAlieveTime 不需要設置。
3,單線程
核心線程、最大線程,數量1,LinkedBlockingQueue 任務隊列,不設置空閑時間,無界隊列。
適用單個任務順序執行場景。
只有一個核心線程,新任務加入隊列。
4,定時任務
ScheledThreadPoolExecutor 實例,ThreadPoolExecutor 子類。
核心線程自定義,最大線程 MAX_VALUE,DelayedWorkQueue 任務隊列,空閑時間10s。
適用指定有延遲的任務或周期性重復任務場景。
隊列不是按照 submit 時間排序,以延時時間為優先順序升序排序,延時時間短的,優先順序高,先執行。
線程池 ThreadPoolExecutor 創建,將任務派發給線程池,execute() 方法,自動分配線程執行。
workerCountOf(c) 方法,判斷工作線程數量,當 <核心線程數量,addWorker() 方法,創建線程,設標志位參數代表核心線程,任務將由新建核心線程處理。
當 >= 核心線程,isRunning(c) 方法,表示 c<0,offer()方法,將任務加入隊列。
offer() 是非阻塞方法,如果隊列已滿,返回失敗,此時,addWorker() 方法,創建線程,不設標志位參數賀李代表非核心線程,任務將由新建的非核心線程處理。
新線程創建
如果工作線程 >CAPACITY 容量,或 >= 允許的最大值(創建核心線程 >= 核心線程數量),失敗返回。
創建一個 Runnable 類型 Worker 對象,ThreadPoolExecutor 內部靜態類,用戶任務封裝,newThread() 方法,創建新線程,將 Worker(this) 作為新線程任務主體。
將 Worker 任務加入 HashSet 集合,設置 workerAdded 標志,啟動新線程( start方法),設置 workerStarted 啟動標志,代表線程啟動,執行 Worker 任務的 run() 方法,調用 runWorker() 方法(外部類方法)。
新線程執行 Worker 任務的 run() 方法,藉助 Worker 開始為線程池工作,從 Worker 獲取內部 Runnable(即 execute 派送的用戶任務),並執行 run() 方法。
用戶任務處理完成,線程不會馬上結束,while 循環,繼續 getTask() 方法,從任務隊列中獲取任務,該方法可能會導致阻塞,隊列空則線程結束。
BlockingQueue 阻塞隊列。
1,工作線程 wc > 核心線程
設置 timed 標志,隊列採用阻塞等待,(poll + timeout方式),timeout 設置線程 keepAliveTime 時間 。
因此,即使隊列沒有任務,線程仍然存活,(任務進隊列後可立即喚醒展開工作)。
2,工作線程 wc < 核心線程
(僅有核心線程),隊列採用一直阻塞,( take 方式),隊列是空時,線程一直阻塞,核心線程不會結束。
3,隊列空,poll 超時
設置 timeOut 已超時標志,下次循環時,如果再發生工作線程 wc > 核心線程 ( timed 和 timedOut 標志並存),線程數量自減,退出 while 返回空,線程結束。
4,設置核心線程 allowCoreThreadTimeOut
不管工作線程 wc,採用 poll + timeout 方式,keepAliveTime 隊列無任務,所有線程都會 timedOut 超時標志,下次循環自動結束。
即使 wc < 核心線程,線程也會結束退出,允許核心線程超時結束。
線程池本質,設置一定數量的線程集合,任務結束,但線程不結束,根據設定值,請求任務隊列,繼續任務,復用線程。
線程等待,利用任務隊列的阻塞特性。阻塞任務隊列,訪問空隊列時,線程等待,timeout 超時時間,實現線程 keepAliveTime 活動或一直存活。
任務隊列
poll() 方法,取隊列,非阻塞,poll + timeout,阻塞 timeout 時間。
take() 方法,取隊列,阻塞等待。
put() 方法,存隊列,阻塞等待,
offer() 方法,存隊列,非阻塞,offer + timeout,阻塞 timeout 時間。
核心線程,在線程集合中,未具體標明,若 All 線程都完成任務,空閑,且隊列空,在 getTask() 方法,根據超時時間,逐一喚醒,結束,剩下的數量 = 核心,它們即核心。
任重而道遠
⑥ 華為技術架構師分享:高並發場景下緩存處理的一些思路
在實際的開發當中,我們經常需要進行磁碟數據的讀取和搜索,因此經常會有出現從資料庫讀取數據的場景出現。但是當數據訪問量次數增大的時候,過多的磁碟讀取可能會最終成為整個系統的性能瓶頸,甚至是壓垮整個資料庫,導致系統卡死等嚴重問題。
常規的應用系統中,我們通常會在需要的時候對資料庫進行查找,因此系統的大致結構如下所示:
1.緩存和資料庫之間數據一致性問題
常用於緩存處理的機制我總結為了以下幾種:
首先來簡單說說Cache aside的這種方式:
Cache Aside模式
這種模式處理緩存通常都是先從資料庫緩存查詢,如果緩存沒有命中則從資料庫中進行查找。
這裡面會發生的三種情況如下:
緩存命中:
當查詢的時候發現緩存存在,那麼直接從緩存中提取。
緩存失效:
當緩存沒有數據的時候,則從database裡面讀取源數據,再加入到cache裡面去。
緩存更新:
當有新的寫操作去修改database裡面的數據時,需要在寫操作完成之後,讓cache裡面對應的數據失效。
關於這種模式下依然會存在缺陷。比如,一個是讀操作,但是沒有命中緩存,然後就到資料庫中取數據,此時來了一個寫操作,寫完資料庫後,讓緩存失效,然後,之前的那個讀操作再把老的數據放進去,所以,會造成臟數據。
Facebook的大牛們也曾經就緩存處理這個問題發表過相關的論文,鏈接如下:
分布式環境中要想完全的保證數據一致性是一件極為困難的事情,我們只能夠盡可能的減低這種數據不一致性問題產生的情況。
Read Through模式
Read Through模式是指應用程序始終從緩存中請求數據。 如果緩存沒有數據,則它負責使用底層提供程序插件從資料庫中檢索數據。 檢索數據後,緩存會自行更新並將數據返回給調用應用程序。使用Read Through 有一個好處。
我們總是使用key從緩存中檢索數據, 調用的應用程序不知道資料庫, 由存儲方來負責自己的緩存處理,這使代碼更具可讀性, 代碼更清晰。但是這也有相應的缺陷,開發人員需要給編寫相關的程序插件,增加了開發的難度性。
Write Through模式
Write Through模式和Read Through模式類似,當數據發生更新的時候,先去Cache裡面進行更新,如果命中了,則先更新緩存再由Cache方來更新database。如果沒有命中的話,就直接更新Cache裡面的數據。
2.緩存穿透問題
在高並發的場景中,緩存穿透是一個經常都會遇到的問題。
什麼是緩存穿透?
大量的請求在緩存中沒有查詢到指定的數據,因此需要從資料庫中進行查詢,造成緩存穿透。
會造成什麼後果?
大量的請求短時間內湧入到database中進行查詢會增加database的壓力,最終導致database無法承載客戶單請求的壓力,出現宕機卡死等現象。
常用的解決方案通常有以下幾類:
1.空值緩存
在某些特定的業務場景中,對於數據的查詢可能會是空的,沒有實際的存在,並且這類數據信息在短時間進行多次的反復查詢也不會有變化,那麼整個過程中,多次的請求資料庫操作會顯得有些多餘。
不妨可以將這些空值(沒有查詢結果的數據)對應的key存儲在緩存中,那麼第二次查找的時候就不需要再次請求到database那麼麻煩,只需要通過內存查詢即可。這樣的做法能夠大大減少對於database的訪問壓力。
2.布隆過濾器
通常對於database裡面的數據的key值可以預先存儲在布隆過濾器裡面去,然後先在布隆過濾器裡面進行過濾,如果發現布隆過濾器中沒有的話,就再去redis裡面進行查詢,如果redis中也沒有數據的話,再去database查詢。這樣可以避免不存在的數據信息也去往存儲庫中進行查詢情況。
什麼是緩存雪崩?
當緩存伺服器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給後端系統(比如DB)帶來很大壓力。
如何避免緩存雪崩問題?
1.使用加鎖隊列來應付這種問題。當有多個請求湧入的時候,當緩存失效的時候加入一把分布式鎖,只允許搶鎖成功的請求去庫裡面讀取數據然後將其存入緩存中,再釋放鎖,讓後續的讀請求從緩存中取數據。但是這種做法有一定的弊端,過多的讀請求線程堵塞,將機器內存占滿,依然沒有能夠從根本上解決問題。
2.在並發場景發生前,先手動觸發請求,將緩存都存儲起來,以減少後期請求對database的第一次查詢的壓力。數據過期時間設置盡量分散開來,不要讓數據出現同一時間段出現緩存過期的情況。
3.從緩存可用性的角度來思考,避免緩存出現單點故障的問題,可以結合使用 主從+哨兵的模式來搭建緩存架構,但是這種模式搭建的緩存架構有個弊端,就是無法進行緩存分片,存儲緩存的數據量有限制,因此可以升級為Redis Cluster架構來進行優化處理。(需要結合企業實際的經濟實力,畢竟Redis Cluster的搭建需要更多的機器)
4.Ehcache本地緩存 + Hystrix限流&降級,避免MySQL被打死。
使用 Ehcache本地緩存的目的也是考慮在 Redis Cluster 完全不可用的時候,Ehcache本地緩存還能夠支撐一陣。
使用 Hystrix進行限流 & 降級 ,比如一秒來了5000個請求,我們可以設置假設只能有一秒 2000個請求能通過這個組件,那麼其他剩餘的 3000 請求就會走限流邏輯。
然後去調用我們自己開發的降級組件(降級),比如設置的一些默認值呀之類的。以此來保護最後的 MySQL 不會被大量的請求給打死。
⑦ Tomcat使用線程池配置高並發連接
1:配置executor屬性
打開/conf/server.xml文件,在Connector之前配置一個線程池:
重要參數說明:name :共享線程池的名字。這是Connector為了共享線程池要引用的名字,該名字必須唯一。默認值:None; namePrefix :在JVM上,每個運行線程都可以有一個name 字元串。這一屬性為線程池中每個線程的name字元串設置了一個前綴,Tomcat將把線程號追加到這一前綴的後面。默認值:tomcat-exec-; maxThreads :該線程池可以容納的最大線程數。默認值:200; maxIdleTime :在Tomcat關閉一個空閑線程之前,允許空閑線程持續的時間(以毫秒為單位)。只有當前活躍的線程數大於minSpareThread的值,才會關閉空閑線程。默認值:60000(一分鍾)。 minSpareThreads :Tomcat應該始終打開的最小不活躍線程數。默認值:25。
2:配置Connector
重要參數說明:executor :表示使用該參數值對應的線程池; minProcessors :伺服器啟動時創建的處理請求的線程數; maxProcessors :最大可以創建的處理請求的線程數; acceptCount :指定當所有可以使用的處理請求的線程數都被使用時,可以放到處理隊列中的請求數,超過這個數的請求將不予處理。
一.Tomcat內存優化
Tomcat內存優化主要是對 tomcat 啟動參數優化,我們可以在 tomcat 的啟動腳本 catalina.sh 中設置JAVA_OPTS 參數。
1.JAVA_OPTS參數說明
現公司伺服器內存一般都可以加到最大2G ,所以可以採取以下配置:
在cygwin=false前添加
配置完成後可重啟Tomcat ,通過以下命令進行查看配置是否生效:
首先查看Tomcat 進程號:
result
我們可以看到Tomcat 進程號是27698 。
查看是否配置生效:
能在輸出的信息中找到Heap Configuration中看到MaxHeapSize 等參數已經生效。
二.Tomcat並發優化
1.Tomcat連接相關參數
在Tomcat 配置文件 server.xml 中的 配置中
1.參數說明
minProcessors :最小空閑連接線程數,用於提高系統處理性能,默認值為 10 maxProcessors :最大連接線程數,即:並發處理的最大請求數,默認值為 75 acceptCount :允許的最大連接數,應大於等於 maxProcessors ,默認值為 100 enableLookups :是否反查域名,取值為: true 或 false 。為了提高處理能力,應設置為 false connectionTimeout :網路連接超時,單位:毫秒。設置為 0 表示永不超時,這樣設置有隱患的。通常可設置為 30000 毫秒。其中和最大連接數相關的參數為maxProcessors 和 acceptCount 。如果要加大並發連接數,應同時加大這兩個參數。web server允許的最大連接數還受制於操作系統的內核參數設置,通常 Windows 是 2000 個左右, Linux是 1000 個左右。
2.Tomcat中的配置示例
2.調整連接器connector的並發處理能力
1.參數說明
maxThreads :客戶請求最大線程數 minSpareThreads :Tomcat初始化時創建的 socket 線程數 maxSpareThreads :Tomcat連接器的最大空閑 socket 線程數 enableLookups :若設為true, 則支持域名解析,可把 ip 地址解析為主機名 redirectPort :在需要基於安全通道的場合,把客戶請求轉發到基於SSL 的 redirectPort 埠 acceptAccount :監聽埠隊列最大數,滿了之後客戶請求會被拒絕(不能小於maxSpareThreads ) connectionTimeout :連接超時 minProcessors :伺服器創建時的最小處理線程數 maxProcessors :伺服器同時最大處理線程數 URIEncoding :URL統一編碼
2.Tomcat中的配置示例
3.Tomcat緩存優化
1.參數說明
compression :打開壓縮功能 compressionMinSize :啟用壓縮的輸出內容大小,這裡面默認為2KB compressableMimeType :壓縮類型 connectionTimeout :定義建立客戶連接超時的時間. 如果為 -1, 表示不限制建立客戶連接的時間
2.Tomcat中的配置示例
4.參考配置
1.舊有的配置
參考網路對伺服器做過如下配置,拿出來分享下:
後來發現在訪問量達到3 百萬多的時候出現性能瓶頸。
2.更改後的配置
⑧ 7.單線程並發
單線程並發 意味著貌似可以在單個線程中同時完成多個任務。 從表面上看,單線程並發聽起來有點矛盾。 以前,在多線程體系結構中,多個任務將在多個線程之間分配,以並行執行。 因此察敏,不同任務之間的切換是通過操作系統和CPU在不同線程之間的切換來完成的。 但是,單個線程實際上可以幾乎同時處理多個任務。 在本單線程並發教程中,我將解釋單線程並發如何設計的,以及有何好處。請注意:本教程仍在進行中。 在不久的將來會添加更多!
請注意:本教程仍在進行中。 在不久的將來會添加更多!
在經典的多線程體系架構中,通常將每個任務分配給一個單獨的線程以執行。 每個線程一次只執行一個任務。 在某些設計中,將為每個任務創建一個新線程,因此一旦任務完成,該線程就會死掉。 在其他設計中,線程池保持活動狀態,該線程池一次從任務隊列中執行一個任務,然後執行另一任務,如此往復。有關更多信息,請參閱我的 線程池 教程。
多線程體系架構的優點是簡沒歲,相對容易地在多個線程和多個CPU之間分配工作負載。 只需將任務分配給線程,然後讓OS / CPU將線程調度到CPU。
但是,如果正在執行的任務需要共享數據,則多線程體系架構可能會導致許多並發問題,例如 競態條件 , 死鎖 , 飢餓 , 滑動條件 , 嵌套監視器鎖定 等。通常,越多的線程共享相同的數據和數據結構,發生並發問題的可能性就越高。 換句話說,您需要在設計時留意更多內容。
當多個線程試圖同時訪問同一個數據結構時,經典的多線程體系結構有時還會導致擁塞。 這取決於給定數據結構的實現方式,某些線程可能會被阻塞,以等待其他正在訪問該數據結構線程訪問完成。
經典多線程體系結構的替代方法是單線程或 同線程 。 通過僅使用一個線程來執行應用程序中的所有任務,就可以完全避免前一部分(經典的多線程並發架構)中列出的所有並發問題。
您可以擴展單線程體系結構以使用多個線程,其中每個線程的行為就像是一個單獨的隔離的單線程系統。 在那種情況下,我將此架構稱為相同線程。 執行任務所需的所有數據仍保持隔離在單個線程內-在同一線程內。
如果只有一個線程執行應用程序的所有任務,則可能會導致一些問題:
● 從任務中阻止IO操作,將阻止線程,從而阻止整個應用程序。
● 長時間運行的任務,可能會產生無法接受的延遲其他任務的執行。
● 單個線程只能使用到單個CPU。
可以解決這些問題,但又不會失去單線程並發體系結構的簡單性的優勢,也不會使整體設計過於復雜。
大多數長時間運行的應用程序以某種循環執行,其中應用程序主線程正在等待來自應用程序外部的輸入,處理該輸入,然後返回等待狀態。
這種線程循環在伺服器應用程序(Web服務,服務等)和GUI應用程序中都可以使用。 有時,您可以看到該線程循環, 而有時則看不到。
您可能會想知道,在一遍又一遍地密集循環中,執行的線程是否會浪費大量CPU時間。 如果線程在運行時沒有任何實際工作要做,那麼可能會浪費掉大量CPU時間。攔睜 因此,如果執行循環的線程判斷出休眠幾毫秒是可行的,則可能會使用「休眠」,而非循環, 這樣可以減少CPU的時間浪費。
線程循環通常在其生命周期內執行兩種類型的任務:
● 重復任務
● 一次性任務
以下各節將對這兩項任務進行更詳細的說明。
重復任務是一個重復執行的任務,它在執行該任務的線程的生命周期內一次又一次地執行。 通常,對於任務的每次調用,將完全執行重復的任務。
重復任務的一個示例是檢查一組入站網路連接上的傳入數據。 如果檢測到任何傳入數據,將對其進行處理,並且在處理之後,將針對此特定調用執行重復的任務。 但是,需要一次又一次地檢查入站數據,以使應用程序能夠連續響應傳入的數據。
一次性任務是只需要執行一次的任務。一次性任務可以是短期運行,也可以是長期運行。
短時任務是一個足夠短的任務,可以在一個執行階段中完成,而又不會使執行該任務的線程因該線程承擔的其他職責(它必須執行的其他任務)而延遲。
一次性長時間運行的任務是在單個執行階段中花費太長時間才能完成的任務。 「花費太長時間」是指執行任務中的全部工作量將佔用太多的線程時間,因此其他重復任務或一次性任務將被延遲太多,以至於應用程序的總響應速度受到傷害。
為了避免單個長時間運行的任務佔用過多的線程執行時間,將完成任務所需的全部工作分解為較小的塊,可以一次執行一個塊。每個塊必須足夠小,以免延遲線程執行過多任務所需的其他任務。
長時間運行的任務在內部跟蹤其執行塊。執行長時間運行的任務的線程將多次調用其執行方法,直到所有任務塊均已完全執行。在調用特定長時間運行任務的執行方法之間,線程可以調用其他長時間運行任務,其他重復任務或線程承擔的任何職責的執行方法。
一次性的長期運行任務可能是處理目錄中的N個文件。可以將N個文件的處理分解成較小的塊,而不是在單個執行階段中處理所有N個文件,而每個塊都在單個執行階段中進行處理。例如,每個執行階段可以處理1個文件。要處理所有N個文件的任務
在線程循環中,一次性任務通常由重復任務檢測並執行,如下所示。
為了能夠似乎同時在一個以上的任務上取得進展,在任務上取得進展的線程必須能夠在這些任務之間進行切換。 這也稱為任務切換。
任務切換的確切工作方式取決於任務的類型-線程是在重復任務還是一次性任務之間進行切換。 雖然總的原理還是一樣的。 我將在以下各節中對這兩者進行更詳細的說明。
重復的任務通常只有一個方法,該方法被同一線程重復調用。 重復任務是應在應用程序的整個生命周期中重復的任務,因此它永遠不會真正「完成」。 重復的任務執行了所需的操作,然後退出其執行方法,將控制權交還給調用線程。
通過以循環方式調用它們的執行方法,單個線程可以在多個重復任務之間進行切換。 首先重復執行的任務A有執行的機會,然後是B,然後是C,然後是A,依此類推。
萬一重復任務沒有完全完成它開始的任何工作,它可以記錄它在內部走了多遠,並在下次調用重復任務時從那裡繼續。
一次性任務與重復任務的不同之處在於,一次性任務有望在某個時間點完成。 這意味著,有時需要從任務池中刪除一次性任務。
除此之外,一次完成任務之間的切換類似於重復任務之間的切換。 執行線程調用給定的一次性任務的執行方法,該任務在短時間內取得進展,然後在內部記錄其執行的距離,然後退出其執行方法,將控制權交還給調用線程。 現在,調用線程可以循環方式調用任務池中的下一個一次性任務。
每次調用一次性任務的執行方法後,調用線程將檢查任務是否已完成。 如果已刪除,則一次性任務將從任務池中刪除。
在實踐中,一個應用程序可能包含一個調用一個或多個重復任務的線程循環,重復任務可以執行一次任務作為重復行為的一部分。 下圖說明了這一點。 該圖僅描述了一個重復的任務,但根據具體應用,可能還會有更多任務。
當單個線程要在多個任務(無論是重復任務還是一次性任務)之間切換時,必須確保在一次調用任務時,這些任務不會佔用過多的線程執行時間。換句話說,確保每個任務之間執行時間的公平平衡是每個任務幫助的職責。
任務應該允許自己執行多長時間,具體取決於系統設計者。對於一次性任務,這可能會有些復雜。有些任務自然很快就完成了,而另一些任務自然要花費更長的時間才能完成。對於運行時間較長的任務,由任務的實現者來估計如何將工作分解為足夠小的分區,以便可以在不延遲其他任務過多的情況下執行每個分區。
需要注意的一件有趣的事是,如果線程以循環方式調用每個一次性任務,那麼任務執行器包含的一次性任務越多,每個線程獲得的執行時間就越短,因為在執行任務之前需要更長的時間接下來的執行時間。
可以實現一個將某些任務優先於其他任務的任務執行器。 例如,任務執行者可以在內部將任務保存在不同的列表中,例如 執行低優先順序任務列表中的任務每執行1次,就執行2次高優先順序列表中的任務。
確切地說,如何執行優先任務執行器將取決於具體需求。 還有多少個優先順序,例如 低/高,或低/中/高等
如果一次性任務正在等待某些非同步操作完成,例如 如果來自遠程伺服器的答復,則一次性任務將無法繼續進行下去,直到它正在等待的非同步操作完成為止。 在那種情況下,一次又一次地調用該任務可能沒有意義,只是為了使該任務意識到它無法取得任何進展並立即將控制權返回給調用線程。
在這種情況下,一次性任務能夠將自己「停放」在任務執行器內部可能是有意義的,因此不再被調用。 非同步操作完成後,一次性任務可以取消停放,然後重新插入到活動任務中,這些活動將連續調用以取得進展。 當然,要能夠取消任務,系統的其他部分必須檢測到非同步操作已完成,以及要為該非同步操作取消任務。
顯然,如果在應用程序中只有一個線程正在執行,則不能利用多個CPU。 解決方案是啟動多個線程。 通常,每個CPU一個線程-取決於您的線程需要執行哪種任務。 如果您有需要執行阻塞IO工作的任務,例如從文件系統或網路中讀取數據,則每個CPU可能需要多個線程。 每個線程將在等待阻塞的IO操作完成時被阻塞,不執行任何操作。
當您將單線程體系結構擴展到多個單線程子系統時,從技術上講,它不再是單線程的。 但是,每個單線程子系統通常都將被設計為一個單線程系統,並表現為一個單線程系統。 我曾經將這樣的多線程單線程系統稱為同線程系統,盡管我不確定這實際上是最精確的術語。 我們可能需要重新審視這些不同的設計,並在將來為它們提供更具描述性的術語。
譯自: Singlethreaded Concurrency
Jakob Jenkov
Last update: 2020-12-11
⑨ 並發編程解惑之線程
主要內容:
進程是資源分配的最小單位,每個進程都有獨立的代碼和數據空間,一個進程包含 1 到 n 個線程。線程是 CPU 調度的最小單位,每個線程有獨立的運行棧和程序計數器,線程切換開銷小。
Java 程序總是從主類的 main 方法開始執行,main 方法就是 Java 程序默認的主線程,而在 main 方法中再創建的線程就是其他線程。在 Java 中,每次程序啟動至少啟動 2 個線程。一個是 main 線程,一個是垃圾收集線程。每次使用 Java 命令啟動一個 Java 程序,就相當於啟動一個 JVM 實例,而每個 JVM 實例就是在操作系統中啟動的一個進程。
多線程可以通過繼承或實現介面的方式創建。
Thread 類是 JDK 中定義的用於控制線程對象的類,該類中封裝了線程執行體 run() 方法。需要強調的一點是,線程執行先後與創建順序無關。
通過 Runnable 方式創建線程相比通過繼承 Thread 類創建線程的優勢是避免了單繼承的局限性。若一個 boy 類繼承了 person 類,boy 類就無法通過繼承 Thread 類的方式來實現多線程。
使用 Runnable 介面創建線程的過程:先是創建對象實例 MyRunnable,然後將對象 My Runnable 作為 Thread 構造方法的入參,來構造出線程。對於 new Thread(Runnable target) 創建的使用同一入參目標對象的線程,可以共享該入參目標對象 MyRunnable 的成員變數和方法,但 run() 方法中的局部變數相互獨立,互不幹擾。
上面代碼是 new 了三個不同的 My Runnable 對象,如果只想使用同一個對象,可以只 new 一個 MyRunnable 對象給三個 new Thread 使用。
實現 Runnable 介面比繼承 Thread 類所具有的優勢:
線程有新建、可運行、阻塞、等待、定時等待、死亡 6 種狀態。一個具有生命的線程,總是處於這 6 種狀態之一。 每個線程可以獨立於其他線程運行,也可和其他線程協同運行。線程被創建後,調用 start() 方法啟動線程,該線程便從新建態進入就緒狀態。
NEW 狀態(新建狀態) 實例化一個線程之後,並且這個線程沒有開始執行,這個時候的狀態就是 NEW 狀態:
RUNNABLE 狀態(就緒狀敬春則態):
阻塞狀態有 3 種:
如果一個線程調用了一個對象的 wait 方法, 那麼這個線程就會處於等待狀態(waiting 狀態)直到另外一個線程調用這個對象的 notify 或者 notifyAll 方法後才會解除這個狀態。
run() 里的代碼執行完畢後,線程進入終結狀態(TERMINATED 狀態)。
線程狀態有 6 種:新建、可運行、阻塞、等待、定時等待、死亡。
我們看下 join 方法的使用:
運行結果:
我們來看下 yield 方法的使用:
運行結果:
線程與線程之間是無法直接通信的,A 線程無法直接通知 B 線程,Java 中線程之間交換信息是通過共享的內存來實現的,控制共享資源的讀寫的訪問,使得多個線程輪流執行對共享數據的操作,線程之間通信是通過對共享資源上鎖或釋放鎖來實現的。線程排隊輪流執行共享資源,這稱為線程的同步。
Java 提供了很多同森歷步操作(也就是線程間的通信方式),同步可使用 synchronized 關鍵字、Object 類的 wait/notifyAll 方法、ReentrantLock 鎖、無鎖同步 CAS 等方式來實現。
ReentrantLock 是 JDK 內置的一個鎖對象,用於線程同步(線程通信),需要用戶手動釋放鎖。
運行結果:
這表明同一時間段只能有 1 個線程執行 work 方法,因為 work 方法里的代碼需要獲取到鎖才能執行,這就實現了多個線程間的通信,線程 0 獲取鎖,先執行,線程 1 等待,線程 0 釋放鎖,線程 1 繼續執行。
synchronized 是一亮棚種語法級別的同步方式,稱為內置鎖。該鎖會在代碼執行完畢後由 JVM 釋放。
輸出結果跟 ReentrantLock 一樣。
Java 中的 Object 類默認是所有類的父類,該類擁有 wait、 notify、notifyAll 方法,其他對象會自動繼承 Object 類,可調用 Object 類的這些方法實現線程間的通信。
除了可以通過鎖的方式來實現通信,還可通過無鎖的方式來實現,無鎖同 CAS(Compare-and-Swap,比較和交換)的實現,需要有 3 個操作數:內存地址 V,舊的預期值 A,即將要更新的目標值 B,當且僅當內存地址 V 的值與預期值 A 相等時,將內存地址 V 的值修改為目標值 B,否則就什麼都不做。
我們通過計算器的案例來演示無鎖同步 CAS 的實現方式,非線程安全的計數方式如下:
線程安全的計數方式如下:
運行結果:
線程安全累加的結果才是正確的,非線程安全會出現少計算值的情況。JDK 1.5 開始,並發包里提供了原子操作的類,AtomicBoolean 用原子方式更新的 boolean 值,AtomicInteger 用原子方式更新 int 值,AtomicLong 用原子方式更新 long 值。 AtomicInteger 和 AtomicLong 還提供了用原子方式將當前值自增 1 或自減 1 的方法,在多線程程序中,諸如 ++i 或 i++ 等運算不具有原子性,是不安全的線程操作之一。 通常我們使用 synchronized 將該操作變成一個原子操作,但 JVM 為此種操作提供了原子操作的同步類 Atomic,使用 AtomicInteger 做自增運算的性能是 ReentantLock 的好幾倍。
上面我們都是使用底層的方式實現線程間的通信的,但在實際的開發中,我們應該盡量遠離底層結構,使用封裝好的 API,例如 J.U.C 包(java.util.concurrent,又稱並發包)下的工具類 CountDownLath、CyclicBarrier、Semaphore,來實現線程通信,協調線程執行。
CountDownLatch 能夠實現線程之間的等待,CountDownLatch 用於某一個線程等待若干個其他線程執行完任務之後,它才開始執行。
CountDownLatch 類只提供了一個構造器:
CountDownLatch 類中常用的 3 個方法:
運行結果:
CyclicBarrier 字面意思循環柵欄,通過它可以讓一組線程等待至某個狀態之後再全部同時執行。當所有等待線程都被釋放以後,CyclicBarrier 可以被重復使用,所以有循環之意。
相比 CountDownLatch,CyclicBarrier 可以被循環使用,而且如果遇到線程中斷等情況時,可以利用 reset() 方法,重置計數器,CyclicBarrier 會比 CountDownLatch 更加靈活。
CyclicBarrier 提供 2 個構造器:
上面的方法中,參數 parties 指讓多少個線程或者任務等待至 barrier 狀態;參數 barrierAction 為當這些線程都達到 barrier 狀態時會執行的內容。
CyclicBarrier 中最重要的方法 await 方法,它有 2 個重載版本。下面方法用來掛起當前線程,直至所有線程都到達 barrier 狀態再同時執行後續任務。
而下面的方法則是讓這些線程等待至一定的時間,如果還有線程沒有到達 barrier 狀態就直接讓到達 barrier 的線程執行任務。
運行結果:
CyclicBarrier 用於一組線程互相等待至某個狀態,然後這一組線程再同時執行,CountDownLatch 是不能重用的,而 CyclicBarrier 可以重用。
Semaphore 類是一個計數信號量,它可以設定一個閾值,多個線程競爭獲取許可信號,執行完任務後歸還,超過閾值後,線程申請許可信號時將會被阻塞。Semaphore 可以用來 構建對象池,資源池,比如資料庫連接池。
假如在伺服器上運行著若干個客戶端請求的線程。這些線程需要連接到同一資料庫,但任一時刻只能獲得一定數目的資料庫連接。要怎樣才能夠有效地將這些固定數目的資料庫連接分配給大量的線程呢?
給方法加同步鎖,保證同一時刻只能有一個線程去調用此方法,其他所有線程排隊等待,但若有 10 個資料庫連接,也只有一個能被使用,效率太低。另外一種方法,使用信號量,讓信號量許可與資料庫可用連接數為相同數量,10 個資料庫連接都能被使用,大大提高性能。
上面三個工具類是 J.U.C 包的核心類,J.U.C 包的全景圖就比較復雜了:
J.U.C 包(java.util.concurrent)中的高層類(Lock、同步器、阻塞隊列、Executor、並發容器)依賴基礎類(AQS、非阻塞數據結構、原子變數類),而基礎類是通過 CAS 和 volatile 來實現的。我們盡量使用頂層的類,避免使用基礎類 CAS 和 volatile 來協調線程的執行。J.U.C 包其他的內容,在其他的篇章會有相應的講解。
Future 是一種非同步執行的設計模式,類似 ajax 非同步請求,不需要同步等待返回結果,可繼續執行代碼。使 Runnable(無返回值不支持上報異常)或 Callable(有返回值支持上報異常)均可開啟線程執行任務。但是如果需要非同步獲取線程的返回結果,就需要通過 Future 來實現了。
Future 是位於 java.util.concurrent 包下的一個介面,Future 介面封裝了取消任務,獲取任務結果的方法。
在 Java 中,一般是通過繼承 Thread 類或者實現 Runnable 介面來創建多線程, Runnable 介面不能返回結果,JDK 1.5 之後,Java 提供了 Callable 介面來封裝子任務,Callable 介面可以獲取返回結果。我們使用線程池提交 Callable 介面任務,將返回 Future 介面添加進 ArrayList 數組,最後遍歷 FutureList,實現非同步獲取返回值。
運行結果:
上面就是非同步線程執行的調用過程,實際開發中用得更多的是使用現成的非同步框架來實現非同步編程,如 RxJava,有興趣的可以繼續去了解,通常非同步框架都是結合遠程 HTTP 調用 Retrofit 框架來使用的,兩者結合起來用,可以避免調用遠程介面時,花費過多的時間在等待介面返回上。
線程封閉是通過本地線程 ThreadLocal 來實現的,ThreadLocal 是線程局部變數(local vari able),它為每個線程都提供一個變數值的副本,每個線程對該變數副本的修改相互不影響。
在 JVM 虛擬機中,堆內存用於存儲共享的數據(實例對象),也就是主內存。Thread Local .set()、ThreadLocal.get() 方法直接在本地內存(工作內存)中寫和讀共享變數的副本,而不需要同步數據,不用像 synchronized 那樣保證數據可見性,修改主內存數據後還要同步更新到工作內存。
Myabatis、hibernate 是通過 threadlocal 來存儲 session 的,每一個線程都維護著一個 session,對線程獨享的資源操作很方便,也避免了線程阻塞。
ThreadLocal 類位於 Thread 線程類內部,我們分析下它的源碼:
ThreadLocal 和 Synchonized 都用於解決多線程並發訪問的問題,訪問多線程共享的資源時,Synchronized 同步機制採用了以時間換空間的方式,提供一份變數讓多個線程排隊訪問,而 ThreadLocal 採用了以空間換時間的方式,提供每個線程一個變數,實現數據隔離。
ThreadLocal 可用於資料庫連接 Connection 對象的隔離,使得每個請求線程都可以復用連接而又相互不影響。
在 Java 裡面,存在強引用、弱引用、軟引用、虛引用。我們主要來了解下強引用和弱引用:
上面 a、b 對實例 A、B 都是強引用
而上面這種情況就不一樣了,即使 b 被置為 null,但是 c 仍然持有對 C 對象實例的引用,而間接的保持著對 b 的強引用,所以 GC 不會回收分配給 b 的空間,導致 b 無法回收也沒有被使用,造成了內存泄漏。這時可以通過 c = null; 來使得 c 被回收,但也可以通過弱引用來達到同樣目的:
從源碼中可以看出 Entry 里的 key 對 ThreadLocal 實例是弱引用:
Entry 里的 key 對 ThreadLocal 實例是弱引用,將 key 值置為 null,堆中的 ThreadLocal 實例是可以被垃圾收集器(GC)回收的。但是 value 卻存在一條從 Current Thread 過來的強引用鏈,只有當當前線程 Current Thread 銷毀時,value 才能被回收。在 threadLocal 被設為 null 以及線程結束之前,Entry 的鍵值對都不會被回收,出現內存泄漏。為了避免泄漏,在 ThreadLocalMap 中的 set/get Entry 方法里,會對 key 為 null 的情況進行判斷,如果為 null 的話,就會對 value 置為 null。也可以通過 ThreadLocal 的 remove 方法(類似加鎖和解鎖,最後 remove 一下,解鎖對象的引用)直接清除,釋放內存空間。
總結來說,利用 ThreadLocal 來訪問共享數據時,JVM 通過設置 ThreadLocalMap 的 Key 為弱引用,來避免內存泄露,同時通過調用 remove、get、set 方法的時候,回收弱引用(Key 為 null 的 Entry)。當使用 static ThreadLocal 的時候(如上面的 Spring 多數據源),static 變數在類未載入的時候,它就已經載入,當線程結束的時候,static 變數不一定會被回收,比起普通成員變數使用的時候才載入,static 的生命周期變長了,若沒有及時回收,容易產生內存泄漏。
使用線程池,可以重用存在的線程,減少對象創建、消亡的開銷,可控制最大並發線程數,避免資源競爭過度,還能實現線程定時執行、單線程執行、固定線程數執行等功能。
Java 把線程的調用封裝成了一個 Executor 介面,Executor 介面中定義了一個 execute 方法,用來提交線程的執行。Executor 介面的子介面是 ExecutorService,負責管理線程的執行。通過 Executors 類的靜態方法可以初始化
ExecutorService 線程池。Executors 類的靜態方法可創建不同類型的線程池:
但是,不建議使用 Executors 去創建線程池,而是通過 ThreadPoolExecutor 的方式,明確給出線程池的參數去創建,規避資源耗盡的風險。
如果使用 Executors 去創建線程池:
最佳的實踐是通過 ThreadPoolExecutor 手動地去創建線程池,選取合適的隊列存儲任務,並指定線程池線程大小。通過線程池實現類 ThreadPoolExecutor 可構造出線程池的,構造函數有下面幾個重要的參數:
參數 1:corePoolSize
線程池核心線程數。
參數 2:workQueue
阻塞隊列,用於保存執行任務的線程,有 4 種阻塞隊列可選:
參數 3:maximunPoolSize
線程池最大線程數。如果阻塞隊列滿了(有界的阻塞隊列),來了一個新的任務,若線程池當前線程數小於最大線程數,則創建新的線程執行任務,否則交給飽和策略處理。如果是無界隊列就不存在這種情況,任務都在無界隊列里存儲著。
參數 4:RejectedExecutionHandler
拒絕策略,當隊列滿了,而且線程達到了最大線程數後,對新任務採取的處理策略。
有 4 種策略可選:
最後,還可以自定義處理策略。
參數 5:ThreadFactory
創建線程的工廠。
參數 6:keeyAliveTime
線程沒有任務執行時最多保持多久時間終止。當線程池中的線程數大於 corePoolSize 時,線程池中所有線程中的某一個線程的空閑時間若達到 keepAliveTime,則會終止,直到線程池中的線程數不超過 corePoolSize。但如果調用了 allowCoreThread TimeOut(boolean value) 方法,線程池中的線程數就算不超過 corePoolSize,keepAlive Time 參數也會起作用,直到線程池中的線程數量變為 0。
參數 7:TimeUnit
配合第 6 個參數使用,表示存活時間的時間單位最佳的實踐是通過 ThreadPoolExecutor 手動地去創建線程池,選取合適的隊列存儲任務,並指定線程池線程大小。
運行結果:
線程池創建線程時,會將線程封裝成工作線程 Worker,Worker 在執行完任務後,還會不斷的去獲取隊列里的任務來執行。Worker 的加鎖解鎖機制是繼承 AQS 實現的。
我們來看下 Worker 線程的運行過程:
總結來說,如果當前運行的線程數小於 corePoolSize 線程數,則獲取全局鎖,然後創建新的線程來執行任務如果運行的線程數大於等於 corePoolSize 線程數,則將任務加入阻塞隊列 BlockingQueue 如果阻塞隊列已滿,無法將任務加入 BlockingQueue,則獲取全局所,再創建新的線程來執行任務
如果新創建線程後使得線程數超過了 maximumPoolSize 線程數,則調用 Rejected ExecutionHandler.rejectedExecution() 方法根據對應的拒絕策略處理任務。
CPU 密集型任務,線程執行任務佔用 CPU 時間會比較長,應該配置相對少的線程數,避免過度爭搶資源,可配置 N 個 CPU+1 個線程的線程池;但 IO 密集型任務則由於需要等待 IO 操作,線程經常處於等待狀態,應該配置相對多的線程如 2*N 個 CPU 個線程,A 線程阻塞後,B 線程能馬上執行,線程多競爭激烈,能飽和的執行任務。線程提交 SQL 後等待資料庫返回結果時間較長的情況,CPU 空閑會較多,線程數應設置大些,讓更多線程爭取 CPU 的調度。